Files
app/frontend/src/components/Dashboard/dashboard.css
T
stroblme 10fe2fd760 A buttons widget: a grid of presses on one message
Several stateless instructions where a single button is one — six presets are
otherwise six tiles to place, six titles to read, and the message they share
repeated six times. Nothing is read back, as for a single button: what these
send is an instruction, and the last one sent is not a state to draw.

Auto-fit columns rather than a configured count: a tile is resized in the
editor and scaled again to whatever panel it hangs on, so how many fit is not
something the document can know.

The editor reads `cfg.buttons` raw rather than through `buttonsOf`, which
drops the blanks — a row being typed into is blank until the first keystroke.

Also carried along by the hooks: the generated SDK was stale (it had no
`RunsReadMetricNames`) and one pre-existing block in `test_panels.py` was
unformatted.
2026-09-04 08:44:16 +02:00

122 lines
4.3 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);
}
/*
* A grid of presses, inside one tile.
*
* Auto-fit rather than a configured column count: the tile is resized in the
* editor and scaled again to whatever panel it hangs on, so the number of
* columns that fits is not something the document can know. `1fr` rows so the
* presses share the tile's height however many rows they wrap into — a target
* on a wall panel is pressed without looking at it.
*/
.widget-buttons {
display: grid;
gap: 0.5rem;
grid-template-columns: repeat(auto-fit, minmax(5.5rem, 1fr));
grid-auto-rows: 1fr;
height: 100%;
min-height: 0;
}