---
name: plainkit-sdk
description: Build web pages and apps with the Plainkit SDK, a dependency-free UI toolkit of pk-* custom elements (pk-button, pk-input, pk-dialog, pk-table, pk-app-shell and about 139 more), CSS tokens and small ES modules, with no framework and no build step. Use it when a project links plainkit.css or plainkit.js, uses pk-* tags, or the user asks for a Plainkit page, form, dialog, layout, page template, theme, the Plainkit dev tools dock, logs viewer or logging (createLogger, ?pk-log=). Look elements, props, slots and events up in the references instead of guessing. For Blazor apps use the plainkit-blazor skill.
---

# Plainkit SDK

Plainkit 0.28.0. Generated by scripts/build-skills.mjs; do not edit.

Plainkit is plain HTML, CSS custom properties and ES modules. Components are custom elements (`pk-*`) that load on demand; there is no framework, no build step and no runtime request to another origin.

## Rules

- Use only elements, props, slots and events that appear in the references. Never invent a `pk-*` tag or an attribute: open `references/elements-index.md`, find the tag, and open the file it names. If a component you need does not exist, say so.
- Props are attributes in kebab-case (`hide-close`) or properties in camelCase (`hideClose`); a boolean prop is present or absent. Events are `addEventListener('pk-...')` and carry a `detail`.
- No inline `style` attributes, `<style>` elements, inline event handlers or inline scripts (the toolkit runs under `script-src 'self'; style-src 'self'`). Put scripts in files; style with props, `::part()`, CSS custom properties and tokens from a stylesheet.
- No literal colours: use the tokens (`references/theming.md`). No literal breakpoint widths in scripts: use the named breakpoints (same file, "Breakpoints"). The old class-based components (`.btn`, `.card`, `.modal-*`) no longer exist.
- Text is `pk-text`, not a `<p>` or `<span>` with a class: a paragraph (`<pk-text tone="muted">`), a run inside a line (`<pk-text inline weight="semibold">`), a lead or eyebrow (`variant="lead"`, `variant="eyebrow"`), a mono figure (`font="mono"`), text with its own line breaks (`white-space="pre-wrap"`). Real headings stay native `<h1>` to `<h6>` (they carry the heading role, level and outline); `variant="h1"` to `"h6"` is only the look, for text that should resemble a heading without being one. Layout is `pk-stack`, `pk-cluster` and `pk-grid`, not a styled `<div>`. A bulleted or numbered prose list is `pk-list` (`ordered`, `marker`: `auto`/`disc`/`decimal`/`check`/`none`, `gap`, `dense`), items as plain elements (a `span`, an `a`, `pk-text inline`) never a raw `ul`, `ol` or `li`. A max-width, padded content region (a doc-like page, a gallery stage, a code view) is `pk-container` (`size` `sm`/`md`/`lg`/`full` on the `--content-*` tokens, `padding` on the space scale, `scroll` `y`/`both` for a region that scrolls on its own — it needs `label` and gets `tabindex=0`/`role=region`), not a hand-styled `<div>` with `max-width`, padding and `overflow`.
- A built-in page type's `heading` (and `pk-page-header`'s `title` slot) is a light-DOM `<pk-heading level="1">`, a real heading in its own shadow tree: `querySelector('h1')` does not find it, so select `h1, pk-heading[level="1"]` (what `mountApp` focuses after navigation; the page shell gives it `tabindex="-1"`). Blazor's `PkPageHeader` draws the same slotted `pk-heading` for `Title`. The `record` page type asks before an in-app leave with unsaved edits (link, breadcrumb, back/forward: `ctx.dialogs.confirm`, Stay keeps the page, edits and address; a Save that navigates itself and `ctx.navigate` are not asked): nothing to configure. `list`, `dashboard`, `master-detail`, `settings` and `tool` configs also take `pk-page-header`'s title bar keys (`badges`, `actionsOverflow`, `overflowLabel`, `homeHref`, `homeLabel`, `homeIcon`). `pk-page-header back-link` (Blazor `BackLink`) draws a back chevron to the parent crumb; on a header of 30rem or less without a heading it folds the parent crumbs into that chevron so the title shares row one with the actions. A search box hint that says more than its accessible name: `pk-table-filters placeholder` (`searchPlaceholder` on `pk-data-table` and in a `list` config, Blazor `PkDataTable.SearchPlaceholder`), with `searchLabel` the name.
- Never use anything a reference marks **Deprecated**: it logs a warning once, and it is removed in the release named there; use what the table says.
- A `mountApp` module route marked `persist: true` keeps its built page (hidden) while another route shows and is freed with the module; use it for list-like routes, not ones that depend on params (other params rebuild it).
- Nothing fails silently: mistakes are logged as warnings (see `references/logging.md`). When a tag does nothing, check the console for a `loader` or element warning. A tag that lives in a separate package (`<pk-canvas>` and `<pk-heat-map>` are in `plainkit.canvas`, `<pk-dock>` in `plainkit.dock`, `<pk-markdown>` and `<pk-markdown-editor>` in `plainkit.markdown`) on a folder that only holds core logs one `loader` error naming the package and the zip (`plainkit.canvas-<version>.zip`, unzipped over the runtime folder; Pages: `packages/<id>/`); the tag stays inert. The shipped `dist/` already holds every package, so it never happens there. Saved state goes through `createStore` (`references/state.md`): bad or old data gives the defaults and one warning, and secrets never go in it. An app is made of modules (`defineModule`, `createModuleHost`; `references/app.md`), page `load`/`save` callbacks need no hand-written cancel, busy or disposed guards (the `list` and `record` types pass `load` an AbortSignal as its last argument and drop stale or post-leave results), long work is a task (never a hand-built toast or progress bar), and messages and questions go through `ctx.notify`/`ctx.dialogs` (never hand-built toast or dialog markup) — see `references/app.md`, "Tasks" and "Notifications and dialogs".

## References (open on demand)

- `references/app.md`: app modules: `defineModule`, `moduleFromMount`, `createModuleHost` (lifecycle, boundaries, ctx, custom page types and layouts)
- `references/build-an-app.md`: build an app: `mountPage` or `mountApp`, a module, every page type with a running example, routes, nav, state, guards, the speed, reliability and security tenets
- `references/canvas-editor.md`: an editor: a pk-dock with a toolbox, a pk-canvas and a properties panel (selection, move and resize events, snapping, keyboard, undo)
- `references/choosing.md`: choose before you build: decision path, use-case table (page type to template, layout, pattern, element, component), anti-patterns, how to ask for a missing component
- `references/conformance-rules.md`: the audit CLI's full rule catalogue (id, normal/strict severity, what it detects, a wrong/right snippet) - what `npx plainkit audit --explain <id>` also shows
- `references/custom-sdk.md`: exporting a themed or re-breakpointed `dist` from the theme editor, and the `.zip` builder underneath
- `references/elements-index.md`: every tag by group, with the file that documents the group (start here to find an element). `references/full/` holds the long form (further `examples-<group>.md`, and the templates, patterns and tool headers over 3,000 characters, by id)
- `references/known-gaps.md`: what is not built, what not to assume
- `references/layouts.md`: page anatomies (list, record, setup, tool, wizard)
- `references/loading.md`: ways to load Plainkit, `initPlainkit`, the element loader
- `references/logging.md`: the SDK logger: levels, scopes, routes, `?pk-log=`, `PkLog`, measuring a page (`measurePage`, `watchVitals`)
- `references/openers.md`: `data-open`, `data-toggle`, `data-close`
- `references/page.md`: `createPage`: busy overlay, breadcrumbs from a route tree, a `pk-card` whose body a page draws loading, empty and error states into, `pk-property-grid`
- `references/patterns.md`: composed patterns (confirm delete, filter table, forms, ...), and the paged list and cross-page selection (`pk-data-table`, `pk-table` `total`)
- `references/state.md`: the state store: `createStore`, namespaced validated versioned state, `readSetting`/`writeSetting`
- `references/templates.md`: full-page starting points
- `references/theming.md`: tokens and themes
- `references/tools.md`: dev tools dock, logs, logging settings, scorecard, performance, console, quality, theme editor, layout builder, code explorer, gallery: `mountX(container, options)` and events
- `references/upgrading.md`: moving this app to a newer Plainkit version: a blast-radius checklist, not a changelog readout
- `references/elements-actions.md`: Actions
- `references/elements-data-display.md`: Data display, media and code
- `references/elements-feedback.md`: Feedback and status
- `references/elements-form-controls.md`: Form controls
- `references/elements-form-layout.md`: Form layout
- `references/elements-layout.md`: Layout, structure and containers
- `references/elements-navigation.md`: Navigation
- `references/elements-overlays.md`: Overlays

## Workflows

Below, `plainkit/` is a copy of `dist` next to your page (see `references/loading.md` for the release zip, NuGet and other ways).

### Choose before you build

Before writing markup for a page or a job, open `references/choosing.md` (decision path, use-case table, anti-patterns) and do this:

1. Name the page type or job, and find it in the use-case table.
2. Open what it names: a template (`templates.md`), else a layout (`layouts.md`), else patterns (`patterns.md`), else the element that names the job (`elements-index.md`; a paged, searchable, selectable list is `pk-data-table` and selecting across pages `pk-table` with `total`: `patterns.md`; a routed list that opens a record sets `row-href-template="/orders/{id}"` (or `#/orders/{id}`) on `pk-table`, `pk-data-table` or `pk-list-page`, which makes the first cell a real link and routes a plain click, with no navigation code of your own; `config.toolbarInHeader` on `pk-list-page` puts the breadcrumb and heading in the toolbar row with the search and Filters (the title bar is then no `pk-page-header`: no sticky header, no back link); a form field that picks one or several records from a long list is `pk-lookup-picker`, whose `load(query)` is the data-table's, not a `pk-combobox` with thousands of options). The frame around pages is `pk-app-shell` with `pk-side-nav` or `pk-navbar`, or `pk-app-frame` when the whole frame (header, module picker, search, nav, breadcrumb) should come from data (a nav model, `current-id`, `crumbs`; the host navigates); an app of several parts is `mountPage`/`mountApp` and page types with no markup of your own (workflow **Build an app**, `references/build-an-app.md`; API in `references/app.md`). A dashboard can compose from what modules contribute (`contributes` on the config entry, `widgets` in the module, `surface` on the dashboard route: `references/build-an-app.md`; the open tab is kept in `?tab=`, filters in the module `state`, `size` spans columns; a route curates with `order`, `allow`, `deny` by `module:key`). Blazor has no app framework surface yet.
3. Paste its markup into `<main>` and its page script (if shown) into your script file (the demo shell `mountChrome` is not part of it). Change only content, slots, props, `::part()`, `--pk-*` properties and tokens. Never copy an element's internals, add `!important` or wrap slotted content in a `display: contents` element.
   Marketing or landing page: template `landing` and pattern `pricing-cards`; testimonial `pk-quote`, closing band `pk-cta-band`, and the recipes (logo strip = `pk-cluster` of `a`>`pk-media`; contact = `address`>`pk-field-list`) in pattern `marketing-sections` (no `pk-hero`, tokens only, no hero images). Rich text: `pk-markdown-editor` (the value is Markdown; toolbar of bold, italic, heading, quote, lists and link, Enter continues a list, pasting an address over selected text links it, Write and Preview; show stored text with `renderMarkdown`, never by writing the text into markup). At-a-glance overview where area is a count and colour a status ("issues by area"): `pk-heat-map` (a squarified treemap; `items` is `[{ id, label, value, severity?, href? }]` (or a slotted `<table>` as the direct child: first cell the name, a link in it the href, column 2 the value unless a header says value/count/total, headers severity/status and intensity pick those columns, `data-value` on a cell beats its text, `data-id` on a row is the id; `items` wins when set; Blazor uses `Items`) with `severity` neutral, info, success, warning or danger, `min-box` merges tiny boxes into "Other (n)" (which takes the worst severity of what it holds), `layout="rows"` keeps the input order for live data, `scale="intensity"` with per-item `intensity` 0 to 1, `max-items`, `value-format` (number, compact, share), `group-by` (an item with `children` is a framed area holding them, one level; Blazor `PkHeatItem.Children` and `GroupBy`), activating "Other" opens its members as a table over the map, `pk-heat-focus-item`, `show-table` shows the data table, `pk-heat-select` reports a box; the height is `--pk-heat-map-height`), never hand-computed `pk-grid` spans; it lives in `plainkit.canvas`. Fixed panes versus user-arranged panes: `pk-workspace` versus `pk-dock`, row in `references/choosing.md`. A nav pane that needs more room: `nav-width` on `pk-workspace` (`sm|md|lg|xl` or a CSS length; Blazor `NavWidth`); a long `pk-tree-item` label is cut with an ellipsis and carries its full text as a tooltip, or add `wrap-labels` to `pk-tree` (Blazor `PkTree WrapLabels`) to let it wrap. A testimonial is `<pk-quote>` around your own `blockquote` (slots `portrait`, `name`, `role`, `rating`; `cite`, `size="large"`; the plain blockquote shows before it upgrades; Blazor `PkQuote`). A call-to-action band is `<pk-cta-band tone="brand" align="center" bleed>` with your own `<h2 slot="heading">`, text, and `<span slot="actions">` holding `<pk-button block>`s (full width on a phone; the heading level is yours).
4. Write your own (from `pk-stack`, `pk-cluster`, `pk-grid`, `pk-text`, tokens) only when nothing fits, say which gap it fills, and never invent a `pk-*` tag. Never put a list and its record on one page (it scrolls three ways and the record is off screen): they are two routed pages, template `routed-pair` (page types `list` and `record`), and a peek at context is a flyout (`pk-drawer`); "Which list shape" in `references/choosing.md`.

### Start a page

```html
<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>My app</title>
<link rel="stylesheet" href="plainkit/plainkit.min.css">
<script type="module" src="app.js"></script>
</head>
<body>
<main>
<pk-page-header heading="Settings" level="1"></pk-page-header>
</main>
</body>
</html>
```

For a page with a header, tabs and states of its own, use `pk-page-shell` instead of assembling `pk-page-header`, `pk-tabs` and the state markup by hand: `<pk-page-shell heading="Orders" crumbs='[...]'>` with `slot="actions"` buttons, a `pk-tabs` in `slot="tabs"`, and your content as the default slot (one region: swap it on `pk-tab-change`). `state="loading|empty|error|forbidden"` (with `state-heading`, `state-description`) replaces the content and keeps the header; Retry in the error state raises `pk-page-shell-retry`. Blazor: `PkPageShell` (`Title`, `Crumbs`, `State`, `OnRetry`, `ActionsContent`, `TabsContent`). `pk-print-page` prints only its document (for labels or cards inside it use `pk-label-sheet cell-width="50mm" cell-height="25mm" columns="3" gap="2mm"`, one child per cell, `outline` for cut guides, `offset-x`/`offset-y` for pre-cut stock; Blazor `PkLabelSheet`), `pk-canvas` (plain wheel is the page's until focus; `wheel`; `auto-grid` for a grid that adapts to the zoom, `unit="mm"` with `rulers` for labelled rulers and a pointer readout, `selectable` for click, Shift-click, marquee and Ctrl+A selection of 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; `selection` is the array of ids, `pk-canvas-select` the event), `px-per-unit` when the world is already in the unit) and `pk-design-surface` are the pan-zoom and device-width grounds of a design tool, and `pk-specimen token="--space-4"` draws a design token. A hideable side panel is `pk-splitter collapsible` (`persist-key="k"` remembers its width and state; `toggle-label` names the button); a file listing with a title bar is `pk-code-view header language="js" copy` with `slot="actions"` buttons (add `fill` in a bounded parent).

```js
// app.js
import { initPlainkit } from './plainkit/js/init.js'; initPlainkit();
```

`js/init.js` is the small entry, `initPlainkit` alone. `js/plainkit.js` is the same plus the dynamic-value, theming and colour helpers; import it instead if the page uses those too (see `references/loading.md`).

### Add a form

```html
<pk-form summary>
  <form id="settings">
    <pk-form-section heading="Profile" description="Shown on invoices.">
      <pk-field label="Business name" required><pk-input name="name" required></pk-input></pk-field>
      <pk-field label="Website" help="Include https://"><pk-input name="website" type="url"></pk-input></pk-field>
      <pk-field label="Plan"><pk-select name="plan"><option value="free">Free</option><option value="pro">Pro</option></pk-select></pk-field>
    </pk-form-section>
    <pk-form-actions align="end">
      <pk-button variant="ghost" type="reset">Reset</pk-button>
      <pk-button type="submit">Save</pk-button>
    </pk-form-actions>
  </form>
</pk-form>
```

```js
document.getElementById('settings').addEventListener('submit', event => {
    event.preventDefault();
    console.log(Object.fromEntries(new FormData(event.target)));
});
```

`pk-form` shows the browser's validation messages in each field and a summary; the controls are form-associated, so `FormData` sees them by `name`. Field and control details: `references/elements-form-layout.md`, `references/elements-form-controls.md`.

A create-or-edit record page (toolbar with Cancel, Delete and Save, tabs, an error alert, a sidebar) is `pk-record-form` around your own `<form>`: `<pk-record-form cancellable deletable error=""><form>...</form><pk-card slot="sidebar">...</pk-card></pk-record-form>`. Listen for `pk-record-save` (it fires only once `pk-form` found the form valid), `pk-record-cancel` and `pk-record-delete`; set `busy` and `error` while and after you save, and call its `submit()` from a Save button in the page header (`actions-in-header`). It owns no values or save logic; the toolbar buttons fold to icons on a phone. Tabs: put a `pk-tabs` in `slot="tabs"` and tag sections (in the form or the sidebar) with `tab="key"` (several keys space-separated); at or below `tabs-from` (`phone` default, `tablet`, `always`) only the chosen tab's sections show, above it all show in two columns and the strip hides. Untagged sections always show; no per-page CSS. `tab-param="tab"` keeps the chosen tab in that query parameter (replaceState, no history entry) and opens on it from a link. Tabs as data: the `tabs` property (a JSON attribute or an array of `{ value, label, count?, visible? }`; Blazor `TabItems` of `PkRecordTab`) draws the strip itself, `count` as the badge and `visible: false` leaving a tab out; a `pk-tabs` in the `tabs` slot wins. `pk-detail-layout` does the same for `data-pk-section` cards: `tabs-from` (`collapse` default = once it collapses to one column, `phone`, `tablet`, `always`) moves where they become tabs, and `always` keeps the two-column layout with one section at a time. `hide-tabs` leaves the strip to a `pk-page-header` (set `section` from its `pk-tab-change`); `data-pk-section-order="2"` on a section's first element sorts its tab (ascending, stable; unordered sections follow in document order, so a sidebar section can sit between main ones) and `data-pk-section-count="5"` shows the tab badge; `hide-next` drops the Next button after the main column (Blazor `HideNext`); `sections-above` also manages tagged siblings before the layout (a full-width strip). `tab-param="tab"` keeps the chosen section in that query parameter the same way, and a plain `pk-tabs` takes `tab-param` too (Blazor `TabParam`): the address picks the first tab (it wins over `value`), a change is written with replaceState, no router navigation. The docked strip of `pk-page-header` (it stays in view with `sticky`) takes the same data: `tabs` (JSON array of `{ value, label, count?, visible? }`; Blazor `TabItems`) and `tabs-from` (`always` default, `phone`, `tablet`) draw it at the foot of the header; a `pk-tabs` in `slot="tabs"` wins; listen for `pk-tab-change`.

### Link that looks like a button

A control that navigates is a link, not a click handler: give `pk-button` an `href` and it renders a real anchor with the same variants, sizes and icons (middle and ctrl-click, the status-bar URL and Enter work natively; Space does not activate a link).

```html
<pk-button href="/reports">Open reports</pk-button>
<pk-button href="https://example.com/help" target="_blank" variant="secondary">Help</pk-button>
<pk-button href="/export.csv" download="export.csv" variant="ghost">Download</pk-button>
```

`target`, `rel` (`noopener` is the default for `_blank`) and `download` only apply with `href`. `disabled` and `busy` drop the href and report `aria-disabled`. A link still fires `click`, so a host can track it. An icon-only button with no box is `variant="plain"` (`ghost` keeps its 1px border, `secondary` an accent one); the collapsed `pk-app-bar-search` button is already plain, and `--pk-button-border` on a `pk-button` or on the search host sets any border colour. A bookmarklet (a javascript URL a reader drags to the bookmarks bar) needs the `bookmarklet` attribute, otherwise the href is dropped; never set it on an href from data. For a text link in prose, `pk-link` is a real anchor instead: `href` for an ordinary address, or `to` for an app route (a plain click emits a cancelable `pk-navigate` that `mountRouter({ intercept: true })`/`mountApp` picks up like a plain `<a href>`, falling back to a normal navigation outside an app); `variant` is `inline`/`muted`/`plain`, `current` sets `aria-current="page"`. Every click on a `pk-link` (`href` or `to`) also reaches a `click` listener on the element without changing the navigation, so a menu or flyout of links closes there.

### Icon-only button

Write an icon button like any other button, with its name as the text, and add `icon`: the text is hidden visually and stays the accessible name (and the hover tooltip); only the icon is drawn. `icon-name` draws a sprite symbol for you; a `pk-icon` or an svg in the button works too. `label` overrides the text. An icon button with no name at all fails the scorecard.

To show icon and text on a large screen and only the icon on a small one, give a button with an icon `collapse="phone"` (icon only on a phone) or `collapse="tablet"` (icon only on a tablet and a phone). The text stays the accessible name; a button with no icon never collapses. For a record page with several actions, put `actions-overflow="phone"` (or `tablet`, `always`) on `pk-page-header`: the slotted actions then fold into one "Actions" menu button at that width and below, each item keeping its label, icon, disabled state and click handler (no app CSS, no duplicated menu); Blazor: `ActionsOverflow`. A `pk-dropdown` or `pk-card-menu` among the actions is flattened: its items (headers kept) become menu items and choosing one selects the original.

```html
<pk-button icon variant="ghost" icon-name="plus">Add item</pk-button>
<pk-button icon variant="ghost" href="/orders"><pk-icon name="chevron-left"></pk-icon>Back to Orders</pk-button>
```

`busy` swaps the icon for the spinner and keeps the name; `toggle` keeps `aria-pressed`. Inside a `pk-tooltip` the tooltip shows the name instead of the native title.

### Open a dialog

```html
<pk-button data-open="#confirm">Delete</pk-button>
<pk-dialog id="confirm" heading="Delete this item?" size="sm">
  <p>This cannot be undone.</p>
  <pk-button slot="footer" variant="ghost" data-close>Cancel</pk-button>
  <pk-button slot="footer" variant="warn" id="confirm-delete" data-close>Delete</pk-button>
</pk-dialog>
```

`data-open`, `data-toggle` and `data-close` need no script (`references/openers.md`). On a link (`<a href="/cart" data-open="#cart">`) the opener opens the overlay instead of following the link once the overlay is defined, and the link still works with no JavaScript: progressive enhancement with no click handler of your own. To ask from code, `PkDialog.confirm({ heading, message, danger })` returns a promise of a boolean, and `PkToast.show(message, { kind })` shows a toast. `PkDialog` exists once a `pk-dialog` has connected and `PkToast` once a `pk-toast-stack` has, so have one of them in the page (empty is fine).

### Wire the dev tools

```js
import { mountDevTools } from './plainkit/modules/devtools/devtools.js';

if (location.hostname === 'localhost') {
    const tools = await mountDevTools(null, { mode: 'dock', size: 'medium' });
    tools.select('logs');
}
```

Development only (Ctrl+\` toggles it). The tools are their own unit, `dist/modules/`: unzip `plainkit-modules-<version>.zip` (or `plainkit.tools-<version>.zip`, the same modules plus the helper files they use, for a core + modules setup). `pk-canvas` (with `js/svg/`, the SVG toolkit) is likewise its own unit in a lean setup: `plainkit.canvas-<version>.zip` unzipped over the runtime folder, and `pk-markdown` / `pk-markdown-editor` (with `js/markdown/`) the same way with `plainkit.markdown-<version>.zip`, and `pk-dock` (with the `js/dock-*.js` files) with `plainkit.dock-<version>.zip`; the full zip and `dist/` already contain it into the runtime `dist` folder so it lands at `plainkit/modules/`. Options, the handle, custom panels and every other tool (`mountLogs`, `mountScorecard`, `mountThemeEditor`, ...): `references/tools.md`.

`mountDevTools` is a thin consumer of `mountToolDock`, the generic docked tabbed panel (`pk-tray` is the same job in your own markup); when to use each, how it differs from `pk-dock`, and the options: `references/tools.md`, "Tool dock and tray".

### Let people build pages with the layout builder

Pages built by your users (or by your team) from the SDK's own elements, kept as JSON and exported as CSP-safe markup. The host stores the page; the builder stores nothing.

Call `mountLayoutBuilder(element, { html | model, onchange, onsave })` from `./plainkit/modules/layout-builder/layout-builder.js` (it returns a promise of the builder handle); the full example is in `references/tools.md`.

The palette lists every element in `elements/api.json`, so a new element appears without a change. The chrome is a `pk-dock` (Palette, Structure and HTML tabs | canvas | Properties: resize, drag or use the Move menu to move a panel between groups, close and reopen panels) with File (Save, only when `onsave` is given) and Edit menus in its toolbar, each item showing its shortcut. Selection, moving (Alt+arrows or Edit > Move), duplicate, wrap, delete, undo and redo work by keyboard, menu and touch; right click (or Shift+F10, or a long press) on a canvas element opens its element menu, and on a palette button offers Add; a pointer or touch drag on a row's handle reorders the top-level page, and a palette button drags onto the canvas to insert (slot-aware when it lands on a container); each element also gets an Edit/Delete chip on hover or selection. At phone width the dock becomes one tab strip and the File and Edit menus stay in its toolbar, so Save is reachable by touch. The inspector edits the selected element's props from its API metadata. `builder.getModel()` and `builder.toHtml()` are the outputs; the model is `js/layout-model.js` (`createRegistry`, `validateDoc`, `toHtml`, `fromHtml`), which refuses unknown tags, props, slots and enum values and never lets a script, style or event handler in. The full option list and handle are in `references/tools.md`. An editor that puts a pk-canvas in a pk-dock: `references/canvas-editor.md`.

### Enable logging

In code: `configureLogging({ level, scopes, routes })` once at start, then `createLogger('checkout')` from `./plainkit/js/log.js` and `log.info(...)`; the worked example is in `references/logging.md`.

Without code: `?pk-log=debug` in the address or `data-pk-log="debug"` on `<html>`. Levels, scopes, outputs, the viewer and measuring a page's own load metrics (`measurePage`): `references/logging.md`.

### Build a page

`createPage` gives a page the bookkeeping it always needs (title, busy overlay, breadcrumb trail, a status alert) built from elements already in your markup: it never creates or owns them, only drives their props (the one exception is the loading overlay it creates for you).

```js
import { createPage } from './plainkit/js/page.js';

const page = createPage({
    alert: document.getElementById('page-alert'),
    body: document.getElementById('page-body'),   // wrapped in a pk-loading-overlay the page creates and owns
    breadcrumb: document.getElementById('page-crumbs'),
    scope: 'orders',
});
page.setTitle('Orders');
page.setBreadcrumbs([{ label: 'Home', href: '/' }, { label: 'Orders' }]);
await page.busy(() => fetchOrders(), 'Loading orders…');
// a fetchOrders() rejection is logged (through js/log.js under the given scope), shown as a
// pk-alert danger status, and rethrown so your own error handling still runs
```

A route or nav `route` ending in `/*` (`/stock/*`) owns that path and everything under it (exact routes win, then the longest prefix); Blazor `PkPageContext`/`PkNavModel` match the same way. A flat record route keeps its list crumb without being nested: `{ path: '/things/:id', labelParam/label, under: '/things' }` (Blazor `PkRoute(..., Under: "/things")`) puts it below the route owning `/things` in the trail and the nav row; the list crumb links, the record crumb is last. Where the reader is (nav row, trail, title) has ONE answer on a page that is not under `mountApp`: `import { createPageContext, bindPageContext } from './plainkit/js/page-context.js'`; `const ctx = createPageContext({ router, routes, nav })` (`nav`: `[{ id, title, route }]`, optional) gives `{ ids, section, current, crumbs, title }`, and `bindPageContext(ctx, { nav: sideNav, header, breadcrumb, title: true })` writes it as plain attributes (rows with `data-id` get `current`/`expanded`, the page header `crumbs` and `heading`, the breadcrumb its links, `document.title`) and returns the unsubscribe. A route node's `context` (object or `({ path, params, query }) => object`) and `ctx.set({ title })` (data loaded after render, forgotten at the next navigation) override field by field. No element reads the context; `pk-side-nav[current-path]` stays the zero-JS fallback. `pk-side-nav` keeps one open branch per level (opening one collapses its siblings); add `allow-multiple` to let several stay open (Blazor `AllowMultiple`). In Blazor, `PkPageContext.Compute(routes, nav, path, override)` (with `PkRoute`, `PkNavEntry`, `PkPageContextOverride`) is the same algorithm in C#, so a server-rendered page needs no interop; pass the result to the header, breadcrumb and side nav parameters yourself. A shared fixture (`core/tests/fixtures/page-context.json`) keeps JS and C# identical.

Busy is counted (overlapping calls never clear each other early), the overlay delays and debounces itself, and `page.destroy()` releases everything. Driving breadcrumbs from a route tree (`mountRouter`), a `pk-card` whose body the page fills with a loading, empty or error state (`showState`) and `pk-property-grid` (a live property inspector): `references/page.md`.

### Respond to screen size and change the theme

The elements already respond at the named breakpoints (`phone` 640, `tablet` 1024, `wide` 1280 px, desktop-first); do not restyle them there. `data-theme="dark|light"` and `data-density="compact|spacious"` on `<html>` or any element; override tokens in a stylesheet loaded after `plainkit.css`. Reading a breakpoint from script (`mediaBelow`), the literal media-query widths and every token: `references/theming.md`.

### Ship a custom SDK

A consumer's theme and breakpoint widths are two independent choices, both made in the theme editor's **Custom SDK** tab (`mountThemeEditor(el)`), never by hand-editing `dist`. Colours and styles alone export a small `plainkit-theme.css` to load after `plainkit.css`; different breakpoint widths export a whole rebuilt `dist/` zip. Steps, the zip contents and the Blazor equivalent (`PkThemeEditor`): `references/custom-sdk.md`.

### Upgrade this app to a newer Plainkit

`references/upgrading.md` is a blast-radius recipe, not a changelog summary. While you are in the app, also check it against `references/choosing.md`: a hand-built table (an editable grid is `pk-table` with `editable` and per-column `editor`s (text, number, select, date, textarea, combobox), cancelable `pk-cell-edit`, Ctrl+Z / Ctrl+Y undo and redo also go through that event, Shift+arrows select a range, Ctrl+D fills it down, Ctrl+C copies it as TSV, and pasting spreadsheet text into a cell fills a block, each cell through that event with `detail.source` `paste`, summarised by `pk-cells-paste`; a host that saves asynchronously calls `detail.wait(promise)` in that handler, which marks the cell `aria-busy` until it settles and puts the old value back with the rejection message if it rejects; an `editor: "date"` column is a native date input holding an ISO date), modal, header search or record page that a newer element or template now covers is worth replacing. Recipe: find the installed and target versions, read `CHANGELOG.md` between them (Breaking/Removed/Changed first), grep this app for what those entries name, and turn the matches into a severity-ordered checklist. Do the mechanical renames; flag what needs a judgment call.

### Share a context menu across targets

Several always-visible per-row or per-card icon buttons competing for space is the signal: wrap the region in one `pk-context-menu` (opens on right-click, Shift+F10 or a touch long-press) instead of a button row. Its `pk-open` detail names what was targeted (`context`, the nearest ancestor's `data-pk-context`; `pk-table` sets one per row already), so the handler rebuilds the `menu` slot before it paints: `references/elements-overlays.md`.

### Use the npm package with a bundler, or ship a lean production page

Import by the package's `exports` map, not by filesystem path: `import "plainkit"` (init, loader), `import "plainkit/elements/button"` (registers `pk-button`; bundlers keep it because `sideEffects` names the element modules), `import "plainkit/css"` (`plainkit.min.css`), `plainkit/app`. `plainkit/dist/*` still works for one release; move off it. No bundler? `node core/tools/bundle-page.mjs --html index.html --out public/plainkit` (or `--elements pk-button,pk-input`; `--app`, `--full`, `--dist node_modules/plainkit/dist`; needs `npm i -D esbuild`) writes `plainkit.bundle.js`, `.css` and `chunks/` (with `--flat`: one folder of `plainkit-*` files, sprite and registry included, for a host without sub-folders such as Shopify theme assets): about ten requests, not 20 to 27. Call `initPlainkit()`; re-run when the page gains an element. Sprite elsewhere on the same site: `<meta name="pk-icons" content="...">`.

### Check your work

Before you finish, run `npx plainkit audit --strict` from the app root and fix each finding by its rule id (`references/conformance-rules.md` explains the id; `npx plainkit audit --explain <id>` shows the same thing from the command line). A finding that is a real gap in the SDK — nothing else does the job — goes to the tracker (issue #336 in this repository, or the app's own tracker for a consumer project), never a local workaround. For an agent that cannot run Node, the rule families below are the checklist: read the ones that apply to what you just wrote.

- **A — accessibility attributes an element needs to have an accessible name or state**: `A1`, `A2`, `A3`, `A4`, `A5`, `A6`, `A7`, `A8`
- **B — Blazor and Razor only: a raw tag or a hand-styled/unknown Pk* component, a Razor-only syntax shape**: `B2`, `B3`, `B4`, `B5`, `B6`, `B7`, `B8`
- **D — duplicating an element or its interaction logic instead of using the one that exists**: `D1`, `D2`, `D3`, `D4`, `D5`, `D6`, `D7`, `D8`, `D9`
- **P — pages and app structure: mountApp/defineModule usage versus a hand-built page**: `P1`, `P2`, `P3`, `P4`, `P5`, `P6`, `P7`, `P8`, `P9`, `P10`
- **S — strict-module standards (security, styling, structure) carried over for a consumer app**: `S1`, `S2`, `S3`, `S4`, `S5`, `S6`, `S7`, `S8`, `S9`
- **T — tokens and standards: literal colours/sizes/fonts, deprecated names, the wrong import path**: `T1`, `T2`, `T3`, `T4`, `T5`, `T6`, `T7`, `T8`

## What is not built

`references/known-gaps.md` lists what does not exist and what not to assume (the Guides are a first set of seven with no search yet, no reactive template layer, no reordering inside a layout builder container by drag, ).
