@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
@@ -0,0 +1,30 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ interface TabScaffoldTab {
4
+ id: string;
5
+ label: string;
6
+ /** Decorative icon shown above the label in the bottom bar. */
7
+ icon?: SafeHtml;
8
+ /** The tab's content — typically a `NavStack` so each tab keeps its own stack. */
9
+ content: SafeHtml;
10
+ }
11
+ interface TabScaffoldProps {
12
+ id: string;
13
+ /** Accessible name for the tab bar. */
14
+ label: string;
15
+ tabs: TabScaffoldTab[];
16
+ /** The controlled active tab id (the app owns selection). */
17
+ active: string;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * A mobile-first, iOS-like bottom tab scaffold: a bottom tab bar that switches
22
+ * between major sections, each tab keeping its own content (usually a `NavStack`)
23
+ * mounted so its stack and scroll survive a switch. Controlled — the app owns
24
+ * `active`; wire selection with `@kerfjs/ui/wire-tab-scaffold`'s `wireTabScaffold`.
25
+ * On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of
26
+ * a bottom bar. See `docs/23-app-layouts.md` §3.4.
27
+ */
28
+ declare function TabScaffold({ id, label, tabs, active, className }: TabScaffoldProps): SafeHtml;
29
+
30
+ export { TabScaffold, type TabScaffoldProps, type TabScaffoldTab };
@@ -0,0 +1,16 @@
1
+ import { jsxs, jsx } from 'kerfjs/jsx-runtime';
2
+
3
+ // src/tab-scaffold.tsx
4
+ function TabScaffold({ id, label, tabs, active, className = "" }) {
5
+ return /* @__PURE__ */ jsxs("section", { class: `kui-tab-scaffold ${className}`.trim(), id, "data-component": "tab-scaffold", "data-tab-scaffold-id": id, children: [
6
+ /* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scenes", children: tabs.map((tab) => /* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scene", "data-tab-scaffold-scene": tab.id, "data-active": String(tab.id === active), "aria-hidden": String(tab.id !== active), children: tab.content })) }),
7
+ /* @__PURE__ */ jsx("nav", { class: "kui-tab-scaffold__bar", role: "tablist", "aria-label": label, children: tabs.map((tab) => /* @__PURE__ */ jsxs("button", { type: "button", class: "kui-tab-scaffold__tab", role: "tab", "data-tab-scaffold-tab": tab.id, "aria-selected": String(tab.id === active), tabindex: tab.id === active ? "0" : "-1", children: [
8
+ tab.icon && /* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-icon", "aria-hidden": "true", children: tab.icon }),
9
+ /* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-label", children: tab.label })
10
+ ] })) })
11
+ ] });
12
+ }
13
+
14
+ export { TabScaffold };
15
+
16
+ //# sourceMappingURL=tab-scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tab-scaffold.tsx"],"names":[],"mappings":";;;AA6BO,SAAS,WAAA,CAAY,EAAE,EAAA,EAAI,KAAA,EAAO,MAAM,MAAA,EAAQ,SAAA,GAAY,IAAG,EAAqB;AACzF,EAAA,uBAAO,IAAA,CAAC,SAAA,EAAA,EAAQ,KAAA,EAAO,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,EAAA,EAAQ,gBAAA,EAAe,cAAA,EAAe,sBAAA,EAAsB,EAAA,EACzH,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,0BAAA,EACR,QAAA,EAAA,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,qBAAQ,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,yBAAA,EAA0B,yBAAA,EAAyB,GAAA,CAAI,EAAA,EAAI,aAAA,EAAa,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,CAAA,EAAG,aAAA,EAAa,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,CAAA,EAAI,QAAA,EAAA,GAAA,CAAI,OAAA,EAAQ,CAAM,CAAA,EAC9L,CAAA;AAAA,oBACA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,uBAAA,EAAwB,MAAK,SAAA,EAAU,YAAA,EAAY,KAAA,EAC3D,QAAA,EAAA,IAAA,CAAK,IAAI,CAAC,GAAA,qBAAQ,IAAA,CAAC,QAAA,EAAA,EAAO,MAAK,QAAA,EAAS,KAAA,EAAM,uBAAA,EAAwB,IAAA,EAAK,KAAA,EAAM,uBAAA,EAAuB,GAAA,CAAI,EAAA,EAAI,iBAAe,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,GAAG,QAAA,EAAU,GAAA,CAAI,EAAA,KAAO,MAAA,GAAS,MAAM,IAAA,EAC5L,QAAA,EAAA;AAAA,MAAA,GAAA,CAAI,IAAA,wBAAS,MAAA,EAAA,EAAK,KAAA,EAAM,8BAA6B,aAAA,EAAY,MAAA,EAAQ,cAAI,IAAA,EAAK,CAAA;AAAA,sBACnF,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAM,6BAAA,EAA+B,cAAI,KAAA,EAAM;AAAA,KAAA,EACvD,CAAS,CAAA,EACX;AAAA,GAAA,EACF,CAAA;AACF","file":"tab-scaffold.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nexport interface TabScaffoldTab {\n id: string;\n label: string;\n /** Decorative icon shown above the label in the bottom bar. */\n icon?: SafeHtml;\n /** The tab's content — typically a `NavStack` so each tab keeps its own stack. */\n content: SafeHtml;\n}\n\nexport interface TabScaffoldProps {\n id: string;\n /** Accessible name for the tab bar. */\n label: string;\n tabs: TabScaffoldTab[];\n /** The controlled active tab id (the app owns selection). */\n active: string;\n className?: string;\n}\n\n/**\n * A mobile-first, iOS-like bottom tab scaffold: a bottom tab bar that switches\n * between major sections, each tab keeping its own content (usually a `NavStack`)\n * mounted so its stack and scroll survive a switch. Controlled — the app owns\n * `active`; wire selection with `@kerfjs/ui/wire-tab-scaffold`'s `wireTabScaffold`.\n * On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of\n * a bottom bar. See `docs/23-app-layouts.md` §3.4.\n */\nexport function TabScaffold({ id, label, tabs, active, className = '' }: TabScaffoldProps) {\n return <section class={`kui-tab-scaffold ${className}`.trim()} id={id} data-component=\"tab-scaffold\" data-tab-scaffold-id={id}>\n <div class=\"kui-tab-scaffold__scenes\">\n {tabs.map((tab) => <div class=\"kui-tab-scaffold__scene\" data-tab-scaffold-scene={tab.id} data-active={String(tab.id === active)} aria-hidden={String(tab.id !== active)}>{tab.content}</div>)}\n </div>\n <nav class=\"kui-tab-scaffold__bar\" role=\"tablist\" aria-label={label}>\n {tabs.map((tab) => <button type=\"button\" class=\"kui-tab-scaffold__tab\" role=\"tab\" data-tab-scaffold-tab={tab.id} aria-selected={String(tab.id === active)} tabindex={tab.id === active ? '0' : '-1'}>\n {tab.icon && <span class=\"kui-tab-scaffold__tab-icon\" aria-hidden=\"true\">{tab.icon}</span>}\n <span class=\"kui-tab-scaffold__tab-label\">{tab.label}</span>\n </button>)}\n </nav>\n </section>;\n}\n"]}
@@ -1,11 +1,13 @@
1
1
  import * as kerfjs from 'kerfjs';
2
2
 
3
- type ToolbarTextSize = 'large' | 'default' | 'small';
3
+ type ToolbarTextSize = 'xlarge' | 'large' | 'default' | 'small';
4
4
  interface ToolbarTextProps {
5
5
  text: string;
6
6
  size?: ToolbarTextSize;
7
7
  className?: string;
8
+ /** Optional id, e.g. so a dialog can reference the title via aria-labelledby. */
9
+ id?: string;
8
10
  }
9
- declare function ToolbarText({ text, size, className }: ToolbarTextProps): kerfjs.SafeHtml;
11
+ declare function ToolbarText({ text, size, className, id }: ToolbarTextProps): kerfjs.SafeHtml;
10
12
 
11
13
  export { ToolbarText, type ToolbarTextProps, type ToolbarTextSize };
@@ -1,3 +1,3 @@
1
- export { ToolbarText } from './chunk-GY5WH7TO.js';
1
+ export { ToolbarText } from './chunk-24Z2XE6C.js';
2
2
 
3
3
  //# sourceMappingURL=toolbar-text.js.map
@@ -0,0 +1,15 @@
1
+ interface WireNavStackOptions {
2
+ /** Invoked when the back control is activated. The app pops its own stack. */
3
+ onBack?: () => void;
4
+ /** Transition duration in ms (default 200). Set 0 to disable animation. */
5
+ duration?: number;
6
+ }
7
+ /**
8
+ * Animate a `NavStack`'s push/pop transitions and wire its back control. The app
9
+ * owns the stack (a signal of `NavStackView[]`) and re-renders `NavStack` when it
10
+ * changes; this helper slides the content and settles the chrome across each
11
+ * change, and calls `onBack` when the back control is used. Returns a disposer.
12
+ */
13
+ declare function wireNavStack(root: Element, options?: WireNavStackOptions): () => void;
14
+
15
+ export { type WireNavStackOptions, wireNavStack };
@@ -0,0 +1,88 @@
1
+ import { delegate } from 'kerfjs';
2
+
3
+ // src/wire-nav-stack.ts
4
+ var DEFAULT_DURATION = 200;
5
+ function viewsOf(viewport) {
6
+ return Array.from(viewport.querySelectorAll(":scope > .kui-nav-stack__view"));
7
+ }
8
+ function topView(viewport) {
9
+ const live = viewsOf(viewport).filter((view) => view.dataset.navExiting !== "true");
10
+ return live[live.length - 1];
11
+ }
12
+ function reducedMotion(view) {
13
+ return typeof view.matchMedia === "function" && view.matchMedia("(prefers-reduced-motion: reduce)").matches;
14
+ }
15
+ function wireNavStack(root, options = {}) {
16
+ const section = root.matches('[data-component="nav-stack"]') ? root : root.querySelector('[data-component="nav-stack"]');
17
+ const viewport = section?.querySelector("[data-nav-stack-viewport]");
18
+ if (!(section instanceof HTMLElement) || !viewport) return () => {
19
+ };
20
+ const view = section.ownerDocument.defaultView ?? window;
21
+ const duration = options.duration ?? DEFAULT_DURATION;
22
+ const disposeBack = delegate(section, "click", "[data-nav-back]", () => options.onBack?.());
23
+ let activeKey = topView(viewport)?.dataset.navKey;
24
+ const timers = /* @__PURE__ */ new Set();
25
+ const settle = (fn) => {
26
+ if (duration <= 0 || reducedMotion(view)) {
27
+ fn();
28
+ return;
29
+ }
30
+ const timer = view.setTimeout(() => {
31
+ timers.delete(timer);
32
+ fn();
33
+ }, duration);
34
+ timers.add(timer);
35
+ };
36
+ const play = (el, kind) => {
37
+ const animated = duration > 0 && !reducedMotion(view);
38
+ if (kind === "entering") {
39
+ el.classList.add("kui-nav-stack__view--entering");
40
+ if (animated) {
41
+ void el.offsetWidth;
42
+ view.requestAnimationFrame(() => el.classList.remove("kui-nav-stack__view--entering"));
43
+ } else {
44
+ el.classList.remove("kui-nav-stack__view--entering");
45
+ }
46
+ } else if (animated) {
47
+ view.requestAnimationFrame(() => el.classList.add("kui-nav-stack__view--exiting"));
48
+ } else {
49
+ el.classList.add("kui-nav-stack__view--exiting");
50
+ }
51
+ };
52
+ const observer = new MutationObserver((records) => {
53
+ const removed = [];
54
+ let added = false;
55
+ for (const record of records) {
56
+ record.removedNodes.forEach((node) => {
57
+ if (node instanceof HTMLElement && node.classList.contains("kui-nav-stack__view") && node.dataset.navExiting !== "true") removed.push(node);
58
+ });
59
+ record.addedNodes.forEach((node) => {
60
+ if (node instanceof HTMLElement && node.classList.contains("kui-nav-stack__view")) added = true;
61
+ });
62
+ }
63
+ const top = topView(viewport);
64
+ const currentKey = top?.dataset.navKey;
65
+ const poppedTop = removed.find((node) => node.dataset.navKey === activeKey);
66
+ if (poppedTop && currentKey !== activeKey) {
67
+ poppedTop.dataset.navExiting = "true";
68
+ poppedTop.setAttribute("aria-hidden", "true");
69
+ viewport.append(poppedTop);
70
+ play(poppedTop, "exiting");
71
+ settle(() => poppedTop.remove());
72
+ } else if (added && currentKey !== activeKey && top) {
73
+ play(top, "entering");
74
+ }
75
+ activeKey = currentKey;
76
+ });
77
+ observer.observe(viewport, { childList: true });
78
+ return () => {
79
+ disposeBack();
80
+ observer.disconnect();
81
+ for (const timer of timers) view.clearTimeout(timer);
82
+ timers.clear();
83
+ };
84
+ }
85
+
86
+ export { wireNavStack };
87
+
88
+ //# sourceMappingURL=wire-nav-stack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wire-nav-stack.ts"],"names":[],"mappings":"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","file":"wire-nav-stack.js","sourcesContent":["import { delegate } from 'kerfjs';\n\nexport interface WireNavStackOptions {\n /** Invoked when the back control is activated. The app pops its own stack. */\n onBack?: () => void;\n /** Transition duration in ms (default 200). Set 0 to disable animation. */\n duration?: number;\n}\n\nconst DEFAULT_DURATION = 200;\n\nfunction viewsOf(viewport: Element): HTMLElement[] {\n return Array.from(viewport.querySelectorAll<HTMLElement>(':scope > .kui-nav-stack__view'));\n}\n\nfunction topView(viewport: Element): HTMLElement | undefined {\n const live = viewsOf(viewport).filter((view) => view.dataset.navExiting !== 'true');\n return live[live.length - 1];\n}\n\nfunction reducedMotion(view: Window): boolean {\n return typeof view.matchMedia === 'function' && view.matchMedia('(prefers-reduced-motion: reduce)').matches;\n}\n\n/**\n * Animate a `NavStack`'s push/pop transitions and wire its back control. The app\n * owns the stack (a signal of `NavStackView[]`) and re-renders `NavStack` when it\n * changes; this helper slides the content and settles the chrome across each\n * change, and calls `onBack` when the back control is used. Returns a disposer.\n */\nexport function wireNavStack(root: Element, options: WireNavStackOptions = {}): () => void {\n const section = root.matches('[data-component=\"nav-stack\"]') ? root : root.querySelector('[data-component=\"nav-stack\"]');\n const viewport = section?.querySelector('[data-nav-stack-viewport]');\n if (!(section instanceof HTMLElement) || !viewport) return () => {};\n\n const view = section.ownerDocument.defaultView ?? window;\n const duration = options.duration ?? DEFAULT_DURATION;\n const disposeBack = delegate(section, 'click', '[data-nav-back]', () => options.onBack?.());\n\n let activeKey = topView(viewport)?.dataset.navKey;\n const timers = new Set<number>();\n\n const settle = (fn: () => void): void => {\n if (duration <= 0 || reducedMotion(view)) {\n fn();\n return;\n }\n const timer = view.setTimeout(() => {\n timers.delete(timer);\n fn();\n }, duration);\n timers.add(timer);\n };\n\n const play = (el: HTMLElement, kind: 'entering' | 'exiting'): void => {\n const animated = duration > 0 && !reducedMotion(view);\n if (kind === 'entering') {\n // Push: start the incoming view off the trailing edge, then release it so\n // it slides to rest (translateX(100%) → 0).\n el.classList.add('kui-nav-stack__view--entering');\n if (animated) {\n // Force a reflow so the starting transform applies before we clear it.\n void el.offsetWidth;\n view.requestAnimationFrame(() => el.classList.remove('kui-nav-stack__view--entering'));\n } else {\n el.classList.remove('kui-nav-stack__view--entering');\n }\n } else if (animated) {\n // Pop: the outgoing view is at rest; add the off-edge class on the next\n // frame so it slides OUT (translateX(0) → 100%), not in.\n view.requestAnimationFrame(() => el.classList.add('kui-nav-stack__view--exiting'));\n } else {\n el.classList.add('kui-nav-stack__view--exiting');\n }\n };\n\n const observer = new MutationObserver((records) => {\n const removed: HTMLElement[] = [];\n let added = false;\n for (const record of records) {\n record.removedNodes.forEach((node) => {\n if (node instanceof HTMLElement && node.classList.contains('kui-nav-stack__view') && node.dataset.navExiting !== 'true') removed.push(node);\n });\n record.addedNodes.forEach((node) => {\n if (node instanceof HTMLElement && node.classList.contains('kui-nav-stack__view')) added = true;\n });\n }\n\n const top = topView(viewport);\n const currentKey = top?.dataset.navKey;\n const poppedTop = removed.find((node) => node.dataset.navKey === activeKey);\n\n if (poppedTop && currentKey !== activeKey) {\n // Pop: bring the removed node back briefly to slide it out over the revealed view.\n poppedTop.dataset.navExiting = 'true';\n poppedTop.setAttribute('aria-hidden', 'true');\n viewport.append(poppedTop);\n play(poppedTop, 'exiting');\n settle(() => poppedTop.remove());\n } else if (added && currentKey !== activeKey && top) {\n // Push: slide the new top in.\n play(top, 'entering');\n }\n\n activeKey = currentKey;\n });\n observer.observe(viewport, { childList: true });\n\n return () => {\n disposeBack();\n observer.disconnect();\n for (const timer of timers) view.clearTimeout(timer);\n timers.clear();\n };\n}\n"]}
@@ -0,0 +1,11 @@
1
+ interface WireTabScaffoldOptions {
2
+ /** Invoked with the selected tab id when a bottom-bar tab is activated. */
3
+ onSelect: (tabId: string) => void;
4
+ }
5
+ /**
6
+ * Wire a `TabScaffold`'s bottom tab bar: clicking a tab calls `onSelect` with its
7
+ * id (the app then updates its controlled `active`). Returns a disposer.
8
+ */
9
+ declare function wireTabScaffold(root: Element, options: WireTabScaffoldOptions): () => void;
10
+
11
+ export { type WireTabScaffoldOptions, wireTabScaffold };
@@ -0,0 +1,16 @@
1
+ import { delegate } from 'kerfjs';
2
+
3
+ // src/wire-tab-scaffold.ts
4
+ function wireTabScaffold(root, options) {
5
+ const section = root.matches('[data-component="tab-scaffold"]') ? root : root.querySelector('[data-component="tab-scaffold"]');
6
+ if (!(section instanceof HTMLElement)) return () => {
7
+ };
8
+ return delegate(section, "click", "[data-tab-scaffold-tab]", (_event, target) => {
9
+ const tabId = target.getAttribute("data-tab-scaffold-tab");
10
+ if (tabId) options.onSelect(tabId);
11
+ });
12
+ }
13
+
14
+ export { wireTabScaffold };
15
+
16
+ //# sourceMappingURL=wire-tab-scaffold.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wire-tab-scaffold.ts"],"names":[],"mappings":";;;AAWO,SAAS,eAAA,CAAgB,MAAe,OAAA,EAA6C;AAC1F,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,CAAQ,iCAAiC,IAAI,IAAA,GAAO,IAAA,CAAK,cAAc,iCAAiC,CAAA;AAC7H,EAAA,IAAI,EAAE,OAAA,YAAmB,WAAA,CAAA,EAAc,OAAO,MAAM;AAAA,EAAC,CAAA;AACrD,EAAA,OAAO,SAAS,OAAA,EAAS,OAAA,EAAS,yBAAA,EAA2B,CAAC,QAAQ,MAAA,KAAW;AAC/E,IAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,YAAA,CAAa,uBAAuB,CAAA;AACzD,IAAA,IAAI,KAAA,EAAO,OAAA,CAAQ,QAAA,CAAS,KAAK,CAAA;AAAA,EACnC,CAAC,CAAA;AACH","file":"wire-tab-scaffold.js","sourcesContent":["import { delegate } from 'kerfjs';\n\nexport interface WireTabScaffoldOptions {\n /** Invoked with the selected tab id when a bottom-bar tab is activated. */\n onSelect: (tabId: string) => void;\n}\n\n/**\n * Wire a `TabScaffold`'s bottom tab bar: clicking a tab calls `onSelect` with its\n * id (the app then updates its controlled `active`). Returns a disposer.\n */\nexport function wireTabScaffold(root: Element, options: WireTabScaffoldOptions): () => void {\n const section = root.matches('[data-component=\"tab-scaffold\"]') ? root : root.querySelector('[data-component=\"tab-scaffold\"]');\n if (!(section instanceof HTMLElement)) return () => {};\n return delegate(section, 'click', '[data-tab-scaffold-tab]', (_event, target) => {\n const tabId = target.getAttribute('data-tab-scaffold-tab');\n if (tabId) options.onSelect(tabId);\n });\n}\n"]}
@@ -1,11 +1,53 @@
1
+ import { Signal } from 'kerfjs';
2
+
1
3
  interface TokenSearchSubmit {
2
4
  id: string;
3
5
  editor: HTMLElement;
4
6
  }
7
+ /**
8
+ * Managed collapsible behavior for the iconic TokenSearchField. Every piece is on
9
+ * by default; disable a specific one to own it in the app. Provide `signals` to
10
+ * drive app-owned `expanded` signals per field id instead of helper-created ones.
11
+ */
12
+ interface TokenSearchCollapsibleOptions {
13
+ /** Expand the field and focus its editor when the iconic trigger is activated. Default: true. */
14
+ expandOnActivate?: boolean;
15
+ /** Collapse the field when focus leaves it while it is empty. Default: true. */
16
+ collapseOnEmptyBlur?: boolean;
17
+ /** Collapse an empty field on Escape and restore focus to its trigger. Default: true. */
18
+ collapseOnEscape?: boolean;
19
+ /** Focus the editor on expand and the trigger on Escape-collapse. Default: true. */
20
+ manageFocus?: boolean;
21
+ /** App-owned `expanded` signals keyed by field id; adopted instead of helper-created. */
22
+ signals?: Readonly<Record<string, Signal<boolean>>>;
23
+ }
5
24
  interface WireTokenSearchFieldsOptions {
6
- onSubmit: (submission: TokenSearchSubmit) => void;
25
+ onSubmit?: (submission: TokenSearchSubmit) => void;
26
+ /** Managed collapsible transient behavior. `true`/omitted = on with defaults; `false` = fully off. */
27
+ collapsible?: boolean | TokenSearchCollapsibleOptions;
28
+ }
29
+ /**
30
+ * The value returned from {@link wireTokenSearchFields}: call it (or `dispose()`) to
31
+ * tear down. When collapsible behavior is managed, it also exposes the transient
32
+ * `expanded` state per field id so the app can read it in render, hand in its own
33
+ * signal, or drive it imperatively.
34
+ */
35
+ interface TokenSearchFieldsHandle {
36
+ (): void;
37
+ dispose(): void;
38
+ /** The managed `expanded` signal for a field id (adopted or helper-created); undefined when unmanaged. */
39
+ expanded(id: string): Signal<boolean> | undefined;
40
+ /** Expand the field (and, when focus is managed, focus its editor). */
41
+ open(id: string): void;
42
+ /** Collapse the field (and, when focus is managed, restore focus to its trigger). */
43
+ close(id: string): void;
7
44
  }
8
- /** Keep TokenSearchField wrapping, submit Enter, and preserve its caret across controlled token deletion. */
9
- declare function wireTokenSearchFields(root: HTMLElement, { onSubmit }: WireTokenSearchFieldsOptions): () => void;
45
+ /**
46
+ * Wire every TokenSearchField under `root`: submit on Enter, preserve the caret across
47
+ * controlled token deletion, and (by default) manage the collapsible field's transient
48
+ * expand/collapse/focus. Returns a {@link TokenSearchFieldsHandle} — a disposer that also
49
+ * exposes the managed `expanded` state per field id.
50
+ */
51
+ declare function wireTokenSearchFields(root: HTMLElement, { onSubmit, collapsible }?: WireTokenSearchFieldsOptions): TokenSearchFieldsHandle;
10
52
 
11
- export { type TokenSearchSubmit, type WireTokenSearchFieldsOptions, wireTokenSearchFields };
53
+ export { type TokenSearchCollapsibleOptions, type TokenSearchFieldsHandle, type TokenSearchSubmit, type WireTokenSearchFieldsOptions, wireTokenSearchFields };
@@ -1,4 +1,4 @@
1
- export { wireTokenSearchFields } from './chunk-H5AGGVU5.js';
1
+ export { wireTokenSearchFields } from './chunk-2RJBFNB6.js';
2
2
 
3
3
 
4
4
 
@@ -0,0 +1,33 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ /** A collapsible Workbench panel — a side rail or the bottom drawer. */
4
+ interface WorkbenchPanel {
5
+ content: SafeHtml;
6
+ /** Whether the panel is currently collapsed (the app owns this). */
7
+ collapsed?: boolean;
8
+ /** Rail width, or drawer height, in px. Overrides the CSS default. */
9
+ size?: number;
10
+ /** Accessible name for the panel region. */
11
+ label?: string;
12
+ }
13
+ interface WorkbenchProps {
14
+ id: string;
15
+ label: string;
16
+ /** The central work area. */
17
+ main: SafeHtml;
18
+ leftRail?: WorkbenchPanel;
19
+ rightRail?: WorkbenchPanel;
20
+ bottomDrawer?: WorkbenchPanel;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * The Xcode-like multi-panel workspace: a collapsible left rail, right rail, and
25
+ * bottom drawer around a central work area (any absent). Collapsing snaps the
26
+ * panel's track to zero in one reflow while its fixed-size content slides out via
27
+ * a composited transform — the instant-width / sliding-content technique, so the
28
+ * work area relayouts once, not per frame. The app owns each `collapsed` flag;
29
+ * the collapse is pure CSS (no wire). See `docs/23-app-layouts.md` §3.3.
30
+ */
31
+ declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, className }: WorkbenchProps): SafeHtml;
32
+
33
+ export { Workbench, type WorkbenchPanel, type WorkbenchProps };
@@ -0,0 +1,17 @@
1
+ import { jsxs, jsx } from 'kerfjs/jsx-runtime';
2
+
3
+ // src/workbench.tsx
4
+ function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, className = "" }) {
5
+ return /* @__PURE__ */ jsxs("section", { class: `kui-workbench ${className}`.trim(), id, "data-component": "workbench", "aria-label": label, children: [
6
+ leftRail && /* @__PURE__ */ jsx("aside", { class: "kui-workbench__rail kui-workbench__rail--left", "data-workbench-rail": "left", "data-collapsed": String(leftRail.collapsed ?? false), "aria-label": leftRail.label || void 0, style: leftRail.size ? `--kui-workbench-rail-width: ${leftRail.size}px` : void 0, children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: leftRail.content }) }),
7
+ /* @__PURE__ */ jsxs("div", { class: "kui-workbench__center", children: [
8
+ /* @__PURE__ */ jsx("div", { class: "kui-workbench__main", "data-workbench-main": true, children: main }),
9
+ bottomDrawer && /* @__PURE__ */ jsx("section", { class: "kui-workbench__drawer", "data-workbench-drawer": true, "data-collapsed": String(bottomDrawer.collapsed ?? false), "aria-label": bottomDrawer.label || void 0, style: bottomDrawer.size ? `--kui-workbench-drawer-height: ${bottomDrawer.size}px` : void 0, children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: bottomDrawer.content }) })
10
+ ] }),
11
+ rightRail && /* @__PURE__ */ jsx("aside", { class: "kui-workbench__rail kui-workbench__rail--right", "data-workbench-rail": "right", "data-collapsed": String(rightRail.collapsed ?? false), "aria-label": rightRail.label || void 0, style: rightRail.size ? `--kui-workbench-rail-width: ${rightRail.size}px` : void 0, children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: rightRail.content }) })
12
+ ] });
13
+ }
14
+
15
+ export { Workbench };
16
+
17
+ //# sourceMappingURL=workbench.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workbench.tsx"],"names":[],"mappings":";;;AAgCO,SAAS,SAAA,CAAU,EAAE,EAAA,EAAI,KAAA,EAAO,IAAA,EAAM,UAAU,SAAA,EAAW,YAAA,EAAc,SAAA,GAAY,EAAA,EAAG,EAAmB;AAChH,EAAA,uBAAO,IAAA,CAAC,SAAA,EAAA,EAAQ,KAAA,EAAO,CAAA,cAAA,EAAiB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,EAAA,EAAQ,gBAAA,EAAe,WAAA,EAAY,YAAA,EAAY,KAAA,EACxG,QAAA,EAAA;AAAA,IAAA,QAAA,oBAAY,GAAA,CAAC,OAAA,EAAA,EAAM,KAAA,EAAM,+CAAA,EAAgD,qBAAA,EAAoB,MAAA,EAAO,gBAAA,EAAgB,MAAA,CAAO,QAAA,CAAS,SAAA,IAAa,KAAK,CAAA,EAAG,cAAY,QAAA,CAAS,KAAA,IAAS,MAAA,EAAW,KAAA,EAAO,QAAA,CAAS,IAAA,GAAO,CAAA,4BAAA,EAA+B,QAAA,CAAS,IAAI,CAAA,EAAA,CAAA,GAAO,MAAA,EAC3Q,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,8BAAA,EAAgC,QAAA,EAAA,QAAA,CAAS,SAAQ,CAAA,EAC9D,CAAA;AAAA,oBACA,IAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,uBAAA,EACT,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,qBAAA,EAAsB,qBAAA,EAAmB,MAAE,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,MAC1D,YAAA,oBAAgB,GAAA,CAAC,SAAA,EAAA,EAAQ,KAAA,EAAM,uBAAA,EAAwB,uBAAA,EAAqB,IAAA,EAAC,gBAAA,EAAgB,MAAA,CAAO,YAAA,CAAa,SAAA,IAAa,KAAK,GAAG,YAAA,EAAY,YAAA,CAAa,KAAA,IAAS,MAAA,EAAW,KAAA,EAAO,YAAA,CAAa,IAAA,GAAO,CAAA,+BAAA,EAAkC,aAAa,IAAI,CAAA,EAAA,CAAA,GAAO,MAAA,EACvQ,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,8BAAA,EAAgC,QAAA,EAAA,YAAA,CAAa,SAAQ,CAAA,EAClE;AAAA,KAAA,EACF,CAAA;AAAA,IACC,SAAA,oBAAa,GAAA,CAAC,OAAA,EAAA,EAAM,KAAA,EAAM,gDAAA,EAAiD,qBAAA,EAAoB,OAAA,EAAQ,gBAAA,EAAgB,MAAA,CAAO,SAAA,CAAU,SAAA,IAAa,KAAK,GAAG,YAAA,EAAY,SAAA,CAAU,KAAA,IAAS,MAAA,EAAW,KAAA,EAAO,SAAA,CAAU,IAAA,GAAO,CAAA,4BAAA,EAA+B,UAAU,IAAI,CAAA,EAAA,CAAA,GAAO,MAAA,EAClR,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,8BAAA,EAAgC,QAAA,EAAA,SAAA,CAAU,SAAQ,CAAA,EAC/D;AAAA,GAAA,EACF,CAAA;AACF","file":"workbench.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\n/** A collapsible Workbench panel — a side rail or the bottom drawer. */\nexport interface WorkbenchPanel {\n content: SafeHtml;\n /** Whether the panel is currently collapsed (the app owns this). */\n collapsed?: boolean;\n /** Rail width, or drawer height, in px. Overrides the CSS default. */\n size?: number;\n /** Accessible name for the panel region. */\n label?: string;\n}\n\nexport interface WorkbenchProps {\n id: string;\n label: string;\n /** The central work area. */\n main: SafeHtml;\n leftRail?: WorkbenchPanel;\n rightRail?: WorkbenchPanel;\n bottomDrawer?: WorkbenchPanel;\n className?: string;\n}\n\n/**\n * The Xcode-like multi-panel workspace: a collapsible left rail, right rail, and\n * bottom drawer around a central work area (any absent). Collapsing snaps the\n * panel's track to zero in one reflow while its fixed-size content slides out via\n * a composited transform — the instant-width / sliding-content technique, so the\n * work area relayouts once, not per frame. The app owns each `collapsed` flag;\n * the collapse is pure CSS (no wire). See `docs/23-app-layouts.md` §3.3.\n */\nexport function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, className = '' }: WorkbenchProps) {\n return <section class={`kui-workbench ${className}`.trim()} id={id} data-component=\"workbench\" aria-label={label}>\n {leftRail && <aside class=\"kui-workbench__rail kui-workbench__rail--left\" data-workbench-rail=\"left\" data-collapsed={String(leftRail.collapsed ?? false)} aria-label={leftRail.label || undefined} style={leftRail.size ? `--kui-workbench-rail-width: ${leftRail.size}px` : undefined}>\n <div class=\"kui-workbench__panel-content\">{leftRail.content}</div>\n </aside>}\n <div class=\"kui-workbench__center\">\n <div class=\"kui-workbench__main\" data-workbench-main>{main}</div>\n {bottomDrawer && <section class=\"kui-workbench__drawer\" data-workbench-drawer data-collapsed={String(bottomDrawer.collapsed ?? false)} aria-label={bottomDrawer.label || undefined} style={bottomDrawer.size ? `--kui-workbench-drawer-height: ${bottomDrawer.size}px` : undefined}>\n <div class=\"kui-workbench__panel-content\">{bottomDrawer.content}</div>\n </section>}\n </div>\n {rightRail && <aside class=\"kui-workbench__rail kui-workbench__rail--right\" data-workbench-rail=\"right\" data-collapsed={String(rightRail.collapsed ?? false)} aria-label={rightRail.label || undefined} style={rightRail.size ? `--kui-workbench-rail-width: ${rightRail.size}px` : undefined}>\n <div class=\"kui-workbench__panel-content\">{rightRail.content}</div>\n </aside>}\n </section>;\n}\n"]}
@@ -41,16 +41,16 @@ The handle exposes separator role, orientation, name, minimum, maximum, and curr
41
41
 
42
42
  The application owns persistence and collapsed/expanded policy. Keep the last expanded size outside the component and restore it when reopening. An optional `handleIcon` replaces only decorative dormant content; it must not contain controls or interactive roles because the separator remains the sole focus and interaction owner.
43
43
 
44
- ## DialogHeader
45
-
46
- `DialogHeader` keeps its icon and `h2` together in the leading borderless
47
- `ToolbarControlGroup`, with optional actions in a separate trailing group. Pass
48
- localized `actionsLabel` text when the action group needs an accessible name;
49
- the component does not synthesize an English label. The application connects
50
- `titleId` and an optional `summaryId` to the owning dialog through
51
- `aria-labelledby` and `aria-describedby`. Pass action children directly so the
52
- component can group them; existing pre-grouped actions remain compatible, but
53
- their inner groups retain their own labels and semantics.
44
+ ## PanelHeader
45
+
46
+ `PanelHeader` is a plain `Toolbar` heading. Its leading zone holds an optional
47
+ icon (a normal bordered `ToolbarControlGroup`) and the title as extra-large
48
+ `ToolbarText`, and the app's trailing controls go straight into the trailing
49
+ zone. The title carries no native heading role, so the application connects
50
+ `titleId` and an optional `summaryId` to the owning dialog or panel through
51
+ `aria-labelledby` and `aria-describedby` (and provides a document heading
52
+ separately when one is required). Pass the trailing controls as a labeled
53
+ `ToolbarControlGroup` when that group needs an accessible name.
54
54
 
55
55
  ## Tabs
56
56
 
@@ -0,0 +1,57 @@
1
+ # Choosing an app layout
2
+
3
+ `@kerfjs/ui` ships four opt-in, tree-shakeable whole-screen layouts plus the
4
+ [`device-class`](device-class.md) signal that drives their responsive behavior.
5
+ This guide maps a **data + interaction + device** situation to the layout to
6
+ reach for, and states the device-class threshold at which the presentation
7
+ changes. The layouts:
8
+
9
+ - [`NavStack`](nav-stack.md) — push/pop navigation (a single pane is a one-entry stack).
10
+ - [`SplitView`](split-view.md) — list-detail (two panes, collapsing to a stack).
11
+ - [`Workbench`](workbench.md) — the Xcode-like collapsible rails + drawer.
12
+ - [`TabScaffold`](tab-scaffold.md) — the iOS bottom tab bar (each tab a stack).
13
+
14
+ Derive responsiveness from `deviceClass()`: `compact` (a handset or portrait
15
+ tablet) means "one pane at a time"; `atLeast('tablet')` / `atLeast('desktop')`
16
+ gate the roomier presentations.
17
+
18
+ ## Decision matrix
19
+
20
+ | Situation | Layout | Device threshold |
21
+ | --- | --- | --- |
22
+ | Simple app, a few flat sections | `NavStack` with one entry (single pane); add `TabScaffold` for 2–5 co-equal sections on handset | `TabScaffold` on `compact`; promote its tabs to a `Workbench` rail / sidebar `atLeast('desktop')` |
23
+ | Drill-down browsing (list → item → sub-item) | `NavStack`; upgrade to `SplitView` once list + detail fit together | `SplitView` two-pane `atLeast('tablet')` landscape / non-`compact`; `NavStack` form on `compact` |
24
+ | Two related panes, selecting on the left updates the right | `SplitView` | two panes when not `compact`; collapses to `NavStack` (list → detail) on `compact` |
25
+ | Complex tool / editor with peripheral panels (navigator, inspector, console) | `Workbench` | full three-panel `atLeast('desktop')`; on smaller classes present the rails via `NavStack` / overlay drawers, not a shrunken shell |
26
+ | Mobile app with 2–5 top-level destinations, each its own drill-down | `TabScaffold`, each tab a `NavStack` | bottom bar on `compact`; promote to a rail / sidebar `atLeast('desktop')` |
27
+
28
+ ### Worked examples
29
+
30
+ - **Settings screen (simple):** one `NavStack` entry per screen; push a subpage
31
+ on tap. No `SplitView`/`Workbench` — it is a single flow.
32
+ - **Mail (drill-down + two-pane):** `SplitView` with `list={<ThreadList/>}` and
33
+ `detail={<Message/>}`, `compact={device.value.compact}`,
34
+ `detailActive={selected != null}`. On desktop both panes show with a resizable
35
+ separator; on a phone it is a `NavStack` (threads → message, back clears the
36
+ selection).
37
+ - **IDE (complex tool):** `Workbench` with a left navigator rail, a right
38
+ inspector rail, and a bottom console drawer, each `collapsed` bound to a
39
+ signal. Only offer this `atLeast('desktop')`.
40
+ - **Social app (tabbed):** `TabScaffold` with Home / Search / Profile tabs, each
41
+ `content` a `NavStack`. On a tablet/desktop, render the same sections as a
42
+ `Workbench` left rail instead of a bottom bar.
43
+
44
+ ## Dialogs
45
+
46
+ Pick the dialog's inner layout by the same complexity axis, then apply the device
47
+ class to how it is presented (compose with [`overlay`](../../docs/19-native-overlay-backing.md)):
48
+
49
+ - **desktop:** an inline dialog — a `SplitView` two-pane body, or a `NavStack`
50
+ for a wizard.
51
+ - **portrait tablet / handset:** present a `SplitView`/complex dialog as a
52
+ full-screen modal (its `compact` `NavStack` form).
53
+ - **landscape tablet:** a large partial-cover modal (does not need to go full
54
+ screen).
55
+
56
+ A `NavStack` works as a dialog body at every size — a wizard pushes and pops its
57
+ steps with cross-faded chrome.
@@ -51,6 +51,9 @@ and disposal.
51
51
  contain controls. When a row needs an independently interactive trailing
52
52
  region, `MenuActionRow` owns the noninteractive root and the two sibling native
53
53
  buttons; the application owns their delegated behavior and controlled state.
54
+ Both row components align a leading icon with the first label line when
55
+ `multiline` allows the label to wrap; additional lines extend below that fixed
56
+ visual anchor.
54
57
 
55
58
  ### Public CSS anatomy
56
59
 
@@ -118,14 +121,14 @@ into the section label. Do not add padding to pane shells,
118
121
  double child-owned geometry with wrapper insets, or create competing scroll
119
122
  owners. The [layout contract](./layout.md) lists the public roles and tokens.
120
123
 
121
- `DialogHeader` composes its icon and first title line inside a borderless
122
- `ToolbarControlGroup` in a real top `Toolbar`. Direct `actions` children are
123
- automatically placed in one contained `ToolbarControlGroup`; use the optional
124
- localized `actionsLabel` when that group needs an accessible name. Existing
125
- pre-grouped action content remains layout-compatible, but new callers should
126
- pass the action children rather than groups. The optional summary is a separate
127
- row aligned below the title, so it cannot pull the 24px icon or 34px circular
128
- icon background out of vertical alignment with the 44px action row.
124
+ `PanelHeader` is a plain top `Toolbar` used as a panel, dialog, or page heading;
125
+ it overrides no Toolbar styles. The leading zone holds an optional icon (a normal
126
+ bordered `ToolbarControlGroup` given a brand fill with a matching border) and the
127
+ title as extra-large `ToolbarText`, and the app's `actions` go straight into the
128
+ trailing zone (typically as a `ToolbarControlGroup`). The icon group is omitted
129
+ when no icon is passed. The optional summary is a separate row aligned below the
130
+ title, so it cannot pull the icon group out of vertical alignment with the
131
+ title and action row.
129
132
 
130
133
  `ValueTable` composes typed `ValueTableRow` entries. A row owns its `dt`/`dd`
131
134
  semantics and may receive a leading `SafeHtml` icon. Every row keeps 8px of
@@ -150,7 +153,32 @@ overrides load later in the cascade or set scoped `--kui-*` variables. JavaScrip
150
153
  modules are pure except the browser style wrappers and `@kerfjs/ui/select/register`,
151
154
  which registers exactly the Web Awesome elements used by `Select`. Eventful
152
155
  helpers such as `wireResizableRegions` and `wireTabBars` attach listeners only
153
- when called and return disposers. CSS, the generated wrappers that make it
156
+ when called and return disposers. `wireTokenSearchFields` goes one step further:
157
+ by default it also owns the collapsible field's transient expand/collapse/focus
158
+ (activate to reveal and focus, Escape or empty blur to collapse), holding that
159
+ state in a signal it exposes on the returned handle. An app reads that signal in
160
+ render, hands in its own via `collapsible.signals`, drives it through
161
+ `handle.open`/`handle.close`, or disables any individual behavior — so transient
162
+ UI is consistent by default without every app reinventing it.
163
+
164
+ `wireTokenSearchFields` is a deliberate exception, not the rule for `wire…`
165
+ helpers. Its collapse behavior was *rich and error-prone* — reveal, focus
166
+ transfer, Escape, empty-blur collapse, focus return — the kind of transient chrome
167
+ apps kept reimplementing inconsistently, so the helper owns it. Everywhere else the
168
+ app's state is **domain or persisted, not transient chrome, and stays app-owned**: a
169
+ `NavStack`'s view stack is navigation history, a `TabBar`/`TabScaffold`'s selection
170
+ and tab order are data, a `ResizableRegion`'s committed size and a
171
+ `Workbench`/`SplitView` rail's `collapsed` flag are persisted layout preferences.
172
+ Each helper already owns only the *ephemeral mechanics* around that state —
173
+ `wireNavStack` the push/pop animation, `wireTabBars` the overflow autoscroll and
174
+ drag preview, `wireResizableRegions` the live drag preview — and reports committed
175
+ changes through callbacks. A `MenuHeader` `toggle` disclosure's `expanded` is
176
+ likewise app-owned: it is a one-line boolean the app already tracks and must read to
177
+ render the section body, so a managed helper would remove no real complexity. Reach
178
+ for a managed default only when the transient behavior is substantial enough that
179
+ hand-rolling it produces genuine, inconsistent variation.
180
+
181
+ CSS, the generated wrappers that make it
154
182
  reachable, and the registration module are the package's only declared side
155
183
  effects.
156
184