The grouped, searchable, filterable palette (ADR-0005 decisions 8, 10, 13).
The entry's icon
An entry renders the tile the card it produces will carry, resolved by the
same seam and through the same icon assign - Editor hands this
component and Editor.BlockNode the identical value. The tile was declared
here and rendered nowhere for the whole of the graduated editor's life
(sb-jja), so a host could not put an icon on a palette row at all; a
palette that showed no icons above a canvas that did was the visible half
of that.
Everything it renders comes from palette_entry/0 through
StatifierBlocks.ViewModel, which already applied decision 10's defaults -
label to the type name, group to "Other", description to "",
icon to nil, keywords to [], order to 0. A block type that
implements none of the callback still renders here, and ADR-0002 decision
5 promised it would.
Two filters compose, and they are different in kind:
- Search is the author's, over label, description, type name and
keywords. Purely presentational. - Acceptance is the slot's. When the palette was opened from a
specific gap,
allowedcarries the type names that slot will take, computed byStatifierBlocks.Editorwith the same predicate a drag uses -Edit.Targets.droppable_slots_for/3against a probe block of each candidate type. Decision 8 is explicit that the filter uses the same predicate as decision 5, not a parallel implementation, and this component deliberately computes none of it: it is handed a set.
What a row says, and what the count line says (parity item 1.3)
A row is the tile, the label, and the one-line description the type declared - three things, not one, because the label alone answers "what is this called" and never "what would it do". The tile is a slot rather than an icon: a type that named none still renders the box, so the column of names lines up whether or not every type in a host's registry got around to declaring a glyph.
Above the groups sits one line of arithmetic. Unfiltered it is the size of the palette; filtered it is how much of the palette is left and why. The "why" matters more than it looks: two different filters can be narrowing this list - the author's query and the slot's acceptance set - and an author who opened the palette from a gap never typed anything, so a line that only ever explained queries would leave the more confusing of the two cases unexplained.
The per-group count is the same idea one level down, and it is the count of what is under that header now, not of the group in the registry.
The strip, below 780 (7A)
The 2026-08-29 shell amendment gives the palette a second shape: below a container width of 780 it collapses to a strip - a label and a "+" - that opens as a sheet over the canvas, so the inspector gets the full row. Both shapes are always in the markup and the stylesheet decides which one is on screen, because the breakpoint is a container query and the server does not know how wide the host gave the editor. What the server owns is whether the sheet is open, which is one boolean and one event; selecting a block or picking an entry closes it, since a sheet left open covers the thing the author just chose.
The pane header, and the collapse (parity item 1.1)
Above the strip and the body sits the pane's own header row: the name of the pane, and a chevron that folds it. It is the palette's half of the frame the spike gives both side panes, and 8A puts it on the package's side of the split - it operates on the document that is open rather than deciding which one is.
The collapse is a server-side command in the same shape everything else in
the shell amendment uses: one boolean on the editor, one event, no hook.
Collapsed, the body goes with display: none rather than a zero width, so
a folded pane is out of the tab order and out of the accessibility tree -
a pane an author can still Tab into is a pane that reads as broken to
everyone not using a mouse. The chevron stays, because it is the way back.
The header belongs to the wide arrangement. Below 780 the strip (7A) is the palette's whole chrome and the stylesheet puts the header away: two stacked headers is one more than a one-column arrangement has room for, and the collapse has nothing to fold there - the body is already a sheet.
Picking an entry emits an :insert at exactly the position the "+" named,
which is the identical command a successful drop would produce. That is
what makes the whole insertion path exercisable in LiveViewTest without
simulating a drag, and it is why decision 8 is not only an accessibility
affordance - though it is that, drag-and-drop being unusable by keyboard
and hostile on touch.
The entry is also a drag source (sb-4nep)
A row carries draggable="true" and data-sb-drag-type, which is the
whole of this component's part in palette drag-to-insert. Dragging a type
onto a gap and picking it at an armed gap produce the same :insert at
the same position, so this is a second gesture onto decision 8's one path
rather than a second path - the record's command set is untouched, and the
keyboard route above is unchanged and still the one the tests drive.
The type name rather than a payload: the server owns what a new block of
that type is (Editor.new_block/2 mints the id and the default config at
gesture time, decision 2), so what crosses the wire is the name the author
reached for and nothing that would have to be trusted.
Summary
Functions
The groups a query and an acceptance set leave visible, with empty groups dropped. Pure, so the palette's filtering is asserted directly rather than through markup.
The palette: a header row, a search box, a count line, then a section per
entry.group.
Functions
@spec filter( [StatifierBlocks.ViewModel.PaletteGroup.t()], String.t(), MapSet.t(String.t()) | nil ) :: [ StatifierBlocks.ViewModel.PaletteGroup.t() ]
The groups a query and an acceptance set leave visible, with empty groups dropped. Pure, so the palette's filtering is asserted directly rather than through markup.
The palette: a header row, a search box, a count line, then a section per
entry.group.
Attributes
groups(:list) (required)query(:string) - Defaults to"".allowed(:any) - MapSet of accepted type names, or nil for unfiltered. Defaults tonil.target(:any) (required)icon(:any) - The host's icon component, or nil forStatifierBlocks.Editor.Icons. The same value the canvas cards get, so a type looks the same before and after the pick that puts it in the document.Defaults to
nil.class(:string) - Defaults tonil.sheet_open(:boolean) - Whether the narrow-layout sheet is open (7A). Ignored above 780. Defaults tofalse.collapsed(:boolean) - Whether the pane is folded to its header. The wide arrangement's affordance; below 780 the strip is the palette's chrome and this is ignored.Defaults to
false.insert_target(:any) - Where an armed pick would land, as%{slot: label, parent: title}, or nil when nothing is armed.StatifierBlocks.Shell.insert_target/2computes it; this component only prints it.Defaults to
nil.unarmed_pick(:boolean) - Whether the last pick was made with nothing armed, and so did nothing. The visible half of that no-op.Defaults to
false.