@kerfjs/ui 5.0.0-beta.3 → 5.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +12 -9
  2. package/ai/component-catalog.json +314 -172
  3. package/ai/public-api-signatures-v1.md +333 -26
  4. package/ai/skill.md +17 -9
  5. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  6. package/dist/browser/{dialog-header.js → panel-header.js} +3 -2
  7. package/dist/{chunk-GY5WH7TO.js → chunk-24Z2XE6C.js} +3 -3
  8. package/dist/chunk-24Z2XE6C.js.map +1 -0
  9. package/dist/chunk-2RJBFNB6.js +172 -0
  10. package/dist/chunk-2RJBFNB6.js.map +1 -0
  11. package/dist/chunk-KZKSBUKC.js +19 -0
  12. package/dist/chunk-KZKSBUKC.js.map +1 -0
  13. package/dist/chunk-RBTVBGRD.js +23 -0
  14. package/dist/chunk-RBTVBGRD.js.map +1 -0
  15. package/dist/device-class.d.ts +62 -0
  16. package/dist/device-class.js +75 -0
  17. package/dist/device-class.js.map +1 -0
  18. package/dist/index.d.ts +2 -3
  19. package/dist/index.js +8 -9
  20. package/dist/nav-stack.d.ts +38 -0
  21. package/dist/nav-stack.js +4 -0
  22. package/dist/nav-stack.js.map +1 -0
  23. package/dist/panel-header.d.ts +26 -0
  24. package/dist/panel-header.js +6 -0
  25. package/dist/panel-header.js.map +1 -0
  26. package/dist/split-view.d.ts +42 -0
  27. package/dist/split-view.js +20 -0
  28. package/dist/split-view.js.map +1 -0
  29. package/dist/styles/foundation.css +21 -0
  30. package/dist/styles/menu-action-row.css +6 -0
  31. package/dist/styles/menu-header.css +19 -2
  32. package/dist/styles/menu-item.css +5 -0
  33. package/dist/styles/nav-stack.css +112 -0
  34. package/dist/styles/panel-header.css +59 -0
  35. package/dist/styles/split-view.css +35 -0
  36. package/dist/styles/styles.css +1 -2
  37. package/dist/styles/tab-scaffold.css +84 -0
  38. package/dist/styles/toolbar-control-group.css +52 -2
  39. package/dist/styles/toolbar-text.css +11 -0
  40. package/dist/styles/toolbar.css +9 -0
  41. package/dist/styles/workbench.css +94 -0
  42. package/dist/tab-scaffold.d.ts +30 -0
  43. package/dist/tab-scaffold.js +16 -0
  44. package/dist/tab-scaffold.js.map +1 -0
  45. package/dist/toolbar-text.d.ts +4 -2
  46. package/dist/toolbar-text.js +1 -1
  47. package/dist/wire-nav-stack.d.ts +15 -0
  48. package/dist/wire-nav-stack.js +88 -0
  49. package/dist/wire-nav-stack.js.map +1 -0
  50. package/dist/wire-tab-scaffold.d.ts +11 -0
  51. package/dist/wire-tab-scaffold.js +16 -0
  52. package/dist/wire-tab-scaffold.js.map +1 -0
  53. package/dist/wire-token-search-fields.d.ts +46 -4
  54. package/dist/wire-token-search-fields.js +1 -1
  55. package/dist/workbench.d.ts +33 -0
  56. package/dist/workbench.js +17 -0
  57. package/dist/workbench.js.map +1 -0
  58. package/docs/accessibility.md +10 -10
  59. package/docs/app-layouts.md +57 -0
  60. package/docs/component-contract.md +37 -9
  61. package/docs/component-selection.md +57 -8
  62. package/docs/design-philosophy.md +23 -1
  63. package/docs/device-class.md +54 -0
  64. package/docs/layout.md +44 -8
  65. package/docs/nav-stack.md +47 -0
  66. package/docs/recipes.md +29 -9
  67. package/docs/split-view.md +49 -0
  68. package/docs/tab-scaffold.md +41 -0
  69. package/docs/ux-demo.md +11 -9
  70. package/docs/workbench.md +47 -0
  71. package/llms.txt +107 -43
  72. package/package.json +37 -11
  73. package/ux-demo/recipes/app-shell.tsx +3 -3
  74. package/ux-demo/recipes/composer-form.tsx +5 -4
  75. package/ux-demo/recipes/list-workspace-states.tsx +2 -2
  76. package/ux-demo/recipes/loaders.ts +2 -0
  77. package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
  78. package/ux-demo/recipes/mount-recipe.ts +3 -0
  79. package/ux-demo/recipes/navigation-stack.tsx +76 -0
  80. package/ux-demo/recipes/recipes.css +40 -13
  81. package/ux-demo/recipes/workspace-header.tsx +2 -2
  82. package/dist/browser/page-header.js +0 -3
  83. package/dist/chunk-2PES33HS.js +0 -13
  84. package/dist/chunk-2PES33HS.js.map +0 -1
  85. package/dist/chunk-GY5WH7TO.js.map +0 -1
  86. package/dist/chunk-H5AGGVU5.js +0 -75
  87. package/dist/chunk-H5AGGVU5.js.map +0 -1
  88. package/dist/chunk-K3G72I6D.js +0 -24
  89. package/dist/chunk-K3G72I6D.js.map +0 -1
  90. package/dist/dialog-header.d.ts +0 -15
  91. package/dist/dialog-header.js +0 -5
  92. package/dist/dialog-header.js.map +0 -1
  93. package/dist/page-header.d.ts +0 -9
  94. package/dist/page-header.js +0 -3
  95. package/dist/page-header.js.map +0 -1
  96. package/dist/styles/dialog-header.css +0 -87
  97. package/dist/styles/page-header.css +0 -26
@@ -15,6 +15,15 @@ The application adapter is usually a plain function that maps domain state to
15
15
  component props plus stable `data-action` values. It is not a fork of package
16
16
  markup or CSS.
17
17
 
18
+ **Don't fight the components.** The package is built to look right unstyled, so
19
+ custom CSS is the exception. Before adding `padding`, `margin`, `width`, `height`,
20
+ `border`, `background`, a wrapper card, or a decoration, check whether the
21
+ component, the pane, or the content-item already owns it — it almost always does,
22
+ and adding more usually double-insets or fights it. Trust component defaults and
23
+ fix the surrounding layout instead of overriding a control. See
24
+ [`design-philosophy.md`](./design-philosophy.md) "Reach for the primitive, not for
25
+ CSS".
26
+
18
27
  ## Production recipes
19
28
 
20
29
  Use the [complete recipe guide](./recipes.md) when several primitives form one
@@ -29,6 +38,7 @@ application boundary:
29
38
  | Composer form | [Catalog](../ux-demo/) · `?component=recipe-composer-form` |
30
39
  | List workspace states | [Catalog](../ux-demo/) · `?component=recipe-list-workspace-states` |
31
40
  | Compact toolbar choices and actions | [Catalog](../ux-demo/) · `?component=recipe-compact-toolbar` |
41
+ | Navigation stack | [Catalog](../ux-demo/) · `?component=recipe-navigation-stack` |
32
42
 
33
43
  Recipes use public production exports and show ownership boundaries; they are
34
44
  copyable reference compositions, not new monolithic components.
@@ -50,35 +60,74 @@ an upstream component or recipe request.
50
60
  | --- | --- | --- | --- | --- | --- | --- |
51
61
  | Icon — `LucideIcon` | A decorative or explicitly labeled Lucide-compatible icon belongs in app UI. | Do not use an icon as the only name of an unfamiliar action; add visible or accessible text. Prefer it over Web Awesome `wa-icon`. | None. | Icon choice and meaningful label. | `@kerfjs/ui/lucide-icon` | [Accessibility](./accessibility.md#shared-rules) |
52
62
  | Disclosure indicator — `DisclosureArrow` | A control needs one animated 18px root-scaled visual for open and closed state, including configurable directions or a replacement icon. | Do not use it as the interactive control or accessible name; place it inside the button or control that exposes expanded state. | Pass the controlled `open` state, render it inside the owning control, author replacement icon content facing right before transforms, and override `--kui-disclosure-arrow-size` only when another visual size is required. Direction changes use the shortest rotation path, with counterclockwise chosen for a 180-degree closed-to-open tie. | Open state, interaction, accessible name, size override, replacement glyph, direction choices, and shortest-path rotation. | `@kerfjs/ui/disclosure-arrow` | [Component ownership](./component-contract.md#ownership-boundaries) |
53
- | Application toolbar — `Toolbar` | Leading identity, optional centered content, and trailing controls form one horizontal app bar. | Do not use it for a page title and one page action; use `PageHeader`. Do not use it as a dialog heading; use `DialogHeader`. | Compose `ToolbarText` and `ToolbarControlGroup` where their contracts fit. | Actions, command availability, responsive relocation, and state. | `@kerfjs/ui/toolbar` | [Toolbar composition](../README.md#component-subpaths) |
63
+ | Application toolbar — `Toolbar` | Leading identity, optional centered content, and trailing controls form one horizontal app bar. | Do not use it for a page, panel, or dialog heading; use `PanelHeader`. | Compose `ToolbarText` and `ToolbarControlGroup` where their contracts fit. | Actions, command availability, responsive relocation, and state. | `@kerfjs/ui/toolbar` | [Toolbar composition](../README.md#component-subpaths) |
54
64
  | Toolbar control cluster — `ToolbarControlGroup` | Related toolbar controls need contained, borderless, pressed, or single-control treatment. | Do not use it merely to align unrelated buttons; use toolbar slots or ordinary layout. Web Awesome `wa-button-group` is only for an exceptional grouped-action contract. | Delegate child actions; use `SegmentedControl` for an exclusive choice. | Actions, pressed/expanded state, and policy. | `@kerfjs/ui/toolbar-control-group` | [Component ownership](./component-contract.md#ownership-boundaries) |
55
- | Toolbar identity text — `ToolbarText` | A toolbar needs large, default, or compact textual identity. | Do not substitute it for document heading semantics; use `PageHeader` or native headings. | None. | Text and responsive priority. | `@kerfjs/ui/toolbar-text` | [Toolbar composition](../README.md#component-subpaths) |
65
+ | Toolbar identity text — `ToolbarText` | A toolbar needs extra-large (page/panel title), large, default, or compact textual identity. | Do not substitute it for document heading semantics; use `PanelHeader` or native headings. | None. | Text and responsive priority. | `@kerfjs/ui/toolbar-text` | [Toolbar composition](../README.md#component-subpaths) |
56
66
  | Navigation row — `MenuItem` | A pane or navigation area needs a selectable, disabled, dormant-trailing, or multiline action row. | Do not put a control in `trailing`; use `MenuActionRow` when the trailing region must be independently interactive. Use an `<a>` for navigation that must retain link behavior, a native `<button>` for an ordinary action, or implement the complete ARIA menu widget. | Delegate its `data-action`; compose inside a `.kui-content` section. Put domain event/drop metadata in `rootAttributes` rather than adding wrapper markup. | Routing, selection, permissions, copy, action handling, and domain `data-*` values. | `@kerfjs/ui/menu-item` | [Pane geometry](../README.md#pane-and-content-geometry) |
57
67
  | Navigation row with trailing action — `MenuActionRow` | A full-width row needs a selectable primary action and an independently focusable trailing action. | Use `MenuItem` when trailing content is dormant metadata. Do not put controls inside the row's `label`, `icon`, or `trailingActionIcon` SafeHtml slots. Do not use `AppTab` outside tablist semantics or `ToolbarControlGroup` outside a toolbar. | Delegate both action strings; update controlled selection and any popover/context-menu state in the app. | Routing, selection, both action policies, domain metadata, and popover/context-menu behavior. | `@kerfjs/ui/menu-action-row` | [Accessibility](./accessibility.md#menuactionrow) |
58
- | Navigation section heading — `MenuHeader` | A menu section needs a full-width label, semantic count, non-count badge, logical-end action, or real disclosure state. | Do not concatenate counts into `label` or put numeric content in `badge`; use `count` with the localized full phrase in `countLabel`. Do not add a disclosure arrow to navigation that reveals nothing. Do not shrink its 44px action target to the 18px visual. Do not use it as a page or dialog title; use `PageHeader` or `DialogHeader`. | Delegate its optional action; the app controls expanded state and revealed content. Toggle mode supplies `DisclosureArrow` unless `actionIcon` replaces it. Use `triggerAttributes` only for domain `data-*` or a native popover relationship. | Section organization, valid count and localized count label, disclosure state and content, non-count badge content, popover target behavior, and policy. | `@kerfjs/ui/menu-header` | [Pane geometry](../README.md#pane-and-content-geometry) |
59
- | Application layout composition | A sidebar, main area, inspector, or dialog needs shared toolbar/content/footer and child geometry. | Do not pad the pane shell, wrap child-owned geometry in competing insets, or invent unrelated centered measures. | Use `.kui-pane` and one `.kui-pane__content`; add `.kui-content` and `.kui-content-item` as needed. | Layout hierarchy, reading width, scroll ownership, and responsive relocation. | `@kerfjs/ui/layout.css` | [Pane anatomy](./layout.md#anatomy) |
68
+ | Navigation section heading — `MenuHeader` | A menu section needs a full-width label, semantic count, non-count badge, logical-end action, or real disclosure state. | Do not concatenate counts into `label` or put numeric content in `badge`; use `count` with the localized full phrase in `countLabel`. Do not add a disclosure arrow to navigation that reveals nothing. Do not shrink its 44px action target to the 18px visual. Do not use it as a page, panel, or dialog title; use `PanelHeader`. | Delegate its optional action; the app controls expanded state and revealed content. Toggle mode supplies `DisclosureArrow` unless `actionIcon` replaces it. Use `triggerAttributes` only for domain `data-*` or a native popover relationship. | Section organization, valid count and localized count label, disclosure state and content, non-count badge content, popover target behavior, and policy. | `@kerfjs/ui/menu-header` | [Pane geometry](../README.md#pane-and-content-geometry) |
69
+ | Application layout composition | A sidebar, main area, inspector, or dialog needs shared toolbar/content/footer and child geometry. | Do not pad the pane shell, wrap child-owned geometry in competing insets, invent unrelated centered measures, or leave an icon-only rail for a hidden pane. | Use `.kui-pane` and one `.kui-pane__content`; add `.kui-content` and `.kui-content-item` as needed. A visible pane owns collapse in its toolbar; move a hidden inline-start pane's restore control to the main toolbar leading edge and an inline-end pane's restore control to its trailing edge. | Layout hierarchy, reading width, scroll ownership, responsive relocation, and pane visibility state. | `@kerfjs/ui/layout.css` | [Pane anatomy](./layout.md#anatomy) |
60
70
  | Menu composition | Navigation sections need full-size rows and the same content-item geometry as every other pane. | Do not add sidebar-specific wrapper padding, shrink targets to icon size, nest an interactive trailing control in `MenuItem`, or use a chevron on a row that does not disclose content. Use ordinary links for a different navigation contract. | Compose `MenuHeader`, `MenuItem`, and `MenuActionRow` in `.kui-content`; use `MenuHeader` toggle mode with real controlled content, and use `.kui-content-item` for other surfaces plus a pane footer for toolbar actions. | Information architecture, disclosure content and state, responsive drawer/shell behavior, and token overrides. | `@kerfjs/ui/layout.css` | [Pane geometry](../README.md#pane-and-content-geometry) |
61
71
  | Resizable application pane — `ResizableRegion`, `clampRegionSize`, `resizeRegionFromPointer` | A controlled split pane needs the Kerf separator, collapse state, pointer plus keyboard resizing, or a product-specific decorative grip. | Do not use it for a static two-column layout; use CSS grid. Prefer it over Web Awesome `wa-split-panel` unless that component's distinct API is required. Keep `handleIcon` noninteractive. | Call `wireResizableRegions` from `@kerfjs/ui/wire-resizable-regions` once and retain its disposer. | Size signal, min/max policy, collapse policy, persistence, and optional decorative handle icon. | `@kerfjs/ui/resizable-region` | [ResizableRegion contract](./accessibility.md#resizableregion) |
62
72
  | One application tab — `AppTab` | A controlled app tab needs selection, close, drag, leading/trailing anatomy, safe domain metadata, or a product-specific close glyph. | Do not render it alone or use it for a small settings choice; compose in `TabBar`, or use `SegmentedControl`. Keep `closeIcon` noninteractive. | Compose in `TabBar`; let `wireTabBars` manage interaction. Put only domain `data-*` values in `rootAttributes`. | Tab identity, order, selection, close policy, content, and domain metadata values. | `@kerfjs/ui/app-tab` | [Tabs contract](./accessibility.md#tabs) |
63
73
  | Application tab strip — `TabBar`, `wireTabBars`, `reorderTabs` | Tabs switch page regions and may overflow, close, or reorder. | Do not use it for a compact local view toggle; use `SegmentedControl`. Do not use it for a long choice list; use `Select`. Prefer it over Web Awesome `wa-tab-group`, `wa-tab`, and `wa-tab-panel` for Kerf app tabs. | Call `wireTabBars` once, retain the disposer, and apply `onReorder` synchronously; `reorderTabs` is the default array helper. | Ordered tabs, selection, panels, routing, closing, and persistence. | `@kerfjs/ui/tab-bar` plus `@kerfjs/ui/wire-tab-bars` | [Tabs contract](./accessibility.md#tabs) |
64
- | Page title and action — `PageHeader` | A page needs its primary heading and an optional trailing action. | Do not use it as persistent app chrome; use `Toolbar`. Do not use it inside a modal task; use `DialogHeader`. | Delegate an optional action normally. | Page title, action, and responsive placement. | `@kerfjs/ui/page-header` | [Header ownership](./component-contract.md#extracted-versus-application-specific) |
65
- | Dialog title and summary — `DialogHeader` | A dialog needs a toolbar-aligned title, optional summary/id and icon, and grouped actions wired to the dialog's ARIA references. | Do not use it as the page's `h1`; use `PageHeader`. It supplies header structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. | Connect title and optional summary ids to the dialog host, pass action children directly, localize `actionsLabel` when the group needs a name, and delegate actions. | Open state, focus lifecycle, dismissal, actions, labels, and copy. | `@kerfjs/ui/dialog-header` | [Header ownership](./component-contract.md#extracted-versus-application-specific) |
74
+ | Panel, dialog, or page heading — `PanelHeader` | A panel, dialog, or page needs a heading with an extra-large title, an optional icon and subtitle, and trailing actions. | Do not use it as persistent app chrome; use `Toolbar`. It supplies header structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. | Connect the title id and any provided summary id to the dialog or panel host, pass the trailing controls (typically a `ToolbarControlGroup`), and delegate their actions. | Open state, focus lifecycle, dismissal, the trailing controls, labels, and copy. | `@kerfjs/ui/panel-header` | [Header ownership](./component-contract.md#extracted-versus-application-specific) |
66
75
  | Key/value facts — `ValueTable`, `ValueTableRow` | Read-only labels and values form a semantic definition list, optionally with a leading icon. | Do not use it for editable form fields or a row/column data grid; use native form or table semantics. | Compose typed `ValueTableRow` entries; pass `icon` when a 24px leading icon adds useful context. | Values, formatting, icon meaning, and empty/loading policy. | `@kerfjs/ui/value-table` | [Component ownership](./component-contract.md#ownership-boundaries) |
67
76
  | Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use Web Awesome `wa-progress-bar` or `wa-progress-ring`. Direct Web Awesome UI may use `wa-spinner`; do not mix spinner systems within one surface. | None; pass a label when the spinner conveys status. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
68
77
  | Value selection — `Select` | A controlled form value comes from a moderate or long choice list, possibly grouped or icon-bearing. | Do not use it for commands; use a real action menu. Do not use it for a small visible choice set; use `SegmentedControl`. Prefer it over direct `wa-select`, `wa-option`, or value-like `wa-dropdown`/`wa-dropdown-item` composition. | Import `@kerfjs/ui/select/register` once; listen for standard input/change events. | Controlled value, validation, choices, and domain mapping. | `@kerfjs/ui/select` | [Web Awesome integration](../README.md#web-awesome-theme) |
69
78
  | Small exclusive choice — `SegmentedControl` | A few visible choices switch a compact view or setting, with toolbar, rounded, or pill presentation. | Do not use it for tabpanel semantics; use `TabBar`. Do not use it for many choices; use `Select`. Prefer it over `wa-button-group` when the controls select one value. | Delegate its action, read `data-segment-value`, update `value`, and rerender. | Controlled value, labels, action, and persistence. | `@kerfjs/ui/segmented-control` | [SegmentedControl contract](./accessibility.md#segmentedcontrol) |
70
- | Structured search editor — `TokenSearchField`, `readTokenSearchField`, `placeTokenSearchCaret`, `wireTokenSearchFields` | Free text and ordered, editable, removable filter tokens share one searchbox; enable `collapsible` when an empty, unfocused field should reduce to one iconic action, standalone or in a toolbar group. | Do not use it for ordinary text entry; use a native input or Web Awesome `wa-input`. Do not use it when filters belong in separate form controls. | Read DOM-owned text on input, empty `textContent` on clear, and use `placeTokenSearchCaret` after explicit controlled focus changes. Call `wireTokenSearchFields` from `@kerfjs/ui/wire-token-search-fields` once so Enter submits without adding a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. In `collapsible` mode, control `expanded`, transfer focus after activation, and clear the transient state after focus leaves. | Parsing, suggestions, tokens, query execution, results, announcements, and the transient focus/open signal. | `@kerfjs/ui/token-search-field` | [TokenSearchField contract](./accessibility.md#tokensearchfield) |
79
+ | Structured search editor — `TokenSearchField`, `readTokenSearchField`, `placeTokenSearchCaret`, `wireTokenSearchFields` | Free text and ordered, editable, removable filter tokens share one searchbox; enable `collapsible` when an empty, unfocused field should reduce to one iconic action, standalone or in a toolbar group. | Do not use it for ordinary text entry; use a native input or Web Awesome `wa-input`. Do not use it when filters belong in separate form controls. | Read DOM-owned text on input, empty `textContent` on clear, and use `placeTokenSearchCaret` after explicit controlled focus changes. Call `wireTokenSearchFields` from `@kerfjs/ui/wire-token-search-fields` once so Enter submits without adding a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. In `collapsible` mode it also manages the transient expand/collapse/focus by default (activate to reveal + focus, Escape or empty blur to collapse); bind the field's `expanded` to the signal on the returned handle (`handle.expanded(id)`) or adopt your own via `collapsible.signals`, and opt out per behavior only when the app must own it. | Parsing, suggestions, tokens, query execution, results, announcements, and — only if overriding the default the collapsible `expanded` signal. | `@kerfjs/ui/token-search-field` | [TokenSearchField contract](./accessibility.md#tokensearchfield) |
71
80
  | Persistent inline status — `StateBanner` | A neutral, info, success, warning, or danger message belongs next to the affected work. | Do not use it for a no-content screen; use `EmptyState`. Do not use it for transient confirmation; use a toast. Web Awesome `wa-callout` is the ecosystem alternative for Web Awesome-owned content. | Delegate an optional action; choose alert urgency only for attention-requiring failure. | State mapping, message lifetime, retry/action behavior, and copy. | `@kerfjs/ui/state-banner` | [Feedback accessibility](./accessibility.md#shared-rules) |
72
81
  | Empty or busy content area — `EmptyState` | A content region has no items, cannot proceed, or is loading and needs explanation plus an optional action. | Do not use it for an inline status update; use `StateBanner`. Do not use it for transient success; use `wa-toast`/`wa-toast-item` or the application's toast system. | Delegate its optional action; it composes `LoadingSpinner` when busy. | Empty/busy policy, recovery action, illustration, and copy. | `@kerfjs/ui/empty-state` | [Feedback ownership](./component-contract.md#extracted-versus-application-specific) |
73
82
 
74
83
  ## Ambiguous choices
75
84
 
76
- - `Toolbar` is persistent app chrome; `PageHeader` identifies a page; `DialogHeader` labels a focused modal task.
85
+ - `Toolbar` is persistent app chrome; `PanelHeader` heads a panel, dialog, or page.
77
86
  - `TabBar` changes tabpanels and supports overflow/reorder; `SegmentedControl` chooses among a few compact views; `Select` handles a longer value list.
78
87
  - `StateBanner` persists beside affected work; `EmptyState` replaces absent content; `wa-callout` is contextual ecosystem content; `wa-toast` and `wa-toast-item` are transient and must not carry the only copy of important state.
79
88
  - `ResizableRegion` is an interactive controlled pane. CSS grid is the right answer when columns do not need a user-operable separator.
80
89
  - `TokenSearchField` is a structured editor. A native input or `wa-input` is the right answer for ordinary text.
81
90
 
91
+ ## Toolbar composition
92
+
93
+ A `Toolbar` has three zones — `leading`, `center`, and `trailing`. In almost
94
+ every case the only things that go **directly** in a zone are `ToolbarText`
95
+ (identity/title text) and `ToolbarControlGroup` (any control or cluster of
96
+ controls). Do not drop bare buttons, inputs, links, or arbitrary markup straight
97
+ into a zone; wrap controls in a `ToolbarControlGroup` so they get the shared
98
+ toolbar geometry, hover/pressed treatment, and grouping. `SegmentedControl`,
99
+ `Select`, a collapsible `TokenSearchField`, and Web Awesome controls all live
100
+ **inside** a `ToolbarControlGroup`, not loose in the zone. `PanelHeader` is the
101
+ one wrapper that composes these for you as a panel/dialog/page heading.
102
+
103
+ Common toolbar patterns:
104
+
105
+ | Want | Put in the zone | Notes |
106
+ | --- | --- | --- |
107
+ | Identity or title text | `<ToolbarText text="…" size="large" />` (or `xlarge` for a page/panel title) | Wrap in a `single` borderless group only when it must align with adjacent control pills |
108
+ | One or more icon/text buttons | `<ToolbarControlGroup>{buttons}</ToolbarControlGroup>` | Use `buttonAppearance="push"` for toggle buttons with `aria-pressed`; `single` for a lone control |
109
+ | An exclusive view switch | `<ToolbarControlGroup><SegmentedControl … /></ToolbarControlGroup>` | Not `TabBar`, which switches tabpanels |
110
+ | A value list | `<ToolbarControlGroup><Select … /></ToolbarControlGroup>` | Register `@kerfjs/ui/select/register` once |
111
+ | A collapsible search box | `<ToolbarControlGroup single><TokenSearchField collapsible … /></ToolbarControlGroup>` | The group animates the iconic ↔ expanded states; `wireTokenSearchFields` manages expand/collapse/focus by default |
112
+
113
+ A **popup menu in a toolbar** is a `single` `ToolbarControlGroup` wrapping a Web
114
+ Awesome `wa-dropdown`: its `slot="trigger"` `wa-button` is the toolbar button and
115
+ the `wa-dropdown-item`s are the menu. Keep the dropdown's managed light-DOM
116
+ children under `data-morph-skip-children` so kerf does not reconcile Web Awesome's
117
+ own DOM.
118
+
119
+ ```tsx
120
+ <ToolbarControlGroup single>
121
+ <wa-dropdown placement="bottom-start" data-morph-skip-children>
122
+ <wa-button slot="trigger" appearance="plain" with-caret aria-label="Sort">
123
+ <LucideIcon icon={ArrowDownAZ} name="arrow-down-a-z" />
124
+ </wa-button>
125
+ <wa-dropdown-item data-action="sort-recent">Recently updated</wa-dropdown-item>
126
+ <wa-dropdown-item data-action="sort-priority">Priority</wa-dropdown-item>
127
+ </wa-dropdown>
128
+ </ToolbarControlGroup>
129
+ ```
130
+
82
131
  ## Correct composition and duplicated-markup trap
83
132
 
84
133
  Correct: let the pane stay unpadded while its children own the shared 8/1/8
@@ -18,12 +18,27 @@ A reference image defines hierarchy, relationships, and intended feel. It does n
18
18
 
19
19
  ### Continuity is correctness
20
20
 
21
- Unrelated updates must not close controls, move focus, reset a draft, replace a selection, or jump scroll. Give every durable and transient state an explicit owner. Preserve DOM identity when meaning has not changed. Distinguish blocking foreground work from background synchronization.
21
+ Unrelated updates must not close controls, move focus, reset a draft, replace a selection, or jump scroll. Give every durable and transient state an explicit owner. The default owner is the application: domain data and persisted layout preferences — a navigation stack, a selection, a tab order, a pane size, a `collapsed`/`expanded` disclosure flag — are the app's signals, which it reads to render. A `wire…` helper owns only the *ephemeral mechanics* around that state (a push/pop animation, overflow autoscroll, a live resize preview) and reports committed changes through callbacks. A helper takes over a piece of transient state itself only when hand-rolling that behavior is substantial and error-prone enough to cause real, inconsistent variation across apps — `wireTokenSearchFields`'s collapsible reveal/focus/Escape/blur-collapse is the bar; a one-line boolean toggle is not. Preserve DOM identity when meaning has not changed. Distinguish blocking foreground work from background synchronization.
22
22
 
23
23
  ### Hierarchy precedes decoration
24
24
 
25
25
  Establish one reading order and one dominant action per decision point. Group first with alignment, spacing, typography, and shared surfaces. A border, fill, badge, or nested card must communicate a real distinction.
26
26
 
27
+ ### Reach for the primitive, not for CSS
28
+
29
+ The package is designed to look right *unstyled*. A screen built from the
30
+ primitives, their props, and the semantic tokens should already read well, so
31
+ custom CSS is the exception. Before adding any `padding`, `margin`, `width`,
32
+ `height`, `border`, `background`, wrapper card, or decoration, check whether the
33
+ component, the pane, or the content-item already owns it — it almost always does,
34
+ and adding more usually **double-insets** or fights the component (the most common
35
+ mistake). Trust component defaults: render a control at its natural size and color
36
+ and fix the surrounding layout rather than overriding the control. Legitimate
37
+ custom CSS is limited to genuinely new structure, and even then may only join a
38
+ component's documented `publicClasses` or override documented `--kui-*` tokens at a
39
+ real composition boundary — never size, space, or re-skin a primitive by hand, and
40
+ never build a heading, toolbar, card, or pane geometry that a primitive provides.
41
+
27
42
  ### Panes share one child-owned geometry
28
43
 
29
44
  Sidebars, main areas, inspectors, and dialogs use the same unpadded pane. Their
@@ -80,6 +95,13 @@ foregrounds compatible with Hot Sheet 2 and Web Awesome. Consumers may replace
80
95
  the global palette, one semantic role, or one component instance through
81
96
  documented custom properties without rewriting component CSS.
82
97
 
98
+ Spacing is likewise a finite, semantic scale, not a free measurement. Five
99
+ canonical steps — 0, 4, 8, 16, and 24px, each a `--kui-space-*` token — map to a
100
+ single relationship apiece, chosen by how connected two elements are: no
101
+ separation, very minor air, standard within a group, between homogeneous groups,
102
+ and major between heterogeneous groups. Off-scale values (12px, 32px) are
103
+ deliberate exceptions. See [`layout.md`](layout.md) "Spacing scale".
104
+
83
105
  ### Responsive design reprioritizes
84
106
 
85
107
  Protect primary content, readable type, recognizable icons, and usable targets. Relocate secondary information before compressing it below a usable scale. Keep one clear scroll owner per region and test narrow, wide, zoomed, and intermediate layouts.
@@ -0,0 +1,54 @@
1
+ # Device class
2
+
3
+ `@kerfjs/ui/device-class` reports the current viewport as a reactive **device
4
+ class** — a size bucket × orientation × viewport-segment count — so a layout can
5
+ pick its presentation from one signal instead of hand-wiring `matchMedia`. It is
6
+ the foundation the app/dialog layouts build on (see
7
+ [`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
8
+
9
+ ```ts
10
+ import { deviceClass } from '@kerfjs/ui/device-class';
11
+
12
+ const device = deviceClass(); // ReadonlySignal<DeviceClass>
13
+
14
+ effect(() => {
15
+ if (device.value.compact) showStack();
16
+ else showSplit();
17
+ });
18
+ ```
19
+
20
+ ## `DeviceClass`
21
+
22
+ | Field | Meaning |
23
+ | --- | --- |
24
+ | `size` | `xs-mobile` \| `mobile` \| `tablet` \| `desktop` \| `xl-desktop` |
25
+ | `orientation` | `portrait` \| `landscape` |
26
+ | `segments` / `verticalSegments` | viewport segment counts (foldables / dual-screen); `1` on ordinary devices |
27
+ | `handset` | `xs-mobile` or `mobile` |
28
+ | `compact` | "one pane at a time" — a handset or a portrait tablet |
29
+ | `atLeast(size)` | true when the current size is `size` or larger |
30
+
31
+ Reading `device.value` inside an `effect`/`computed` re-runs when the viewport
32
+ crosses a breakpoint, rotates, or changes its segment count. One shared viewport
33
+ source backs every reader.
34
+
35
+ ## Breakpoints
36
+
37
+ Default minimum widths (px): `mobile` 360, `tablet` 720, `desktop` 1024,
38
+ `xl-desktop` 1440. Override per reader:
39
+
40
+ ```ts
41
+ const device = deviceClass({ breakpoints: { tablet: 900 } });
42
+ ```
43
+
44
+ The same numbers are mirrored as CSS custom properties in the foundation
45
+ (`--kui-bp-mobile`, `--kui-bp-tablet`, `--kui-bp-desktop`, `--kui-bp-xl-desktop`)
46
+ so CSS media queries and the JS signal read one source. `classifyViewport(width,
47
+ orientation, segments?, verticalSegments?, breakpoints?)` is exported as the
48
+ pure, DOM-free bucketing core.
49
+
50
+ ## Server rendering
51
+
52
+ Without a DOM, `deviceClass()` resolves to an SSR default (1024×768, landscape,
53
+ one segment) and hydrates to the real class on the client. Override the assumed
54
+ viewport with `deviceClass({ ssr: { width, height } })`.
package/docs/layout.md CHANGED
@@ -38,9 +38,11 @@ changing its geometry by setting `--kui-content-item-border` and
38
38
  `--kui-content-item-background`.
39
39
 
40
40
  A visible parent surface does not make every child another visible card. The
41
- composer recipe keeps its form as the single surface and uses three transparent
42
- content items for the introduction, fields, and footer. A conditional
43
- `StateBanner` remains visibly distinct because it communicates semantic status.
41
+ composer recipe keeps its form as the single surface, uses `PanelHeader` for
42
+ its task hierarchy, and places field and action control edges on the shared 8px
43
+ inline gutter instead of nesting them inside another padded content item. A
44
+ conditional `StateBanner` remains visibly distinct because it communicates
45
+ semantic status.
44
46
 
45
47
  ## Public roles and tokens
46
48
 
@@ -56,7 +58,7 @@ content items for the introduction, fields, and footer. A conditional
56
58
  | Explicit scroll owner outside a pane | `.kui-scroll-owner` | `overflow: auto` |
57
59
 
58
60
  The component layer applies the same contract to `Toolbar`, `MenuHeader`,
59
- `MenuItem`, `PageHeader`, `DialogHeader`, `StateBanner`, `ValueTable`,
61
+ `MenuItem`, `PanelHeader`, `StateBanner`, `ValueTable`,
60
62
  `ValueTableRow`, tabs, and form controls. A value-table row separator starts at
61
63
  the row's 8px content inset, or at 40px when the row contains its 24px leading
62
64
  icon and 8px gap, and always ends 8px from the right edge. Each value-table row
@@ -64,10 +66,37 @@ also keeps 8px of root-scaled block padding independently of its semantic inline
64
66
  inset. Most interactive rows and toolbar groups are 44px tall.
65
67
  Toolbar groups reserve a real 1px outer border around a 42px inner area, even
66
68
  when their border and background are transparent.
67
- `DialogHeader` applies the same rule internally: its icon/title identity is a
68
- borderless group in a top toolbar, its direct action children are wrapped in a
69
- contained group, and its optional subtitle is a separate row aligned below the
70
- title. The icon visual is 24px inside a 34px circular background.
69
+ `PanelHeader` is a plain top `Toolbar` used as a panel, dialog, or page heading
70
+ and overrides no Toolbar styles. The leading zone holds the optional icon (a
71
+ normal bordered `ToolbarControlGroup` given a brand fill with a matching border,
72
+ a 22px glyph) and the title as extra-large `ToolbarText`; the app's trailing
73
+ controls go straight into the trailing zone, and the icon group is omitted when
74
+ no icon is passed. The optional subtitle is a separate row below the toolbar, its
75
+ text left-aligned with the title text.
76
+
77
+ ## Spacing scale
78
+
79
+ Spacing is not a free choice. The official scale is five canonical steps, each
80
+ expressing exactly one relationship — pick the step by **how connected two
81
+ elements are**, not by eye. Every scalable value is `remify`-authored against the
82
+ fixed 16px baseline, so it delivers as `rem`.
83
+
84
+ | Value | Token | Relationship — when to use |
85
+ | --- | --- | --- |
86
+ | 0px | `--kui-space-none` | No separation. The elements read as a single unit (a control and its own affordance, adjacent `MenuItem` rows). |
87
+ | 4px | `--kui-space-2xs` | Very minor. Still one connected cluster, but readability or aesthetics demand a hair of air (inline metadata, an icon beside its label). |
88
+ | 8px | `--kui-space-xs` | Standard. Between elements **within a group** — the content-item padding/gutter, gaps between toolbar controls in a group. |
89
+ | 16px | `--kui-space-m` | Minor. Between **homogeneous groups** — two lists, two sibling sections of the same kind. |
90
+ | 24px | `--kui-space-l` | Major. Between **heterogeneous groups** — the `.kui-content` rhythm between major, differing regions. |
91
+
92
+ The two relationships that get confused most are 8px vs 24px: 8px is *inside* a
93
+ group, 24px is *between* major, differing regions. 16px sits between them for
94
+ same-kind groups.
95
+
96
+ `--kui-space-s` (12px) and `--kui-space-xl` (32px) exist but are **off the
97
+ canonical rhythm** — reach for them only as a deliberate exception, never as a
98
+ default step. Prefer the five canonical tokens so spacing stays legible and
99
+ consistent across every surface.
71
100
 
72
101
  ## Ownership rules
73
102
 
@@ -87,8 +116,15 @@ title. The icon visual is 24px inside a 34px circular background.
87
116
  replaces it.
88
117
  `MenuActionRow` uses a noninteractive row root around sibling 44px primary
89
118
  and trailing buttons. `MenuItem.trailing` remains dormant content.
119
+ In multiline `MenuItem` and `MenuActionRow` rows, the leading icon stays
120
+ centered on the label's first line rather than the full wrapped label.
90
121
  6. Reading width, column placement, and responsive relocation remain application
91
122
  decisions. The shared classes define local geometry, not the whole shell.
123
+ 7. A visible collapsible pane owns its collapse control in that pane's toolbar.
124
+ When the pane is hidden, move the restore control into the adjacent main
125
+ toolbar on the same logical edge: an inline-start pane restores from the
126
+ main toolbar's leading group, and an inline-end pane restores from its
127
+ trailing group. Do not leave an otherwise empty icon-only rail behind.
92
128
 
93
129
  At narrow widths or 200% zoom, relocate or stack panes before shrinking targets.
94
130
  The 8/1/8 item contract and 44px controls remain stable, so screenshots and
@@ -0,0 +1,47 @@
1
+ # Navigation stack
2
+
3
+ `@kerfjs/ui/nav-stack` is an iOS-style push/pop navigation stack: views slide in
4
+ and out over one another while the top chrome settles. A **single-pane layout is
5
+ a `NavStack` with one entry**. It is one of the opt-in app layouts (see
6
+ [`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
7
+
8
+ Import the component and its companion stylesheet:
9
+
10
+ ```ts
11
+ import { NavStack, type NavStackView } from '@kerfjs/ui/nav-stack';
12
+ import { wireNavStack } from '@kerfjs/ui/wire-nav-stack';
13
+ import '@kerfjs/ui/nav-stack.css';
14
+ ```
15
+
16
+ ## State lives in the app
17
+
18
+ Like every `@kerfjs/ui` component, `NavStack` is declarative: the app owns the
19
+ stack as a `signal<NavStackView[]>`, `NavStack({ views })` renders it, and
20
+ `wireNavStack` animates the transitions.
21
+
22
+ ```tsx
23
+ const views = signal<NavStackView[]>([{ key: 'inbox', title: 'Inbox', content: <InboxView /> }]);
24
+
25
+ // render inside mount():
26
+ <NavStack id="mail" label="Mail" views={views.value} />;
27
+
28
+ // once, after first render:
29
+ const dispose = wireNavStack(root, { onBack: () => { views.value = views.value.slice(0, -1); } });
30
+
31
+ // push / pop by editing the signal:
32
+ views.value = [...views.value, { key: id, title: 'Message', content: <MessageView id={id} /> }];
33
+ ```
34
+
35
+ `NavStack` renders every entry stacked, the last one active and the rest kept
36
+ mounted (so their DOM state and focus survive) but hidden. Each entry carries a
37
+ `key` (stable identity), `content`, an optional `title`, and optional per-view
38
+ `toolbar` actions. The back control appears automatically once the stack has more
39
+ than one entry; `wireNavStack`'s `onBack` is where the app pops its own signal.
40
+
41
+ ## Transitions
42
+
43
+ `wireNavStack(root, { onBack, duration? })` observes the rendered stack and
44
+ animates each change: a pushed view slides in from the trailing edge; a popped
45
+ view slides back off it over the revealed view. It returns a disposer. The
46
+ animation honors `prefers-reduced-motion` (transitions collapse to instant) and
47
+ `duration: 0` disables it. Applicable at every device size and inside dialogs.
package/docs/recipes.md CHANGED
@@ -33,6 +33,10 @@ specific dispatch; either way, wire once at a stable root and retain disposal.
33
33
  Use `Toolbar`, controlled `ResizableRegion` panes, and one
34
34
  `.kui-pane__content` scroll owner per pane. The recipe owns the shell topology;
35
35
  the app owns routing, responsive pane visibility, sizes, persistence, and data.
36
+ Keep each visible pane's collapse action in its own toolbar. Once hidden, put
37
+ its restore action in the adjacent main toolbar on the same logical edge:
38
+ leading for an inline-start sidebar and trailing for an inline-end inspector.
39
+ Collapse the pane completely rather than preserving an empty icon rail.
36
40
  Adapt only public `--kui-layout-*` and component variables.
37
41
 
38
42
  ## Navigation sidebar
@@ -52,7 +56,7 @@ no custom `actionIcon` is needed; ordinary navigation rows stay chevron-free.
52
56
 
53
57
  [Open the recipe](../ux-demo/?component=recipe-workspace-header) · [TSX source](../ux-demo/recipes/workspace-header.tsx)
54
58
 
55
- `PageHeader` owns the page heading while one control cluster holds secondary,
59
+ `PanelHeader` owns the page heading while one control cluster holds secondary,
56
60
  overflow, and primary actions. The app owns authorization and command policy;
57
61
  controls relocate without changing focus order.
58
62
 
@@ -62,21 +66,25 @@ controls relocate without changing focus order.
62
66
 
63
67
  The production Web Awesome dialog owns modal focus and Escape; the thin recipe
64
68
  adapter restores the invoking control consistently after the hide event.
65
- `DialogHeader`, `MenuHeader`, `MenuItem`, and `ValueTable` own their included
69
+ `PanelHeader`, `MenuHeader`, `MenuItem`, and `ValueTable` own their included
66
70
  anatomy. The application owns open state, selection, dismissal policy, and
67
- record actions. Pass the dialog action children directly; `DialogHeader` places
68
- them in its top toolbar's action group and uses the localized `actionsLabel`.
71
+ record actions. `PanelHeader` places the trailing controls the app passes
72
+ directly into its top toolbar's trailing zone.
73
+ The header sits on the dialog edge while retaining its internal control inset;
74
+ the selected title receives the full content gutter, the metadata table fills
75
+ the available detail width between the usual outer margins, and the action
76
+ cluster uses one outer gutter without a second content-item inset.
69
77
  Do not rebuild the dialog or reach into private shadow parts.
70
78
 
71
79
  ## Composer form
72
80
 
73
81
  [Open the recipe](../ux-demo/?component=recipe-composer-form) · [TSX source](../ux-demo/recipes/composer-form.tsx)
74
82
 
75
- Production fields own labels, help, and native focus; semantic layout classes
76
- compose one visible form surface from exactly three transparent content items:
77
- introduction, fields, and a footer containing actions plus ownership guidance.
78
- The three sections retain 8px margin, 1px transparent border, and 8px padding,
79
- with 24px between major children and 8px inside each section. The app owns
83
+ `PanelHeader` supplies the task title and summary, with their ids referenced
84
+ by the form. Production fields own labels, help, and native focus. The field
85
+ and footer control edges sit directly on the shared 8px inline gutter rather
86
+ than acquiring a second content-item padding inset; major children remain 24px
87
+ apart and related controls use 8px gaps. The app owns
80
88
  validation, drafts, permissions, and transport. Persistent error or success
81
89
  feedback is the only nested semantic surface and uses `StateBanner`, not a toast.
82
90
  Because upgraded Web Awesome fields retain live value properties, controlled
@@ -101,6 +109,18 @@ visible exclusive choices, `Select` for a longer value list, and an ordinary
101
109
  button for an independent command. The app owns values, actions, persistence,
102
110
  and responsive priority.
103
111
 
112
+ ## Navigation stack
113
+
114
+ [Open the recipe](../ux-demo/?component=recipe-navigation-stack) · [TSX source](../ux-demo/recipes/navigation-stack.tsx)
115
+
116
+ Drill from a library list into a detail and back with `NavStack`
117
+ (`@kerfjs/ui/nav-stack`): the app owns the stack as a signal of views and
118
+ pushes/pops it, `NavStack` renders it, and `wireNavStack` slides the content and
119
+ settles the chrome (reduced motion collapses the slide to instant). A live
120
+ `deviceClass()` badge shows the current size/orientation. See the layout guide
121
+ [`app-layouts.md`](app-layouts.md) for choosing among `NavStack`, `SplitView`,
122
+ `Workbench`, and `TabScaffold`.
123
+
104
124
  ## Rules shared by every recipe
105
125
 
106
126
  - Import `@kerfjs/ui/layout.css`; keep every pane unpadded and use exactly one
@@ -0,0 +1,49 @@
1
+ # Split view (list-detail)
2
+
3
+ `@kerfjs/ui/split-view` is a list-detail (master-detail) layout: a list and a
4
+ detail side by side on roomy screens, collapsing to a `NavStack` (list → detail)
5
+ on compact ones. One of the opt-in app layouts (see
6
+ [`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
7
+
8
+ ```ts
9
+ import { SplitView } from '@kerfjs/ui/split-view';
10
+ import { deviceClass } from '@kerfjs/ui/device-class';
11
+ import '@kerfjs/ui/split-view.css';
12
+ // plus nav-stack.css when the compact path is reachable, and
13
+ // wireResizableRegions / wireNavStack for the interactive behavior.
14
+ ```
15
+
16
+ ## Responsive by device class
17
+
18
+ `SplitView` is declarative; the app derives `compact` from the device class and
19
+ tracks its own selection:
20
+
21
+ ```tsx
22
+ const device = deviceClass();
23
+ const selected = signal<string | null>(null);
24
+
25
+ <SplitView
26
+ id="mail"
27
+ label="Mail"
28
+ compact={device.value.compact}
29
+ detailActive={selected.value !== null}
30
+ list={<ThreadList />}
31
+ detail={<Message id={selected.value} />}
32
+ listTitle="Threads"
33
+ detailTitle="Message"
34
+ resizable={{ size: 320, min: 220, max: 480 }}
35
+ />;
36
+ ```
37
+
38
+ - **Roomy** (`compact: false`): both panes show. With `resizable`, the list sits
39
+ in a `ResizableRegion` (wire it with `wireResizableRegions`); without it the
40
+ list takes a fixed `--kui-split-view-list-width` (default 320px) and the detail
41
+ fills the rest.
42
+ - **Compact** (`compact: true`): the split collapses to a `NavStack`. The list is
43
+ the root; when `detailActive` is true the detail is pushed over it with an
44
+ automatic back control (wire it with `wireNavStack`, whose `onBack` clears the
45
+ app's selection). This is the portrait-tablet / handset presentation; as a
46
+ dialog the compact form is a full-screen or large partial-cover modal.
47
+
48
+ Compose the interactive wiring from the existing helpers — `SplitView` adds no
49
+ wire of its own.
@@ -0,0 +1,41 @@
1
+ # Tab scaffold
2
+
3
+ `@kerfjs/ui/tab-scaffold` is a mobile-first, iOS-like bottom tab bar that switches
4
+ between major app sections, where **each tab keeps its own content mounted** —
5
+ usually a `NavStack`, so each tab's stack and scroll survive a switch. One of the
6
+ opt-in app layouts (see [`../../docs/23-app-layouts.md`](../../docs/23-app-layouts.md)).
7
+ It is distinct from `TabBar` (document-oriented, reorderable strips).
8
+
9
+ ```ts
10
+ import { TabScaffold } from '@kerfjs/ui/tab-scaffold';
11
+ import { wireTabScaffold } from '@kerfjs/ui/wire-tab-scaffold';
12
+ import '@kerfjs/ui/tab-scaffold.css';
13
+ ```
14
+
15
+ ## Controlled selection
16
+
17
+ The app owns the active tab (a signal); `TabScaffold` renders every tab's scene
18
+ (only the active one visible) plus the bottom bar, and `wireTabScaffold` reports
19
+ clicks.
20
+
21
+ ```tsx
22
+ const active = signal('home');
23
+
24
+ <TabScaffold
25
+ id="app"
26
+ label="Sections"
27
+ active={active.value}
28
+ tabs={[
29
+ { id: 'home', label: 'Home', icon: <HomeIcon />, content: <HomeStack /> },
30
+ { id: 'search', label: 'Search', icon: <SearchIcon />, content: <SearchStack /> },
31
+ ]}
32
+ />;
33
+
34
+ // once, after first render:
35
+ const dispose = wireTabScaffold(root, { onSelect: (id) => { active.value = id; } });
36
+ ```
37
+
38
+ Each `TabScaffoldTab` has an `id`, `label`, optional `icon`, and `content`. The
39
+ bottom bar respects the home-indicator safe area (`env(safe-area-inset-bottom)`)
40
+ and keeps 44px targets. On larger device classes, promote the tab set to a
41
+ `Workbench` rail or a persistent sidebar instead of a bottom bar.
package/docs/ux-demo.md CHANGED
@@ -1,28 +1,30 @@
1
1
  # UX catalog contract
2
2
 
3
- Run `npm run dev` from `ui/`. The catalog is a development and review surface, not a second implementation.
3
+ Run `npm run dev` from `ui/`. The catalog is a development and review surface, not a second implementation. The development server allows repo-owned assets used by the catalog, and its production bundle uses relative asset URLs so the complete catalog remains intact below preview and proxy paths.
4
4
 
5
5
  Seven lazy production-composition routes live under the `Recipes` category.
6
6
  Each `?component=recipe-*` route uses public package primitives, real wiring,
7
7
  deterministic state, and semantic layout owners from the [recipe guide](./recipes.md).
8
8
 
9
9
  - It imports public component subpaths, exercising their browser-selected reachable CSS rather than a catalog-only style path.
10
- - The `MenuActionRow` route demonstrates sibling primary/trailing native controls, independent disabled states, controlled selection, and an application-owned native popover relationship without nesting controls.
10
+ - The `MenuActionRow` route demonstrates sibling primary/trailing native controls, independent disabled states, controlled selection, first-line leading-icon alignment for wrapped labels, and an application-owned native popover relationship without nesting controls. The `MenuItem` route demonstrates the same multiline leading-icon anchor alongside its single-control navigation contract.
11
11
  - The `AppTab` route demonstrates a runtime-filtered domain metadata slot and a decorative replacement close icon without changing TabBar selection, close, reorder, or focus ownership. The `ResizableRegion` route likewise replaces only the decorative handle glyph while retaining separator semantics and shared wiring.
12
12
  - Every first-party visual component and every free Web Awesome 3.12 component has its own stable `?component=<id>` URL and focused demo. Composite header, menu, feedback, and theme scenarios remain addressable alongside their subcomponents; the root opens the first Kerf component.
13
13
  - `ai/component-catalog.json` is the canonical catalog metadata. A deterministic checked-in projection supplies the demo's unique id, category, kind (`component`, `composition`, or `recipe`), source (`kerf` or `webawesome`), name, description, direct `uses` dependencies, existing documentation path, and demo-source path; first-party, recipe, and Web Awesome renderer maps are exhaustively typed against its generated id unions. Demo paths resolve to `ui/ux-demo/main.tsx`, the matching `ui/ux-demo/recipes/*.tsx` file, or `ui/ux-demo/webawesome-demos.tsx` according to the canonical entry. First-party component entries additionally derive `ui/src/<browser-subpath>.tsx` from their canonical `delivery.browserImport`.
14
- - Every detail header visibly pairs `View demo source` with `Read guidance`; first-party component details also expose `View component source`. Web Awesome details say `Read Kerf integration guidance` to distinguish the local integration contract from the upstream component implementation. All links show their repository-relative path and use deploy-safe absolute GitHub blob URLs with contextual new-tab names; the demo does not depend on a local source server or add a package runtime export.
14
+ - Every detail footer groups compact `Demo source` and `Guidance` actions; first-party component details also expose `Component source`. Web Awesome details say `Integration guidance` to distinguish the local integration contract from the upstream component implementation. Each button retains its repository-relative path in the DOM for verification and uses a deploy-safe absolute GitHub blob URL with a contextual new-tab name; the demo does not depend on a local source server or add a package runtime export. At phone widths, the resource group and related-component selector stack as full-width rows, with any resource overflow contained locally so labels cannot overlap the selector or widen the document.
15
15
  - The master/detail shell groups first-party routes by category with production `MenuHeader` and `MenuItem` components. The 70 ecosystem components live in a separate `Web Awesome` disclosure with seven subgroups, so they remain discoverable without being presented as first-class `@kerfjs/ui` exports. Direct Web Awesome routes open the disclosure and reveal the selected row at wide sizes; narrow layouts keep the expanded list in document flow rather than introducing a competing nested scroller.
16
- - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. At narrow widths, the focused `ResizableRegion` specimen moves its committed-width status below the controlled pane while retaining local horizontal access when the pane is resized wider than the stage; its split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
17
- - The composer-form recipe demonstrates one coherent visible form surface with exactly three transparent 8/1/8 content sections and 24px major rhythm. Its conditional error or success `StateBanner` is the only nested semantic surface. Reset synchronizes the upgraded Web Awesome fields' live value properties with their empty controlled attributes and announces `Draft reset`; browser coverage preserves behavior and verifies wide, narrow, dark, validation, success, reset, 200% zoom, and forced-colors states.
16
+ - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. Its sidebar identity uses the Kerf logo beside a vertically centered title, with the subtitle in its own aligned row like `PanelHeader`; the current-view heading is also a toolbar. Collapsing the catalog removes the pane completely and moves its restore action to the main toolbar's leading edge instead of leaving an icon-only rail. The selected view description remains inset below the page toolbar. The checkerboard belongs to the complete scrollable preview region rather than a nested card. A lightly separated footer keeps live status above compact source, guidance, and relationship groups. The focused `ResizableRegion` specimen stretches its surface through the available stage height and reports committed width in that shared status footer; narrow layouts retain local horizontal access when the pane is resized wider than the stage, and the split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
17
+ - The composer-form recipe demonstrates one coherent visible form surface with `PanelHeader` title/summary hierarchy, fields and actions on the shared 8px control gutter, and 24px major rhythm. Its conditional error or success `StateBanner` is the only nested semantic surface. Reset synchronizes the upgraded Web Awesome fields' live value properties with their empty controlled attributes and announces `Draft reset`; browser coverage preserves behavior and verifies wide, narrow, dark, validation, success, reset, 200% zoom, and forced-colors states.
18
+ - The master-detail dialog recipe places its header directly on the dialog edge while preserving the header's internal inset, gives the selected project title the complete outer-plus-inner content gutter, lets the value table fill the available detail column between the shared outer margins, and keeps record actions on one outer gutter without an extra content-item inset. Browser geometry coverage protects those relationships at wide and narrow widths.
18
19
  - Catalog-only brand marks, preview dimensions, checkerboard tiles, and the compound display-settings control retain local geometry because they are specimen chrome or intrinsic control anatomy rather than reusable application spacing roles.
19
20
  - The menu composition route demonstrates toolbar/content/footer anatomy, 24px major gaps, 8/1/8 child geometry, the full-width `MenuHeader` count-or-badge/action split, logical-end 44px action targets with 18px visuals, and 44px row/header/toolbar targets. Its one chevron is a production 18px `DisclosureArrow` inside a controlled Tools header that actually reveals and hides content; ordinary Projects navigation has no false disclosure affordance. Browser assertions cover stable naming, expanded/content transitions, visual rotation, node identity, reduced motion, RTL alignment and containment, and wide, narrow, dark, and 200% layouts. The focused MenuHeader route separately shows the shared neutral count pill for Attachments, Notes (including zero), and Duplicates plus a non-count legacy badge, full-width logical-end actions, and no disclosure without content.
20
21
  - The `DisclosureArrow` route renders the production 18px root-scaled default without a catalog-only size override. Its two native buttons independently control the default right-to-down chevron and a recognizable right-facing replacement arrow with left-to-up custom directions; both use shortest-path rotation and their names stay stable while `aria-expanded` communicates state. Browser coverage verifies the custom clockwise path, pointer and native keyboard activation, focus, narrow and 200%-zoom stacking with contained labels, consumer sizing, and the independent `.5` Web Awesome glyph scale retained by Kerf `Select`.
21
- - A detail with relationships renders one grouped selector: `Uses` links to direct dependencies and `Used by` links to reverse dependents. Choosing either navigates to that component's demo. The footer is absent when both groups are empty; no duplicate relationship summary is rendered.
22
+ - A detail with relationships renders one grouped footer selector: `Uses` links to direct dependencies and `Used by` links to reverse dependents. Choosing either navigates to that component's demo. The relationship group is absent when both sets are empty; no duplicate summary is rendered.
22
23
  - Demo-only CSS supplies only the catalog shell and stages; component appearance stays in package CSS. Decorative chrome and Web Awesome controls share the same semantic system palette instead of carrying a separate demo identity.
23
- - Scenarios use deterministic data and cover real variants, long content, disabled state, selection, progress, feedback, overlays, media, helpers, and stateful interactions. The Web Awesome theme route remains the broad system view, while individual action, form, layout, navigation, feedback, media, and helper routes provide focused specimens; MenuItem and MenuHeader demonstrate typed domain metadata, protected native button/disclosure semantics, and a native popover trigger relationship without product props; DialogHeader demonstrates its top-toolbar identity/action groups, optional subtitle row, and 24px icon inside a 34px circle; the Select route verifies that option icons survive unrelated Kerf rerenders and selected slot content follows value changes; the Animation route exposes preset, easing, duration, playback-rate, and transport controls with live lifecycle status; observer routes provide user-driven intersection, mutation, and resize events with live results; ToolbarControlGroup exposes all eight Hot Sheet 2 variants, loads the Web Awesome registrations needed by its popup specimen, and preserves the upgraded dropdown's managed light-DOM children across Kerf rerenders; the Toolbar route composes a collapsible TokenSearchField inside a group, animates between the empty iconic and complete field states, stays open while populated, and focuses the revealed editor; SegmentedControl covers toolbar, rounded-rectangle, pill, equal-width, and disabled presentations; TokenSearchField covers editable ordered chips, pointer and keyboard removal, caret-preserving controlled deletion, editing, clearing, free text, disabled state, and fixed first-line alignment while content wraps; StateBanner exposes every built-in tone plus a scoped override; TabBar demonstrates overflow, edge autoscroll, and controlled pointer/keyboard reordering.
24
- - Settings exercise light/dark, increased contrast, and reduced motion without changing component code, and expose each preference through `aria-pressed`.
25
- - The action log is an `aria-live` region, making interactions visible to both people and browser tests.
24
+ - Scenarios use deterministic data and cover real variants, long content, disabled state, selection, progress, feedback, overlays, media, helpers, and stateful interactions. The Web Awesome theme route remains the broad system view, while individual action, form, layout, navigation, feedback, media, and helper routes provide focused specimens; MenuItem and MenuHeader demonstrate typed domain metadata, protected native button/disclosure semantics, and a native popover trigger relationship without product props; PanelHeader demonstrates its plain top-toolbar identity, extra-large title, optional subtitle row, and bordered brand-filled icon group; the Select route verifies that option icons survive unrelated Kerf rerenders and selected slot content follows value changes; the Animation route exposes preset, easing, duration, playback-rate, and transport controls with live lifecycle status; observer routes provide user-driven intersection, mutation, and resize events with live results; ToolbarControlGroup exposes all eight Hot Sheet 2 variants, loads the Web Awesome registrations needed by its popup specimen, and preserves the upgraded dropdown's managed light-DOM children across Kerf rerenders; the Toolbar route composes a collapsible TokenSearchField inside a group, animates between the empty iconic and complete field states, stays open while populated, and focuses the revealed editor; SegmentedControl covers toolbar, rounded-rectangle, pill, equal-width, and disabled presentations; TokenSearchField covers editable ordered chips, pointer and keyboard removal, caret-preserving controlled deletion, editing, clearing, free text, disabled state, and fixed first-line alignment while content wraps; StateBanner exposes every built-in tone plus a scoped override; TabBar demonstrates overflow, edge autoscroll, and controlled pointer/keyboard reordering.
25
+ - Recipe ownership notes are hidden by default. A page-toolbar toggle reveals them as floating warning-toned notes with a folded corner, keeping instructional copy visually distinct and removable from the specimen flow.
26
+ - Settings exercise light/dark, increased contrast, and reduced motion without changing component code. The theme action starts from the operating system's effective color scheme, always names the appearance it will switch to, and installs an explicit light or dark override when activated; contrast and reduced motion expose their independent preferences through `aria-pressed`.
27
+ - The footer action log is an `aria-live` region, making interactions visible to both people and browser tests.
26
28
  - The focused Toast route calls Web Awesome's documented programmatic stack API and notes that Hot Sheet 2 still uses its own app-level toast implementation; the catalog demonstrates ecosystem support without implying current product adoption.
27
29
  - Browser coverage walks every Kerf, composition, and Web Awesome route, checks every listed Web Awesome custom element is registered in its focused specimen, exercises the ecosystem disclosure and both relationship directions, validates offline source/guidance hrefs and accessible wide/narrow/200%-zoom wrapping, and covers tab selection/reordering/overflow, semantic palettes and scoped overrides, light/dark rendering, toolbar popup and responsive find behavior, single/multiline search alignment, menu alignment, banner urgency, keyboard resizing, live custom-element values, and representative wide/narrow captures.
28
30
  - The production build keeps the catalog shell and Kerf routes in the entry chunk, then dynamically imports the complete Web Awesome specimen and registration surface when an ecosystem route, the broad Web Awesome theme route, or the ToolbarControlGroup popup specimen is selected. Directly addressed routes pass through the same loader.