The cells below form a small dataflow (freq, amp → wave → wavePlot, waveStats). The widget surface at the bottom composes the named pieces: rearrange them on the grid, use the + add cell dropdown / ✕ to change what's included, and watch the widget cell's source rewrite its include: and layout: literals (open its editor to see them change live).
The + add cell menu also lists templates — cell groups named template_*. This notebook ships a template_dial group (a hue slider + its colour swatch, template_dial_swatch); pick ⊕ dial to instantiate a fresh copy (dial1, dial2, …), each a working linked pair dropped onto the grid.
A responsive container that composes named cells as rearrangeable atoms on a snap-to-grid surface — a widget builder. Atoms are live dataflow elements: any HTML or value is adopted via Inspector, recomputes reactively, and the underlying cell source is editable in a floating editor (editor-5).
Grid container is a self-editing view, like editable-md: mutating the UI rewrites the defining cell's own source code. Both the include: list (which cells are on the surface) and the layout: literal (where they are) live in the cell that calls gridContainer — drag an atom or add a cell and the source updates, so export, diff and undo all see the widget as code.
import {gridContainer} from '@tomlarkworthy/grid-container'
widget = gridContainer(runtime, {
invalidation, // required for teardown
module, // module to render, default main
include: ["viewof x"], // cell names to show; omit to show all non-plumbing cells
layout: {}, // {frame: {h}, atoms: {name: {x,y,w,h}}} — grid units, rewritten in place
columns: 12, // column count: pitch = width/columns; atom x/y/w/h are grid units
portrait: 640, // width (px) below which the grid stacks into one column
showGrid, // draw the dot grid, default true
filter, // extra predicate (cell_name, variables, i, state) => boolean
persist, // rewrite own source on change, default true
detachNodes // steal cell DOM from other views, default true
})
The grid is always responsive: width is 100%, the pitch is that width divided by columns (so cells are square and reflow as the container resizes), and height is open — the frame grows to fit its content, so the page/parent y-scrolls rather than a nested box. All positions and sizes in layout are grid units, not pixels. Below the portrait width the grid collapses into a single full-width column (reading order: row y, then left edge x — a centre-straddling atom sorts left). The breakpoint keys off the component's own width, so a nested grid stacks independently of the viewport. gridControls() exposes columns and showGrid as an Inputs.form.
The grid has no built-in chrome. Its operations are a public API on the frame element (frame.grid: addCell, removeCell, pack, candidates, templates, instantiate), and the bundled controls are an ordinary view built with gridControls() — include it in the grid like any cell (controls = gridControls()), drag it, remove it, edit it. It drives whichever grid hosts it in the DOM, and user cells can implement their own controls against the same API. + cell lists the module's cells not yet on the surface (and template_* groups to instantiate); ⊞ pack shelf-packs all atoms left-to-right; a columns slider and grid dots toggle (an Inputs.form) are shown in the same component.
Hover an atom for its handle: drag ⠿ to move (snaps to grid), ✕ removes it from include:, ✎ opens the cell editor in a floating panel above the surface. Drag an atom's bottom-right corner to resize — size snaps to whole grid cells on release, content stretches to the atom, and the body gains scrollbars only when smaller than its content; unresized atoms track their content size. Drag the frame's bottom edge to set its logical height in rows — this reserves empty grid space below the content (content always fits; dragging shorter than the content is a no-op).
Atoms hold the live DOM: observers are dispatched in attach order (runtime-sdk observe), so the last-attached view — the grid — adopts each cell's element and widgets stay real-time.