The graph has pan/zoom (ensureZoomControls, web/app.js:1791) but no way to move or resize a node — positions are dagre's, take them or leave them. For screenshots, docs, and just living with a graph daily, hand-placement matters: dagre is a good first draft and a bad final layout.
Scope (deliberately client-first, no database):
- Drag — pointer-drag a node card (or a containment box) applies a
transform delta to its data-node-id group; edges connected to it re-anchor (straight-line fallback is acceptable for a dragged node; full spline re-route is not required).
- Resize — a corner handle on containment boxes and (later) cards; boxes matter more, cards may be v2.
- Persistence, no DB — a layout sidecar keyed by node id, two tiers:
localStorage per project+lens (free, survives reload, per-browser);
- optional
POST /api/layout writing .behold/layout.json next to the project (shareable, versionable, still not a database). Server applies stored deltas at render time so exports and snapshots honor them.
- Reset — a "relayout" action that clears the sidecar for the current lens.
Deltas are keyed by stable node id + lens + zoom tier, and a stored delta for a node that no longer exists is silently dropped — the graph is chant's, the layout is yours, and the source of truth ordering never inverts.
Deferral note: this is consciously behind icons/render-coverage in priority. Filing now so the shape (deltas-over-dagre, sidecar-not-DB) is agreed before anyone builds the wrong version.
Acceptance
- Drag a card, reload: it stays put (localStorage tier). Export SVG: it stays put (server tier, if enabled).
- Delete the node from the estate: the stale delta is ignored without error.
?logical=1 and the plain graph keep independent layouts.
The graph has pan/zoom (
ensureZoomControls,web/app.js:1791) but no way to move or resize a node — positions are dagre's, take them or leave them. For screenshots, docs, and just living with a graph daily, hand-placement matters: dagre is a good first draft and a bad final layout.Scope (deliberately client-first, no database):
transformdelta to itsdata-node-idgroup; edges connected to it re-anchor (straight-line fallback is acceptable for a dragged node; full spline re-route is not required).localStorageper project+lens (free, survives reload, per-browser);POST /api/layoutwriting.behold/layout.jsonnext to the project (shareable, versionable, still not a database). Server applies stored deltas at render time so exports and snapshots honor them.Deltas are keyed by stable node id + lens + zoom tier, and a stored delta for a node that no longer exists is silently dropped — the graph is chant's, the layout is yours, and the source of truth ordering never inverts.
Deferral note: this is consciously behind icons/render-coverage in priority. Filing now so the shape (deltas-over-dagre, sidecar-not-DB) is agreed before anyone builds the wrong version.
Acceptance
?logical=1and the plain graph keep independent layouts.