The block editor: the only stateful module in the package's rendered half (ADR-0005 decisions 6, 8, 9, 13).
Everything this component does is translation. Every author gesture
reduces to one of the four commands StatifierBlocks.Edit defines, and
the mutation, its inverse and the set of places a block may be dropped are
all pure functions of {document, palette} that were tested with LiveView
absent from the dependency tree. What is left here is turning a phx-
event into a command, offering it to StatifierBlocks.Edit.History, and
re-rendering.
That split is the single most load-bearing constraint in ADR-0005, and the reason is drag-and-drop specifically: it is the interaction most likely to be tested by clicking around and declared fine, and the one most likely to corrupt a document when it is wrong. If the semantics of a drag lived here, they would be testable only through a browser driver.
The drag, in two round-trips
dragstart pushes one event. Edit.Targets.droppable_slots/3 runs once,
the result goes into the drag session, and the re-render stamps
data-drop on every slot - so every valid target highlights before the
pointer has moved, and hover costs nothing. drop pushes the position, one
:move is built and applied, and the tree re-renders. dragend clears the
session. There is no third round-trip and no client-side validity logic.
Config, and the gate that keeps the document sound
ADR-0002 decision 6 guarantees slots/1 returns without raising only for
config validate_config/1 accepts - and an author halfway through typing
an identifier has invalid config almost continuously. So an
:update_config reaches the document only when it validates, which
Edit.check_config/3 enforces inside History.commit/4.
In-progress form state that does not validate lives in this component's
drafts assign: never in the document, never on the undo stack. It is
overlaid onto the selected block's form at render time - values and
findings both - so the author keeps their keystrokes and sees findings
about the value they are currently typing, while the tree, the slot set
and every consumer downstream still see the last config that validated.
Crucially, the overlay never calls slots/1: it touches the form and
nothing else, which is what keeps the draft from reaching a callback that
was only promised valid config.
What stays the host's
Which palette entries a tenant may use, who may edit or publish a
document, where it is stored, and what publishing means are all outside
this package (decision 15). So is concurrency: this is a single-session
component, it surfaces the revision it loaded so a host can do
optimistic concurrency on save, and it does not merge, rebase or resolve
anything.
Assigns
| Assign | Required | Meaning |
|---|---|---|
id | yes | the LiveComponent id |
document | yes | the document being edited |
palette | yes | the host's palette |
findings | no | caller-supplied findings, merged with the two ViewModel derives |
on_change | no | one-argument function called with each new document |
icon | no | function component resolving an icon name to markup |
expression_component | no | override for :expression fields (sui-bob's seam) |
theme | no | --sb-* custom properties for the canvas root |
class | no | appended to the root element's own classes |
history_limit | no | bound on the undo stack; :infinity by default |