<!-- 04_VISUAL/xwui/xwui-lib/xwui-lib-super/src/layout/XWUIFilterableGrid/XWUIFilterableGrid.md -->
# XWUIFilterableGrid

A card grid filtered by a search field and one or more facet groups. Every group shows faceted counts (what picking an
option would show), the grid shows a live "{shown} of {total}", an empty state with Clear, a sort choice, an open
control per card and, on narrow containers, the facets in a bottom sheet. The query, the facets, the sort and the open
item can live in the page URL. Each item keeps ONE keyed cell for its whole life: a filter hides it, a sort moves it, so
a live card mounted with `mountItem` keeps its state. Facet groups are live XWUIChoiceGroup (C04) children.
Spec: plnx `REF_06_GAPS.md` C06 and 2.3 (promotion).

## Basic Usage

```example
file: examples/BasicUsage.ts
html: examples/BasicUsage.html
title: Basic Usage
description: Six systems, a search field and one category group whose options name their items; each card is live content mounted with mountItem.
```

```example
file: examples/IntegrationsDirectory.ts
html: examples/IntegrationsDirectory.html
title: Integrations directory
description: Category and direction facets at the inline start with faceted counts, search, sort, URL state and a card that opens a drawer.
```

```example
file: examples/TemplateGallery.ts
html: examples/TemplateGallery.html
title: Template gallery
description: One row of category chips above a four-column grid, no search, sort by name or size.
```

## Data

| Field | Type | Meaning |
| --- | --- | --- |
| `items[]` | `XWUIFilterableGridItem` | The cards, in data order (see below). Blank and repeated ids are dropped. |
| `facets[]` | `XWUIFilterableGridFacet` | `{ id, label, kind?: 'single' \| 'radio' \| 'multi', options[] }`. |
| `query` | `string` | The search text (at most 200 characters). |
| `selection` | `Record<facetId, id \| ids>` | Single and radio an option id, multi a list. Unknown and disabled ids constrain nothing. |
| `sort` | `string` | A `config.sorts` id. Unset: the first sort. |
| `openId` | `string \| null` | The open item (its cell carries `aria-current` and a ring). |
| `header` | view tree | Optional projected header (sanitised); or `config.headerSlot` with `mountHeader`. |
| `status`, `message` | `'ready' \| 'loading' \| 'error'` | Skeleton cards with `aria-busy`, or an alert with the message. |

`XWUIFilterableGridItem`: `id`, `label?` (the accessible name of the open control; no label, no open control),
`searchText?` (what the search matches; unset: the label), `facets?: Record<facetId, optionId | optionId[]>`,
`sortKeys?: Record<sortId, string | number>`, `href?` (the open control becomes a link; http, https, mailto and
relative only), `content?` (a pure view tree such as a thumbnail; live content goes through `mountItem`).

`XWUIFilterableGridOption`: `id`, `label`, `hue?` (a NAME: `teal` resolves to `var(--xwui-hue-teal)`), `itemIds?`
(explicit membership; wins over the items' `facets`; `[]` is a valid zero-result option), `all?` (the group's "All": picking
it removes the constraint), `disabled?`. Option ids starting with `__` are reserved.

Rules: a single group is pressed buttons with at most one on (a second press, or "All", clears it); a radio group always
has one on (its `all` option stands for "no constraint"); a multi group ORs its options. Groups AND together. The search
folds case, Latin diacritics, Arabic short vowels, tatweel, the alef, yeh and teh marbuta forms and Arabic-Indic digits,
and every word must match.

Pure content and the projected header are data, so they pass a sanitiser: no `html`, events, refs, props, host-owned
subtrees, `on*` or `style` attributes, no script-capable element, links only through the safe-link rule.

## Options

| Option | Default | What it does |
| --- | --- | --- |
| `minChildWidth` | `'17rem'` | Narrowest card before the grid drops a column (a CSS length or `var(--token)`). |
| `columns` | as many as fit | Most columns (1..12). |
| `gap` | `var(--spacing-md)` | Gap between cards. |
| `facetsAt` | `'top'` | `start` (a column at the inline start), `top` (rows above the grid), `sheet` (always a bottom sheet). |
| `sheetBelow` | `640` | Container width (CSS px) below which the facets move into the sheet; `0` never. Measured on the container, not the viewport. |
| `search` | `true` | The search field. |
| `sorts` | none | `{ id, label, dir? }[]`; a select shows when there are two or more. |
| `showCounts` | `true` | Faceted counts in each option's pill; an option that would show nothing is dimmed, never disabled. |
| `summaryTemplate` | `xwui.filterable_grid.summary` | `{shown}` and `{total}`; `setSummaryFormatter` wins for plural rules. |
| `emptyText`, `clearLabel` | locale pack | Empty-state text and the Clear button. |
| `urlState` | none | `{ query, facets: { facetId: param }, sort?, open? }`: parameter names mirrored with `history.replaceState`. |
| `ariaLabel` | none | Names the whole region (a `section`). |
| `headerSlot` | `false` | Reserve a host-owned header slot for `mountHeader`. |
| `skeletonCount` | `6` | Placeholder cards while loading. |
| `labels` | locale pack | `search`, `sort`, `filters`, `active` (`{n}`), `showResults` (`{shown}`), `close`, `loading`, `error`, `results`. |
| `locale`, `numberingSystem` | page, `'latn'` | Counts and sorting; Western digits unless asked. |

## Events and methods

- Events are user actions only; a host write (`setData`, `updateData`, the URL read on start) never makes one.
  - `filterChange { query, selection, visibleIds, shown, total }`: the query, a group, or Clear changed.
  - `itemOpen { id }`: an open control was used (reported every time, so a closed drawer can reopen).
  - `sortChange { id }`.
  Each also bubbles from the host as a DOM `CustomEvent` of the same name, and the URL follows it.
- `onFilterChange(fn)`, `onItemOpen(fn)`, `onSortChange(fn)`; `setQuery(text)`, `pressOption(facetId, optionId)`,
  `setFacet(facetId, value)`, `clear()` (keeps the sort), `setSort(id)`, `open(id)`, `closeItem()` (silent),
  `openSheet()`, `closeSheet()`, `setNarrow(flag)`, `setSummaryFormatter(fn)`, `getVisibleIds()`, `updateData(patch)`,
  `updateConfig(patch)`, `getElement()`, `destroy()`.
- `mountItem(id, (slot, factory) => child)` and `mountHeader(...)` return an idempotent cleanup. Forward `factory` so the
  child renders on the same engine. Filtering, sorting and loading never dispose a registration; removing the item,
  giving it pure content, replacing the registration or destroying the grid does.
- Constructor `(host, data, config, createViewMount?, environment?)`: `environment` is the host and test seam for
  `matchMedia`, `url` (`{ read, write, subscribe }`, default `location` + `history`) and `ResizeObserver` (null: the host
  calls `setNarrow` itself).
- Custom element `<xwui-filterable-grid>`; static `projectView` composes a still render (facets projected in place).

## Accessibility and RTL

- The search is a `role=search` landmark with a named `type=search` field; Escape clears a non-empty query. Facet groups
  are XWUIChoiceGroup: single and multi are `role=group` of `aria-pressed` buttons, radio is a `radiogroup`; each is
  named by its visible label; one roving tab stop; arrows move (mirrored in RTL), Home and End jump, Enter and Space
  press; the count is spoken with the option. Selection is a check or a dot as well as colour.
- The summary is a polite, atomic status. Hidden cards leave layout, hit testing and the accessibility tree.
- The open control is a full-cell button (or link) named by the item's label, under the card; the card's own controls
  stay on top and keep working. The open item carries `aria-current`.
- The sheet is a `role=dialog` with `aria-modal`, a title, a close button and "Show {shown} results"; focus moves into it,
  Tab stays inside, Escape and the scrim close it, focus returns to the trigger, whose badge counts active groups.
- Logical properties only; direction comes from the parent `dir`; the facet column sits at the inline start.
- Motion (card hover lift, sheet rise, skeleton shimmer) stops under `prefers-reduced-motion`, in CSS and in JS (the
  adapter follows the media query and adds `.xwui-filterable-grid-still`).

## Provenance

Promoted from `03_BASE/xwflow/xwflow-web/src/components/XWUIFilterableGrid` (app-owned, 659 lines of TS: one filter
dimension as tabs with `itemIds`, `filterChange { filterId, visibleIds, shown, total }`, `mountItem`, `mountHeader`,
`setSummaryFormatter`, the hidden-wins-over-equal-height rule). Kept: keyed cells that only hide, host-owned content and
header slots with idempotent cleanup, the summary formatter, the CSS regression, pure item content. The app's filter
tabs became one `single` facet (an option without `itemIds` is now `all: true`); its `activate(id)` is
`pressOption(facetId, id)`; its state and view tests are ported in the "promoted regression floor" block of
`XWUIFilterableGrid.test.ts`. `xwflow-web` keeps its copy until it adopts this one (no shim either way).

```api
```
