# `MishkaGervaz.Table.Templates.Shared`
[🔗](https://github.com/mishka-group/mishka_gervaz/blob/v0.0.5/lib/mishka_gervaz/table/templates/shared.ex#L1)

Shared rendering functions used by all templates.

These provide default implementations for common UI elements like
filters, bulk actions, pagination, and template switcher.

## Assigns

All functions expect two key assigns:
- `@static` - Same reference always (columns, filters, ui_adapter, etc.)
- `@state` - Changes trigger re-render (page, filter_values, etc.)

See `MishkaGervaz.Table.Behaviours.Template`,
`MishkaGervaz.Table.Templates.Table`,
`MishkaGervaz.Table.Templates.MediaGallery`, and
`MishkaGervaz.Table.Web.Renderer`.

# `accessible_group?`

Checks if a filter group is visible and accessible for the current state.

# `action_visible?`

Checks if a row action is visible for a given record and state.

Used by templates to filter visible row actions.

# `assign_card_flags`

```elixir
@spec assign_card_flags(map()) :: map()
```

The five "is this part on?" flags a card template's `render/1` opens with.

Each is a feature flag crossed with whether there is anything to show — `:paginate` alone does not
mean a pager, it means a pager if the count is known and non-zero.

Assigns `show_checkboxes`, `show_filters`, `show_pagination`, `show_bulk_actions` and
`show_switcher`.

# `build_active_filter_chips`

```elixir
@spec build_active_filter_chips(map(), map()) :: {[map()], boolean()}
```

Builds active filter chip data for custom templates.

Returns `{visible_chips, has_any_active_filters}` where:
- `visible_chips` excludes `visible: false` filters (hidden from UI but still active in state)
- `has_any_active_filters` is true if any filter has a value (including hidden ones)

Use `has_any_active_filters` for the "Clear all" button visibility so users can always
clear hidden filters (e.g., `id` filter from URL).

# `bulk_selection_count`

```elixir
@spec bulk_selection_count(map()) :: non_neg_integer() | nil
```

The number of rows a bulk action runs on, or `nil` when the table cannot tell.

Checked rows the table's read still finds are counted; the ones it hides, such as rows a search
left out (`hidden_ids`), are not, since the action reaches only rows the read finds. With select
all, the count is the table's total minus the unchecked rows, and the table can tell only when it
holds every row or counts its rows (`:numbered` pagination, or `show_total true` in the pagination
`ui` block).

# `check_all_gallery`

```elixir
@spec check_all_gallery(Phoenix.LiveView.JS.t(), String.t() | nil) ::
  Phoenix.LiveView.JS.t()
```

Checks all media gallery checkboxes.

# `check_all_table`

```elixir
@spec check_all_table(Phoenix.LiveView.JS.t(), String.t() | nil) ::
  Phoenix.LiveView.JS.t()
```

Checks all table row checkboxes and adds selection highlighting.

# `custom_row_class`

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

The extra classes the resource asks this record's row to wear, if any.

`row do class do apply … end end` is where a resource says which records look different.
Callers compose the result — selection state usually has to win over it.

# `expandable?`

```elixir
@spec expandable?([map()], map(), map()) :: boolean()
```

Whether this row expands: one of the table's `:accordion` actions is visible for the record, as
`action_visible?/3` answers for every row action, and is not `restricted` from the reader. A
template draws the row's caret, and its expanded details, only then.

# `expandable_rows?`

```elixir
@spec expandable_rows?([map()], map()) :: boolean()
```

Whether some row of the table may expand for the reader: an `:accordion` action that is not
`restricted` from them and whose `visible` is not `false`, nor the other archive view's
(`:active` / `:archived`). A `visible fn record, state` is asked per row, by `expandable?/3`.

A template draws its caret column when this is true, so a row without a caret keeps the
table's columns.

# `format_chip_value`

```elixir
@spec format_chip_value(term()) :: String.t()
```

Format a filter value for display in a chip.

# `grid_cols`

Returns the Tailwind grid-cols class for a given column count.

# `group_columns`

```elixir
@spec group_columns(map()) :: pos_integer() | nil
```

Returns the column count from a filter group's UI config, or nil if not set.
Pass the result to `grid_cols/1` for the Tailwind class.

# `group_icon`

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

Returns the icon name for a filter group, falling back to "hero-funnel".
Useful for custom templates rendering their own filter group UI.

# `group_label`

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

Returns the resolved label for a filter group, falling back to humanized group name.
Useful for custom templates rendering their own filter group UI.

# `group_panel_class`

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

Returns the CSS class for a collapsible filter group panel wrapper.
Falls back to a default rounded panel style if not set in DSL.

# `has_chip_value?`

```elixir
@spec has_chip_value?(term()) :: boolean()
```

Check if a filter value is meaningful (non-nil, non-empty).

# `has_user_visible_actions?`

```elixir
@spec has_user_visible_actions?(list(), list() | nil, map()) :: boolean()
```

Checks if any row actions or dropdowns are potentially visible for the current user.

Used by templates to decide whether to render the Actions column header.
Does not evaluate per-record visibility functions — only checks `restricted` and
boolean/atom `visible` values.

# `has_visible_bulk_actions?`

```elixir
@spec has_visible_bulk_actions?(list() | term(), atom()) :: boolean()
```

Check if any bulk actions are visible for the given archive status.

Returns true if there is at least one bulk action that should be shown
for the current mode (active or archived).

# `inline_filter_class`

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

Returns a CSS class for inline (non-collapsible) filters based on filter type.
Text filters get a flexible width, relation filters a minimum width, others a smaller minimum.
Only used when the group has no custom class (i.e., wrapper is `"contents"`).
Custom templates can override this if they need different sizing.

# `inline_group_class`

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

Returns the CSS class for an inline (non-collapsible) group wrapper.
If the group has `ui do class "..." end`, uses that. Otherwise defaults to
`"contents"` which makes the wrapper transparent so filters participate
directly in the parent flex layout.

# `merge_relation_filter_state`

```elixir
@spec merge_relation_filter_state(list(), map()) :: list()
```

Merges dynamic relation filter state (options, loading, etc.) into filter configs.

# `non_accordion_actions`

```elixir
@spec non_accordion_actions([map()]) :: [map()]
```

The row actions a template should draw as buttons — everything except an `:accordion`.

An `:accordion` action is a declaration that the row expands, not a button: a template renders the
caret itself, wherever its design puts it.

# `pagination_type`

```elixir
@spec pagination_type(map()) :: atom()
```

Which pager a resource asked for — numbered pages, a Load-more button, or infinite scroll.

`render_pagination/1` reads this itself when the caller has not already assigned it, so a template
only passes `pagination_type` when it wants to override what the resource declared.

# `prepare_filter_groups`

```elixir
@spec prepare_filter_groups(list(), list(), map()) :: map()
```

Prepares all filter groups for rendering by custom templates.

Resolves accessible filters, splits them into visible groups (sorted by position),
and identifies ungrouped filters. Each group entry includes the resolved filter
structs (not just names).

Returns a map:

    %{
      all_filters: [filter_struct],
      visible_groups: [%{group | resolved_filters: [filter_struct]}],
      ungrouped_filters: [filter_struct],
      inline_groups: [group],      # non-collapsible
      collapsible_groups: [group]   # collapsible
    }

# `rail_class`

```elixir
@spec rail_class(list() | nil, :grid | :top | :rail | :bottom) :: String.t() | nil
```

The four class strings that put a page's `:rail` slot beside its records rather than above them.

Returns `nil` for every region when the page passed no rail, and HEEx omits a `nil` attribute
entirely, so a template can spread these across its markup unconditionally.

# `range_label`

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

Where the reader is in a `:load_more` or `:infinite` list: `Showing 1–40 of 136`, from the first
row shown to the last, of the table's total. A list opened at page 3 of 20-row pages starts at 41
until Load earlier brings the pages before it. The last row is `state.next_offset`, the end of the
rows the view shows, so a row the view took out is not counted. A page past the last reads
`Showing 0 of 136`.

`nil` when the table keeps no count (`show_total false`) or holds no rows.

# `record_checked?`

```elixir
@spec record_checked?(map(), term()) :: boolean()
```

Whether this record's tick is on, under either selection mode.

Normally `selected_ids` lists what is ticked; once "select all" is on it is `excluded_ids` that
lists what has been un-ticked, so the same tick reads from a different set and with the opposite
sense.

# `render_bulk_actions`

Renders the bulk-action bar shown once rows are selected.

Only the actions visible in the current archive scope are drawn — see
`has_visible_bulk_actions?/2` to decide whether to give it any room at all.

    <div :if={@show_bulk_actions}>{Shared.render_bulk_actions(assigns)}</div>

# `render_cell`

Renders one cell the way the plain table does — `format` first, then the column's `render`
if it has one, then its type module, then the bare value.

A custom template should call this rather than reading the value itself, or a column's
`render` and `ui do type … end` stop working inside it.

    <td :for={column <- @columns}>{Shared.render_cell(%{column: column, record: @record, static: @static, state: @state})}</td>

# `render_empty_state`

```elixir
@spec render_empty_state(map()) :: Phoenix.LiveView.Rendered.t()
```

Renders the empty state with configurable message, icon, and action.

On a page past the table's last one (`MishkaGervaz.Table.Web.Renderer.past_last_page?/1`) it
says the page has no rows and its action is a button that opens page 1, in place of the
configured message and action.

# `render_error_state`

```elixir
@spec render_error_state(map()) :: Phoenix.LiveView.Rendered.t()
```

Renders the error state with configurable message, icon, and retry button.

# `render_filter`

One filter's input, resolved from its type — text, select, boolean, relation, date range — including
the disabled state a `depends_on` filter wears until its parent has a value.

Call it from a custom template that lays the filters out itself, the same way `render_cell/1` and
`render_row_actions/1` are called.

Expects `:filter`, `:all_filters`, `:state`, `:static` and `:myself`.

# `render_filters`

Renders the filter bar: the toolbar row, any collapsible filter groups, the active-filter
chips and the archive toggle.

The default `render_filters/1` of every template built with
`use MishkaGervaz.Table.Behaviours.Template`. Call it from a custom template rather than
rebuilding the bar.

    def render_filters(assigns), do: Shared.render_filters(assigns)

Expects `@static` and `@state`.

# `render_header`

```elixir
@spec render_header(map()) :: Phoenix.LiveView.Rendered.t()
```

No header at all — the default for a template whose page already names itself.

Returns an empty template.

# `render_load_earlier`

```elixir
@spec render_load_earlier(map()) :: Phoenix.LiveView.Rendered.t()
```

Renders the Load earlier button that goes above the rows of a `:load_more` or `:infinite` table
opened past its first page, as a reload after Load more opens it. A click sends `"load_earlier"`,
which reads the page before the first one shown and puts its rows above them, one page a click,
until the first page. Nothing is drawn while a load runs, nor on a table that shows its first
page.

    <Shared.render_load_earlier static={@static} state={@state} myself={@myself} />

# `render_loading`

```elixir
@spec render_loading(map()) :: Phoenix.LiveView.Rendered.t()
```

Renders the loading state with spinner and configurable text.

# `render_pagination`

Renders the pagination control — numbered pages, a load-more button or the infinite-scroll
sentinel, whichever the resource configured.

Numbered, `:load_more` and `:infinite` tables share one row, `render_pagination_row/1`: the page
size selector first, the page buttons or Load More in the middle, and last the page info of a
numbered table or the `range_label/1` of a growing list.

Reads its labels from `static.pagination_ui`, so `pagination do ui do … end end` on the
resource is honoured without the template knowing about it.

# `render_pagination_row`

```elixir
@spec render_pagination_row(map()) :: Phoenix.LiveView.Rendered.t()
```

Lays out the row under a table's rows in three parts on one line: `leading` at the start,
`center` in the middle and `trailing` at the end. The middle part stays in the middle of the row
whatever the widths of the other two, and wraps its buttons when the row is too narrow for them,
while the last part keeps its words on one line. Below 640px the parts stack, each centred, so
the row never scrolls sideways. A part left out keeps its place on a wide screen and takes no
line on a narrow one.

`render_pagination/1` draws every pager with it: the page size selector first, Load More or the
page buttons in the middle, and last where the reader is (`range_label/1` on a `:load_more` or
`:infinite` table, the page info on a numbered one). A template that draws its own pager lays it
out with this to look like every other table.

    <Shared.render_pagination_row>
      <:leading>…</:leading>
      <:center>…</:center>
      <:trailing>{Shared.range_label(@state)}</:trailing>
    </Shared.render_pagination_row>

## Slots

* `leading` - the first part, such as the page size selector.
* `center` - the middle part, such as Load More or the page buttons.
* `trailing` - the last part, such as where the reader is in the list.

# `render_row_actions`

Renders one row's action strip: the inline buttons, then any dropdown.

Honours `actions_layout` placement and each action's `visible` rule, and leaves
`:accordion` actions out — the caret is the table's own, not a button. Expects `@record`,
`@static`, `@state` and `@myself`.

# `render_template_switcher`

Renders the Table/Cards switcher for a resource that declared `switchable_templates`.

Reads `@ui_adapter` when given and falls back to the Tailwind adapter, so it can be called
from a context that has not resolved one.

# `resolve_chip_value`

```elixir
@spec resolve_chip_value(atom(), term(), map()) :: String.t()
```

Resolve a filter chip's display value, using relation labels when available.

# `sort_by_position`

Sorts filter groups by their position attribute.

# `uncheck_all`

```elixir
@spec uncheck_all(Phoenix.LiveView.JS.t(), String.t() | nil) ::
  Phoenix.LiveView.JS.t()
```

Unchecks all selection checkboxes and removes selection highlighting.

---

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