StatifierBlocks.Editor.Slot (StatifierBlocks v0.3.0)

Copy Markdown View Source

One named slot: its header, its children, its gaps, and its "+" buttons (ADR-0005 decisions 5, 6, 8, 11, 13).

Validity is a property of the slot, and it arrives as markup

Decision 5 makes drop-target validity per-slot rather than per-gap, and decision 6 makes it reach the client as markup. So during a drag session this component stamps data-drop="ok" on the slot when the server's one-shot Edit.Targets.droppable_slots/3 accepted it and data-drop="no" when it did not, and hover highlighting is CSS on that attribute. There is zero round-trip per hover, the client holds no validity logic to fall out of sync, and the highlight cannot disagree with the server because it is the server's answer.

Outside a drag session the attribute is absent entirely, which is what distinguishes "not a target for the block being dragged" from "no drag is happening".

A refused slot that has a data-flow reason to give also carries data-drop-reason - not_assignable, or fixable_by:<block id> naming the block whose declaration an author would change (ADR-0003's 2026-08-29 amendment). It is absent when the refusal was structural, when it was for room or for the dragged block's own subtree, or when different gaps in the slot refused for different reasons: those are refusals with no single honest sentence to show, and an attribute is not the place to guess one. Nothing reads it yet; it is markup so that the hover affordance that eventually does needs no round-trip and no JavaScript, exactly as data-drop needs none.

The gaps, and why "+" is always there

A slot with n children has n+1 gaps, each carrying data-parent-id, data-slot and data-index - the DOM contract decision 7's hook reads. Each also carries a "+" button, and it is rendered whether or not a drag is in progress: decision 8 wants every drop target reachable without dragging, and an affordance that only appears mid-drag would be reachable only by dragging. The palette that opens is filtered by the same predicate the drag uses, not a parallel implementation of it.

The rail partition

:secondary and :failure are both attached rails rather than body slots (amendment 10h's placement row), and ViewModel.rail?/1 is that partition. The container's boundary box is derived from the same partition, one level up in BlockNode.

What tells the two apart is a class each and nothing else: sb-slot--secondary for the interrupt's dashed warning edge, and sb-slot--failure for the failure path's solid error-family one. The vocabularies say opposite things about the same placement - an interrupt fires whether or not you get there, a failure path is the continuation this step takes when it goes badly - so a shared placement with no distinction read as a second set of interrupt rules, which is what campaign 013's screens recorded.

The exit edge

A rail's exit is the second question 10h asks of the style, and the operator ruled it on sb-67s: a failure rail leaves by the ordinary flow edge, and the dashed exit channel stays interrupt vocabulary. ViewModel.exit_edge/1 is that derivation and data-exit-edge is it in the markup, stamped on the rails and nowhere else: a rail is the only slot whose exit rejoins a flow the reader can see, and the stylesheet draws the mark off the attribute's presence, so an attribute on a body slot would be a claim about a mark that is not there. The function is total over the three styles all the same - the rail test belongs here, not in every caller. Nothing here reads a type name to reach either answer: core.invoke gets the failure vocabulary by declaring slot_style: %{"on_error" => :failure}, and a host type declaring the same gets the same.

The fan's landing point

The header carries data-sb-anchor, which is where a fan edge lands when the container above arranges more than one slot (the amendment to decision 7, and StatifierBlocks.Connectors). The header rather than the first card inside it: the header is what carries the slot's name and its guard, and an edge that ran past it to the card would cross the very condition the edge is subject to. An empty slot's header is also its exit anchor - an arm with nothing in it yet is a real arm, and a fan that silently skipped it would tell an author their empty arm does not exist.

Recursion

Slot renders children via BlockNode and BlockNode renders slots via Slot, and that is the whole tree. Groups, lanes, branch arms and interrupt rails are these same two components differing only by the layout and slot_style metadata decision 10 put on palette_entry/0. There is no Group component and no Parallel component, and decision 13 says there must never be one.

Summary

Functions

One slot: header, findings, and alternating gaps and children.

Functions

slot(assigns)

One slot: header, findings, and alternating gaps and children.

Attributes

  • slot (StatifierBlocks.ViewModel.Slot) (required)
  • parent_id (:string) (required)
  • drag (:any) - Defaults to nil.
  • selected_id (:string) - Defaults to nil.
  • target (:any) (required)
  • icon (:any) - Defaults to nil.
  • class (:string) - Defaults to nil.