Stack a bar's readings, and stop widgets taking the phone sideways

A bar drew its nested reading on top of the outer one in --chart-5, which
measures 2.53:1 against --primary and lost the 3:1 guideline for non-text. The
readings now partition the fill end to end, up to three of them, in a token of
their own: --primary-nested, the primary hue a few steps deeper, 3.14:1 light
and 3.12:1 dark. It cannot also clear 3:1 against --muted — in dark those two
are 5.82:1 apart and a colour 3:1 from both would need a 9:1 gap — so a segment
is drawn inside a gutter of outer fill rather than ever bordering the track,
which is what separates neighbours too, and what caps the count at three. A
nested value larger than its outer used to spill onto the track; it is clamped.
`inner` still reads as a single binding, so no dashboard needs migrating.

On a phone, .widget-grid took its width from the widest thing any widget held —
a truncating flex item still offers its whole unwrapped line as a min-content
contribution — and a handful of widgets had no floor of their own: the uPlot
legend is a table, a fieldset carries min-inline-size: min-content from the UA
sheet, and buttons are whitespace-nowrap. Each is capped now. A widget's body
scrolls rather than clipping, so long text stops painting over the title.

Gauges and bars move between readings instead of jumping, and a segmented
control slides one thumb rather than recolouring cells. The gauge arc is drawn
whole and revealed by its dash, because `d` cannot be transitioned.

UplotChart pushed new readings only when the point count changed, so once a
rolling window was full a refetch left the old values on screen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
2026-08-21 10:11:44 +02:00
co-authored by Claude Opus 5
parent d375ea97cc
commit fe1ba46d4e
10 changed files with 589 additions and 85 deletions
@@ -32,6 +32,12 @@
gap: 0.75rem;
grid-auto-rows: 5rem;
grid-template-columns: repeat(var(--widget-cols), minmax(0, 1fr));
/* Stacked, this is a flex item in an `auto` track, and its automatic minimum
size is the widest thing any widget holds — one long message name would
size the column and take the page sideways with it. The fixed-pixel canvas
the non-stacked path sits on already uses `minmax(0, 1fr)`, so this only
ever matters on a phone. See DESIGN-GUIDELINES.md -> Responsive. */
min-width: 0;
}
.widget-cell {
@@ -66,6 +72,27 @@
height: calc(var(--h) * 5rem + (var(--h) - 1) * 0.75rem);
}
/*
* Motion. A value settling is a neutral state change; a selection indicator
* moving is emphasized (Material). `<MotionConfig reducedMotion="user">` only
* covers `motion/react`, so CSS asks for itself.
*/
@media (prefers-reduced-motion: no-preference) {
/* The arc is the full 240 degrees and the dash hides the rest of it, so the
reading changes by animating one number rather than re-pathing. */
.widget-gauge-arc {
transition: stroke-dashoffset var(--duration-base) var(--ease-standard);
}
/* One indicator that slides between segments, rather than a fill that jumps
from cell to cell. */
.widget-segment-thumb {
transition:
left var(--duration-base) var(--ease-emphasized),
width var(--duration-base) var(--ease-emphasized);
}
}
/*
* uPlot, routed through the tokens. Its own legend is the hover readout as
* well — the value each line carried at the cursor — so it is styled as chart
@@ -77,6 +104,12 @@
margin-top: 0.25rem;
}
/* Series labels default to the message name, which has no spaces to break at
— and a table cannot lay out below its min-content width. */
.u-legend th {
overflow-wrap: anywhere;
}
.u-legend .u-marker {
width: 0.5rem;
height: 0.5rem;