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

Catalogue item selector modal: the catalogue's analogue of core's
`MediaSelectorModal`. A logged-in user browses the catalogue inside a
modal — search, category chips, photo-forward card grid — picks items,
sets a quantity per item, reviews the selection in a tray, and confirms.

## Usage

    # In the parent LiveView's template. Mount it with :if — unmounting
    # on close is what gives clean reopen semantics (fresh search, fresh
    # scroll, preselects re-read).
    <.live_component
      :if={@show_item_selector}
      module={PhoenixKitCatalogue.Web.Components.ItemSelectorModal}
      id="order-item-selector"
      scope={%{catalogue_uuids: [@catalogue.uuid]}}
      selected={@order_lines_by_uuid}
    />

## Required host wiring (do not skip — silent failure otherwise)

This is a `LiveComponent`; it reports through process messages to the
host LiveView, exactly like `MediaSelectorModal`. The host MUST handle
both, or a confirmed selection is silently dropped:

  * `handle_info({:items_selected, %{id: id, mode: mode, picks: picks}}, socket)`
    — fired on Confirm. Each pick is a map with `:uuid`, `:qty`
    (**always** a `Decimal`, integers included — hosts write one clause),
    `:unit`, and a display snapshot: `:name`, `:sku`, `:price`,
    `:line_total` (`price × qty`, nil when the item has no price) and
    `:photo_url` (a signed URL — it expires, render it, never persist
    it). The snapshot is for rendering the host's own summary without a
    re-query; it is NOT an order record — re-read and re-price items
    server-side when the selection becomes something real.
  * `handle_info({:item_selector_closed, %{id: id}}, socket)` — fired on
    cancel/ESC/backdrop, AND after a confirm. Reset the `:if` assign
    here.

## Scope

`scope` fixes what the user may browse: any of `:catalogue_uuids`,
`:category_uuids`, `:only`, `:statuses`, `:include_descendants` (the
`Catalogue.search_items/2` vocabulary). It is enforced in `BrowseState`
— every fetch re-derives from it, and client events can only narrow
within it, so a crafted event cannot browse or select outside what the
host allowed. Selection events are additionally accepted only for uuids
the component itself has rendered (or that arrived preselected).

## Preselection

`selected` is `%{uuid => qty}` (string uuids, `Decimal` or integer
quantities). Preselected items are hydrated at mount ignoring scope —
the tray must be able to render what the host handed it. A hydrated item
that falls OUTSIDE the scope renders in the tray marked unavailable and
is excluded from the confirm picks, never silently dropped.

## Quantities

`qty_precision: 0` (default) is whole numbers; a positive precision
turns the same stepper decimal-capable ("2.5" of unit "L"). The input
commits on blur/Enter; decimal commas are accepted ("2,5" — ru/et
keyboards); all limits re-clamped server-side.

---

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