From 14e651d5eb7fee3f8a63575c44dc11cebb71755a Mon Sep 17 00:00:00 2001 From: stroblme Date: Thu, 15 Oct 2026 16:05:23 +0200 Subject: [PATCH] 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. --- docs/interface/dashboards.md | 4 ++ .../components/Dashboard/DashboardEditor.tsx | 7 ++- .../components/Dashboard/DashboardView.tsx | 53 +++++++++++++++++-- .../src/components/Dashboard/dashboard.css | 15 ++++++ 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/docs/interface/dashboards.md b/docs/interface/dashboards.md index da58efe..775a847 100644 --- a/docs/interface/dashboards.md +++ b/docs/interface/dashboards.md @@ -16,6 +16,10 @@ Edits stay in a draft; paired screens show the published version. On phones, tiles stack in one column and dragging is disabled, but settings remain editable. In edit mode, ⌘Z / Ctrl-Z undoes an edit and ⌘⇧Z / Ctrl-Shift-Z redoes it. +A widget can end up below the canvas, for example after the canvas was made +smaller. In edit mode the canvas then grows to reach it, under a dashed line +where the panel ends. Scroll down and drag it back, or use **Reflow**. + Controls can publish real values while you edit, so choose their targets with care. Your new Switch may already be flipping something! diff --git a/frontend/src/components/Dashboard/DashboardEditor.tsx b/frontend/src/components/Dashboard/DashboardEditor.tsx index ed4edae..8a3acb4 100644 --- a/frontend/src/components/Dashboard/DashboardEditor.tsx +++ b/frontend/src/components/Dashboard/DashboardEditor.tsx @@ -561,6 +561,11 @@ export function DashboardEditor({ // now falls past its bottom edge drawn under the clip. A round face has no // grid to fall off. const clipped = round ? [] : layout.filter((item) => item.y + item.h > rows) + // Editing, the canvas grows to reach them, so they can be dragged back. + const reach = + edit && clipped.length + ? Math.max(...clipped.map((item) => item.y + item.h)) + : undefined const shown = Math.max( 0, widgets.findIndex((widget) => widget.id === (selected ?? face)), @@ -669,7 +674,7 @@ export function DashboardEditor({ ) : null} ) : ( - + {(scale) => !edit ? ( diff --git a/frontend/src/components/Dashboard/DashboardView.tsx b/frontend/src/components/Dashboard/DashboardView.tsx index 3c15089..4495289 100644 --- a/frontend/src/components/Dashboard/DashboardView.tsx +++ b/frontend/src/components/Dashboard/DashboardView.tsx @@ -176,6 +176,7 @@ export function CanvasSurface({ dots, rail, touch, + reach, children, }: { dashboard: Dashboard @@ -185,6 +186,13 @@ export function CanvasSurface({ rail?: React.ReactNode /** Whether the panel this canvas hangs on is touched rather than pointed at. */ touch?: boolean + /** + * How many grid rows the arrangement reaches down to, while it is being + * arranged. A canvas they fall off grows to reach them, with its own edge + * still drawn, so they can be scrolled to and moved back. The scale stays + * the panel's. + */ + reach?: number children: (scale: number) => React.ReactNode }) { const ref = useRef(null) @@ -208,6 +216,17 @@ export function CanvasSurface({ const root = useCanvasRoot(dashboard, touch) const round = isRound(dashboard) const area = areaOf(dashboard, Boolean(rail)) + // How far past the bottom edge the rows reached go, margin included. + const beyond = reach + ? Math.max( + 0, + area.top + + reach * rowPitchOf(dashboard, Boolean(rail)) - + GRID_GAP + + GRID_INSET - + height, + ) + : 0 // The arrangement, held across the renders that are not about it. This // component reads the dashboard's own settings, so a bound one wakes it on // every message the flow sends — and calling a render prop again rebuilds @@ -219,7 +238,17 @@ export function CanvasSurface({ ) return ( -
+
{/* Measured first: a guessed scale would place the whole panel once and then move it. */} {scale > 0 ? ( @@ -238,7 +267,7 @@ export function CanvasSurface({ { ...root.style, width, - height, + height: height + beyond, // Round, nothing outside the circle is drawn. clipPath: round ? "circle(50%)" : undefined, left: (box.width - width * scale) / 2, @@ -255,14 +284,30 @@ export function CanvasSurface({ {/* Full bleed, and under everything: a background covers the panel, margin included. */} - {rail} + {beyond ? ( +
+ ) : null} + {/* Centred on the panel, not on a canvas grown past it. */} + {rail ? ( +
+ {rail} +
+ ) : null}
{/* Over the ground rather than under it: the dots are what is diff --git a/frontend/src/components/Dashboard/dashboard.css b/frontend/src/components/Dashboard/dashboard.css index e4496d0..2676c90 100644 --- a/frontend/src/components/Dashboard/dashboard.css +++ b/frontend/src/components/Dashboard/dashboard.css @@ -19,6 +19,21 @@ 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