Files
app/frontend/src/components/Dashboard/dashboard.css
T
stroblmeandClaude Opus 5.5 361b9d8e74 Grow the editor's canvas to reach widgets below its edge
A widget that fell off the panel, after the canvas was made smaller, was
drawn under the clip and could only be re-packed with Reflow. Editing, the
canvas now extends to the lowest widget and scrolls, with the panel's own
edge dashed and the area past it hatched, so the widget can be dragged
back. The scale stays the panel's, and view mode is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
2026-10-15 16:05:23 +02:00

137 lines
4.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;
}
/*
* Below the panel's bottom edge, while the editor has grown the canvas to
* reach widgets that fell off it: hatched, under a dashed line where the panel
* ends, so what it will not show reads as outside it.
*/
.canvas-beyond {
border-top: 3px dashed
color-mix(in srgb, var(--muted-foreground) 70%, transparent);
background: repeating-linear-gradient(
-45deg,
color-mix(in srgb, var(--muted-foreground) 14%, transparent) 0 3px,
transparent 3px 16px
);
}
/*
* 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;
}