The tree's root and the drag hook's element (ADR-0005 decisions 7, 13, 14).
This component is small on purpose. It is the one element carrying
phx-hook="StatifierBlocksDrag", and every drag event in the editor
arrives through it: the hook is attached to the canvas root rather than to
each block, so adding a block adds no listeners and the hook has one
lifetime rather than one per node.
It is also the measurement stage. The 2026-08-29 amendment to decision 7
admits a second hook whose whole job is reading laid-out boxes, and every
box it reads is relative to this element - which is why this element
carries data-sb-anchor="stage" and why the connector overlay is drawn
inside it, in its own untransformed coordinate space. The hook itself
rides a child element rather than this one, because an element carries one
phx-hook and this one is the drag hook's; ConnectorLayer renders that
child and says why.
It is also where decision 14's theming lands. The --sb-* custom
properties are declared on this element by the package's stylesheet, so a
host that wants a different palette sets them here through the theme
assign - a map of property name to value, rendered as an inline style -
and needs no stylesheet of its own to do it.
The panel is the scroller; this element is the stage
Parity item 1.2 gives the canvas a bordered, dotted ground, and both halves
of that land on two elements rather than one. .sb-canvas-panel is the
box - the border, and overflow - and this element stays the stage inside
it, sized by the tree it holds.
The split is forced by the connector overlay above, not chosen for tidiness.
.sb-connectors is absolutely positioned against this element and sized
from the stage's scroll extent; put overflow on the same element and the
overlay is sized to the padding box instead, so the connectors stay put
while the tree scrolls out from under them. A scroller one level out leaves
the overlay's containing block exactly where the measurement hook already
reads it from.
The dots ride the panel for the same reason, one step on. A ground has to be visible to be a ground, and the only place it can show is the band the panel's padding opens around the tree - the root block's own surface covers everything inside it. Painting them on the stage instead and padding the stage to make room is the version that displaces every connector by the padding, which is the trade the paragraph above already made.
Zoom is a third element between the two
A zoom is a CSS transform on the stage, and a transform is drawn after
layout: it changes what the stage looks like and nothing about the space
it takes. Left there, the panel scrolls over the unscaled tree - zoomed
in, the bottom right corner is unreachable; zoomed out, most of the scroll
range is empty. So the stage is wrapped, and the wrapper carries the
scaled size the server computes from the measurement
(Shell.zoom_extent/2).
The wrapper is a plain block with no size of its own, so at 100% - where
zoom_extent/2 returns nothing - it lays out exactly as the stage did
when it was the panel's only child. It is deliberately outside the
stage: everything inside is measured in the stage's own untransformed
space, and a scaled box in the middle of that is the thing the overlay
cannot survive.
So --sb-canvas-grid is a stylesheet-tier override rather than a theme
assign one: it is consumed above the element that assign writes to, exactly
as the pane widths and the drawer height are. docs/theming.md calls a
stylesheet the ordinary case and the assign the computed one, so this is
where the grid was always going to land.
Summary
Functions
The canvas root: the hook's element, and the tree beneath it.
Functions
The canvas root: the hook's element, and the tree beneath it.
Attributes
root(StatifierBlocks.ViewModel.Node) (required)drag(:any) - Defaults tonil.selected_id(:string) - Defaults tonil.armed(:any) - The{parent_id, slot, index}the palette is armed at, or nil (sb-dfyk). Defaults tonil.collapsed(:any) - TheMapSetof block ids the author has folded shut, threaded the wayselected_idis and for the same reason: it is editor state rather than anything the document holds, and only the editor knows it.Defaults to
nil.target(:any) (required)icon(:any) - Defaults tonil.theme(:map) - Defaults to%{}.edges(:list) - Defaults to[].stage(:any) - Defaults tonil.zoom(:integer) - Defaults to100.viewport(:any) - Defaults tonil.reveal(:string) - Defaults tonil.class(:string) - Defaults tonil.