# `PhoenixKitWeb.Components.MediaGallery`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v3.5.0/lib/phoenix_kit_web/components/media_gallery.ex#L1)

A general-purpose LiveComponent for selecting, ordering, previewing and removing
a set of images.

Generalizes the gallery + picker pattern so any PhoenixKit consumer can embed it
instead of re-implementing the plumbing. The component manages an ordered list of
file UUIDs and reports changes to the parent LiveView.

## Usage

    <.live_component
      module={PhoenixKitWeb.Components.MediaGallery}
      id="my-gallery"
      selected={@image_uuids}
      phoenix_kit_current_user={@current_user}
    />

    # Receive changes in parent LiveView
    def handle_info({PhoenixKitWeb.Components.MediaGallery, "my-gallery", {:changed, uuids}}, socket) do
      {:noreply, assign(socket, :image_uuids, uuids)}
    end

## Attrs

- `id` — required; used for element IDs and change notifications
- `title` — optional heading above the gallery
- `selected` — ordered list of file UUIDs (current selection); default `[]`
- `mode` — `:single` or `:multiple` (default `:multiple`)
- `cols` — grid columns for the thumbnail layout (default `4`). Either an
  integer 1..6, or a string of Tailwind grid-column classes for a responsive
  grid (e.g. `"grid-cols-4 lg:grid-cols-6 2xl:grid-cols-8"`). Plumbed straight
  through to `<.draggable_list>`.
- `featured_first` — when `true`, the first item in `:selected` renders a
  "Featured" badge in the bottom-left corner. Matches the
  `phoenix_kit_posts` post-creation convention where the first image is the
  featured one and drag-reordering changes the feature. Default `false` so
  existing consumers aren't surprised.
- `scope_folder_id` — folder scope passed to the picker
- `group_by_folder` — passed to the picker: `true` lists a scope that holds
  subfolders grouped by folder, under one heading each (see
  `MediaSelectorModal`); default `false`
- `folder_labels` — passed to the picker: `%{folder_uuid => name}` shown in
  its folder headings instead of the stored folder names; default `%{}`
- `picker_size` — the picker's `size`: `:default` or `:full` (the whole
  viewport, more files a page); default `:default`
- `phoenix_kit_current_user` — required for upload in the picker
- `readonly` — when `true`, hides the pick button, the tile menu, and DnD;
  preview (lightbox) still works; the server refuses pick, remove, trash and
  reorder events too; default `false`
- `removal_guard` — the module asked before "Move to trash" (see "Tile
  actions"); `nil` takes the configured one, `false` goes without; default `nil`
- `phoenix_kit_current_scope` — the scope of the person acting, handed to the
  removal guard; optional. A passed scope is used as given when the gallery
  acts, so the host keeps it current; without it one is built from
  `phoenix_kit_current_user` at the moment of each action
- `external_picker` — when `true`, the "Add" tile asks the host to pick
  instead of opening the built-in `MediaSelectorModal` (which is then never
  rendered); see "External picker" below. Default `false`
- `max_count` — integer upper bound on the number of selected images for
  `:multiple` mode; `nil` means unlimited. For `:single` mode the limit is
  always 1 (implied by `mode`). When the limit is reached, the "Add" tile is
  hidden entirely (not just disabled) and `apply_selection` refuses to exceed it.

## Tile actions

Each tile has the preview (eye) button in its top-left corner and a ⋮ menu in
its top-right corner. Both are shown on hover on a mouse screen and always on
a touch screen (`pointer-coarse`), and on keyboard focus. The menu holds:

- **Remove from selection** — takes the file out of the list (the file itself
  stays in the library) and reports `{:changed, uuids}`.
- **Move to trash** — sends the file to the media trash, and takes it out of
  the list the same way. Shown only to a person who may change that file
  (`Libraries.can?(scope, file, :edit)`: its uploader, an Owner/Admin, a
  `media.view_all` holder), and only for a file that is in the selection.

A readonly gallery shows the eye and no menu, and refuses both events.

### How "Move to trash" reuses the media browser's removal guard

The gallery does not copy the browser's removal flow and does not make the
host write one. It keeps its own small version of it, built from the same
parts the browser uses: `Storage.removal_plan/2` says whether the file would
disappear, `MediaBrowser.Removal` asks the host's
`PhoenixKit.Modules.Storage.RemovalGuard` module (the configured one, or the
`removal_guard` attribute) where the file is in use, and the browser's
`RemovalModal` draws the dialog inside the gallery, so it looks and reads
the same (and Escape closes it). The dialog always asks first, as the menu
sits next to the harmless "Remove from selection":

- not in use — "Move 1 file to trash?" with the name; the confirmation
  trashes the file;
- in use — the places are listed, nothing is removed, and when the guard
  offers a release (the "unpin everywhere" button) pressing it releases the
  file and asks again;
- the guard failed — "Could not check the files", nothing is removed.

The confirmation re-reads the file, the plan and the guard's answer and
trashes only what the dialog showed. On success the file is dropped from the
selection and `{:changed, uuids}` is sent, so the host stores the shorter
list as for any remove. The host needs no extra wiring for the dialog.

The host must **persist** that `{:changed, uuids}` (as for every change): a
trashed file is gone from the gallery's list, but the host's stored list
still names it until the host saves the shorter one. A guard that reads only
saved state does not see the gallery's current selection as a use, and an
abandoned form leaves a reference to a trashed file. Store the list when the
message arrives, or make the guard count the unsaved selection as a use.

## Change notifications — required host wiring (silent failure otherwise)

This is a `LiveComponent`, so it has no `handle_info` of its own: after
any change (pick / remove / reorder) it sends a **process message to the
host LiveView** via `send/2`:

    {PhoenixKitWeb.Components.MediaGallery, id, {:changed, ordered_uuids}}

The host MUST handle this (see the Usage example above) and persist
`ordered_uuids` — it is a *controlled* component: it renders whatever the
host passes back as `:selected`. Forget the handler and the user's
picks/reorders are silently dropped (no crash, no warning). Each host
stores the selection differently (its own field/assoc), so there is
intentionally no `use ...Embed` macro — the handling is yours to write.

## External picker

With `external_picker={true}` the host supplies its own picker. Clicking
"Add" sends the host LiveView (via `send/2`, like `{:changed, …}`):

    {PhoenixKitWeb.Components.MediaGallery, id,
     {:open_picker, %{selected: uuids, mode: :single | :multiple, max_count: integer | nil}}}

The host hands the choice back with `send_update/2`. `media_selected` is the
complete new selection, not an addition: it replaces `selected`, is limited
by `mode` and `max_count` the same way as the built-in picker's, and the
gallery then reports it as `{:changed, uuids}` like any other change. Not
answering (the person cancelled) leaves the gallery as it was. Thumbnails,
reordering, removal and the lightbox work as without the option; uploads
are the host picker's business. A `readonly` gallery shows no "Add" tile and
refuses `open_picker` on the server, so the host is never asked.

- The host is not asked while the "Add" tile is hidden either — the
  selection is at `max_count`, or a `:single` gallery already holds a file
  — and a forged `open_picker` then is refused on the server.
- Make the host's `handle_info` idempotent: every click sends a request, so
  a double click delivers two `{:open_picker, …}` messages.
- Pass `media_selected` uuids without repeats; the gallery does not
  deduplicate the list.
- Return image files only. The built-in picker is locked to images; the
  gallery does not filter what `media_selected` names, and a PDF or a video
  renders as a broken thumbnail.
- Build the reply from the selection the host holds *now*, not from
  `request.selected`: that is a snapshot taken at the click, and a remove or
  reorder made while a non-modal host picker was open would be reverted by
  a reply built from it.

    <.live_component
      module={PhoenixKitWeb.Components.MediaGallery}
      id="my-gallery"
      selected={@image_uuids}
      external_picker={true}
    />

    def handle_info(
          {PhoenixKitWeb.Components.MediaGallery, "my-gallery", {:open_picker, request}},
          socket
        ) do
      # Open the host's picker; `request.selected` is the selection at the click.
      {:noreply, assign(socket, :picker_request, request)}
    end

    # Later, when the person confirms in the host's picker:
    send_update(PhoenixKitWeb.Components.MediaGallery, id: "my-gallery", media_selected: uuids)

## Reorder event contract

The thumbnail grid uses the canonical `<.draggable_list>` primitive
(`PhoenixKitWeb.Components.Core.DraggableList`), which fires the
`"reorder_images"` event with payload `%{"ordered_ids" => uuids}`.

Because `MediaGallery` is a LiveComponent, the grid is rendered with
`target={"##{@id}"}` so the `SortableGrid` hook routes the event via
`pushEventTo` to this component's own `handle_event/3` — not the host
LiveView. Each gallery's `id` is unique, so multiple galleries on the same
page never cross-deliver reorder events.

# `render`

---

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