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
138 lines
4.7 KiB
CSS
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);
|
|
}
|