# `MishkaCmsCoreWeb.Templates.CardChrome`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core_web/templates/_card_chrome.ex#L5)

The chrome every document card page wears: the control row, the count over the list, the loading
veils and the pagination underneath.

`MishkaDocumentWeb.Templates.IssueCard`, `MishkaDocumentWeb.Templates.DocumentSection` and
`MishkaBlogWeb.Templates.CollectionCard` draw the same
furniture around different cards — only `render_item/1` really differs between them — so the furniture
lives here once rather than being copied per template.

Most of what is here are the pieces, and a template that lays its page out differently takes only the
pieces it wants. But the six blog card pages had assembled those pieces into byte-identical pages —
six copies of the same ninety lines differing in a wrapper class, two empty labels and an icon — so
`card_scaffold/1` is the assembled page as well, for the templates whose layout genuinely is the
common one. A template with its own layout (`MishkaDocumentWeb.Templates.DocumentCard`) still builds
it from the pieces.

Everything here is a plain `Phoenix.Component`, so a template calls it as
`<Shared.filter_bar … />`. The events (`filter`, `clear_filters`, `archive_filter`,
`change_page_size`, `prev_page` / `next_page` / `go_to_page`, `toggle_select_all`) and the input names
are Gervaz's contract — only the look is ours.

See `MishkaGervaz.Table.Templates.Shared`, which this leans on for the parts that stay generic
(`render_filter/1`, `render_bulk_actions/1`, relation filter state), and
`MishkaCmsCoreWeb.UIAdapters.Admin`, which draws the archive switch and the row actions.

# `card_scaffold`

A whole card page: the count, the order and the view switch, the filter row, the bulk bar, the stream
and its pager.

This is the layout the blog card pages share — a list or a grid of records with the same furniture
around it. What differs between them is only what a card is called and how its records are laid out,
so those arrive as classes and labels rather than as six copies of this markup.

The records themselves stay with the caller, in the `:records` slot: they are that template's own
`render_item/1` over the stream, and the stream loop has to be written where `@stream` is. This only
supplies the `phx-update="stream"` container it belongs in, and the empty state that shares it.

Pass the five `show_*` flags from `MishkaGervaz.Table.Templates.Shared.assign_card_flags/1`.

The caller keeps the page's outermost `<div>` rather than passing its class in: a template's
`render/1` is the root of a stateful LiveComponent, and LiveView requires that root to be a literal
HTML tag — a component call there raises "Stateful components must have a single static HTML tag at
the root".

## Attributes

* `static` (`:map`) (required)
* `state` (`:map`) (required)
* `myself` (`:any`) (required)
* `container_class` (`:string`) (required) - the stream container: a column, or a grid.
* `data_role` (`:string`) (required) - test/debug handle on the stream container.
* `empty_label` (`:string`) (required) - the count's zero form, e.g. `0 posts`.
* `empty_icon` (`:string`) (required)
* `empty_class` (`:string`) - `col-span-full` when the container is a grid. Defaults to `nil`.
* `show_checkboxes` (`:boolean`) (required)
* `show_filters` (`:boolean`) (required)
* `show_pagination` (`:boolean`) (required)
* `show_bulk_actions` (`:boolean`) (required)
* `show_switcher` (`:boolean`) (required)
* `before_table` (`:any`) - optional slot the page puts above the filter row. Defaults to `nil`.
## Slots

* `records` (required)

# `collapse_editions`

```elixir
@spec collapse_editions(String.t()) :: Phoenix.LiveView.JS.t()
```

Hides a card's extra edition chips again — the exact inverse of `expand_editions/1`.

# `edition_title`

```elixir
@spec edition_title(map()) :: String.t()
```

The tooltip on an edition chip — "Open the FA edition".

Every card that lists sibling editions words this the same way, so the string is translated once.

# `empty_state`

What a stream shows when it holds nothing.

Muted by class rather than dropped by `:if` — a stream container keeps children the server stops
rendering, and `hidden only:flex` is what makes this appear exactly while it is the sole child.

## Attributes

* `id` (`:string`) (required)
* `static` (`:map`) (required)
* `icon` (`:string`) - Defaults to `"hero-document"`.
* `class` (`:string`) - Defaults to `nil`.

# `expand_editions`

```elixir
@spec expand_editions(String.t()) :: Phoenix.LiveView.JS.t()
```

Reveals a card's remaining edition chips.

Opening the row is a client-side class flip, not a round trip: the chips are already rendered, and
a stream row's expanded state must not depend on the server remembering it. Every id is scoped by
the row's own DOM id so one card's toggle cannot reach another's — the card supplies that id and
must name its three elements `<id>-editions-rest`, `-more` and `-less`.

`hidden` alone cannot hide a button that also carries `inline-flex` — both set `display`, and which
one wins is down to their order in the stylesheet, not the order they were added. So each toggle
takes the display class off as well as putting `hidden` on.

# `filter_bar`

The control row — search, the archive switch, an Advanced panel holding every other filter — plus
whatever the page wants on either side of it.

Nothing here names a filter: the one `:text` filter becomes the search box and the rest fall into the
panel, so the resource stays the single source of truth. `accessible?/2` is what keeps a `restricted`
filter (a site or contributor picker) away from a tenant and a `visible false` one (a deep-link id)
out of the panel entirely.

The archive switch draws its own `archive_filter` form, so it stays a **sibling** of the filter form
rather than a child — a nested form is dropped by the parser, and its `status` radio would then be
serialized into `filter`, where it collides with a status filter of the same name and narrows every
search to nothing. The filter form is `display: contents`, so this row lays its children out and
`order-*` puts them back in the design's sequence.

## Slots
- `:leading` — sits before the search box (a primary action such as "Add Section")
- `:trailing` — sits after the Advanced button (a view switcher)

## Attributes

* `static` (`:map`) (required)
* `state` (`:map`) (required)
* `myself` (`:any`) (required)
* `search_placeholder` (`:string`) - Defaults to `nil`.
## Slots

* `leading`
* `trailing`

# `filter_grid`

Every filter laid out at once, each under its own label, with the switches and the count beneath.

The alternative to `filter_bar/1`: a page with only a handful of filters shows them all rather than
hiding them behind a drawer. `:boolean` filters drop out of the grid and join the Select-all line as
switches, which is where a "Featured only" reads naturally.

All of it — including Select all — sits in **one** `phx-change="filter"` form, because Gervaz rebuilds
the whole filter set from the submitted params on every change: a filter left out of the form is a
filter cleared. The archive switch stays outside, since it draws a form of its own and a nested form
is dropped by the parser.

## Attributes

* `static` (`:map`) (required)
* `state` (`:map`) (required)
* `myself` (`:any`) (required)
* `show_checkboxes` (`:boolean`) (required)
* `empty_label` (`:string`) - Defaults to `nil`.

# `loading`

The spinner shown while the first page is still on its way.
## Attributes

* `text` (`:string`) - Defaults to `nil`.

# `loading_overlay`

A veil over the list while it reloads — the cards stay readable underneath, so the spinner rides on
the page rather than on a panel of its own.

## Attributes

* `text` (`:string`) - Defaults to `nil`.

# `modal_body`

The scrolling body of an admin modal, capped so the head and footer stay put on a short screen.

Its scrollbar is hidden, as every scrolling surface in this admin is.

## Attributes

* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `modal_footer`

The footer of an admin modal — a ruled band under the body for counts and controls.
## Attributes

* `class` (`:string`) - Defaults to `nil`.
## Slots

* `inner_block` (required)

# `modal_head`

The head of an admin modal: an icon tile, a title, and the thing the modal is about beneath it.

Every modal on these pages wears the same head — version history, an edition's details, an edit form —
so they read as one surface with different contents rather than three unrelated dialogs. Pair it with
`MishkaCmsCore.CoreComponents.modal/1` given `padding_class="p-0"`, which is what lets the head sit
edge to edge above a scrolling body.

The right padding leaves room for the modal's own close button; anything in the `:actions` slot sits
to the left of it. When the icon, title and subtitle need the row, the actions move to a row of
their own beneath them, wrapping again if they still do not fit.

## Attributes

* `icon` (`:string`) (required)
* `title` (`:string`) (required)
* `subtitle` (`:string`) - Defaults to `nil`.
* `tone` (`:string`) - Defaults to `"bg-[#f2f1fc] text-[#5b57d6]"`.
## Slots

* `actions`

# `page_window`

```elixir
@spec page_window(pos_integer(), pos_integer()) :: [pos_integer() | :gap]
```

Which page buttons to draw: all of them while they fit, otherwise the ends, a window around where you
are, and an ellipsis for the rest.

# `pagination`

What sits under the list: numbered pages, or a Load More button over the page-size picker.

Every event and figure here is Gervaz's — only the look is ours. A table that asked for `:infinite`
keeps the shared pagination, which is the only thing that knows how to draw it.

## Attributes

* `static` (`:map`) (required)
* `state` (`:map`) (required)
* `myself` (`:any`) (required)

# `range_label`

```elixir
@spec range_label(map(), pos_integer(), String.t() | nil) :: String.t()
```

Which slice of the result set is under your eyes — `Showing 1–20 of 48`.

The last page holds a remainder rather than a full page, so the upper bound is clamped by the total
rather than by where the page would end.

# `select_row`

The Select-all checkbox on the left, the count on the right, immediately over the list.

The checkbox keeps the `gervaz-select-all-checkbox` class because that is what
`MishkaGervaz.Table.Templates.Shared.uncheck_all/2` and `check_all_gallery/2` target — without it the
optimistic toggle silently no-ops and the box stays checked for a round trip after "Clear selection".

## Attributes

* `static` (`:map`) (required)
* `state` (`:map`) (required)
* `myself` (`:any`) (required)
* `show_checkboxes` (`:boolean`) (required)
* `empty_label` (`:string`) - Defaults to `nil`.

# `toggle_all_js`

```elixir
@spec toggle_all_js(boolean()) :: Phoenix.LiveView.JS.t()
```

What the select-all tick does when clicked, given whether it is currently on.

The event and the DOM flip travel together: the server is told to invert the selection, and the
same click sets or clears every checkbox on the page without waiting for the round trip. Public
because a card template that draws its own toolbar still needs this exact pair.

# `total_label`

```elixir
@spec total_label(map(), String.t() | nil) :: String.t()
```

How many records the table holds in total — the `24 items` over the grid.

Unlike `range_label/3` this counts the whole result set rather than the page, because the grid's own
count sits beside the view switch where the page is not yet the subject.

---

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