PhoenixKitCatalogue.Web.Components (PhoenixKitCatalogue v0.1.14)

Copy Markdown View Source

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

catalogue_rules_picker(assigns)

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".
  • id (:string) - Defaults to "catalogue-rules-picker".
  • class (:string) - Defaults to "".

empty_state(assigns)

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

item_picker(assigns)

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 (:list) - Defaults to nil.
  • catalogue_uuids (:list) - 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.

item_table(assigns)

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 false.
  • 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.
  • 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.

metadata_editor(assigns)

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.

scope_selector(assigns)

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(assigns)

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(assigns)

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(assigns)

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