Files
app/frontend/src/components/Dashboard/dashboard.css
T
stroblmeandClaude Opus 5 eb2e6bf6d2 Edit the whole dashboard, and let the grid follow its canvas
The editor only ever arranged the first section, so the demo's Home
dashboard lost 16 of its 25 widgets the moment it was edited. A page's
sections are now read as one arrangement — each pushed below the one
before it — and written back as one, which is the shape a dashboard was
already heading for: one dashboard, one canvas, and the panel rail for
the several-dashboards story. The page tabs that story made dead are
gone; PageDef/SectionDef stay in the schema and a page the editor does
not show round-trips untouched.

The row height derives from the canvas as the column width already did,
so the same arrangement is the same picture on a 7" panel as on a 4K
one. The uPlot rules move beside UplotChart, where a chart on Health is
styled without a dashboard having been visited first, and the bar's
readout travels on one property instead of jumping sides at 30%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
2026-08-22 12:22:37 +02:00

138 lines
4.7 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-color: var(--background);
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);
}
/*
* A publish in flight, drawn as a ring just inside the tile's own edge.
*
* An overlay rather than anything the widget owns: it takes no layout box and
* no clicks, so a control being used never resizes its tile or moves what sits
* around it. Full opacity at rest, so a panel that asks for no motion still
* gets the ring — the animation only breathes it.
*/
.widget-transmit {
position: absolute;
inset: 0;
pointer-events: none;
border-radius: var(--radius-lg);
box-shadow: inset 0 0 0 2px var(--primary);
}
/*
* 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) {
/* One beat per second, which reads as "on its way" from across a room
without becoming the loudest thing in a browser tab. */
.widget-transmit {
animation: widget-transmit var(--duration-pulse) var(--ease-standard)
infinite alternate;
}
@keyframes widget-transmit {
from {
opacity: 0.2;
}
}
/* 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);
}
}
/* 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);
}