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