@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.
- package/README.md +12 -9
- package/ai/component-catalog.json +314 -172
- package/ai/public-api-signatures-v1.md +333 -26
- package/ai/skill.md +17 -9
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/browser/{dialog-header.js → panel-header.js} +3 -2
- package/dist/{chunk-GY5WH7TO.js → chunk-24Z2XE6C.js} +3 -3
- package/dist/chunk-24Z2XE6C.js.map +1 -0
- package/dist/chunk-2RJBFNB6.js +172 -0
- package/dist/chunk-2RJBFNB6.js.map +1 -0
- package/dist/chunk-KZKSBUKC.js +19 -0
- package/dist/chunk-KZKSBUKC.js.map +1 -0
- package/dist/chunk-RBTVBGRD.js +23 -0
- package/dist/chunk-RBTVBGRD.js.map +1 -0
- package/dist/device-class.d.ts +62 -0
- package/dist/device-class.js +75 -0
- package/dist/device-class.js.map +1 -0
- package/dist/index.d.ts +2 -3
- package/dist/index.js +8 -9
- package/dist/nav-stack.d.ts +38 -0
- package/dist/nav-stack.js +4 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/panel-header.d.ts +26 -0
- package/dist/panel-header.js +6 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/split-view.d.ts +42 -0
- package/dist/split-view.js +20 -0
- package/dist/split-view.js.map +1 -0
- package/dist/styles/foundation.css +21 -0
- package/dist/styles/menu-action-row.css +6 -0
- package/dist/styles/menu-header.css +19 -2
- package/dist/styles/menu-item.css +5 -0
- package/dist/styles/nav-stack.css +112 -0
- package/dist/styles/panel-header.css +59 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/styles.css +1 -2
- package/dist/styles/tab-scaffold.css +84 -0
- package/dist/styles/toolbar-control-group.css +52 -2
- package/dist/styles/toolbar-text.css +11 -0
- package/dist/styles/toolbar.css +9 -0
- package/dist/styles/workbench.css +94 -0
- package/dist/tab-scaffold.d.ts +30 -0
- package/dist/tab-scaffold.js +16 -0
- package/dist/tab-scaffold.js.map +1 -0
- package/dist/toolbar-text.d.ts +4 -2
- package/dist/toolbar-text.js +1 -1
- package/dist/wire-nav-stack.d.ts +15 -0
- package/dist/wire-nav-stack.js +88 -0
- package/dist/wire-nav-stack.js.map +1 -0
- package/dist/wire-tab-scaffold.d.ts +11 -0
- package/dist/wire-tab-scaffold.js +16 -0
- package/dist/wire-tab-scaffold.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +46 -4
- package/dist/wire-token-search-fields.js +1 -1
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +17 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +10 -10
- package/docs/app-layouts.md +57 -0
- package/docs/component-contract.md +37 -9
- package/docs/component-selection.md +57 -8
- package/docs/design-philosophy.md +23 -1
- package/docs/device-class.md +54 -0
- package/docs/layout.md +44 -8
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +29 -9
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +11 -9
- package/docs/workbench.md +47 -0
- package/llms.txt +107 -43
- package/package.json +37 -11
- package/ux-demo/recipes/app-shell.tsx +3 -3
- package/ux-demo/recipes/composer-form.tsx +5 -4
- package/ux-demo/recipes/list-workspace-states.tsx +2 -2
- package/ux-demo/recipes/loaders.ts +2 -0
- package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
- package/ux-demo/recipes/mount-recipe.ts +3 -0
- package/ux-demo/recipes/navigation-stack.tsx +76 -0
- package/ux-demo/recipes/recipes.css +40 -13
- package/ux-demo/recipes/workspace-header.tsx +2 -2
- package/dist/browser/page-header.js +0 -3
- package/dist/chunk-2PES33HS.js +0 -13
- package/dist/chunk-2PES33HS.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-H5AGGVU5.js +0 -75
- package/dist/chunk-H5AGGVU5.js.map +0 -1
- package/dist/chunk-K3G72I6D.js +0 -24
- package/dist/chunk-K3G72I6D.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -15
- package/dist/dialog-header.js +0 -5
- package/dist/dialog-header.js.map +0 -1
- package/dist/page-header.d.ts +0 -9
- package/dist/page-header.js +0 -3
- package/dist/page-header.js.map +0 -1
- package/dist/styles/dialog-header.css +0 -87
- 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"]}
|
package/dist/toolbar-text.d.ts
CHANGED
|
@@ -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 };
|
package/dist/toolbar-text.js
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
9
|
-
|
|
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 };
|
|
@@ -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"]}
|
package/docs/accessibility.md
CHANGED
|
@@ -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
|
-
##
|
|
45
|
-
|
|
46
|
-
`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
`titleId` and an optional `summaryId` to the owning dialog through
|
|
51
|
-
`aria-labelledby` and `aria-describedby
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
`
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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.
|
|
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
|
|