The dashboard builder — a responsive, server-rendered grid of widgets that can be added from the catalog, reordered, resized, configured, and removed.
Phoenix-first, server-authoritative
The grid is plain HEEx + a CSS grid: each widget instance is anchored at its
placement's x/y cells (grid-column/-row: <line> / span <n>) on the active
breakpoint's column grid. It renders — and is fully readable — without any
JavaScript. The server owns the canonical layout (the JSONB layout list);
every mutation re-renders normally (no phx-update="ignore"), so
adding/removing/moving/resizing a widget is live.
There are two render modes, derived from the dashboard's type (fixed at
creation — config["type"] = "grid" | "pixel") via Dashboard.layout_mode/1
(legacy config["mode"] still maps):
- grid — explicit cell placement on a responsive column grid: each widget
is anchored at
x/ycells and spansw×h— place it ANYWHERE (gaps are fine), widgets never overlap. Drag by the grip (DashboardGridDrag, cell- snapped live preview), resize by the corner (snaps + grows until blocked). - free — an absolute pixel canvas: drag and resize anywhere, exact px,
no snapping. Widgets may overlap — deliberately, via the per-widget
bring-to-front / send-to-back controls (a
zkey in thepixelmap). Uses separatefx/fy/fw/fhpx keys so the two layouts never disturb each other. TheDashboardFreeFithook scales the whole canvas to fill the available width (fit-to-width, so the exact layout is preserved on any screen — it just shrinks on a phone); vertical overflow scrolls.
Widgets with a refresh_interval are re-queried by a host-driven :refresh_tick
loop.
Custom hooks (shipped via js_sources/0): DashboardGridDrag (grid cell
placement — the widget follows the drag cell-by-cell, never onto an occupied
spot), DashboardCatalogDrag (drag a widget type OUT of the catalog and drop
it on a grid cell or canvas point; a plain click still adds at the first free
spot), DashboardFreeDrag (free-canvas move via left/top) and
DashboardResize (corner grip .pk-resize-handle — px in free mode; cell-snap
clamped by neighbours in grid). The non-hook fallback is the Settings modal's
inputs (px size + X/Y position in free; cell size + Column/Row position in
grid), all server-driven. Every layout-tweak handler is
guarded so a hostile/malformed event can't crash or brick the builder.