# `StatifierBlocks.Editor.PaletteBrowser`
[🔗](https://github.com/riddler/statifier_blocks/blob/v0.44.1/lib/statifier_blocks/editor/palette_browser.ex#L2)

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,
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, and it is two sets because the palette
    has two kinds of row. `allowed` carries the type names that slot will
    take, computed by `StatifierBlocks.Editor` with the *same* predicate a
    drag uses - `Edit.Targets.droppable_slots_for/3` against a probe block
    of each candidate type, whose config since 2026-09-06 is the entry's
    `default_config`, so a type refused for a READ rather than for its
    kind is filtered here too. `allowed_recipes` carries the recipe names
    whose arrangement lands at the armed position, which is the recipe's
    own question and not a set of type names (ADR-0005 clause 1C):
    `insert/2` at that position, and `Recipe.within_reach?/2` over what it
    answers with. Decision 8 is explicit that the filter uses the same
    predicate as decision 5, not a parallel implementation, and this
    component deliberately computes neither of them: it is handed the sets.

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

Wherever that line says "block type" or "fit" it is counting **types
only**, with the recipes named in a clause of their own. A recipe is not a
block type (ADR-0005 clause 1C) and no acceptance set can answer for its
fit, so a single number over both kinds said two untrue things at once: it
called a recipe a type, and it counted a recipe among the entries the slot
had accepted.

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

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.

# `filter`

```elixir
@spec filter(
  [StatifierBlocks.ViewModel.PaletteGroup.t()],
  String.t(),
  MapSet.t(String.t()) | nil,
  MapSet.t(String.t()) | nil
) :: [StatifierBlocks.ViewModel.PaletteGroup.t()]
```

The groups a query and the two acceptance sets leave visible, with empty
groups dropped. Pure, so the palette's filtering is asserted directly
rather than through markup.

`allowed_recipes` defaults to `nil` - unfiltered - so a caller that has
only the type set, and every existing one did, is unchanged.

# `palette_browser`

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 to `nil`.
* `allowed_recipes` (`:any`) - MapSet of the recipe names whose arrangement lands at the armed
  position, or nil for unfiltered. Independent of `allowed`: a recipe is
  not a block type, so the two sets are two namespaces (ADR-0005 clause
  1C) and neither answers for the other's kind.

  Defaults to `nil`.
* `target` (`:any`) (required)
* `icon` (`:any`) - The host's icon component, or nil for `StatifierBlocks.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 to `nil`.
* `sheet_open` (`:boolean`) - Whether the narrow-layout sheet is open (7A). Ignored above 780. Defaults to `false`.
* `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/2`
  computes 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`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
