@kerfjs/ui 5.0.0-beta.4 → 5.0.0-beta.7
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 +9 -8
- package/ai/component-catalog.json +379 -174
- package/ai/public-api-signatures-v1.md +387 -33
- package/ai/skill.md +15 -7
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/app-tab.d.ts +3 -1
- package/dist/app-tab.js +2 -1
- package/dist/browser/app-tab.js +1 -0
- package/dist/browser/menu-action-row.js +1 -0
- package/dist/browser/menu-header.js +1 -0
- package/dist/browser/menu-item.js +1 -0
- package/dist/browser/panel-header.js +7 -0
- package/dist/browser/segmented-control.js +1 -0
- package/dist/browser/select.js +1 -0
- package/dist/browser/skeleton.js +3 -0
- package/dist/browser/state-banner.js +1 -0
- package/dist/browser/toolbar-text.js +1 -0
- package/dist/browser/value-table.js +1 -0
- package/dist/chunk-2RJBFNB6.js +172 -0
- package/dist/chunk-2RJBFNB6.js.map +1 -0
- package/dist/{chunk-XZXRLY5Q.js → chunk-2Z5TWHKQ.js} +7 -6
- package/dist/chunk-2Z5TWHKQ.js.map +1 -0
- package/dist/{chunk-EGHL5S4P.js → chunk-3YD6ZNRA.js} +8 -7
- package/dist/chunk-3YD6ZNRA.js.map +1 -0
- package/dist/chunk-B24C4A6T.js +22 -0
- package/dist/chunk-B24C4A6T.js.map +1 -0
- package/dist/{chunk-NPPD2A6O.js → chunk-H6URQGJI.js} +8 -6
- package/dist/chunk-H6URQGJI.js.map +1 -0
- package/dist/chunk-I7INRJO4.js +17 -0
- package/dist/chunk-I7INRJO4.js.map +1 -0
- package/dist/chunk-KJYXOTG3.js +10 -0
- package/dist/chunk-KJYXOTG3.js.map +1 -0
- package/dist/{chunk-CTQKI2XL.js → chunk-LC3YSAZC.js} +14 -3
- package/dist/chunk-LC3YSAZC.js.map +1 -0
- package/dist/chunk-N3SUXLLS.js +23 -0
- package/dist/chunk-N3SUXLLS.js.map +1 -0
- package/dist/{chunk-R6JHHBKQ.js → chunk-PONXLV5U.js} +5 -5
- package/dist/chunk-PONXLV5U.js.map +1 -0
- package/dist/chunk-TZNCCM5H.js +20 -0
- package/dist/chunk-TZNCCM5H.js.map +1 -0
- package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
- package/dist/chunk-YGN7OMLT.js.map +1 -0
- package/dist/chunk-YKUUZPED.js +24 -0
- package/dist/chunk-YKUUZPED.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 +3 -3
- package/dist/index.js +15 -15
- package/dist/menu-action-row.d.ts +3 -1
- package/dist/menu-action-row.js +2 -1
- package/dist/menu-header.d.ts +3 -1
- package/dist/menu-header.js +2 -1
- package/dist/menu-item.d.ts +3 -1
- package/dist/menu-item.js +2 -1
- 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 +28 -0
- package/dist/panel-header.js +7 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/segmented-control.d.ts +3 -1
- package/dist/segmented-control.js +2 -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/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 +64 -0
- package/dist/styles/select.css +42 -0
- package/dist/styles/skeleton.css +31 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/styles.css +2 -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 +6 -2
- package/dist/toolbar-text.js +2 -1
- package/dist/value-table.d.ts +3 -1
- package/dist/value-table.js +2 -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 +46 -9
- package/docs/component-selection.md +57 -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 +116 -43
- package/package.json +43 -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/dialog-header.js +0 -5
- 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-CTQKI2XL.js.map +0 -1
- package/dist/chunk-EGHL5S4P.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js +0 -10
- 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/chunk-NPPD2A6O.js.map +0 -1
- package/dist/chunk-R6JHHBKQ.js.map +0 -1
- package/dist/chunk-S3TPM2LP.js +0 -23
- package/dist/chunk-S3TPM2LP.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-XZXRLY5Q.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
|
@@ -43,6 +43,14 @@
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
.kui-toolbar-control-group[data-appearance="borderless"] {
|
|
46
|
+
/* A borderless group has no base fill to lift from, so the default
|
|
47
|
+
surface-colored hover/selected background is invisible against the page.
|
|
48
|
+
Use a visible neutral tint so buttons still show hover/press feedback
|
|
49
|
+
(KF-WZDQS8). */
|
|
50
|
+
--kui-toolbar-control-hover-background: var(--kui-color-neutral-fill-normal);
|
|
51
|
+
--kui-toolbar-control-selected-background: var(
|
|
52
|
+
--kui-color-neutral-fill-normal
|
|
53
|
+
);
|
|
46
54
|
border-color: transparent;
|
|
47
55
|
background: transparent;
|
|
48
56
|
}
|
|
@@ -107,6 +115,10 @@
|
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
.kui-toolbar-control-group wa-button::part(base) {
|
|
118
|
+
/* Match min-width to the 40px height so an icon-only button's highlight is at
|
|
119
|
+
least a circle, not a vertical oval — the host is 40px wide but the base
|
|
120
|
+
part was sizing to its ~32px content. Text/caret buttons grow past it. */
|
|
121
|
+
min-width: 2.5rem;
|
|
110
122
|
min-height: 2.5rem;
|
|
111
123
|
box-sizing: border-box;
|
|
112
124
|
border: 1px solid transparent;
|
|
@@ -133,14 +145,52 @@
|
|
|
133
145
|
background: transparent;
|
|
134
146
|
}
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
|
|
148
|
+
/* A group holding exactly one control behaves like `single` even without the
|
|
149
|
+
flag: the whole group background changes on hover instead of highlighting an
|
|
150
|
+
inner layer inset from the border (KF-J9P1ZD — a PanelHeader's lone Close/
|
|
151
|
+
Done action). */
|
|
152
|
+
.kui-toolbar-control-group:not([data-single="true"]):has(
|
|
153
|
+
> :is(button, wa-button):only-child
|
|
154
|
+
):hover {
|
|
155
|
+
background: var(--kui-toolbar-control-hover-background);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.kui-toolbar-control-group:not([data-single="true"]):has(
|
|
159
|
+
> :is(button, wa-button):only-child
|
|
160
|
+
)
|
|
161
|
+
> :is(button, wa-button):hover,
|
|
162
|
+
.kui-toolbar-control-group:not([data-single="true"]):has(
|
|
163
|
+
> :is(button, wa-button):only-child
|
|
164
|
+
)
|
|
165
|
+
wa-button:hover::part(base) {
|
|
166
|
+
border-color: transparent;
|
|
167
|
+
background: transparent;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/* A plain <button> child paints its own box for the selected state. */
|
|
171
|
+
.kui-toolbar-control-group[data-single="false"] > button[aria-pressed="true"] {
|
|
138
172
|
border-color: var(--kui-toolbar-control-selected-border);
|
|
139
173
|
color: var(--kui-toolbar-control-selected-color);
|
|
140
174
|
background: var(--kui-toolbar-control-selected-background);
|
|
141
175
|
box-shadow: var(--kui-toolbar-control-selected-shadow);
|
|
142
176
|
}
|
|
143
177
|
|
|
178
|
+
/* A wa-button's visual is ::part(base), not the outer 40px host box (which has no
|
|
179
|
+
pill radius). Paint the selected background/border/shadow on ::part(base) —
|
|
180
|
+
mirroring the hover rule — so the selected state matches the pill radius/size
|
|
181
|
+
and stays inside the group's border instead of overflowing it as an oversized
|
|
182
|
+
square (KF-5BDDQ9). The selected text color rides on the host and inherits in. */
|
|
183
|
+
.kui-toolbar-control-group[data-single="false"]
|
|
184
|
+
> wa-button[aria-pressed="true"] {
|
|
185
|
+
color: var(--kui-toolbar-control-selected-color);
|
|
186
|
+
}
|
|
187
|
+
.kui-toolbar-control-group[data-single="false"]
|
|
188
|
+
> wa-button[aria-pressed="true"]::part(base) {
|
|
189
|
+
border-color: var(--kui-toolbar-control-selected-border);
|
|
190
|
+
background: var(--kui-toolbar-control-selected-background);
|
|
191
|
+
box-shadow: var(--kui-toolbar-control-selected-shadow);
|
|
192
|
+
}
|
|
193
|
+
|
|
144
194
|
.kui-toolbar-control-group > :is(button, wa-button):focus-visible {
|
|
145
195
|
outline: var(--kui-focus-ring);
|
|
146
196
|
outline-offset: 1px;
|
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
align-items: center;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
.kui-toolbar-text[data-size="xlarge"] {
|
|
18
|
+
/* Matches the page/panel title: the same responsive clamp so a PanelHeader
|
|
19
|
+
title reads at the page-title scale without a bespoke override. Height grows
|
|
20
|
+
to the larger text (the fixed 40px would clip its ascenders). */
|
|
21
|
+
height: auto;
|
|
22
|
+
min-height: 2.75rem;
|
|
23
|
+
color: var(--kui-color-text);
|
|
24
|
+
font-size: clamp(var(--kui-font-l), 2.3vw, var(--kui-font-2xl));
|
|
25
|
+
letter-spacing: -0.025em;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
.kui-toolbar-text[data-size="large"] {
|
|
18
29
|
font-size: var(--kui-font-m);
|
|
19
30
|
letter-spacing: -0.015em;
|
package/dist/styles/toolbar.css
CHANGED
|
@@ -33,16 +33,25 @@
|
|
|
33
33
|
gap: var(--kui-toolbar-gap, 0.5rem);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/* Pin each zone to its column so a display:none (empty) leading or center zone
|
|
37
|
+
doesn't let the trailing zone auto-place into the first column — which left an
|
|
38
|
+
extra gap + empty track to the trailing zone's right (KF-5BEF2M). The trailing
|
|
39
|
+
zone anchors to the last column via -2/-1, so it stays flush right for both
|
|
40
|
+
the 3- and 2-column layouts, and still stacks under the leading zone when a
|
|
41
|
+
consumer collapses the toolbar to a single column. */
|
|
36
42
|
.kui-toolbar__leading {
|
|
43
|
+
grid-column: 1;
|
|
37
44
|
overflow: hidden;
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
.kui-toolbar__center {
|
|
41
48
|
justify-content: center;
|
|
49
|
+
grid-column: 2;
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
.kui-toolbar__trailing {
|
|
45
53
|
justify-content: flex-end;
|
|
54
|
+
grid-column: -2 / -1;
|
|
46
55
|
}
|
|
47
56
|
|
|
48
57
|
.kui-toolbar[data-has-center="false"] .kui-toolbar__center,
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
.kui-workbench {
|
|
2
|
+
display: flex;
|
|
3
|
+
min-width: 0;
|
|
4
|
+
min-height: 0;
|
|
5
|
+
height: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
background: var(--kui-color-surface);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* Side rails. Collapsing snaps the flex track to zero (one reflow) while the
|
|
11
|
+
fixed-width content slides out via transform (composited, clipped) — never a
|
|
12
|
+
per-frame width animation. See docs/18 / the KF-7QKJRK catalog sidebar. */
|
|
13
|
+
.kui-workbench__rail {
|
|
14
|
+
flex: 0 0 var(--kui-workbench-rail-width, 17.5rem);
|
|
15
|
+
min-width: 0;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
background: var(--kui-color-surface);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.kui-workbench__rail--left {
|
|
21
|
+
border-inline-end: 1px solid var(--kui-color-border);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.kui-workbench__rail--right {
|
|
25
|
+
border-inline-start: 1px solid var(--kui-color-border);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.kui-workbench__rail[data-collapsed="true"] {
|
|
29
|
+
flex-basis: 0;
|
|
30
|
+
border-inline: 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.kui-workbench__center {
|
|
34
|
+
display: flex;
|
|
35
|
+
flex: 1;
|
|
36
|
+
min-width: 0;
|
|
37
|
+
min-height: 0;
|
|
38
|
+
overflow: hidden;
|
|
39
|
+
flex-direction: column;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.kui-workbench__main {
|
|
43
|
+
flex: 1;
|
|
44
|
+
min-height: 0;
|
|
45
|
+
overflow: auto;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Bottom drawer — the vertical analogue of a rail. */
|
|
49
|
+
.kui-workbench__drawer {
|
|
50
|
+
flex: 0 0 var(--kui-workbench-drawer-height, 13.75rem);
|
|
51
|
+
min-height: 0;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
border-block-start: 1px solid var(--kui-color-border);
|
|
54
|
+
background: var(--kui-color-surface);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kui-workbench__drawer[data-collapsed="true"] {
|
|
58
|
+
flex-basis: 0;
|
|
59
|
+
border-block-start: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* The fixed-size sliding content. The rail/drawer keeps a definite size on its
|
|
63
|
+
content so the transform reads as a slide, not a squeeze. */
|
|
64
|
+
.kui-workbench__rail .kui-workbench__panel-content {
|
|
65
|
+
width: var(--kui-workbench-rail-width, 17.5rem);
|
|
66
|
+
height: 100%;
|
|
67
|
+
transition: transform 200ms ease;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.kui-workbench__drawer .kui-workbench__panel-content {
|
|
71
|
+
width: 100%;
|
|
72
|
+
height: var(--kui-workbench-drawer-height, 13.75rem);
|
|
73
|
+
transition: transform 200ms ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.kui-workbench__rail--left[data-collapsed="true"]
|
|
77
|
+
.kui-workbench__panel-content {
|
|
78
|
+
transform: translateX(-100%);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.kui-workbench__rail--right[data-collapsed="true"]
|
|
82
|
+
.kui-workbench__panel-content {
|
|
83
|
+
transform: translateX(100%);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.kui-workbench__drawer[data-collapsed="true"] .kui-workbench__panel-content {
|
|
87
|
+
transform: translateY(100%);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@media (prefers-reduced-motion: reduce) {
|
|
91
|
+
.kui-workbench__panel-content {
|
|
92
|
+
transition: none;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SafeHtml } from 'kerfjs';
|
|
2
|
+
|
|
3
|
+
interface TabScaffoldTab {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
/** Decorative icon shown above the label in the bottom bar. */
|
|
7
|
+
icon?: SafeHtml;
|
|
8
|
+
/** The tab's content — typically a `NavStack` so each tab keeps its own stack. */
|
|
9
|
+
content: SafeHtml;
|
|
10
|
+
}
|
|
11
|
+
interface TabScaffoldProps {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Accessible name for the tab bar. */
|
|
14
|
+
label: string;
|
|
15
|
+
tabs: TabScaffoldTab[];
|
|
16
|
+
/** The controlled active tab id (the app owns selection). */
|
|
17
|
+
active: string;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A mobile-first, iOS-like bottom tab scaffold: a bottom tab bar that switches
|
|
22
|
+
* between major sections, each tab keeping its own content (usually a `NavStack`)
|
|
23
|
+
* mounted so its stack and scroll survive a switch. Controlled — the app owns
|
|
24
|
+
* `active`; wire selection with `@kerfjs/ui/wire-tab-scaffold`'s `wireTabScaffold`.
|
|
25
|
+
* On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of
|
|
26
|
+
* a bottom bar. See `docs/23-app-layouts.md` §3.4.
|
|
27
|
+
*/
|
|
28
|
+
declare function TabScaffold({ id, label, tabs, active, className }: TabScaffoldProps): SafeHtml;
|
|
29
|
+
|
|
30
|
+
export { TabScaffold, type TabScaffoldProps, type TabScaffoldTab };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsxs, jsx } from 'kerfjs/jsx-runtime';
|
|
2
|
+
|
|
3
|
+
// src/tab-scaffold.tsx
|
|
4
|
+
function TabScaffold({ id, label, tabs, active, className = "" }) {
|
|
5
|
+
return /* @__PURE__ */ jsxs("section", { class: `kui-tab-scaffold ${className}`.trim(), id, "data-component": "tab-scaffold", "data-tab-scaffold-id": id, children: [
|
|
6
|
+
/* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scenes", children: tabs.map((tab) => /* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scene", "data-tab-scaffold-scene": tab.id, "data-active": String(tab.id === active), "aria-hidden": String(tab.id !== active), children: tab.content })) }),
|
|
7
|
+
/* @__PURE__ */ jsx("nav", { class: "kui-tab-scaffold__bar", role: "tablist", "aria-label": label, children: tabs.map((tab) => /* @__PURE__ */ jsxs("button", { type: "button", class: "kui-tab-scaffold__tab", role: "tab", "data-tab-scaffold-tab": tab.id, "aria-selected": String(tab.id === active), tabindex: tab.id === active ? "0" : "-1", children: [
|
|
8
|
+
tab.icon && /* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-icon", "aria-hidden": "true", children: tab.icon }),
|
|
9
|
+
/* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-label", children: tab.label })
|
|
10
|
+
] })) })
|
|
11
|
+
] });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export { TabScaffold };
|
|
15
|
+
|
|
16
|
+
//# sourceMappingURL=tab-scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/tab-scaffold.tsx"],"names":[],"mappings":";;;AA6BO,SAAS,WAAA,CAAY,EAAE,EAAA,EAAI,KAAA,EAAO,MAAM,MAAA,EAAQ,SAAA,GAAY,IAAG,EAAqB;AACzF,EAAA,uBAAO,IAAA,CAAC,SAAA,EAAA,EAAQ,KAAA,EAAO,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,EAAA,EAAQ,gBAAA,EAAe,cAAA,EAAe,sBAAA,EAAsB,EAAA,EACzH,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,0BAAA,EACR,QAAA,EAAA,IAAA,CAAK,GAAA,CAAI,CAAC,GAAA,qBAAQ,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,yBAAA,EAA0B,yBAAA,EAAyB,GAAA,CAAI,EAAA,EAAI,aAAA,EAAa,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,CAAA,EAAG,aAAA,EAAa,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,CAAA,EAAI,QAAA,EAAA,GAAA,CAAI,OAAA,EAAQ,CAAM,CAAA,EAC9L,CAAA;AAAA,oBACA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,uBAAA,EAAwB,MAAK,SAAA,EAAU,YAAA,EAAY,KAAA,EAC3D,QAAA,EAAA,IAAA,CAAK,IAAI,CAAC,GAAA,qBAAQ,IAAA,CAAC,QAAA,EAAA,EAAO,MAAK,QAAA,EAAS,KAAA,EAAM,uBAAA,EAAwB,IAAA,EAAK,KAAA,EAAM,uBAAA,EAAuB,GAAA,CAAI,EAAA,EAAI,iBAAe,MAAA,CAAO,GAAA,CAAI,EAAA,KAAO,MAAM,GAAG,QAAA,EAAU,GAAA,CAAI,EAAA,KAAO,MAAA,GAAS,MAAM,IAAA,EAC5L,QAAA,EAAA;AAAA,MAAA,GAAA,CAAI,IAAA,wBAAS,MAAA,EAAA,EAAK,KAAA,EAAM,8BAA6B,aAAA,EAAY,MAAA,EAAQ,cAAI,IAAA,EAAK,CAAA;AAAA,sBACnF,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAM,6BAAA,EAA+B,cAAI,KAAA,EAAM;AAAA,KAAA,EACvD,CAAS,CAAA,EACX;AAAA,GAAA,EACF,CAAA;AACF","file":"tab-scaffold.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nexport interface TabScaffoldTab {\n id: string;\n label: string;\n /** Decorative icon shown above the label in the bottom bar. */\n icon?: SafeHtml;\n /** The tab's content — typically a `NavStack` so each tab keeps its own stack. */\n content: SafeHtml;\n}\n\nexport interface TabScaffoldProps {\n id: string;\n /** Accessible name for the tab bar. */\n label: string;\n tabs: TabScaffoldTab[];\n /** The controlled active tab id (the app owns selection). */\n active: string;\n className?: string;\n}\n\n/**\n * A mobile-first, iOS-like bottom tab scaffold: a bottom tab bar that switches\n * between major sections, each tab keeping its own content (usually a `NavStack`)\n * mounted so its stack and scroll survive a switch. Controlled — the app owns\n * `active`; wire selection with `@kerfjs/ui/wire-tab-scaffold`'s `wireTabScaffold`.\n * On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of\n * a bottom bar. See `docs/23-app-layouts.md` §3.4.\n */\nexport function TabScaffold({ id, label, tabs, active, className = '' }: TabScaffoldProps) {\n return <section class={`kui-tab-scaffold ${className}`.trim()} id={id} data-component=\"tab-scaffold\" data-tab-scaffold-id={id}>\n <div class=\"kui-tab-scaffold__scenes\">\n {tabs.map((tab) => <div class=\"kui-tab-scaffold__scene\" data-tab-scaffold-scene={tab.id} data-active={String(tab.id === active)} aria-hidden={String(tab.id !== active)}>{tab.content}</div>)}\n </div>\n <nav class=\"kui-tab-scaffold__bar\" role=\"tablist\" aria-label={label}>\n {tabs.map((tab) => <button type=\"button\" class=\"kui-tab-scaffold__tab\" role=\"tab\" data-tab-scaffold-tab={tab.id} aria-selected={String(tab.id === active)} tabindex={tab.id === active ? '0' : '-1'}>\n {tab.icon && <span class=\"kui-tab-scaffold__tab-icon\" aria-hidden=\"true\">{tab.icon}</span>}\n <span class=\"kui-tab-scaffold__tab-label\">{tab.label}</span>\n </button>)}\n </nav>\n </section>;\n}\n"]}
|
package/dist/toolbar-text.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
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;
|
|
10
|
+
/** Render the text as an unanimated loading skeleton instead of its value. */
|
|
11
|
+
placeholder?: boolean;
|
|
8
12
|
}
|
|
9
|
-
declare function ToolbarText({ text, size, className }: ToolbarTextProps): kerfjs.SafeHtml;
|
|
13
|
+
declare function ToolbarText({ text, size, className, id, placeholder }: ToolbarTextProps): kerfjs.SafeHtml;
|
|
10
14
|
|
|
11
15
|
export { ToolbarText, type ToolbarTextProps, type ToolbarTextSize };
|
package/dist/toolbar-text.js
CHANGED
package/dist/value-table.d.ts
CHANGED
|
@@ -5,8 +5,10 @@ interface ValueTableRowProps {
|
|
|
5
5
|
value: string | SafeHtml;
|
|
6
6
|
icon?: SafeHtml;
|
|
7
7
|
className?: string;
|
|
8
|
+
/** Render the value as an unanimated loading skeleton, keeping the field label. */
|
|
9
|
+
placeholder?: boolean;
|
|
8
10
|
}
|
|
9
|
-
declare function ValueTableRow({ label, value, icon, className }: ValueTableRowProps): SafeHtml;
|
|
11
|
+
declare function ValueTableRow({ label, value, icon, className, placeholder }: ValueTableRowProps): SafeHtml;
|
|
10
12
|
|
|
11
13
|
interface ValueTableProps {
|
|
12
14
|
label: string;
|
package/dist/value-table.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
|
|