# Elements: Data display, media and code

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/dist/elements/api.json. Do not edit: change the source and run the generator.

How to read these tables: see `elements-index.md`. Each element shows its first example; the others are in `references/full/examples-data-display.md` (kept apart so the group files stay short).

## `pk-avatar`

**Avatar** (Data display). A person or entity as an image or initials, with sizes, shapes, a stable colour per name and an online, away or busy status dot.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The accessible name and the source of the initials. |
| `src` | string | `""` |  | An image URL; initials (or the placeholder glyph when there is no name) show until it loads or if it fails. |
| `size` | enum | `"md"` | `md` `sm` `lg` | Size. |
| `shape` | enum | `"circle"` | `circle` `square` | Shape. |
| `status` | enum | `"none"` | `none` `online` `away` `busy` | Status dot. |
| `colour` | string | `""` |  | A colour slot 1 to 5, or muted; by default a stable slot chosen from the name. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Replaces the neutral person glyph shown when there is no name and no image. |

**Methods**

| Method | Description |
|---|---|
| `initials(name, max)` | Initials for a name. |
| `colourSlot(name, count)` | Stable colour slot. |

**CSS parts**

| Part | Description |
|---|---|
| `image` | The image. |
| `initials` | The initials. |
| `placeholder` | The neutral person glyph (or the slotted icon), shown only when there is no name and no image. |
| `glyph` | The built-in person glyph inside the placeholder. |
| `status` | The status dot. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-avatar-size` | `2.25rem` | Diameter. |
| `--pk-avatar-bg` | `the colour slot` | Fill colour. |
| `--pk-avatar-fg` | `var(--chart-on)` | Initials colour. |

**Accessibility.** role=img with the name (and status) as its label; the image itself has an empty alt because the host already names it.

Example: Sizes and status

```html
<pk-avatar name="Ada Lovelace" size="sm"></pk-avatar>
<pk-avatar name="Sam Lee" status="online"></pk-avatar>
<pk-avatar name="Ana Ruiz" size="lg" shape="square"></pk-avatar>
```

## `pk-avatar-group`

**AvatarGroup** (Data display). Overlapping avatars with a +N tile for the overflow.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `max` | number | `4` | Avatars shown before the +N tile. |
| `label` | string | `""` | Accessible name of the group. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-avatar children. |

**Methods**

| Method | Description |
|---|---|
| `groupOverflow(people, max)` | Shown people and the remainder. |

**CSS parts**

| Part | Description |
|---|---|
| `more` | The +N tile. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-avatar-group-overlap` | `var(--space-2)` | How far avatars overlap. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the slotted pk-avatar children | `hidden` | Every avatar past max gets hidden, and loses it again when max grows or an avatar is removed before it. Kept when the element disconnects. |

**Accessibility.** role=group with a label; the +N tile is text, so the count of hidden people is available without seeing them.

Example: Overflow

```html
<pk-avatar-group max="2" label="Team">
  <pk-avatar name="Ada L"></pk-avatar>
  <pk-avatar name="Sam L"></pk-avatar>
  <pk-avatar name="Ana R"></pk-avatar>
</pk-avatar-group>
```

## `pk-badge`

**Badge** (Data display). A count or short label in a pill, or a status dot with a label; counts cap at a maximum (99+).

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `variant` | enum | `"accent"` | `accent` `muted` `ok` `warn` `danger` `outline` | Colour. |
| `count` | string | `""` |  | Value to show when the slot is empty, capped by max. |
| `max` | number | `99` |  | Counts above read as max+; 0 disables capping. |
| `dot` | boolean | `false` |  | A status dot with the text as its label instead of a pill. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Text; overrides the count. |

**Methods**

| Method | Description |
|---|---|
| `formatCount(value, max)` | Text such as 99+. |

**CSS parts**

| Part | Description |
|---|---|
| `dot` | The status dot. |
| `text` | The label wrapper. |
| `count` | The formatted count shown when the slot is empty. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-badge-bg` | `the variant colour` | Pill fill. |
| `--pk-badge-fg` | `the variant text colour` | Pill text. |

**Accessibility.** The text is the accessible name; put a noun in the slot ("4 unread") when the count alone is ambiguous. Dot mode never relies on colour alone: the label states the status.

Example: Variants

```html
<pk-badge count="12"></pk-badge>
<pk-badge variant="ok">Live</pk-badge>
<pk-badge variant="danger" count="120"></pk-badge>
```

## `pk-chart`

**Chart** (Data display). Bar, line, donut, stacked-bar and sparkline charts in pure SVG, drawn from a slotted data table (which stays as the accessible fallback) or from a data property.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `kind` | enum | `"bar"` | `bar` `line` `donut` `stack` `spark` | Chart type. |
| `caption` | string | `""` |  | Title (figcaption). |
| `height` | number | `140` |  | viewBox height of bar and line charts. |
| `data` | json | `null` |  | Data as a property instead of a table: { labels: string[], series: { name, values: number[] }[] }. |
| `data-label` (property `dataLabel`) | string | `"Show data"` |  | Summary text of the data details. |

**Slots**

| Slot | Description |
|---|---|
| (default) | A table: the first column holds labels, each further column a series. A cell may carry data-value. |
| `caption` | A rich caption; overrides caption. |

**Methods**

| Method | Description |
|---|---|
| `barRects(series)` | Bar geometry. |
| `linePoints(series)` | Line points. |
| `donutSegments(values)` | Donut dash arrays. |
| `sparkPoints(values)` | Sparkline points. |

**CSS parts**

| Part | Description |
|---|---|
| `figure` | The figure. |
| `caption` | The caption. |
| `plot` | The svg container. |
| `legend` | The legend list. |
| `data` | The data details. |
| `data-toggle` | The data details summary. |

**Accessibility.** The svg is role=img with a generated text alternative listing the values; the source table stays in a details for exact numbers. Series colours are the chart tokens and every multi-series chart has a legend, so colour is not the only key.

Example: Bar from a table

```html
<pk-chart kind="bar" caption="Sales">
  <table><thead><tr><th>Month</th><th>Sales</th></tr></thead>
  <tbody><tr><th scope="row">Jan</th><td>4</td></tr><tr><th scope="row">Feb</th><td>6</td></tr></tbody></table>
</pk-chart>
```

## `pk-code-block`

**Code block** (Code). A block of code with an optional label bar, line numbers, a wrap toggle and a copy button. The code is the element's text content.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `label` | string | `""` | File or language shown in the bar; also the region's name. |
| `line-numbers` (property `lineNumbers`) | boolean | `false` | A gutter of line numbers. |
| `wrap` | boolean | `false` | Wrap long lines. |
| `no-copy` (property `noCopy`) | boolean | `false` | Hide the copy button. |
| `max-height` (property `maxHeight`) | string | `""` | Cap the height, any CSS length. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-copy` | `{ ok: bool }` | After a copy attempt. |
| `pk-wrap-change` | `{ wrap: bool }` | The wrap toggle was pressed; wrap already has the new value. |

**Methods**

| Method | Description |
|---|---|
| `splitLines(text)` | Lines without blank ends. |
| `dedent(text)` | Remove the shared indent. |
| `copyText(text)` | Async clipboard copy. |

**CSS parts**

| Part | Description |
|---|---|
| `bar` | The header bar. |
| `label` | The label. |
| `wrap` | The wrap toggle. |
| `copy` | The copy button. |
| `status` | The live region for the copy result. |
| `body` | The scrolling region. |
| `code` | The pre. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-code-block-bg` | `var(--cv-bg)` | Background. |
| `--pk-code-block-fg` | `var(--cv-text)` | Text colour. |
| `--pk-code-block-max-height` | `none` | Maximum body height. |
| `--pk-code-block-gutter` | `2` | Line-number width in characters; set by the element. |

**Accessibility.** The body is a labelled, focusable region so a keyboard can scroll it; the copy result is announced in a polite status region; line numbers are CSS-generated and not selectable. Lines are set as text, never parsed as HTML.

Example: With line numbers

```html
<pk-code-block label="import.json" line-numbers>{ "sku": "AC-001", "qty": 24 }</pk-code-block>
```

## `pk-code-view`

**Code view** (Code). A read-only, line-numbered listing for code or text: sticky gutter numbers a selection leaves out, highlighted lines, coloured segments, sideways scrolling.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `lines` | json | `[]` | Lines to show; the text content when empty. |
| `segments` | json | `[]` | Colouring: per line an array of { text, kind, match, word }; kind is keyword, string, comment, number, type, tag, attribute, directive, key or plain; match and word mark a search hit and the selected word. |
| `start` | number | `1` | First line number. |
| `highlight` | string | `""` | Lines to highlight, such as 12 or 3, 5-7; the first is scrolled to the middle. |
| `label` | string | `"Code"` | Accessible name. |
| `max-height` (property `maxHeight`) | string | `""` | Cap the height, any CSS length. |
| `fill` | boolean | `false` | Fill the container; scrolls inside. Ignores maxHeight. |
| `header` | boolean | `false` | Show the header bar. |
| `copy` | boolean | `false` | Header Copy button; copies all lines. |
| `language` | string | `""` | Language tag in the header. |

**Slots**

| Slot | Description |
|---|---|
| `header-start` | After the title. |
| `actions` | Right side. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-line-click` | `{ line: number, column: number }` | A click left a caret in a line's code: its line and zero-based column. |
| `pk-copy` | `{ ok: boolean }` | After a copy attempt. |

**Methods**

| Method | Description |
|---|---|
| `parseLines(spec)` | Highlighted line numbers. |
| `sourceLines(lines, text)` | The lines to show. |
| `lineSegments(text, segments)` | Normalised segments. |

**CSS parts**

| Part | Description |
|---|---|
| `header` | The header bar. |
| `title` | The label. |
| `language` | The language tag. |
| `count` | The line count. |
| `actions` | The right-side action area. |
| `copy` | The Copy button. |
| `status` | Live result. |
| `body` | The scrolling region. |
| `rows` | The rows. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-code-view-bg` | `var(--cv-bg)` | Background. |
| `--pk-code-view-fg` | `var(--cv-text)` | Text colour. |
| `--pk-code-view-max-height` | `none` | Max body height. |
| `--pk-code-view-gutter` | `2` | Gutter width in characters. |

**Accessibility.** The body is a labelled, focusable region a keyboard can scroll. Line numbers are generated content: not selectable, not copied. A highlighted line has a background and a leading edge. Lines are text, never HTML.

Example: Header with a language, line count, copy and actions

```html
<pk-code-view header copy label="order.js" language="js"><pk-button slot="actions" size="mini" variant="ghost">Outline</pk-button>function total(items) {
    return items.length;
}</pk-code-view>
```

## `pk-data-table`

**Data table** (Data display). A paged, searchable, sortable table that owns its data flow: a pk-table (manual), a pk-table-filters search/filter panel and a pk-pagination footer, with the query, the load and the states in one place. Set the load(query) callback as a property (business logic, never config data): it receives { page, pageSize, sort, sortDir, search, filters } and, as a second argument, { signal }, an AbortSignal that aborts when a newer request replaces it or the element leaves the page (pass it to fetch), and returns (or resolves to) { rows, total, filterOptions? } for that page (filterOptions: { filterKey: [{ value, label }] } replaces those select filters' options, e.g. labels with counts). A loading state shows while in flight, a rejection shows the error state with Retry, zero rows the empty state, and a stale response is dropped. With selectable set, the selection survives paging and searching: selected holds the chosen ids, selectScope is 'page' (those ids) or 'all' (Select all N rows: the ids stay the loaded page). Each change raises pk-select with the ids, the scope and the current query, so for scope 'all' the host runs its bulk action against that query on the server. A search or filter change narrows scope 'all' back to 'page'. rowHref(row), also a callback property, makes rows clickable and is called with the row. pk-list-page and the lookup picker are built on this element.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `columns` | json | `[]` |  | pk-table's column definitions ({ key, label, type?, align?, sortable?, hidePhone?, shed?, title? }[]). The table shows nothing without them. |
| `filters` | json | `[]` |  | The filter fields beside the search box: [{ key, label, type?, options?, anyLabel? }] (type is a pk-input type, 'select' with options, or 'multiselect': a checklist of options whose chosen values arrive as an array, empty when none). Each is a labelled control; its value goes to load(query) as query.filters[key]. Controls are rebuilt only when this array changes, so typed values survive. |
| `page-size` (property `pageSize`) | number | `25` |  | The page size to start with (initial only; the user changes it in the pager). |
| `page-size-options` (property `pageSizeOptions`) | json | `null` |  | The page sizes the pager offers, a number array; null (the default) gives the pager no size choice. |
| `sort` | string | `""` |  | The key of the column to sort by first (initial only; empty loads the default order). |
| `sort-dir` (property `sortDir`) | string | `"ascending"` |  | ascending or descending, for sort. |
| `hide-search` (property `hideSearch`) | boolean | `false` |  | Hides the search box. |
| `search` | string | `""` |  | The search term to start with, for example from a query string: it fills the search box and the first load, and typing replaces it. Changing it later replaces the term and loads page 1. |
| `filter-values` (property `filterValues`) | json | `{}` |  | Filter values to start with, by key. |
| `page` | number | `1` |  | The page to load first (initial only). |
| `search-label` (property `searchLabel`) | string | `""` |  | The accessible name of the search box, and its placeholder unless searchPlaceholder is set (default "Search"). |
| `search-placeholder` (property `searchPlaceholder`) | string | `""` |  | The text shown in the empty search box when it should say more than searchLabel (for example "Order number, customer or SKU"); empty shows the label. |
| `search-debounce` (property `searchDebounce`) | number | `250` |  | Milliseconds to wait after the last keystroke before searching. |
| `pager-label` (property `pagerLabel`) | string | `""` |  | The accessible name of the pager (default "Pagination"). |
| `label` | string | `""` |  | The accessible name of the table. |
| `caption` | string | `""` |  | The table caption, as text. |
| `empty` | json | `null` |  | { heading?, description? } shown when a page has zero rows and nothing is searched; null shows the built-in empty state. |
| `no-results` (property `noResults`) | json | `null` |  | { heading?, description? } shown instead while a search or filter is active; null uses the empty state. |
| `load-error` (property `loadError`) | string | `""` |  | The heading of the failed-load message (with Retry). |
| `add-label` (property `addLabel`) | string | `""` |  | The text of a primary action button after the search box and the actions slot (for example "+ Add customer"); pressing it raises pk-add. Empty shows no button. |
| `selectable` | boolean | `false` |  | Show a checkbox column and the bulk bar, with 'Select all N rows' when the query has more rows than the page. |
| `row-key` (property `rowKey`) | string | `"id"` |  | The row field that identifies a row, which is what selected holds. |
| `clickable` | boolean | `false` |  | Rows can be activated: a click, or Enter or Space on the focused row (each row is a tab stop), raises pk-row-click with the row id. For a host that cannot pass a rowHref callback (a wrapper, a page that opens the row itself). Setting rowHref makes rows clickable too. |
| `row-href-template` (property `rowHrefTemplate`) | string | `""` |  | Row links: an address with {id} (and any {field}, URL-encoded), like /orders/{id} or #/orders/{id}. The first cell of each row becomes a real link; a plain click on it or the row goes through the router (pk-navigate). Plain data, so a wrapper can set it (rowHref is a callback). |
| `navigate` | string | `""` |  | Who follows a row link: router (default, empty), host or none. |
| `current-row` (property `currentRow`) | string | `""` |  | The id (rowKey value) of the row that is open elsewhere, for example in a detail pane: pk-table tints it, marks it with an accent bar and gives it aria-current. The host sets it, from the route or from pk-row-click. |
| `cards` | boolean | `false` |  | Rows become stacked cards on a phone, as pk-table's cards. |
| `striped` | boolean | `false` |  | Alternate row tint, as pk-table's striped. |
| `density` | enum | `"default"` | `default` `compact` `comfortable` | Row height, as pk-table's density. |
| `max-height` (property `maxHeight`) | string | `""` |  | A CSS length that caps the table's height (the rows scroll inside it), as pk-table's maxHeight; combine with stickyHeader. |
| `sticky-header` (property `stickyHeader`) | boolean | `false` |  | Keep the header row visible while the rows scroll, as pk-table's stickyHeader. In a flex column of definite height (a pk-stack[fill], an app-shell body) it fills that height: only the rows scroll, the pager and bulk bar stay in view. Without a definite height the table is as tall as its rows. |
| `expandable` | boolean | `false` |  | Rows that have a detail-<rowId> child get a toggle that shows or hides it under the row, as pk-table's expandable. The detail is yours to render for the loaded page's rows. Expanding is not a row click. |
| `expander` | string | `""` |  | Set to none to leave out the toggle column: the host opens rows by setting expanded, as pk-table's expander. |
| `expanded` | json | `[]` |  | Ids of the expanded rows, as pk-table's expanded: the reader changes it and pk-row-expand reports each change; after that the host owns it. Set it to open rows from your own control. |
| `selected` | json | `[]` |  | The ids of the selected rows, as strings, across every page and search the reader has visited. Set it to preselect; read it (or the pk-select event) to act on the selection. For selectScope 'all' it is only the loaded page: expand the scope with the query. |
| `select-page-only` (property `selectPageOnly`) | boolean | `false` |  | With selectable: never offer 'Select all N rows'. The header checkbox selects the loaded page and selectScope stays 'page'; for a bulk action that works only on explicit ids. |
| `select-scope` (property `selectScope`) | enum | `"page"` | `page` `all` | 'all' once the reader chose Select all N rows: every row of the current query is selected, not only the ids in selected. |

**Slots**

| Slot | Description |
|---|---|
| `breadcrumb` | Breadcrumb at the start of the toolbar row; wraps when narrow. |
| `title` | Heading beside it. |
| `actions` | Toolbar buttons at the end of the search row. On a phone a slotted field (a select, an input) takes the full toolbar width; buttons keep their size. |
| `empty` | Content shown instead of the built-in empty state when load() returns no rows and no search or filter is active (an illustration, a call to action). With a search active the no-results slot, else the built-in state, shows. |
| `no-results` | Content shown instead of the built-in no-results state when a search or filter is active and nothing matches. |
| `bulk` | Bulk actions (buttons) shown in the bar that appears while rows are selected, beside the count; act on the pk-select detail (the ids, or the query for scope 'all'). Needs selectable. |
| `cell-<rowId>-<key>` | Custom content for one cell, exactly as pk-table's cell slots: the row's id (its rowKey field) and the column key. A row with no such child shows the plain value. Column options (align, type, hidePhone) in columns apply as in pk-table. |
| `detail-<rowId>` | Detail content of one row, shown under it when the row is expanded (expandable), exactly as pk-table's detail slots (detail-<rowId>.<name> panels too). A row without it has no toggle. |
| `header-<key>` | Content beside one column header's title (a help tooltip, an icon button), exactly as pk-table's header slots; the text title and the sort button stay. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-add` | `none` | The add button (addLabel) was pressed. |
| `pk-load-error` | `{ error: unknown }` | load() rejected: the built-in error state with Retry is showing. error is what it rejected with (not raised for a response a newer request overtook). |
| `pk-query-change` | `{ sortKey: string \| null, descending: boolean, search: string, page: number, pageSize: number, filters: object }` | The reader changed the query (sorted, searched, filtered, paged or chose a page size); raised before load() runs for it. Not raised for a prop set from script or for refresh(). Use it to keep SortKey, Search and PageSize in sync with the host (two-way binding). |
| `pk-row-click` | `{ id: string, row: object }` | pk-table's own event, passed through: a clickable row was activated. id is the row's rowKey value, row the row object from load(). A control inside the row (checkbox, button, link) does not raise it. |
| `pk-navigate` | `{ to: string, href: string }` | A click on a row link (pk-table's event, passed through). |
| `pk-row-expand` | `{ id: string, index: number, expanded: bool }` | The reader expanded or collapsed a row (pk-table's event, passed on); expanded is the new state and the expanded prop already holds it. |
| `pk-select` | `{ selected: string[], scope: 'page' \| 'all', query: { page, pageSize, sort, sortDir, search, filters } }` | The selection changed (a row, the page or Select all N, a cleared selection, or a search that narrowed scope all to page). For scope 'all' run the bulk action against query on the server. pk-table's own pk-select and pk-select-all stop inside the element. |

**Methods**

| Method | Description |
|---|---|
| `focus(where?)` | Moves focus from the host. No argument: the search box, or the first row when the search is hidden. "search": the search box only (nothing while it is hidden or not drawn yet). "next": the row after the focused one, the first when no row has focus. "previous", "first", "last": only while a row has focus (the call does nothing otherwise, so Home and the arrows stay the search box's own). Returns whether focus moved. |
| `refresh()` | Runs load(query) again for the current query, for example after a bulk action. Returns a promise. |
| `query` | A getter: a copy of the current query { page, pageSize, sort, sortDir, search, filters }. |

**CSS parts**

| Part | Description |
|---|---|
| `table` | The pk-table, in manual mode. |
| `toolbar` | Holds the filters and the actions, above the table. |
| `filters` | The pk-table-filters search/filter panel, in the toolbar above the table (it stays shown and focused while rows load and when nothing matches). |
| `actions` | Holds the actions slot, at the end of the toolbar. |
| `add` | The primary add button, after the actions slot (shown when addLabel is set). |
| `pagination` | The pk-pagination, in the table's footer. |
| `footer` | The table footer bar holding the pager. |
| `pager` | The pk-pagination bar. |
| `pager-prev` | Pager previous button. |
| `pager-next` | Pager next button. |
| `empty` | Holds the empty slot while it is shown. |
| `noResults` | Holds the no-results slot while shown. |
| `state` | Holds the loading/empty/error state while load() is in flight or the result is empty; the table is hidden while this is shown. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-data-table-actions-flex` | `1 1 100%` | The flex of the actions row on a phone (the actions take a row of their own). pk-list-page sets 0 0 0 while it has no actions or toolbar content so that no empty row leaves a gap under the search; set it the same way when your own actions slot is empty. |

**Accessibility.** Every filter field is a labelled real control; the table, search box, bulk bar and pagination keep their own roles, names and announcements ('Select all N rows' is a named button); loading and error states use pk-skeleton and pk-alert's roles.

Example: Orders (set load from script: an inline script cannot run under this site's CSP; with none, the empty state shows)

```html
<pk-data-table selectable columns='[{"key":"sku","label":"SKU"},{"key":"name","label":"Name"}]' empty='{"heading":"No orders yet","description":"Orders you receive show here."}'></pk-data-table>
```

## `pk-field-list`

**Field list** (Data display). A titled description list: term and value pairs as slotted dt and dd, or a data-driven items property, beside each other, stacked or run in; with dividers and a dense mode. Before it upgrades (or with JavaScript off) its slotted content shows as plain markup, not hidden.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `items` | json | `[]` |  | Data-driven rows, rendered as dt/dd pairs alongside any slotted ones: { label, value, hidden?, href? }[]. value is text only (no HTML); a rich value still uses the slot form. A row with an empty value (null, undefined or '') hides itself unless showEmpty is set; hidden always hides it. href renders value as a link. |
| `show-empty` (property `showEmpty`) | boolean | `false` |  | Shows rows whose value is empty instead of hiding them: items rows with no value, and slotted dt/dd pairs whose dd has no element and no text (issue 801). |
| `heading` | string | `""` |  | Section title. |
| `level` | number | `3` |  | Heading level exposed to assistive technology. |
| `layout` | enum | `"grid"` | `grid` `stacked` | Term beside value, or above it. |
| `dividers` | boolean | `false` |  | A rule under each value. |
| `dense` | boolean | `false` |  | Tighter rhythm and smaller text. |

**Slots**

| Slot | Description |
|---|---|
| (default) | dt and dd pairs; a dd may hold chips, links or code. Renders alongside any items rows. A pair whose dd is empty (no element, no text) hides itself unless showEmpty is set, so an optional value needs no wrapper. |
| `heading` | A rich heading; overrides heading. |

**CSS parts**

| Part | Description |
|---|---|
| `section` | The wrapper. |
| `heading` | The heading. |
| `list` | The description list. |
| `items` | The wrapper around the rows rendered from the items property. |
| `term` | A dt rendered from an items row. |
| `value` | A dd rendered from an items row. |
| `value-link` | The link inside a value, when an items row has an href. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-field-list-term` | `minmax(9rem, 1fr)` | The term column width. |
| `--pk-field-list-columns` | `term column and 2fr; one column on a phone` | The whole column template, for a page that re-flows the list (a flyout on a phone sizes the term to its text; a wide card runs two pairs per line). |
| `--pk-field-list-gap` | `0` | Row and column gap on a phone. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the slotted dt and dd of an empty pair | `hidden` | A dt with only empty dd siblings is hidden together with them, and shown again when a value appears; a hidden attribute the author set is never removed. |

**Accessibility.** A real description list: slotted dt and dd, and rows rendered from items, are exposed as terms and definitions. The heading names the section. Below 640px the pairs stack.

Example: Product fields

```html
<pk-field-list heading="Identifiers">
  <dt>SKU</dt><dd><code>AC-001</code></dd>
  <dt>Vendor</dt><dd>Acme Supply</dd>
</pk-field-list>
```

## `pk-frame`

**Frame** (Media). A sandboxed iframe (an inline srcdoc document or a same-site src) with named width presets and a light/dark theme hand-off.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `title` | string | `""` |  | The framed document's accessible name; warns once when empty. |
| `html` | string | `""` |  | An inline document, written as the frame's srcdoc. Ignored when src is set. |
| `src` | string | `""` |  | A same-site path or an http(s) address to load; takes priority over html. |
| `sandbox` | string | `"allow-scripts"` |  | Space-separated sandbox tokens. allow-same-origin is dropped when allow-scripts is also present, unless allowSameOrigin opts in. |
| `allow-same-origin` (property `allowSameOrigin`) | boolean | `false` |  | Adds allow-same-origin to sandbox even alongside allow-scripts; only for a trusted, cooperating page. |
| `preset` | enum | `"full"` | `phone` `tablet` `desktop` `full` | A named width cap from tokens/breakpoints.json (phone, tablet, desktop -> the wide breakpoint) or full (no cap). |
| `height` | string | `"auto"` |  | auto keeps a fallback height until a cooperating document posts pk-frame-resize; any other value is a CSS length. |
| `theme` | enum | `"inherit"` | `light` `dark` `inherit` | Handed to a cooperating document as ?pk-theme= on a same-origin src and as a postMessage; inherit sends nothing. |
| `loading` | enum | `"lazy"` | `lazy` `eager` | The native iframe loading mode. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-frame-load` | `{ }` | The framed document finished loading. |
| `pk-frame-resize` | `{ height: number }` | A cooperating document reported its height (height="auto" only). |

**Methods**

| Method | Description |
|---|---|
| `resolveSandbox(sandbox, allowSameOrigin)` | The sandbox token string actually applied. |
| `withThemeParam(url, theme, base)` | url with ?pk-theme= appended when it is same-origin with base. |

**CSS parts**

| Part | Description |
|---|---|
| `frame` | The iframe. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-frame-background` | `var(--color-surface-alt)` | Frame background before the document paints. |

**Accessibility.** title is the framed document's accessible name (like an ordinary iframe title, required for assistive tech); pk-frame warns once when it is empty. The framed document is a separate document for a screen reader - its own headings and landmarks are not part of this page's outline. Reduced motion: pk-frame never animates the resize itself, so it respects whatever the OS setting already does to the surrounding page.

Example: Inline document, tablet width

```html
<pk-frame title="Preview" preset="tablet" html='<!doctype html><body>Hello from the frame</body>'></pk-frame>
```

## `pk-gallery`

**Gallery** (Data display). The SDK gallery inside your page: every control, element, layout and template with live samples, narrowed by attributes to just what you want to show. Loads on demand in its own frame.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `kind` | string | `""` |  | What to show: foundations, controls, elements, layouts or templates. Empty shows the controls when a group or control is named, otherwise everything. |
| `group` | string | `""` |  | One control group by name ("Forms & inputs") or slug (forms-inputs); with kind layouts or templates, that samples group. |
| `control` | string | `""` |  | One control, element or sample id, or a comma separated list ("button,input"); elements also match by tag (pk-tabs). |
| `theme` | enum | `"auto"` | `auto` `dark` `light` | The theme of the gallery and its samples. auto keeps the gallery's own default (dark). |
| `width` | enum | `"desktop"` | `desktop` `phone` | Render the samples at desktop width or in a 375px phone frame. |
| `filter` | string | `""` |  | Search text: only controls whose name (or group) contains it are shown. |
| `chrome` | enum | `"none"` | `none` `full` | none shows only the content, sized to fit it; full keeps the contents nav, the viewport and text size toolbar and the element inspector (Details: an element's API and live markup). |
| `height` | number | `0` |  | A fixed height in pixels. 0 fits the content when chrome is none and uses the default height when chrome is full. |
| `src` | string | `""` |  | Address of the gallery's embed.html when it is not next to the element module (the npm package leaves dist/gallery out), for example the Pages site or a CDN prefix; without it a frame that never answers shows a notice that says so. A relative address resolves against the document's base URI (the base URI), so it works on a routed page. |
| `sections` | string | `""` |  | Sections to add to the gallery's Details drawer (chrome full), as JSON text: a list of { tag?, title, open?, lines?, columns?, rows?, code? }, all text. The element sends them to its frame by message once the gallery is ready and whenever the text changes; tag limits one to an element (pk-button), none shows it for every element. Text only, cut to limits; see js/gallery-sections.js. |

**CSS parts**

| Part | Description |
|---|---|
| `frame` | The iframe that hosts the gallery. |
| `notice` | The message shown when the frame loads but no gallery answers (its files are not where the element looks). |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-gallery-height` | `28rem` | Height of the frame. The element sets it from height or from the height the gallery reports. |

**Accessibility.** The frame is titled "SDK gallery". With chrome full, the gallery is a landmark layout with a contents nav, a toolbar and a labelled inspector; with chrome none it is the content alone, and each sample sits in its own titled frame.

Example: Every form control, phone width, light theme

```html
<pk-gallery kind="controls" group="Forms & inputs" width="phone" theme="light" height="480"></pk-gallery>
```

## `pk-heat-map`

**Heat map** (Data display). A weighted treemap: a fixed-height area filled with boxes whose area is each item's value and whose colour is its status (neutral, info, success, warning, danger). Boxes are real links or buttons with one tab stop and arrow-key movement, a data table is always kept beside the map as the accessible fallback, and boxes narrower than min-box merge into one "Other" box. Data is a property, not child elements.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `items` | json | `[]` |  | The boxes: [{ id, label, value, severity?, intensity?, href?, children? }]. value above 0 sets the area; severity is neutral (default), info, success, warning or danger; href makes the box a link. An item with no usable value is skipped and said so once in the log. Instead of this property, a plain <table> as the direct child is read (first cell the name, a link in it the href, column 2 the value unless a header says value, count, total, size or amount, headers severity or status and intensity pick those columns, a cell's data-value beats its text, a row's data-id is the id; a table edit redraws); this property wins when it has items. children is a list of items of the same shape (one level, used with group-by); a group takes the sum of its children as its value and the most serious of their severities. |
| `label` | string | `""` |  | The accessible name of the map (and the caption of its data table). |
| `min-box` (property `minBox`) | number | `48` |  | The smallest side of a box in pixels. Boxes below it merge into one "Other (n)" box (never dropped; the data table still lists every item); a box that cannot be merged is given extra area until it reaches the minimum, so no tile is a thin strip. Other takes the most serious severity of its members. 0 never merges. |
| `group-by` (property `groupBy`) | boolean | `false` |  | One level of grouping: an item with children is drawn as a framed area (its name and total in a header) whose area is the sum of its children and which holds them as boxes. Without group-by such an item is one plain box. A child's name carries its group ("Payments: Invoices"). |
| `show-table` (property `showTable`) | boolean | `false` |  | Show the data table in place of the map (also the print and narrow-screen fallback you choose). The table is always in the element for assistive technology. |
| `layout` | enum | `"squarified"` | `squarified` `rows` | squarified sorts by value into near-square boxes; rows keeps the input order (weighted rows), so positions stay put while values change: use it for live data. |
| `scale` | enum | `"status"` | `status` `intensity` | status colours a box by its severity; intensity uses one hue by each item's intensity (0 to 1), shown as a percentage and carried in the box's name. |
| `max-items` (property `maxItems`) | number | `0` |  | 0 is no cap; otherwise the top N items by value are boxes and the rest merge into Other. |
| `value-format` (property `valueFormat`) | enum | `"number"` | `number` `compact` `share` | How a value reads: number (locale grouping), compact (1.2K) or share (percent of the total). |

**Slots**

| Slot | Description |
|---|---|
| `empty` | Shown when there is no usable item. Default text: No data. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-heat-select` | `{ id: string }` | A box was activated (click, Enter or Space; a box with an href is a real link). detail.id is the item id, detail.item the item itself; for the merged box id is "__other" and item.members lists the merged items. Cancel it to stop a link navigating. Activating the merged Other box also opens its members as a table over the map (Escape or Close returns focus to the box); cancel the event to stop that. |
| `pk-heat-focus-item` | `{ id: string }` | Focus moved to a box (arrow keys, Tab or a click). detail.id is the item id, detail.item the item. |

**CSS parts**

| Part | Description |
|---|---|
| `map` | The list of boxes. |
| `empty` | The empty state. |
| `data` | The visually hidden box that holds the data table. |
| `table` | The data table. |
| `caption` | The data table caption. |
| `rows` | The data table body. |
| `more` | The panel that lists the members of Other. |
| `more-head` | Its header. |
| `more-title` | Its title. |
| `more-close` | Its Close button. |
| `more-body` | Its scrolling body. |
| `more-table` | Its table. |
| `more-rows` | Its table body. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-heat-map-height` | `20rem` | The block size of the map. The element fills it and never scrolls. |
| `--pk-heat-map-gap` | `var(--space-1)` | The gap between boxes. |
| `--pk-heat-map-radius` | `var(--radius-sm)` | The corner radius of a box. |
| `--pk-heat-map-neutral` | `var(--color-surface-alt)` | The background of a neutral box. |
| `--pk-heat-map-text` | `var(--color-text)` | The text colour of a neutral box. |

**Accessibility.** The element is a group named by label; each box is a link (with href) or a button named "Label, value, Status", in a list. The whole map is one tab stop (roving), the arrow keys move to the nearest box in that direction, Home and End to the first and last, Enter and Space activate. Colour is never the only carrier: a box that is not neutral carries its severity in its name and title, and shows the word when the box is tall enough (a small box keeps its label and drops what does not fit, never showing half a line). A data table (name, value, share, status) is always in the element for assistive technology and shows in place of the map with show-table. With scale=intensity the intensity shows as a percentage and is in the name. A tile too narrow for its text shows only its number; label and status stay in the name and title. Value changes animate only when the user allows motion. With group-by the frames are decoration and each child names its group. Other opens its members as a table in a labelled region over the map; focus moves to Close and returns to the Other box on Escape or Close.

Example: Issues by area

```html
<pk-heat-map label="Issues by area" items='[{"id":"billing","label":"Billing","value":42,"severity":"warning"},{"id":"auth","label":"Sign-in","value":31,"severity":"danger"},{"id":"search","label":"Search","value":24},{"id":"reports","label":"Reports","value":18,"severity":"info"},{"id":"docs","label":"Docs","value":12,"severity":"success"},{"id":"api","label":"API","value":9},{"id":"mobile","label":"Mobile","value":6,"severity":"warning"},{"id":"admin","label":"Admin","value":3}]'></pk-heat-map>
```

## `pk-icon`

**Icon** (Media). A symbol from the SDK sprite, drawn in the surrounding text colour and size. The sprite is found next to the element module, so it works wherever dist is copied.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `name` | string | `""` |  | The symbol id in the sprite (search, settings, orders, ...). |
| `size` | enum | `"md"` | `sm` `md` `lg` `xl` | md is 1.25em and follows the text; sm, lg and xl are fixed steps of the space scale. |
| `label` | string | `""` |  | The accessible name. Without it the icon is decorative and hidden from assistive technology. |

**CSS parts**

| Part | Description |
|---|---|
| `svg` | The svg element. |
| `use` | The use element that points into the sprite. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-icon-size` | `the size step` | Width and height, overriding size. |

**Accessibility.** With a label the host is role img with that name; without one it is aria-hidden, so put the meaning in the surrounding text (a button label) and leave the icon decorative. The stroke follows currentColor, so it meets the contrast of its text. Give an icon-only button a 44px target.

Example: Follows text colour and size

```html
<p><pk-icon name="search"></pk-icon> Search &nbsp; <pk-icon name="settings" label="Settings"></pk-icon> &nbsp; <pk-icon name="orders" size="xl"></pk-icon></p>
```

## `pk-image-gallery`

**Image gallery** (Data display). A grid of square thumbnails with one marked primary (a badge and an accent frame). Clicking a thumbnail opens the lightbox. Editable, it adds a make-primary and a remove button under each image, a drag handle (or Alt+Up/Alt+Down) to reorder them, and an add tile that picks files.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `images` | json | `[]` | The images: [{ src, alt, primary?, status? }]. status is a short warning badge such as Staged. Property or a JSON attribute. Only same-site paths, http(s) and raster data URLs are shown. |
| `primary` | number | `-1` | The index of the primary image; -1 uses the first image flagged primary in images, or none. |
| `columns` | number | `0` | A fixed number of equal columns (1 to 12). 0 fits as many as min allows. |
| `min` | string | `"6.25rem"` | The narrowest a thumbnail may get, as a css length in px, rem or em. Used when columns is 0. |
| `editable` | boolean | `false` | Show make-primary and remove buttons under each image and the add tile. |
| `hide-add` (property `hideAdd`) | boolean | `false` | Editable, keep make-primary, remove and reorder but skip drawing the add tile, so the host supplies its own add control elsewhere (a real Blazor InputFile, say) instead of reading pk-add, which only carries file name/size/type, never the bytes. |
| `add-label` (property `addLabel`) | string | `"Add image"` | Text on the add tile. |
| `accept` | string | `"image/*"` | The file types the add tile's picker offers. |

**Slots**

| Slot | Description |
|---|---|
| `input` | An existing file input (a Blazor InputFile) that keeps doing the reading: the add tile then only draws the target, and pk-add does not fire for a pick made on it. In plain HTML put slot="input" on the input itself; the generated Blazor InputContent parameter sets it for you (<InputContent><InputFile OnChange=... /></InputContent>). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-primary-change` | `{ index: number, src: string, previous: number }` | Make primary was pressed. Cancelable: preventDefault keeps the current primary (a controlled gallery updates images and primary itself). |
| `pk-remove` | `{ index: number, src: string }` | Remove was pressed. Cancelable: preventDefault keeps the image. Otherwise it is taken out of images, and if it was the primary the first image becomes the primary. |
| `pk-add` | `{ files: File[], names: string[] }` | Files were chosen on the add tile's own picker (not one made on a slotted input slot="input" input, where the host's own change/OnChange is the read path instead). The gallery adds nothing itself: read the files (a data URL) and set images. |
| `pk-reorder` | `{ order: string[], from: number, to: number, item: string }` | A tile was dragged by its handle to a new position, or moved with Alt+Up/Alt+Down (built on pk-sortable, core/elements/sortable/). order is every image's src in the new order; item is the moved image's src; from and to are its old and new index. Cancelable: preventDefault keeps the current order (a controlled gallery reorders images itself). |

**Methods**

| Method | Description |
|---|---|
| `view(index)` | Opens the lightbox at an image. |
| `makePrimary(index)` | Marks an image primary, as the button does (raises pk-primary-change first). |
| `removeImage(index)` | Removes an image, as the button does (raises pk-remove first). |
| `reorder(from, to)` | Moves an image to a new index, as a drag or Alt+Up/Alt+Down does (raises pk-reorder first). |

**CSS parts**

| Part | Description |
|---|---|
| `grid` | The thumbnail list. |
| `tile` | One thumbnail with its badges and buttons. |
| `media` | The thumbnail's pk-media. |
| `badge` | The Primary badge. |
| `status` | The status badge (Staged and the like). |
| `actions` | The row of buttons under an image. |
| `action` | A make-primary or remove pk-button. |
| `add-tile` | The list item holding the add tile. |
| `add` | The add tile: a pk-dropzone (the picker and file drops). Style its box with ::part(zone) through this part. |
| `add-icon` | The plus on the add tile. |
| `add-text` | The text on the add tile. |
| `viewer` | The pk-lightbox. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-image-gallery-radius` | `var(--radius-lg)` | Corner radius of the thumbnails and the add tile. |

**Accessibility.** The grid is a list (pk-sortable, role=list). Each thumbnail is a button that opens the lightbox (Enter or Space); its name is the image's alt text. Make primary and Remove name their image ("Remove: Back view"). Editable, each tile is also a pk-sortable-item (role=listitem): its handle drags to reorder, or Alt+Up/Alt+Down on the focused tile moves it and announces the new position through a visually-hidden live region. The add tile is a real file input, reachable and operable by keyboard, with a visible focus ring, unless hideAdd is set, in which case it is not drawn at all and the host's own add control (elsewhere on the page) carries its own accessible name and focus order. After a removal focus moves to the next image, or the add tile when one is drawn and none is left. Primary and status are text badges, never colour alone.

Example: Editable: make primary, remove, reorder (drag the handle or Alt+Up/Alt+Down) and an add tile

```html
<pk-image-gallery editable images='[{"src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAmklEQVR42gXBIQ4AIAgAQB/GD/yBP6DQ2Cw2N4uNzUJjs9iINqLP8i4BvUxR6CJ5pTPIFukmScAvcxS+yF75DLbFulkStJdblHaxeW1nNFtNd5ME/eUepV/sXvsZ3VbX3SXBfHlGmRen13nGtDV1T0kgL0sUuShe5QyxJbpFEujLGkUvqlc9Q22pbpUE9rJFsYvm1c4wW6bb5AMXIGmBJ4HH0wAAAABJRU5ErkJggg==","alt":"Front view","primary":true},{"src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAiklEQVR42gXBEQIAMAgAwD0xSZLJZJJM4jiO4ziOe9bu1giMwhiMwwRMwhRMw2rBVmzDduzATuzCblwlVEplVE4VVElVVE0rZafutJ2+M3bmztrZe4Wc0BN2wk/EiTxRJ/osl+t63a779bie1+t632XCpmzG5mzBlmzF1rxUnupTe+pP42k+raf9PlY/aGHmDWEHAAAAAElFTkSuQmCC","alt":"Back view","status":"Staged"},{"src":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAIAAABLbSncAAAAm0lEQVR42gXBkRIAIAwA0H1iFmZhNky6ZJiFWXeTolEWRaP9T+9BcUxuNyfdneHudG85FWdQPJPfzUv3Z/g7/VtexRuUwBR2C9LDGeHO8FZQCQYlMsXdovR4RrwzvhVVokFJTGm3JD2dke5MbyWVZFCQCXdD6XgG3olvoQoalMyUd8vS8xn5zvxWVskGpTLV3ar0eka9s75VVap9GM1xYYXoGKsAAAAASUVORK5CYII=","alt":"Detail"}]'></pk-image-gallery>
```

## `pk-list-group`

**List group** (Data display). A bordered list of rows: plain, actionable (links and buttons), with badges at the end, or a checklist.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `variant` | enum | `"plain"` | `plain` `action` `flush` | Row behaviour and frame. |
| `checklist` | boolean | `false` |  | Rows are labels with a checkbox. |
| `dense` | boolean | `false` |  | Compact rows for long lists of results or an outline: rows at text height with the children laid out inline from the start, and no 44px minimum on desktop (an actionable row still gets the touch target on a phone). A row can carry data-depth="1" to "3" to indent. |
| `label` | string | `""` |  | Accessible name of the list. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Rows: div, a, button or label elements. |

**Writes to host nodes**

| Target | Writes | Why |
|---|---|---|
| the slotted rows | `role` | A row without a role gets role=listitem so the list has the items the host role=list expects; a role the host set is left alone. Not removed. Kept when the element disconnects. |

**Accessibility.** The host is role=list and each row gets role=listitem unless it already has a role. Rows are native links, buttons or labels, so keyboard and names come from them. The current row uses aria-current. Actionable rows are at least 44px tall; in a dense list only on a phone, where the touch target still applies. A focused dense row shows the focus ring inside its edge.

Example: Actionable with badges

```html
<pk-list-group variant="action" label="Shortcuts">
  <a href="#">Receive PO 1042 <pk-badge count="3" variant="warn"></pk-badge></a>
  <a href="#" aria-current="page">Low stock</a>
</pk-list-group>
```

## `pk-local-time`

**Local time** (Data display). A UTC instant written in the reader's own locale and time zone, as a date, a time, both, or relative to now (3 hours ago). Keeps a real time element inside, so the machine value stays readable.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `datetime` | string | `""` |  | The instant: an ISO 8601 string (2026-09-19T14:30:00Z). A date-only value is a calendar day and is never shifted by the time zone. |
| `format` | enum | `"datetime"` | `datetime` `date` `time` `relative` | What to show. relative reads 3 hours ago and refreshes every 30 seconds while connected; its title holds the full date. |
| `length` | enum | `"medium"` | `short` `medium` `long` | How much of the date to spell out (9/19/26, Sep 19, 2026, September 19, 2026). |
| `locale` | string | `""` |  | A BCP 47 tag such as de-DE; empty uses the reader's browser locale. |
| `time-zone` (property `timeZone`) | string | `""` |  | An IANA zone such as Europe/Berlin; empty uses the reader's own zone. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Fallback text, shown only while the datetime is empty or not a date. |

**Methods**

| Method | Description |
|---|---|
| `formatLocal(value, options)` | The pure formatter the element uses: format, length, locale, timeZone and now. |

**CSS parts**

| Part | Description |
|---|---|
| `time` | The inner time element (its datetime attribute is the machine value). |
| `text` | The formatted text. |

**Accessibility.** The text is inside a real time element carrying the datetime, so assistive technology and search see the exact instant. Relative text is not live-announced; the title holds the full date for pointer users. Prefer date or datetime where the exact moment matters.

Example: Date, time and both

```html
<p><pk-local-time datetime="2026-09-19T14:30:00Z"></pk-local-time></p>
<p><pk-local-time datetime="2026-09-19T14:30:00Z" format="date" length="long"></pk-local-time> at <pk-local-time datetime="2026-09-19T14:30:00Z" format="time"></pk-local-time></p>
```

## `pk-log`

**Log** (Data display). A streaming log viewer: monospace rows in a scroller that sticks to the newest row until the user scrolls up, with a button to jump back, and a cap that drops the oldest rows. Rows are appended from script without redrawing the ones already there.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `label` | string | `"Log"` |  | The log's accessible name (aria-label). |
| `max` | number | `1000` |  | Most rows kept: once there are more, the oldest are removed. 0 keeps every row. |
| `rows` | json | `[]` |  | Replaces every row at once with an array of strings or { text, level, time } objects (level is debug, info, warn or error; time is text, a Date or milliseconds). For a fixed list or a Blazor page; a stream should use append(), which draws only the new rows. |
| `paused` | boolean | `false` |  | True while auto-scroll is paused because the user scrolled up; new rows are still added but the view stays where it is. Scrolling back to the bottom or pressing the resume button clears it. The element owns it while the user scrolls; after pk-pause the host does. Setting it to false jumps to the newest row, setting it to true stops following. |
| `live` | enum | `"polite"` | `polite` `off` | Whether a screen reader announces new rows (aria-live). Use off for a fast stream; the rows stay readable by moving through the log. |
| `resume-label` (property `resumeLabel`) | string | `"Jump to latest"` |  | Text of the button shown while auto-scroll is paused. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Text shown while the log has no rows (default: No entries yet.). |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-pause` | `{ paused: boolean }` | The user paused auto-scroll by scrolling up, or resumed it by scrolling to the bottom or pressing the resume button. Never raised for a change the host made; not cancelable. |

**Methods**

| Method | Description |
|---|---|
| `append(...rows)` | Adds rows at the end (strings or { text, level, time }). A burst of calls is drawn as one insertion; the existing rows are not touched, the oldest go past max, and the view stays at the bottom unless paused. |
| `clear()` | Removes every row. |

**CSS parts**

| Part | Description |
|---|---|
| `root` | The bordered frame. |
| `scroller` | The role=log scroll area; it takes focus so the keyboard can scroll it. |
| `empty` | The text shown while there are no rows. |
| `list` | The rows. |
| `row` | One log row (dynamic); its level is a class (debug, info, warn or error). |
| `resume` | The jump-to-latest button, shown while paused. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-log-height` | `20rem` | Height of the scroll area. |
| `--pk-log-bg` | `var(--cv-bg)` | Background. |
| `--pk-log-fg` | `var(--cv-text)` | Text colour. |

**Accessibility.** The scroll area is role=log (a polite live region, aria-live=off on request) with an accessible name, and it is focusable so the keyboard can scroll it. A level is written as a word on the row, and a warning or error also gets a coloured edge, so it is never colour alone. The resume button is a real button that appears only while paused.

Example: Rows with levels

```html
<pk-log label="Build output" rows='[{"time":"12:00:01","level":"info","text":"Compiling 48 elements"},{"time":"12:00:02","level":"warn","text":"pk-table is near its size budget"},{"time":"12:00:03","level":"error","text":"Test failed: dropzone.pick"},"Plain text rows work too"]'></pk-log>
```

## `pk-markdown`

**Markdown** (Data display). A read-only view of Markdown text, drawn with the toolkit's own parser and renderer (the one behind pk-markdown-editor's preview): DOM calls only, so raw HTML is shown as text, a link must be http, https, mailto or a relative address, and nothing from the source becomes markup. The Markdown subset is paragraphs, bold, italic, code, fenced code, headings 1 to 3, quotes, lists and links. A click on a relative link raises pk-link-click, so the app routes or rewrites it. Use it instead of writing rendered HTML into the page yourself.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `markdown` | string | `""` | The Markdown text to show. Text longer than 200000 characters is cut and a note is added. |
| `intercept-links` (property `interceptLinks`) | boolean | `false` | Cancel the click on a relative link (not an #anchor, not an absolute address) so the page does not navigate; pk-link-click still reports its address. Without it the link navigates unless a pk-link-click listener calls preventDefault. A click with a modifier key or a middle click is left to the browser. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-link-click` | `{ href: string }` | A relative link was clicked: href is its address as written. Cancel it (preventDefault), or set interceptLinks, to route or rewrite it yourself. |

**CSS parts**

| Part | Description |
|---|---|
| `content` | The rendered Markdown. |

**Accessibility.** Headings, lists, quotes and links are real elements, so the text keeps its structure for assistive technology; a heading takes its place in the page outline.

Example: Basic

```html
<pk-markdown markdown="## Release notes

The **spring** update adds *two* things:

- a [guide](guide/intro)
- `code` samples"></pk-markdown>
```

## `pk-media`

**Media** (Media). An image or video in a fixed aspect-ratio box with an optional figure caption and lightbox-ready behaviour.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `ratio` | string | `"16/9"` |  | Aspect ratio as width/height: 16/9, 4/3, 1/1, 3/4. |
| `fit` | enum | `"cover"` | `cover` `contain` | How media fills the box. |
| `caption` | string | `""` |  | Figure caption. |
| `lightbox` | boolean | `false` |  | The box becomes a button that fires pk-open. |
| `square` | boolean | `false` |  | Square corners. |

**Slots**

| Slot | Description |
|---|---|
| (default) | An img, picture, video or svg. |
| `caption` | A rich caption; overrides caption. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-open` | `{ src: string, alt: string, caption: string }` | A lightbox request. Cancel if you handle it yourself. |

**CSS parts**

| Part | Description |
|---|---|
| `figure` | The figure. |
| `box` | The ratio box. |
| `caption` | The caption. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-media-radius` | `var(--radius-md)` | Corner radius. |

**Accessibility.** A real figure with a figcaption; slotted images keep their author-written alt text. Lightbox mode makes the box a focusable button (Enter and Space) named "Enlarge". The ratio reserves space so a late image never shifts the layout.

Example: Captioned 4:3

```html
<pk-media ratio="4/3" caption="Widget 1">
  <svg viewBox="0 0 4 3" role="img" aria-label="Placeholder"><rect width="4" height="3" fill="currentColor" opacity="0.15"></rect></svg>
</pk-media>
```

## `pk-quote`

**Quote** (Data display). A testimonial or pull quote: your own blockquote, with a caption for the portrait, name, role and rating. The element draws a figure and figcaption around your markup and never changes it; before it upgrades (or with JavaScript off) the plain blockquote shows.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `cite` | string | `""` |  | The source URL (a same-site path or http(s) address). Renders one Source link in the caption; your own blockquote cite attribute is untouched. |
| `size` | enum | `"default"` | `default` `large` | large sets bigger type for a lone hero quote. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The author's blockquote. |
| `portrait` | A pk-avatar or image beside the name. |
| `name` | Who said it. |
| `role` | Their role or context, under the name. |
| `rating` | A read-only pk-rating. |

**CSS parts**

| Part | Description |
|---|---|
| `figure` | The figure around the quote and its caption. |
| `caption` | The figcaption row; hidden when it has nothing to show. |
| `who` | The name and role column. |
| `source` | The Source link when cite is set. |

**Accessibility.** A figure with a figcaption around your blockquote, so the attribution belongs to the quote. The portrait is named by pk-avatar and the rating keeps its own label, so each is spoken once. The element adds no hover-only content and never edits the nodes you wrote.

Example: Testimonial

```html
<pk-quote>
  <blockquote><p>Fast delivery and the fit was perfect.</p></blockquote>
  <pk-avatar slot="portrait" name="Alex Roe"></pk-avatar>
  <span slot="name">Alex Roe</span>
  <span slot="role">Customer since 2023</span>
  <pk-rating slot="rating" readonly value="5" label="5 out of 5"></pk-rating>
</pk-quote>
```

## `pk-specimen`

**Specimen** (Data display). A design-token sample for spacing, size, radius, shadow and layer tokens, drawn at its resolved value with the token name and value as text.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `token` | string | `""` | A CSS custom property to sample, for example "--space-4", "--radius-md", "--shadow-card" or "--z-sticky"; resolved against this element's computed style, so it follows the theme and density in effect. |
| `kind` | string | `""` | One of space, radius, shadow, size or layer. How to draw the token: a bar of the resolved length (space, size), a corner sample (radius), an elevated tile (shadow) or a chip with the number (layer). Inferred from the token prefix (--space-, --radius-, --shadow-, --z-, --touch-) when empty. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Optional label shown instead of the token name. |

**CSS parts**

| Part | Description |
|---|---|
| `sample` | The box holding the drawing. |
| `shape` | The drawing: the bar, the corner sample, the shadow tile or the layer chip. |
| `info` | The wrapper around the name and the value. |
| `name` | The token name, or the slotted label. |
| `token` | The token name shown when no label is slotted. |
| `value` | The resolved value, with the measured pixel length for a bar. |

**Accessibility.** The drawing is decorative (aria-hidden); the token name and the resolved value are text, so the result never depends on the picture alone.

Example: Spacing and size

```html
<pk-specimen token="--space-2"></pk-specimen>
<pk-specimen token="--space-4"></pk-specimen>
<pk-specimen token="--touch-target"></pk-specimen>
```

## `pk-stat`

**Stat tile** (Data display). A headline number with a label, an optional change indicator (arrow, sign and a spoken form), a sparkline and a tone. With an href the whole tile is one link. The tile prop gives the compact dashboard look; interactive makes a tile with no href a button.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `label` | string | `""` |  | What the number is. |
| `value` | string | `""` |  | The headline number, already formatted. |
| `subtext` | string | `""` |  | A line under the value. |
| `tone` | enum | `"neutral"` | `neutral` `positive` `warning` `critical` | Value colour. |
| `delta` | string | `""` |  | The change, for example 12.5 or -3; shows an arrow and a signed number, a percentage unless deltaUnit says points. |
| `delta-unit` (property `deltaUnit`) | enum | `"percent"` | `percent` `points` | What delta measures: percent shows +12.5%, points shows +4 pts (a score or rate that moved by an absolute amount) and is spoken as points. |
| `delta-direction` (property `deltaDirection`) | enum | `"auto"` | `auto` `up` `down` `flat` | Override the direction derived from delta. |
| `invert` | boolean | `false` |  | Down is good news (costs, returns): swaps the trend colours. |
| `versus` | string | `"the previous period"` |  | What the change compares with, spoken to assistive technology. |
| `values` | json | `[]` |  | Points for the sparkline (needs at least two). |
| `href` | string | `""` |  | Makes the tile a link. |
| `tile` | boolean | `false` |  | The compact dashboard tile look: less padding, a 600-weight larger value, a panel background. |
| `interactive` | boolean | `false` |  | Without an href, make the tile a button (Enter and Space) that fires pk-activate. |

**Slots**

| Slot | Description |
|---|---|
| `icon` | An icon before the value. |
| (default) | The subtext; overrides subtext. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-activate` | `{ href: string }` | The tile link was pressed. Cancel to stop navigation (a Blazor OnClick handler navigates itself). |

**Methods**

| Method | Description |
|---|---|
| `delta(current, previous)` | Direction and percent. |
| `formatDelta(d, unit)` | A signed percentage, or points when unit is points. |
| `deltaSpeech(d, versus, unit)` | The spoken form. |

**CSS parts**

| Part | Description |
|---|---|
| `tile` | The panel. |
| `label` | The label. |
| `row` | The value row. |
| `value` | The value. |
| `delta` | The change indicator. |
| `subtext` | The subtext wrapper. |
| `spark` | The sparkline wrapper. |
| `link` | The overlay link. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-stat-value-size` | `1.5rem` | Value font size. |

**Accessibility.** The change is text (an arrow glyph and a sign) and has a spoken form ("up 12.5 percent versus last month"), so colour is never the only signal. A tile with href holds one link named by the label and value, so there is a single tab stop. An interactive tile is a role=button overlay reachable by keyboard.

Example: Tones

```html
<pk-stat label="Open POs" value="12"></pk-stat>
<pk-stat label="Negative on hand" value="3" tone="critical" href="#"></pk-stat>
```

## `pk-swatch`

**Swatch** (Data display). A colour or design-token sample with its resolved value and a WCAG AA contrast label against a comparison colour.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `token` | string | `""` | A CSS custom property to sample, for example "--color-accent"; resolved against this element's computed style. Takes precedence over value. |
| `value` | string | `""` | A literal colour to sample when token is not set, for example "#4e93e3"; useful for showing what a literal would look like (the scorecard and docs tooling, for example). |
| `compare` | string | `"--color-bg"` | A token or literal colour the swatch is contrasted against; a token is resolved the same way as token. Defaults to the page background. |
| `large` | boolean | `false` | Grade against the WCAG AA large-text threshold (3:1) instead of normal text (4.5:1). |

**CSS parts**

| Part | Description |
|---|---|
| `swatch` | The coloured box. |
| `info` | The wrapper around the value, ratio and grade text. |
| `value` | The resolved colour value. |
| `ratio` | The measured contrast ratio, such as "4.63:1". |
| `grade` | The AA pass/fail badge. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-swatch-size` | `2.5rem` | The swatch box's width and height. |

**Accessibility.** The swatch box is decorative (aria-hidden); the resolved value, the ratio and the pass/fail grade are all text, so the result never depends on colour alone.

Example: A token against the page background

```html
<pk-swatch token="--color-accent"></pk-swatch>
<pk-swatch token="--color-accent-fill" compare="--color-text"></pk-swatch>
```

## `pk-table`

**Table** (Data display). A data table. Two modes: hand it a raw table in the default slot (rows and cells exactly as authored) and it supplies the scroll frame, toolbar, bulk, empty and footer slots; or give it columns and rows and it renders the table itself, with sorting, filtering, selection, loading and phone cards. The data-driven mode: striped, hover, bordered, density, sticky header and first column, sortable headers, filter row, row selection with a bulk bar, loading and empty states, expandable detail rows and a card layout on a phone. Custom cell content goes in slots named cell-<rowId>-<key>; expandable rows show the slot detail-<rowId>. With manual set, the host owns sorting, filtering and paging and the element only renders and reports. At 500 rows or more (a fixed threshold; js/table-vw.js), a non-expandable, non-slotted table windows: only the rows near the scroll frame's viewport are drawn, and sort, filter and selection still act on every row. In data-driven mode, each row carries data-pk-context="<rowId>", so wrapping the whole table in a pk-context-menu can serve different menu content per row (see that element's context field); js/context-actions.js's wireContextMenu(menuEl, { items, run }) is the shared plumbing for that: it paints the { action, label, shortcut?, disabled?, danger? } list a row's own state calls for and dispatches a choice through the same run(action) an existing toolbar or keyboard shortcut already uses (issue #586). With editable set, cells of columns with an editor edit in place (pk-cell-edit, cellErrors); an editable table windows too (#1474): the active row and the row being edited stay drawn wherever they are, arrow keys, PageUp/PageDown and Ctrl+Home/End move by row id, and paste and undo reach rows that are not drawn. Rows must keep a fixed height, so an expandable table never windows (host-slotted cells stay in the light DOM; the card layout on a phone is not measured).

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `columns` | json | `[]` |  | Column definitions: { key, label, type?: text\|number\|date, align?: start\|end, sortable?, hidePhone?, shed?: 1\|2\|3\|width, noWrap?, title?, editor?: text\|number\|select\|date\|textarea\|combobox, options?, free?, required?, min?, max?, maxLength?, width? }[]. A JSON attribute or a property. type decides how the column sorts and filters, not how a value is shown: cells always show the row value as given, as text, and the element does no formatting, locale or time zone handling, so the host sends the display string it wants ("$1,204.50", "7 Jan 2026"). number sorts numerically and reads through currency symbols and grouping ("$1,204.50" is 1204.5; text that holds no number sorts first) and aligns to the end unless align says otherwise. date sorts chronologically by Date.parse of the value, so use ISO 8601 (a date-only value such as 2026-01-07 is read as UTC midnight, a value with an offset keeps it; text that does not parse sorts first). text sorts alphabetically with numeric collation, ignoring case. In an editable table a column with an editor is editable: editor is text, number, select (with options: strings or { value, label }) textarea (Enter commits, Shift+Enter adds a line), combobox (a native input with a list of options; the text must be an option's value or label unless free; pk-cell-query lets the host fill the list as the user types) or date (a native date input; the cell value is an ISO 8601 date such as 2026-01-07, and min and max are ISO dates, checked as "On or after" and "On or before"), and required, min, max and maxLength are checked before a value is committed. hidePhone hides the column below 640px, in the cards layout too. noWrap keeps the cell's text on one line and lets the column size to it (a date-time that would otherwise break over lines; the cards layout still wraps). shed drops the column by the table's own width, wherever the table sits: 3 below 48rem, 2 below 36rem, 1 below 24rem, any other value is a width: a number above 3 is rem, or a length as text ("60rem", "900px"), so the column goes below that width (a lazily loaded rule per distinct width); the higher width goes first and one table sheds its derived columns in a narrow pane or card before it scrolls sideways; the cards layout keeps every column. title marks the column that names the row (put it first): in the phone cards layout its cell is the card heading, with no label, the full width, start-aligned, wrapped and in heading weight, and its data-label is omitted; on a wide screen it is an ordinary cell. A label of "" also omits data-label, so no label is drawn in a card. |
| `rows` | json | `[]` |  | Row data, one object per row. A JSON attribute or a property. Custom cell content goes in the cell-<rowId>-<key> slots, expanded content in detail-<rowId>. Two optional row fields mark a row: tone (warning, positive, accent or critical) tints it and draws a leading rule, and indent (1 or 2) steps it in, for rows that need attention or sit under a parent; both carry through to the phone card (issue 1019). Any other value is ignored. |
| `row-key` (property `rowKey`) | string | `"id"` |  | The field that identifies a row. |
| `striped` | boolean | `false` |  | Alternate row tint. |
| `hover` | boolean | `false` |  | Tint the row under the pointer. |
| `bordered` | boolean | `false` |  | Border on every cell. |
| `density` | enum | `"default"` | `default` `compact` `comfortable` | Cell padding step. |
| `sticky-header` (property `stickyHeader`) | boolean | `false` |  | Pin the header row (give the table a maxHeight). |
| `sticky-column` (property `stickyColumn`) | boolean | `false` |  | Pin the first column. |
| `selectable` | boolean | `false` |  | Checkbox column with select-all. |
| `clickable` | boolean | `false` |  | Rows are activatable: they show a pointer, take the keyboard (each row is a tab stop; Enter or Space on the row) and fire pk-row-click. A click inside a control in a cell (a native input, button, link, select or label, or a pk-link or pk-button) stays with that control and is not a row click. |
| `row-href-template` (property `rowHrefTemplate`) | string | `""` |  | Row links: /orders/{id} or #/orders/{id} ({field} too, URL-encoded). The first cell links; a click goes through the router. |
| `navigate` | string | `""` |  | Who follows a row link: router (default, empty), host or none. |
| `current-row` (property `currentRow`) | string | `""` |  | The id of the row that is open elsewhere (the record shown in a detail pane): that row is tinted, marked with an accent bar and given aria-current. The host sets it (from the route, or from pk-row-click); the element never changes it. Empty marks no row. |
| `filterable` | boolean | `false` |  | A filter input under each header. |
| `manual` | boolean | `false` |  | The host sorts, filters and pages; rows are shown exactly as given. |
| `sort` | string | `""` |  | Key of the column being sorted; empty when the rows are in their natural order. |
| `sort-dir` (property `sortDir`) | enum | `"ascending"` | `ascending` `descending` | Sort direction. |
| `filters` | json | `{}` |  | Filter text per column key: { [key]: text }. |
| `selected` | json | `[]` |  | Ids of the selected rows. The user changes it with the row checkboxes and the element raises pk-select with the new ids; a change the host makes raises nothing. In a manual table the ids of rows that are not loaded stay (paging, searching and sorting replace the rows, not the selection); see total and selectScope for selecting every row of the query. |
| `total` | number | `0` |  | How many rows match the current query across all pages (a manual table shows one page of them). With a total larger than the loaded rows, once every loaded row is selected the bulk bar offers a "Select all N rows" button, which raises pk-select-all with scope "all". 0 means unknown or not paged: no button. The selection survives the host replacing the rows (paging, search, sort): ids are kept, not matched against the loaded rows, so clear selected when the query changes if the selection should not outlive it. |
| `select-page-only` (property `selectPageOnly`) | boolean | `false` |  | Never offer "Select all N rows" (it needs total): the header checkbox selects the loaded page and the scope stays "page". For a list whose bulk action works only on explicit ids. |
| `select-scope` (property `selectScope`) | enum | `"page"` | `page` `all` | "all" after the user chose "Select all N rows": every row of the query counts as selected, though selected holds only the ids that were loaded. Any change of the selection, and a host that empties selected, sets it back to "page". The element sets it; a host may set it to restore a state. |
| `editable` | boolean | `false` |  | Inline cell editing: the table becomes an ARIA grid with one active cell (arrow keys, Home, End move it; Tab moves it right along the row, then leaves the grid at the row's last cell rather than trapping focus), and a cell of a column that has an editor opens with Enter, F2, a second click or tap on the active cell, or by typing a printable character (which replaces the cell's content, as in a spreadsheet). While editing, Enter commits the draft and moves the active cell down one row, Tab commits and moves it right, Escape cancels, and leaving the cell commits. A cell the host fills through a cell-<rowId>-<key> slot is read-only to the table and owned by the host: give the control tabindex="-1" so the cell stays the one tab stop, and an accessible name; Space or Enter on the cell moves focus to the control, and Escape in the control returns focus to the cell. Ctrl/Cmd+Z undoes the last committed edit of this table (up to 100) and Ctrl+Y or Ctrl/Cmd+Shift+Z redoes it; each step goes through pk-cell-edit, so the host may refuse it. Pasting tab-separated text with a cell active fills a block from that cell: each cell is checked and offered through pk-cell-edit (source paste), read-only columns and cells beyond the grid are skipped, invalid cells show aria-invalid, the rest commit, one undo takes the whole paste back, and pk-cells-paste summarises it. Shift+arrows, Shift+PageUp/PageDown, Shift+Ctrl+Home/End and Shift+click extend a rectangular selection by row id, so it spans rows that are not drawn; a plain move or Escape collapses it. Ctrl/Cmd+D fills the top row of the selection down its rows, each cell through pk-cell-edit (source fill), read-only columns and refused values skipped, one undo for the whole fill; Ctrl/Cmd+C copies the selection (or the active cell) as tab-separated text. A commit raises pk-cell-edit, and unless the host cancels it the row holds the new value. A host that saves asynchronously calls event.detail.wait(promise) in its pk-cell-edit handler: the value shows at once and the cell is aria-busy (dimmed) until the promise settles; if it rejects, the previous value comes back without a new event, the cell shows the rejection's message (or "Not saved") with aria-invalid, the edit leaves the undo history and a polite status says so. A message on a cell not being edited (a refused paste, a failed save) shows below it while hovered or focused and is always read by assistive technology. At 500 rows or more it windows like a read-only table (see the summary): the active row and the row being edited stay drawn. |
| `resizable` | boolean | `false` |  | Columns get a resize handle: drag it, or Left and Right. |
| `reorderable` | boolean | `false` |  | Columns move: drag a header, or Alt+Left and Alt+Right. |
| `cell-errors` (property `cellErrors`) | json | `{}` |  | Cells the host marks invalid: { "<rowId>:<key>": "message" }. The cell shows the message and aria-invalid. Set it in a pk-cell-edit handler that cancels the event (the editor stays open on the message), or after a server check. The element never changes it. |
| `expandable` | boolean | `false` |  | Rows that have a detail-<rowId> slot get a toggle that shows or hides that slot under the row. |
| `expander` | string | `""` |  | Set to none to leave out the toggle column of an expandable table: the host opens rows from its own control (a disclosure inside a cell) by setting expanded, which is still honoured, and the detail-<rowId> slots are still shown for the ids in it. |
| `expanded` | json | `[]` |  | Ids of the expanded rows. The user changes it and pk-row-expand reports each change; after that the host owns it. |
| `loading` | boolean | `false` |  | Show a placeholder row, announce Loading and mark the table busy. |
| `empty-text` (property `emptyText`) | string | `"No rows"` |  | Text shown when there are no rows. The empty slot replaces it. |
| `cards` | boolean | `false` |  | Each row becomes a card below 640px. A cell with nothing in it (no text, or a cell template that rendered nothing) is left out of its card, label and all. |
| `caption` | string | `""` |  | Table caption. |
| `label` | string | `""` |  | Accessible name of the scrolling region. |
| `max-height` (property `maxHeight`) | string | `""` |  | Bounds the frame; sticky header and column need it. |
| `flow` | boolean | `false` |  | Slotted-table mode: no scroll frame, for a table inside a container that already scrolls. |

**Slots**

| Slot | Description |
|---|---|
| (default) | A raw table (thead, tbody, tfoot, any rows). The page-level layer styles it; the element frames it. Put the num class on a th or td to right-align a numeric cell with tabular figures: the consumer audit rule S3 (class=) exempts exactly that class on a th or td. A th or td also takes width="fit" (shrink to its content, no wrap) or width="sm", "md", "lg" (6, 10 or 16rem) for a compact column, with no class. |
| `toolbar` | Search, filters and buttons above the table, laid out in a wrapping row. Give the item that should sit at the far end (an action button beside a search box) the attribute data-end: it and the items after it move to the end of the row. pk-table-filters (issue 203) fits here: a debounced search box plus a Filters trigger and panel around your own filter fields, so a table gets consistent search/filter UI without a page rebuilding that shell by hand. |
| `bulk` | Actions shown while rows are selected. |
| `caption` | Rich caption. |
| `cell-<rowId>-<key>` | Custom content for one cell of a data-driven table: rowId is the row's rowKey value, key the column key. Replaces the cell text. In an editable table the cell is read-only to the table; a control in it (a pk-switch) is the host's: tabindex="-1" on it keeps the cell the one tab stop, and its own change event carries the value. |
| `header-<key>` | Content beside the title of one column header of a data-driven table (key is the column key): a help tooltip, an icon button, a menu. The text title (and the sort button) stays; the slotted content sits after it, outside the sort button, so using it never sorts, and it is a tab stop of its own. Give an action control an accessible name. Hidden with the header row in the phone card layout. |
| `detail-<rowId>` | Detail content of one row, shown under it when the row is expanded (expandable). A row without this slot has no toggle. |
| `detail-<rowId>.<name>` | A further detail panel of one row, shown under it when expanded holds the string "<rowId>.<name>". A row can have several, plus detail-<rowId>. Named panels have no built-in toggle: the host opens them by setting expanded (use expander="none" to drop the toggle column altogether). |
| `empty` | Empty state; an pk-empty-state fits. Replaces emptyText. |
| `footer` | Below the table; an pk-pagination fits. |
| `foot` | Data-driven mode: a footer or summary row (totals, a note that the figures do not add up). One tfoot cell spanning every column after the rows on a desktop; a full-width trailing card after the last card on a phone. Raw mode authors its own tfoot instead. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-sort` | `{ key: string, direction: string }` | A sortable header was activated. Each activation of the same header cycles ascending, descending, then cleared: a cleared sort reports key and direction null and the rows return to their natural order (a manual table reports it too, and the host restores its own order). Cancel to keep the current sort. |
| `pk-filter` | `{ filters: IDictionary<string, string> }` | A filter input changed (debounced 250 ms). |
| `pk-select` | `{ selected: IReadOnlyList<string> }` | The selection changed. |
| `pk-navigate` | `{ to: string, href: string }` | A plain click on a row link (see rowHrefTemplate); cancel it to navigate yourself, else the browser follows the link. |
| `pk-row-click` | `{ id: string, row: object }` | A clickable row was activated by a click, or by Enter or Space while the row itself has focus. A click or key on a checkbox, button, link or select inside the row does not raise it. |
| `pk-select-all` | `{ scope: string, count: number }` | Raised on a table that has a total (the rows are paged by the host) when the user selects every loaded row (scope "page", count the rows on the page; pk-select carries the ids as well) or chooses "Select all N" (scope "all", count the total; no ids: the host expands the scope by asking its data source again, with the query it has). selectScope is then "all" until the selection changes. |
| `pk-row-expand` | `{ id: string, index: number, expanded: bool }` | The user expanded or collapsed a row. expanded is the new state; the expanded prop already holds it. |
| `pk-cell-edit` | `{ id: string, index: number, row: object, key: string, value: object, previous: object, source: string, wait: object }` | An edited cell is about to be committed (an editable table). value is typed by the column (a number for a number editor, else text) and previous is the value it replaces; the event only fires when they differ, after the element checked required, min, max and maxLength. Cancel it to reject the value: the editor stays open showing cellErrors for the cell, or "Value not accepted". source says where it came from: keyboard, paste, fill, undo or redo (undo and redo carry the value being restored); a paste raises it once per cell. Not cancelled, the table replaces its rows with a copy that holds the value and the host takes it from there. detail.wait(promise) (a function, not serialised) tells the table the host is still saving: see the editable field. |
| `pk-column-resize` | `{ key: string, width: number }` | A column was resized. |
| `pk-column-reorder` | `{ key: string, from: number, to: number, order: IReadOnlyList<string> }` | A column moved; cancel to refuse. |
| `pk-cell-query` | `{ id: string, key: string, query: string }` | The user typed in a combobox cell editor. Call detail.set(options) (strings or { value, label }) with the suggestions to show; use free on the column to accept values outside them. |
| `pk-cells-paste` | `{ cells: object, rejected: object }` | A paste into an editable table finished. cells lists the cells that took a value ({ id, key, value }); rejected lists those that did not ({ row, col, reason }: outside the grid, readonly, a failed check message, or refused by a cancelled pk-cell-edit). Each accepted cell already raised pk-cell-edit with source paste. |

**Methods**

| Method | Description |
|---|---|
| `sortBy(key, direction)` | Sort programmatically. sortBy(null) clears the sort. |

**CSS parts**

| Part | Description |
|---|---|
| `toolbar` | The toolbar wrapper. |
| `bulk` | The bulk bar. |
| `bulk-count` | The selected count. |
| `bulk-all` | The button that widens the selection to every row of the query ("Select all 112 rows"), then clears it ("Clear selection"). Shown only with a total. |
| `scroll` | The scrolling frame. |
| `table` | The table. |
| `caption` | The caption. |
| `head` | The thead. |
| `body` | The tbody. |
| `empty` | The empty wrapper. |
| `footer` | The footer wrapper. |
| `foot` | The single cell of the foot row (in a tfoot), spanning every column. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-table-footer-padding` | `var(--space-3)` | Block padding of the footer bar. |
| `--pk-table-max-height` | `none` | Height of the scrolling frame (set from maxHeight). |

**Accessibility.** A real table with a caption, aria-rowcount and aria-busy. Sortable headers hold a button and carry aria-sort. The scrolling frame is a labelled focusable region so the keyboard can scroll it. A clickable row is focusable (one tab stop per row, before the checkbox and buttons inside it) and Enter or Space on it activates it, with a visible focus ring; the controls inside a row keep their own keys. Each expandable row has a real button (Enter or Space) with aria-expanded and aria-controls pointing at its detail row, and keeps focus after it toggles. The loading row has a status text. Checkboxes name their row; the selection count is a polite status, and the "Select all N rows" / "Clear selection" button inside it is a real button (a tab stop, named with the count) whose change of text is announced by the same status. The card layout keeps the header for assistive technology. Cell values are text, never HTML. The current row (current-row) carries aria-current="true", so a screen reader announces which record is open. An editable table is role="grid": one cell is the tab stop (arrow keys, Home and End move it, Tab moves and commits), the active cell has aria-selected, a cell that cannot be edited has aria-readonly, a cell whose save the host has not answered yet has aria-busy, an invalid cell has aria-invalid and its message is tied to its editor with aria-describedby, every editor is named "<column>, row <id>" (a control the host slots into a cell needs its own name), and a committed or cancelled edit is announced in a polite status region. When the rows are windowed (500 or more) aria-rowcount is the whole view plus the header and every drawn row carries its aria-rowindex; PageUp, PageDown, Ctrl+Home and Ctrl+End move by rows and announce "Row N of M". Resize handles are separators.

Example: Shed columns by the table's width

```html
<pk-grid ratio="1:3"><pk-table label="Accounts" columns='[{"key":"name","label":"Name"},{"key":"plan","label":"Plan","shed":1},{"key":"seats","label":"Seats","type":"number","shed":2},{"key":"owner","label":"Owner","shed":3}]'
  rows='[{"id":1,"name":"Acme","plan":"Team","seats":12,"owner":"Ada"},{"id":2,"name":"Globex","plan":"Pro","seats":4,"owner":"Grace"}]'></pk-table>
<pk-table label="Accounts, wide" columns='[{"key":"name","label":"Name"},{"key":"plan","label":"Plan","shed":1},{"key":"seats","label":"Seats","type":"number","shed":2},{"key":"owner","label":"Owner","shed":3}]'
  rows='[{"id":1,"name":"Acme","plan":"Team","seats":12,"owner":"Ada"}]'></pk-table></pk-grid>
```

## `pk-table-filters`

**Table filters** (Data display). A debounced global search box plus a Filters trigger (with an active-count badge) that opens a panel around your own filter fields, so a data table or list gets consistent search/filter UI without every page rebuilding the trigger-button-plus-badge-plus-panel shell by hand. Slot your own filter fields (selects, date ranges, whatever the fields are) into the default slot; put the element itself in pk-table's toolbar slot. It never searches, filters or clears anything itself: it only reports pk-search, pk-toggle and pk-clear-filters for the host to act on, the same as pk-table's own pk-sort and pk-filter.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `label` | string | `"Search"` | The search box's accessible name, and its placeholder unless placeholder is set. |
| `placeholder` | string | `""` | The text shown in the empty search box, when it should say more than the accessible name (for example "Order number, customer or SKU"). Empty shows the label. |
| `value` | string | `""` | The text in the search box, to preset it or replace it later. Typing writes it at once and pk-search follows after the debounce; a change the host makes raises nothing. |
| `debounce` | number | `250` | Milliseconds between the last keystroke and pk-search. |
| `filter-count` (property `filterCount`) | number | `0` | Active-filter count shown as a badge on the Filters trigger. The host counts its own slotted fields; 0 hides the badge. |
| `open` | boolean | `false` | Whether the filter panel is open. The user opens and closes it with the Filters trigger and the panel's close button, and the element raises pk-toggle; a change the host makes raises nothing. |

**Slots**

| Slot | Description |
|---|---|
| (default) | Your own filter fields (selects, date ranges, text inputs), shown in the filter panel. The Filters trigger only appears once this slot has content. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-search` | `{ query: string }` | The search box changed (debounced 250 ms). |
| `pk-toggle` | `{ open: bool }` | The filter panel was opened or closed. open already holds the new state. |
| `pk-clear-filters` | `none` | Clear filters was activated in the panel. The host owns the slotted fields; it clears their own state. |

**CSS parts**

| Part | Description |
|---|---|
| `search` | The search box |
| `trigger` | The Filters trigger button |
| `trigger-icon` | The filter icon on the trigger |
| `trigger-label` | The trigger's word; icon only on a phone |
| `count` | The active-filter-count badge |
| `panel` | The filter panel |
| `header` | The panel's header row |
| `heading` | The panel's heading text |
| `close` | The panel's close button |
| `body` | The panel's body, holding the default slot |
| `footer` | The panel's footer, holding Clear filters |
| `clear` | The Clear filters button |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-table-filters-radius` | `var(--pk-control-radius, var(--radius-sm))` | Corner radius of the search box and trigger; var(--radius-pill) for a pill. |
| `--pk-control-radius` | `var(--radius-sm)` | The shared control radius. |

**Accessibility.** The search box is labelled by label. The Filters trigger reflects aria-expanded; the panel is a normal document region (not a modal: no focus trap, no backdrop), so the page behind stays usable. The close button is labelled Close filters.

Example: In a table's toolbar

```html
<pk-table columns='[{"key":"sku","label":"SKU"}]' rows='[{"id":1,"sku":"AC-001"}]'>
  <pk-table-filters slot="toolbar" label="Search products" filter-count="1">
    <pk-select label="Category"><option>Accessories</option></pk-select>
  </pk-table-filters>
</pk-table>
```

## `pk-tag`

**Tag** (Data display). A neutral label that can carry a remove button.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `removable` | boolean | `false` | Show the remove button. |
| `value` | string | `""` | Sent in the remove event (the identifier, not state); defaults to the text. |
| `disabled` | boolean | `false` | Dim the tag and disable the button. |
| `controlled` | boolean | `false` | The host removes the tag: a press only raises pk-remove and never removes the element (the host renders the list, so it removes the tag from its own state). Used by the Blazor wrapper. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The label. |

**Events**

| Event | Detail | Description |
|---|---|---|
| `pk-remove` | `{ value: string }` | The remove button was pressed. preventDefault keeps the tag; when nobody cancels it and controlled is off, the tag removes itself after the event. With controlled the host removes it. |

**CSS parts**

| Part | Description |
|---|---|
| `label` | The label. |
| `remove` | The remove button. |

**CSS custom properties**

| Property | Default | Description |
|---|---|---|
| `--pk-tag-bg` | `var(--color-surface-alt)` | Fill. |
| `--pk-tag-border` | `var(--color-border)` | Border colour. |

**Accessibility.** The remove control is a real button named "Remove <label>"; it is 44px on a phone.

Example: Removable

```html
<pk-tag removable>Green</pk-tag>
<pk-tag removable>Variant</pk-tag>
<pk-tag>Signed</pk-tag>
```

## `pk-timeline`

**Timeline** (Data display). A vertical activity feed of timeline items joined by a line.

**Props**

| Attribute | Type | Default | Description |
|---|---|---|---|
| `label` | string | `""` | Accessible name of the list. |

**Slots**

| Slot | Description |
|---|---|
| (default) | pk-timeline-item children, in the order to show them. |

**Accessibility.** The host is role=list; each pk-timeline-item is a listitem with a real time element. State is also given in words in the heading when it matters, never by the marker colour alone.

Example: Order history

```html
<pk-timeline label="Order history">
  <pk-timeline-item heading="Shipped" time="2026-09-16" status="done">Tracking 9400.</pk-timeline-item>
  <pk-timeline-item heading="Packed" time="2026-09-15" status="active"></pk-timeline-item>
</pk-timeline>
```

## `pk-timeline-item`

**Timeline item** (Data display). One event on a timeline: a marker, a heading, a time and a body.

**Props**

| Attribute | Type | Default | Values | Description |
|---|---|---|---|---|
| `heading` | string | `""` |  | Title of the event. |
| `time` | string | `""` |  | An ISO date or any text; shown in a time element. |
| `status` | enum | `"pending"` | `pending` `done` `active` `danger` | Marker colour and fill. |

**Slots**

| Slot | Description |
|---|---|
| (default) | The body. |
| `heading` | A rich heading; overrides heading. |
| `time` | A rich time; overrides time. |

**CSS parts**

| Part | Description |
|---|---|
| `marker` | The dot. |
| `content` | Heading, time and body. |
| `head` | The heading row. |
| `heading` | The heading. |
| `time` | The time. |
| `body` | The body. |

**Accessibility.** role=listitem through ElementInternals; the time uses a real time element with a datetime attribute.

Example: Statuses

```html
<pk-timeline-item heading="Delivered" time="2026-09-18" status="done"></pk-timeline-item>
<pk-timeline-item heading="Payment failed" status="danger">Retried and cleared.</pk-timeline-item>
```
