/* * 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). `` 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); }