PhoenixKitCatalogue.Web.Components.Browse (PhoenixKitCatalogue v0.20.0)

Copy Markdown View Source

Embeddable, selection-agnostic building blocks for browsing catalogue items — the pieces ItemSelectorModal is assembled from, exposed so a host LiveView can compose its own browse surface (a storefront section, a picker, a read-only category wall) without copying markup.

Everything here is a pure function component: state in, events out. Each interactive component takes a target (phx-target) so it works inside a LiveComponent as well as straight in a LiveView — leave it nil and events go to the host LV. Event names are fixed (documented per component) so one handle_event/3 vocabulary serves every embedding.

The data these render is a presented item — a plain map produced by present_items/2, which resolves translations and the featured-photo URL once per fetch rather than on every render:

items
|> Browse.present_items(locale)
# => [%{uuid: "…", name: "…", sku: "…", price: %Decimal{}|nil,
#       unit: "piece", photo_url: "/…/medium/…"|nil,
#       manufacturer: "…"|nil, default_qty: %Decimal{1}}]

Pair them with PhoenixKitCatalogue.Catalogue.BrowseState for the fetch/paging state machine; the moduledoc there shows the loop.

Summary

Functions

Horizontally scrollable category filter chips: "All" plus one per category. Dispatches browse_category with phx-value-uuid ("" for All).

A columns-visibility dropdown: one checkbox row per TOGGLEABLE column, pushing event (default "toggle_column") with %{"col" => col} to target on each row click. Presentation only — the caller owns which columns are toggleable at all (its pre-approved set minus pinned ones) and what is currently visible. Focus-based dropdown, so several columns can be flipped before it closes on blur.

The default column set — selling price with inline unit, no raw base price.

Signed URL for an item's featured photo (medium variant), or nil. Signing is pure computation — no Storage roundtrip — so this is safe per item; it lives here so every surface resolves photos one way.

Formats a Decimal price for the card/tray. Bare number, no currency symbol — the same convention as the module's item table (format_price in Web.Components): which currency a price is in is host business the catalogue has never decided.

Placeholder cards with the exact geometry of item_card/1, so the grid does not reflow when real items arrive.

One product card: photo-forward (square, object-cover, lazy), then name / sku / price. Selection chrome is NOT built in — the picker layers it through the :footer slot and the selected ring, so a plain browse embedding renders the same card with neither.

The responsive card grid. Cards come in through the default slot so the caller decides what a card is — this component owns only the layout.

One selectable row for item_table. Every cell except :qty carries the same card_click toggle the card face uses — one event vocabulary, two views — while the :qty cell (the :qty slot, typically a qty_stepper) is deliberately not click-bound so stepping a quantity can never toggle the row underneath it.

The table twin of item_grid: an admin-look list for the same presented maps. Hosts configure which columns render (and their order) via columns — the popup is potentially client-facing, so nothing is shown that the host didn't ask for. Rows go inside via item_row/1 with the SAME columns value.

Denormalizes schema items into presented maps: translated name, signed featured-photo URL, selling price (Catalogue.item_pricing/1's final_price, matching ItemPicker), and a starting quantity of 1.

Quantity stepper: minus / text input / plus. The input commits on blur or Enter (qty_commit with %{"uuid" =>, "value" =>}) — never on keystroke, so typing "2." on the way to "2.5" is not fought. The buttons dispatch qty_dec / qty_inc immediately.

The legal item_table/item_row column atoms, in canonical order.

A server-driven segmented view toggle: one button per mode, the active one highlighted, pushing event (default "set_view") with %{"mode" => mode} to target. Presentation only — the caller owns the state and any persistence. (The admin pages' view_mode_toggle is the localStorage/client-side sibling; this one is for LiveComponents that hold their view in assigns.)

Functions

category_chips(assigns)

Horizontally scrollable category filter chips: "All" plus one per category. Dispatches browse_category with phx-value-uuid ("" for All).

Attributes

  • id (:string) (required)
  • categories (:list) (required) - [%{uuid:, name:}].
  • active_uuid (:any) - Defaults to nil.
  • target (:any) - Defaults to nil.
  • show_uncategorized (:boolean) - Adds an Uncategorized chip (value "uncategorized") after the category chips — for scopes where items without a category exist and the chips would otherwise never add up. Defaults to false.

column_toggle(assigns)

A columns-visibility dropdown: one checkbox row per TOGGLEABLE column, pushing event (default "toggle_column") with %{"col" => col} to target on each row click. Presentation only — the caller owns which columns are toggleable at all (its pre-approved set minus pinned ones) and what is currently visible. Focus-based dropdown, so several columns can be flipped before it closes on blur.

Attributes

  • id (:string) (required)
  • columns (:list) (required) - toggleable columns, display order.
  • visible (:list) (required)
  • event (:string) - Defaults to "toggle_column".
  • target (:any) - Defaults to nil.

default_table_columns()

The default column set — selling price with inline unit, no raw base price.

format_price(d)

Formats a Decimal price for the card/tray. Bare number, no currency symbol — the same convention as the module's item table (format_price in Web.Components): which currency a price is in is host business the catalogue has never decided.

grid_skeleton(assigns)

Placeholder cards with the exact geometry of item_card/1, so the grid does not reflow when real items arrive.

Attributes

  • id (:string) (required)
  • count (:integer) - Defaults to 8.

item_card(assigns)

One product card: photo-forward (square, object-cover, lazy), then name / sku / price. Selection chrome is NOT built in — the picker layers it through the :footer slot and the selected ring, so a plain browse embedding renders the same card with neither.

Dispatches card_click with phx-value-uuid when clickable.

Attributes

  • id (:string) (required)
  • item (:map) (required) - a presented item (see present_items/2).
  • selected (:boolean) - Defaults to false.
  • clickable (:boolean) - Defaults to true.
  • show_price (:boolean) - Defaults to true.
  • show_sku (:boolean) - Defaults to true.
  • target (:any) - Defaults to nil.

Slots

  • footer

item_grid(assigns)

The responsive card grid. Cards come in through the default slot so the caller decides what a card is — this component owns only the layout.

Attributes

  • id (:string) (required)
  • class (:string) - Defaults to nil.

Slots

  • inner_block (required)

item_row(assigns)

One selectable row for item_table. Every cell except :qty carries the same card_click toggle the card face uses — one event vocabulary, two views — while the :qty cell (the :qty slot, typically a qty_stepper) is deliberately not click-bound so stepping a quantity can never toggle the row underneath it.

Attributes

  • id (:string) (required)
  • item (:map) (required) - a presented item (see present_items/2).
  • columns (:list) (required) - the same list the item_table got.
  • selected (:boolean) - Defaults to false.
  • clickable (:boolean) - Defaults to true.
  • target (:any) - Defaults to nil.

Slots

  • qty - rendered in the :qty cell when that column is present.

item_table(assigns)

The table twin of item_grid: an admin-look list for the same presented maps. Hosts configure which columns render (and their order) via columns — the popup is potentially client-facing, so nothing is shown that the host didn't ask for. Rows go inside via item_row/1 with the SAME columns value.

Attributes

  • id (:string) (required)
  • columns (:list) (required) - subset of table_columns/0, display order.

Slots

  • inner_block (required)

present_items(items, locale)

Denormalizes schema items into presented maps: translated name, signed featured-photo URL, selling price (Catalogue.item_pricing/1's final_price, matching ItemPicker), and a starting quantity of 1.

Item.default_value is the smart-catalogue fee fallback (percent/flat), not a pick quantity — do not use it as a stepper default.

Runs once per fetched page — never call translation or URL helpers from a template; a quantity keystroke re-renders every card.

qty_stepper(assigns)

Quantity stepper: minus / text input / plus. The input commits on blur or Enter (qty_commit with %{"uuid" =>, "value" =>}) — never on keystroke, so typing "2." on the way to "2.5" is not fought. The buttons dispatch qty_dec / qty_inc immediately.

Integer mode is precision: 0 (the default); a decimal item is the same component with precision > 0 and a unit suffix — no redesign, which is the point. All limits are re-enforced server-side; these attrs only shape the keyboard.

Attributes

  • id (:string) (required)
  • uuid (:string) (required)
  • qty (:string) (required) - display string, already formatted.
  • unit (:string) - Defaults to nil.
  • precision (:integer) - Defaults to 0.
  • target (:any) - Defaults to nil.
  • size (:string) - Defaults to "sm". Must be one of "xs", or "sm".

table_columns()

The legal item_table/item_row column atoms, in canonical order.

view_toggle(assigns)

A server-driven segmented view toggle: one button per mode, the active one highlighted, pushing event (default "set_view") with %{"mode" => mode} to target. Presentation only — the caller owns the state and any persistence. (The admin pages' view_mode_toggle is the localStorage/client-side sibling; this one is for LiveComponents that hold their view in assigns.)

Attributes

  • id (:string) (required)
  • modes (:list) (required) - [%{mode:, icon:, label:}] in display order.
  • current (:string) (required)
  • event (:string) - Defaults to "set_view".
  • target (:any) - Defaults to nil.