Rework the dashboards onto the flow canvas

One shell for both editors. Flows and dashboards each get a searchable
overview under the padded shell, their editors move to the full-bleed
canvas, and the floating chrome is shared: a title bar that only says
what you are looking at, and a bottom dock carrying everything else —
the flow bar's status, settings and Publish moved down there, the
add-flow button moved to the overview.

Dashboards gain the rest of M4's visualization work:

- widgets are picked by clicking them, with the header as the drag
  handle so a slider still slides and a switch still flips while
  editing; settings moved into the flows' SidePanel
- react-grid-layout for drag and edge-resize, so the stored x/y finally
  mean something; a dashboard nobody arranged is shelf-packed once
- a per-dashboard grid size, so a panel can be matched to its screen
- the chart widget, drawn with uPlot: several messages on one axis, fed
  from the stored history plus the live socket tail, coloured from the
  new --chart-1..5 ramp
- /view/{name}: the URL a wall panel is pointed at — no sidebar, no
  footer, no editing, and no editor code, since routes are split
- a widget wired to a payload type it cannot carry, or wired to nothing
  at all, carries the same red dot a failing node does; the picker
  records the type it bound and WidgetDef refuses a mismatch on save

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 17:13:10 +02:00
co-authored by Claude Fable 5
parent 74bb956805
commit 0af09eedbe
24 changed files with 1799 additions and 1558 deletions
@@ -0,0 +1,102 @@
/*
* Dashboard surface, routed through the design tokens.
*
* Scoped like `Flow/flow.css`: the grid maths and the react-grid-layout
* overrides live beside the components that use them rather than in index.css,
* 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. */
.dot-canvas {
background-color: var(--background);
background-image: radial-gradient(
circle at 1px 1px,
color-mix(in srgb, var(--muted-foreground) 30%, transparent) 1.5px,
transparent 0
);
background-size: 24px 24px;
}
/*
* 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.
*/
.widget-grid {
display: grid;
gap: 0.75rem;
grid-auto-rows: 5rem;
grid-template-columns: minmax(0, 1fr);
}
.widget-cell {
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);
}
}
/*
* uPlot, routed through the tokens. Its own legend is the hover readout as
* well — the value each line carried at the cursor — so it is styled as chart
* furniture rather than replaced.
*/
.u-legend {
font-size: 0.75rem;
color: var(--muted-foreground);
margin-top: 0.25rem;
}
.u-legend .u-marker {
width: 0.5rem;
height: 0.5rem;
border-width: 2px;
border-radius: 9999px;
}
.u-legend .u-value {
font-variant-numeric: tabular-nums;
color: var(--foreground);
}
.u-legend .u-series.u-off {
opacity: 0.4;
}
.u-cursor-x,
.u-cursor-y {
border-color: color-mix(in srgb, var(--muted-foreground) 55%, transparent);
}
.u-select {
background: color-mix(in srgb, var(--primary) 12%, transparent);
}
/* react-grid-layout ships a red placeholder and a black handle glyph. */
.react-grid-item.react-grid-placeholder {
background: var(--primary);
border-radius: var(--radius-lg);
opacity: 0.12;
}
.react-grid-item > .react-resizable-handle::after {
border-right-color: var(--muted-foreground);
border-bottom-color: var(--muted-foreground);
}