@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.
- package/README.md +8 -8
- package/ai/component-catalog.json +303 -167
- package/ai/public-api-signatures-v1.md +333 -26
- package/ai/skill.md +14 -7
- 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-header.css +19 -2
- 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 +34 -9
- package/docs/component-selection.md +56 -7
- package/docs/design-philosophy.md +23 -1
- package/docs/device-class.md +54 -0
- package/docs/layout.md +33 -8
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +17 -5
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +3 -3
- 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 +2 -2
- 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 +19 -8
- 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 -33
|
@@ -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.
|
|
@@ -121,14 +121,14 @@ into the section label. Do not add padding to pane shells,
|
|
|
121
121
|
double child-owned geometry with wrapper insets, or create competing scroll
|
|
122
122
|
owners. The [layout contract](./layout.md) lists the public roles and tokens.
|
|
123
123
|
|
|
124
|
-
`
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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.
|
|
132
132
|
|
|
133
133
|
`ValueTable` composes typed `ValueTableRow` entries. A row owns its `dt`/`dd`
|
|
134
134
|
semantics and may receive a leading `SafeHtml` icon. Every row keeps 8px of
|
|
@@ -153,7 +153,32 @@ overrides load later in the cascade or set scoped `--kui-*` variables. JavaScrip
|
|
|
153
153
|
modules are pure except the browser style wrappers and `@kerfjs/ui/select/register`,
|
|
154
154
|
which registers exactly the Web Awesome elements used by `Select`. Eventful
|
|
155
155
|
helpers such as `wireResizableRegions` and `wireTabBars` attach listeners only
|
|
156
|
-
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
|
|
157
182
|
reachable, and the registration module are the package's only declared side
|
|
158
183
|
effects.
|
|
159
184
|
|
|
@@ -15,6 +15,15 @@ The application adapter is usually a plain function that maps domain state to
|
|
|
15
15
|
component props plus stable `data-action` values. It is not a fork of package
|
|
16
16
|
markup or CSS.
|
|
17
17
|
|
|
18
|
+
**Don't fight the components.** The package is built to look right unstyled, so
|
|
19
|
+
custom CSS is the exception. Before adding `padding`, `margin`, `width`, `height`,
|
|
20
|
+
`border`, `background`, a wrapper card, or a decoration, check whether the
|
|
21
|
+
component, the pane, or the content-item already owns it — it almost always does,
|
|
22
|
+
and adding more usually double-insets or fights it. Trust component defaults and
|
|
23
|
+
fix the surrounding layout instead of overriding a control. See
|
|
24
|
+
[`design-philosophy.md`](./design-philosophy.md) "Reach for the primitive, not for
|
|
25
|
+
CSS".
|
|
26
|
+
|
|
18
27
|
## Production recipes
|
|
19
28
|
|
|
20
29
|
Use the [complete recipe guide](./recipes.md) when several primitives form one
|
|
@@ -29,6 +38,7 @@ application boundary:
|
|
|
29
38
|
| Composer form | [Catalog](../ux-demo/) · `?component=recipe-composer-form` |
|
|
30
39
|
| List workspace states | [Catalog](../ux-demo/) · `?component=recipe-list-workspace-states` |
|
|
31
40
|
| Compact toolbar choices and actions | [Catalog](../ux-demo/) · `?component=recipe-compact-toolbar` |
|
|
41
|
+
| Navigation stack | [Catalog](../ux-demo/) · `?component=recipe-navigation-stack` |
|
|
32
42
|
|
|
33
43
|
Recipes use public production exports and show ownership boundaries; they are
|
|
34
44
|
copyable reference compositions, not new monolithic components.
|
|
@@ -50,35 +60,74 @@ an upstream component or recipe request.
|
|
|
50
60
|
| --- | --- | --- | --- | --- | --- | --- |
|
|
51
61
|
| Icon — `LucideIcon` | A decorative or explicitly labeled Lucide-compatible icon belongs in app UI. | Do not use an icon as the only name of an unfamiliar action; add visible or accessible text. Prefer it over Web Awesome `wa-icon`. | None. | Icon choice and meaningful label. | `@kerfjs/ui/lucide-icon` | [Accessibility](./accessibility.md#shared-rules) |
|
|
52
62
|
| Disclosure indicator — `DisclosureArrow` | A control needs one animated 18px root-scaled visual for open and closed state, including configurable directions or a replacement icon. | Do not use it as the interactive control or accessible name; place it inside the button or control that exposes expanded state. | Pass the controlled `open` state, render it inside the owning control, author replacement icon content facing right before transforms, and override `--kui-disclosure-arrow-size` only when another visual size is required. Direction changes use the shortest rotation path, with counterclockwise chosen for a 180-degree closed-to-open tie. | Open state, interaction, accessible name, size override, replacement glyph, direction choices, and shortest-path rotation. | `@kerfjs/ui/disclosure-arrow` | [Component ownership](./component-contract.md#ownership-boundaries) |
|
|
53
|
-
| Application toolbar — `Toolbar` | Leading identity, optional centered content, and trailing controls form one horizontal app bar. | Do not use it for a page
|
|
63
|
+
| Application toolbar — `Toolbar` | Leading identity, optional centered content, and trailing controls form one horizontal app bar. | Do not use it for a page, panel, or dialog heading; use `PanelHeader`. | Compose `ToolbarText` and `ToolbarControlGroup` where their contracts fit. | Actions, command availability, responsive relocation, and state. | `@kerfjs/ui/toolbar` | [Toolbar composition](../README.md#component-subpaths) |
|
|
54
64
|
| Toolbar control cluster — `ToolbarControlGroup` | Related toolbar controls need contained, borderless, pressed, or single-control treatment. | Do not use it merely to align unrelated buttons; use toolbar slots or ordinary layout. Web Awesome `wa-button-group` is only for an exceptional grouped-action contract. | Delegate child actions; use `SegmentedControl` for an exclusive choice. | Actions, pressed/expanded state, and policy. | `@kerfjs/ui/toolbar-control-group` | [Component ownership](./component-contract.md#ownership-boundaries) |
|
|
55
|
-
| Toolbar identity text — `ToolbarText` | A toolbar needs large, default, or compact textual identity. | Do not substitute it for document heading semantics; use `
|
|
65
|
+
| Toolbar identity text — `ToolbarText` | A toolbar needs extra-large (page/panel title), large, default, or compact textual identity. | Do not substitute it for document heading semantics; use `PanelHeader` or native headings. | None. | Text and responsive priority. | `@kerfjs/ui/toolbar-text` | [Toolbar composition](../README.md#component-subpaths) |
|
|
56
66
|
| Navigation row — `MenuItem` | A pane or navigation area needs a selectable, disabled, dormant-trailing, or multiline action row. | Do not put a control in `trailing`; use `MenuActionRow` when the trailing region must be independently interactive. Use an `<a>` for navigation that must retain link behavior, a native `<button>` for an ordinary action, or implement the complete ARIA menu widget. | Delegate its `data-action`; compose inside a `.kui-content` section. Put domain event/drop metadata in `rootAttributes` rather than adding wrapper markup. | Routing, selection, permissions, copy, action handling, and domain `data-*` values. | `@kerfjs/ui/menu-item` | [Pane geometry](../README.md#pane-and-content-geometry) |
|
|
57
67
|
| Navigation row with trailing action — `MenuActionRow` | A full-width row needs a selectable primary action and an independently focusable trailing action. | Use `MenuItem` when trailing content is dormant metadata. Do not put controls inside the row's `label`, `icon`, or `trailingActionIcon` SafeHtml slots. Do not use `AppTab` outside tablist semantics or `ToolbarControlGroup` outside a toolbar. | Delegate both action strings; update controlled selection and any popover/context-menu state in the app. | Routing, selection, both action policies, domain metadata, and popover/context-menu behavior. | `@kerfjs/ui/menu-action-row` | [Accessibility](./accessibility.md#menuactionrow) |
|
|
58
|
-
| Navigation section heading — `MenuHeader` | A menu section needs a full-width label, semantic count, non-count badge, logical-end action, or real disclosure state. | Do not concatenate counts into `label` or put numeric content in `badge`; use `count` with the localized full phrase in `countLabel`. Do not add a disclosure arrow to navigation that reveals nothing. Do not shrink its 44px action target to the 18px visual. Do not use it as a page or dialog title; use `
|
|
68
|
+
| Navigation section heading — `MenuHeader` | A menu section needs a full-width label, semantic count, non-count badge, logical-end action, or real disclosure state. | Do not concatenate counts into `label` or put numeric content in `badge`; use `count` with the localized full phrase in `countLabel`. Do not add a disclosure arrow to navigation that reveals nothing. Do not shrink its 44px action target to the 18px visual. Do not use it as a page, panel, or dialog title; use `PanelHeader`. | Delegate its optional action; the app controls expanded state and revealed content. Toggle mode supplies `DisclosureArrow` unless `actionIcon` replaces it. Use `triggerAttributes` only for domain `data-*` or a native popover relationship. | Section organization, valid count and localized count label, disclosure state and content, non-count badge content, popover target behavior, and policy. | `@kerfjs/ui/menu-header` | [Pane geometry](../README.md#pane-and-content-geometry) |
|
|
59
69
|
| Application layout composition | A sidebar, main area, inspector, or dialog needs shared toolbar/content/footer and child geometry. | Do not pad the pane shell, wrap child-owned geometry in competing insets, invent unrelated centered measures, or leave an icon-only rail for a hidden pane. | Use `.kui-pane` and one `.kui-pane__content`; add `.kui-content` and `.kui-content-item` as needed. A visible pane owns collapse in its toolbar; move a hidden inline-start pane's restore control to the main toolbar leading edge and an inline-end pane's restore control to its trailing edge. | Layout hierarchy, reading width, scroll ownership, responsive relocation, and pane visibility state. | `@kerfjs/ui/layout.css` | [Pane anatomy](./layout.md#anatomy) |
|
|
60
70
|
| Menu composition | Navigation sections need full-size rows and the same content-item geometry as every other pane. | Do not add sidebar-specific wrapper padding, shrink targets to icon size, nest an interactive trailing control in `MenuItem`, or use a chevron on a row that does not disclose content. Use ordinary links for a different navigation contract. | Compose `MenuHeader`, `MenuItem`, and `MenuActionRow` in `.kui-content`; use `MenuHeader` toggle mode with real controlled content, and use `.kui-content-item` for other surfaces plus a pane footer for toolbar actions. | Information architecture, disclosure content and state, responsive drawer/shell behavior, and token overrides. | `@kerfjs/ui/layout.css` | [Pane geometry](../README.md#pane-and-content-geometry) |
|
|
61
71
|
| Resizable application pane — `ResizableRegion`, `clampRegionSize`, `resizeRegionFromPointer` | A controlled split pane needs the Kerf separator, collapse state, pointer plus keyboard resizing, or a product-specific decorative grip. | Do not use it for a static two-column layout; use CSS grid. Prefer it over Web Awesome `wa-split-panel` unless that component's distinct API is required. Keep `handleIcon` noninteractive. | Call `wireResizableRegions` from `@kerfjs/ui/wire-resizable-regions` once and retain its disposer. | Size signal, min/max policy, collapse policy, persistence, and optional decorative handle icon. | `@kerfjs/ui/resizable-region` | [ResizableRegion contract](./accessibility.md#resizableregion) |
|
|
62
72
|
| One application tab — `AppTab` | A controlled app tab needs selection, close, drag, leading/trailing anatomy, safe domain metadata, or a product-specific close glyph. | Do not render it alone or use it for a small settings choice; compose in `TabBar`, or use `SegmentedControl`. Keep `closeIcon` noninteractive. | Compose in `TabBar`; let `wireTabBars` manage interaction. Put only domain `data-*` values in `rootAttributes`. | Tab identity, order, selection, close policy, content, and domain metadata values. | `@kerfjs/ui/app-tab` | [Tabs contract](./accessibility.md#tabs) |
|
|
63
73
|
| Application tab strip — `TabBar`, `wireTabBars`, `reorderTabs` | Tabs switch page regions and may overflow, close, or reorder. | Do not use it for a compact local view toggle; use `SegmentedControl`. Do not use it for a long choice list; use `Select`. Prefer it over Web Awesome `wa-tab-group`, `wa-tab`, and `wa-tab-panel` for Kerf app tabs. | Call `wireTabBars` once, retain the disposer, and apply `onReorder` synchronously; `reorderTabs` is the default array helper. | Ordered tabs, selection, panels, routing, closing, and persistence. | `@kerfjs/ui/tab-bar` plus `@kerfjs/ui/wire-tab-bars` | [Tabs contract](./accessibility.md#tabs) |
|
|
64
|
-
|
|
|
65
|
-
| Dialog title and summary — `DialogHeader` | A dialog needs a toolbar-aligned title, optional summary/id and icon, and grouped actions wired to the dialog's ARIA references. | Do not use it as the page's `h1`; use `PageHeader`. It supplies header structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. | Connect title and optional summary ids to the dialog host, pass action children directly, localize `actionsLabel` when the group needs a name, and delegate actions. | Open state, focus lifecycle, dismissal, actions, labels, and copy. | `@kerfjs/ui/dialog-header` | [Header ownership](./component-contract.md#extracted-versus-application-specific) |
|
|
74
|
+
| Panel, dialog, or page heading — `PanelHeader` | A panel, dialog, or page needs a heading with an extra-large title, an optional icon and subtitle, and trailing actions. | Do not use it as persistent app chrome; use `Toolbar`. It supplies header structure, not modal behavior; use an application overlay or Web Awesome `wa-dialog` for that behavior. | Connect the title id and any provided summary id to the dialog or panel host, pass the trailing controls (typically a `ToolbarControlGroup`), and delegate their actions. | Open state, focus lifecycle, dismissal, the trailing controls, labels, and copy. | `@kerfjs/ui/panel-header` | [Header ownership](./component-contract.md#extracted-versus-application-specific) |
|
|
66
75
|
| Key/value facts — `ValueTable`, `ValueTableRow` | Read-only labels and values form a semantic definition list, optionally with a leading icon. | Do not use it for editable form fields or a row/column data grid; use native form or table semantics. | Compose typed `ValueTableRow` entries; pass `icon` when a 24px leading icon adds useful context. | Values, formatting, icon meaning, and empty/loading policy. | `@kerfjs/ui/value-table` | [Component ownership](./component-contract.md#ownership-boundaries) |
|
|
67
76
|
| Indeterminate activity — `LoadingSpinner` | A Kerf surface needs compact, labeled or decorative indeterminate progress. | Do not use it for known progress; use Web Awesome `wa-progress-bar` or `wa-progress-ring`. Direct Web Awesome UI may use `wa-spinner`; do not mix spinner systems within one surface. | None; pass a label when the spinner conveys status. | Loading lifecycle and adjacent status copy. | `@kerfjs/ui/loading-spinner` | [Accessibility](./accessibility.md#shared-rules) |
|
|
68
77
|
| Value selection — `Select` | A controlled form value comes from a moderate or long choice list, possibly grouped or icon-bearing. | Do not use it for commands; use a real action menu. Do not use it for a small visible choice set; use `SegmentedControl`. Prefer it over direct `wa-select`, `wa-option`, or value-like `wa-dropdown`/`wa-dropdown-item` composition. | Import `@kerfjs/ui/select/register` once; listen for standard input/change events. | Controlled value, validation, choices, and domain mapping. | `@kerfjs/ui/select` | [Web Awesome integration](../README.md#web-awesome-theme) |
|
|
69
78
|
| Small exclusive choice — `SegmentedControl` | A few visible choices switch a compact view or setting, with toolbar, rounded, or pill presentation. | Do not use it for tabpanel semantics; use `TabBar`. Do not use it for many choices; use `Select`. Prefer it over `wa-button-group` when the controls select one value. | Delegate its action, read `data-segment-value`, update `value`, and rerender. | Controlled value, labels, action, and persistence. | `@kerfjs/ui/segmented-control` | [SegmentedControl contract](./accessibility.md#segmentedcontrol) |
|
|
70
|
-
| Structured search editor — `TokenSearchField`, `readTokenSearchField`, `placeTokenSearchCaret`, `wireTokenSearchFields` | Free text and ordered, editable, removable filter tokens share one searchbox; enable `collapsible` when an empty, unfocused field should reduce to one iconic action, standalone or in a toolbar group. | Do not use it for ordinary text entry; use a native input or Web Awesome `wa-input`. Do not use it when filters belong in separate form controls. | Read DOM-owned text on input, empty `textContent` on clear, and use `placeTokenSearchCaret` after explicit controlled focus changes. Call `wireTokenSearchFields` from `@kerfjs/ui/wire-token-search-fields` once so Enter submits without adding a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. In `collapsible` mode,
|
|
79
|
+
| Structured search editor — `TokenSearchField`, `readTokenSearchField`, `placeTokenSearchCaret`, `wireTokenSearchFields` | Free text and ordered, editable, removable filter tokens share one searchbox; enable `collapsible` when an empty, unfocused field should reduce to one iconic action, standalone or in a toolbar group. | Do not use it for ordinary text entry; use a native input or Web Awesome `wa-input`. Do not use it when filters belong in separate form controls. | Read DOM-owned text on input, empty `textContent` on clear, and use `placeTokenSearchCaret` after explicit controlled focus changes. Call `wireTokenSearchFields` from `@kerfjs/ui/wire-token-search-fields` once so Enter submits without adding a line break and keyboard chip deletion restores focus plus the text-relative caret after controlled replacement. In `collapsible` mode it also manages the transient expand/collapse/focus by default (activate to reveal + focus, Escape or empty blur to collapse); bind the field's `expanded` to the signal on the returned handle (`handle.expanded(id)`) or adopt your own via `collapsible.signals`, and opt out per behavior only when the app must own it. | Parsing, suggestions, tokens, query execution, results, announcements, and — only if overriding the default — the collapsible `expanded` signal. | `@kerfjs/ui/token-search-field` | [TokenSearchField contract](./accessibility.md#tokensearchfield) |
|
|
71
80
|
| Persistent inline status — `StateBanner` | A neutral, info, success, warning, or danger message belongs next to the affected work. | Do not use it for a no-content screen; use `EmptyState`. Do not use it for transient confirmation; use a toast. Web Awesome `wa-callout` is the ecosystem alternative for Web Awesome-owned content. | Delegate an optional action; choose alert urgency only for attention-requiring failure. | State mapping, message lifetime, retry/action behavior, and copy. | `@kerfjs/ui/state-banner` | [Feedback accessibility](./accessibility.md#shared-rules) |
|
|
72
81
|
| Empty or busy content area — `EmptyState` | A content region has no items, cannot proceed, or is loading and needs explanation plus an optional action. | Do not use it for an inline status update; use `StateBanner`. Do not use it for transient success; use `wa-toast`/`wa-toast-item` or the application's toast system. | Delegate its optional action; it composes `LoadingSpinner` when busy. | Empty/busy policy, recovery action, illustration, and copy. | `@kerfjs/ui/empty-state` | [Feedback ownership](./component-contract.md#extracted-versus-application-specific) |
|
|
73
82
|
|
|
74
83
|
## Ambiguous choices
|
|
75
84
|
|
|
76
|
-
- `Toolbar` is persistent app chrome; `
|
|
85
|
+
- `Toolbar` is persistent app chrome; `PanelHeader` heads a panel, dialog, or page.
|
|
77
86
|
- `TabBar` changes tabpanels and supports overflow/reorder; `SegmentedControl` chooses among a few compact views; `Select` handles a longer value list.
|
|
78
87
|
- `StateBanner` persists beside affected work; `EmptyState` replaces absent content; `wa-callout` is contextual ecosystem content; `wa-toast` and `wa-toast-item` are transient and must not carry the only copy of important state.
|
|
79
88
|
- `ResizableRegion` is an interactive controlled pane. CSS grid is the right answer when columns do not need a user-operable separator.
|
|
80
89
|
- `TokenSearchField` is a structured editor. A native input or `wa-input` is the right answer for ordinary text.
|
|
81
90
|
|
|
91
|
+
## Toolbar composition
|
|
92
|
+
|
|
93
|
+
A `Toolbar` has three zones — `leading`, `center`, and `trailing`. In almost
|
|
94
|
+
every case the only things that go **directly** in a zone are `ToolbarText`
|
|
95
|
+
(identity/title text) and `ToolbarControlGroup` (any control or cluster of
|
|
96
|
+
controls). Do not drop bare buttons, inputs, links, or arbitrary markup straight
|
|
97
|
+
into a zone; wrap controls in a `ToolbarControlGroup` so they get the shared
|
|
98
|
+
toolbar geometry, hover/pressed treatment, and grouping. `SegmentedControl`,
|
|
99
|
+
`Select`, a collapsible `TokenSearchField`, and Web Awesome controls all live
|
|
100
|
+
**inside** a `ToolbarControlGroup`, not loose in the zone. `PanelHeader` is the
|
|
101
|
+
one wrapper that composes these for you as a panel/dialog/page heading.
|
|
102
|
+
|
|
103
|
+
Common toolbar patterns:
|
|
104
|
+
|
|
105
|
+
| Want | Put in the zone | Notes |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| Identity or title text | `<ToolbarText text="…" size="large" />` (or `xlarge` for a page/panel title) | Wrap in a `single` borderless group only when it must align with adjacent control pills |
|
|
108
|
+
| One or more icon/text buttons | `<ToolbarControlGroup>{buttons}</ToolbarControlGroup>` | Use `buttonAppearance="push"` for toggle buttons with `aria-pressed`; `single` for a lone control |
|
|
109
|
+
| An exclusive view switch | `<ToolbarControlGroup><SegmentedControl … /></ToolbarControlGroup>` | Not `TabBar`, which switches tabpanels |
|
|
110
|
+
| A value list | `<ToolbarControlGroup><Select … /></ToolbarControlGroup>` | Register `@kerfjs/ui/select/register` once |
|
|
111
|
+
| A collapsible search box | `<ToolbarControlGroup single><TokenSearchField collapsible … /></ToolbarControlGroup>` | The group animates the iconic ↔ expanded states; `wireTokenSearchFields` manages expand/collapse/focus by default |
|
|
112
|
+
|
|
113
|
+
A **popup menu in a toolbar** is a `single` `ToolbarControlGroup` wrapping a Web
|
|
114
|
+
Awesome `wa-dropdown`: its `slot="trigger"` `wa-button` is the toolbar button and
|
|
115
|
+
the `wa-dropdown-item`s are the menu. Keep the dropdown's managed light-DOM
|
|
116
|
+
children under `data-morph-skip-children` so kerf does not reconcile Web Awesome's
|
|
117
|
+
own DOM.
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
<ToolbarControlGroup single>
|
|
121
|
+
<wa-dropdown placement="bottom-start" data-morph-skip-children>
|
|
122
|
+
<wa-button slot="trigger" appearance="plain" with-caret aria-label="Sort">
|
|
123
|
+
<LucideIcon icon={ArrowDownAZ} name="arrow-down-a-z" />
|
|
124
|
+
</wa-button>
|
|
125
|
+
<wa-dropdown-item data-action="sort-recent">Recently updated</wa-dropdown-item>
|
|
126
|
+
<wa-dropdown-item data-action="sort-priority">Priority</wa-dropdown-item>
|
|
127
|
+
</wa-dropdown>
|
|
128
|
+
</ToolbarControlGroup>
|
|
129
|
+
```
|
|
130
|
+
|
|
82
131
|
## Correct composition and duplicated-markup trap
|
|
83
132
|
|
|
84
133
|
Correct: let the pane stay unpadded while its children own the shared 8/1/8
|