Give a dashboard the panel's own size, and put the dots on its grid

A dashboard now carries the canvas it is drawn for (canvas_width /
canvas_height, presets plus two numbers in the settings panel). Editor and
wall panel render that surface at its true pixel size and scale it to fit,
so a side panel opening changes only the scale — never the arrangement
being made. With the width and column count known the dot pitch is exact,
(width + gap) / columns by row height + gap, so a dot sits where every
widget corner snaps. The wall panel shows no dots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7LwYgJfpkbLCTeiAf8U4A
This commit is contained in:
2026-08-16 19:08:09 +02:00
co-authored by Claude Fable 5
parent fcd43e9ad9
commit c2321fe942
8 changed files with 308 additions and 79 deletions
+15 -22
View File
@@ -6,8 +6,9 @@
* so nothing here touches the byte-identical token blocks.
*/
/* The same dot grid the flow viewport paints, as plain CSS: a dashboard has no
zoom, so it needs none of React Flow's rescaling. */
/* 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(
@@ -15,42 +16,34 @@
color-mix(in srgb, var(--muted-foreground) 30%, transparent) 1.5px,
transparent 0
);
background-size: 24px 24px;
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. Below the large breakpoint the stored
* placement is meaningless — three columns cannot hold a twelve-column
* arrangement — so widgets stack full width and keep only their height.
* panel can be matched to its own width. There is no responsive fallback: the
* grid lives on a canvas of the panel's own pixel size, which is scaled to the
* viewport rather than reflowed into it.
*/
.widget-grid {
display: grid;
gap: 0.75rem;
grid-auto-rows: 5rem;
grid-template-columns: minmax(0, 1fr);
grid-template-columns: repeat(var(--widget-cols), minmax(0, 1fr));
}
.widget-cell {
grid-column: span var(--w);
grid-row: span var(--h);
min-width: 0;
}
@media (min-width: 64rem) {
.widget-grid {
grid-template-columns: repeat(var(--widget-cols), minmax(0, 1fr));
}
.widget-cell {
grid-column: span var(--w);
}
/* 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);
}
/* 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);
}
/*