Grid Container

edit
controls
demo

Demo

The cells below form a small dataflow (freq, ampwavewavePlot, 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.

viewof freq
viewof amp
waveStats
waveStats = Object {n: 120, min: "-29.8", max: "29.8"}
wavePlot
export_ui
fork notebook options
edit

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.

edit
sg_css = `.sg-frame { position: relative; overflow: hidden; box-sizing: border-box; resize: both; border: 1px solid var(--theme-foreground-faintest, #ddd); border-radius: 8px; background-color: var(--theme-background-alt, #fafafa); } .sg-scroll { position: absolute; inset: 0; overflow: auto; padding-top: 18px; background-image: radial-gradient(circle, color-mix(in srgb, currentColor 14%, transparent) 1px, transparent 1px); background-attachment: local; } .sg-controls { display: flex; flex-direction: column; gap: 6px; align-items: flex-start; } .sg-controls-row { display: flex; gap: 4px; align-items: center; } .sg-add-select { font: 11px var(--sans-serif, system-ui); padding: 2px 6px; cursor: pointer; background: var(--theme-background, #fff); color: var(--theme-foreground, #111); border: 1px solid var(--theme-foreground-faintest, #ddd);Show 135 truncated lines
edit
gridContainer = ƒ(…)
edit
gridControls = ƒ()
edit
edit
edit
viewof freq = HTMLFormElement {0: HTMLInputElement, 1: HTMLInputElement}
edit
viewof amp = HTMLFormElement {0: HTMLInputElement, 1: HTMLInputElement}
edit
wave = Array(120) [Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, …]
edit
waveStats = Object {n: 120, min: "-29.8", max: "29.8"}
edit
wavePlot = SVGSVGElement {}
edit
edit
edit
viewof gridModule = EventTarget {tag: Symbol()}
edit
export_ui = HTMLDivElement {handler: Comment, prerender: HTMLFormElement, theme: HTMLFormElement, bootloader: HTMLFormElement, copyjs: HTMLFormElement, blob: HTMLFormElement, html: HTMLFormElement}
edit
edit
edit
edit
edit
edit