StatifierBlocks.Editor (StatifierBlocks v0.5.0)

Copy Markdown View Source

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.slot_verdicts/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.

That one enumeration answers both halves of the drag. The accepting slots are data-drop; the refusing ones that have a data-flow reason to give (the 2026-08-29 ADR-0003 amendment's vocabulary) carry it as data-drop-reason beside it, so a later hover affordance has the reason already in the markup and still needs no round-trip and no JavaScript. The host's widening relation reaches all of this the way ADR-0003 decision 6 says it does and the way nothing else could: through palette.assignability, consulted by the one Assignability the compiler's validate/3 consults.

The other thing a drag can carry (sb-4nep)

A palette entry is a drag source too, and dragging one onto a gap inserts a block of that type there. It is the same two round-trips with the same one enumeration: insert-dragstart builds the session against a probe block of the dragged type instead of against a block the document holds, and insert-drop hands the gap's position to the same insert_from_palette/3 a pick uses. The drop therefore produces decision 2's :insert at a position the author named, exactly as the "+" and the pick do, and ADR-0005's command set is untouched - what is new is a gesture, not a command.

Which is also why the position comes from the gap rather than from palette_position. Arming is how the click path names a destination; a drag names one by landing on it. A drop made while some other gap is armed lands where it was dropped and clears the mode, because the gesture that just finished is the one that said where.

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.

The datamodel, and why it is one assign and no logic

A host may hand the editor the datamodel paths it declares. The only thing that buys is the undeclared-path advisory ADR-0005 amendment 11e-11g specifies: a config field a block type annotated datamodel_path?: true whose value is not in that set gets an :info finding anchored on the field. The rule itself is StatifierBlocks.Datamodel's and is pure, so it is tested with LiveView absent; what happens here is one normalization on update/3 and one concatenation in rebuild/1, which is the same translation-only posture as everything else in this module.

nil is the default and means no datamodel supplied, which per 11f produces nothing anywhere - the check does not run at all. That is not the same as an empty set, which is a host declaring that its documents address nothing.

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).

The 2026-08-29 shell amendment's ruling 8A adds two more, and names the seam for each: slots for markup, events for actions. The outer header - document identity, the document switcher, the theme control, compile and publish - is the :header slot, because markup is exactly the thing a host wants to own and a slot costs this package no API surface. The drawer's height is on_drawer_resize out and drawer_height back in, because the height is remembered per viewer and this package has no viewer. A host that renders its own publish button into the slot and receives the press as its own event is the intended shape; the editor draws no header of its own and is not waiting for permission to. 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.

The insert mode, and the pick that lands nowhere (sb-dfyk)

palette_position is a mode, and every visible part of it hangs off that one assign: the armed gap on the canvas, the instruction naming where the pick will land, the Cancel beside it, and the Escape binding - which is on the root only while the mode is open, so a resting editor holds no window listener for a mode nobody opened. Arming also un-collapses the palette, because a mode whose only instruction is inside a folded pane is the same defect in a different place.

A pick made with nothing armed stays a no-op, and that is a ruling rather than an omission. The alternative on the table was appending to the selected container's default slot, and "default slot" is a rule no accepted ADR states: decision 8 ties a pick to a position the author named at a gap, and inventing a destination on their behalf would be a new contract written into a handler rather than into the record. What the no-op was missing was not a destination but a reason, so it now gives one - palette_unarmed_pick is that sentence, and it renders in the same region the armed case uses for its instruction.

Assigns

AssignRequiredMeaning
idyesthe LiveComponent id
documentyesthe document being edited
paletteyesthe host's palette
findingsnocaller-supplied findings, merged with the two ViewModel derives
datamodelnothe paths the host declares; drives the undeclared-path advisories, and nil (the default) turns them off entirely
on_changenoone-argument function called with each new document
iconnofunction component resolving an icon name to markup
expression_componentnooverride for :expression fields (sui-bob's seam)
themeno--sb-* custom properties for the canvas root
fixturesno%{block_id => [TruthTable.t()]} the drawer's truth-table tab reads; nil (the default) means no fixtures source, and the drawer is still there with a count of 0
drawer_heightnothe drawer's height in rem, remembered by the host per viewer (2A); bounded on the way in
on_drawer_resizenoone-argument function called with each new drawer height, which is how the host comes to have one to remember
classnoappended to the root element's own classes
history_limitnobound on the undo stack; :infinity by default

Summary

Functions

render(assigns)

Slots

  • header - The host's outer header (8A): document identity, the document switcher, the theme control, compile and publish. The package renders the slot's markup and none of its own, and a host that fills nothing gets no header element at all.