Rework the dashboard into two looks over one behaviour

A dashboard is a wall panel somebody hangs in their own hallway, so it
now wears what they choose: a look, and a palette of their own colours.

Two complete component sets live under `Dashboard/ui/` — `glass`
(translucent panes over a slowly moving ground) and `material` (Material
3 tonal cards) — behind one prop contract. Every control's state,
keyboard and `aria-` live in `ui/core` and are shared, so the two sets
are the same dashboard drawn twice rather than two products: a set only
decides what a control looks like while doing it.

Four settings join the channel, each drivable by a flow like any other:
`look`, `palette`, `background` and `touch`. A palette is an ordered list
of hex colours — background, surface, primary, accent, text, then more
chart colours — pasted from a coolors.co link or typed, written onto the
canvas as the token variables everything already reads. Trailing roles
are derived, so three colours are a whole dashboard, and derived text is
held to AA rather than trusted (`theme.check.ts` measures it). A palette
also decides light or dark, since its first colour is the ground.

Widgets are measured against their own tile with container queries rather
than against the viewport, animate through `motion`, and can be drawn
without their title. The three reworks:

- a bar draws a row per reading, up to eight, each in the dashboard's own
  data colours and each able to carry its own scale — replacing readings
  nested in one fill, which could only ever share one colour and stop at
  three. Documents written the old way are read as rows.
- a chart's range picker moved to a column down its right-hand edge, which
  gives the plot back a whole row of a short tile.
- the colour wheel became a disc: hue is the angle and saturation the
  distance from the middle, so a colour is one gesture rather than three,
  with brightness on a slider beside it.

`index.css` and `lib/motion.ts` are untouched — the dashboard overrides
token *values* on its canvas, never the blocks the two repos share.
This commit is contained in:
2026-08-23 21:52:14 +02:00
parent d4c5af4d5a
commit 0b5ce4fcbb
52 changed files with 5517 additions and 1568 deletions
+63 -9
View File
@@ -28,7 +28,7 @@ and dragging is off. Picking a widget and editing its settings still works.
| **Value** | anything | a formatted reading with a unit and a precision |
| **Gauge** | `float`, `int` | min, max, unit |
| **Chart** | `float`, `int` | up to five series; see *Two kinds of chart* below |
| **Bar** | `float`, `int` | a reading, optionally nesting up to three inside it |
| **Bar** | `float`, `int` | up to eight readings, a row each, with a scale per row |
| **Icon** | numbers, booleans, weather strings | maps a value onto a glyph |
| **Text** | — | markdown you write; a label, a note, an instruction |
| **Agenda** | `list` | upcoming items, e.g. from a calendar connector |
@@ -36,6 +36,11 @@ and dragging is off. Picking a widget and editing its settings still works.
| **Notification** | `record` | title, body and severity — what an alert channel writes |
| **Clock** | — | the time, in a size a wall can read |
Every widget carries a **title**, and **Show title** decides whether the panel
draws it. Turned off, the tile is just the reading — which is what a row of
gauges under one heading wants. The title is still the widget's name: what a
screen reader calls its controls, and what a published value is labelled with.
### Input
| Widget | Publishes | Notes |
@@ -88,17 +93,21 @@ ignored rather than two charts overwriting each other's picture.
## Dashboard settings
Most of what a dashboard carries is a widget: a tile bound to a message. Two
things are not, because they belong to the whole surface rather than to any
tile on it — and a screen bolted to a wall has nobody standing at it to set
them.
Most of what a dashboard carries is a widget: a tile bound to a message. A
handful of things are not, because they belong to the whole surface rather than
to any tile on it — and a screen bolted to a wall has nobody standing at it to
set them.
| Setting | Is | Driven by |
|---|---|---|
| **Look** | `Material` or `Glass` | a `str` message |
| **Theme** | `System`, `Light` or `Dark` | a `str` message |
| **Palette** | the dashboard's colours, in order | a `list` message |
| **Background** | the URL of an image | a `str` message |
| **Touch** | touch friendly on or off | a `bool` message |
| **Lock** | read-only on or off | a `bool` message |
Both work the same way, and both halves are optional:
They all work the same way, and both halves are optional:
- **Just a value.** Set Theme to `Dark` and that dashboard is dark wherever it
is shown, whatever the device or the browser prefers. This costs no flow at
@@ -115,9 +124,54 @@ nothing further.
There is no schedule field, on purpose. **A schedule is a node publishing to
the bound message**: an `inject` with a cron expression, feeding a `change`
node that maps the hour onto `"dark"` or `"light"`, is the whole of "dark after
sunset" — and the same channel then serves anything else you want to drive,
including locking a panel down remotely.
node that maps the hour onto a palette, is the whole of "warmer after sunset" —
and the same channel then serves anything else you want to drive, including
locking a panel down remotely.
### Look
**Material** lays flat, tonal cards on a plain ground. **Glass** floats
translucent tiles over a soft, slowly moving one. They are two complete sets of
components, not two stylesheets — but they are the same dashboard: every widget,
every control and every keystroke behaves identically, so switching look never
changes what a panel can do.
### Palette
A palette is an ordered list of colours, and **position is the role**:
| # | Role |
|---|---|
| 1 | the ground the dashboard sits on |
| 2 | the surface a widget is |
| 3 | the primary — fills, active controls, the first chart line |
| 4 | the accent — the second chart line |
| 5 | text |
| 6+ | further chart colours |
Paste a [coolors.co](https://coolors.co) link (or a list of hex colours) and the
whole dashboard is recoloured — every widget, the rail and the charts. Leave the
later roles off and they are worked out from the ones you gave, so **three
colours are a whole dashboard**. **Rotate** turns the list when the roles landed
in the wrong order. Text that could not be read on a surface is replaced with
black or white there, so no palette can produce a line nobody can see.
While a palette is set, **Theme is idle**: the first colour is the ground, so
whether the panel reads light or dark is already decided by the palette itself.
Fault and success keep their own colours in every palette — a failure must never
be paintable as a reading.
### Background
An image drawn under the widgets, covering the canvas. It replaces the ground
the Glass look brings with it. Bound to a message, a flow decides the picture.
### Touch
Bigger controls, and nothing that only happens on hover. A phone gets this
anyway, from its own width; a wall panel has no way to say so for itself.
### Lock
Lock is a read-only *surface*, not a permission. The controls stay visible,
stop publishing and read as disabled, and the panel says **Read-only** in the