# `PhoenixKitCatalogue.Web.Components`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/v0.19.1/lib/phoenix_kit_catalogue/web/components.ex#L1)

Reusable UI components for the Catalogue module.

All components are designed to be opt-in — features are off by default and
enabled via attributes. Import into any LiveView with:

    import PhoenixKitCatalogue.Web.Components

## Components

  * `search_input/1` — search bar with debounce and clear button
  * `search_results_summary/1` — "N results for …" / "X of Y" summary line
  * `scope_selector/1` — disclosure with catalogue/category checkbox lists
    for narrowing a search (pairs with `Catalogue.search_items/2` filters)
  * `catalogue_rules_picker/1` — smart-catalogue rule editor (checkbox +
    value + unit per catalogue; pairs with `Catalogue.put_catalogue_rules/3`)
  * `view_mode_toggle/1` — table/card view toggle synced via localStorage
  * `item_table/1` — configurable item table with selectable columns
  * `item_picker/1` — combobox for picking a single item via server-side
    search; backed by `Components.ItemPicker` LiveComponent, fires
    `{:item_picker_select, id, item}` / `{:item_picker_clear, id}` upward
  * `featured_image_card/1` — the shared featured-image card used on
    catalogue / category / item forms (thumbnail or empty state + picker
    buttons). Expects `open_featured_image_picker` / `clear_featured_image`
    events wired up in the owning LV — see `Attachments`.
  * `metadata_editor/1` — the shared metadata tab body for catalogue and
    item forms (opt-in fields from `Metadata.definitions/1`). Expects
    `add_meta_field` and `remove_meta_field` events wired up in the LV;
    text edits are absorbed via the form's `validate`.

## Embeddable browse surfaces (separate modules)

  * `Components.ItemSelectorModal` — LiveComponent: the client-facing
    "pick items + quantities from the catalogue" modal. Scoped via
    `search_items/2` opts, reports `{:items_selected, %{picks: …}}` /
    `{:item_selector_closed, _}` to the host LV.
  * `Components.CatalogueBrowse` — LiveComponent: the same browse
    surface (search + category chips + card grid) without selection
    chrome, for embedding a catalogue view on any logged-in page.
    Reports `{:catalogue_browse, %{event: :item_clicked, …}}`.
  * `Components.Browse` — the pure function components both are built
    from (`item_card/1`, `item_grid/1`, `category_chips/1`,
    `qty_stepper/1`, `grid_skeleton/1`, `present_items/2`) for hosts
    that want to compose their own surface with
    `Catalogue.BrowseState`.

Several of these (`search_input`, `search_results_summary`,
`view_mode_toggle`) are deliberately generic — no
catalogue-specific schema knowledge — and are candidates for
promotion to `phoenix_kit` core once a coordinated release lands.
Keeping them here for now avoids coupling catalogue's hex dep to
unpublished core features.

## Examples

    <%!-- Minimal item table: just name and SKU --%>
    <.item_table items={@items} columns={[:name, :sku]} />

    <%!-- Full-featured table with search, pricing, and actions --%>
    <.item_table
      items={@items}
      columns={[:name, :sku, :base_price, :price, :unit, :status, :category, :manufacturer]}
      markup_percentage={@catalogue.markup_percentage}
      edit_path={&Paths.item_edit/1}
      on_delete="delete_item"
    />

    <%!-- Search bar --%>
    <.search_input query={@search_query} placeholder={Gettext.gettext(PhoenixKitCatalogue.Gettext, "Search items...")} />

# `any_featured_thumb?`

Whether any row in a list carries a featured image — gates the photo
column in table views, so a table with no images at all doesn't spend a
permanently empty column on them.

# `any_media_thumb?`

Like `any_featured_thumb?/1`, but also counts the paperclip indicator:
the media column earns its place when some row has an image OR attached
documents (per the `counts` map from `Catalogue.attached_file_counts/1`).

# `attachments_files_panel`

The shared "Photos and Files" tab panel: featured-image card plus the
attached-files manager (dropzone, in-flight uploads, file grid with
signed links and a confirm-guarded remove). One implementation for
the catalogue / category / item forms so the three tabs cannot drift.

Consumer contract: the LiveView uses `PhoenixKitCatalogue.Attachments`
(mount_attachments + allow_attachment_upload) and delegates the
`cancel_upload` and `remove_file` events; this component only renders.

## Attributes

* `uploads` (`:any`) (required)
* `files_state` (`:map`) (required)
* `featured_image_uuid` (`:any`) - Defaults to `nil`.
* `featured_image_file` (`:any`) - Defaults to `nil`.
* `featured_subtitle` (`:string`) (required)
* `files_hint` (`:string`) (required)
* `remove_confirm` (`:string`) (required)
* `remove_title` (`:string`) (required)

# `catalogue_rules_picker`

Renders the smart-catalogue rule editor: one row per candidate
catalogue with a checkbox, a numeric value input, and a unit dropdown.

Pairs with `PhoenixKitCatalogue.Catalogue.put_catalogue_rules/3`. The
component is thin — the caller (usually `ItemFormLive`) owns the
working-rules state in a map `%{referenced_catalogue_uuid => %{value, unit}}`
and calls `put_catalogue_rules/3` on save.

**Event flow:**

  * `on_toggle` — `%{"uuid" => uuid}` when the checkbox is clicked.
    Caller toggles membership in its rules map.
  * `on_set_value` — `%{"uuid" => uuid, "value" => string}` when the
    user edits the amount input.
  * `on_set_unit` — `%{"uuid" => uuid, "unit" => string}` when the
    user picks a different unit.
  * `on_clear` — no params; clear every checked row. Shown only when
    at least one rule is active.

Rows for an unchecked catalogue render disabled inputs but stay
visible so the user always sees the full picker. When `value` is blank
and `item_default_value` is given, the input's placeholder previews
the inherited default (e.g. `"Inherit: 5"`). The unit dropdown is
self-contained per row — it does not inherit from any item-level
default, so changing the item's `default_unit` never flips a rule
row's visible unit.

## Attributes

  * `catalogues` — list of `%Catalogue{}` the user can pick (required).
    Typically `Catalogue.list_catalogues()` filtered to active/archived
    and excluding the parent smart catalogue itself.
  * `rules` — map `%{referenced_catalogue_uuid => %{value, unit}}`
    (or `%CatalogueRule{}` values; only `:value` / `:unit` are read).
    Unchecked catalogues simply don't appear in the map (default `%{}`).
  * `item_default_value` — item's `default_value`, used as the value
    input's placeholder (default `nil`)
  * `units` — list of unit options for the dropdown
    (default `["percent", "flat"]`). The first entry is the fallback
    shown when a rule has no unit set yet.
  * `on_toggle` — event name (default `"toggle_catalogue_rule"`)
  * `on_set_value` — event name (default `"set_catalogue_rule_value"`)
  * `on_set_unit` — event name (default `"set_catalogue_rule_unit"`)
  * `on_clear` — event name (default `"clear_catalogue_rules"`)
  * `id` — DOM id (default `"catalogue-rules-picker"`)
  * `class` — extra wrapper classes

## Example

    <.catalogue_rules_picker
      catalogues={@candidate_catalogues}
      rules={@working_rules}
      item_default_value={@item_default_value}
    />

## Attributes

* `catalogues` (`:list`) (required)
* `rules` (`:map`) - Defaults to `%{}`.
* `item_default_value` (`:any`) - Defaults to `nil`.
* `units` (`:list`) - Defaults to `["percent", "flat"]`.
* `on_toggle` (`:string`) - Defaults to `"toggle_catalogue_rule"`.
* `on_set_value` (`:string`) - Defaults to `"set_catalogue_rule_value"`.
* `on_set_unit` (`:string`) - Defaults to `"set_catalogue_rule_unit"`.
* `on_clear` (`:string`) - Defaults to `"clear_catalogue_rules"`.
* `on_reorder` (`:string`) - When set, rule rows are draggable. Defaults to `nil`.
* `id` (`:string`) - Defaults to `"catalogue-rules-picker"`.
* `class` (`:string`) - Defaults to `""`.

# `featured_image_card`

Renders the featured-image card used on catalogue, category, and item forms.

Shown on the form in a self-contained card: a thumbnail + file name + size
when an image is set, or a dashed empty-state with a primary button when
not. Owning LV must handle the three events wired up by this component:

  * `open_featured_image_picker` — opens the `MediaSelectorModal`
  * `clear_featured_image` — nulls the pointer
  * (change — same `open_featured_image_picker` event)

Each of those has a one-liner delegator to `Attachments`; see the
reference wiring in `catalogue_form_live.ex`, `category_form_live.ex`,
or `item_form_live.ex`.

## Attributes

  * `featured_image_uuid` — uuid string or nil; drives which branch renders
  * `featured_image_file` — the `%Storage.File{}` struct (for name/size) or nil
  * `subtitle` — override the default caption text (optional)
  * `class` — extra classes merged onto the outer card

## Examples

    <.featured_image_card
      featured_image_uuid={@featured_image_uuid}
      featured_image_file={@featured_image_file}
    />

    <.featured_image_card
      featured_image_uuid={@featured_image_uuid}
      featured_image_file={@featured_image_file}
      subtitle={gettext("Shown on category landing pages.")}
    />

## Attributes

* `featured_image_uuid` (`:string`) - Defaults to `nil`.
* `featured_image_file` (`:any`) - Defaults to `nil`.
* `subtitle` (`:string`) - Defaults to `nil`.
* `class` (`:string`) - Defaults to `""`.

# `featured_thumb`

Featured-image thumbnail for list rows, rendered to the left of the name.
Renders nothing when the resource carries no attached image. Works on
anything with a `data` map holding `"featured_image_uuid"` — catalogue /
category / item structs and the index's `Map.from_struct/1` row maps alike.

The URL is signed straight off the stored uuid (no per-row file lookup, so
lists stay query-free); a dangling pointer — the file was deleted after
being attached — 404s and removes itself via `onerror` instead of showing
the browser's broken-image glyph.

## Attributes

* `resource` (`:any`) (required)
* `class` (`:any`) - Defaults to `"w-10 h-10"`.
* `has_files` (`:boolean`) - The file-attached indicator: with an image, a small paperclip emblem in the thumb's top-right corner; with no image, a muted paperclip tile in the same slot. Feed it from `Catalogue.attached_file_counts/1` — it means "has attached documents" (the non-image files the product card's Files section lists). Defaults to `false`.
* `on_click` (`:string`) - When set, the thumb becomes a button pushing this event with the resource's uuid — the product-view hook ("pressing on the featured image"). nil keeps the thumb inert. Defaults to `nil`.

# `format_supplier_costs`

```elixir
@spec format_supplier_costs(list() | nil) :: String.t()
```

The "Supplier price" cell text for one item's cost ranges (see
`Catalogue.supplier_cost_ranges/1`): one supplier → `5.69`, several →
`5.69–9.99` (min–max of the current rows). Rows priced in different
currencies are shown as separate ranges with their code — `5.69–9.99
EUR, 4.00 USD` — so two currencies are never collapsed into one span.
Nothing priced → `—`.

# `item_card_menu`

The same Edit / Search PDFs / Delete menu WITHOUT the table cell —
for card footers, where the boss standard is the ⋮ menu, not a row of
icon buttons.

## Attributes

* `item` (`:any`) (required)
* `id_prefix` (`:string`) - Defaults to `"item-card-menu"`.
* `edit_path` (`:any`) - Defaults to `nil`.
* `on_delete` (`:string`) - Defaults to `nil`.
* `pdf_search_event` (`:string`) - Defaults to `nil`.

# `item_picker`

Combobox for picking a single catalogue item via server-side search.

Thin wrapper around the `ItemPicker` LiveComponent — it's the
LiveComponent that owns search state, events, and the colocated JS
hook. This wrapper exists so consumers have an attr-declared call
site and don't have to remember `<.live_component module={...}>`.

The parent LiveView reacts to two messages in its `handle_info/2`:

    {:item_picker_select, id, %Item{}}   # user chose an item
    {:item_picker_clear,  id}            # user cleared the selection

where `id` is the `:id` you passed in — handy for multiple pickers on
one page.

## Examples

    <.item_picker
      id={"row-#{@row.id}-picker"}
      category_uuids={[@category_uuid]}
      selected_item={@row.item}
      excluded_uuids={@used_uuids}
      locale="en"
    />

See `PhoenixKitCatalogue.Web.Components.ItemPicker` for the full attr
reference and the keyboard / a11y contract.

## Attributes

* `id` (`:string`) (required)
* `category_uuids` (`:any`) - Defaults to `nil`.
* `catalogue_uuids` (`:any`) - Defaults to `nil`.
* `include_descendants` (`:boolean`) - Defaults to `true`.
* `only` (`:atom`) - Restrict results to uncategorised or categorised items only. Defaults to `nil`. Must be one of `nil`, `:uncategorized_only`, or `:categorized_only`.
* `selected_item` (`:any`) - Defaults to `nil`.
* `excluded_uuids` (`:list`) - Defaults to `[]`.
* `locale` (`:string`) (required)
* `placeholder` (`:string`) - Defaults to `nil`.
* `empty_query_limit` (`:integer`) - Defaults to `10`.
* `page_size` (`:integer`) - Defaults to `20`.
* `disabled` (`:boolean`) - Defaults to `false`.
* `format_price` (`:any`) - Defaults to `nil`.
* `format_unit` (`:any`) - Defaults to `nil`.
* `show_unit` (`:boolean`) - Defaults to `false`.
* `highlight_selected` (`:boolean`) - Defaults to `true`.
* `initial_query` (`:string`) - Defaults to `nil`.
* `photo_clickable` (`:boolean`) - Defaults to `false`.

# `item_pricing_cell`

The name + SKU + sale-price cells for an item, rendered as standalone
`<td>`s for a core-toolkit `<.table_default>` row. Pricing uses
`Catalogue.item_pricing/1` so the figures match the rest of the
module. Pass `edit_path` (a 1-arity `uuid -> path` fn) to make the
name a link.

Renders the name cell (link), then one cell per entry in `columns` —
`"sku"` / `"price"` / `"unit"` / `"status"` — in the given order, so
a Columns configuration controls both visibility and sequence.

## Attributes

* `item` (`:any`) (required)
* `edit_path` (`:any`) - Defaults to `nil`.
* `has_attributes` (`:boolean`) - Defaults to `false`.
* `file_count` (`:integer`) - Defaults to `0`.
* `columns` (`:list`) - Defaults to `["sku", "price", "unit", "status"]`.
* `supplier_costs` (`:list`) - This item's entry from `Catalogue.supplier_cost_ranges/1` (drives `"supplier_price"`). Defaults to `[]`.

# `item_row_menu`

The per-row action menu for an active item (Edit / Search PDFs /
Delete), rendered as a standalone `<td>` for a core-toolkit row.
Mirrors `item_table`'s `item_actions` action set for the active list.

## Attributes

* `item` (`:any`) (required)
* `edit_path` (`:any`) - Defaults to `nil`.
* `on_delete` (`:string`) - Defaults to `nil`.
* `pdf_search_event` (`:string`) - Defaults to `nil`.

# `item_table`

Renders a configurable item table with optional card view toggle.

Columns are opt-in — only the columns you list are shown. Actions (edit, delete,
restore) are opt-in via their respective attributes.

## Attributes

  * `items` — list of items to display (required)
  * `columns` — list of column atoms to show (default: `[:name, :sku, :base_price, :status]`)
    Available: [:name, :sku, :base_price, :price, :discount, :final_price, :unit, :status, :category, :catalogue, :manufacturer]
  * `cards` — enable card view toggle (default: `false`). When enabled, renders a
    table/card toggle button and shows items as cards on mobile. The card view
    shows the item name as the title, selected columns as key-value fields,
    and action buttons in the card footer.
  * `id` — unique ID for the component (required when `cards` is true, used by
    the JS hook to persist view preference)
  * `markup_percentage` — catalogue markup for `:price` and `:final_price` columns
    (required when either is listed; ignored otherwise)
  * `discount_percentage` — catalogue discount for `:discount` and `:final_price`
    columns (required when either is listed; ignored otherwise). The `:discount`
    column honors per-item overrides via `Item.effective_discount/2`.
  * `edit_path` — 1-arity function `(uuid -> path)` to enable edit links
  * `on_delete` — event name for soft-delete button (e.g. `"delete_item"`)
  * `on_restore` — event name for restore button (e.g. `"restore_item"`)
  * `on_permanent_delete` — event name for permanent delete (e.g. `"show_delete_confirm"`)
  * `permanent_delete_type` — type string passed as `phx-value-type` (e.g. `"item"`)
  * `catalogue_path` — 1-arity function `(uuid -> path)` for catalogue links in `:catalogue` column
  * `variant` — table variant: `"default"` or `"zebra"` (default: `"default"`)
  * `size` — table size: `"xs"`, `"sm"`, `"md"`, `"lg"` (default: `"sm"`)
  * `wrapper_class` — override wrapper CSS class

## Examples

    <%!-- Table only --%>
    <.item_table items={@items} columns={[:name, :sku, :base_price]} />

    <%!-- With card view toggle --%>
    <.item_table
      items={@items}
      columns={[:name, :sku, :base_price, :price, :status]}
      cards={true}
      id="catalogue-items"
      markup_percentage={@catalogue.markup_percentage}
      edit_path={&Paths.item_edit/1}
      on_delete="delete_item"
    />

## Attributes

* `items` (`:list`) (required)
* `columns` (`:list`) - Defaults to `[:name, :sku, :base_price, :status]`.
* `cards` (`:boolean`) - Defaults to `true`.
* `photo_click` (`:string`) - Event pushed (with the item's uuid) when a featured-image thumb is clicked — the host renders the ProductCard modal and handles its events. nil keeps thumbs inert. Defaults to `nil`.
* `file_counts` (`:map`) - %{item_uuid => attached-document count} from Catalogue.attached_file_counts/1 — drives the paperclip indicator in the photo column. Computed by the caller (function components must not query). Defaults to `%{}`.
* `attribute_map` (`:map`) - %{item_uuid => attribute_group_uuid} from Catalogue.item_attribute_group_map/1 — drives the swatch indicator beside the name. Computed by the caller. Defaults to `%{}`.
* `show_toggle` (`:boolean`) - Defaults to `true`.
* `id` (`:string`) - Defaults to `nil`.
* `storage_key` (`:string`) - Defaults to `nil`.
* `markup_percentage` (`:any`) - Defaults to `nil`.
* `discount_percentage` (`:any`) - Defaults to `nil`.
* `edit_path` (`:any`) - Defaults to `nil`.
* `name_path` (`:any`) - Where an item's NAME links, as a 1-arity function of the item. Defaults to
  `edit_path` (the catalogue's own convention). An embedded, read-only list
  can point it somewhere else — landing on an edit form from a list you are
  only reading is a surprise.

  Defaults to `nil`.
* `on_delete` (`:string`) - Defaults to `nil`.
* `on_restore` (`:string`) - Defaults to `nil`.
* `on_permanent_delete` (`:string`) - Defaults to `nil`.
* `permanent_delete_type` (`:string`) - Defaults to `"item"`.
* `catalogue_path` (`:any`) - Defaults to `nil`.
* `variant` (`:string`) - Defaults to `"default"`.
* `size` (`:string`) - Defaults to `"sm"`.
* `wrapper_class` (`:string`) - Defaults to `nil`.
* `pdf_search_event` (`:string`) - When set, action menu gets a 'Search PDFs' entry that pushes this event with phx-value-uuid. Defaults to `nil`.
* `on_reorder` (`:string`) - When set, rows become draggable and emit this event. Defaults to `nil`.
* `reorder_scope` (`:map`) - Map of extra scope values (e.g. %{catalogue_uuid: "...", category_uuid: "..."}) — exposed to the SortableGrid hook as data-sortable-scope-* attrs. Defaults to `%{}`.
* `reorder_group` (`:string`) - SortableJS group name; tables sharing a group can exchange items via cross-container drag (e.g. items moving between categories). Defaults to `nil`.
* `selectable` (`:boolean`) - When true, each row gets a checkbox in the leftmost column (combined with the drag handle when reorderable). The drag handle is hidden until the row is hovered. Defaults to `false`.
* `selected_uuids` (`:any`) - MapSet of selected item UUIDs. Defaults to `nil`.
* `on_toggle_select` (`:string`) - Event name fired when the user toggles a row's checkbox. The LV handler receives `phx-value-uuid`. Defaults to `nil`.

# `metadata_editor`

Renders the metadata editor used inside the Metadata tab on the item
and catalogue forms — heading + empty-state alert + one text input
per attached key + add-picker dropdown.

Owner LV must handle the three events wired up by this component:

  * `add_meta_field` (from the add-picker `<.select>`'s `phx-change`)
  * `remove_meta_field` (per-row × button)
  * (text edits are absorbed by the form's `phx-change="validate"`
    via `Metadata.absorb_params/2`)

## Attributes

  * `resource_type` — `:item` or `:catalogue`; drives which
    `Metadata.definitions/1` list is consumed for the add-picker and
    for legacy-key detection
  * `state` — the `%{attached: [key], values: %{key => string}}` map
    produced by `Metadata.build_state/2` and kept on the socket
  * `id_prefix` — DOM-id prefix for inputs and the add-picker (so the
    same Metadata editor can render twice on a page without colliding)
  * `title` — heading text (optional, defaults to "Metadata")
  * `description` — the grey subtitle under the heading (optional)

## Examples

    <.metadata_editor
      resource_type={:catalogue}
      state={@meta_state}
      id_prefix="catalogue"
    />

## Attributes

* `resource_type` (`:atom`) (required)
* `state` (`:map`) (required)
* `id_prefix` (`:string`) (required)
* `title` (`:string`) - Defaults to `nil`.
* `description` (`:string`) - Defaults to `nil`.

# `party_items_columns`

The configurable columns of `party_items_table/1`, in the shape core's
`column_settings_modal/1` expects. `name` is deliberately absent: it is
always shown, so it is not the picker's to remove.

# `party_items_default_columns`

Default shown columns for `party_items_table/1`.

# `party_items_table`

An item list for another module to embed — the CRM company page's
Catalogue tab today.

This is a deliberately narrow wrapper around `item_table/1` rather than a
second table. A caller outside this package cannot invoke `item_table/1`
itself: HEEx injects `attr` defaults at the CALL SITE, so reaching it
through `apply/3` would mean the caller supplying every attribute by hand
and re-supplying each new one we add. Here the defaults are applied inside
the catalogue, and the contract with the caller is two keys.

Takes a plain map (no attr defaults are available through `apply/3`):

  * `:items` — `%Item{}` structs, ideally hydrated by
    `Manufacturers.hydrate/1` so the manufacturer column has a name
  * `:id` — unique DOM id for the table
  * `:columns` — optional; which columns to show, in order. Defaults to a
    sensible set. Pair it with core's `column_settings_modal/1` and
    `managed_columns/0` below to give the embed a working column picker.

Presentation — the image column, the card/table toggle, price and status
formatting — stays owned by the catalogue, so an embedded list keeps
matching the catalogue's own. That includes the convention that an item's
name opens its edit form; the `catalogue` column is the way back to where
the item lives.

# `scope_selector`

Renders a compact scope selector for narrowing a search to a subset of
catalogues and/or categories.

Designed to pair with `Catalogue.search_items/2`'s `:catalogue_uuids`
and `:category_uuids` options. The component is thin — the parent
LiveView owns the selection state and decides which catalogues and
categories are pickable. Typical flow:

    # LV loads the pickable set (e.g. via list_catalogues_by_name_prefix/2)
    socket
    |> assign(:scope_catalogues, Catalogue.list_catalogues_by_name_prefix("Kit"))
    |> assign(:scope_categories, [])
    |> assign(:selected_catalogue_uuids, [])
    |> assign(:selected_category_uuids, [])

Renders as a disclosure with a summary ("2 catalogues · all categories")
and two checkbox lists inside. Each section is only rendered when its
list is non-empty, so callers can use it for catalogue-only or
category-only scoping.

## Events

Emits four events (all names customizable via attrs):

  * `on_toggle_catalogue` — `%{"uuid" => uuid}` when a catalogue is clicked
  * `on_toggle_category` — `%{"uuid" => uuid}` when a category is clicked
  * `on_clear_catalogues` — no params; clear all catalogue selections
  * `on_clear_categories` — no params; clear all category selections

The LV toggles membership in its own selection lists, then re-runs
the search with the updated scope.

## Attributes

  * `catalogues` — list of `%Catalogue{}` the user can pick from (default `[]`)
  * `categories` — list of `%Category{}` the user can pick from (default `[]`)
  * `selected_catalogue_uuids` — currently selected catalogue UUIDs (default `[]`)
  * `selected_category_uuids` — currently selected category UUIDs (default `[]`)
  * `on_toggle_catalogue` — event name (default `"toggle_catalogue_scope"`)
  * `on_toggle_category` — event name (default `"toggle_category_scope"`)
  * `on_clear_catalogues` — event name (default `"clear_catalogue_scope"`)
  * `on_clear_categories` — event name (default `"clear_category_scope"`)
  * `id` — DOM id (default `"scope-selector"`)
  * `open` — force the disclosure open (default `false` — collapsed until clicked)
  * `class` — extra CSS classes on the wrapper

## Example

    <.scope_selector
      catalogues={@scope_catalogues}
      categories={@scope_categories}
      selected_catalogue_uuids={@selected_catalogue_uuids}
      selected_category_uuids={@selected_category_uuids}
    />

## Attributes

* `catalogues` (`:list`) - Defaults to `[]`.
* `categories` (`:list`) - Defaults to `[]`.
* `selected_catalogue_uuids` (`:list`) - Defaults to `[]`.
* `selected_category_uuids` (`:list`) - Defaults to `[]`.
* `on_toggle_catalogue` (`:string`) - Defaults to `"toggle_catalogue_scope"`.
* `on_toggle_category` (`:string`) - Defaults to `"toggle_category_scope"`.
* `on_clear_catalogues` (`:string`) - Defaults to `"clear_catalogue_scope"`.
* `on_clear_categories` (`:string`) - Defaults to `"clear_category_scope"`.
* `id` (`:string`) - Defaults to `"scope-selector"`.
* `open` (`:boolean`) - Defaults to `false`.
* `class` (`:string`) - Defaults to `""`.

# `search_input`

Renders a search input with debounce and clear button.

Emits `search` event with `%{"query" => value}` on change/submit,
and `clear_search` on clear button click. Override event names via attrs.

## Attributes

  * `query` — current search query string (required)
  * `placeholder` — input placeholder text. `nil` (default) resolves
    to a translated `gettext("Search...")` inside the component body.
    Pass an explicit string to override (e.g.
    `gettext("Search items...")`).
  * `on_search` — event name for search (default: "search")
  * `on_clear` — event name for clear (default: "clear_search")
  * `debounce` — debounce ms (default: 300)
  * `class` — additional CSS classes on the wrapper div

## Attributes

* `query` (`:string`) (required)
* `placeholder` (`:string`) - Defaults to `nil`.
* `on_search` (`:string`) - Defaults to `"search"`.
* `on_clear` (`:string`) - Defaults to `"clear_search"`.
* `debounce` (`:integer`) - Defaults to `300`.
* `class` (`:string`) - Defaults to `""`.

# `search_results_summary`

Renders a search results count summary line.

## Attributes

  * `count` — total number of matching results (required)
  * `query` — the search query string (required)
  * `loaded` — optional count of results currently rendered. When given
    and less than `count`, the summary shows "X of Y" so users know the
    list is paging. Omit or pass `nil` for a plain "N results" line.

## Attributes

* `count` (`:integer`) (required)
* `query` (`:string`) (required)
* `loaded` (`:integer`) - Defaults to `nil`.

# `view_mode_toggle`

Renders a table/card view toggle that syncs all tables sharing the same storage key.

Place this once at the top of a page, and set `show_toggle={false}` +
matching `storage_key` on the individual `item_table` components.

Uses the same localStorage mechanism as `table_default`'s built-in toggle,
so all tables reading the same key will respect the user's choice.

## Attributes

  * `storage_key` — the localStorage key to sync (required, must match the tables)
  * `class` — additional CSS classes

## Examples

    <.view_mode_toggle storage_key="catalogue-items" />
    <.item_table cards={true} show_toggle={false} storage_key="catalogue-items" ... />

## Attributes

* `storage_key` (`:string`) (required)
* `class` (`:string`) - Defaults to `""`.

---

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