Say when a widget falls off the canvas, and offer to pack it back

Shrinking a dashboard's canvas silently clipped whatever now fell past the
bottom edge: `maxRows` constrains a drag and nothing else, so a stored placement
is corrected against the column count alone. Nothing warned, and nothing offered
a way out.

The remedy is a notice rather than a reflow, because the canvas height is
written on every keystroke — typing 400 passes through 4 and 40, and anything
that moved widgets would flatten the arrangement while the number was still
being typed. The notice carries the reflow as its one button, and packing is
sideways because the grid already compacts vertically: nothing below the canvas
has room above it.

Dropping a widget also selected it, which opened its panel, which rescaled the
canvas under the pointer. The drag handle was simply missing from the selector
that already exempts the resize handle — which is why resizing never had this
problem.

Also: an icon rule's caption could only be set through the API, and the panel
rail drew two letters where a dashboard can now carry a lucide icon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uq8mtNb97A7praJLyeEYgs
This commit is contained in:
2026-08-21 14:33:12 +02:00
co-authored by Claude Opus 5
parent c3ea884d72
commit 06f84e18ae
4 changed files with 250 additions and 8 deletions
+38 -1
View File
@@ -730,7 +730,7 @@ export function WidgetPanel({
<div
// Position is the only identity a rule row has.
key={`rule-${index}`}
className="flex items-end gap-1.5"
className="flex flex-wrap items-end gap-1.5"
>
<Input
className="w-20"
@@ -803,6 +803,21 @@ export function WidgetPanel({
>
<X />
</Button>
<Input
className="basis-full"
value={rule.label ?? ""}
placeholder="Caption under the glyph"
aria-label="Rule label"
onChange={(event) =>
setRules(
rules.map((other, at) =>
at === index
? { ...other, label: event.target.value }
: other,
),
)
}
/>
</div>
))}
<Button
@@ -928,6 +943,28 @@ export function DashboardPanel({
}
>
<div className="grid gap-5 p-4">
<div className="grid gap-2">
<span className={PANEL_SECTION}>Rail icon</span>
<Select
value={str(dashboard.icon)}
onValueChange={(icon) => onChange({ icon })}
>
<SelectTrigger data-testid="dashboard-icon">
<SelectValue placeholder="Two letters of the title" />
</SelectTrigger>
<SelectContent>
{ICON_NAMES.map((name) => (
<SelectItem key={name} value={name}>
{name}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Drawn on the rail when a panel carries more than one dashboard.
</p>
</div>
<div className="grid gap-2">
<span className={PANEL_SECTION}>Grid</span>
<Select