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
+74 -4
View File
@@ -27,7 +27,13 @@ import {
SelectValue,
} from "@/components/ui/select"
import { MAX_SERIES } from "./ChartWidget"
import { COLUMN_CHOICES, columnsOf, type Dashboard } from "./DashboardView"
import {
CANVAS_PRESETS,
COLUMN_CHOICES,
canvasOf,
columnsOf,
type Dashboard,
} from "./DashboardView"
import { messageCatalogQueryOptions } from "./queries"
import {
acceptsDtype,
@@ -314,12 +320,16 @@ export function WidgetPanel({
)
}
/** Presets are matched on their size, so a custom one simply matches none. */
const sizeKey = (size: { width: number; height: number }) =>
`${size.width}x${size.height}`
/**
* The dashboard's own settings, in the panel its widgets use.
*
* The grid size is the one that matters: a wall panel is a fixed width, and
* twelve columns on a seven-inch screen is a different dashboard than twelve
* on a television.
* The canvas is the one that matters: a wall panel is a fixed size, and twelve
* columns on a seven-inch screen is a different dashboard than twelve on a
* television.
*/
export function DashboardPanel({
open,
@@ -337,6 +347,7 @@ export function DashboardPanel({
onClose: () => void
}) {
const [confirmOpen, setConfirmOpen] = useState(false)
const canvas = canvasOf(dashboard)
return (
<>
@@ -390,6 +401,65 @@ export function DashboardPanel({
</p>
</div>
<div className="grid gap-2">
<span className={PANEL_SECTION}>Canvas</span>
<Select
value={
CANVAS_PRESETS.some(
(preset) => sizeKey(preset) === sizeKey(canvas),
)
? sizeKey(canvas)
: ""
}
onValueChange={(value) => {
const preset = CANVAS_PRESETS.find(
(candidate) => sizeKey(candidate) === value,
)
if (preset)
onChange({
canvas_width: preset.width,
canvas_height: preset.height,
})
}}
>
<SelectTrigger data-testid="dashboard-canvas-size">
<SelectValue placeholder="Custom" />
</SelectTrigger>
<SelectContent>
{CANVAS_PRESETS.map((preset) => (
<SelectItem key={sizeKey(preset)} value={sizeKey(preset)}>
{preset.label} {preset.width}×{preset.height}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="flex gap-2">
<Input
type="number"
aria-label="Canvas width"
data-testid="canvas-width"
value={canvas.width}
onChange={(event) =>
onChange({ canvas_width: Number(event.target.value) || 0 })
}
/>
<Input
type="number"
aria-label="Canvas height"
data-testid="canvas-height"
value={canvas.height}
onChange={(event) =>
onChange({ canvas_height: Number(event.target.value) || 0 })
}
/>
</div>
<p className="text-sm text-muted-foreground">
The panel this is drawn for, in pixels. Editing and viewing both
scale that surface to fit, so the arrangement is the same
everywhere.
</p>
</div>
<div className="grid gap-2">
<span className={PANEL_SECTION}>Contents</span>
<p className="text-sm text-muted-foreground">