# Page templates

> Plainkit 0.28.0. Generated by scripts/build-skills.mjs from core/samples/templates and the element API. Do not edit: change the source and run the generator.

Full-page starting points, one folder each in `gallery/templates/<id>/` (on the Pages site at https://skulmunkie.github.io/plainkit/dist/gallery/templates/ and in the release zip; the npm package leaves the gallery out). The markup below is the page content (the demo pages put it inside `<main id="content">`, or use a `pk-workspace` with that id); the demo pages wrap it in a demo shell (`chrome.js`), which you should not copy: build your own frame from `pk-app-shell` with `pk-side-nav` or `pk-navbar` (see `elements-navigation.md`, and the `overlays-nav` template, which shows one). To start from a template: put its markup in your page, keep the page script if one is shown (it is what the demo runs after the shell; its imports assume `plainkit/` is a copy of `dist` next to your page), and change the text and data.

## page: Page

Breadcrumbs, title with optional back button, page actions, content, footer.

Elements used: `pk-button` `pk-card` `pk-field-list` `pk-stack` `pk-text`.

```html
<pk-stack>
<pk-card heading="Content"><p>The page template is breadcrumbs, a title with an optional back button (both in the top bar), page actions, content and a footer. Content goes in cards or any block inside the container.</p><pk-text tone="muted">Slots: crumbs, title, actions, content, footer.</pk-text></pk-card>
<pk-card heading="Another block"><pk-field-list><dt>Name</dt><dd>Item one</dd><dt>Status</dt><dd>Active</dd><dt>Created</dt><dd>Sep 12</dd><dt>Amount</dt><dd>$12.00</dd></pk-field-list></pk-card>
</pk-stack>
```

## app: App shell

A whole app from one config: mountApp builds the chrome and each module supplies routes of built-in page types: an overview (dashboard) and an inbox (master-detail).

Start here for an app with several areas: `mountApp(el, { brand, modules })`, each module a `defineModule({ id, title, nav, routes })`, each route a page type with its config. `master-detail` shows a list and the open record on one route; it is not the default for a list and its record (two routes, `routed-pair`). `rowHref` and `listHref` only navigate; `mountDetail(pane, id, ctx)` returns a handle with `destroy()`.

Elements used: .

The full text is long (3143 characters): it is in `references/full/templates.md`, section `app` (kept apart so the group files stay short).

## workspace: Workspace

A nav rail, one tab strip over a content pane and a docked inspector. Fills the viewport; rail and pane scroll on their own.

Elements used: `pk-button` `pk-field-list` `pk-input` `pk-stack` `pk-tab` `pk-tab-panel` `pk-tabs` `pk-text` `pk-toolbar` `pk-tree` `pk-tree-item` `pk-workspace`.

```html
<pk-workspace id="content" fill nav-label="List" main-label="Content" aside-label="Inspector">
  <pk-stack slot="nav" gap="sm">
    <pk-input type="search" label="Filter the list" placeholder="Filter the list"></pk-input>
    <pk-tree label="Items"><pk-tree-item label="Group" expanded><pk-tree-item label="First item" selected></pk-tree-item><pk-tree-item label="Second item"></pk-tree-item></pk-tree-item></pk-tree>
  </pk-stack>
  <pk-tabs value="one" overflow="scroll">
    <pk-tab value="one" closable>First item</pk-tab>
    <pk-tab value="two">Second item</pk-tab>
    <pk-button slot="trailing" id="inspector-toggle" toggle variant="ghost" size="mini">Inspector</pk-button>
    <pk-tab-panel value="one"><pk-stack><h2>First item</h2><p>The pane shows what the active tab selects.</p><pk-field-list><dt>Name</dt><dd>Item one</dd><dt>Status</dt><dd>Active</dd><dt>Created</dt><dd>Sep 12</dd><dt>Amount</dt><dd>$12.00</dd></pk-field-list></pk-stack></pk-tab-panel>
    <pk-tab-panel value="two"><p>Second item content.</p></pk-tab-panel>
  </pk-tabs>
  <pk-stack slot="aside" gap="sm"><pk-toolbar heading="Inspector"><pk-button id="inspector-close" slot="actions" size="mini" variant="plain" icon label="Close inspector" icon-name="x"></pk-button></pk-toolbar><pk-text tone="muted">A docked inspector covers the pane, not the page.</pk-text></pk-stack>
</pk-workspace>
```

Page script:

```js
// The inspector is the workspace's docked aside; its toggle and its close button keep asideOpen and the pressed state together.
const workspace = document.getElementById('content');
const toggle = document.getElementById('inspector-toggle');
toggle.addEventListener('pk-toggle', e => { workspace.asideOpen = e.detail.pressed; });
document.getElementById('inspector-close').addEventListener('click', () => { workspace.asideOpen = false; toggle.pressed = false; });

// Closing a tab removes it and its panel; the strip then selects a neighbour.
const tabs = document.querySelector('pk-tabs');
tabs.addEventListener('pk-tab-close', e => {
    const tab = e.target.closest('pk-tab');
    const panel = tabs.querySelector('pk-tab-panel[value="' + tab.value + '"]');
    const all = [...tabs.querySelectorAll('pk-tab')];
    const neighbour = all[all.indexOf(tab) + 1] ?? all[all.indexOf(tab) - 1];
    tab.remove(); panel?.remove();
    if (neighbour) tabs.value = neighbour.value;
});
```

## routed-pair: List page and record page

The canonical list-and-record shape: a list page (search, filter, sort, paging) and a record page (form, save, dirty guard) as two separately routed pages. A row click navigates to the record; Save navigates back.

Use this one by default. It is a mountApp module of two page types: `list` (a pk-data-table: search, status filter, sort, paging, empty and error states, all from one `load(query)` callback) and `record` (a form with Save; `load(id)` for an existing record, none for `/new`). The record route is a child of the list route, so the list's nav entry stays current and the breadcrumbs read Things > Thing 7. A row click is `rowHref` (a route change) and Save calls `ctx.navigate('/')`, so a deep link, a refresh and the back button all land in the right place and the route is the only state. The record page marks itself dirty and asks before the tab closes with unsaved edits. Do not put a list and its record on one page (a list with the record stacked below or beside it): it scrolls three ways and the record is off screen while the list is in view. When the record is only a peek at context, open a flyout over the list instead. A tool shell with panes (`routed-list-detail`) is the workspace variant, not the default.

Elements used: .

The full text is long (4284 characters): it is in `references/full/templates.md`, section `routed-pair` (kept apart so the group files stay short).

## dashboard: Dashboard

Stat tiles, a chart area and a table.

Elements used: `pk-badge` `pk-card` `pk-grid` `pk-icon` `pk-progress` `pk-stack` `pk-stat` `pk-table`.

```html
<pk-stack>
<pk-grid min="12rem"><pk-stat tile label="Metric one" value="128"></pk-stat><pk-stat tile tone="positive" label="Metric two" value="$4,210" subtext="+12% vs last period"></pk-stat><pk-stat tile tone="warning" label="Metric three" value="7" subtext="Needs attention"><pk-icon slot="icon" name="warning"></pk-icon></pk-stat><pk-stat tile tone="critical" label="Metric four" value="3" subtext="Below target"><pk-icon slot="icon" name="error"></pk-icon></pk-stat></pk-grid>
<pk-grid columns="2" min="20rem"><pk-card heading="Trend"><pk-stack gap="sm"><pk-progress label="P1" value="80" show-value></pk-progress><pk-progress label="P2" value="60" show-value></pk-progress><pk-progress label="P3" value="90" show-value></pk-progress><pk-progress label="P4" value="45" show-value></pk-progress><pk-progress label="P5" value="70" show-value></pk-progress></pk-stack></pk-card>
<pk-card heading="Recent"><pk-table label="Recent" clickable cards columns='[{"key":"name","label":"Name"},{"key":"status","label":"Status"},{"key":"amount","label":"Amount","type":"number"}]' rows='[{"id":1,"name":"Item one","status":"Active","amount":"$12.00"},{"id":2,"name":"Item two","status":"Draft","amount":"$8.50"},{"id":3,"name":"Item three","status":"Review","amount":"$20.00"},{"id":4,"name":"Item four","status":"Active","amount":"$5.25"}]'><a slot="cell-1-name" href="#">Item one</a><pk-badge slot="cell-1-status" variant="ok">Active</pk-badge><a slot="cell-2-name" href="#">Item two</a><pk-badge slot="cell-2-status" variant="muted">Draft</pk-badge><a slot="cell-3-name" href="#">Item three</a><pk-badge slot="cell-3-status" variant="warn">Review</pk-badge><a slot="cell-4-name" href="#">Item four</a><pk-badge slot="cell-4-status" variant="ok">Active</pk-badge></pk-table></pk-card></pk-grid>
</pk-stack>
```

## form: Form and settings

Sectioned form with help and validation states and a sticky save bar.

Elements used: `pk-button` `pk-card` `pk-field` `pk-form-actions` `pk-grid` `pk-input` `pk-range` `pk-select` `pk-stack` `pk-switch` `pk-text`.

```html
<form>
<pk-stack>
<pk-card heading="Section one"><pk-grid min="16rem"><pk-field label="Name" required help="Help text sits below the input."><pk-input value="Example"></pk-input></pk-field><pk-field label="Email" error="Enter a valid email address."><pk-input value="not-an-email"></pk-input></pk-field><pk-field label="Choice"><pk-select><option>One</option></pk-select></pk-field></pk-grid></pk-card>
<pk-card heading="Section two"><pk-stack gap="sm"><pk-switch checked>Option enabled</pk-switch><pk-range label="Amount" min="0" max="100" value="40" output></pk-range></pk-stack></pk-card>
<pk-form-actions sticky><pk-button type="submit">Save</pk-button><pk-button variant="ghost">Cancel</pk-button><pk-text inline tone="muted" slot="status">Unsaved changes</pk-text></pk-form-actions>
</pk-stack>
</form>
```

## wizard: Wizard

A stepper, one card per step, Back and Next.

Elements used: `pk-button` `pk-card` `pk-field` `pk-grid` `pk-input` `pk-stack` `pk-step` `pk-stepper` `pk-toolbar`.

```html
<pk-stack>
<pk-stepper label="Wizard" current="1" clickable><pk-step heading="Step one"></pk-step><pk-step heading="Step two"></pk-step><pk-step heading="Review"></pk-step></pk-stepper>
<pk-card id="step-card" heading="Step two"><pk-grid min="16rem"><pk-field label="Field"><pk-input value="Value"></pk-input></pk-field><pk-field label="Other field"><pk-input></pk-input></pk-field></pk-grid></pk-card>
<pk-toolbar><pk-button id="back" slot="actions" variant="ghost">Back</pk-button><pk-button id="next" slot="actions">Next</pk-button></pk-toolbar>
</pk-stack>
```

Page script:

```js
// The stepper, the card title and the Back and Next buttons stay in step: the elements do the rest.
const titles = ['Step one', 'Step two', 'Review'];
const stepper = document.querySelector('pk-stepper');
const card = document.getElementById('step-card');
const back = document.getElementById('back');
const next = document.getElementById('next');
function update(n) {
    card.heading = titles[n];
    back.disabled = n === 0;
    next.disabled = n === titles.length - 1;
}
function show(n) { stepper.current = n; update(n); }
back.addEventListener('click', () => show(stepper.current - 1));
next.addEventListener('click', () => show(stepper.current + 1));
// A click on a reached step: the event fires before the stepper moves, so it carries the target index.
stepper.addEventListener('pk-step-change', e => update(e.detail.index));
show(stepper.current);
```

## states: Empty, error and loading

What a region shows with no data, on failure and while loading.

Elements used: `pk-alert` `pk-button` `pk-card` `pk-cluster` `pk-empty-state` `pk-grid` `pk-skeleton` `pk-spinner` `pk-stack` `pk-text`.

```html
<pk-grid min="14rem"><pk-card><pk-empty-state heading="Nothing here yet" description="Say what is missing and what to do."><pk-button slot="actions">Add one</pk-button></pk-empty-state></pk-card>
<pk-card><pk-stack><pk-alert kind="danger" heading="Something failed.">Explain it and offer a way out.</pk-alert><pk-alert kind="warning" compact>Two items are queued.<pk-button slot="action" size="mini">Retry now</pk-button></pk-alert></pk-stack></pk-card>
<pk-card aria-busy="true"><pk-stack gap="sm"><pk-cluster gap="sm"><pk-spinner size="sm" label="Loading"></pk-spinner><pk-text inline tone="muted" role="status">Loading…</pk-text></pk-cluster><pk-skeleton></pk-skeleton><pk-skeleton></pk-skeleton></pk-stack></pk-card></pk-grid>
```

## auth: Auth (sign in)

A centred card on a centred shell.

Elements used: `pk-button` `pk-card` `pk-checkbox` `pk-field` `pk-input` `pk-stack` `pk-text`.

```html
<main class="u-w-full u-mw-24r u-p-1r-1p25r">
    <pk-card heading="Sign in" level="1">
      <pk-stack>
        <pk-field label="Email"><pk-input autocomplete="username"></pk-input></pk-field>
        <pk-field label="Password"><pk-input type="password" reveal autocomplete="current-password"></pk-input></pk-field>
        <pk-checkbox>Keep me signed in</pk-checkbox>
        <pk-button block>Sign in</pk-button>
        <pk-text tone="muted"><a href="../page/page.html">Back to the templates</a></pk-text>
      </pk-stack>
    </pk-card>
  </main>
```

Page script:

```js
import { loadElements, observeElements } from './plainkit/js/loader.js';
import { setTheme } from './plainkit/js/theme.js';
setTheme(document.documentElement, new URLSearchParams(location.search).get('theme') === 'light' ? 'light' : 'dark');
loadElements(document);
observeElements(document);
```

## landing: Landing page

A marketing page composed from existing elements: hero, feature grid, pricing cards and FAQ. Colours and sizes come from tokens; imagery, if any, goes in pk-media.

Elements used: `pk-accordion` `pk-accordion-item` `pk-badge` `pk-button` `pk-card` `pk-cluster` `pk-container` `pk-grid` `pk-heading` `pk-list` `pk-stack` `pk-text`.

```html
<pk-container size="lg">
<pk-stack gap="lg">
<pk-stack gap="md" align="start">
<pk-text variant="eyebrow">Product name</pk-text>
<pk-heading level="1">One sentence that says what it does</pk-heading>
<pk-text variant="lead">A second sentence for who it is for and why it is better.</pk-text>
<pk-cluster gap="sm"><pk-button variant="primary">Get started</pk-button><pk-button>See how it works</pk-button></pk-cluster>
</pk-stack>
<pk-stack gap="md">
<pk-heading level="2">Features</pk-heading>
<pk-grid min="16rem">
<pk-card heading="Feature one" level="3"><pk-text>One line on the benefit, not the mechanism.</pk-text></pk-card>
<pk-card heading="Feature two" level="3"><pk-text>One line on the benefit, not the mechanism.</pk-text></pk-card>
<pk-card heading="Feature three" level="3"><pk-text>One line on the benefit, not the mechanism.</pk-text></pk-card>
</pk-grid>
</pk-stack>
<pk-stack gap="md">
<pk-heading level="2">Pricing</pk-heading>
<pk-grid min="16rem">
<pk-card heading="Starter" level="3"><pk-stack><pk-text variant="h2">$0 <pk-text inline tone="muted">per month</pk-text></pk-text><pk-list dense><li>One project</li><li>Community support</li></pk-list><pk-button block>Start free</pk-button></pk-stack></pk-card>
<pk-card heading="Team" level="3"><pk-badge slot="actions">Popular</pk-badge><pk-stack><pk-text variant="h2">$12 <pk-text inline tone="muted">per seat, per month</pk-text></pk-text><pk-list dense><li>Unlimited projects</li><li>Email support</li></pk-list><pk-button variant="primary" block>Choose Team</pk-button></pk-stack></pk-card>
<pk-card heading="Business" level="3"><pk-stack><pk-text variant="h2">Talk to us</pk-text><pk-list dense><li>Single sign-on</li><li>Named contact</li></pk-list><pk-button block>Contact sales</pk-button></pk-stack></pk-card>
</pk-grid>
</pk-stack>
<pk-stack gap="md">
<pk-heading level="2">Questions</pk-heading>
<pk-accordion><pk-accordion-item heading="Can I change plans later?">Yes, at any time; billing adjusts on the next invoice.</pk-accordion-item><pk-accordion-item heading="Is there a free trial?">Say what the trial includes and how long it lasts.</pk-accordion-item></pk-accordion>
</pk-stack>
</pk-stack>
</pk-container>
```

Page script:

```js
import { loadElements, observeElements } from './plainkit/js/loader.js';
import { setTheme } from './plainkit/js/theme.js';
setTheme(document.documentElement, new URLSearchParams(location.search).get('theme') === 'light' ? 'light' : 'dark');
loadElements(document);
observeElements(document);
```

## overlays-nav: Overlays and navigation

An app page built from the overlay, feedback and navigation elements: app shell with a filterable side nav, breadcrumb, banner, stepper, on-this-page list, dropdown, select, tooltip, confirm popover, drawer, dialog, context menu, loading overlay, pager, command palette and back to top.

Elements used: `pk-alert` `pk-app-shell` `pk-back-to-top` `pk-breadcrumb` `pk-button` `pk-card` `pk-cluster` `pk-command-palette` `pk-context-menu` `pk-dialog` `pk-drawer` `pk-dropdown` `pk-grid` `pk-loading-overlay` `pk-menu-item` `pk-nav-item` `pk-pager` `pk-popover` `pk-scroll-progress` `pk-select` `pk-side-nav` `pk-skip-link` `pk-stack` `pk-step` `pk-stepper` `pk-text` `pk-toc` `pk-tooltip`.

The full text is long (7336 characters): it is in `references/full/templates.md`, section `overlays-nav` (kept apart so the group files stay short).

## routed-list-detail: Workspace list and record

The workspace (tool shell) variant, not the default list-and-record shape (see "List page and record page"): a pk-data-table (search, status filter, sort, pager) in the main pane and the open record in the aside, driven by the route.

Use this only for a tool or workspace shell where the list and one record share a screen on purpose (an inspector, a console). A list with its record as two routes is `routed-pair`, the default; a list with the record stacked or beside it on an ordinary page is not offered (it scrolls three ways and the record is off screen while the list is in view). Put the list in the main pane and the record in the aside. The route is the only state: a record active means `aside-open`, `active-pane="aside"`, `aside-label` set to the record's name and `current-row` set to its id; no record means the list alone. A row click, Back, Close, Cancel and Save only change the route, and a handler sets the properties from it, so a deep link, a refresh and the back button all land in the same place. On a phone the workspace shows one pane at a time with a tab strip (list, record) and the header's back arrow (shown below 641px only) returns to the list; a tap on the list tab raises a cancelable `pk-pane-change` that the page vetoes with `preventDefault()` before changing the route. The aside has no header of its own: build one from `pk-page-header` (`variant="section"`, Back and Close in its `actions` slot). The list is one `pk-data-table`: it owns the query (search, status filter, sort, pager), the loading, empty and error states, and the page only answers `load(query)`; `js/list-query.js` is a generic filter/search/sort/paginate function, not page-specific logic. The page-level titlebar's New-thing action is an icon-only `Actions` button (`pk-dropdown` + `pk-menu-item`, the same pattern the shell uses for its own User menu).

In Blazor the same page is a `@page` component over `PkWorkspace` and `PkTable` (`CurrentRow`), with the route as the only state (the tabs, search and pager are omitted here for brevity; wire them the same way as the SDK sample: filter/sort/page the list in code, and pass the result as `Items`):

```razor
@page "/things"
@page "/things/{Id}"
@inject NavigationManager Nav

<PkWorkspace Fill MainLabel="Things" AsideLabel="@_title" AsideOpen="@_open" ActivePane="@_pane" OnPaneChange="PaneChanged">
    <PkStack Gap="sm">
        <PkPageHeader Heading="Things" />
        <PkTable Items="_things" Columns="_columns" IdOf="t => t.Id" Label="Things" Clickable CurrentRow="@Id" OnRowClick="Open" />
    </PkStack>
    <AsideContent>
        <PkStack Gap="sm">
            <PkPageHeader Variant="PageHeaderVariant.Section" Heading="@_title">
                <ActionsContent>
                    <PkButton Size="ButtonSize.Mini" Variant="ButtonVariant.Ghost" OnClick="Close">Back</PkButton>
                </ActionsContent>
            </PkPageHeader>
            <PkForm Summary OnValid="Close">
                <form @onsubmit:preventDefault>
                    <PkField Label="Name" Required><PkInput @bind-Value="_name" Name="name" Required /></PkField>
                    <PkFormActions Align="end">
                        <PkButton Type="button" Variant="ButtonVariant.Plain" OnClick="Close">Cancel</PkButton>
                        <PkButton Type="submit" Variant="ButtonVariant.Primary">Save</PkButton>
                    </PkFormActions>
                </form>
            </PkForm>
        </PkStack>
    </AsideContent>
</PkWorkspace>

@code {
    [Parameter] public string? Id { get; set; }

    private readonly List<Thing> _things = [new("1", "Blue widget"), new("2", "Red widget")];
    private readonly IReadOnlyList<PkTableColumn<Thing>> _columns = [new() { Key = "name", Label = "Name" }];
    private Thing? _thing;
    private string? _name;
    private string _title = "Thing";
    private WorkspacePane _pane = WorkspacePane.Main;
    private bool _open;

    // The route is the only state: it says which record is open, and this handler derives everything else from it.
    protected override void OnParametersSet()
    {
        _thing = _things.FirstOrDefault(t => t.Id == Id);
        _name = _thing?.Name;
        _title = _thing?.Name ?? "Thing";
        _open = _thing is not null;
        _pane = _open ? WorkspacePane.Aside : WorkspacePane.Main;
    }

    private void Open(PkTableRowClickArgs<Thing> row) => Nav.NavigateTo($"/things/{row.Id}");
    private void Close() => Nav.NavigateTo("/things");
    private void PaneChanged(PkPaneChangeEventArgs e) { if (e.Pane == "main") Close(); }

    private record Thing(string Id, string Name);
}
```

Elements used: `pk-button` `pk-data-table` `pk-dropdown` `pk-field` `pk-form` `pk-form-actions` `pk-icon` `pk-input` `pk-menu-item` `pk-page-header` `pk-select` `pk-stack` `pk-workspace`.

The full text is long (6456 characters): it is in `references/full/templates.md`, section `routed-list-detail` (kept apart so the group files stay short).
