StatifierBlocks.Editor.Drawer (StatifierBlocks v0.18.0)

Copy Markdown View Source

The full-width drawer row (ADR-0005, the 2026-08-29 shell amendment, rulings 1A and 2A).

1A says what belongs here and the test is two words: tabular and document-level. Content that is a grid of rows about the whole document goes in the drawer; content about one block does not, whatever its shape.

Five tabs ship. Truth tables were first. The document-level findings list joined them under operator ruling R4 (2026-08-29), which retired the text block that used to sit under the canvas: a list of findings is a grid of rows about the whole document, so 1A's test admits it and the canvas gets its height back. Declarations joined them under the 2026-09-01 amendment (clause 2i), which took ADR-0001 11i's named door: the document's own datamodel roots are a grid of rows about the envelope, which is 1A's test again. Fixture runs joined under sb-4yze: one row per fixture row in the document, each carrying the outcome slot it expected against the one the compiled chart actually took (StatifierBlocks.Runtime.FixtureRuns does the driving; this module only draws the table). The read-only declared-path view took the last reserved place: one row per path the three declaring surfaces name, with the surfaces that named it and the shape the ADR-0006 projection carries, which is a grid of rows about the whole document and admitted by 1A for the same reason the four before it were. It is read-only on purpose - the editable half is the Declarations tab beside it, over the document's own roots - and no reserved place remains behind it.

The measurable reason the drawer exists at all: a truth table for a branch in a credit-card processing document is one row per case and one column per bound input plus the verdicts, and at the inspector's width it either scrolls sideways or inverts its column order to keep the answers on screen. The spike did the second and filed the inversion as a readability defect. A drawer is as wide as the editor is, which is the axis the table needs.

Never open-or-gone

2A: collapsed, the drawer is a strip carrying a title and a count - a small-caps label beside a chip, "Truth tables 3" - and that is what makes the content discoverable from any state rather than only from the affordance that opens it. Opening it with no table on the selected block shows the index page: the blocks that do own one, each of them a jump. That is the whole of the spike's cold-start gap, closed.

The strip reports the active tab, and an author who has not picked one gets the first tab that actually holds something (StatifierBlocks.Shell.drawer_view/1 resolves it). That is 2A's own reasoning applied to a second tab: a strip is worth having because it says what is in the drawer, and one that names an empty tab while the other has four rows in it says the opposite.

The count is the document's, not the selection's. A strip reading 0 because nothing happens to be selected would tell an author something false about their document.

A host's own tab

8A's split is that the package ships the editing surface and the host ships what surrounds it. A host tab is that reaching the drawer: the editor's drawer_tabs assign carries a label, a count and a function component, the function is called here when its tab is the active one, and the package learns nothing about what it draws.

This admits no new package tab. 1A's test - tabular, and about the whole document - governs the tabs shipped here, unchanged and unweakened; for a tab the host contributes, that test transfers to the host as its own obligation. The 2026-08-30 amendment recording the drawer's tab strip as a host seam is where that split is written down.

It is called the way HEEx calls <.tab /> rather than by applying it to a bare map, which is StatifierBlocks.Editor.Icons' rule (sb-b8g) and the same one for the same reason: a host component that derives a value with assign/3, which is what an ordinary component does, raises on an assigns map with no change-tracking key in it.

A function rather than a slot, for the reason StatifierBlocks.Editor's own moduledoc gives: the first thing a host wants a tab for is a feed of something happening now, and a slot body reading a host assign does not redraw when that assign moves, because the editor is a LiveComponent and a slot body is not one of the assigns it was passed. A pushed descriptor is.

Host data enters the panel through assign/2

The content function is called with the two assigns this seam has - %{id: ..., count: ...} - and everything else the panel draws comes from the host, out of the closure the descriptor was built with. Getting it from the closure into the markup is one line, and which line it is matters:

content: fn assigns -> MyApp.run_feed(assign(assigns, :events, events)) end

Map.merge/2 and Map.put/3 write the same key and are the trap. The assigns map that arrives carries a __changed__ naming the two keys this call passed, id and count, because the call is change-tracked like any other component call. A merged key is absent from that map, so every part of the template that reads it is treated as unchanged and never reaches the diff: the panel draws once, holding whatever the closure held when the tab was opened, and then stands still while the count on the tab beside it keeps climbing. Nothing raises and nothing warns, because nothing is missing - the value is in the assigns, and change tracking was only never told about it.

Phoenix.Component.assign/2 is what tells it. It writes the key and records the key in __changed__, and that record is the whole of the difference between the two lines.

The resize is a command, not a hook

Decision 7 ships exactly one JavaScript hook and this section adds none. The resize is a native range control inside a phx-change form: the new height crosses as an ordinary event, StatifierBlocks.Shell.clamp_height/1 bounds it, and the host stores it - one round trip, and the state that matters lives where state already lives. 2A is explicit that the height is remembered per viewer and that the package has no viewer, so nothing here persists anything.

Summary

Functions

The drawer row: a strip when collapsed, tabs and a table when open.

Functions

drawer(assigns)

The drawer row: a strip when collapsed, tabs and a table when open.

Attributes

  • view (:map) (required) - StatifierBlocks.Shell.drawer_view/1's value.

  • height (:float) (required)

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

  • target (:any) (required)

  • class (:string) - Defaults to nil.

  • declarations (:list) - the entries the Declarations tab draws - the document's, or an author's draft. Defaults to [].

  • declaration_refusal (:string) - the sentence for a refused declaration edit, or nil. Defaults to nil.

  • fixture_runs (:any) - StatifierBlocks.Runtime.FixtureRuns.t() for the Fixtures tab, or nil. Defaults to nil.

  • declared_view (:list) - StatifierBlocks.Datamodel.declared_view/3's rows for the Datamodel tab. Defaults to [].

  • host_tabs (:list) - The host's tab descriptors, already through StatifierBlocks.Shell.host_tabs/1. The active one's content is called for the panel; the strip draws them all from @view.tabs, where their labels and counts already are.

    content is called with %{id: ..., count: ...} and with change tracking on those two keys only, so a host value the function adds for its own markup is added with Phoenix.Component.assign/2 and never merged in - see the moduledoc.

    Defaults to [].