@kerfjs/ui 5.0.0-beta.4 → 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 (95) hide show
  1. package/README.md +8 -8
  2. package/ai/component-catalog.json +303 -167
  3. package/ai/public-api-signatures-v1.md +333 -26
  4. package/ai/skill.md +14 -7
  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-header.css +19 -2
  31. package/dist/styles/nav-stack.css +112 -0
  32. package/dist/styles/panel-header.css +59 -0
  33. package/dist/styles/split-view.css +35 -0
  34. package/dist/styles/styles.css +1 -2
  35. package/dist/styles/tab-scaffold.css +84 -0
  36. package/dist/styles/toolbar-control-group.css +52 -2
  37. package/dist/styles/toolbar-text.css +11 -0
  38. package/dist/styles/toolbar.css +9 -0
  39. package/dist/styles/workbench.css +94 -0
  40. package/dist/tab-scaffold.d.ts +30 -0
  41. package/dist/tab-scaffold.js +16 -0
  42. package/dist/tab-scaffold.js.map +1 -0
  43. package/dist/toolbar-text.d.ts +4 -2
  44. package/dist/toolbar-text.js +1 -1
  45. package/dist/wire-nav-stack.d.ts +15 -0
  46. package/dist/wire-nav-stack.js +88 -0
  47. package/dist/wire-nav-stack.js.map +1 -0
  48. package/dist/wire-tab-scaffold.d.ts +11 -0
  49. package/dist/wire-tab-scaffold.js +16 -0
  50. package/dist/wire-tab-scaffold.js.map +1 -0
  51. package/dist/wire-token-search-fields.d.ts +46 -4
  52. package/dist/wire-token-search-fields.js +1 -1
  53. package/dist/workbench.d.ts +33 -0
  54. package/dist/workbench.js +17 -0
  55. package/dist/workbench.js.map +1 -0
  56. package/docs/accessibility.md +10 -10
  57. package/docs/app-layouts.md +57 -0
  58. package/docs/component-contract.md +34 -9
  59. package/docs/component-selection.md +56 -7
  60. package/docs/design-philosophy.md +23 -1
  61. package/docs/device-class.md +54 -0
  62. package/docs/layout.md +33 -8
  63. package/docs/nav-stack.md +47 -0
  64. package/docs/recipes.md +17 -5
  65. package/docs/split-view.md +49 -0
  66. package/docs/tab-scaffold.md +41 -0
  67. package/docs/ux-demo.md +3 -3
  68. package/docs/workbench.md +47 -0
  69. package/llms.txt +107 -43
  70. package/package.json +37 -11
  71. package/ux-demo/recipes/app-shell.tsx +3 -3
  72. package/ux-demo/recipes/composer-form.tsx +2 -2
  73. package/ux-demo/recipes/list-workspace-states.tsx +2 -2
  74. package/ux-demo/recipes/loaders.ts +2 -0
  75. package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
  76. package/ux-demo/recipes/mount-recipe.ts +3 -0
  77. package/ux-demo/recipes/navigation-stack.tsx +76 -0
  78. package/ux-demo/recipes/recipes.css +19 -8
  79. package/ux-demo/recipes/workspace-header.tsx +2 -2
  80. package/dist/browser/page-header.js +0 -3
  81. package/dist/chunk-2PES33HS.js +0 -13
  82. package/dist/chunk-2PES33HS.js.map +0 -1
  83. package/dist/chunk-GY5WH7TO.js.map +0 -1
  84. package/dist/chunk-H5AGGVU5.js +0 -75
  85. package/dist/chunk-H5AGGVU5.js.map +0 -1
  86. package/dist/chunk-K3G72I6D.js +0 -24
  87. package/dist/chunk-K3G72I6D.js.map +0 -1
  88. package/dist/dialog-header.d.ts +0 -15
  89. package/dist/dialog-header.js +0 -5
  90. package/dist/dialog-header.js.map +0 -1
  91. package/dist/page-header.d.ts +0 -9
  92. package/dist/page-header.js +0 -3
  93. package/dist/page-header.js.map +0 -1
  94. package/dist/styles/dialog-header.css +0 -87
  95. package/dist/styles/page-header.css +0 -33
@@ -0,0 +1,172 @@
1
+ import { placeTokenSearchCaret, readTokenSearchField } from './chunk-7LDCTU7D.js';
2
+ import { delegate, signal } from 'kerfjs';
3
+
4
+ function editorFromEvent(root, event) {
5
+ const target = event.target;
6
+ if (!(target instanceof Element)) return;
7
+ const editor = target.closest("[data-token-search-editor]");
8
+ return editor && root.contains(editor) ? editor : void 0;
9
+ }
10
+ function caretQueryOffset(editor) {
11
+ const selection = editor.ownerDocument.getSelection();
12
+ if (!selection || selection.rangeCount === 0) return;
13
+ const selectionRange = selection.getRangeAt(0);
14
+ if (!editor.contains(selectionRange.startContainer)) return;
15
+ const prefixRange = editor.ownerDocument.createRange();
16
+ prefixRange.selectNodeContents(editor);
17
+ prefixRange.setEnd(selectionRange.startContainer, selectionRange.startOffset);
18
+ const prefix = editor.ownerDocument.createElement("div");
19
+ prefix.append(prefixRange.cloneContents());
20
+ return readTokenSearchField(prefix).query.length;
21
+ }
22
+ function replacementEditor(root, id) {
23
+ return [...root.querySelectorAll("[data-token-search-editor]")].find((editor) => editor.dataset.tokenSearchEditor === id);
24
+ }
25
+ function editorIsEmpty(editor) {
26
+ const value = readTokenSearchField(editor);
27
+ return value.query.length === 0 && value.tokens.length === 0;
28
+ }
29
+ function wireTokenSearchFields(root, { onSubmit, collapsible = true } = {}) {
30
+ const managed = collapsible !== false;
31
+ const config = typeof collapsible === "object" ? collapsible : {};
32
+ const expandOnActivate = managed && (config.expandOnActivate ?? true);
33
+ const collapseOnEmptyBlur = managed && (config.collapseOnEmptyBlur ?? true);
34
+ const collapseOnEscape = managed && (config.collapseOnEscape ?? true);
35
+ const manageFocus = managed && (config.manageFocus ?? true);
36
+ const adopted = config.signals ?? {};
37
+ const created = /* @__PURE__ */ new Map();
38
+ const signalFor = (id) => {
39
+ const own = adopted[id];
40
+ if (own) return own;
41
+ let field = created.get(id);
42
+ if (!field) {
43
+ field = signal(false);
44
+ created.set(id, field);
45
+ }
46
+ return field;
47
+ };
48
+ const setExpanded = (id, next) => {
49
+ const state = signalFor(id);
50
+ if (state.value !== next) state.value = next;
51
+ };
52
+ const view = () => root.ownerDocument.defaultView;
53
+ const focusAfterRender = (id, selector) => {
54
+ view().requestAnimationFrame(() => {
55
+ const target = root.querySelector(
56
+ `[data-component="token-search-field"][data-token-search-id="${CSS.escape(id)}"] ${selector}`
57
+ );
58
+ if (!target) return;
59
+ if (target.matches("[data-token-search-editor]")) placeTokenSearchCaret(target);
60
+ else target.focus();
61
+ });
62
+ };
63
+ const openField = (id) => {
64
+ setExpanded(id, true);
65
+ if (manageFocus) focusAfterRender(id, "[data-token-search-editor]");
66
+ };
67
+ const closeField = (id) => {
68
+ setExpanded(id, false);
69
+ if (manageFocus) focusAfterRender(id, ".kui-token-search__expand");
70
+ };
71
+ const pending = /* @__PURE__ */ new WeakMap();
72
+ const onBeforeInput = (event) => {
73
+ const inputEvent = event;
74
+ const editor = editorFromEvent(root, event);
75
+ if (!editor || !inputEvent.inputType.startsWith("delete") || editor.ownerDocument.activeElement !== editor) return;
76
+ const field = editor.closest('[data-component="token-search-field"]');
77
+ const id = field?.dataset.tokenSearchId;
78
+ const offset = caretQueryOffset(editor);
79
+ if (!id || offset === void 0 || field?.dataset.disabled === "true") return;
80
+ pending.set(editor, {
81
+ id,
82
+ offset,
83
+ tokenCount: editor.querySelectorAll('[data-component="token-search-token"]').length
84
+ });
85
+ };
86
+ const onInput = (event) => {
87
+ const editor = editorFromEvent(root, event);
88
+ if (!editor) return;
89
+ const deletion = pending.get(editor);
90
+ pending.delete(editor);
91
+ if (!deletion || editor.querySelectorAll('[data-component="token-search-token"]').length >= deletion.tokenCount) return;
92
+ editor.ownerDocument.defaultView.requestAnimationFrame(() => {
93
+ const replacement = replacementEditor(root, deletion.id);
94
+ if (!replacement) return;
95
+ const active = replacement.ownerDocument.activeElement;
96
+ if (active !== editor && active !== replacement && active !== replacement.ownerDocument.body) return;
97
+ placeTokenSearchCaret(replacement, deletion.offset);
98
+ });
99
+ };
100
+ root.addEventListener("beforeinput", onBeforeInput, true);
101
+ root.addEventListener("input", onInput, true);
102
+ const disposers = [
103
+ () => root.removeEventListener("beforeinput", onBeforeInput, true),
104
+ () => root.removeEventListener("input", onInput, true)
105
+ ];
106
+ disposers.push(
107
+ delegate(root, "keydown", "[data-token-search-editor]", (event, element) => {
108
+ const keyboardEvent = event;
109
+ const editor = element;
110
+ if (keyboardEvent.isComposing) return;
111
+ const field = editor.closest('[data-component="token-search-field"]');
112
+ const id = field?.dataset.tokenSearchId;
113
+ if (!id || field?.dataset.disabled === "true") return;
114
+ if (keyboardEvent.key === "Enter") {
115
+ keyboardEvent.preventDefault();
116
+ onSubmit?.({ id, editor });
117
+ return;
118
+ }
119
+ if (collapseOnEscape && keyboardEvent.key === "Escape" && field?.dataset.collapsible === "true" && editorIsEmpty(editor)) {
120
+ keyboardEvent.preventDefault();
121
+ closeField(id);
122
+ }
123
+ })
124
+ );
125
+ if (expandOnActivate) {
126
+ disposers.push(
127
+ delegate(root, "click", ".kui-token-search__expand", (_event, element) => {
128
+ const field = element.closest('[data-component="token-search-field"]');
129
+ const id = field?.dataset.tokenSearchId;
130
+ if (id && field?.dataset.disabled !== "true") openField(id);
131
+ })
132
+ );
133
+ }
134
+ if (collapseOnEmptyBlur) {
135
+ disposers.push(
136
+ delegate(root, "mousedown", '[data-component="token-search-field"][data-collapsible="true"] button', (event) => {
137
+ event.preventDefault();
138
+ })
139
+ );
140
+ disposers.push(
141
+ delegate(root, "focusout", "[data-token-search-editor]", (event, element) => {
142
+ const editor = element;
143
+ const field = editor.closest('[data-component="token-search-field"]');
144
+ const id = field?.dataset.tokenSearchId;
145
+ if (!id || field?.dataset.collapsible !== "true" || field.dataset.disabled === "true") return;
146
+ const next = event.relatedTarget;
147
+ if (next instanceof Node && field.contains(next)) return;
148
+ if (!editorIsEmpty(editor)) return;
149
+ view().queueMicrotask(() => {
150
+ if (!field.contains(field.ownerDocument.activeElement)) setExpanded(id, false);
151
+ });
152
+ })
153
+ );
154
+ }
155
+ const dispose = () => {
156
+ for (const stop of disposers.splice(0)) stop();
157
+ };
158
+ const handle = (() => dispose());
159
+ handle.dispose = dispose;
160
+ handle.expanded = (id) => managed ? signalFor(id) : void 0;
161
+ handle.open = (id) => {
162
+ if (managed) openField(id);
163
+ };
164
+ handle.close = (id) => {
165
+ if (managed) closeField(id);
166
+ };
167
+ return handle;
168
+ }
169
+
170
+ export { wireTokenSearchFields };
171
+
172
+ //# sourceMappingURL=chunk-2RJBFNB6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/wire-token-search-fields.ts"],"names":[],"mappings":"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","file":"chunk-2RJBFNB6.js","sourcesContent":["import { delegate, type Signal,signal } from 'kerfjs';\n\nimport { placeTokenSearchCaret, readTokenSearchField } from './token-search-field.js';\n\nexport interface TokenSearchSubmit {\n id: string;\n editor: HTMLElement;\n}\n\n/**\n * Managed collapsible behavior for the iconic TokenSearchField. Every piece is on\n * by default; disable a specific one to own it in the app. Provide `signals` to\n * drive app-owned `expanded` signals per field id instead of helper-created ones.\n */\nexport interface TokenSearchCollapsibleOptions {\n /** Expand the field and focus its editor when the iconic trigger is activated. Default: true. */\n expandOnActivate?: boolean;\n /** Collapse the field when focus leaves it while it is empty. Default: true. */\n collapseOnEmptyBlur?: boolean;\n /** Collapse an empty field on Escape and restore focus to its trigger. Default: true. */\n collapseOnEscape?: boolean;\n /** Focus the editor on expand and the trigger on Escape-collapse. Default: true. */\n manageFocus?: boolean;\n /** App-owned `expanded` signals keyed by field id; adopted instead of helper-created. */\n signals?: Readonly<Record<string, Signal<boolean>>>;\n}\n\nexport interface WireTokenSearchFieldsOptions {\n onSubmit?: (submission: TokenSearchSubmit) => void;\n /** Managed collapsible transient behavior. `true`/omitted = on with defaults; `false` = fully off. */\n collapsible?: boolean | TokenSearchCollapsibleOptions;\n}\n\n/**\n * The value returned from {@link wireTokenSearchFields}: call it (or `dispose()`) to\n * tear down. When collapsible behavior is managed, it also exposes the transient\n * `expanded` state per field id so the app can read it in render, hand in its own\n * signal, or drive it imperatively.\n */\nexport interface TokenSearchFieldsHandle {\n (): void;\n dispose(): void;\n /** The managed `expanded` signal for a field id (adopted or helper-created); undefined when unmanaged. */\n expanded(id: string): Signal<boolean> | undefined;\n /** Expand the field (and, when focus is managed, focus its editor). */\n open(id: string): void;\n /** Collapse the field (and, when focus is managed, restore focus to its trigger). */\n close(id: string): void;\n}\n\ninterface PendingTokenDeletion {\n id: string;\n offset: number;\n tokenCount: number;\n}\n\nfunction editorFromEvent(root: HTMLElement, event: Event): HTMLElement | undefined {\n const target = event.target;\n if (!(target instanceof Element)) return;\n const editor = target.closest<HTMLElement>('[data-token-search-editor]');\n return editor && root.contains(editor) ? editor : undefined;\n}\n\nfunction caretQueryOffset(editor: HTMLElement): number | undefined {\n const selection = editor.ownerDocument.getSelection();\n if (!selection || selection.rangeCount === 0) return;\n const selectionRange = selection.getRangeAt(0);\n if (!editor.contains(selectionRange.startContainer)) return;\n const prefixRange = editor.ownerDocument.createRange();\n prefixRange.selectNodeContents(editor);\n prefixRange.setEnd(selectionRange.startContainer, selectionRange.startOffset);\n const prefix = editor.ownerDocument.createElement('div');\n prefix.append(prefixRange.cloneContents());\n return readTokenSearchField(prefix).query.length;\n}\n\nfunction replacementEditor(root: HTMLElement, id: string): HTMLElement | undefined {\n return [...root.querySelectorAll<HTMLElement>('[data-token-search-editor]')]\n .find((editor) => editor.dataset.tokenSearchEditor === id);\n}\n\nfunction editorIsEmpty(editor: HTMLElement): boolean {\n const value = readTokenSearchField(editor);\n return value.query.length === 0 && value.tokens.length === 0;\n}\n\n/**\n * Wire every TokenSearchField under `root`: submit on Enter, preserve the caret across\n * controlled token deletion, and (by default) manage the collapsible field's transient\n * expand/collapse/focus. Returns a {@link TokenSearchFieldsHandle} — a disposer that also\n * exposes the managed `expanded` state per field id.\n */\nexport function wireTokenSearchFields(\n root: HTMLElement,\n { onSubmit, collapsible = true }: WireTokenSearchFieldsOptions = {},\n): TokenSearchFieldsHandle {\n const managed = collapsible !== false;\n const config = typeof collapsible === 'object' ? collapsible : {};\n const expandOnActivate = managed && (config.expandOnActivate ?? true);\n const collapseOnEmptyBlur = managed && (config.collapseOnEmptyBlur ?? true);\n const collapseOnEscape = managed && (config.collapseOnEscape ?? true);\n const manageFocus = managed && (config.manageFocus ?? true);\n const adopted = config.signals ?? {};\n const created = new Map<string, Signal<boolean>>();\n\n const signalFor = (id: string): Signal<boolean> => {\n const own = adopted[id];\n if (own) return own;\n let field = created.get(id);\n if (!field) {\n field = signal(false);\n created.set(id, field);\n }\n return field;\n };\n const setExpanded = (id: string, next: boolean) => {\n const state = signalFor(id);\n if (state.value !== next) state.value = next;\n };\n const view = () => root.ownerDocument.defaultView!;\n const focusAfterRender = (id: string, selector: string) => {\n view().requestAnimationFrame(() => {\n const target = root.querySelector<HTMLElement>(\n `[data-component=\"token-search-field\"][data-token-search-id=\"${CSS.escape(id)}\"] ${selector}`,\n );\n if (!target) return;\n if (target.matches('[data-token-search-editor]')) placeTokenSearchCaret(target);\n else target.focus();\n });\n };\n const openField = (id: string) => {\n setExpanded(id, true);\n if (manageFocus) focusAfterRender(id, '[data-token-search-editor]');\n };\n const closeField = (id: string) => {\n setExpanded(id, false);\n if (manageFocus) focusAfterRender(id, '.kui-token-search__expand');\n };\n\n const pending = new WeakMap<HTMLElement, PendingTokenDeletion>();\n const onBeforeInput = (event: Event) => {\n const inputEvent = event as InputEvent;\n const editor = editorFromEvent(root, event);\n if (!editor || !inputEvent.inputType.startsWith('delete') || editor.ownerDocument.activeElement !== editor) return;\n const field = editor.closest<HTMLElement>('[data-component=\"token-search-field\"]');\n const id = field?.dataset.tokenSearchId;\n const offset = caretQueryOffset(editor);\n if (!id || offset === undefined || field?.dataset.disabled === 'true') return;\n pending.set(editor, {\n id,\n offset,\n tokenCount: editor.querySelectorAll('[data-component=\"token-search-token\"]').length,\n });\n };\n const onInput = (event: Event) => {\n const editor = editorFromEvent(root, event);\n if (!editor) return;\n const deletion = pending.get(editor);\n pending.delete(editor);\n if (!deletion || editor.querySelectorAll('[data-component=\"token-search-token\"]').length >= deletion.tokenCount) return;\n editor.ownerDocument.defaultView!.requestAnimationFrame(() => {\n const replacement = replacementEditor(root, deletion.id);\n if (!replacement) return;\n const active = replacement.ownerDocument.activeElement;\n if (active !== editor && active !== replacement && active !== replacement.ownerDocument.body) return;\n placeTokenSearchCaret(replacement, deletion.offset);\n });\n };\n root.addEventListener('beforeinput', onBeforeInput, true);\n root.addEventListener('input', onInput, true);\n\n const disposers: Array<() => void> = [\n () => root.removeEventListener('beforeinput', onBeforeInput, true),\n () => root.removeEventListener('input', onInput, true),\n ];\n\n disposers.push(\n delegate(root, 'keydown', '[data-token-search-editor]', (event, element) => {\n const keyboardEvent = event as KeyboardEvent;\n const editor = element as HTMLElement;\n if (keyboardEvent.isComposing) return;\n const field = editor.closest<HTMLElement>('[data-component=\"token-search-field\"]');\n const id = field?.dataset.tokenSearchId;\n if (!id || field?.dataset.disabled === 'true') return;\n if (keyboardEvent.key === 'Enter') {\n keyboardEvent.preventDefault();\n onSubmit?.({ id, editor });\n return;\n }\n if (collapseOnEscape && keyboardEvent.key === 'Escape' && field?.dataset.collapsible === 'true' && editorIsEmpty(editor)) {\n keyboardEvent.preventDefault();\n closeField(id);\n }\n }),\n );\n\n if (expandOnActivate) {\n disposers.push(\n delegate(root, 'click', '.kui-token-search__expand', (_event, element) => {\n // The selector guarantees a collapsible field ancestor; only skip a disabled one.\n const field = (element as HTMLElement).closest<HTMLElement>('[data-component=\"token-search-field\"]');\n const id = field?.dataset.tokenSearchId;\n if (id && field?.dataset.disabled !== 'true') openField(id);\n }),\n );\n }\n\n if (collapseOnEmptyBlur) {\n // Pressing an in-field control (clear, trailing, token, or the trigger) must not\n // blur the editor — otherwise the transient blur would collapse the field before\n // the control's own handler runs. Keeping focus also keeps the click firing.\n disposers.push(\n delegate(root, 'mousedown', '[data-component=\"token-search-field\"][data-collapsible=\"true\"] button', (event) => {\n event.preventDefault();\n }),\n );\n disposers.push(\n delegate(root, 'focusout', '[data-token-search-editor]', (event, element) => {\n const editor = element as HTMLElement;\n const field = editor.closest<HTMLElement>('[data-component=\"token-search-field\"]');\n const id = field?.dataset.tokenSearchId;\n if (!id || field?.dataset.collapsible !== 'true' || field.dataset.disabled === 'true') return;\n const next = (event as FocusEvent).relatedTarget;\n if (next instanceof Node && field.contains(next)) return;\n if (!editorIsEmpty(editor)) return;\n view().queueMicrotask(() => {\n if (!field.contains(field.ownerDocument.activeElement)) setExpanded(id, false);\n });\n }),\n );\n }\n\n const dispose = () => {\n for (const stop of disposers.splice(0)) stop();\n };\n const handle = (() => dispose()) as TokenSearchFieldsHandle;\n handle.dispose = dispose;\n handle.expanded = (id) => (managed ? signalFor(id) : undefined);\n handle.open = (id) => {\n if (managed) openField(id);\n };\n handle.close = (id) => {\n if (managed) closeField(id);\n };\n return handle;\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { ToolbarControlGroup } from './chunk-MFKMHK5W.js';
2
+ import { Toolbar } from './chunk-DAFFDO33.js';
3
+ import { ToolbarText } from './chunk-24Z2XE6C.js';
4
+ import { jsxs, jsx, Fragment } from 'kerfjs/jsx-runtime';
5
+
6
+ function PanelHeader({ title, titleId, summary, summaryId, icon, iconClassName = "", actions }) {
7
+ const identity = /* @__PURE__ */ jsxs(Fragment, { children: [
8
+ icon && /* @__PURE__ */ jsx(ToolbarControlGroup, { single: true, className: `kui-panel-header__icon ${iconClassName}`.trim(), children: icon }),
9
+ /* @__PURE__ */ jsx(ToolbarText, { text: title, size: "xlarge", id: titleId, className: "kui-panel-header__title" })
10
+ ] });
11
+ return /* @__PURE__ */ jsxs("div", { class: "kui-panel-header", "data-component": "panel-header", "data-has-icon": String(Boolean(icon)), "data-has-actions": String(Boolean(actions)), "data-has-summary": String(Boolean(summary)), children: [
12
+ /* @__PURE__ */ jsx(Toolbar, { leading: identity, trailing: actions, divider: false }),
13
+ summary && /* @__PURE__ */ jsx("p", { class: "kui-panel-header__summary", id: summaryId, children: summary })
14
+ ] });
15
+ }
16
+
17
+ export { PanelHeader };
18
+
19
+ //# sourceMappingURL=chunk-KZKSBUKC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/panel-header.tsx"],"names":[],"mappings":";;;;;AA4BO,SAAS,WAAA,CAAY,EAAE,KAAA,EAAO,OAAA,EAAS,OAAA,EAAS,WAAW,IAAA,EAAM,aAAA,GAAgB,EAAA,EAAI,OAAA,EAAQ,EAAqB;AACvH,EAAA,MAAM,2BAAW,IAAA,CAAA,QAAA,EAAA,EACd,QAAA,EAAA;AAAA,IAAA,IAAA,oBAAQ,GAAA,CAAC,mBAAA,EAAA,EAAoB,MAAA,EAAM,IAAA,EAAC,SAAA,EAAW,0BAA0B,aAAa,CAAA,CAAA,CAAG,IAAA,EAAK,EAAI,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,oBACxG,GAAA,CAAC,eAAY,IAAA,EAAM,KAAA,EAAO,MAAK,QAAA,EAAS,EAAA,EAAI,OAAA,EAAS,SAAA,EAAU,yBAAA,EAA0B;AAAA,GAAA,EAC3F,CAAA;AAEA,EAAA,uBAAO,IAAA,CAAC,SAAI,KAAA,EAAM,kBAAA,EAAmB,kBAAe,cAAA,EAAe,eAAA,EAAe,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,GAAG,kBAAA,EAAkB,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAC,CAAA,EAAG,oBAAkB,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAC,CAAA,EAC5L,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,WAAQ,OAAA,EAAS,QAAA,EAAU,QAAA,EAAU,OAAA,EAAS,SAAS,KAAA,EAAO,CAAA;AAAA,IAC9D,2BAAW,GAAA,CAAC,GAAA,EAAA,EAAE,OAAM,2BAAA,EAA4B,EAAA,EAAI,WAAY,QAAA,EAAA,OAAA,EAAQ;AAAA,GAAA,EAC3E,CAAA;AACF","file":"chunk-KZKSBUKC.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport { Toolbar } from './toolbar.js';\nimport { ToolbarControlGroup } from './toolbar-control-group.js';\nimport { ToolbarText } from './toolbar-text.js';\n\nexport interface PanelHeaderProps {\n title: string;\n titleId: string;\n summary?: string;\n summaryId?: string;\n icon?: SafeHtml;\n iconClassName?: string;\n actions?: SafeHtml;\n}\n\n/**\n * The heading of a panel, dialog, or page: a plain `Toolbar` whose leading zone\n * holds an optional icon control group and the title as extra-large `ToolbarText`,\n * whose trailing zone holds the app's action controls, and with an optional\n * subtitle on its own row, left-aligned with the title.\n *\n * PanelHeader overrides no Toolbar styles — it is just a Toolbar with an xl title.\n * The only styling it adds is the icon group's fill/border color and the subtitle.\n * When no icon is provided, the icon group is omitted entirely. The `actions` slot\n * is passed straight into the toolbar's trailing zone; the app supplies whatever\n * trailing controls it needs (typically a `ToolbarControlGroup`).\n */\nexport function PanelHeader({ title, titleId, summary, summaryId, icon, iconClassName = '', actions }: PanelHeaderProps) {\n const identity = <>\n {icon && <ToolbarControlGroup single className={`kui-panel-header__icon ${iconClassName}`.trim()}>{icon}</ToolbarControlGroup>}\n <ToolbarText text={title} size=\"xlarge\" id={titleId} className=\"kui-panel-header__title\" />\n </>;\n\n return <div class=\"kui-panel-header\" data-component=\"panel-header\" data-has-icon={String(Boolean(icon))} data-has-actions={String(Boolean(actions))} data-has-summary={String(Boolean(summary))}>\n <Toolbar leading={identity} trailing={actions} divider={false} />\n {summary && <p class=\"kui-panel-header__summary\" id={summaryId}>{summary}</p>}\n </div>;\n}\n"]}
@@ -0,0 +1,23 @@
1
+ import { ToolbarText } from './chunk-24Z2XE6C.js';
2
+ import { jsxs, jsx } from 'kerfjs/jsx-runtime';
3
+
4
+ function NavStack({ id, label, views, backLabel = "Back", hideToolbar = false, bottomToolbar, className = "" }) {
5
+ const topIndex = views.length - 1;
6
+ const top = views[topIndex];
7
+ const canPop = views.length > 1;
8
+ return /* @__PURE__ */ jsxs("section", { class: `kui-nav-stack ${className}`.trim(), id, "data-component": "nav-stack", "data-nav-stack-id": id, "data-depth": String(views.length), "aria-label": label, children: [
9
+ !hideToolbar && /* @__PURE__ */ jsxs("header", { class: "kui-nav-stack__chrome", "data-nav-stack-chrome": true, children: [
10
+ /* @__PURE__ */ jsxs("div", { class: "kui-nav-stack__lead", children: [
11
+ canPop && /* @__PURE__ */ jsx("button", { type: "button", class: "kui-nav-stack__back", "data-nav-back": true, "aria-label": backLabel, children: /* @__PURE__ */ jsx("svg", { class: "kui-nav-stack__back-icon", viewBox: "0 0 24 24", width: "18", height: "18", "aria-hidden": "true", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("path", { d: "m15 18-6-6 6-6" }) }) }),
12
+ /* @__PURE__ */ jsx(ToolbarText, { text: top?.title ?? "", size: "large", className: "kui-nav-stack__title" })
13
+ ] }),
14
+ top?.toolbar && /* @__PURE__ */ jsx("div", { class: "kui-nav-stack__actions", children: top.toolbar })
15
+ ] }),
16
+ /* @__PURE__ */ jsx("div", { class: "kui-nav-stack__viewport", "data-nav-stack-viewport": true, children: views.map((view, index) => /* @__PURE__ */ jsx("article", { class: "kui-nav-stack__view", "data-key": view.key, "data-nav-key": view.key, "data-nav-active": String(index === topIndex), "aria-hidden": String(index !== topIndex), children: view.content })) }),
17
+ bottomToolbar && /* @__PURE__ */ jsx("footer", { class: "kui-nav-stack__bottom", "data-nav-stack-bottom": true, children: bottomToolbar })
18
+ ] });
19
+ }
20
+
21
+ export { NavStack };
22
+
23
+ //# sourceMappingURL=chunk-RBTVBGRD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/nav-stack.tsx"],"names":[],"mappings":";;;AAuCO,SAAS,QAAA,CAAS,EAAE,EAAA,EAAI,KAAA,EAAO,KAAA,EAAO,SAAA,GAAY,MAAA,EAAQ,WAAA,GAAc,KAAA,EAAO,aAAA,EAAe,SAAA,GAAY,EAAA,EAAG,EAAkB;AACpI,EAAA,MAAM,QAAA,GAAW,MAAM,MAAA,GAAS,CAAA;AAChC,EAAA,MAAM,GAAA,GAAM,MAAM,QAAQ,CAAA;AAC1B,EAAA,MAAM,MAAA,GAAS,MAAM,MAAA,GAAS,CAAA;AAC9B,EAAA,4BAAQ,SAAA,EAAA,EAAQ,KAAA,EAAO,iBAAiB,SAAS,CAAA,CAAA,CAAG,MAAK,EAAG,EAAA,EAAQ,kBAAe,WAAA,EAAY,mBAAA,EAAmB,IAAI,YAAA,EAAY,MAAA,CAAO,MAAM,MAAM,CAAA,EAAG,cAAY,KAAA,EACjK,QAAA,EAAA;AAAA,IAAA,CAAC,+BAAe,IAAA,CAAC,QAAA,EAAA,EAAO,KAAA,EAAM,uBAAA,EAAwB,yBAAqB,IAAA,EAC1E,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,OAAM,qBAAA,EACR,QAAA,EAAA;AAAA,QAAA,MAAA,wBAAW,QAAA,EAAA,EAAO,IAAA,EAAK,QAAA,EAAS,KAAA,EAAM,uBAAsB,eAAA,EAAa,IAAA,EAAC,YAAA,EAAY,SAAA,EACrF,8BAAC,KAAA,EAAA,EAAI,KAAA,EAAM,0BAAA,EAA2B,OAAA,EAAQ,aAAY,KAAA,EAAM,IAAA,EAAK,MAAA,EAAO,IAAA,EAAK,eAAY,MAAA,EAAO,IAAA,EAAK,MAAA,EAAO,MAAA,EAAO,gBAAe,cAAA,EAAa,GAAA,EAAI,gBAAA,EAAe,OAAA,EAAQ,mBAAgB,OAAA,EAAQ,QAAA,kBAAA,GAAA,CAAC,UAAK,CAAA,EAAE,gBAAA,EAAiB,GAAE,CAAA,EACnO,CAAA;AAAA,wBACA,GAAA,CAAC,eAAY,IAAA,EAAM,GAAA,EAAK,SAAS,EAAA,EAAI,IAAA,EAAK,OAAA,EAAQ,SAAA,EAAU,sBAAA,EAAuB;AAAA,OAAA,EACrF,CAAA;AAAA,MACC,KAAK,OAAA,oBAAW,GAAA,CAAC,SAAI,KAAA,EAAM,wBAAA,EAA0B,cAAI,OAAA,EAAQ;AAAA,KAAA,EACpE,CAAA;AAAA,oBACA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,yBAAA,EAA0B,2BAAuB,IAAA,EACzD,QAAA,EAAA,KAAA,CAAM,GAAA,CAAI,CAAC,MAAM,KAAA,qBAAU,GAAA,CAAC,SAAA,EAAA,EAAQ,KAAA,EAAM,uBAAsB,UAAA,EAAU,IAAA,CAAK,GAAA,EAAK,cAAA,EAAc,IAAA,CAAK,GAAA,EAAK,iBAAA,EAAiB,MAAA,CAAO,UAAU,QAAQ,CAAA,EAAG,aAAA,EAAa,MAAA,CAAO,UAAU,QAAQ,CAAA,EAAI,QAAA,EAAA,IAAA,CAAK,OAAA,EAAQ,CAAU,CAAA,EAC7N,CAAA;AAAA,IACC,iCAAiB,GAAA,CAAC,QAAA,EAAA,EAAO,OAAM,uBAAA,EAAwB,uBAAA,EAAqB,MAAE,QAAA,EAAA,aAAA,EAAc;AAAA,GAAA,EAC/F,CAAA;AACF","file":"chunk-RBTVBGRD.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport { ToolbarText } from './toolbar-text.js';\n\n/**\n * One entry in a {@link NavStack}. The app owns the stack as an array (usually a\n * signal); `NavStack` renders it and `wireNavStack` animates the transitions.\n */\nexport interface NavStackView {\n /** Stable identity for keyed reconcile and transition direction. */\n key: string;\n content: SafeHtml;\n /** Title shown in the top toolbar for this view. */\n title?: string;\n /** Trailing actions for this view's top toolbar. */\n toolbar?: SafeHtml;\n}\n\nexport interface NavStackProps {\n id: string;\n /** Accessible name for the stack region. */\n label: string;\n /** The stack, root first; the last entry is the active top view. */\n views: NavStackView[];\n /** Accessible label for the back control (default \"Back\"). */\n backLabel?: string;\n /** Hide the top toolbar entirely (rare — a fully custom-chrome view). */\n hideToolbar?: boolean;\n /** Optional persistent bottom toolbar. */\n bottomToolbar?: SafeHtml;\n className?: string;\n}\n\n/**\n * A navigation stack (iOS-style push/pop). Renders every entry stacked, the last\n * one active; `@kerfjs/ui/wire-nav-stack`'s `wireNavStack` slides the content and\n * cross-fades the chrome across a change. A single-pane layout is a `NavStack`\n * with one entry. See `docs/23-app-layouts.md` §3.1.\n */\nexport function NavStack({ id, label, views, backLabel = 'Back', hideToolbar = false, bottomToolbar, className = '' }: NavStackProps) {\n const topIndex = views.length - 1;\n const top = views[topIndex];\n const canPop = views.length > 1;\n return <section class={`kui-nav-stack ${className}`.trim()} id={id} data-component=\"nav-stack\" data-nav-stack-id={id} data-depth={String(views.length)} aria-label={label}>\n {!hideToolbar && <header class=\"kui-nav-stack__chrome\" data-nav-stack-chrome>\n <div class=\"kui-nav-stack__lead\">\n {canPop && <button type=\"button\" class=\"kui-nav-stack__back\" data-nav-back aria-label={backLabel}>\n <svg class=\"kui-nav-stack__back-icon\" viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" aria-hidden=\"true\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"m15 18-6-6 6-6\" /></svg>\n </button>}\n <ToolbarText text={top?.title ?? ''} size=\"large\" className=\"kui-nav-stack__title\" />\n </div>\n {top?.toolbar && <div class=\"kui-nav-stack__actions\">{top.toolbar}</div>}\n </header>}\n <div class=\"kui-nav-stack__viewport\" data-nav-stack-viewport>\n {views.map((view, index) => <article class=\"kui-nav-stack__view\" data-key={view.key} data-nav-key={view.key} data-nav-active={String(index === topIndex)} aria-hidden={String(index !== topIndex)}>{view.content}</article>)}\n </div>\n {bottomToolbar && <footer class=\"kui-nav-stack__bottom\" data-nav-stack-bottom>{bottomToolbar}</footer>}\n </section>;\n}\n"]}
@@ -0,0 +1,62 @@
1
+ import { ReadonlySignal } from 'kerfjs';
2
+
3
+ /**
4
+ * Reactive device-class detection for `@kerfjs/ui` (see `docs/23-app-layouts.md`
5
+ * §2). `deviceClass()` returns a `ReadonlySignal<DeviceClass>` describing the
6
+ * current viewport as a size bucket × orientation × viewport-segment count, so a
7
+ * layout can pick its presentation reactively instead of hand-wiring `matchMedia`.
8
+ *
9
+ * One shared viewport source backs every reader; the pure `classifyViewport`
10
+ * core is DOM-free and directly unit-tested.
11
+ */
12
+ type DeviceSize = 'xs-mobile' | 'mobile' | 'tablet' | 'desktop' | 'xl-desktop';
13
+ type DeviceOrientation = 'portrait' | 'landscape';
14
+ /** Minimum widths (px) at which each larger bucket begins. `xs-mobile` is 0. */
15
+ interface DeviceBreakpoints {
16
+ mobile: number;
17
+ tablet: number;
18
+ desktop: number;
19
+ 'xl-desktop': number;
20
+ }
21
+ interface DeviceClass {
22
+ size: DeviceSize;
23
+ orientation: DeviceOrientation;
24
+ /** Horizontal viewport segments (foldables / dual-screen); 1 on ordinary devices. */
25
+ segments: number;
26
+ /** Vertical viewport segments; 1 on ordinary devices. */
27
+ verticalSegments: number;
28
+ /** Small phones — `xs-mobile` or `mobile`. */
29
+ handset: boolean;
30
+ /** "One pane at a time" — a handset or a portrait tablet. */
31
+ compact: boolean;
32
+ /** True when the current size is `size` or larger, e.g. `atLeast('tablet')`. */
33
+ atLeast(size: DeviceSize): boolean;
34
+ }
35
+ /** A raw viewport snapshot, before breakpoints are applied. */
36
+ interface Viewport {
37
+ width: number;
38
+ height: number;
39
+ segments: number;
40
+ verticalSegments: number;
41
+ }
42
+ interface DeviceClassOptions {
43
+ /** Override any of the default bucket thresholds. */
44
+ breakpoints?: Partial<DeviceBreakpoints>;
45
+ /** The viewport assumed when there is no DOM (SSR). Defaults to 1024×768, one segment. */
46
+ ssr?: Partial<Viewport>;
47
+ }
48
+ declare const DEFAULT_BREAKPOINTS: DeviceBreakpoints;
49
+ /**
50
+ * Classify a raw viewport into a {@link DeviceClass}. Pure and DOM-free — the
51
+ * single source of truth for the bucketing rules.
52
+ */
53
+ declare function classifyViewport(width: number, orientation: DeviceOrientation, segments?: number, verticalSegments?: number, breakpoints?: DeviceBreakpoints): DeviceClass;
54
+ /**
55
+ * A reactive signal of the current {@link DeviceClass}. Reading it inside an
56
+ * `effect`/`computed` re-runs when the viewport crosses a breakpoint, rotates,
57
+ * or changes its segment count. Without a DOM it resolves to `options.ssr`
58
+ * (default 1024×768, landscape, one segment).
59
+ */
60
+ declare function deviceClass(options?: DeviceClassOptions): ReadonlySignal<DeviceClass>;
61
+
62
+ export { DEFAULT_BREAKPOINTS, type DeviceBreakpoints, type DeviceClass, type DeviceClassOptions, type DeviceOrientation, type DeviceSize, type Viewport, classifyViewport, deviceClass };
@@ -0,0 +1,75 @@
1
+ import { computed, signal } from 'kerfjs';
2
+
3
+ // src/device-class.ts
4
+ var DEFAULT_BREAKPOINTS = {
5
+ mobile: 360,
6
+ tablet: 720,
7
+ desktop: 1024,
8
+ "xl-desktop": 1440
9
+ };
10
+ var SIZE_ORDER = ["xs-mobile", "mobile", "tablet", "desktop", "xl-desktop"];
11
+ var SSR_VIEWPORT = { width: 1024, height: 768, segments: 1, verticalSegments: 1 };
12
+ function classifyViewport(width, orientation, segments = 1, verticalSegments = 1, breakpoints = DEFAULT_BREAKPOINTS) {
13
+ const size = width >= breakpoints["xl-desktop"] ? "xl-desktop" : width >= breakpoints.desktop ? "desktop" : width >= breakpoints.tablet ? "tablet" : width >= breakpoints.mobile ? "mobile" : "xs-mobile";
14
+ const rank = SIZE_ORDER.indexOf(size);
15
+ return {
16
+ size,
17
+ orientation,
18
+ segments,
19
+ verticalSegments,
20
+ handset: rank <= 1,
21
+ compact: rank <= 1 || size === "tablet" && orientation === "portrait",
22
+ atLeast: (target) => rank >= SIZE_ORDER.indexOf(target)
23
+ };
24
+ }
25
+ function countSegments(axis, view) {
26
+ const mq = view.matchMedia;
27
+ if (typeof mq !== "function") return 1;
28
+ for (let n = 4; n >= 2; n--) {
29
+ if (mq.call(view, `(${axis}-viewport-segments: ${n})`).matches) return n;
30
+ }
31
+ return 1;
32
+ }
33
+ function readViewport(view) {
34
+ return {
35
+ width: view.innerWidth,
36
+ height: view.innerHeight,
37
+ segments: countSegments("horizontal", view),
38
+ verticalSegments: countSegments("vertical", view)
39
+ };
40
+ }
41
+ function browserWindow() {
42
+ return typeof window === "undefined" ? void 0 : window;
43
+ }
44
+ var sharedSource;
45
+ function viewportSource(view) {
46
+ if (sharedSource) return sharedSource;
47
+ const source = signal(readViewport(view));
48
+ sharedSource = source;
49
+ const update = () => {
50
+ source.value = readViewport(view);
51
+ };
52
+ view.addEventListener("resize", update);
53
+ view.addEventListener("orientationchange", update);
54
+ return source;
55
+ }
56
+ function orientationOf(view) {
57
+ return view.height > view.width ? "portrait" : "landscape";
58
+ }
59
+ function deviceClass(options = {}) {
60
+ const breakpoints = { ...DEFAULT_BREAKPOINTS, ...options.breakpoints };
61
+ const view = browserWindow();
62
+ if (!view) {
63
+ const snapshot = { ...SSR_VIEWPORT, ...options.ssr };
64
+ return computed(() => classifyViewport(snapshot.width, orientationOf(snapshot), snapshot.segments, snapshot.verticalSegments, breakpoints));
65
+ }
66
+ const source = viewportSource(view);
67
+ return computed(() => {
68
+ const v = source.value;
69
+ return classifyViewport(v.width, orientationOf(v), v.segments, v.verticalSegments, breakpoints);
70
+ });
71
+ }
72
+
73
+ export { DEFAULT_BREAKPOINTS, classifyViewport, deviceClass };
74
+
75
+ //# sourceMappingURL=device-class.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/device-class.ts"],"names":[],"mappings":"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","file":"device-class.js","sourcesContent":["import { computed, type ReadonlySignal, signal } from 'kerfjs';\n\n/**\n * Reactive device-class detection for `@kerfjs/ui` (see `docs/23-app-layouts.md`\n * §2). `deviceClass()` returns a `ReadonlySignal<DeviceClass>` describing the\n * current viewport as a size bucket × orientation × viewport-segment count, so a\n * layout can pick its presentation reactively instead of hand-wiring `matchMedia`.\n *\n * One shared viewport source backs every reader; the pure `classifyViewport`\n * core is DOM-free and directly unit-tested.\n */\n\nexport type DeviceSize = 'xs-mobile' | 'mobile' | 'tablet' | 'desktop' | 'xl-desktop';\nexport type DeviceOrientation = 'portrait' | 'landscape';\n\n/** Minimum widths (px) at which each larger bucket begins. `xs-mobile` is 0. */\nexport interface DeviceBreakpoints {\n mobile: number;\n tablet: number;\n desktop: number;\n 'xl-desktop': number;\n}\n\nexport interface DeviceClass {\n size: DeviceSize;\n orientation: DeviceOrientation;\n /** Horizontal viewport segments (foldables / dual-screen); 1 on ordinary devices. */\n segments: number;\n /** Vertical viewport segments; 1 on ordinary devices. */\n verticalSegments: number;\n /** Small phones — `xs-mobile` or `mobile`. */\n handset: boolean;\n /** \"One pane at a time\" — a handset or a portrait tablet. */\n compact: boolean;\n /** True when the current size is `size` or larger, e.g. `atLeast('tablet')`. */\n atLeast(size: DeviceSize): boolean;\n}\n\n/** A raw viewport snapshot, before breakpoints are applied. */\nexport interface Viewport {\n width: number;\n height: number;\n segments: number;\n verticalSegments: number;\n}\n\nexport interface DeviceClassOptions {\n /** Override any of the default bucket thresholds. */\n breakpoints?: Partial<DeviceBreakpoints>;\n /** The viewport assumed when there is no DOM (SSR). Defaults to 1024×768, one segment. */\n ssr?: Partial<Viewport>;\n}\n\nexport const DEFAULT_BREAKPOINTS: DeviceBreakpoints = {\n mobile: 360,\n tablet: 720,\n desktop: 1024,\n 'xl-desktop': 1440,\n};\n\nconst SIZE_ORDER: readonly DeviceSize[] = ['xs-mobile', 'mobile', 'tablet', 'desktop', 'xl-desktop'];\n\nconst SSR_VIEWPORT: Viewport = { width: 1024, height: 768, segments: 1, verticalSegments: 1 };\n\n/**\n * Classify a raw viewport into a {@link DeviceClass}. Pure and DOM-free — the\n * single source of truth for the bucketing rules.\n */\nexport function classifyViewport(\n width: number,\n orientation: DeviceOrientation,\n segments = 1,\n verticalSegments = 1,\n breakpoints: DeviceBreakpoints = DEFAULT_BREAKPOINTS,\n): DeviceClass {\n const size: DeviceSize =\n width >= breakpoints['xl-desktop']\n ? 'xl-desktop'\n : width >= breakpoints.desktop\n ? 'desktop'\n : width >= breakpoints.tablet\n ? 'tablet'\n : width >= breakpoints.mobile\n ? 'mobile'\n : 'xs-mobile';\n const rank = SIZE_ORDER.indexOf(size);\n return {\n size,\n orientation,\n segments,\n verticalSegments,\n handset: rank <= 1,\n compact: rank <= 1 || (size === 'tablet' && orientation === 'portrait'),\n atLeast: (target) => rank >= SIZE_ORDER.indexOf(target),\n };\n}\n\n/** Count viewport segments along one axis, feature-detecting the media query. */\nfunction countSegments(axis: 'horizontal' | 'vertical', view: Window): number {\n const mq = view.matchMedia;\n if (typeof mq !== 'function') return 1;\n for (let n = 4; n >= 2; n--) {\n if (mq.call(view, `(${axis}-viewport-segments: ${n})`).matches) return n;\n }\n return 1;\n}\n\n/** Read the current viewport from a browser window. */\nfunction readViewport(view: Window): Viewport {\n return {\n width: view.innerWidth,\n height: view.innerHeight,\n segments: countSegments('horizontal', view),\n verticalSegments: countSegments('vertical', view),\n };\n}\n\n/** The browser window, or `undefined` when running without a DOM (SSR). */\nfunction browserWindow(): Window | undefined {\n return typeof window === 'undefined' ? undefined : window;\n}\n\nlet sharedSource: ReturnType<typeof signal<Viewport>> | undefined;\n\n/**\n * The shared reactive viewport source. Installed lazily on first read and kept\n * for the life of the page — there is only ever one viewport, so a single\n * listener set backs every `deviceClass()` reader.\n */\nfunction viewportSource(view: Window): ReturnType<typeof signal<Viewport>> {\n if (sharedSource) return sharedSource;\n const source = signal(readViewport(view));\n sharedSource = source;\n const update = (): void => {\n source.value = readViewport(view);\n };\n view.addEventListener('resize', update);\n view.addEventListener('orientationchange', update);\n return source;\n}\n\nfunction orientationOf(view: Viewport): DeviceOrientation {\n return view.height > view.width ? 'portrait' : 'landscape';\n}\n\n/**\n * A reactive signal of the current {@link DeviceClass}. Reading it inside an\n * `effect`/`computed` re-runs when the viewport crosses a breakpoint, rotates,\n * or changes its segment count. Without a DOM it resolves to `options.ssr`\n * (default 1024×768, landscape, one segment).\n */\nexport function deviceClass(options: DeviceClassOptions = {}): ReadonlySignal<DeviceClass> {\n const breakpoints: DeviceBreakpoints = { ...DEFAULT_BREAKPOINTS, ...options.breakpoints };\n const view = browserWindow();\n if (!view) {\n const snapshot: Viewport = { ...SSR_VIEWPORT, ...options.ssr };\n return computed(() => classifyViewport(snapshot.width, orientationOf(snapshot), snapshot.segments, snapshot.verticalSegments, breakpoints));\n }\n const source = viewportSource(view);\n return computed(() => {\n const v = source.value;\n return classifyViewport(v.width, orientationOf(v), v.segments, v.verticalSegments, breakpoints);\n });\n}\n"]}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  export { AppTab, AppTabProps } from './app-tab.js';
2
- export { DialogHeader, DialogHeaderProps } from './dialog-header.js';
3
2
  export { DisclosureArrow, DisclosureArrowProps, DisclosureDirection } from './disclosure-arrow.js';
4
3
  export { EmptyState, EmptyStateProps } from './empty-state.js';
5
4
  export { LoadingSpinner, LoadingSpinnerProps } from './loading-spinner.js';
@@ -7,7 +6,7 @@ export { LucideIcon, LucideIconProps, LucideNode } from './lucide-icon.js';
7
6
  export { MenuActionRow, MenuActionRowProps } from './menu-action-row.js';
8
7
  export { MenuHeader, MenuHeaderProps } from './menu-header.js';
9
8
  export { MenuItem, MenuItemProps } from './menu-item.js';
10
- export { PageHeader, PageHeaderProps } from './page-header.js';
9
+ export { PanelHeader, PanelHeaderProps } from './panel-header.js';
11
10
  export { ResizableRegion, ResizableRegionAxis, ResizableRegionEdge, ResizableRegionProps, clampRegionSize, resizeRegionFromPointer } from './resizable-region.js';
12
11
  export { SegmentedControl, SegmentedControlAppearance, SegmentedControlChoice, SegmentedControlLayout, SegmentedControlProps, SegmentedControlShape, SegmentedControlSize } from './segmented-control.js';
13
12
  export { Select, SelectChoice, SelectProps } from './select.js';
@@ -20,6 +19,6 @@ export { ToolbarText, ToolbarTextProps, ToolbarTextSize } from './toolbar-text.j
20
19
  export { ValueTable, ValueTableProps, ValueTableRow, ValueTableRowProps } from './value-table.js';
21
20
  export { ResizeCommit, WireResizableRegionsOptions, wireResizableRegions } from './wire-resizable-regions.js';
22
21
  export { TabDropPosition, TabReorder, TabReorderSource, WireTabBarsOptions, reorderTabs, wireTabBars } from './wire-tab-bars.js';
23
- export { TokenSearchSubmit, WireTokenSearchFieldsOptions, wireTokenSearchFields } from './wire-token-search-fields.js';
22
+ export { TokenSearchCollapsibleOptions, TokenSearchFieldsHandle, TokenSearchSubmit, WireTokenSearchFieldsOptions, wireTokenSearchFields } from './wire-token-search-fields.js';
24
23
  import 'kerfjs';
25
24
  import 'lucide';
package/dist/index.js CHANGED
@@ -1,21 +1,20 @@
1
+ export { ValueTable, ValueTableRow } from './chunk-R6JHHBKQ.js';
2
+ export { wireResizableRegions } from './chunk-AQNKJRFH.js';
1
3
  export { reorderTabs, wireTabBars } from './chunk-MQIZAD4A.js';
2
- export { wireTokenSearchFields } from './chunk-H5AGGVU5.js';
4
+ export { wireTokenSearchFields } from './chunk-2RJBFNB6.js';
5
+ export { StateBanner } from './chunk-VHW5YNIB.js';
3
6
  export { TabBar } from './chunk-MBR3TYNL.js';
4
7
  export { TokenSearchField, placeTokenSearchCaret, readTokenSearchField } from './chunk-7LDCTU7D.js';
5
- export { ToolbarText } from './chunk-GY5WH7TO.js';
6
- export { ValueTable, ValueTableRow } from './chunk-R6JHHBKQ.js';
7
- export { wireResizableRegions } from './chunk-AQNKJRFH.js';
8
8
  export { MenuHeader } from './chunk-NPPD2A6O.js';
9
9
  export { MenuItem } from './chunk-S3TPM2LP.js';
10
- export { PageHeader } from './chunk-2PES33HS.js';
10
+ export { PanelHeader } from './chunk-KZKSBUKC.js';
11
+ export { ToolbarControlGroup } from './chunk-MFKMHK5W.js';
12
+ export { Toolbar } from './chunk-DAFFDO33.js';
13
+ export { ToolbarText } from './chunk-24Z2XE6C.js';
11
14
  export { ResizableRegion, clampRegionSize, resizeRegionFromPointer } from './chunk-M5JRI6MN.js';
12
15
  export { SegmentedControl } from './chunk-TRNBHPNW.js';
13
16
  export { Select } from './chunk-CTQKI2XL.js';
14
- export { StateBanner } from './chunk-VHW5YNIB.js';
15
17
  export { AppTab } from './chunk-XZXRLY5Q.js';
16
- export { DialogHeader } from './chunk-K3G72I6D.js';
17
- export { ToolbarControlGroup } from './chunk-MFKMHK5W.js';
18
- export { Toolbar } from './chunk-DAFFDO33.js';
19
18
  export { DisclosureArrow } from './chunk-56RJXPHG.js';
20
19
  export { EmptyState } from './chunk-WVPYVDXY.js';
21
20
  export { LoadingSpinner } from './chunk-55DATV3A.js';
@@ -0,0 +1,38 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ /**
4
+ * One entry in a {@link NavStack}. The app owns the stack as an array (usually a
5
+ * signal); `NavStack` renders it and `wireNavStack` animates the transitions.
6
+ */
7
+ interface NavStackView {
8
+ /** Stable identity for keyed reconcile and transition direction. */
9
+ key: string;
10
+ content: SafeHtml;
11
+ /** Title shown in the top toolbar for this view. */
12
+ title?: string;
13
+ /** Trailing actions for this view's top toolbar. */
14
+ toolbar?: SafeHtml;
15
+ }
16
+ interface NavStackProps {
17
+ id: string;
18
+ /** Accessible name for the stack region. */
19
+ label: string;
20
+ /** The stack, root first; the last entry is the active top view. */
21
+ views: NavStackView[];
22
+ /** Accessible label for the back control (default "Back"). */
23
+ backLabel?: string;
24
+ /** Hide the top toolbar entirely (rare — a fully custom-chrome view). */
25
+ hideToolbar?: boolean;
26
+ /** Optional persistent bottom toolbar. */
27
+ bottomToolbar?: SafeHtml;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * A navigation stack (iOS-style push/pop). Renders every entry stacked, the last
32
+ * one active; `@kerfjs/ui/wire-nav-stack`'s `wireNavStack` slides the content and
33
+ * cross-fades the chrome across a change. A single-pane layout is a `NavStack`
34
+ * with one entry. See `docs/23-app-layouts.md` §3.1.
35
+ */
36
+ declare function NavStack({ id, label, views, backLabel, hideToolbar, bottomToolbar, className }: NavStackProps): SafeHtml;
37
+
38
+ export { NavStack, type NavStackProps, type NavStackView };
@@ -0,0 +1,4 @@
1
+ export { NavStack } from './chunk-RBTVBGRD.js';
2
+
3
+
4
+ //# sourceMappingURL=nav-stack.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"nav-stack.js"}
@@ -0,0 +1,26 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ interface PanelHeaderProps {
4
+ title: string;
5
+ titleId: string;
6
+ summary?: string;
7
+ summaryId?: string;
8
+ icon?: SafeHtml;
9
+ iconClassName?: string;
10
+ actions?: SafeHtml;
11
+ }
12
+ /**
13
+ * The heading of a panel, dialog, or page: a plain `Toolbar` whose leading zone
14
+ * holds an optional icon control group and the title as extra-large `ToolbarText`,
15
+ * whose trailing zone holds the app's action controls, and with an optional
16
+ * subtitle on its own row, left-aligned with the title.
17
+ *
18
+ * PanelHeader overrides no Toolbar styles — it is just a Toolbar with an xl title.
19
+ * The only styling it adds is the icon group's fill/border color and the subtitle.
20
+ * When no icon is provided, the icon group is omitted entirely. The `actions` slot
21
+ * is passed straight into the toolbar's trailing zone; the app supplies whatever
22
+ * trailing controls it needs (typically a `ToolbarControlGroup`).
23
+ */
24
+ declare function PanelHeader({ title, titleId, summary, summaryId, icon, iconClassName, actions }: PanelHeaderProps): SafeHtml;
25
+
26
+ export { PanelHeader, type PanelHeaderProps };
@@ -0,0 +1,6 @@
1
+ export { PanelHeader } from './chunk-KZKSBUKC.js';
2
+
3
+
4
+
5
+
6
+ //# sourceMappingURL=panel-header.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"panel-header.js"}
@@ -0,0 +1,42 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ interface SplitViewResizable {
4
+ size: number;
5
+ min: number;
6
+ max: number;
7
+ }
8
+ interface SplitViewProps {
9
+ id: string;
10
+ label: string;
11
+ /** The list (primary) pane. */
12
+ list: SafeHtml;
13
+ /** The detail (secondary) pane. */
14
+ detail: SafeHtml;
15
+ /**
16
+ * Compact ("one pane at a time") classes — a handset or portrait tablet.
17
+ * Derive from `deviceClass().value.compact`. When true the split collapses to
18
+ * a `NavStack`: the list is the root and the detail is pushed over it.
19
+ */
20
+ compact?: boolean;
21
+ /** In compact mode, whether the detail is currently pushed over the list. */
22
+ detailActive?: boolean;
23
+ /** Title/label for the list (compact NavStack root + region label). */
24
+ listTitle?: string;
25
+ /** Title/label for the detail (compact NavStack pushed view + region label). */
26
+ detailTitle?: string;
27
+ /** Back label for the compact NavStack (default "Back"). */
28
+ backLabel?: string;
29
+ /** A resizable separator on roomy classes (min/max px). Omit for a fixed split. */
30
+ resizable?: SplitViewResizable;
31
+ className?: string;
32
+ }
33
+ /**
34
+ * A list-detail (master-detail) split. On roomy classes it shows both panes side
35
+ * by side with an optional resizable separator; on compact classes it collapses
36
+ * to a `NavStack` (list → detail). See `docs/23-app-layouts.md` §3.2. Compose the
37
+ * resizable wiring with `wireResizableRegions` and the compact back with
38
+ * `wireNavStack`.
39
+ */
40
+ declare function SplitView({ id, label, list, detail, compact, detailActive, listTitle, detailTitle, backLabel, resizable, className }: SplitViewProps): SafeHtml;
41
+
42
+ export { SplitView, type SplitViewProps, type SplitViewResizable };