# `StatifierBlocks.Editor.Canvas`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/editor/canvas.ex#L2)

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.

# `canvas`

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. 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`.
* `pending_remove` (`:any`) - The delete offer, `%{block_id:, ids:}` or `nil`, threaded to the card
  that raised it (ADR-0005's 2026-09-07 amendment, clause 3D).

  Defaults to `nil`.
* `expandable` (`:any`) - The document's composite block ids, a `MapSet` or `nil`, threaded to the
  card that draws the Expand control (ADR-0005's 2026-09-07 amendment,
  clause 1E).

  Defaults to `nil`.
* `collapsible` (`:boolean`) - Whether the mount registered an `on_collapse` callback, threaded down
  the tree so a card draws the "Save as a step" control only where there
  is a host to hand the declaration to (ADR-0005's Note of 2026-09-08,
  item 1). `false` is the default for the reason the callback's own
  default is: a gesture whose only outcome is a callback nobody
  registered has nothing to offer.

  Defaults to `false`.
* `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`.
* `drag_hook?` (`:boolean`) - Whether the canvas mounts decision 7's one drag hook. `false` for a
  read-only mount (ADR-0005's 2026-09-07 profile amendment, `read_only?`
  clause 2): not a hook that refuses a drag, but no hook at all. The
  measure hook on the connector layer is unaffected - it is decision 7's
  read-only measurement and reads nothing an author can change.

  Defaults to `true`.
* `read_only` (`:boolean`) - Whether this mount edits (ADR-0005's 2026-09-07 profile amendment,
  `read_only?`). Threaded down the tree so every gap draws its position
  and no "+" button. Separate from `drag_hook?` on purpose: one is about
  the hook this element mounts, the other about what the tree beneath it
  draws, and a host reading either should not have to know they always
  arrive together.

  Defaults to `false`.
* `class` (`:string`) - Defaults to `nil`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
