@kerfjs/ui 4.4.1 → 5.0.0-beta.10
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 +240 -10
- package/ai/component-catalog.json +4766 -0
- package/ai/component-catalog.schema.json +79 -0
- package/ai/public-api-signatures-v1.md +1327 -0
- package/ai/skill.md +73 -11
- package/ai/webawesome-jsx-signatures-v1.md +106 -0
- package/dist/app-tab.d.ts +14 -2
- package/dist/app-tab.js +3 -1
- package/dist/browser/app-tab.js +3 -2
- package/dist/browser/disclosure-arrow.js +4 -0
- package/dist/browser/empty-state.js +3 -3
- package/dist/browser/list-action-row.js +4 -0
- package/dist/browser/list-header.js +6 -0
- package/dist/browser/list-item.js +4 -0
- package/dist/browser/loading-spinner.js +2 -2
- package/dist/browser/lucide-icon.js +2 -2
- package/dist/browser/panel-header.js +7 -0
- package/dist/browser/resizable-region.js +2 -2
- package/dist/browser/segmented-control.js +3 -2
- package/dist/browser/select.js +4 -3
- package/dist/browser/skeleton.js +3 -0
- package/dist/browser/state-banner.js +3 -2
- package/dist/browser/tab-bar.js +2 -2
- package/dist/browser/token-search-field.js +4 -0
- package/dist/browser/toolbar-control-group.js +2 -2
- package/dist/browser/toolbar-text.js +3 -2
- package/dist/browser/toolbar.js +2 -2
- package/dist/browser/value-table.js +3 -2
- package/dist/catalog.d.ts +119 -0
- package/dist/catalog.js +143 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-2Z5TWHKQ.js +34 -0
- package/dist/chunk-2Z5TWHKQ.js.map +1 -0
- package/dist/chunk-37T3VNIZ.js +19 -0
- package/dist/chunk-37T3VNIZ.js.map +1 -0
- package/dist/chunk-56RJXPHG.js +43 -0
- package/dist/chunk-56RJXPHG.js.map +1 -0
- package/dist/chunk-7BR3DO2A.js +31 -0
- package/dist/chunk-7BR3DO2A.js.map +1 -0
- package/dist/chunk-7JB7VPRI.js +34 -0
- package/dist/chunk-7JB7VPRI.js.map +1 -0
- package/dist/chunk-7LDCTU7D.js +134 -0
- package/dist/chunk-7LDCTU7D.js.map +1 -0
- package/dist/{chunk-4HHCYSGT.js → chunk-AQNKJRFH.js} +2 -2
- package/dist/{chunk-4HHCYSGT.js.map → chunk-AQNKJRFH.js.map} +1 -1
- package/dist/{chunk-MBR3TYNL.js → chunk-AVZHJN6C.js} +3 -3
- package/dist/chunk-AVZHJN6C.js.map +1 -0
- package/dist/chunk-B24C4A6T.js +22 -0
- package/dist/chunk-B24C4A6T.js.map +1 -0
- package/dist/chunk-BLK3UEEI.js +23 -0
- package/dist/chunk-BLK3UEEI.js.map +1 -0
- package/dist/chunk-CVZYNJJ5.js +265 -0
- package/dist/chunk-CVZYNJJ5.js.map +1 -0
- package/dist/chunk-I7INRJO4.js +17 -0
- package/dist/chunk-I7INRJO4.js.map +1 -0
- package/dist/chunk-L4OKFDJY.js +24 -0
- package/dist/chunk-L4OKFDJY.js.map +1 -0
- package/dist/chunk-LC3YSAZC.js +39 -0
- package/dist/chunk-LC3YSAZC.js.map +1 -0
- package/dist/{chunk-3O7ZEUW2.js → chunk-M5JRI6MN.js} +3 -3
- package/dist/chunk-M5JRI6MN.js.map +1 -0
- package/dist/chunk-RUNWEPR4.js +10 -0
- package/dist/chunk-RUNWEPR4.js.map +1 -0
- package/dist/chunk-W5EQ3HYQ.js +42 -0
- package/dist/chunk-W5EQ3HYQ.js.map +1 -0
- package/dist/{chunk-MQIZAD4A.js → chunk-XXMOARTH.js} +5 -3
- package/dist/chunk-XXMOARTH.js.map +1 -0
- package/dist/chunk-Y5MRDTLC.js +20 -0
- package/dist/chunk-Y5MRDTLC.js.map +1 -0
- package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
- package/dist/chunk-YGN7OMLT.js.map +1 -0
- package/dist/collapsible-panel.d.ts +66 -0
- package/dist/collapsible-panel.js +22 -0
- package/dist/collapsible-panel.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/disclosure-arrow.d.ts +14 -0
- package/dist/disclosure-arrow.js +4 -0
- package/dist/disclosure-arrow.js.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.js +21 -16
- package/dist/list-action-row.d.ts +52 -0
- package/dist/list-action-row.js +5 -0
- package/dist/list-action-row.js.map +1 -0
- package/dist/list-header.d.ts +43 -0
- package/dist/list-header.js +7 -0
- package/dist/list-header.js.map +1 -0
- package/dist/list-item.d.ts +33 -0
- package/dist/list-item.js +5 -0
- package/dist/list-item.js.map +1 -0
- package/dist/nav-stack.d.ts +38 -0
- package/dist/nav-stack.js +5 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/panel-header.d.ts +36 -0
- package/dist/panel-header.js +7 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/resizable-region.d.ts +3 -1
- package/dist/resizable-region.js +1 -1
- package/dist/segmented-control.d.ts +3 -1
- package/dist/segmented-control.js +2 -1
- package/dist/select-register.d.ts +5 -1
- package/dist/select-register.js +3 -0
- package/dist/select-register.js.map +1 -1
- package/dist/select.d.ts +5 -2
- package/dist/select.js +2 -1
- package/dist/skeleton.d.ts +24 -0
- package/dist/skeleton.js +3 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/split-view.d.ts +42 -0
- package/dist/split-view.js +21 -0
- package/dist/split-view.js.map +1 -0
- package/dist/state-banner.d.ts +3 -1
- package/dist/state-banner.js +2 -1
- package/dist/styles/app-tab.css +152 -0
- package/dist/styles/catalog.css +498 -0
- package/dist/styles/collapsible-panel.css +146 -0
- package/dist/styles/disclosure-arrow.css +23 -0
- package/dist/styles/empty-state.css +47 -0
- package/dist/styles/foundation.css +347 -0
- package/dist/styles/layout.css +73 -0
- package/dist/styles/list-action-row.css +160 -0
- package/dist/styles/list-header.css +160 -0
- package/dist/styles/list-item.css +123 -0
- package/dist/styles/loading-spinner.css +21 -0
- package/dist/styles/lucide-icon.css +6 -0
- package/dist/styles/nav-stack.css +111 -0
- package/dist/styles/panel-header.css +64 -0
- package/dist/styles/resizable-region.css +111 -0
- package/{src → dist/styles}/segmented-control.css +62 -31
- package/dist/styles/select.css +138 -0
- package/dist/styles/skeleton.css +31 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/state-banner.css +173 -0
- package/dist/styles/styles.css +23 -0
- package/dist/styles/tab-bar.css +45 -0
- package/dist/styles/tab-scaffold.css +84 -0
- package/dist/styles/token-search-field.css +289 -0
- package/dist/styles/toolbar-control-group.css +211 -0
- package/dist/styles/toolbar-text.css +40 -0
- package/dist/styles/toolbar.css +61 -0
- package/dist/styles/value-table.css +75 -0
- package/{src → dist/styles}/webawesome.css +119 -29
- package/dist/styles/workbench.css +94 -0
- package/dist/tab-bar.d.ts +8 -1
- package/dist/tab-bar.js +1 -1
- 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/token-search-field.d.ts +43 -0
- package/dist/token-search-field.js +4 -0
- package/dist/token-search-field.js.map +1 -0
- package/dist/toolbar-text.d.ts +15 -3
- package/dist/toolbar-text.js +2 -1
- package/dist/value-table.d.ts +12 -2
- package/dist/value-table.js +2 -1
- package/dist/webawesome.d.ts +100 -0
- package/dist/webawesome.js +3 -0
- package/dist/webawesome.js.map +1 -0
- package/dist/wire-catalog.d.ts +26 -0
- package/dist/wire-catalog.js +48 -0
- package/dist/wire-catalog.js.map +1 -0
- 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-resizable-regions.js +1 -1
- package/dist/wire-sidebar.d.ts +43 -0
- package/dist/wire-sidebar.js +117 -0
- package/dist/wire-sidebar.js.map +1 -0
- package/dist/wire-tab-bars.d.ts +15 -2
- package/dist/wire-tab-bars.js +1 -1
- 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 +97 -0
- package/dist/wire-token-search-fields.js +5 -0
- package/dist/wire-token-search-fields.js.map +1 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +17 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +101 -3
- package/docs/app-layouts.md +61 -0
- package/docs/catalog.md +155 -0
- package/docs/collapsible-panel.md +102 -0
- package/docs/component-contract.md +169 -4
- package/docs/component-selection.md +185 -0
- package/docs/design/templates/panel-header/icon-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-actions.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -0
- package/docs/design/templates/panel-header/no-icon-dark.svg +1 -0
- package/docs/design/templates/panel-header/no-icon.svg +1 -0
- package/docs/design/templates/panel-header/page-heading-dark.svg +1 -0
- package/docs/design/templates/panel-header/page-heading.svg +1 -0
- package/docs/design/templates/panel-header/title-only-dark.svg +1 -0
- package/docs/design/templates/panel-header/title-only.svg +1 -0
- package/docs/design/templates/panel-header-dark.svg +13 -0
- package/docs/design/templates/panel-header.svg +13 -0
- package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
- package/docs/design/templates/toolbar-control-group-dark.svg +11 -0
- package/docs/design/templates/toolbar-control-group.svg +11 -0
- package/docs/design/templates.md +83 -0
- package/docs/design-philosophy.md +61 -1
- package/docs/device-class.md +54 -0
- package/docs/examples/command-palette-adapter.tsx +70 -0
- package/docs/layout.md +131 -0
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +167 -0
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +23 -9
- package/docs/webawesome-theme.md +15 -0
- package/docs/workbench.md +47 -0
- package/llms.txt +123 -14
- package/package.json +139 -44
- package/ux-demo/recipes/app-shell.tsx +32 -0
- package/ux-demo/recipes/collapsible-sidebar.tsx +100 -0
- package/ux-demo/recipes/compact-toolbar.tsx +43 -0
- package/ux-demo/recipes/composer-form.tsx +63 -0
- package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
- package/ux-demo/recipes/list-workspace-states.tsx +26 -0
- package/ux-demo/recipes/loaders.ts +33 -0
- package/ux-demo/recipes/loading-inspector.tsx +81 -0
- package/ux-demo/recipes/mount-recipe.ts +43 -0
- package/ux-demo/recipes/navigation-sidebar.tsx +26 -0
- package/ux-demo/recipes/navigation-stack.tsx +76 -0
- package/ux-demo/recipes/recipes.css +448 -0
- package/ux-demo/recipes/types.ts +17 -0
- package/ux-demo/recipes/workspace-header.tsx +12 -0
- package/dist/browser/dialog-header.js +0 -3
- package/dist/browser/menu-header.js +0 -3
- package/dist/browser/menu-item.js +0 -3
- 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-3O7ZEUW2.js.map +0 -1
- package/dist/chunk-723YNJBH.js +0 -17
- package/dist/chunk-723YNJBH.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js +0 -10
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-MBR3TYNL.js.map +0 -1
- package/dist/chunk-MQIZAD4A.js.map +0 -1
- package/dist/chunk-NHXX4JI5.js +0 -28
- package/dist/chunk-NHXX4JI5.js.map +0 -1
- package/dist/chunk-OPCJRCYO.js +0 -19
- package/dist/chunk-OPCJRCYO.js.map +0 -1
- package/dist/chunk-T5XNMVOI.js +0 -24
- package/dist/chunk-T5XNMVOI.js.map +0 -1
- package/dist/chunk-TRNBHPNW.js.map +0 -1
- package/dist/chunk-VHW5YNIB.js +0 -17
- package/dist/chunk-VHW5YNIB.js.map +0 -1
- package/dist/chunk-VKH2JLDZ.js +0 -10
- package/dist/chunk-VKH2JLDZ.js.map +0 -1
- package/dist/chunk-WY4CAEPG.js +0 -14
- package/dist/chunk-WY4CAEPG.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -14
- package/dist/dialog-header.js +0 -3
- package/dist/dialog-header.js.map +0 -1
- package/dist/menu-header.d.ts +0 -15
- package/dist/menu-header.js +0 -3
- package/dist/menu-header.js.map +0 -1
- package/dist/menu-item.d.ts +0 -22
- package/dist/menu-item.js +0 -3
- package/dist/menu-item.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/src/app-tab.css +0 -20
- package/src/dialog-header.css +0 -9
- package/src/empty-state.css +0 -6
- package/src/foundation.css +0 -106
- package/src/loading-spinner.css +0 -4
- package/src/lucide-icon.css +0 -1
- package/src/menu-header.css +0 -13
- package/src/menu-item.css +0 -12
- package/src/page-header.css +0 -3
- package/src/resizable-region.css +0 -19
- package/src/select.css +0 -18
- package/src/state-banner.css +0 -31
- package/src/styles.css +0 -18
- package/src/tab-bar.css +0 -4
- package/src/toolbar-control-group.css +0 -25
- package/src/toolbar-text.css +0 -4
- package/src/toolbar.css +0 -9
- package/src/value-table.css +0 -5
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Signal } from 'kerfjs';
|
|
2
|
+
|
|
3
|
+
interface TokenSearchSubmit {
|
|
4
|
+
id: string;
|
|
5
|
+
editor: HTMLElement;
|
|
6
|
+
}
|
|
7
|
+
/** Reported when adjacent-token keyboard deletion asks the app to drop a chip. */
|
|
8
|
+
interface TokenSearchTokenRemoval {
|
|
9
|
+
id: string;
|
|
10
|
+
/** The `data-token-value` of the token the app should remove from its state. */
|
|
11
|
+
value: string;
|
|
12
|
+
editor: HTMLElement;
|
|
13
|
+
/** `'backward'` = the token before the caret (Backspace); `'forward'` = after (Delete). */
|
|
14
|
+
direction: 'backward' | 'forward';
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Opt-in keyboard behavior for the atomic token chips. Off unless `keyboard` is
|
|
18
|
+
* set; each piece defaults on once opted in. The helper never mutates app state:
|
|
19
|
+
* a removal is reported through {@link TokenSearchKeyboardOptions.onRemoveToken}
|
|
20
|
+
* for the caller to apply, while caret movement past a chip is a pure ephemeral
|
|
21
|
+
* mechanic the helper performs itself.
|
|
22
|
+
*/
|
|
23
|
+
interface TokenSearchKeyboardOptions {
|
|
24
|
+
/**
|
|
25
|
+
* From a collapsed caret with no selection, Backspace removes the token
|
|
26
|
+
* immediately before it and Delete the token immediately after — reported via
|
|
27
|
+
* `onRemoveToken` — instead of deleting a character. Default: true.
|
|
28
|
+
*/
|
|
29
|
+
removeAdjacentToken?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* ArrowRight moves the caret past a trailing atomic token so text typed next
|
|
32
|
+
* lands after the chip. Default: true.
|
|
33
|
+
*/
|
|
34
|
+
moveCaretPastToken?: boolean;
|
|
35
|
+
/** Apply the reported removal to your controlled state, then re-render. */
|
|
36
|
+
onRemoveToken?: (removal: TokenSearchTokenRemoval) => void;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Managed collapsible behavior for the iconic TokenSearchField. Every piece is on
|
|
40
|
+
* by default; disable a specific one to own it in the app. Provide `signals` to
|
|
41
|
+
* drive app-owned `expanded` signals per field id instead of helper-created ones.
|
|
42
|
+
*/
|
|
43
|
+
interface TokenSearchCollapsibleOptions {
|
|
44
|
+
/** Expand the field and focus its editor when the iconic trigger is activated. Default: true. */
|
|
45
|
+
expandOnActivate?: boolean;
|
|
46
|
+
/** Collapse the field when focus leaves it while it is empty. Default: true. */
|
|
47
|
+
collapseOnEmptyBlur?: boolean;
|
|
48
|
+
/** Collapse an empty field on Escape and restore focus to its trigger. Default: true. */
|
|
49
|
+
collapseOnEscape?: boolean;
|
|
50
|
+
/** Focus the editor on expand and the trigger on Escape-collapse. Default: true. */
|
|
51
|
+
manageFocus?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Keep an empty field expanded when focus moves to a caller-owned surface
|
|
54
|
+
* rendered outside the field — a suggestions dropdown, date picker, or help
|
|
55
|
+
* popover shown beside it. Return true for any focus target that must NOT
|
|
56
|
+
* trigger collapse-on-empty-blur. An element carrying `data-token-search-keep-open`
|
|
57
|
+
* (or any node inside one) is always exempt, so this predicate is only needed
|
|
58
|
+
* for surfaces you cannot mark declaratively.
|
|
59
|
+
*/
|
|
60
|
+
keepOpenOn?: (target: Node | null) => boolean;
|
|
61
|
+
/** App-owned `expanded` signals keyed by field id; adopted instead of helper-created. */
|
|
62
|
+
signals?: Readonly<Record<string, Signal<boolean>>>;
|
|
63
|
+
}
|
|
64
|
+
interface WireTokenSearchFieldsOptions {
|
|
65
|
+
onSubmit?: (submission: TokenSearchSubmit) => void;
|
|
66
|
+
/** Fired on every editor `input`, after the browser mutates it, so a caller can drop its own `input` listener. */
|
|
67
|
+
onEdit?: (edit: TokenSearchSubmit) => void;
|
|
68
|
+
/** Managed collapsible transient behavior. `true`/omitted = on with defaults; `false` = fully off. */
|
|
69
|
+
collapsible?: boolean | TokenSearchCollapsibleOptions;
|
|
70
|
+
/** Opt-in atomic-chip keyboard behavior (off by default). `true` = on with defaults. */
|
|
71
|
+
keyboard?: boolean | TokenSearchKeyboardOptions;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The value returned from {@link wireTokenSearchFields}: call it (or `dispose()`) to
|
|
75
|
+
* tear down. When collapsible behavior is managed, it also exposes the transient
|
|
76
|
+
* `expanded` state per field id so the app can read it in render, hand in its own
|
|
77
|
+
* signal, or drive it imperatively.
|
|
78
|
+
*/
|
|
79
|
+
interface TokenSearchFieldsHandle {
|
|
80
|
+
(): void;
|
|
81
|
+
dispose(): void;
|
|
82
|
+
/** The managed `expanded` signal for a field id (adopted or helper-created); undefined when unmanaged. */
|
|
83
|
+
expanded(id: string): Signal<boolean> | undefined;
|
|
84
|
+
/** Expand the field (and, when focus is managed, focus its editor). */
|
|
85
|
+
open(id: string): void;
|
|
86
|
+
/** Collapse the field (and, when focus is managed, restore focus to its trigger). */
|
|
87
|
+
close(id: string): void;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Wire every TokenSearchField under `root`: submit on Enter, preserve the caret across
|
|
91
|
+
* controlled token deletion, and (by default) manage the collapsible field's transient
|
|
92
|
+
* expand/collapse/focus. Returns a {@link TokenSearchFieldsHandle} — a disposer that also
|
|
93
|
+
* exposes the managed `expanded` state per field id.
|
|
94
|
+
*/
|
|
95
|
+
declare function wireTokenSearchFields(root: HTMLElement, { onSubmit, onEdit, collapsible, keyboard }?: WireTokenSearchFieldsOptions): TokenSearchFieldsHandle;
|
|
96
|
+
|
|
97
|
+
export { type TokenSearchCollapsibleOptions, type TokenSearchFieldsHandle, type TokenSearchKeyboardOptions, type TokenSearchSubmit, type TokenSearchTokenRemoval, type WireTokenSearchFieldsOptions, wireTokenSearchFields };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"wire-token-search-fields.js"}
|
|
@@ -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
|
@@ -6,11 +6,28 @@ These are normative contracts for package components and consuming applications.
|
|
|
6
6
|
|
|
7
7
|
- All controls have accessible names and visible `:focus-visible` treatment.
|
|
8
8
|
- Decorative `LucideIcon` output is `aria-hidden`; pass `label` only when the SVG itself conveys meaning.
|
|
9
|
-
- `
|
|
9
|
+
- `ListItem` uses native button Enter/Space behavior. `selected` maps to `aria-current="page"`; `pressed` maps to `aria-pressed`. Its `trailing` content is dormant and must not contain controls. Its `rootAttributes` slot accepts only application `data-*` metadata and deliberately cannot emit `role="menuitem"` in isolation—a true ARIA menu must own arrow, Home/End, Escape, and focus behavior as one widget.
|
|
10
|
+
- `ListHeader` owns action naming, disabled state, and disclosure `aria-expanded`. Toggle mode supplies one decorative 18px `DisclosureArrow` when `actionIcon` is omitted; it mirrors `expanded` while the native button's accessible name stays stable. The application must update that controlled state and reveal or hide real content. A valid `count` is visually rendered in an `aria-hidden` neutral pill while the required localized `countLabel` becomes part of the owning heading or disclosure button's accessible name; zero remains a real count. Its narrow `triggerAttributes` slot may describe a native popover or controlled-content relationship with `popoverTarget`, `popoverTargetAction`, `aria-controls`, and `aria-haspopup`; the application owns the target surface and its focus/dismissal behavior.
|
|
11
|
+
- In any `.kui-pane`, menu rows, `ListHeader` actions, and toolbar groups keep a minimum 44px target in both dimensions. A `ListHeader` fills the available inline width and keeps that action at the logical end, including in RTL. Its action visual defaults to 18px through `--kui-list-header-action-icon-size`; do not reduce the target to the visible icon.
|
|
10
12
|
- `LoadingSpinner` is either labeled (`role="img"`) or hidden. Its rotation stops for reduced motion.
|
|
11
13
|
- `StateBanner` defaults to polite `role="status"`; use `urgency="alert"` only for an attention-requiring failure.
|
|
12
14
|
- `EmptyState` reports busy state through `aria-busy` and never relies on an illustration as its label.
|
|
13
|
-
- `Select` follows the Web Awesome host's standard `input`/`change` events. Application tests verify the live `value`, focus, and events—not attributes alone.
|
|
15
|
+
- `Select` follows the Web Awesome host's standard `input`/`change` events. Application tests verify the live `value`, focus, and events—not attributes alone. Its decorative option icons and value-dependent selected content remain present after controlled rerenders, so the visible choice does not silently lose its non-text cue.
|
|
16
|
+
- `DisclosureArrow` is an 18px root-scaled decorative visual by default, not an interaction target. Its owning native control supplies a stable accessible name, pointer and keyboard interaction, and `aria-expanded`; multiple arrows keep independently controlled state. Consumers may override `--kui-disclosure-arrow-size` without changing that ownership. Configured directions animate over the shortest path; a 180-degree closed-to-open tie uses counterclockwise rotation.
|
|
17
|
+
|
|
18
|
+
## ListActionRow
|
|
19
|
+
|
|
20
|
+
`ListActionRow` renders a noninteractive visual root containing primary and
|
|
21
|
+
trailing native buttons as siblings. The primary comes first in DOM and Tab
|
|
22
|
+
order and alone receives `aria-current` or `aria-pressed`; the trailing button
|
|
23
|
+
has its own required accessible name and disabled state. Both controls carry
|
|
24
|
+
the row's item id for application delegation, while the root deliberately has
|
|
25
|
+
no action or role. `label`, `icon`, and `trailingActionIcon` are dormant visual
|
|
26
|
+
content and must not contain buttons, links, interactive roles, or other
|
|
27
|
+
controls. A trailing click, double click, context menu, Enter, or Space
|
|
28
|
+
must never activate the primary control. The application owns controlled
|
|
29
|
+
selection and the lifecycle, focus, and dismissal policy of any related
|
|
30
|
+
popover or context menu.
|
|
14
31
|
|
|
15
32
|
## ResizableRegion
|
|
16
33
|
|
|
@@ -22,12 +39,33 @@ The handle exposes separator role, orientation, name, minimum, maximum, and curr
|
|
|
22
39
|
- primary-pointer drag with clamping;
|
|
23
40
|
- preview callbacks while dragging and one commit callback on release.
|
|
24
41
|
|
|
25
|
-
The application owns persistence and collapsed/expanded policy. Keep the last expanded size outside the component and restore it when reopening.
|
|
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
|
+
|
|
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. By default the title carries no native heading role, so for a **dialog or
|
|
50
|
+
panel** the application connects `titleId` and an optional `summaryId` to the
|
|
51
|
+
owning host through `aria-labelledby` and `aria-describedby`. For a **page or
|
|
52
|
+
view** title, pass `headingLevel` (usually `1`): the title then exposes
|
|
53
|
+
`role="heading"` with a matching `aria-level`, giving the view a heading landmark
|
|
54
|
+
so screen-reader heading navigation and "main heading" semantics work — the same
|
|
55
|
+
role/level pair `ToolbarText` exposes when it is given `headingLevel` directly.
|
|
56
|
+
Keep the levels meaningful and non-skipping within a view. Pass the trailing
|
|
57
|
+
controls as a labeled `ToolbarControlGroup` when that group needs an accessible
|
|
58
|
+
name.
|
|
26
59
|
|
|
27
60
|
## Tabs
|
|
28
61
|
|
|
29
62
|
`AppTab` renders one controlled tab. `TabBar` supplies the containing tab list,
|
|
30
63
|
fixed leading/trailing regions, and a horizontally scrollable strip.
|
|
64
|
+
`AppTab.rootAttributes` accepts application `data-*` metadata only; runtime
|
|
65
|
+
filtering rejects roles plus case variants of component- or wiring-owned
|
|
66
|
+
action, identity, selection, drag, drop, and component attributes. An optional
|
|
67
|
+
`closeIcon` is decorative dormant content inside the already named close button
|
|
68
|
+
and must not contain interactive descendants.
|
|
31
69
|
`wireTabBars()` adds Left/Right wrapping, Home/End, Delete/Backspace close
|
|
32
70
|
activation, same-bar pointer reordering, `Alt+Shift+ArrowLeft/ArrowRight`
|
|
33
71
|
reordering, focus restoration, scroll-into-view, and pointer-proximity
|
|
@@ -35,6 +73,15 @@ autoscroll at either horizontal edge. Edge autoscroll is direct manipulation,
|
|
|
35
73
|
stops on drop/drag end/disposal, and does not change keyboard behavior. It
|
|
36
74
|
returns a disposer.
|
|
37
75
|
|
|
76
|
+
Arrow / Home / End follow the ARIA Tabs **automatic-activation** pattern by
|
|
77
|
+
default: they move roving focus and select the focused tab. Pass
|
|
78
|
+
`activation: 'manual'` (or set `data-tab-activation="manual"` on a strip via the
|
|
79
|
+
`TabBar` `activation` prop, which overrides the option per bar) for
|
|
80
|
+
**manual activation**: arrow keys move roving focus only and the user selects
|
|
81
|
+
with Enter / Space (native on the tab button) or click. Use manual activation
|
|
82
|
+
when selecting a tab is a heavy or side-effecting action — e.g. a tab that loads
|
|
83
|
+
a project — so arrowing through the strip does not trigger it on every tab.
|
|
84
|
+
|
|
38
85
|
The application owns the ordered tab array, selection, panels, close policy,
|
|
39
86
|
routing, and persistence. On `onReorder`, synchronously render the reported
|
|
40
87
|
order so the helper can restore focus to the moved tab. Pair tabs with
|
|
@@ -51,6 +98,57 @@ re-renders. Keep labels unique and meaningful even when `content` shows only an
|
|
|
51
98
|
icon. Use tabs—not a segmented control—when choices switch page regions that
|
|
52
99
|
need `tab`/`tabpanel` semantics.
|
|
53
100
|
|
|
101
|
+
## TokenSearchField
|
|
102
|
+
|
|
103
|
+
`TokenSearchField` exposes the editable surface as a named `searchbox`. Each
|
|
104
|
+
chip is atomic (`contenteditable="false"`) and contains separately named edit
|
|
105
|
+
and remove buttons; the clear action is also named. Disabled fields publish
|
|
106
|
+
`aria-disabled` and stop editing without hiding the current expression. The
|
|
107
|
+
application owns query parsing and must announce result-count or loading
|
|
108
|
+
changes separately when that feedback is useful. Use `readTokenSearchField()`
|
|
109
|
+
to ignore the chip buttons' visible text when reading browser-edited content,
|
|
110
|
+
and `placeTokenSearchCaret()` to restore a text caret without landing inside a
|
|
111
|
+
chip. Call `wireTokenSearchFields()` once at a stable root so Enter submits
|
|
112
|
+
without inserting a contenteditable line break and keyboard chip deletion
|
|
113
|
+
restores focus plus the text-relative caret after controlled rendering replaces
|
|
114
|
+
the editor; the editor still wraps text visually at its inline edge. Editable text is DOM-owned between token changes; a clear handler empties
|
|
115
|
+
the editor's `textContent` before updating application state. Leading and
|
|
116
|
+
trailing controls share the first text line's fixed vertical center and remain
|
|
117
|
+
there as the editor wraps. In `collapsible` mode, the closed state is one named
|
|
118
|
+
iconic search button and the open state is the same named searchbox, whether
|
|
119
|
+
the field stands alone or is composed inside `ToolbarControlGroup`.
|
|
120
|
+
Applications set `expanded` while the field is focused, move focus from the
|
|
121
|
+
trigger into the revealed editor, and clear `expanded` only after focus leaves
|
|
122
|
+
the complete component. Text or tokens keep the field expanded even when that
|
|
123
|
+
transient state is false. Width animation is disabled under reduced motion.
|
|
124
|
+
|
|
125
|
+
When the field is composed with a caller-owned surface rendered outside it — a
|
|
126
|
+
suggestions listbox, date picker, or help popover — mark that surface (or wrap
|
|
127
|
+
it) with `data-token-search-keep-open`, or pass `collapsible.keepOpenOn(target)`,
|
|
128
|
+
so focus moving into it does not collapse an empty field. `wireTokenSearchFields`
|
|
129
|
+
can also, opt-in via `keyboard`, own atomic-chip editing keys: from a collapsed
|
|
130
|
+
caret with no selection, Backspace removes the token before the caret and Delete
|
|
131
|
+
the token after it (reported through `onRemoveToken` for the app to apply to its
|
|
132
|
+
controlled state), and ArrowRight moves the caret past a trailing chip so typed
|
|
133
|
+
text lands after it. An optional `onEdit({ id, editor })` fires on every editor
|
|
134
|
+
`input`, letting a caller drop its own `input` listener; the application still
|
|
135
|
+
owns query parsing and result-count/loading announcements.
|
|
136
|
+
|
|
137
|
+
## CollapsiblePanel / sidebar
|
|
138
|
+
|
|
139
|
+
`CollapsiblePanel` is a labeled `aside` region that is `aria-hidden` while
|
|
140
|
+
collapsed; its content slides via transform (disabled under reduced motion).
|
|
141
|
+
`CollapsiblePanelToggle` is a named button carrying `aria-expanded` and the
|
|
142
|
+
standard per-side collapse/expand glyph (`PanelLeft*` / `PanelRight*` /
|
|
143
|
+
`PanelBottom*`); place a collapse toggle inside the panel and an expand toggle in
|
|
144
|
+
an always-visible location so it is reachable while collapsed. `wireSidebar()`
|
|
145
|
+
moves focus into the panel when it opens and restores it to the trigger when it
|
|
146
|
+
closes. When a `deviceClass()` reports `compact`, an open panel becomes an overlay
|
|
147
|
+
with a dismissable backdrop, Escape and backdrop-click collapse it, and Tab is
|
|
148
|
+
trapped within it (the ARIA dialog pattern). The application owns each `collapsed`
|
|
149
|
+
signal, the panels, their sizes, and content; the wire may persist the collapsed
|
|
150
|
+
state per panel.
|
|
151
|
+
|
|
54
152
|
## Verification matrix
|
|
55
153
|
|
|
56
154
|
For each changed component, inspect default, hover, focus, disabled, selected/pressed, busy/error, long-content, wide, narrow, light, dark, increased-contrast, reduced-motion, keyboard-only, and 200%-zoom states where applicable. DOM order must match reading and focus order, with no clipping or unreachable action.
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
- [`CollapsiblePanel` + `wireSidebar`](collapsible-panel.md) — a standalone
|
|
14
|
+
collapsible rail or bottom drawer (with `CollapsiblePanelToggle`), outside a full
|
|
15
|
+
shell: the standard collapse animation, icon convention, and `wireSidebar`
|
|
16
|
+
semantics (focus, compact overlay, persistence).
|
|
17
|
+
|
|
18
|
+
Derive responsiveness from `deviceClass()`: `compact` (a handset or portrait
|
|
19
|
+
tablet) means "one pane at a time"; `atLeast('tablet')` / `atLeast('desktop')`
|
|
20
|
+
gate the roomier presentations.
|
|
21
|
+
|
|
22
|
+
## Decision matrix
|
|
23
|
+
|
|
24
|
+
| Situation | Layout | Device threshold |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| 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')` |
|
|
27
|
+
| 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` |
|
|
28
|
+
| Two related panes, selecting on the left updates the right | `SplitView` | two panes when not `compact`; collapses to `NavStack` (list → detail) on `compact` |
|
|
29
|
+
| 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 |
|
|
30
|
+
| 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')` |
|
|
31
|
+
|
|
32
|
+
### Worked examples
|
|
33
|
+
|
|
34
|
+
- **Settings screen (simple):** one `NavStack` entry per screen; push a subpage
|
|
35
|
+
on tap. No `SplitView`/`Workbench` — it is a single flow.
|
|
36
|
+
- **Mail (drill-down + two-pane):** `SplitView` with `list={<ThreadList/>}` and
|
|
37
|
+
`detail={<Message/>}`, `compact={device.value.compact}`,
|
|
38
|
+
`detailActive={selected != null}`. On desktop both panes show with a resizable
|
|
39
|
+
separator; on a phone it is a `NavStack` (threads → message, back clears the
|
|
40
|
+
selection).
|
|
41
|
+
- **IDE (complex tool):** `Workbench` with a left navigator rail, a right
|
|
42
|
+
inspector rail, and a bottom console drawer, each `collapsed` bound to a
|
|
43
|
+
signal. Only offer this `atLeast('desktop')`.
|
|
44
|
+
- **Social app (tabbed):** `TabScaffold` with Home / Search / Profile tabs, each
|
|
45
|
+
`content` a `NavStack`. On a tablet/desktop, render the same sections as a
|
|
46
|
+
`Workbench` left rail instead of a bottom bar.
|
|
47
|
+
|
|
48
|
+
## Dialogs
|
|
49
|
+
|
|
50
|
+
Pick the dialog's inner layout by the same complexity axis, then apply the device
|
|
51
|
+
class to how it is presented (compose with [`overlay`](../../docs/19-native-overlay-backing.md)):
|
|
52
|
+
|
|
53
|
+
- **desktop:** an inline dialog — a `SplitView` two-pane body, or a `NavStack`
|
|
54
|
+
for a wizard.
|
|
55
|
+
- **portrait tablet / handset:** present a `SplitView`/complex dialog as a
|
|
56
|
+
full-screen modal (its `compact` `NavStack` form).
|
|
57
|
+
- **landscape tablet:** a large partial-cover modal (does not need to go full
|
|
58
|
+
screen).
|
|
59
|
+
|
|
60
|
+
A `NavStack` works as a dialog body at every size — a wizard pushes and pops its
|
|
61
|
+
steps with cross-faded chrome.
|
package/docs/catalog.md
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# Catalog — a reusable component-gallery shell
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/catalog` is an opt-in, whole-screen shell for building a **component
|
|
4
|
+
catalog** — the collapsible category sidebar + titled preview stage + resources
|
|
5
|
+
footer that the kerf UI catalog itself uses. Point it at your own components and
|
|
6
|
+
you get the same shell without rebuilding it. Like the app layouts, it is a
|
|
7
|
+
subpath-only, tree-shakeable module that adds nothing to the main barrel.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @kerfjs/ui # kerfjs is a peer; @kerfjs/ui/select/register is needed only if entries use `related`
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `Catalog(props)` returns the shell as `SafeHtml` (a `<main class="kui-catalog">`).
|
|
14
|
+
It is **controlled and stateless**: your app owns the `active`, `collapsed`, and
|
|
15
|
+
`theme` signals and computes the preview `content` from `active` in its own
|
|
16
|
+
`mount()` render.
|
|
17
|
+
- `wireCatalog(root, options)` wires the interactions (sidebar selection, the
|
|
18
|
+
related-entry popup menu, and the collapse/theme toggles) with one delegated
|
|
19
|
+
listener set and returns a disposer; it can also mirror the active id into the
|
|
20
|
+
URL.
|
|
21
|
+
|
|
22
|
+
## What you supply
|
|
23
|
+
|
|
24
|
+
- **`sections`** — category-grouped entries: `{ category, entries: [{ id, name,
|
|
25
|
+
description?, resources?, related? }] }`. Each entry becomes a sidebar `ListItem`
|
|
26
|
+
under a `ListHeader` for its category.
|
|
27
|
+
- **`content`** — the rendered preview for the active entry. Keep a map of `id →
|
|
28
|
+
() => SafeHtml` in your app and call `renderers[active]()` in your render.
|
|
29
|
+
- **`brand`** — `{ title, subtitle?, logoUrl? }` for the sidebar header.
|
|
30
|
+
- **`secondarySections`** — an optional secondary "ecosystem" group shown below the
|
|
31
|
+
primary sections with a quieter treatment: `{ label, sections, collapsible?,
|
|
32
|
+
expanded? }`. When `collapsible`, the label is a disclosure toggle controlling
|
|
33
|
+
`expanded` (the app owns it; wire it with `wireCatalog`'s `onToggleSecondary`).
|
|
34
|
+
- Optional slots: `headerActions` (extra header controls), `sidebarFooter` (extra
|
|
35
|
+
sidebar content), and `status` (a footer status line).
|
|
36
|
+
|
|
37
|
+
Per-entry `resources` render as "open in new tab" links in the footer, and
|
|
38
|
+
`related` renders a "Related entries" popup menu — a `single` `ToolbarControlGroup`
|
|
39
|
+
around a `wa-dropdown` (grouped by each entry's `group`), so register its elements
|
|
40
|
+
with `@kerfjs/ui/select/register` when you use it.
|
|
41
|
+
|
|
42
|
+
## Preview examples
|
|
43
|
+
|
|
44
|
+
Compose each entry's `content` from `CatalogExample` (and `CatalogExampleStack`)
|
|
45
|
+
instead of hand-rolled example markup, so labels, notes, and left-edge alignment
|
|
46
|
+
stay consistent:
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { CatalogExample, CatalogExampleStack } from '@kerfjs/ui/catalog';
|
|
50
|
+
|
|
51
|
+
const buttonPreview = (
|
|
52
|
+
<CatalogExampleStack label="Button variants">
|
|
53
|
+
<CatalogExample label="Icon" note="A bare glyph." align="glyph">
|
|
54
|
+
<LucideIcon icon={Plus} name="plus" />
|
|
55
|
+
</CatalogExample>
|
|
56
|
+
<CatalogExample label="Control" align="inline-control">
|
|
57
|
+
<SegmentedControl id="view" label="View" value="list" choices={choices} />
|
|
58
|
+
</CatalogExample>
|
|
59
|
+
<CatalogExample label="In composition">
|
|
60
|
+
<ValueTable label="Metadata">{rows}</ValueTable>
|
|
61
|
+
</CatalogExample>
|
|
62
|
+
</CatalogExampleStack>
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`label` is optional — omit it for a bare specimen with no `ListHeader`. `align`
|
|
67
|
+
lines a specimen's visible left edge up with its label text: `'glyph'`
|
|
68
|
+
(16px) for a bare glyph/text specimen, `'inline-control'` (8px) for a control that
|
|
69
|
+
already carries ~8px of its own inline padding, and `'none'` (the default) for a
|
|
70
|
+
content-item/composition that already owns its geometry. The inset is published as
|
|
71
|
+
the `--kui-catalog-example-align` custom property so a debug overlay can exclude it
|
|
72
|
+
from a specimen's measured margin.
|
|
73
|
+
|
|
74
|
+
## Complete example
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { mount, signal } from 'kerfjs';
|
|
78
|
+
import { Catalog, type CatalogSection } from '@kerfjs/ui/catalog';
|
|
79
|
+
import { wireCatalog } from '@kerfjs/ui/wire-catalog';
|
|
80
|
+
import '@kerfjs/ui/styles.css'; // or import each primitive's CSS + @kerfjs/ui/catalog.css
|
|
81
|
+
|
|
82
|
+
// 1. Describe your components once.
|
|
83
|
+
const sections: CatalogSection[] = [
|
|
84
|
+
{
|
|
85
|
+
category: 'Controls',
|
|
86
|
+
entries: [
|
|
87
|
+
{ id: 'button', name: 'Button', description: 'A pressable control.',
|
|
88
|
+
resources: [{ label: 'Source', href: '/src/button.tsx', detail: 'src/button.tsx' }] },
|
|
89
|
+
{ id: 'field', name: 'Field', description: 'A labeled input.',
|
|
90
|
+
related: [{ id: 'button', name: 'Button', group: 'Used with' }] },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
{ category: 'Feedback', entries: [{ id: 'toast', name: 'Toast', description: 'A transient message.' }] },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
// 2. One preview render per entry id.
|
|
97
|
+
const renderers: Record<string, () => ReturnType<typeof Button>> = {
|
|
98
|
+
button: () => <Button label="Save" />,
|
|
99
|
+
field: () => <Field label="Name" />,
|
|
100
|
+
toast: () => <Toast>Saved</Toast>,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// 3. App-owned state (domain: which entry; transient: collapsed; global: theme).
|
|
104
|
+
const initial = new URLSearchParams(location.search).get('c') ?? sections[0].entries[0].id;
|
|
105
|
+
const active = signal(initial);
|
|
106
|
+
const collapsed = signal(false);
|
|
107
|
+
const theme = signal<'light' | 'dark'>('light');
|
|
108
|
+
|
|
109
|
+
const app = document.getElementById('app')!;
|
|
110
|
+
mount(app, () => (
|
|
111
|
+
<Catalog
|
|
112
|
+
brand={{ title: 'Acme UI', subtitle: 'Design system' }}
|
|
113
|
+
sections={sections}
|
|
114
|
+
active={active.value}
|
|
115
|
+
content={renderers[active.value]?.() ?? <></>}
|
|
116
|
+
collapsed={collapsed.value}
|
|
117
|
+
theme={theme.value}
|
|
118
|
+
/>
|
|
119
|
+
));
|
|
120
|
+
|
|
121
|
+
wireCatalog(app, {
|
|
122
|
+
onSelect: (id) => { active.value = id; },
|
|
123
|
+
onToggleSidebar: () => { collapsed.value = !collapsed.value; },
|
|
124
|
+
onToggleTheme: () => {
|
|
125
|
+
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
|
126
|
+
document.documentElement.dataset.theme = theme.value; // apply your theme however you like
|
|
127
|
+
},
|
|
128
|
+
urlParam: 'c', // mirror the active id into ?c=<id>
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Ownership boundary
|
|
133
|
+
|
|
134
|
+
`Catalog` renders the shell; the app owns everything stateful:
|
|
135
|
+
|
|
136
|
+
- **`active`** is domain state (which entry is shown) — the app's signal, updated in
|
|
137
|
+
`onSelect`, read to compute `content`.
|
|
138
|
+
- **`collapsed`** is transient UI — the app's signal, flipped in `onToggleSidebar`.
|
|
139
|
+
- **`theme`** is a global preference — the app's signal; `wireCatalog` only reports
|
|
140
|
+
the toggle, the app applies the theme (the shell reads `theme` to show the toggle's
|
|
141
|
+
opposite-state label). Omit `theme` to hide the toggle entirely.
|
|
142
|
+
|
|
143
|
+
## Custom action names
|
|
144
|
+
|
|
145
|
+
The shell emits `data-action="catalog-select"` (sidebar items),
|
|
146
|
+
`catalog-toggle-sidebar`, and `catalog-toggle-theme`. Override them with
|
|
147
|
+
`selectAction` / `toggleSidebarAction` / `toggleThemeAction` on `Catalog` (and the
|
|
148
|
+
matching options on `wireCatalog`) if they collide with your own action table.
|
|
149
|
+
|
|
150
|
+
## CSS
|
|
151
|
+
|
|
152
|
+
`Catalog` composes public primitives (`Toolbar`, `ListHeader`, `ListItem`, `Select`,
|
|
153
|
+
…). Import `@kerfjs/ui/styles.css` for the whole layer, or `@kerfjs/ui/catalog.css`
|
|
154
|
+
plus each composed primitive's CSS. The shell is theme-aware and responsive: it
|
|
155
|
+
stacks the sidebar above the detail below ~832px and hides it when collapsed.
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# CollapsiblePanel + wireSidebar — reusable sidebar / drawer support
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/collapsible-panel` and `@kerfjs/ui/wire-sidebar` are the standalone,
|
|
4
|
+
reusable pieces for an app's own **side rails** and **bottom drawers** — the same
|
|
5
|
+
collapse semantics the `Workbench` shell and the catalog sidebar use, but outside
|
|
6
|
+
a full shell so you can dock one panel wherever your layout needs it. They are
|
|
7
|
+
subpath-only, tree-shakeable modules that add nothing to the main barrel.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @kerfjs/ui # kerfjs is a peer
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Import the panel CSS (`@kerfjs/ui/collapsible-panel.css`) alongside `foundation.css`.
|
|
14
|
+
|
|
15
|
+
## The pieces
|
|
16
|
+
|
|
17
|
+
- **`CollapsiblePanel({ id, side, collapsed?, size?, label?, children })`** — the
|
|
18
|
+
docked panel: a `'left'` / `'right'` rail or a `'bottom'` drawer. It owns only
|
|
19
|
+
presentation. Collapsing snaps the panel's own size to zero in one reflow while
|
|
20
|
+
the fixed-size content slides out via `transform` (composited, clipped) — never
|
|
21
|
+
a per-frame width/height animation. The app owns the `collapsed` signal; `size`
|
|
22
|
+
overrides the CSS default width/height.
|
|
23
|
+
- **`CollapsiblePanelToggle({ side, collapsed, action, panelId?, label? })`** and
|
|
24
|
+
**`collapsiblePanelToggleIcon(side, collapsed)`** — the standard toggle
|
|
25
|
+
affordance and its icon convention, so every sidebar reads the same: `PanelLeft*`
|
|
26
|
+
for a left rail, `PanelRight*` for a right rail, `PanelBottom*` for a bottom
|
|
27
|
+
drawer — the `Close` glyph while open, the `Open` glyph while collapsed. Placement
|
|
28
|
+
is the app's: put a collapse toggle in the panel's own header and an expand toggle
|
|
29
|
+
somewhere always-visible (a toolbar) so it is reachable while collapsed.
|
|
30
|
+
- **`wireSidebar(root, { panels, deviceClass?, storage? })`** — the interaction
|
|
31
|
+
semantics. Each `panels` entry is `{ id, collapsed, toggleAction, storageKey? }`.
|
|
32
|
+
It:
|
|
33
|
+
- **toggles** the panel's `collapsed` signal when any `[data-action=toggleAction]`
|
|
34
|
+
button is clicked, and remembers the trigger;
|
|
35
|
+
- **manages focus** — moves focus into the panel when it opens, and restores it
|
|
36
|
+
to the trigger when it closes;
|
|
37
|
+
- **presents a compact overlay** when `deviceClass.compact` is true (pass a
|
|
38
|
+
`deviceClass()` signal): the open panel floats over the content with a
|
|
39
|
+
dismissable backdrop, Escape and backdrop-click collapse it, and Tab is trapped
|
|
40
|
+
within the panel (the ARIA dialog pattern);
|
|
41
|
+
- **persists** the collapsed state to `storage` (default `localStorage`) under
|
|
42
|
+
`storageKey`, seeding the signal on wire-up.
|
|
43
|
+
|
|
44
|
+
Returns a disposer. Retain it and call it on teardown.
|
|
45
|
+
|
|
46
|
+
## Example
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
import { signal, mount } from 'kerfjs';
|
|
50
|
+
import { deviceClass } from '@kerfjs/ui/device-class';
|
|
51
|
+
import { CollapsiblePanel, CollapsiblePanelToggle } from '@kerfjs/ui/collapsible-panel';
|
|
52
|
+
import { wireSidebar } from '@kerfjs/ui/wire-sidebar';
|
|
53
|
+
import '@kerfjs/ui/collapsible-panel.css';
|
|
54
|
+
|
|
55
|
+
const navCollapsed = signal(false);
|
|
56
|
+
const device = deviceClass();
|
|
57
|
+
|
|
58
|
+
const app = document.querySelector('#app')!;
|
|
59
|
+
mount(app, () => (
|
|
60
|
+
<div class="layout">
|
|
61
|
+
<CollapsiblePanel id="nav" side="left" collapsed={navCollapsed.value} label="Navigator">
|
|
62
|
+
<header>
|
|
63
|
+
<CollapsiblePanelToggle side="left" collapsed={navCollapsed.value} action="toggle-nav" panelId="nav" />
|
|
64
|
+
</header>
|
|
65
|
+
{/* nav items */}
|
|
66
|
+
</CollapsiblePanel>
|
|
67
|
+
<main>
|
|
68
|
+
{navCollapsed.value && <CollapsiblePanelToggle side="left" collapsed action="toggle-nav" label="Show navigator" />}
|
|
69
|
+
{/* content */}
|
|
70
|
+
</main>
|
|
71
|
+
</div>
|
|
72
|
+
));
|
|
73
|
+
|
|
74
|
+
const stop = wireSidebar(app, {
|
|
75
|
+
panels: [{ id: 'nav', collapsed: navCollapsed, toggleAction: 'toggle-nav', storageKey: 'app.nav-collapsed' }],
|
|
76
|
+
deviceClass: device,
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## When to use which
|
|
81
|
+
|
|
82
|
+
- One or two independent rails / a drawer you place yourself → **`CollapsiblePanel` +
|
|
83
|
+
`wireSidebar`**.
|
|
84
|
+
- A whole Xcode-like workspace (left rail + right rail + bottom drawer + work area
|
|
85
|
+
in one shell) → **[`Workbench`](workbench.md)**, which owns the layout and the same
|
|
86
|
+
collapse animation.
|
|
87
|
+
- Drag-to-resize a panel → compose **[`ResizableRegion`](../src/resizable-region.tsx)**
|
|
88
|
+
/ `wireResizableRegions`; the app owns the size signal.
|
|
89
|
+
|
|
90
|
+
The app still owns everything domain-specific — which panels exist, their order,
|
|
91
|
+
sizes, content, and any per-project persistence — exactly as with the other layouts.
|
|
92
|
+
|
|
93
|
+
## Recipe and coverage
|
|
94
|
+
|
|
95
|
+
The catalog ships a runnable **Collapsible sidebar** recipe — a left rail and a
|
|
96
|
+
bottom drawer with the standard toggles, the compact overlay, and per-panel
|
|
97
|
+
persistence: [open it](../ux-demo/?component=recipe-collapsible-sidebar) or read
|
|
98
|
+
[`recipes.md`](recipes.md#collapsible-sidebar) · [TSX source](../ux-demo/recipes/collapsible-sidebar.tsx).
|
|
99
|
+
It is covered end-to-end across Chromium, Firefox, and WebKit by
|
|
100
|
+
`tests/browser/collapsible-sidebar-recipe.spec.ts` (collapse/expand, focus
|
|
101
|
+
move/restore, the compact overlay + Escape/backdrop dismiss, and the Tab trap),
|
|
102
|
+
alongside the component/wire unit tests.
|