Files
app/frontend/src/components/Dashboard/dashboard.css
T
stroblme 0b5ce4fcbb 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.
2026-08-23 21:52:14 +02:00

104 lines
3.8 KiB
CSS

/*
* Dashboard surface, routed through the design tokens.
*
* Scoped like `Flow/flow.css`: the grid maths and the react-grid-layout
* overrides live beside the components that use them rather than in index.css,
* so nothing here touches the byte-identical token blocks.
*/
/* The placement grid, as plain CSS. One dot per cell corner: the surface hands
in the pitch its widgets actually snap to (`--dot-x` / `--dot-y`), and the
negative offset puts a dot centre on the grid's own origin. */
.dot-canvas {
background-image: radial-gradient(
circle at 1px 1px,
color-mix(in srgb, var(--muted-foreground) 30%, transparent) 1.5px,
transparent 0
);
background-size: var(--dot-x, 24px) var(--dot-y, 24px);
background-position: -1px -1px;
}
/*
* View mode's grid. Column count is per dashboard (`--widget-cols`), so a wall
* panel can be matched to its own width. It does not reflow: the grid lives on
* a canvas of the panel's own pixel size, which is scaled to the viewport.
* A phone gets `.widget-stacked` below instead, which is a different surface
* rather than a narrower version of this one.
*/
.widget-grid {
display: grid;
gap: 0.75rem;
/* The row height follows the canvas, handed in beside the column count;
5rem is what a 1920x1080 panel at 12 columns derives. */
grid-auto-rows: var(--row-height, 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 {
grid-column: span var(--w);
grid-row: span var(--h);
min-width: 0;
}
/* Only once something has actually been placed; an untouched dashboard has
every widget at 0,0 and would pile up on one cell. */
.widget-grid[data-placed] .widget-cell {
grid-column: var(--x) / span var(--w);
grid-row: var(--y) / span var(--h);
}
/*
* One column, in reading order, at the viewport's own width — what a phone
* gets instead of a wall panel shrunk to 18%. Arranging is not a phone
* feature, so the stored x/w are dropped and only the height a widget asked
* for survives: a chart still needs its room, a stat still does not.
* See DESIGN-GUIDELINES.md → Responsive.
*/
.widget-grid.widget-stacked,
.widget-grid.widget-stacked[data-placed] {
display: flex;
flex-direction: column;
}
.widget-grid.widget-stacked .widget-cell {
grid-column: auto;
grid-row: auto;
height: calc(var(--h) * var(--row-height, 5rem) + (var(--h) - 1) * 0.75rem);
}
/*
* Motion. A selection indicator moving is emphasized (Material).
* `<MotionConfig reducedMotion="user">` only covers `motion/react`, so CSS
* asks for itself. The dashboard's own widgets are drawn by the component
* sets and animate through `motion`; what is left here is the app-side
* segmented shape, which the flow screens and the dashboard editor share.
*/
@media (prefers-reduced-motion: no-preference) {
/* 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);
}
}
/* react-grid-layout ships a red placeholder and a black handle glyph. */
.react-grid-item.react-grid-placeholder {
background: var(--primary);
border-radius: var(--radius-lg);
opacity: 0.12;
}
.react-grid-item > .react-resizable-handle::after {
border-right-color: var(--muted-foreground);
border-bottom-color: var(--muted-foreground);
}