MishkaCmsCoreWeb.Templates.CardChrome (Mishka CMS Core v0.0.1)

Copy Markdown View Source

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.

Summary

Functions

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

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

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

What a stream shows when it holds nothing.

Reveals a card's remaining edition chips.

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

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

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

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.

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

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

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

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.

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

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

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

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

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

Functions

card_scaffold(assigns)

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

@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(arg)

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

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

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

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

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

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

Attributes

  • text (:string) - Defaults to nil.

loading_overlay(assigns)

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.

page_window(page, total)

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

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(state, page_size, empty_label \\ nil)

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

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(select_all?)

@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(arg1, empty)

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