StatifierBlocks.Editor.Canvas (StatifierBlocks v0.11.0)

Copy Markdown View Source

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

canvas(assigns)

The canvas root: the hook's element, and the tree beneath it.

Attributes

  • root (StatifierBlocks.ViewModel.Node) (required)

  • drag (:any) - Defaults to nil.

  • selected_id (:string) - Defaults to nil.

  • armed (:any) - The {parent_id, slot, index} the palette is armed at, or nil (sb-dfyk). Defaults to nil.

  • collapsed (:any) - The MapSet of block ids the author has folded shut, threaded the way selected_id is and for the same reason: it is editor state rather than anything the document holds, and only the editor knows it.

    Defaults to nil.

  • marks (:any) - The host's run marks - %{active: MapSet.t(), invoke: {block_id, outcome} | nil}, or nil when nothing is marked - threaded the way collapsed is and for the same reason: a mark is editor state that addresses a block, and nothing in the view model carries it.

    Defaults to nil.

  • target (:any) (required)

  • icon (:any) - Defaults to nil.

  • theme (:map) - Defaults to %{}.

  • edges (:list) - Defaults to [].

  • stage (:any) - Defaults to nil.

  • zoom (:integer) - Defaults to 100.

  • viewport (:any) - Defaults to nil.

  • reveal (:string) - Defaults to nil.

  • class (:string) - Defaults to nil.