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.ComponentsComponents
search_input/1— search bar with debounce and clear buttonsearch_results_summary/1— "N results for …" / "X of Y" summary linescope_selector/1— disclosure with catalogue/category checkbox lists for narrowing a search (pairs withCatalogue.search_items/2filters)catalogue_rules_picker/1— smart-catalogue rule editor (checkbox + value + unit per catalogue; pairs withCatalogue.put_catalogue_rules/3)view_mode_toggle/1— table/card view toggle synced via localStorageitem_table/1— configurable item table with selectable columnsitem_picker/1— combobox for picking a single item via server-side search; backed byComponents.ItemPickerLiveComponent, fires{:item_picker_select, id, item}/{:item_picker_clear, id}upwardfeatured_image_card/1— the shared featured-image card used on catalogue / category / item forms (thumbnail or empty state + picker buttons). Expectsopen_featured_image_picker/clear_featured_imageevents wired up in the owning LV — seeAttachments.metadata_editor/1— the shared metadata tab body for catalogue and item forms (opt-in fields fromMetadata.definitions/1). Expectsadd_meta_fieldandremove_meta_fieldevents wired up in the LV; text edits are absorbed via the form'svalidate.empty_state/1— centered empty state card with message and optional action
Several of these (search_input, search_results_summary,
view_mode_toggle, empty_state) 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(PhoenixKitWeb.Gettext, "Search items...")} />
Summary
Functions
Renders the smart-catalogue rule editor: one row per candidate catalogue with a checkbox, a numeric value input, and a unit dropdown.
Renders an empty state card with a message and optional action slot.
Renders the featured-image card used on catalogue, category, and item forms.
Combobox for picking a single catalogue item via server-side search.
Renders a configurable item table with optional card view toggle.
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.
Renders a compact scope selector for narrowing a search to a subset of catalogues and/or categories.
Renders a search input with debounce and clear button.
Renders a search results count summary line.
Renders a table/card view toggle that syncs all tables sharing the same storage key.
Functions
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). TypicallyCatalogue.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/:unitare read). Unchecked catalogues simply don't appear in the map (default%{}).item_default_value— item'sdefault_value, used as the value input's placeholder (defaultnil)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 tonil.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".id(:string) - Defaults to"catalogue-rules-picker".class(:string) - Defaults to"".
Renders an empty state card with a message and optional action slot.
Attributes
message— the text to display (required)
Slots
inner_block— optional action content (buttons, links)
Attributes
message(:string) (required)
Slots
inner_block
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 theMediaSelectorModalclear_featured_image— nulls the pointer- (change — same
open_featured_image_pickerevent)
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 rendersfeatured_image_file— the%Storage.File{}struct (for name/size) or nilsubtitle— 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 tonil.featured_image_file(:any) - Defaults tonil.subtitle(:string) - Defaults tonil.class(:string) - Defaults to"".
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 selectionwhere 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(:list) - Defaults tonil.catalogue_uuids(:list) - Defaults tonil.include_descendants(:boolean) - Defaults totrue.only(:atom) - Restrict results to uncategorised or categorised items only. Defaults tonil. Must be one ofnil,:uncategorized_only, or:categorized_only.selected_item(:any) - Defaults tonil.excluded_uuids(:list) - Defaults to[].locale(:string) (required)placeholder(:string) - Defaults tonil.empty_query_limit(:integer) - Defaults to10.page_size(:integer) - Defaults to20.disabled(:boolean) - Defaults tofalse.format_price(:any) - Defaults tonil.
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 whencardsis true, used by the JS hook to persist view preference)markup_percentage— catalogue markup for:priceand:final_pricecolumns (required when either is listed; ignored otherwise)discount_percentage— catalogue discount for:discountand:final_pricecolumns (required when either is listed; ignored otherwise). The:discountcolumn honors per-item overrides viaItem.effective_discount/2.edit_path— 1-arity function(uuid -> path)to enable edit linkson_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 asphx-value-type(e.g."item")catalogue_path— 1-arity function(uuid -> path)for catalogue links in:cataloguecolumnvariant— 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 tofalse.show_toggle(:boolean) - Defaults totrue.id(:string) - Defaults tonil.storage_key(:string) - Defaults tonil.markup_percentage(:any) - Defaults tonil.discount_percentage(:any) - Defaults tonil.edit_path(:any) - Defaults tonil.on_delete(:string) - Defaults tonil.on_restore(:string) - Defaults tonil.on_permanent_delete(:string) - Defaults tonil.permanent_delete_type(:string) - Defaults to"item".catalogue_path(:any) - Defaults tonil.variant(:string) - Defaults to"default".size(:string) - Defaults to"sm".wrapper_class(:string) - Defaults tonil.
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>'sphx-change)remove_meta_field(per-row × button)- (text edits are absorbed by the form's
phx-change="validate"viaMetadata.absorb_params/2)
Attributes
resource_type—:itemor:catalogue; drives whichMetadata.definitions/1list is consumed for the add-picker and for legacy-key detectionstate— the%{attached: [key], values: %{key => string}}map produced byMetadata.build_state/2and kept on the socketid_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 tonil.description(:string) - Defaults tonil.
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 clickedon_toggle_category—%{"uuid" => uuid}when a category is clickedon_clear_catalogues— no params; clear all catalogue selectionson_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 (defaultfalse— 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 tofalse.class(:string) - Defaults to"".
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 translatedgettext("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 tonil.on_search(:string) - Defaults to"search".on_clear(:string) - Defaults to"clear_search".debounce(:integer) - Defaults to300.class(:string) - Defaults to"".
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 thancount, the summary shows "X of Y" so users know the list is paging. Omit or passnilfor a plain "N results" line.
Attributes
count(:integer) (required)query(:string) (required)loaded(:integer) - Defaults tonil.
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"".