# Components: Layout, structure and containers

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from Generated/*.razor, blazor/mappings, generated.manifest.json and core/dist/elements/api.json. Do not edit: change the source and run the generator.

Every component renders its element with the parameters you set as attributes. An attribute that is not a parameter (`id`, `class`, `data-*`, `aria-*`, ...) is put on the element as it is, and a `class` is added to the component's own (`ExtraClass` on a component that lists it). Names are `Pk` plus the tag in PascalCase.

## `PkAccordion`

`<pk-accordion>`: **Accordion** (Containers). A stack of collapsible sections; exclusive mode keeps one open.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Exclusive` | `bool` |  | attribute `exclusive` | Opening one item closes the others. |
| `Flush` | `bool` |  | attribute `flush` | No gaps: one shared frame. |
| `ChildContent` | `RenderFragment?` |  | default slot | pk-accordion-item children. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-accordion` in the `plainkit-sdk` skill.

## `PkAccordionItem`

`<pk-accordion-item>`: **Accordion item** (Containers). A collapsible section on a native details element.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Heading` | `string?` |  | attribute `heading` | Header text. |
| `Open` | `bool` |  | attribute `open` | Expanded. |
| `OpenChanged` | `EventCallback<bool>` |  | two-way pair of `Open` | Raised with the new value of Open (two-way: Open follows it). |
| `ChildContent` | `RenderFragment?` |  | default slot | The panel. |
| `HeadingContent` | `RenderFragment?` |  | slot `heading` | Rich heading. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Header controls that do not toggle. |
| `IconContent` | `RenderFragment?` |  | slot `icon` | Icon before the heading. |

Two-way binding: `@bind-Open`.

When you set a named fragment (`HeadingContent`, `ActionsContent`, `IconContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-toggle` | `PkToggleEventArgs` | Open: bool?, Expanded: bool?, Pressed: bool?, Value: string?, Id: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-accordion-item` in the `plainkit-sdk` skill.

## `PkAppFrame`

`<pk-app-frame>`: **App frame** (Layout & structure). The app frame from data: header (brand, module picker, search, settings), side nav, breadcrumbs, page and footer. It routes nothing; the host navigates.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Brand` | `string?` |  | attribute `brand` | The brand text in the header. |
| `BrandHref` | `string?` |  | attribute `brand-href` | Where the brand links (same-site path or http(s)); empty is plain text. |
| `NavLabel` | `string?` |  | attribute `nav-label` | The side nav landmark name. |
| `Nav` | `string?` |  | attribute `nav` | Side nav rows: { id, title, href?, icon?, badge?, children? }[]. Empty draws no nav. JSON text, [{ id, title, href, icon, badge, children }]. PkApp serialises its PkNavModel into it; a typed record follows with PkApp. |
| `CurrentId` | `string?` |  | attribute `current-id` | Id of the current row. |
| `Trail` | `string?` |  | attribute `trail` | Ids of the branches to open (ancestors of the current row). JSON text, the ids of the branches to open. |
| `Modules` | `string?` |  | attribute `modules` | The module picker: { id, title, href }[]; fewer than two draws none. On a phone it is the first row of the drawer. JSON text, [{ id, title, href }]. |
| `ModuleTitle` | `string?` |  | attribute `module-title` | The active module, shown on the picker. |
| `Crumbs` | `string?` |  | attribute `crumbs` | Breadcrumbs: { label, href? }[]; the last is the current page. Empty draws none. JSON text, [{ label, href }]. |
| `Search` | `bool` |  | attribute `search` | Shows the header search. |
| `SearchPlaceholder` | `string?` |  | attribute `search-placeholder` | The search field placeholder and name. |
| `Busy` | `bool` |  | attribute `busy` | Blocks the page behind a loading overlay. |
| `BusyLabel` | `string?` |  | attribute `busy-label` | Its label. |
| `SearchItems` | `string?` |  | attribute `search-items` | Search results, pk-app-bar-search items; the host sets them on pk-query. JSON text, pk-app-bar-search items. |
| `SearchError` | `string?` |  | attribute `search-error` | A failed search: while set, the header search's results panel shows this message instead of "No results" (pk-app-bar-search error). Set it when the search behind pk-query throws; clear it with the next result. The panel then shows a Retry button: pressing it raises pk-query again with the text (the search's own pk-retry also bubbles out of the frame). The failed-search message; empty or null for none. |
| `ChildContent` | `RenderFragment?` |  | default slot | The page, with the skip link target (main#pk-main). |
| `FooterContent` | `RenderFragment?` |  | slot `footer` | Footer content. |
| `OnQuery` | `EventCallback<PkQueryEventArgs>` |  | event `pk-query` | The search text changed; set searchItems. |
| `OnSelect` | `EventCallback<PkSelectEventArgs>` |  | event `pk-select` | A search result was chosen. |
| `OnThemeToggle` | `EventCallback` |  | event `pk-theme-toggle` | Toggle theme was chosen. |

When you set a named fragment (`FooterContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-query` | `PkQueryEventArgs` | Query: string? |
| `pk-select` | `PkSelectEventArgs` | Item: JsonElement?, Value: string?, Checked: bool?, Id: string?, Selected: string[]?, Scope: string?, Query: JsonElement? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-app-frame` in the `plainkit-sdk` skill.

## `PkAppShell`

`<pk-app-shell>`: **App shell** (Layout & structure). The page frame: nav column, header, one scrolling body and footer. Header and footer stay put; safe-area insets are added on notched phones; a slotted control marked data-nav-toggle opens the side nav drawer, or hides and shows the nav on a wide screen.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `SidebarContent` | `RenderFragment?` |  | slot `nav` | A pk-side-nav. |
| `HeaderContent` | `RenderFragment?` |  | slot `header` | Top bar content after the title: page actions, an account menu. A direct child marked data-nav-toggle stays first. |
| `BackHref` | `string?` |  | attribute `back-href` | Puts a back link (an arrow) before the title: a same-site path or an http(s) address, usually the parent list. Empty shows no link. Anything else is dropped with a warning. |
| `BackLabel` | `string?` |  | attribute `back-label` | The back link's accessible name (its aria-label), for example "Back to Invoices"; the arrow is the only visible content. |
| `BodyContent` | `RenderFragment?` |  | default slot | The scrolling body. It is a flex column of definite height: ordinary content keeps its height and the body scrolls, while a page type that fills (sets --pk-fill-shrink: 1 and flex: 1 1 auto, min-height: 0, such as a list page with stickyHeader) takes the height left, so its header row and pager stay pinned and only its rows scroll. A host wrapper element between the body and the page needs the same two properties, or none (display: contents). |
| `FooterContent` | `RenderFragment?` |  | slot `footer` | Footer content. |
| `NavOpen` | `bool` |  | attribute `nav-open` | Drawer state, mirrors the side nav. |
| `NavOpenChanged` | `EventCallback<bool>` |  | two-way pair of `NavOpen` | Raised when NavOpen changes (two-way binding: `@bind-NavOpen`). |
| `NavHidden` | `bool` |  | attribute `nav-hidden` | Above the drawer breakpoint (1024px) the side nav is hidden and the body takes the full width; the nav toggle flips it and the nav comes back in its previous expanded or icon-rail state. Remembered under the nav's persist key plus ":nav-hidden". On a phone or tablet the nav is the drawer (navOpen) and this has no effect. |
| `NavHiddenChanged` | `EventCallback<bool>` |  | two-way pair of `NavHidden` | Raised when NavHidden changes (two-way binding: `@bind-NavHidden`). |
| `Contain` | `bool` |  | attribute `contain` | Makes the body a named inline-size container (name from --pk-app-shell-container, default page) so @container page (...) answers to the body width with no wrapper element, clips sideways overflow, and lets any single slotted child fill the height (flex 1, min-height 0) so a list page or a fixed region plus a scrolling one needs no :has() hack. Off by default; composes with flush. |
| `Flush` | `bool` |  | attribute `flush` | Removes the body padding so a workspace, table or map sits edge to edge (the safe-area insets on a notched phone stay). |

Two-way binding: `@bind-NavOpen`, `@bind-NavHidden`.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-nav-toggle` | `PkNavToggleEventArgs` | Open: bool?, Hidden: bool?, Collapsed: bool? |

**Not available as parameters** (they do not exist; do not use them)

| Parameter | Why |
|---|---|
| `ErrorOverlayMessage` | wrapper behaviour, not a property of the element (As ShowErrorOverlay.) |
| `ShowErrorOverlay` | wrapper behaviour, not a property of the element (Re-decided (#1386): stays out. The framework error bar (#blazor-error-ui) belongs to the host page and its layout CSS, and pk-app-shell has no such feature; keep it in your layout.) |

Element details (parts, CSS custom properties, methods, accessibility): `pk-app-shell` in the `plainkit-sdk` skill.

## `PkCanvas`

`<pk-canvas>`: **Canvas** (Layout & structure). A pan and zoom ground for design tools: slotted content sits in a world you drag, pinch or Ctrl+wheel around, over an optional grid. pointToWorld and worldToPoint convert units, data-surface-selected nodes get a frame, and the page owns the selection.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Zoom` | `double` |  | attribute `zoom` | The scale: 1 is actual size. |
| `ZoomChanged` | `EventCallback<double>` |  | two-way pair of `Zoom` | Raised when Zoom changes (two-way binding: `@bind-Zoom`). |
| `PanX` | `double` |  | attribute `pan-x` | Where the world origin sits, px from the left. |
| `PanXChanged` | `EventCallback<double>` |  | two-way pair of `PanX` | Raised when PanX changes (two-way binding: `@bind-PanX`). |
| `PanY` | `double` |  | attribute `pan-y` | Where the world origin sits, px from the top. |
| `PanYChanged` | `EventCallback<double>` |  | two-way pair of `PanY` | Raised when PanY changes (two-way binding: `@bind-PanY`). |
| `Grid` | `double` |  | attribute `grid` | The grid spacing in world units (0 none); pointToWorld snaps to it. |
| `AutoGrid` | `bool` |  | attribute `auto-grid` | An adaptive grid: lines at the 1, 2, 5, 10, 20, 50 ... step that stays 8 to 20 px apart at every zoom, with a stronger line every fifth step or more (colour: --pk-canvas-grid-major). Overrides grid; pointToWorld snaps to the fine step. |
| `Unit` | `string?` |  | attribute `unit` | The unit of the rulers and the auto grid. Only labels change: the stored world numbers never do. |
| `PxPerUnit` | `double` |  | attribute `px-per-unit` | World units per display unit: 0 derives it from unit (mm 96/25.4, in 96, pt 96/72, otherwise 1). Set 1 when the world is already in the unit. |
| `Rulers` | `bool` |  | attribute `rulers` | Rulers along the top and left with a pointer readout, in unit; hidden on a phone, thickness --pk-canvas-ruler-size (1.5rem). Loaded on first use. |
| `Selectable` | `bool` |  | attribute `selectable` | Items that declare a world rect (data-id with data-x, data-y, data-w, data-h, or an SVG child's own x, y, width, height, or a geometry(el) callback property) can be selected: click, Shift or Ctrl click to toggle, a marquee drag on bare ground, Ctrl+A, Escape, and Tab focus. The layer loads on first use. The canvas never writes to the items. |
| `Mode` | `string?` |  | attribute `mode` | What a primary drag on bare ground does in a selectable canvas: select draws a marquee (touch, the middle button and the toolbar still pan), pan drags the view. |
| `Selection` | `IReadOnlyList<string>?` |  | attribute `selection` | The ids of the selected items (an array, or a JSON attribute). The canvas sets it from a gesture and emits pk-canvas-select; set it to select from code. The ids of the selected items, as JSON text; items declare their world rect with data-id, data-x, data-y, data-w, data-h (the geometry callback is JS only). |
| `ReadOnly` | `bool` |  | attribute `readonly` | View only: pan, zoom and selection still work, but the selection cannot be moved or resized (no pk-canvas-move or pk-canvas-resize, no handles). |
| `Snap` | `string?` |  | attribute `snap` | What a dragged selection snaps to, a space list: grid, guides, edges, centres (edges and centres of the other items and of the targets property, a JS-only array of extra { x, y, w, h } world rects). Empty: no snapping. The lines that hold it are drawn while dragging. |
| `SnapThreshold` | `double` |  | attribute `snap-threshold` | How near, in screen pixels, a snap line pulls. |
| `Guides` | `object?` |  | attribute `guides` | Guide lines [{ axis: 'x' \| 'y', at }] in world units, drawn across a selectable surface and snapped to with snap="guides". Sent as a JSON attribute: [{ axis: 'x' \| 'y', at }] in world units. The targets property (extra snap rects) is JavaScript only. |
| `MinZoom` | `double` |  | attribute `min-zoom` | The smallest scale. |
| `MaxZoom` | `double` |  | attribute `max-zoom` | The largest scale. |
| `Wheel` | `string?` |  | attribute `wheel` | The plain wheel: auto pans only with focus (else the page scrolls), pan, zoom, or page (never taken). Ctrl or Cmd with the wheel, and pinch, always zoom. |
| `Label` | `string?` |  | attribute `label` | The accessible name. Required. |
| `ChildContent` | `RenderFragment?` |  | default slot | The content. Add data-surface-selected to a descendant to frame it. |
| `OnViewChange` | `EventCallback<PkViewChangeEventArgs>` |  | event `pk-view-change` | Panned or zoomed. |
| `OnPick` | `EventCallback<PkCanvasPickEventArgs>` |  | event `pk-canvas-pick` | A press without movement: the deepest node under it, or null on bare ground; additive with Shift, Ctrl or Cmd. |
| `OnKey` | `EventCallback<PkCanvasKeyEventArgs>` |  | event `pk-canvas-key` | Delete, Escape, or Shift with an arrow (when there is no selection to move), on the focused surface, for the page to act on. |
| `OnSelect` | `EventCallback<PkCanvasSelectEventArgs>` |  | event `pk-canvas-select` | The selection changed by a gesture (click, marquee, Ctrl+A, Escape, Tab focus): the new ids. |
| `OnMove` | `EventCallback<PkCanvasMoveEventArgs>` |  | event `pk-canvas-move` | Cancelable: the selection is asked to move to these world positions. Arrow keys send done true; dragging an already selected item sends done false while it runs and done true at the end (Escape puts it back with done true). The page writes them back to the items' data-x and data-y (or an SVG item's x and y); the canvas never moves items itself. |
| `OnResize` | `EventCallback<PkCanvasResizeEventArgs>` |  | event `pk-canvas-resize` | Cancelable: the one selected item is asked to take this world rect, from a drag of one of its eight handles (done false while it runs, done true at the end; Shift keeps the ratio, Alt the centre, Escape puts it back), or from Ctrl or Cmd with an arrow key (done true: one grid step, Shift ten, bottom-right corner). The page writes it back; the canvas never resizes items itself. |

Two-way binding: `@bind-Zoom`, `@bind-PanX`, `@bind-PanY`.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-canvas-key` | `PkCanvasKeyEventArgs` | Key: string?, ShiftKey: bool?, AltKey: bool?, CtrlKey: bool? |
| `pk-canvas-move` | `PkCanvasMoveEventArgs` | Moves: JsonElement?, Done: bool? |
| `pk-canvas-pick` | `PkCanvasPickEventArgs` | Target: JsonElement?, Additive: bool? |
| `pk-canvas-resize` | `PkCanvasResizeEventArgs` | Id: string?, X: double?, Y: double?, W: double?, H: double?, Done: bool? |
| `pk-canvas-select` | `PkCanvasSelectEventArgs` | Ids: string[]? |
| `pk-view-change` | `PkViewChangeEventArgs` | X: double?, Y: double?, Zoom: double? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-canvas` in the `plainkit-sdk` skill.

## `PkCard`

`<pk-card>`: **Card** (Containers). A titled panel with optional media, header actions and footer. Vertical or horizontal (stacking by its own width), flush for tables, and optionally one link.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Heading` | `string?` |  | attribute `heading` | The card title. Without it (and without actions) the header is not shown. |
| `Level` | `int` |  | attribute `level` | The heading level the title exposes to assistive technology (aria-level). |
| `Tone` | `CardTone?` | `Default` `Error` | attribute `tone` | error draws the border in the error colour. |
| `ChildContent` | `RenderFragment?` |  | default slot | The card body. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Buttons or links at the right of the header. |
| `FooterContent` | `RenderFragment?` |  | slot `footer` | Content below the body; hidden when empty. |
| `Orientation` | `CardOrientation?` | `Vertical` `Horizontal` | attribute `orientation` | Horizontal puts the media beside the content; it stacks when the card is narrower than 30rem (a container query, not the viewport). |
| `Flush` | `bool` |  | attribute `flush` | No padding around the body, for a table or an edge-to-edge body; the heading row keeps its padding. |
| `Href` | `string?` |  | attribute `href` | Makes the whole card one link (a single tab stop named by the heading). |
| `MediaContent` | `RenderFragment?` |  | slot `media` | An image, picture or video at the top (or the start when horizontal). |
| `Fill` | `bool` |  | attribute `fill` | Fills a flex parent (a workspace pane): the card takes the remaining height, has no padding, and its body is the scroll region. |

When you set a named fragment (`ActionsContent`, `FooterContent`, `MediaContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-card` in the `plainkit-sdk` skill.

## `PkCluster`

`<pk-cluster>`: **Cluster** (Layout). A wrapping row (or column) of controls with a fixed gap: the layout primitive for button rows, chip lists and toolbars.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Direction` | `ClusterDirection?` | `Horizontal` `Vertical` | attribute `direction` | Row (wraps at the edge) or column. |
| `Gap` | `ClusterGap?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `gap` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default sm (the control gap). |
| `Align` | `ClusterAlign?` | `Start` `Center` `End` `Stretch` `Baseline` | attribute `align` | Cross-axis alignment of the items. |
| `Justify` | `ClusterJustify?` | `Start` `Center` `End` `Between` | attribute `justify` | Main-axis distribution of the items. |
| `NoWrap` | `bool` |  | attribute `nowrap` | Keep a horizontal cluster on one line instead of wrapping (wrapping is the default). |
| `ChildContent` | `RenderFragment?` |  | default slot | The items to lay out. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-cluster` in the `plainkit-sdk` skill.

## `PkContainer`

`<pk-container>`: **Container** (Containers). A max-width, padded content region, optionally scrolling on its own. Wraps a gallery stage, a code view or a doc-like page instead of a hand-styled div with max-width, padding and overflow.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Size` | `ContainerSize?` | `Sm` `Md` `Lg` `Full` | attribute `size` | Max width, on the content scale (sm --content-sm, md --content-md, lg --content-lg). full removes the max width. |
| `Padding` | `ContainerPadding?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `padding` | Padding on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). |
| `Align` | `ContainerAlign?` | `Center` `Start` | attribute `align` | center margins the region within its parent (the usual page layout); start pins it to the inline start instead. |
| `Scroll` | `ContainerScroll?` | `None` `Y` `Both` | attribute `scroll` | Makes the region scroll on its own instead of growing with its content: y for vertical overflow, both for vertical and horizontal. Needs a parent with a definite height (or fill) to have anything to scroll within. A scrolling region gets tabindex=0 and role=region, and needs label. |
| `Label` | `string?` |  | attribute `label` | The accessible name for the scrolling region; required when scroll is not none (a warning is logged once when it is missing). Unused otherwise. |
| `Fill` | `bool` |  | attribute `fill` | Fills a flex or grid parent with a definite height (a workspace pane, an app shell body) instead of sizing to content, so scroll has a height to scroll within. |
| `ChildContent` | `RenderFragment?` |  | default slot | The content. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-container` in the `plainkit-sdk` skill.

## `PkCtaBand`

`<pk-cta-band>`: **CTA band** (Containers). A full-width call-to-action surface: your own heading, a line of text and one or two actions. Neutral or brand surface, start or centred, optionally edge to edge. Your markup is never changed; before it upgrades (or with JavaScript off) the heading, text and links show as plain blocks.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Tone` | `CtaBandTone?` | `Neutral` `Brand` | attribute `tone` | The surface: neutral is the alternate surface colour, brand the accent fill with white text; primary buttons inside it turn light so they stand out. |
| `Align` | `CtaBandAlign?` | `Start` `Center` | attribute `align` | Aligns the text and the actions. |
| `Bleed` | `bool` |  | attribute `bleed` | Runs the band edge to edge of the viewport from inside a padded column. Off, it fills its column. |
| `HeadingContent` | `RenderFragment?` |  | slot `heading` | Your own heading (h2 or the level that fits the page). The element never sets a level or invents one; with none it logs one warning. |
| `ChildContent` | `RenderFragment?` |  | default slot | The text under the heading. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | A wrapper (a span) of one or two buttons or links. They sit side by side; give each button `block` and on a phone each is full width. |

When you set a named fragment (`HeadingContent`, `ActionsContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-cta-band` in the `plainkit-sdk` skill.

## `PkDashboardPage`

`<pk-dashboard-page>`: **Dashboard page** (Page types). A dashboard of widgets, each a pk-card holding a pk-stat, a pk-chart or a pk-table, optionally grouped into tabs and headed sections, with an optional filter bar. Set the load(key) callback as a property (business logic, never config data): it is called with this bound to the page element, once per widget and independently, and resolves to the data merged onto that widget's element (a pk-stat's value/delta/tone/values, a pk-chart's data/caption, or for kind 'table' { columns: [{ key, label, align? }], rows: object[], caption? } drawn in a full-row pk-table captioned by caption or the widget label; no rows shows the card's empty state). A widget in a tab loads the first time its card is within about 200px of the viewport while that tab is shown (an above-the-fold widget at once, a lower one when it scrolls near; cards keep their reserved height, so a late start shifts nothing), never for a tab never opened, never again on a revisit, and a start still waiting when the user leaves the tab is dropped; without IntersectionObserver every widget of the shown tab starts at once. Each widget is its own async boundary through its card's state: loading, the content once it resolves, or an error with Retry on rejection - one slow or rejecting widget never blocks or breaks another. The current filter selections are on this.context (read them inside load); changing a filter reloads every widget that has already loaded.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { tabs?, tab?, widgets, sections?, filters?, values?, empty? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. tab is the tab id to open first; values is { filterKey: value }, the selections the filter bar starts with; a widget may set size ('md' spans two columns and 'lg' the whole row once the section is 48rem wide, 'sm' takes one, and a phone-width section has one column); a load(key) that resolves null hides that widget's card. widgets is [{ key, tab?, label, kind: 'stat'\|'chart'\|'table', empty? }][] - key is passed to load(key), tab is the id of the tab it belongs to (the first tab when missing or unknown), label is the card heading and the pk-stat label or pk-chart caption, kind picks the composed element (default stat; a 'table' card spans the whole row), empty is { heading?, description? } shown in the card when there is no load callback. tabs is [{ id, label }][]; without tabs every widget renders in one ungrouped grid, in array order. sections is [{ heading?, tab?, widgets: string[] }][] naming the keys each groups, scoped to a tab (a tab with no section lists all its widgets). filters is [{ key, type: 'text'\|'email'\|'number'\|'date'\|'select', label, options? }][], the same shape as pk-list-page's, drawn as a bar above the tabs. empty is { heading?, description? } shown in place of everything when widgets is empty. Breaking change: this replaces the earlier config.tiles shape, with no alias. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute: { tabs?: [{ id, label }], widgets: [{ key, tab?, label, kind?: 'stat'\|'chart'\|'table', empty? }], sections?: [{ heading?, tab?, widgets: string[] }], filters?: [{ key, type, label, options? }], empty? }. Breaking: this replaces the earlier { tiles } shape. |
| `Load` | `Func<string, Task<object?>>?` |  |  | Loads one widget by its key (config.widgets[].key), independently of the others, and returns what the widget draws: for a stat an object with value (and delta, trend...), for a chart the chart properties, for a table widget (kind 'table') { columns: [{ key, label, align? }], rows, caption? } (no rows shows the card empty state). Throwing shows that card's error state with Retry; the other widgets are unaffected. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-dashboard-page` in the `plainkit-sdk` skill.

## `PkDesignSurface`

`<pk-design-surface>`: **Design surface** (Layout). A scrolling ground that frames a page being edited at full, tablet or phone width, draws selection, drop-target, hidden and empty marks around the page's nodes, and places an action chip beside a node.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Width` | `DesignSurfaceWidth?` | `Full` `Tablet` `Phone` | attribute `width` | The page's width frame: full fills the surface, tablet and phone cap it (768px and 375px, from --pk-design-surface-tablet-w and --pk-design-surface-phone-w) and centre it. |
| `Label` | `string?` |  | attribute `label` | The surface's accessible name (it is a focusable role=group); warns once when empty. |
| `Dragging` | `bool` |  | attribute `dragging` | An external drag is in progress: the chip is hidden. |
| `Interactive` | `bool` |  | attribute `interactive` | Lets the page be used: links and controls inside it work. By default the page wrapper is inert (a click selects, it follows nothing). Not named inert because inert on the host would make the whole surface and its chip inert, and a boolean attribute defaults to off. |
| `Selected` | `string?` |  |  | The id of the page node framed as selected (a solid accent box). The node is a child of the surface that carries that id; an unknown id clears the frame. The host owns it: the surface never changes it. |
| `DropTarget` | `string?` |  |  | The id of the container an external drag would drop into (a dashed accent box). |
| `ChipFor` | `string?` |  |  | The id of the node the action chip (the Chip slot) follows; null or an unknown id hides the chip. |
| `OnPick` | `EventCallback<PkSurfacePickEventArgs>` |  | event `pk-surface-pick` | A click or tap on the page, hit-tested to the deepest page node under it (null for bare ground); additive is Shift, Ctrl or Cmd. Selecting is the host's: the surface never changes selected. |
| `OnHover` | `EventCallback<PkSurfaceHoverEventArgs>` |  | event `pk-surface-hover` | The pointer moved to another page node (null when it left). At most once per animation frame. |
| `OnKey` | `EventCallback<PkSurfaceKeyEventArgs>` |  | event `pk-surface-key` | An arrow key, Delete or Escape on the focused surface. Cancelable: a host that handles the key calls preventDefault, which also stops the native scroll. |
| `ChildContent` | `RenderFragment?` |  | default slot | The page being edited. Nodes at any depth may carry data-surface-hidden (drawn veiled with a dotted frame) or data-surface-empty (a dashed placeholder frame); the host owns those attributes. |
| `Chip` | `RenderFragment?` |  | slot `chip` | The action chip: real buttons the host owns (Edit, Delete). Placed at the top-right of the chipFor node, above it, inside its top edge when there is no room above, or below it when inside would cover its top-left; clamped to the surface. |
| `Empty` | `RenderFragment?` |  | slot `empty` | Shown while the default slot is empty (for example the text of a drag-an-element-here hint). |

When you set a named fragment (`Chip`, `Empty`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-surface-hover` | `PkSurfaceHoverEventArgs` | Target: JsonElement? |
| `pk-surface-key` | `PkSurfaceKeyEventArgs` | Key: JsonElement?, ShiftKey: JsonElement?, AltKey: JsonElement?, CtrlKey: JsonElement? |
| `pk-surface-pick` | `PkSurfacePickEventArgs` | Target: JsonElement?, Additive: JsonElement? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-design-surface` in the `plainkit-sdk` skill.

## `PkDetailLayout`

`<pk-detail-layout>`: **Detail layout** (Layout & structure). A record-detail page's body: main content and a sticky sidebar of summary cards, Below a container width (not the viewport) it collapses to one column; sidebarFirst puts the sidebar above the main content once collapsed, and sidebarTwoUp lays its own cards two-up instead of stacking one-per-row. Elements carrying data-pk-section are grouped into phone tabs once collapsed. Before it upgrades (or with JavaScript off) its slotted content shows as plain markup, not hidden.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `SidebarFirst` | `bool` |  | attribute `sidebar-first` | Once collapsed to one column, the sidebar comes before the main content instead of after. |
| `SidebarTwoUp` | `bool` |  | attribute `sidebar-two-up` | Once collapsed to one column, the sidebar's own cards lay out two per row instead of stacking one-per-row. |
| `Section` | `string?` |  | attribute `section` | The selected section once the layout has collapsed to one column and its cards name two or more data-pk-section values. Empty selects the first section. |
| `TabsFrom` | `DetailLayoutTabsFrom?` | `Collapse` `Phone` `Tablet` `Always` | attribute `tabs-from` | Where the data-pk-section tabs apply. collapse: once the layout is one column (container query). phone or tablet: at or below that viewport width. always: in two columns too, sidebar cards of the chosen section beside it. Elsewhere every section shows, with no strip. |
| `HideTabs` | `bool` |  | attribute `hide-tabs` | The strip is the host's: the layout draws none and, where tabs-from says the strip applies (below the collapse width by default), shows one data-pk-section at a time from section alone, and still draws Next. Elsewhere every section shows. Supply the strip yourself, for example the docked tabs of a pk-page-header, and set section from its pk-tab-change. |
| `SectionsAbove` | `bool` |  | attribute `sections-above` | Also manage data-pk-section elements that are siblings before the layout (for example a full-width summary strip above it), so they show and hide with the chosen section. |
| `TabParam` | `string?` |  | attribute `tab-param` | A query parameter name ("tab") that keeps the chosen section in the address bar, so a reload or a shared link opens on it. (replaceState, no history entry). |
| `HideNext` | `bool` |  | attribute `hide-next` | Draws no Next button after the main column. The sections still change from the tab strip (or from section); use it when the strip, on a phone too, is enough. |
| `NextLabel` | `string?` |  | attribute `next-label` | The text before the next section's name on the button after the main column ("Next: Pricing"). |
| `OnSectionChange` | `EventCallback<PkSectionChangeEventArgs>` |  | event `pk-section-change` | The user chose a section, with a tab, the Next button, or by submitting a form whose invalid control was in a hidden section. Cancelable: preventDefault keeps the previous section. |
| `ChildContent` | `RenderFragment?` |  | default slot | The main content. |
| `Sidebar` | `RenderFragment?` |  | slot `sidebar` | Summary/status cards; pk-card fits. Sticky in the two-column layout: it sticks by its top edge, or by its bottom edge when it is taller than the viewport, so every card can be reached. |

When you set a named fragment (`Sidebar`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-section-change` | `PkSectionChangeEventArgs` | Section: string?, Previous: string?, Id: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-detail-layout` in the `plainkit-sdk` skill.

## `PkDivider`

`<pk-divider>`: **Divider** (Layout). A rule between content, horizontal or vertical, with an optional label.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Vertical` | `bool` |  | attribute `vertical` | A vertical rule; the parent must be a flex row. |
| `LabelPosition` | `DividerLabelPosition?` | `Center` `Start` | attribute `label-position` | Where the label sits. |
| `Decorative` | `bool` |  | attribute `decorative` | Remove the separator role. |
| `ChildContent` | `RenderFragment?` |  | default slot | The label. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-divider` in the `plainkit-sdk` skill.

## `PkDocPage`

`<pk-doc-page>`: **Doc page** (Page types). A page of long-form documents: a filterable side nav of items, an article with a title, summary and body, a table of contents and a previous/next pager. Mounted by the app framework as the `doc` page type (route param `id`, query `anchor`; pk-navigate goes through the router). Set loadItem(id), href(id, anchor?) and, for a search of your own, searchItems(query) as properties (business logic, never config data); a same-page link inside the article scrolls it and fires pk-navigate instead of touching history itself. Static slotted mode (for server-rendered, JavaScript-optional sites): write the page as plain child markup with no config items, a pk-side-nav in slot="nav", a pk-toc in slot="aside" and the article and pk-pager in the default slot, and the page lays them out as written, takes the current item from your aria-current marking and never creates, moves or changes your elements; on a phone it draws its own Menu button (in its shadow tree) that opens your nav as a drawer.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Config` | `object?` |  | attribute `config` | { items: [{ id, title, summary }], id: currentId\|null, anchor: headingId\|null, search: true, home?: { title, summary, cards?: true }, level?: 1\|2\|3, navLabel?, pagerLabel?, searchLabel?, breadcrumb?: true, embedded?: true, scroller?: selector }. embedded: true is for a host that draws the item nav and the breadcrumb itself (the app shell): neither is drawn here and the search box sits in the article header, under the title. scroller names the host's own scrolling box (a selector) that the table of contents follows instead of the whole page. home.cards shows the home list as a grid of pk-cards (title, summary, link) instead of a plain list; breadcrumb: true draws a pk-breadcrumb above the title (the home title as a link, then the current page); navLabel (default Contents), pagerLabel (default Page navigation) and searchLabel (default Search) name the nav, the pager and the host search box. level is the article title's heading level (default 1 for a page of its own; use 2 when the doc page sits inside a page that already has an h1), read once when the element is built. id null shows the home list (every item, linked); id set loads and shows that item through loadItem(id). anchor scrolls the article to that heading once the item is shown, retried once on the very first load (the elements around the article are still upgrading). Sent as a JSON attribute: { items: [{ id, title, summary }], id: currentId\|null, anchor: headingId\|null, search?, home?: { title, summary }, level?: 1/2/3 (the title heading level; default 1) }. |
| `LoadItem` | `Func<string, Task<object?>>?` |  |  | Loads the page for an item id (config.items[].id): an object with title, summary and html (the body, trusted markup). Null or nothing shows the 'not found' state; throwing shows the error state with Retry. |
| `OnNavigate` | `EventCallback<PkNavigateEventArgs>` |  | event `pk-navigate` | A same-page link inside the article body (the table of contents, a heading's own permalink) was followed. The host rewrites the address (history.pushState) and keeps anchor in the config it sets next; the element never touches history itself. A same-page link in the article (table of contents, heading permalink) was followed: Id is the current item, Anchor the heading id, Replace false. Rewrite the address yourself (NavigationManager) and put Anchor in the next Config; the element never touches history. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-navigate` | `PkNavigateEventArgs` | To: string?, Href: string?, Id: string?, Anchor: string?, Replace: bool? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-doc-page` in the `plainkit-sdk` skill.

## `PkDock`

`<pk-dock>`: **Dock** (Layout). A workspace of panels laid out as a tree of resizable splits and tab groups. Drag a separator or use its arrow keys to resize, choose a tab to switch a group, drag a panel's header or tab onto another group (its center to add as a tab, an edge to split and dock there) or use a group's Move button to do the same without a mouse, Close/reopen a panel from the toolbar's Panels menu, or use a single-panel header's chevron to collapse it to just its title. A collapsed group at a screen edge folds to a narrow rail button instead; activating it opens the panel as a flyout over the content, closing on Escape, an outside click or a blur. The flyout opens with an Expand button ahead of the panel's own content: activating it restores the panel to a normal docked header (a separate action from opening or closing the flyout, and the only way back from a collapsed rail). A toolbar-start slot lets a host add its own menus (File/Edit/View-style) alongside it - the dock draws none of that content itself. A panel can also float: taken out of its group from its own Panel menu's Float item (or a pointer drag once it already floats), it draws as its own window-like overlay inside the dock's own bounds (never over the page), moved and resized by dragging its header/corner grip or, once its frame has focus, by arrow keys (Alt+Arrow resizes, Shift steps bigger); its own Panel menu offers docking it back into the tree at any group and zone instead of the usual Move items. Focusing or grabbing a floater brings it to the front. The layout can be saved and restored across reloads through persistKey. The arrangement is one JSON layout value. Free-running by default (a change applies, then fires pk-layout-change); a host that wants every change to round-trip first sets confirmLayout(arg) as a callback property (business logic, not config data) - arg is { layout: json, reason }, and the settled result (a layout JSON string, or null/undefined to keep the previous one) is what gets applied and reported.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Layout` | `string?` |  | attribute `layout` | The arrangement: { version, seq, root, collapsed } where root is a split { id, type: 'split', orientation, size, min, max, a, b } or a tab group { id, type: 'tabs', active, panels }, and collapsed is the list of open panel ids folded to their header. Leave it unset for the default arrangement derived from each panel's data-group. The element validates what it is given (panels that are not declared are dropped, panels the layout lacks are added, bad numbers are repaired). While the user resizes, chooses a tab or collapses a panel the element owns it; after pk-layout-change the host does. Property or a JSON attribute. The layout document as a JSON string (round-tripped by the same validation as the JavaScript side); leave it null for the default arrangement. The bound value is the JSON of the new layout. |
| `LayoutChanged` | `EventCallback<string?>` |  | two-way pair of `Layout` | Raised when Layout changes (two-way binding: `@bind-Layout`). |
| `Label` | `string?` |  | attribute `label` | Accessible name of a tab group (aria-label), for example the workspace's name. A single-panel group is named by its heading. |
| `ResizeLabel` | `string?` |  | attribute `resize-label` | Accessible name of every separator between two panels (aria-label). |
| `Fill` | `bool` |  | attribute `fill` | Fill the height the parent gives it (a parent with a definite height, or a flex or grid parent) instead of --pk-dock-height. |
| `PersistKey` | `string?` |  | attribute `persist-key` | Save and restore the layout across reloads, through the SDK's store (js/store.js), under the key pk-dock:<persistKey>. Leave unset to disable persistence. On connect, a saved layout is used as the initial arrangement when the layout prop is not set (stored data wins over the default arrangement); every later pk-layout-change (resize, activate, panels) is saved, but a layout the host sets is not. Give each pk-dock on a page its own value; changing it starts a fresh, separately-saved layout. |
| `OnLayoutChange` | `EventCallback<PkLayoutChangeEventArgs>` |  | event `pk-layout-change` | The arrangement changed because of the user or because panels were added or removed: reason is resize (a drag or a splitter's arrow keys moved a separator), activate (a tab was chosen), move (a panel was moved to another group from its Move menu or a pointer drag), collapse (a single-panel header's chevron toggled its body), floater (a floating panel was dragged or its grip resized by pointer), float (a panel was detached to float, from its Panel menu's Float item), dockfloat (a floating panel was docked back into the tree, from its own Panel menu), raise (a floating panel came to the front, from a pointer grab or focus landing anywhere inside it) or panels (the declared panels changed and the layout was repaired). A floater's own frame also has arrow-key move and resize (Alt+Arrow, Shift for a bigger step; reason resize or move, like the splitter's own keys), and a live region (part status) announces every one of these except raise. layout is the new layout JSON. Not raised for a layout the host set, nor for the first render. Not cancelable. |
| `ChildContent` | `RenderFragment?` |  | default slot | The content. The panels: children with slot="<panel id>", so the slot is not a fixed one (the element slots each panel by its own id). slot="empty" and slot="toolbar-start" are reserved (the empty state, and a host app's own top-level toolbar menus) and are never treated as a panel. |
| `EmptyContent` | `RenderFragment?` |  | slot `empty` | Shown when there is no panel. The empty state shown when no panel is open (the element hides it while there is a layout). |
| `ToolbarStartContent` | `RenderFragment?` |  | slot `toolbar-start` | The start of the toolbar (part toolbar), before the dock's own Panels menu: a place for a host app's own top-level menus (File, Edit, View...). The dock draws none of this; it only gives it a stable spot next to its own controls. The toolbar shows whenever this slot holds something or a panel is closed, and collapses away when both are empty. The host app own top-level toolbar menus, drawn at the start of the dock toolbar. |
| `ConfirmLayout` | `Func<string, string, Task<string?>>?` |  |  | Turns on controlled mode: every resize, tab switch or panel repair holds before the element applies it, calling this delegate with the proposed layout (as JSON, the same shape as Layout) and the reason (resize, activate or panels). Return the layout json to apply (typically the proposed one, stored first and returned unchanged, or a different one .NET computed), or null to keep the previous layout unchanged. A delegate that throws also keeps the previous layout. Left null (the default), the element is free-running. |

Two-way binding: `@bind-Layout`.

When you set a named fragment (`EmptyContent`, `ToolbarStartContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-layout-change` | `PkLayoutChangeEventArgs` | Layout: JsonElement?, Reason: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-dock` in the `plainkit-sdk` skill.

## `PkFillColumn`

`<pk-fill-column>`: **Fill column** (Layout). A column in a parent of fixed height: the fixed slot keeps its height, the rest fills and scrolls.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Fixed` | `string?` |  | attribute `fixed` | Fixed region height, a CSS length (12rem). Empty: natural height. |
| `Gap` | `FillColumnGap?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `gap` | Space between regions. |
| `FixedContent` | `RenderFragment?` |  | slot `fixed` | Keeps its height above the rest. Empty: hidden. |
| `ChildContent` | `RenderFragment?` |  | default slot | Fills the rest and scrolls. |

When you set a named fragment (`FixedContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-fill-column` in the `plainkit-sdk` skill.

## `PkGrid`

`<pk-grid>`: **Grid** (Layout). Equal-width responsive columns: as many columns of at least min as fit, one column on a phone.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Min` | `string?` |  | attribute `min` | Minimum column width as a CSS length (for example 12rem or 200px). Columns are equal width and never narrower than this, so the grid drops to one column when the container is narrower than two. |
| `Columns` | `int` |  | attribute `columns` | The most columns to use, 0 for no limit. The grid still drops columns when they would be narrower than min. |
| `Ratio` | `string?` |  | attribute `ratio` | Unequal columns as a ratio, for example 2:1 or 1:2:1. Overrides min and columns; on a phone the grid falls back to auto columns so it stacks. |
| `Tracks` | `string?` |  | attribute `tracks` | A column track list as CSS writes it, for a fixed grid such as a 12-column dashboard (repeat(12, minmax(0, 1fr))) or uneven columns (12rem 1fr). Use minmax(0, ...) so wide content cannot blow a track out. Children take columns with data-col-span. Overrides ratio, min and columns; an unparseable value is ignored with one warning. On a phone the grid falls back to auto columns, so it stacks and spans stop applying. |
| `TabletTracks` | `string?` |  | attribute `tablet-tracks` | Column tracks from 641 to 1024px, for example repeat(6, minmax(0, 1fr)) to halve 12 tracks; data-col-span halves too. |
| `StackBelow` | `GridStackBelow?` | `Phone` `Tablet` | attribute `stack-below` | Stack at phone (640px) or tablet (1024px) width. |
| `Fill` | `bool` |  | attribute `fill` | Takes the remaining height of its parent (a flex column or a sized block). Off on a phone. |
| `Rows` | `string?` |  | attribute `rows` | Row tracks as CSS writes them (minmax(0, 1fr) minmax(0, 2fr)); with fill they share the height. Off on a phone. |
| `StickyFirst` | `bool` |  | attribute `sticky-first` | The first child stays in view while the page scrolls past the other columns (a section nav beside a long article): sticky at --pk-grid-sticky-top (default --space-4) from the top of the scrolling area, sized to its content. Use with ratio or tracks; on a phone the grid stacks and nothing sticks. |
| `Gap` | `GridGap?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `gap` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default md (the card gap). |
| `ChildContent` | `RenderFragment?` |  | default slot | The items to lay out. Any child can carry data-col-span="2" to "12" (spans that many columns) and data-row-span="2" to "4" (spans that many rows), meant for a grid with tracks; on a pk-* child it is a plain attribute (<PkCard data-col-span="6"> in Blazor). Not applied on a phone. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-grid` in the `plainkit-sdk` skill.

## `PkHeading`

`<pk-heading>`: **Heading** (Layout). A real h1 to h6 heading in the shadow tree, with a visual size decoupled from the level: the semantic counterpart of pk-text's look-only h1 to h6 variants, for a strict module that cannot write a raw heading tag.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Level` | `double` |  | attribute `level` | The heading level exposed to assistive technology (1 to 6, clamped and rounded; out of range falls back to 2 and warns once). Rendered as a real h1 to h6 in the shadow tree, so it takes its place in the page outline. |
| `Variant` | `PkHeadingVariant?` | `Inherit` `H1` `H2` `H3` `H4` `H5` `H6` | attribute `variant` | The visual size on the --text-h1 to --text-h6 scale, independent of level: a section can be an h3 in the outline but look like an h1, or the reverse. Default inherit: the size matches level. |
| `Tone` | `PkHeadingTone?` | `Inherit` `Default` `Muted` `Accent` `Positive` `Warning` `Critical` | attribute `tone` | Text colour from the theme, as on pk-text. Default inherit: the colour around it. |
| `Weight` | `PkHeadingWeight?` | `Inherit` `Regular` `Medium` `Semibold` `Bold` | attribute `weight` | Font weight. Default inherit: bold, the native heading weight. |
| `Truncate` | `bool` |  | attribute `truncate` | Keep it to one line and end with an ellipsis when it does not fit. |
| `ChildContent` | `RenderFragment?` |  | default slot | The heading text: phrasing content only. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-heading` in the `plainkit-sdk` skill.

## `PkKanban`

`<pk-kanban>`: **Kanban board** (Layout). A board of pk-kanban-column children, each holding pk-sortable-item cards (usually a pk-card inside). Drag a card's handle to another column or position (pointer or touch), or focus a card and press Alt+Arrow. It never moves its own children: pk-move carries the move and the host applies it.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Label` | `string?` |  | attribute `label` | Accessible name for the board (aria-label). |
| `Disabled` | `bool` |  | attribute `disabled` | No card can be grabbed or moved by the keyboard. |
| `Dragging` | `bool` |  | attribute `dragging` | True while a card is being dragged. A styling hook the element sets itself. |
| `OnMove` | `EventCallback<PkMoveEventArgs>` |  | event `pk-move` | A card move was committed: a drag ended over a new column or position, or Alt+Arrow moved the focused card. item is the card's value, from and to are the columns' values, and fromIndex and toIndex are the card's index in its old column and the index it takes in the new one (the same list with the card lifted out, so within one column it is where the card ends up). The host applies it: pk-kanban never moves its own children. Cancelable: preventDefault declines the move, and the live region says it was cancelled. The event can be cancelled in the browser (preventDefault); a callback cannot cancel it. |
| `ChildContent` | `RenderFragment?` |  | default slot | pk-kanban-column children, in board order. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-move` | `PkMoveEventArgs` | Item: string?, From: string?, To: string?, FromIndex: double?, ToIndex: double? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-kanban` in the `plainkit-sdk` skill.

## `PkKanbanColumn`

`<pk-kanban-column>`: **Kanban column** (Layout). One column of a pk-kanban: a title with a card count, the cards, and a message when it has none. Place it directly inside a pk-kanban.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Value` | `string?` |  | attribute `value` | The identifier pk-kanban reports for this column in a pk-move event's from and to. |
| `Label` | `string?` |  | attribute `label` | The column's title, and its accessible name. Falls back to value. |
| `EmptyText` | `string?` |  | attribute `empty-text` | Shown in place of the cards while the column has none; it is also the drop target for a card dragged in. |
| `DropTarget` | `bool` |  | attribute `drop-target` | Set by the surrounding pk-kanban while a dragged card is over this column. |
| `ChildContent` | `RenderFragment?` |  | default slot | pk-sortable-item cards, in order. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-kanban-column` in the `plainkit-sdk` skill.

## `PkLabelSheet`

`<pk-label-sheet>`: **Label sheet** (Layout). A grid of fixed physical cells for labels, bin tags and entry cards: set the cell size in mm, in or pt, a gap and the cells per row; each child is one cell and is never split across a printed page. Use it inside pk-print-page, whose size and margin define the sheet and which hides the app chrome. Cells show at true scale on screen.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `CellWidth` | `string?` |  | attribute `cell-width` | Cell width: a number with mm, cm, in, pt or px. Anything else falls back to 50mm and warns once. |
| `CellHeight` | `string?` |  | attribute `cell-height` | Cell height, same units. Falls back to 25mm. |
| `Columns` | `int` |  | attribute `columns` | Cells per row; 0 fits as many as the width allows. |
| `Gap` | `string?` |  | attribute `gap` | Gap between cells, same units. |
| `OffsetX` | `string?` |  | attribute `offset-x` | Sheet origin from the left of its box, for pre-cut stock. |
| `OffsetY` | `string?` |  | attribute `offset-y` | Sheet origin from the top of its box, for pre-cut stock. |
| `Outline` | `bool` |  | attribute `outline` | Dashed cut guides around every cell, on screen and in print. |
| `ChildContent` | `RenderFragment?` |  | default slot | The cells: one element per label (a pk-card, a div with an SVG barcode). A child larger than its cell is clipped and marked data-clipped. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-label-sheet` in the `plainkit-sdk` skill.

## `PkList`

`<pk-list>`: **List** (Layout). A plain ordered or unordered prose list: bullets, numbers or checkmarks on items the host writes as plain elements, never a raw ul, ol or li.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Ordered` | `bool` |  | attribute `ordered` | Numbered instead of bulleted, when marker is auto (the default). |
| `Marker` | `string?` |  | attribute `marker` | The bullet: auto follows ordered (decimal when set, disc otherwise), disc and decimal force a style regardless of ordered, check draws a checkmark, none removes the marker and its indent (the list role stays; it comes from ARIA, not the native marker). |
| `Gap` | `ListGap?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `gap` | Space between items, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default xs. |
| `Dense` | `bool` |  | attribute `dense` | Tighter rhythm and smaller text. |
| `Label` | `string?` |  | attribute `label` | Accessible name of the list. |
| `ChildContent` | `RenderFragment?` |  | default slot | Items: plain elements (span, div, a) or pk-text inline. Never li; the list role and item count come from ARIA. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-list` in the `plainkit-sdk` skill.

## `PkListPage`

`<pk-list-page>`: **List page** (Page types). A filterable, sortable, paginated list of records: a pk-data-table (the one implementation of the query, load and states: a manual pk-table, a pk-table-filters search/filter panel and a pk-pagination footer) under an optional title bar, with optional toolbar actions. Set the load(query) callback as a property (business logic, never config data) - it receives { page, pageSize, sort, sortDir, search, filters } 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 rejection shows the built-in error state with Retry, and zero rows show the configured empty state. rowHref(row), also a callback property, makes rows clickable and is called instead of the element navigating itself, so the host wires its router.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { columns, filters?, actions?, empty?, pageSize? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. columns is pk-table's own column definitions ({ key, label, type?, align?, sortable?, hidePhone?, shed?, title? }[]). filters is [{ key, type: 'text'\|'email'\|'number'\|'date'\|'select'\|'multiselect', label, options? }] (a select's options are [{ value, label? }] or plain strings), rendered into the pk-table-filters panel and combined into query.filters as { [key]: text }. actions is [{ label, href?, variant?, icon?, collapse? }], toolbar buttons shown beside the search box; icon is an SDK icon name drawn before the label, and the button folds to the icon alone on a phone (collapse: 'phone' by default with an icon, 'tablet' or 'none'), the label staying its accessible name. empty is { heading?, description? } shown when a page has zero rows. expandable? (boolean), expander? ('none') and expanded? (row ids) are pk-data-table's: rows with a detail-<rowId> child get a toggle and pk-row-expand reports it (expanding is not a row click). selectable? (boolean) shows the checkbox column and the bulk bar, rowKey? names the row field that identifies a row (default id), bulkActions? is [{ id, label, variant?, icon?, collapse? }], the buttons of the bulk bar. pageSize is the initial page size (default 25); search?, filterValues?, sort?, sortDir? and page? seed the first query. Title bar: heading?: string and breadcrumb?: [{ label, href? }]; nothing is drawn when neither is set. toolbarInHeader? puts them in the toolbar row with the search (no pk-page-header: no sticky header or back link). cards? (default true unless selectable): cards below 640px. searchPlaceholder? is the search box text (placeholder and accessible name; searchLabel then is the accessible name only: a placeholder that differs from it). selectPageOnly? (with selectable) selects only the loaded page and never offers "Select all N", so pk-bulk scope is always "page". stickyHeader? (boolean) pins the column header; in a flex column of definite height (a pk-stack[fill], an app-shell body) the rows scroll and the pager and bulk bar stay pinned. Sent as a JSON attribute: { columns: [{ key, label, type?, align?, sortable?, hidePhone?, shed?, title? }], filters?: [{ key, type, label, options? }], actions?: [{ label, href?, variant?, icon?, collapse? }], empty?: { heading?, description? }, pageSize?, selectable?, rowKey?, bulkActions?: [{ id, label, variant?, icon?, collapse? }] }. A column key names a property of TItem in camel case (orderNo for OrderNo). An action's icon (an SDK icon name such as "plus") is drawn before its label, which folds to the icon on a phone (collapse "phone" by default, "tablet" or "none"); put no glyph in the label. |
| `Load` | `Func<PkListRequest, Task<PkListResult<TItem>>>?` |  |  | Loads one page for the request's page, sort, search and filters and returns its items and the total of the whole (searched) list. Throwing shows the element's error state with Retry; zero items show the empty state. |
| `Cells` | `IReadOnlyDictionary<string, RenderFragment<TItem>>?` |  |  | Cell templates by column key (the config column key, camel case): each fills the cell of that column in every loaded row, with the item. A column without one shows its value. Needs IdOf. |
| `Columns` | `IReadOnlyList<PkTableColumn<TItem>>?` |  |  | Typed columns (the PkDataTable column type): replaces the columns of Config. A column with a Cell template fills that cell of every loaded row with the item (needs IdOf). A column with Text shows that text under its Key, and a Key with neither names a property of TItem in any casing, as on PkDataTable. Title and HidePhone shape the phone cards. |
| `IdOf` | `Func<TItem, string>?` |  |  | The row key of an item (the config rowKey property, default id): names the cell slots of the loaded rows. Ids must not contain a double quote. |
| `RowHrefTemplate` | `string?` |  | attribute `row-href-template` | 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). Row links: an address like /orders/{id} (or #/orders/{id}); {id} is the row key and any {field} a camel-case row property, URL-encoded. The first cell of each row becomes a real link (new tab, middle click, copy address) and a plain click navigates through the app router without a reload. Use it for a routed list-to-record pair; OnRowClick stays for an imperative handler. |
| `Navigate` | `string?` |  | attribute `navigate` | Who follows a row link: router (default, empty), host or none. Who follows a RowHrefTemplate link on a plain click: "router" (default), "host" (a notification, nothing navigates) or "none" (the browser follows the address). |
| `ToolbarInHeader` | `bool` |  |  | Puts the breadcrumb and heading in the toolbar row with the search box, Filters and the actions, one row that wraps on a phone. The page-shell title bar is then not drawn (no pk-page-header: no sticky header, no back link). |
| `InitialSearch` | `string?` |  |  | The search term to start with, for example from a query string: it fills the search box and the first Load request, and typing replaces it. Initial only. |
| `InitialFilters` | `IReadOnlyDictionary<string, string>?` |  |  | The filter values to start with, by filter key (for example { ["channel"] = "ebay" } from a query string): each fills its control in the filter panel and the first Load request (Filters), and the reader changing or clearing a filter replaces it. Initial only. |
| `InitialSortKey` | `string?` |  |  | The key of the column to sort by first. Null loads the default order. Initial only. |
| `InitialDescending` | `bool` |  |  | Sort InitialSortKey descending first. Initial only. |
| `InitialPage` | `int?` |  |  | The 1-based page to load first. A page past the last settles on the last page. Initial only. |
| `SearchPlaceholder` | `string?` |  |  | The text of the search box: its placeholder and accessible name, for example "Search by order, vendor or reference". Default "Search". |
| `SelectPageOnly` | `bool` |  |  | With Config selectable: the header checkbox selects the loaded page only and never offers "Select all N", so a bulk action that writes one row at a time only touches rows the reader has seen (PkBulkEventArgs scope stays page). Default false. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Content in the title bar, beside the title (as PkPageHeader ActionsContent): a callback button, or a PkMenu of several actions. The title bar stays shown when Config sets no heading or breadcrumb. |
| `BeforeListContent` | `RenderFragment?` |  | slot `before-list` | Content between the title bar and the list: a status alert, a summary card. It keeps its height while the page has room; on a short page it shrinks and scrolls inside its own box while the list keeps a floor of about three rows (--pk-list-page-table-min), so with Config stickyHeader the column header and pager stay pinned and the rows never collapse. |
| `ToolbarContent` | `RenderFragment?` |  | slot `toolbar` | Content at the end of the list toolbar, beside the search box and after the Config actions (as PkDataTable ToolbarContent). |
| `OnSelect` | `EventCallback<PkSelectEventArgs>` |  | event `pk-select` | The selection changed (config.selectable), passed through from the pk-data-table. For scope 'all' run the bulk action against query on the server, never against the ids. |
| `OnRowClick` | `EventCallback<PkRowClickEventArgs>` |  | event `pk-row-click` | A row was activated (clickable): id is the rowKey value, row the row object from load(). The data table's own event, passed through. A row was clicked (or Enter or Space on the focused row): Id is the row key (config.rowKey, default id), Row the row as JsonElement. The usual handler navigates to the record page. Setting it makes the rows clickable. |
| `Expandable` | `bool` |  |  | Rows with a DetailTemplate get a toggle that shows it under them (the detail is rendered for the loaded page's rows only; needs IdOf). Expanding is not a row click. |
| `Expander` | `string?` |  |  | "none" leaves out the built-in toggle column; the host then opens rows through Expanded. |
| `Expanded` | `IReadOnlyList<string>?` |  |  | The ids (IdOf) of the rows to show expanded. Initial only: the reader's own toggling is reported by OnRowExpand. |
| `DetailTemplate` | `RenderFragment<TItem>?` |  |  | The detail of one loaded row, shown under it when the user expands it (the element's detail-<rowId> slot). Needs Expandable and IdOf. |
| `DetailPanels` | `IReadOnlyDictionary<string, RenderFragment<TItem>>?` |  |  | Further named detail panels of a loaded row, by panel name (the element's detail-<rowId>.<name> slots). Needs Expandable. A named panel has no toggle: open it by putting "<rowId>.<name>" in Expanded. |
| `OnRowExpand` | `EventCallback<PkRowExpandEventArgs>` |  | event `pk-row-expand` | The reader expanded or collapsed a row (config.expandable): expanded is the new state. The table's own event, passed through; not a row click. A row was expanded or collapsed: Id is the row key, Expanded the new state. Not a row click. |
| `OnBulk` | `EventCallback<PkBulkEventArgs>` |  | event `pk-bulk` | A button of config.bulkActions was pressed: its id and the selection at that moment, as pk-select carries it. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-bulk` | `PkBulkEventArgs` | Action: string?, Selected: string[]?, Scope: string?, Query: JsonElement? |
| `pk-row-click` | `PkRowClickEventArgs` | Id: string?, Row: JsonElement? |
| `pk-row-expand` | `PkRowExpandEventArgs` | Id: string?, Index: double?, Expanded: bool? |
| `pk-select` | `PkSelectEventArgs` | Item: JsonElement?, Value: string?, Checked: bool?, Id: string?, Selected: string[]?, Scope: string?, Query: JsonElement? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-list-page` in the `plainkit-sdk` skill.

## `PkMasterDetailPage`

`<pk-master-detail-page>`: **Master-detail page** (Page types). Static slotted mode (for server-rendered, JavaScript-optional sites): write your own list as a child in slot="list" (a pk-side-nav of links, a plain list) and the record as a child in slot="detail", with no config.list. The page lays them out as written (side by side wide; one pane at a time on a phone: the list, or the record when you wrote record-id), takes the current row from your own markup (aria-current / current), and never creates, moves or changes your elements; load, open, close, mountDetail and config do nothing, and the record keeps its own back link. A list or record is real markup even without JavaScript. Dynamic mode: A list of records and the record the route selected. Wide, they sit side by side (a pk-list-page and the record pane); narrow, one shows at a time and the record has a Back button. The host owns the selection: set recordId from the route (a deep link works), and the element reports what the reader did through callback properties. load(query) feeds the list exactly as pk-list-page's does; open(row) is called when a row is chosen and close() when Back is pressed, so the host changes the route. mountDetail(pane, id) draws the record into the pane and returns a cleanup function or a { destroy() } handle, called when the record changes or the element leaves the page. While mountDetail is pending the built-in loading state shows over the record, a rejection shows the error state with Retry. Focus moves to the record once it is drawn, and back to the row that was open when the selection clears.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { list?, backLabel?, none?, label? }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. list is pk-list-page's own config ({ columns, filters?, actions?, empty?, pageSize? }). backLabel is the Back button's text (default 'Back'). none is { heading?, description? } shown in the record pane while nothing is selected. label names the loading state. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute: { list?, backLabel?, none?, label? }. |
| `RecordId` | `string?` |  | attribute `record-id` | The id of the selected record, from the route; empty when none is. Untrusted text: it is only passed to mountDetail. |
| `Fill` | `bool` |  | attribute `fill` | Take all the room a flex column parent leaves. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |
| `Load` | `Func<JsonElement, Task<object?>>?` |  |  | Loads one page of the list: receives the query (page, pageSize, sort, search, filters as JSON) and returns an object { rows, total }. Throwing shows the list's error state with Retry. |
| `Close` | `Func<Task>?` |  |  | Back was pressed in the one-pane view: change the route to the list (NavigationManager.NavigateTo). |
| `DetailContent` | `RenderFragment?` |  | slot `detail` | The record (mountDetail not set), shown only while recordId is set. Static slotted mode: your own record markup, shown as written; on a phone it shows instead of the list while record-id is set. The selected record's content (read RecordId to render it): shown in the record pane only while RecordId is set. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-master-detail-page` in the `plainkit-sdk` skill.

## `PkNotFoundPage`

`<pk-not-found-page>`: **Not found page** (Page types). The "this page doesn't exist" or "you don't have access" screen for a whole page, built on the same shared state markup as pk-states-page's empty state. Works with zero config; add label for a real action button that raises pk-action.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Heading` | `string?` |  | attribute `heading` | Heading. Defaults to "Page not found". |
| `Description` | `string?` |  | attribute `description` | Explanation. Defaults to "The page you're looking for doesn't exist or you don't have access to it." |
| `Label` | `string?` |  | attribute `label` | Text for an action button (e.g. "Go home"); no button is drawn when empty. Clicking it raises pk-action - this element never navigates by itself. |
| `OnAction` | `EventCallback` |  | event `pk-action` | The label action button was activated. The host decides what happens (navigate home, go back); this element never navigates by itself. |
| `TitleContent` | `RenderFragment?` |  | slot `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. When present, the built-in state heading is not drawn a second time. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-not-found-page` in the `plainkit-sdk` skill.

## `PkNotePage`

`<pk-note-page>`: **Note page** (Page types). A static informational page: a heading and a block of prose in a card. No data, no callbacks - the page-type building block for a one-off About/Overview/Summary page.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Config` | `object?` |  | attribute `config` | { heading, body, cardHeading }. heading is the page's h1 text, body is the card's prose, cardHeading is an optional card title (the card has no header without it). Sent as a JSON attribute: { heading, body, cardHeading? }. No callbacks: a static informational page (#678). |

Element details (parts, CSS custom properties, methods, accessibility): `pk-note-page` in the `plainkit-sdk` skill.

## `PkPageHeader`

`<pk-page-header>`: **Page header** (Layout & structure). The top of a page, a section or a record: a breadcrumb row, a title bar with actions, and a note line. Without an explicit heading, the crumbs, chips and actions collapse onto a single row by default; spacious opts back into the multi-row breakdown. Stacks by its own width, with the actions full width and touch-sized on a phone.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Title` | `string?` |  | attribute `heading` | The title. Without it (and without default or actions content) the title bar is not shown. |
| `Level` | `int` |  | attribute `level` | The heading level the title exposes to assistive technology (aria-level). |
| `Variant` | `PageHeaderVariant?` | `Page` `Section` `Record` | attribute `variant` | page is the standard page top; section is a titled section inside a page; record is the compact strip of a single-record page (chips left, actions right; on a narrow container the chips move to a row below). Its title, the heading or the last crumb, is one line: a long one ends in an ellipsis with the full text as its tooltip, and the chips and actions keep their width. |
| `WrapTitle` | `bool` |  | attribute `wrap-title` | Record variant only: on a narrow container (40rem or less, a phone) the title wraps to at most three lines (two on a phone, 30rem or less), then ends in an ellipsis, instead of the single ellipsized line. Wide containers keep one line. Off by default (the one-line title keeps the chips and actions on a steady row); the full title stays the tooltip in the one-line mode. With a slotted pk-heading, leave its truncate off. |
| `Sticky` | `bool` |  | attribute `sticky` | Pins the header to the top of the scrolling page body while the content scrolls: in pk-app-shell flush under the app bar (the body is the scroller), in a plain page at the top of the viewport. It is its own stacking context one above in-body sticky strips, so a popover opened from the header draws over them. |
| `StickyFrom` | `PageHeaderStickyFrom?` | `Always` `Phone` `Tablet` | attribute `sticky-from` | With sticky, the width above which the header stays pinned: always, phone (above the phone breakpoint) or tablet. At and below it the header scrolls away, leaving room above the on-screen keyboard. The opposite sense of tabs-from. |
| `Spacious` | `bool` |  | attribute `spacious` | Without a heading the crumbs, chips and actions share one row. Set spacious for the multi-row breakdown (crumbs, then a title bar with actions, then the note line), for a long title or many badges. No effect when heading is set. |
| `SuffixContent` | `RenderFragment?` |  | default slot | Beside the title: status chips, badges or your own heading element. In the record variant on a narrow container they drop to their own row under the title and actions. |
| `BreadcrumbContent` | `RenderFragment?` |  | slot `breadcrumb` | A pk-breadcrumb (or any trail) above the title; hidden when empty. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Buttons at the right of the title bar; full width and touch-sized on a narrow container. Give each an icon-name and collapse="phone" so it folds to the icon alone on a phone (its text stays the accessible name). |
| `MetaContent` | `RenderFragment?` |  | slot `meta` | A note or metadata line under the title bar; hidden when empty. |
| `ActionsOverflow` | `PageHeaderActionsOverflow?` | `Never` `Phone` `Tablet` `Always` | attribute `actions-overflow` | Collapses the actions slot into one icon button that opens a menu at the named width and below (phone, tablet) or always. Each slotted action becomes a menu item with its label, icon-name, disabled and danger state; choosing one clicks the original, so its handlers, href and events are unchanged, and the title bar stays one row high. Use pk-button or link actions as direct children of the slot; a pk-dropdown or pk-card-menu among them is flattened into its own items (headers kept, selecting one selects the original). A slotted menu with no items adds nothing, and with nothing to choose the overflow button is not drawn. |
| `OverflowLabel` | `string?` |  | attribute `overflow-label` | The accessible name of the overflow button drawn by actionsOverflow. |
| `TabsFrom` | `PageHeaderTabsFrom?` | `Phone` `Tablet` `Always` | attribute `tabs-from` | Where the docked tabs strip shows: always, or only at phone or tablet width and below (above it the strip is hidden, so a page that shows every section at that width needs no strip). |
| `TabItems` | `IReadOnlyList<PkRecordTab>?` |  | attribute `tabs` | The docked strip as data: an array of { value, label, count?, visible? }, the same shape as pk-record-form. The element draws a pk-tabs at the foot of the header (a pk-tabs in the tabs slot wins); count is the badge after the label and a tab with visible false is left out. With sticky the strip stays in view with the bar. Listen for pk-tab-change for the chosen value. |
| `OnTabChange` | `EventCallback<PkTabChangeEventArgs>` |  | event `pk-tab-change` | The docked strip chose a tab. |
| `TabsContent` | `RenderFragment?` |  | slot `tabs` | A tab strip (pk-tabs with tabs only, or your own) docked at the foot of the header, so with sticky it stays in view with the bar; replaces the strip drawn from the tabs array; hidden when empty or when tabs-from says it does not apply at this width. A strip with more tabs than the width holds scrolls inside the header with a fade on the hidden side (pk-tabs overflow="scroll", its default; overflow="menu" lists them in a menu instead), so it never widens the page. |
| `HomeHref` | `string?` |  | attribute `home-href` | An address for a leading home crumb: an icon-only link (named by homeLabel) first in the trail crumbs draws. Empty draws none. |
| `HomeLabel` | `string?` |  | attribute `home-label` | The accessible name of the home crumb. |
| `HomeIcon` | `string?` |  | attribute `home-icon` | The icon of the home crumb (a name of the icon sprite). |
| `Crumbs` | `IReadOnlyList<PkCrumb>?` |  | attribute `crumbs` | The breadcrumb trail as a JSON array of { "label", "href" }, drawn in a pk-breadcrumb above the title (the last crumb is the current page: aria-current, and the page title when no heading is set). An href that is not a same-site path, http(s), mailto, tel or sms address draws a crumb without a link. Use it instead of the breadcrumb slot, not with it. Typed list of PkCrumb: sent as the element's crumbs attribute (a JSON array of { label, href }); the element draws the breadcrumb, home crumb and back link (#801). BreadcrumbContent is for a trail you draw yourself, without Crumbs. |
| `BreadcrumbLabel` | `string?` |  | attribute `breadcrumb-label` | The accessible name of the breadcrumb landmark drawn from crumbs. |
| `BackLink` | `bool` |  | attribute `back-link` | Draws a back link above the header to the last crumb before the current one that has a usable href: a ghost icon-only button with a chevron, named "Back to <label>". On a header of 30rem or less with no heading (the crumbs are the title) the chevron takes the start of row one and the home and parent crumbs fold away, so the title shares the row with the actions. Off by default because the app shell can draw its own. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-tab-change` | `PkTabChangeEventArgs` | Value: string?, Previous: string?, Fallback: bool? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-page-header` in the `plainkit-sdk` skill.

## `PkPageShell`

`<pk-page-shell>`: **Page shell** (Layout & structure). The frame of one page: a pk-page-header (breadcrumb, title, actions, a docked tab strip) above one content region. A whole-page state (loading, empty, error, forbidden) replaces the content. The spacing between header and content is the shell's own, so a page declares none. Tabs: one content region, and the page swaps what it renders when the tab strip fires pk-tab-change.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Title` | `string?` |  | attribute `heading` | The page title, drawn by the header. |
| `Level` | `int` |  | attribute `level` | The heading level the title exposes to assistive technology. A page title is 1. |
| `Crumbs` | `IReadOnlyList<PkCrumb>?` |  | attribute `crumbs` | The breadcrumb trail as a JSON array of { "label", "href" }, as on pk-page-header. Use it instead of the breadcrumb slot. Typed list of PkCrumb: sent as the element's crumbs attribute, as on PkPageHeader. |
| `BackLink` | `bool` |  | attribute `back-link` | Draws a back link to the last crumb that has a usable href. |
| `Variant` | `PageHeaderVariant?` | `Page` `Section` `Record` | attribute `variant` | As on pk-page-header: record is the compact strip of a single-record page. |
| `WrapTitle` | `bool` |  | attribute `wrap-title` | As on pk-page-header (record variant only): on a narrow container the title wraps to at most three lines instead of one ellipsized line. |
| `Sticky` | `bool` |  | attribute `sticky` | Pins the header (with its tabs) to the top of the scrolling page body. |
| `StickyFrom` | `PageHeaderStickyFrom?` | `Always` `Phone` `Tablet` | attribute `sticky-from` | As the header's. |
| `TabsFrom` | `PageHeaderTabsFrom?` | `Phone` `Tablet` `Always` | attribute `tabs-from` | As the header's. |
| `HomeHref` | `string?` |  | attribute `home-href` | As on pk-page-header: an icon-only home crumb first in the trail. |
| `HomeLabel` | `string?` |  | attribute `home-label` | As the header's. |
| `HomeIcon` | `string?` |  | attribute `home-icon` | As the header's. |
| `ActionsOverflow` | `PageHeaderActionsOverflow?` | `Never` `Phone` `Tablet` `Always` | attribute `actions-overflow` | As on pk-page-header: the actions fold into one menu button. |
| `OverflowLabel` | `string?` |  | attribute `overflow-label` | As the header's. |
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (pk-badge) beside the title. |
| `State` | `PageShellState?` | `Ready` `Loading` `Empty` `Error` `Forbidden` | attribute `state` | ready shows the content. Any other value shows that whole-page state in its place and hides the content (it stays in the DOM). The header and its tabs stay. |
| `StateHeading` | `string?` |  | attribute `state-heading` | The state's heading (the loading label for loading). Empty uses a default per state. |
| `StateDescription` | `string?` |  | attribute `state-description` | The line under the state's heading (the error message for error). |
| `OnRetry` | `EventCallback` |  | event `pk-page-shell-retry` | Retry was pressed in the error state: load again, then set state back to ready. |
| `ChildContent` | `RenderFragment?` |  | default slot | The page content, one region. Hidden while a whole-page state shows. |
| `BreadcrumbContent` | `RenderFragment?` |  | slot `breadcrumb` | A pk-breadcrumb above the title, instead of crumbs. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Buttons at the right of the title bar. Give each an icon-name and collapse="phone". |
| `MetaContent` | `RenderFragment?` |  | slot `meta` | A note line under the title bar. |
| `TabsContent` | `RenderFragment?` |  | slot `tabs` | A pk-tabs strip (tabs only) docked at the foot of the header. |
| `TitleContent` | `RenderFragment?` |  | slot `title` | A pk-heading for the title, in place of the built-in text. |

When you set a named fragment (`BadgesContent`, `BreadcrumbContent`, `ActionsContent`, `MetaContent`, `TabsContent`, `TitleContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-page-shell` in the `plainkit-sdk` skill.

## `PkPrintPage`

`<pk-print-page>`: **Print page** (Page types). A page whose document is the only thing that prints: a statement, a sheet of labels, a count sheet. On screen it is a paper-like sheet under a toolbar (flat on a phone); in print the toolbar, anything marked data-screen-only and the app shell's header, footer and nav disappear, @page size and margins come from props, and pk-card, pk-field-list, pk-property-grid, pk-field-group and figure stay whole while a pk-table keeps whole rows and repeats its header on every page. While connected the element adopts one constructed stylesheet onto the document (CSP-safe) and removes it when it leaves.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Size` | `string?` |  | attribute `size` | The CSS @page size: a paper name, optionally with an orientation ("A4", "letter landscape") or two lengths. Empty leaves the printer default. Only letters, digits, spaces, dots, percent signs and dashes are accepted. |
| `Margin` | `string?` |  | attribute `margin` | The CSS @page margin (one to four lengths, for example "15mm" or "20mm 15mm"). |
| `ChildContent` | `RenderFragment?` |  | default slot | The document: the only content that prints. Mark any part of it that should not print with the data-screen-only attribute. |
| `Toolbar` | `RenderFragment?` |  | slot `toolbar` | Screen-only actions (print, back). Hidden when printing. |
| `Title` | `RenderFragment?` |  | slot `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. It prints as part of the page. |

When you set a named fragment (`Toolbar`, `Title`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-print-page` in the `plainkit-sdk` skill.

## `PkRecordPage`

`<pk-record-page>`: **Record page** (Page types). One record shown and edited on a page: the fields read as a pk-field-list, or as a validated pk-form (a pk-field-group per section card) in edit mode, beside sidebar cards (pk-detail-layout, which collapses on a phone). Set load(id) and save(values) as properties (business logic, never config data). load runs when config.id is set and shows the built-in loading, not-found and error (with Retry) states; without an id the page is a new record, in edit mode. save runs only after client validation passes; reject with { errors: { field: message } } to mark fields inline, or any other error for a notice above the form. While edits are unsaved, dirty is true, pk-record-dirty fires, and closing or reloading the tab asks first.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { id?, fields, values?, sections?, sidebar?, titleField?, heading?, saveLabel?, editable?, deletable?, label?, emptyHeading? }. deletable shows a Delete button of an existing record, in view and edit mode (pk-record-delete); the record page type sets it from its delete callback. fields are the field defs of pk-field-group, [{ key, label?, kind?, hint?, help?, required?, readonly?, disabled?, span?, options?, when?, ... }] (name and type are older spellings of key and kind): kind is a pk-input type (text, email, number, date...), textarea, select, checkbox, switch, range, combobox or custom (its body in edit mode is your child with slot editor-<key>) (options: strings or { value, label }). In edit mode they are one pk-field-group, so every member it knows works here. values is the starting record of a new one (no id). sections and sidebar are [{ heading, fields: [keys], columns? }], one card each (columns, 2 or more, flows that card's fields into that many columns in edit mode, as pk-field-group columns does; a narrow card shows one) (main column, beside it; fields no section or sidebar card names lead the main column under heading). Sidebar cards are editable in edit mode (same form, one Save); a field they name is edited there only. titleField is a field key whose loaded or saved value is the page title (title is the fallback). cancellable shows Cancel on a new record too. The host may set a beforeSave(values) property (a function, or async): it runs once the form is valid and before save, and returning false stops the save and keeps the page in edit mode with the edits (the record page type takes it as config.beforeSave(values, ctx)). id selects the record to load; none means a new record. editable: false hides Edit. Optional title bar from the shared page shell: title?: string (the page heading; config.heading titles the field list), breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute with the fields added from Fields: { id?, sections?, sidebar?, titleField?, heading?, title?, saveLabel?, editable?, label?, emptyHeading? }. The id selects the record to load (none: a new record). A sidebar card lists field keys. |
| `Mode` | `RecordPageMode` | `View` `Edit` | attribute `mode` | view reads the record; edit shows the form with Save and Cancel. Edit and a successful save switch it; a new record is always in edit mode. 'view' or 'edit'. |
| `Fields` | `IReadOnlyList<PkFieldSpec<TItem>>?` |  |  | The record's fields, typed as PkFieldGroup's: Key, Label, Kind, Get and Set, plus Hint, Help, Required, Options, Span and the rest. In view mode they read as a list; in edit mode the element draws them as a pk-field-group. When, Disabled, ReadOnly, LabelWhen and HelpWhen are read from the record once it is loaded (they are .NET functions of the item and do not follow edits until the next load or save: a round trip per keystroke is not what the page does; set FollowEdits to have them follow each committed change); for a condition that follows what the user types set the declarative WhenExpr, DisabledExpr, ReadonlyExpr, RequiredExpr, LabelWhenExpr or HelpWhenExpr on the spec: the element evaluates them in the browser against the live values, also across cards (a field in one section can decide a field in another or in the sidebar); a spec Editor is the body of its field in edit mode (it edits the item itself). |
| `Model` | `TItem?` |  |  | The record a new one (Config without an id) starts from and Save receives: set it to an instance to fill. A record with an id is what Load returns. |
| `FollowEdits` | `bool` |  |  | Makes the typed When, Disabled, ReadOnly, LabelWhen and HelpWhen follow what the person edits, not only the loaded or saved record. Each committed field change (a blur or a choice, not each keystroke: one round trip per change, and none for a page that does not set this or OnFieldChanged) is applied through that field's Set (or SetAsync) to a draft copy of the item, the functions read the draft, and the form updates in place: the control being edited keeps its focus and text. The loaded item is not touched before Save; Cancel, a successful Save and a new Load drop the draft, and a rejected Save keeps it with the typed values. Set it before the first render. The copy is a shallow MemberwiseClone unless CopyOf is set; a field's Set runs on it for every committed change, so it must not throw for text a person can type. A field hidden by a typed When has no control, so what was typed in it is not part of the values Save receives. |
| `CopyOf` | `Func<TItem, TItem>?` |  |  | How FollowEdits copies the item into its draft (default: a shallow member-wise copy). Set it when a field's Set changes a nested object or a collection the copy would share with the loaded item. |
| `ViewFields` | `IReadOnlyDictionary<string, RenderFragment>?` |  |  | Value templates by field key: each replaces the text of that field in view mode (the main list and the sidebar cards). A field without one shows its value. The content is yours, so it reads the record from your own state. |
| `Sticky` | `bool` |  | attribute `sticky` | Pins the title bar to the top of the scrolling page, with Edit, Cancel, Save and Delete in it, so they stay in reach while a long form scrolls. The same as config.sticky (either one pins it). Pins the title bar to the top of the scrolling page with Edit, Cancel, Save and Delete in it (as PkPageHeader Sticky does), so they stay in reach while a long form scrolls. |
| `WrapTitle` | `bool` |  | attribute `wrap-title` | Draws the title bar as the compact record strip (pk-page-header variant record) and lets a long title wrap to at most three lines on a narrow container (a phone) instead of one ellipsized line. The same as config.wrapTitle (either one turns it on). Draws the title bar as the compact record strip and wraps a long title to at most three lines on a phone (as PkPageHeader WrapTitle does). |
| `Busy` | `bool` |  | attribute `busy` | The host is working (a delete, a save it runs itself): Edit, Cancel, Save and Delete are disabled and Save shows its busy state, and a submit is ignored, until it is false again. The page's work in flight (bind it to PageBase.IsBusy): Edit, Cancel, Save and Delete wait. |
| `Error` | `string?` |  | attribute `error` | The host's error text, shown as a notice above the form (for example a failed delete); empty clears it. A rejected save shows its own notice in the same place. The page's error text (bind it to PageBase.StatusMessage, or the text DescribeError gives): a notice above the form; null or empty clears it. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |
| `OnDirty` | `EventCallback<PkRecordDirtyEventArgs>` |  | event `pk-record-dirty` | The unsaved-changes state changed. The app framework's record page type uses it to confirm before an in-app leave (link, breadcrumb, back/forward). The unsaved-changes state changed (Dirty): confirm before leaving the page. |
| `OnFieldChanged` | `EventCallback<PkRecordChangeEventArgs>` |  | event `pk-record-change` | A field's change was committed in edit mode (a blur or a choice, not each keystroke): key and value are the field, values the current values of every field of the page, in all its cards. Not raised for a change the host makes. Use it to redraw content beside the form that reads the edits (a summary card in main-after, a preview). A field's change was committed (not each keystroke): Key and Value are the field, Values every field's current value in all the cards (a JSON object keyed by field key). Redraw your own content beside the form, such as a summary card in MainAfter, from it; the item itself is not touched before Save. |
| `OnSave` | `EventCallback` |  | event `pk-record-save` | save() resolved; values are the saved record. Save resolved and the page is back in view mode (Save already received the values). |
| `OnDelete` | `EventCallback<PkRecordDeleteEventArgs>` |  | event `pk-record-delete` | The Delete button (shown for an existing record, in view or edit mode, when config.deletable is set) was pressed. The app framework's record page type asks first, calls its delete(id, ctx) and toasts; a host that uses the element alone handles the event. The Delete button was pressed (Id is the record's id). Setting it shows Delete of an existing record, in view and edit mode; confirm, delete and navigate away in the handler, inside TryBusyAsync on a PageBase page. |
| `Sections` | `IReadOnlyList<PkRecordSection>?` |  |  | Titled cards of fields in the main column (Heading, Fields: the keys, and Columns: in edit mode the card's fields flow into that many columns, a narrow card shows one, and a spec ColumnSpan or Span widens a field, as on PkFieldGroup). Fields no section or sidebar card names come first, under Config's heading. In edit mode each card is a field group of the one form. |
| `Sidebar` | `IReadOnlyList<PkRecordSection>?` |  |  | Cards of fields beside the main column, in place of Config's sidebar. In edit mode they are editable and saved with the rest; a field a sidebar card names is edited there only. |
| `TitleOf` | `Func<TItem, string?>?` |  |  | The page title from the loaded record (or the Model of a new one), read on every render, a save included; Config's title is the fallback while it returns nothing. |
| `IsUserFacing` | `Func<Exception, bool>?` |  |  | Decides that an exception thrown by Load or Save is written for the person even though it does not implement IPkUserFacingException, as PkRecordEditor.IsUserFacing does: its message then reaches the page (an app whose domain exception base cannot reference PlainKit.Blazor passes ex => ex is AppException). Either the marker or the predicate shows the message; a predicate that throws is logged and counts as false. Any other exception shows a generic line and is logged. It covers the two callbacks the element calls; OnDelete is your own handler (inside TryBusyAsync, DescribeError chooses the text). |
| `OnCancel` | `EventCallback<PkRecordCancelEventArgs>` |  | event `pk-record-cancel` | Cancel was pressed; for a new record (id is null) the host decides where to go. Cancel was pressed (Id is null for a new record). Setting it shows Cancel on a new record too; an existing record leaves edit mode itself, so navigate away only for a new one. |
| `MainBefore` | `RenderFragment?` |  | slot `main-before` | Content placed above the field list or form in the main column (a PkCard section, a banner). Shown in view and edit mode. |
| `MainAfter` | `RenderFragment?` |  | slot `main-after` | Content placed below the field list or form in the main column (more PkCard sections). Shown in view and edit mode. |
| `SideAfter` | `RenderFragment?` |  | slot `side-after` | Content placed after the sidebar cards. |
| `Load` | `Func<string, Task<TItem?>>?` |  |  | Loads the record whose id (config.id, the route's param) is given; null shows the 'record not found' state. Its field values are read through Fields.Get. Not called for a new record (no id). Throwing shows the element's error state. |
| `BeforeSave` | `Func<TItem, Task<bool>>?` |  |  | Runs once the form is valid and before Save, with the item the form's values were applied to (the same item Save receives): return false to stop (the page stays in edit mode with the edits and Save is not called), true to go on. The place for a confirm of your own (a dialog saying a rule collides with another record). Save shows its busy state while it runs. Throwing is a failed save (the message follows IsUserFacing). |
| `Save` | `Func<TItem, Task>?` |  |  | Saves the record: the loaded item (or Model for a new one) with the form's values applied through each field's Set (or SetAsync). Having it shows Edit and Save. Throwing keeps the edits and shows the exception message; returning switches to view mode. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |
| `pk-record-cancel` | `PkRecordCancelEventArgs` | Id: string? |
| `pk-record-change` | `PkRecordChangeEventArgs` | Key: string?, Value: JsonElement?, Values: JsonElement? |
| `pk-record-delete` | `PkRecordDeleteEventArgs` | Id: string? |
| `pk-record-dirty` | `PkRecordDirtyEventArgs` | Dirty: bool? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-record-page` in the `plainkit-sdk` skill.

## `PkSettingsPage`

`<pk-settings-page>`: **Settings page** (Page types). A sectioned settings form with a sticky Save/Discard bar that appears once a field differs from its last saved value. Set the save(values) callback as a property (business logic, never config data) - it may reject, which shows the error inline instead of saving. Your own content for a section goes in a light-DOM child with slot="section-<key>" (key = that section's config key, for example slot="section-general"): it shows inside the section's card after its fields, so the section nav and the save bar cover it. An edit in it (a control raising pk-value-change, pk-change or change, or a pk-field-group's pk-field-change) marks the form changed (Discard does not reset your content); the save callback receives only the config fields. A section with `bare: true` draws no card and no heading: its slot content (and fields) sit directly in the page, so content that draws its own pk-cards is not a card inside a card; the nav item, title bar and save bar still cover it.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { sections: [{ heading, fields: [{ key, type: 'text'\|'email'\|'number'\|'date'\|'textarea'\|'select'\|'switch'\|'range', label, placeholder?, required?, options?, min?, max?, step?, hint? }] }] }. pk-page-header title bar keys: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon. A field's hint (or help) draws an info button beside its label that opens a tooltip; a field without one is unchanged. Each section draws as a pk-card (bare?: true draws none and no heading: the section's fields and slot content sit directly in the page and the app's content draws its own cards; the section still has its nav item and tab, its heading is the label, and the save bar covers it); a select's options are [{ value, label? }] or plain strings; styled?: true on a select draws the themed list, the combobox in select mode, as pk-field-group does; type 'combobox' takes mode?: 'select' \| 'autocomplete'. Section nav: nav?: true \| false \| 'auto' (default 'auto': a list of the sections beside the cards from three sections up; false keeps the plain stack, true forces it at one or two), and each section's key? (its anchor, default a slug of its heading). The list is sticky and marks the section in view with aria-current; choosing one scrolls its card to the top and moves focus onto it; on a phone the list is replaced by pk-detail-layout's tab strip, one section at a time. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute: { nav?: true\|false\|'auto', sections: [{ key?, bare?, heading, fields: [{ key, type, label, placeholder?, required?, options?, min?, max?, step? }] }] }. nav defaults to 'auto': a section list beside the cards from three sections (tabs on a phone); false keeps the plain stack. |
| `Values` | `object?` |  | attribute `values` | The current values, keyed by field key (what save() receives and discard() restores to). Set again once loaded values arrive (e.g. from ctx.store) to refresh the fields and take a new baseline. Sent as a JSON attribute: the current values keyed by field key. |
| `Save` | `Func<IReadOnlyDictionary<string, JsonElement>, Task>?` |  |  | Saves the values of the fields, keyed by field key (read text with GetString(), a switch with GetBoolean()). After a successful save the Values parameter becomes the saved values (JSON) and ValuesChanged is raised (@bind-Values). Throwing keeps the unsaved changes and shows the exception message; returning marks them saved. |
| `ValuesChanged` | `EventCallback<string?>` |  | two-way pair of `Values` | Raised when Values changes (two-way binding: `@bind-Values`). |
| `SectionContent` | `IReadOnlyDictionary<string, RenderFragment>?` |  |  | Your own content by section key (the Config section key): each shows inside that section's card after its fields, so the section nav and the save bar cover it. An edit in it (a control raising pk-value-change, pk-change or change, or a PkFieldGroup's pk-field-change) shows the save bar; its values are not in Save's dictionary, keep them in your own state, and Discard does not reset them. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |

Two-way binding: `@bind-Values`.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-settings-page` in the `plainkit-sdk` skill.

## `PkSortable`

`<pk-sortable>`: **Sortable** (Layout). A pointer, touch and keyboard reorder list. Children are pk-sortable-item: drag a row's handle, or focus a row and press Alt+Up or Alt+Down. Never reorders its own children (the host applies the order from pk-reorder); it can also accept a row dragged in from outside it, for a palette-to-canvas drop.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Label` | `string?` |  | attribute `label` | Accessible name for the list (aria-label). |
| `Orientation` | `SortableOrientation?` | `Vertical` `Horizontal` | attribute `orientation` | The axis rows lay out and reorder along; Alt+Up/Alt+Down always means earlier/later in the list, whichever axis that is. |
| `Disabled` | `bool` |  | attribute `disabled` | No row can be grabbed, dropped on or moved by the keyboard. |
| `AcceptExternal` | `bool` |  | attribute `accept-external` | Lets a source outside this list drop a new row in, through beginExternalDrag / externalDragOver / endExternalDrag. Off by default, so a plain reorder list never becomes an accidental drop target. |
| `Dragging` | `bool` |  | attribute `dragging` | True while a row of this list, or an external drag, is being dragged over it. A styling hook the element sets itself. |
| `OnReorder` | `EventCallback<PkReorderEventArgs>` |  | event `pk-reorder` | A reorder was committed: a drag ended over a new position, or Alt+Up/Alt+Down moved the focused row. order is the new list of every row's value, in order (the host applies it: pk-sortable never moves its own children); item, from and to describe the row that moved. For a drop accepted from outside the list (external: true, through endExternalDrag) order, item and from are null, to is the index to insert at, and payload is whatever beginExternalDrag(payload) was called with. Not cancelable: the gesture already happened. |
| `ChildContent` | `RenderFragment?` |  | default slot | pk-sortable-item children, in the current order. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-reorder` | `PkReorderEventArgs` | Order: string[]?, From: double?, To: double?, Item: string?, External: bool?, Payload: JsonElement? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-sortable` in the `plainkit-sdk` skill.

## `PkSortableItem`

`<pk-sortable-item>`: **Sortable item** (Layout). One row of a pk-sortable: drag its handle (pointer or touch) to reorder, or focus the row and press Alt+Up/Alt+Down. Place it directly inside a pk-sortable.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Value` | `string?` |  | attribute `value` | The identifier pk-sortable reports for this row in a pk-reorder event's order and item. |
| `Disabled` | `bool` |  | attribute `disabled` | Cannot be grabbed, dropped on, or moved by the keyboard. |
| `Dragging` | `bool` |  | attribute `dragging` | Set by the surrounding pk-sortable while this row is the one being dragged. |
| `DropIndicator` | `SortableItemDropIndicator?` | `None` `Before` `After` | attribute `drop-indicator` | Set by the surrounding pk-sortable to draw the insertion line above or below this row while another row (or an external drag) hovers next to it. |
| `ChildContent` | `RenderFragment?` |  | default slot | The row's content. |
| `OnSortableGrab` | `EventCallback<PkSortableGrabEventArgs>` |  |  | The handle was pressed (pointerdown); consumed by the parent pk-sortable to start a drag. Not cancelable. |
| `OnSortableDrag` | `EventCallback<PkSortableDragEventArgs>` |  |  | The handle is moving while it holds pointer capture; consumed by the parent pk-sortable to update the drop position. Not cancelable. |
| `OnSortableDrop` | `EventCallback<PkSortableDropEventArgs>` |  |  | The handle released the pointer (or the drag was cancelled); consumed by the parent pk-sortable to finish or abandon the drag. Not cancelable. |

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-sortable-drag` | `PkSortableDragEventArgs` | X: double?, Y: double? |
| `pk-sortable-drop` | `PkSortableDropEventArgs` | Cancelled: bool? |
| `pk-sortable-grab` | `PkSortableGrabEventArgs` | X: double?, Y: double? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-sortable-item` in the `plainkit-sdk` skill.

## `PkSplitter`

`<pk-splitter>`: **Splitter** (Layout). Two panes with a draggable separator between them: resize by pointer or arrow keys, side by side or stacked, with a minimum and maximum size. Sizes are percentages of the room the panes share.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Orientation` | `SplitterOrientation` | `Horizontal` `Vertical` | attribute `orientation` | How the panes lie: horizontal puts them side by side (the separator is a vertical bar), vertical stacks them (the separator is a horizontal bar and it is 16rem tall unless --pk-splitter-height says otherwise). |
| `Size` | `double` |  | attribute `size` | The start pane's share of the room, in percent, kept between min and max. While the user drags or presses a key the element owns it; after pk-resize the host does. |
| `SizeChanged` | `EventCallback<double>` |  | two-way pair of `Size` | Raised when Size changes (two-way binding: `@bind-Size`). |
| `Min` | `double` |  | attribute `min` | Smallest size of the start pane, in percent (aria-valuemin). |
| `Max` | `double` |  | attribute `max` | Largest size of the start pane, in percent (aria-valuemax). |
| `Step` | `double` |  | attribute `step` | Percent moved by one arrow key press. |
| `AriaLabel` | `string?` |  | attribute `label` | The separator's accessible name (aria-label). |
| `Disabled` | `bool` |  | attribute `disabled` | Fixes the size: the separator no longer takes focus or moves. |
| `Collapsible` | `bool` |  | attribute `collapsible` | Shows a toggle on the separator that collapses the end pane (collapsed at first on a phone). |
| `Collapsed` | `bool` |  | attribute `collapsed` | The end pane is collapsed (inert, size kept). |
| `CollapsedChanged` | `EventCallback<bool>` |  | two-way pair of `Collapsed` | Raised when Collapsed changes (two-way binding: `@bind-Collapsed`). |
| `ToggleLabel` | `string?` |  | attribute `toggle-label` | The toggle's name. |
| `PersistKey` | `string?` |  | attribute `persist-key` | Saves size and collapsed in local storage under this key and restores them, unless size or collapsed is set. |
| `StartContent` | `RenderFragment?` |  | slot `start` | The first pane (left, or top when stacked); its size is size percent. |
| `EndContent` | `RenderFragment?` |  | slot `end` | The second pane, which takes the rest. |

Two-way binding: `@bind-Size`, `@bind-Collapsed`.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-collapse-change` | `PkCollapseChangeEventArgs` | Collapsed: bool? |
| `pk-resize` | `PkResizeEventArgs` | Size: double? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-splitter` in the `plainkit-sdk` skill.

## `PkStack`

`<pk-stack>`: **Stack** (Layout). A vertical stack of children with an even gap: the layout primitive for form sections, card bodies and page columns.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Gap` | `StackGap?` | `None` `Xs` `Sm` `Md` `Lg` `Xl` | attribute `gap` | Space between children, on the spacing scale (none 0, xs --space-1, sm --space-2, md --space-4, lg --space-6, xl --space-8). Default md (the flow space). |
| `Align` | `StackAlign?` | `Stretch` `Start` `Center` `End` | attribute `align` | Cross-axis alignment: stretch fills the width, the others size each child to its content. |
| `Dividers` | `bool` |  | attribute `dividers` | Draw a rule between children. |
| `ChildContent` | `RenderFragment?` |  | default slot | The items to lay out. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-stack` in the `plainkit-sdk` skill.

## `PkStatesPage`

`<pk-states-page>`: **States page** (Page types). The loading, empty, error and forbidden content a page shows in place of its own, one at a time: the page-type building block for "nothing to show yet".

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `State` | `PageState` | `Ready` `Loading` `Empty` `Error` `Forbidden` | attribute `state` | Which content shows. ready reveals the slotted content and hides the rest; every other value hides the slotted content and draws the matching built-in state (a labelled skeleton for loading, pk-empty-state for empty and forbidden, a danger pk-alert for error). |
| `Heading` | `string?` |  | attribute `heading` | Heading for empty, error or forbidden. A sensible default is used when empty ("Nothing here yet", "Something went wrong", "Not allowed"). |
| `Description` | `string?` |  | attribute `description` | Explanation for empty, error or forbidden. |
| `Label` | `string?` |  | attribute `label` | Accessible label for the loading skeleton (defaults to "Loading"). |
| `ChildContent` | `RenderFragment?` |  | default slot | The page's own content, shown only in the ready state. |
| `OnRetry` | `EventCallback` |  | event `pk-retry` | The error state's Retry action was activated. The host decides what happens (reload the data, set state back to loading); this element never retries by itself. |
| `TitleContent` | `RenderFragment?` |  | slot `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends, so the app can focus it after navigation. When present, the built-in state heading is not drawn a second time. |

When you set a named fragment (`TitleContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-states-page` in the `plainkit-sdk` skill.

## `PkText`

`<pk-text>`: **Text** (Layout). A themed paragraph, or a run of text inside a line, on the type scale and colour tokens: the text counterpart of pk-stack and pk-cluster. A variant gives it a heading, lead or eyebrow look; real headings stay native h1 to h6.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Inline` | `bool` |  | attribute `inline` | A run of text inside a line (like a span) instead of a paragraph block. |
| `Variant` | `PkTextVariant?` | `Normal` `H1` `H2` `H3` `H4` `H5` `H6` `Lead` `Eyebrow` | attribute `variant` | The type style: normal (the text around it), a heading look h1 to h6 on the --text-h1 to --text-h6 scale, lead (larger, muted intro text) or eyebrow (small uppercase label). A look only: an h1 to h6 variant is not a heading to assistive technology; use a native h1 to h6 for a real heading. size, tone and weight override the variant. |
| `Font` | `PkTextFont?` | `Inherit` `Sans` `Mono` | attribute `font` | Font family: sans (--font-sans) or mono (--font-mono, with tabular figures for ids and amounts). Default inherit. |
| `Size` | `PkTextSize?` | `Inherit` `Meta` `Read` `Lg` | attribute `size` | Font size on the type scale (meta --text-meta, read --text-read, lg --text-lg). Default inherit: the size around it. |
| `Tone` | `PkTextTone?` | `Inherit` `Default` `Muted` `Accent` `Positive` `Warning` `Critical` | attribute `tone` | Text colour from the theme: default is the body text colour (to undo a muted variant), the rest are the status colours. Default inherit: the colour around it. Never the only signal: say it in the words too. |
| `Weight` | `PkTextWeight?` | `Inherit` `Regular` `Medium` `Semibold` `Bold` | attribute `weight` | Font weight. Default inherit. |
| `WhiteSpace` | `PkTextWhiteSpace?` | `Inherit` `Normal` `Nowrap` `PreWrap` | attribute `white-space` | How line breaks and spaces are treated: pre-wrap keeps the text's own line breaks and runs of spaces (notes, raw values), nowrap stays on one line. Default inherit. |
| `Truncate` | `bool` |  | attribute `truncate` | Keep it to one line and end with an ellipsis when it does not fit. |
| `ChildContent` | `RenderFragment?` |  | default slot | The text: phrasing content only (text, links, code, badges). Several paragraphs are several pk-text in a pk-stack. |

Element details (parts, CSS custom properties, methods, accessibility): `pk-text` in the `plainkit-sdk` skill.

## `PkToolPage`

`<pk-tool-page>`: **Tool page** (Page types). A page that takes input, runs one action and shows the outcome: input fields, a Run button, and a stat, table, code block or line of text once it finishes. Set the run(values) callback as a property (business logic, never config data) - it may reject, which shows the built-in error state with Retry.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Title bar buttons, beside any Config actions. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (Config actions entry without an href) was clicked; Key is its key. |
| `Config` | `object?` |  | attribute `config` | Optional title bar from the shared page shell: heading?, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (an action without href fires pk-action), and the title bar keys of pk-page-header: badges [{ label, variant? }], actionsOverflow, overflowLabel, homeHref, homeLabel, homeIcon; nothing is drawn when none is set. { input: [{ key, type: 'text'\|'email'\|'number'\|'date'\|'textarea'\|'select', label, placeholder?, required?, options?, hint? }], outcome: 'stat'\|'table'\|'code'\|'text' }. input builds one labelled control per field (a hint, or help, adds an info button beside the label that opens a tooltip) (a select's options are [{ value, label? }] or plain strings; styled?: true on a select draws the themed list, the combobox in select mode, as pk-field-group does; type 'combobox' takes mode?: 'select' \| 'autocomplete'); outcome picks what the run() result draws as: stat (Object.assign onto pk-stat: value, label, ...), table ({ columns, rows } onto pk-table), code (a string, or { code } onto pk-code-block), text (a string, or { text }). Sent as a JSON attribute: { heading?, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }], badges?: [{ label, variant? }], input: [{ key, type, label, placeholder?, required?, options? }], outcome: 'stat'\|'table'\|'code'\|'text' }. |
| `RunLabel` | `string?` |  | attribute `run-label` | Text of the Run button. Defaults to "Run". |
| `Run` | `Func<IReadOnlyDictionary<string, JsonElement>, Task<object?>>?` |  |  | Runs the action with the values of the input fields, keyed by field key (read text with GetString(), a number with GetDouble()). What it returns is drawn as the outcome: for text a string or an object with text, for stat an object with value and label, for table an object with columns and rows, for code a string or an object with code. Throwing shows the error state with Retry. |
| `ChildContent` | `RenderFragment?` |  | default slot | Your own content in place of the generated input fields, Run button and outcome (Config input and outcome, Run and RunLabel are then unused). |
| `TitleContent` | `RenderFragment?` |  | slot `title` | The page title: a level 1 pk-heading the built-in page factory (mountTitled) appends from config.heading, so the app can focus it after navigation. |

When you set a named fragment (`BadgesContent`, `ActionsContent`, `TitleContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-tool-page` in the `plainkit-sdk` skill.

## `PkToolbar`

`<pk-toolbar>`: **Toolbar** (Layout & structure). A row with a lead (title and note) on the left and actions on the right. It wraps by its own width and stacks on a phone, with touch-sized actions.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Heading` | `string?` |  | attribute `heading` | The lead title, in strong text. |
| `Note` | `string?` |  | attribute `note` | A muted note beside the title, such as a count. |
| `ChildContent` | `RenderFragment?` |  | default slot | More lead content after the title and note (filters, chips). |
| `ActionsContent` | `RenderFragment?` |  | slot `actions` | Buttons at the right; hidden when empty, full width and touch-sized on a narrow container. |

When you set a named fragment (`ActionsContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

Element details (parts, CSS custom properties, methods, accessibility): `pk-toolbar` in the `plainkit-sdk` skill.

## `PkWizardPage`

`<pk-wizard-page>`: **Wizard page** (Page types). A multi-step flow on a page: a pk-stepper for progress, one step's fields at a time as a validated pk-form (or your own content through mountStep), and Back, Next and Submit. Next is blocked until the step's fields are valid and validate(stepId, values) passes; what was typed is kept when you go back; focus moves to the step heading on every step change. An optional Review step lists every answer before Submit. Set validate, submit, load and mountStep as properties (business logic, never config data). Reject or return { errors: { field: message } } to mark fields inline (a submit may name a field of an earlier step: the wizard goes back to it), or any other error for a notice. While answers are unsent, dirty is true, pk-wizard-dirty fires, and closing or reloading the tab asks first.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { steps, review?, reviewLabel?, submitLabel?, doneHeading?, doneDescription?, label? }. steps is [{ id, label, description?, fields? }]: fields is [{ name, label?, type?, required?, help?, options? }] (type is a pk-input type, 'textarea' or 'select'); a step without fields is drawn by mountStep. review: true adds a last step that lists all answers. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute: { steps, review?, reviewLabel?, submitLabel?, doneHeading?, doneDescription?, label? }. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |
| `OnDirty` | `EventCallback<PkWizardDirtyEventArgs>` |  | event `pk-wizard-dirty` | The unsent-answers state changed. An app that owns its router uses it to confirm before leaving. The unsent-answers state changed (Dirty): confirm before leaving the page. |
| `OnSubmit` | `EventCallback` |  | event `pk-wizard-submit` | submit() resolved; values are all the answers. Submit resolved and the done message is showing. |
| `Submit` | `Func<IReadOnlyDictionary<string, JsonElement>, Task<object?>>?` |  |  | Submits all the answers, keyed by field name (read text with GetString(), a switch with GetBoolean()). Throwing shows the exception message above the card and keeps the answers; returning null shows the done message. Return an object with errors (new { errors = new { email = "Taken" } }) to mark fields inline: a field of an earlier step takes the wizard back to it. |
| `Validate` | `Func<string, IReadOnlyDictionary<string, JsonElement>, Task<object?>>?` |  |  | Checks the answers of one step when Next is pressed (the step id and the step's values keyed by field name; the review step is not validated). Return null to go on; return an object with errors (new { errors = new { email = "Taken" } }) to mark fields inline and stay; throwing shows the exception message above the card. |
| `Load` | `Func<Task<IReadOnlyDictionary<string, object?>>>?` |  |  | Loads draft answers, keyed by field name, before the first step (the wizard shows its loading state, and an error with Retry when this throws). Once set, the wizard starts again from the first step with the draft. |
| `ChildContent` | `RenderFragment?` |  | default slot | The content. The app's own steps: one element with slot="step-<id>" per step that has no fields (a step id without spaces), projected into that step's pane. Its controls are not collected into the answers or validated; the app owns them (a Razor flow with fields is a Config step). |

When you set a named fragment (`BadgesContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |
| `pk-wizard-dirty` | `PkWizardDirtyEventArgs` | Dirty: bool? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-wizard-page` in the `plainkit-sdk` skill.

## `PkWorkspace`

`<pk-workspace>`: **Workspace** (Layout & structure). A tool page in panes: a nav pane, a main pane and an optional docked aside, each scrolling on its own. On a phone one pane shows at a time and a tab strip (Contents, Content, Details) switches it.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `Fill` | `bool` |  | attribute `fill` | Fill the height the parent gives it (a parent with a definite height, or a flex or grid parent) and drop the frame, for a tool that owns the whole page. Without it the workspace is as tall as the viewport minus a reserve. |
| `ActivePane` | `WorkspacePane` | `Nav` `Main` `Aside` | attribute `active-pane` | The pane shown on a phone. On a wider screen every pane shows. A pane that does not exist falls back to the main pane. A tab tap raises a cancelable pk-pane-change first and the element changes the pane itself only when it is not cancelled; a host that owns the pane from a route calls preventDefault() and changes the route instead (see the routed-list-detail template). |
| `ActivePaneChanged` | `EventCallback<WorkspacePane>` |  | two-way pair of `ActivePane` | Raised when ActivePane changes (two-way binding: `@bind-ActivePane`). |
| `AsideOpen` | `bool` |  | attribute `aside-open` | Show the aside: a docked column on a wide screen, a panel over the main pane on a tablet and its own pane on a phone. |
| `NavLabel` | `string?` |  | attribute `nav-label` | The nav pane's accessible name and its tab on a phone. |
| `NavWidth` | `string?` |  | attribute `nav-width` | Width of the nav column on a wide screen: sm, md, lg or xl (12, 17.5, 24 or 32rem), or a CSS length. Empty keeps the default (the --pk-workspace-nav-w hook). On a phone the panes show one at a time, so it has no effect there. |
| `MainLabel` | `string?` |  | attribute `main-label` | The main pane's accessible name and its tab on a phone. |
| `AsideLabel` | `string?` |  | attribute `aside-label` | The aside's accessible name and its tab on a phone. |
| `NavContent` | `RenderFragment?` |  | slot `nav` | The nav pane: a file tree, a list of sections, filters. Leave it out for a two-pane workspace. |
| `ChildContent` | `RenderFragment?` |  | default slot | The main pane. |
| `AsideContent` | `RenderFragment?` |  | slot `aside` | The docked aside (an inspector or a details panel). It shows when asideOpen is set. |
| `OnPaneChange` | `EventCallback<PkPaneChangeEventArgs>` |  | event `pk-pane-change` | A tab in the phone strip was chosen. Cancelable: preventDefault keeps the previous pane. |

Two-way binding: `@bind-ActivePane`.

When you set a named fragment (`NavContent`, `AsideContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-pane-change` | `PkPaneChangeEventArgs` | Pane: string?, Previous: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-workspace` in the `plainkit-sdk` skill.

## `PkWorkspacePage`

`<pk-workspace-page>`: **Workspace page** (Page types). A page of side-by-side panes (a nav, a main pane and an optional aside) that each scroll on their own, built on pk-workspace: on a phone one pane shows at a time behind a tab strip. Set the mount(panes) callback as a property (business logic, never config data): it receives { nav, main, aside } (the panes that exist) and returns a cleanup function or a { destroy() } handle, which the element calls when it leaves the page. While mount() is pending the built-in loading state shows, and a rejection shows the error state with Retry.

**Parameters**

| Parameter | Type | Enum values | Sets | Description |
|---|---|---|---|---|
| `BadgesContent` | `RenderFragment?` |  | slot `badges` | Status chips (PkBadge) beside the title, after any Config badges. |
| `Config` | `object?` |  | attribute `config` | { panes?, navLabel?, mainLabel?, asideLabel?, asideOpen?, label? }. panes lists the optional panes to create: any of 'nav' and 'aside' (default ['nav']; the main pane always exists). navLabel, mainLabel and asideLabel are the panes' accessible names and phone tabs (pk-workspace's own defaults otherwise). asideOpen is true unless set to false. label names the loading state. Optional title bar from the shared page shell: heading?: string, breadcrumb?: [{ label, href? }], actions?: [{ key, label, href?, variant?, hint? }] (a hint shows as a tooltip on the button) (an action without href fires pk-action). Nothing is drawn when none is set. Sent as a JSON attribute: { panes?: ('nav'\|'aside')[], navLabel?, mainLabel?, asideLabel?, asideOpen?, label? }. |
| `Fill` | `bool` |  | attribute `fill` | Take all the room a flex column parent leaves (the panes then scroll instead of the page). Without it the page has pk-workspace's default height. |
| `OnAction` | `EventCallback<PkActionEventArgs>` |  | event `pk-action` | A title-bar action (config.actions entry without an href) was clicked. A title-bar action (config.actions entry without an href) was clicked; Key is its key. |
| `NavContent` | `RenderFragment?` |  | slot `nav` | The nav pane (mount() not set). |
| `ChildContent` | `RenderFragment?` |  | default slot | The main pane (mount() not set). |
| `AsideContent` | `RenderFragment?` |  | slot `aside` | The aside pane (mount() not set; config.panes lists it). |

When you set a named fragment (`BadgesContent`, `NavContent`, `AsideContent`), write the body as an explicit `<ChildContent>` tag too: Razor does not allow an implicit body next to a named fragment.

**Event args**

| Event | Args class | Fields |
|---|---|---|
| `pk-action` | `PkActionEventArgs` | Key: string? |

Element details (parts, CSS custom properties, methods, accessibility): `pk-workspace-page` in the `plainkit-sdk` skill.
