Draw each dashboard on a round panel's track in its own look

The canvas states the look of the dashboard shown, so a neighbour beside it
and the one a turn leaves wore that look too. Each page now restates its own
look, theme and palette. The ring's paint moves from rules under
[data-look] to variables each look sets on its root, which stop at the
nearer root where a descendant rule reached into both.
This commit is contained in:
2026-10-15 16:00:10 +02:00
parent e5ecb1e313
commit 984da1706c
6 changed files with 107 additions and 86 deletions
+1 -1
View File
@@ -330,7 +330,7 @@ dashboard's look's.
| What | Moves | From → to | Timing | Input during it |
|---|---|---|---|---|
| Face paging (round) | the faces, side by side on a track | the track slides one face width S, the new face coming in from the side it lies on (the next from the right). A dot that skips faces brings its face in beside the one shown, so a turn is always one width | spring, from where the track is (a swipe's release speed carried on) | a new turn while one runs: the running one lands at once and the next starts |
| Dashboard paging (round panel) | the dashboard, and beside it the next one | slides S up (the next, from below) or down (the previous, from above), the next one drawn on the face it opens on; once landed, it is the dashboard shown, and its name pill shows for a moment. One opened by its address replaces the last at once | spring | further paging waits for the turn to land |
| Dashboard paging (round panel) | the dashboard, and beside it the next one | slides S up (the next, from below) or down (the previous, from above), the next one drawn on the face it opens on; once landed, it is the dashboard shown, and its name pill shows for a moment. Each one on the track wears its own look, theme and palette. The ground is the dashboard shown's and changes over as the turn begins, without a fade. One opened by its address replaces the last at once | spring | further paging waits for the turn to land |
| Page dot | the current dot and the last | muted at 0.5 opacity ↔ text colour at 1 | 200 ms, cubic-bezier(0.4, 0, 0.2, 1) | retargets |
| Dashboard name | the pill, on landing on another dashboard | opacity 0 → 1, held 1000 ms, 1 → 0 | in 200 ms, out 150 ms, cubic-bezier(0.4, 0, 0.2, 1) | — |
| Ring value set by a notch, a key or the engine | the value arc's end, the handle (slider, selector, colour) | from where it is drawn to the new fraction of the sweep | spring | each notch retargets from where it is, at the speed it has |
@@ -15,6 +15,7 @@ import type { WidgetDef } from "@/client"
import { slotsFor } from "@/components/Common/UplotChart"
import { fadeIn } from "@/lib/motion"
import { useShortcuts } from "@/lib/shortcuts"
import { cn } from "@/lib/utils"
import { useClock } from "./ClockWidget"
import type { Dashboard } from "./DashboardView"
import { useBoundValue, useBoundValues } from "./dataContext"
@@ -40,7 +41,7 @@ import {
text,
} from "./ui/core/config"
import { TESTID } from "./ui/core/contract"
import { useLook } from "./ui/core/look"
import { LookProvider, useCanvasRoot, useLook } from "./ui/core/look"
import { LOOK } from "./ui/core/motion"
import {
ANNOUNCE_MS,
@@ -334,12 +335,7 @@ export function RoundPanel({
return (
// Keyed by name, so a neighbour becomes the one shown, and back,
// without being drawn afresh: its own face memory and all.
<div
key={page.name}
className="absolute inset-0"
style={{ translate: `0 ${at * size}px` }}
inert={at !== 0}
>
<TrackPage key={page.name} dashboard={page} at={at} size={size}>
<PaletteProvider dashboard={page}>
{editing ? (
view
@@ -347,13 +343,52 @@ export function RoundPanel({
<LockedProvider dashboard={page}>{view}</LockedProvider>
)}
</PaletteProvider>
</div>
</TrackPage>
)
})}
</motion.div>
)
}
/**
* One dashboard on a round panel's track, in its own look.
*
* The canvas states the look, theme and palette of the dashboard shown, so a
* neighbour beside it, or the one a turn is leaving, would wear those too.
* Each page restates its own instead. The ground stays the canvas's, which
* changes over as soon as the turn begins.
*/
function TrackPage({
dashboard,
at,
size,
children,
}: {
dashboard: Dashboard
/** Its place on the track, in screens from the one shown. */
at: number
size: number
children: React.ReactNode
}) {
// Touch belongs to the screen, so it is the canvas's whichever page this is.
const { touch } = useLook()
const root = useCanvasRoot(dashboard, touch)
return (
<LookProvider dashboard={dashboard} touch={touch}>
<div
data-look={root["data-look"]}
data-touch={root["data-touch"]}
data-palette={root["data-palette"]}
className={cn("absolute inset-0 text-foreground", root.className)}
style={{ ...root.style, translate: `0 ${at * size}px` }}
inert={at !== 0}
>
{children}
</div>
</LookProvider>
)
}
/** A dashboard's icon and title: what a panel shows for a moment of a
* dashboard it has just landed on. Opaque, so the face under it does not
* read through the name. */
@@ -425,7 +425,7 @@
);
--dui-reading: var(--foreground);
--dui-reading-reached: var(--primary-foreground);
--dui-reading-on-handle: var(--foreground);
--dui-reading-on-handle: var(--dui-on-handle, var(--foreground));
}
/*
@@ -785,6 +785,47 @@
outline: none;
}
/*
* The ring's paint. Each look states it on its root as these variables rather
* than in rules under `[data-look]`: a round panel draws a dashboard beside
* one in another look, and a descendant rule would reach into both, where an
* inherited value stops at the nearer root. Reset on every root, so a look
* that leaves one out takes the fallback rather than the outer root's.
*/
:where([data-look]) {
--dui-ring-track: initial;
--dui-ring-glow: initial;
--dui-ring-handle: initial;
--dui-ring-handle-edge: initial;
--dui-ring-handle-edge-width: initial;
--dui-ring-handle-shadow: initial;
--dui-ring-swatch: initial;
--dui-ring-swatch-width: initial;
--dui-on-handle: initial;
}
.dui-ring-track {
stroke: var(--dui-ring-track, var(--muted));
}
.dui-ring-fill {
stroke: var(--primary);
filter: var(--dui-ring-glow, none);
}
.dui-ring-handle {
fill: var(--dui-ring-handle, var(--primary));
stroke: var(--dui-ring-handle-edge, none);
stroke-width: var(--dui-ring-handle-edge-width, 0);
filter: var(--dui-ring-handle-shadow, none);
}
/* A colour's handle is filled with the colour, so only its edge is the look's. */
.dui-ring-swatch {
stroke: var(--dui-ring-swatch, var(--card));
stroke-width: var(--dui-ring-swatch-width, 3px);
}
.dui-ring-hit {
pointer-events: stroke;
cursor: pointer;
@@ -21,6 +21,12 @@
[data-look="fluksio"] {
--fx-radius: var(--radius-lg);
--fx-field: var(--radius-md);
/* A round face's ring (`core.css`): the slider's handle, a ring of primary
around the page colour. */
--dui-ring-handle: var(--background);
--dui-ring-handle-edge: var(--primary);
--dui-ring-handle-edge-width: 2px;
--dui-ring-handle-shadow: drop-shadow(0 1px 2px rgb(0 0 0 / 0.2));
}
/* --- surfaces ------------------------------------------------------------ */
@@ -315,26 +321,3 @@
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent);
}
/* --- a round dashboard's ring -------------------------------------------- */
[data-look="fluksio"] .dui-ring-track {
stroke: var(--muted);
}
[data-look="fluksio"] .dui-ring-fill {
stroke: var(--primary);
}
/* The slider's handle: a ring of primary around the page colour. */
[data-look="fluksio"] .dui-ring-handle {
fill: var(--background);
stroke: var(--primary);
stroke-width: 2px;
filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.2));
}
[data-look="fluksio"] .dui-ring-swatch {
stroke: var(--card);
stroke-width: 3px;
}
@@ -29,6 +29,17 @@
/* A round face's press, which has no card behind it. See below. */
--gl-press: var(--gl-bg);
--gl-press-shadow: var(--gl-inset);
/* A round face's ring (`core.css`): lit, as the gauge's arc is, under a
white handle, which a reading's dot on it shows dark against. */
--dui-ring-track: var(--gl-track);
--dui-ring-glow: drop-shadow(0 0 4px var(--primary));
--dui-ring-handle: #fff;
--dui-ring-handle-edge: var(--gl-border);
--dui-ring-handle-edge-width: 1px;
--dui-ring-handle-shadow: drop-shadow(0 2px 8px rgb(0 0 0 / 0.35));
--dui-ring-swatch: rgb(255 255 255 / 0.85);
--dui-ring-swatch-width: 2px;
--dui-on-handle: rgb(0 0 0 / 0.75);
}
[data-look="glass"].light {
@@ -433,32 +444,3 @@
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* --- a round dashboard's ring -------------------------------------------- */
[data-look="glass"] .dui-ring-track {
stroke: var(--gl-track);
}
/* Lit, as the gauge's arc is. */
[data-look="glass"] .dui-ring-fill {
stroke: var(--primary);
filter: drop-shadow(0 0 4px var(--primary));
}
[data-look="glass"] .dui-ring-handle {
fill: #fff;
stroke: var(--gl-border);
stroke-width: 1px;
filter: drop-shadow(0 2px 8px rgb(0 0 0 / 0.35));
}
/* A reading's dot on the handle, which is white here in either theme. */
[data-look="glass"] :is(.dui-ring, .dui-slider) {
--dui-reading-on-handle: rgb(0 0 0 / 0.75);
}
[data-look="glass"] .dui-ring-swatch {
stroke: rgb(255 255 255 / 0.85);
stroke-width: 2px;
}
@@ -19,6 +19,10 @@
--m3-sc: color-mix(in oklab, var(--primary) 22%, var(--card));
--m3-outline: var(--border);
--m3-radius: 1rem;
/* A round face's ring (`core.css`): a tonal track under a primary handle,
which a reading's dot on it shows light against. */
--dui-ring-track: var(--m3-sc);
--dui-on-handle: var(--primary-foreground);
}
/* --- surfaces ----------------------------------------------------------- */
@@ -319,27 +323,3 @@
outline: 2px solid var(--primary);
outline-offset: 2px;
}
/* --- a round dashboard's ring -------------------------------------------- */
[data-look="material"] .dui-ring-track {
stroke: var(--m3-sc);
}
[data-look="material"] .dui-ring-fill {
stroke: var(--primary);
}
[data-look="material"] .dui-ring-handle {
fill: var(--primary);
}
/* A reading's dot on the handle, which is primary here. */
[data-look="material"] :is(.dui-ring, .dui-slider) {
--dui-reading-on-handle: var(--primary-foreground);
}
[data-look="material"] .dui-ring-swatch {
stroke: var(--card);
stroke-width: 3px;
}