@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.
Files changed (156) hide show
  1. package/README.md +9 -8
  2. package/ai/component-catalog.json +379 -174
  3. package/ai/public-api-signatures-v1.md +387 -33
  4. package/ai/skill.md +15 -7
  5. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  6. package/dist/app-tab.d.ts +3 -1
  7. package/dist/app-tab.js +2 -1
  8. package/dist/browser/app-tab.js +1 -0
  9. package/dist/browser/menu-action-row.js +1 -0
  10. package/dist/browser/menu-header.js +1 -0
  11. package/dist/browser/menu-item.js +1 -0
  12. package/dist/browser/panel-header.js +7 -0
  13. package/dist/browser/segmented-control.js +1 -0
  14. package/dist/browser/select.js +1 -0
  15. package/dist/browser/skeleton.js +3 -0
  16. package/dist/browser/state-banner.js +1 -0
  17. package/dist/browser/toolbar-text.js +1 -0
  18. package/dist/browser/value-table.js +1 -0
  19. package/dist/chunk-2RJBFNB6.js +172 -0
  20. package/dist/chunk-2RJBFNB6.js.map +1 -0
  21. package/dist/{chunk-XZXRLY5Q.js → chunk-2Z5TWHKQ.js} +7 -6
  22. package/dist/chunk-2Z5TWHKQ.js.map +1 -0
  23. package/dist/{chunk-EGHL5S4P.js → chunk-3YD6ZNRA.js} +8 -7
  24. package/dist/chunk-3YD6ZNRA.js.map +1 -0
  25. package/dist/chunk-B24C4A6T.js +22 -0
  26. package/dist/chunk-B24C4A6T.js.map +1 -0
  27. package/dist/{chunk-NPPD2A6O.js → chunk-H6URQGJI.js} +8 -6
  28. package/dist/chunk-H6URQGJI.js.map +1 -0
  29. package/dist/chunk-I7INRJO4.js +17 -0
  30. package/dist/chunk-I7INRJO4.js.map +1 -0
  31. package/dist/chunk-KJYXOTG3.js +10 -0
  32. package/dist/chunk-KJYXOTG3.js.map +1 -0
  33. package/dist/{chunk-CTQKI2XL.js → chunk-LC3YSAZC.js} +14 -3
  34. package/dist/chunk-LC3YSAZC.js.map +1 -0
  35. package/dist/chunk-N3SUXLLS.js +23 -0
  36. package/dist/chunk-N3SUXLLS.js.map +1 -0
  37. package/dist/{chunk-R6JHHBKQ.js → chunk-PONXLV5U.js} +5 -5
  38. package/dist/chunk-PONXLV5U.js.map +1 -0
  39. package/dist/chunk-TZNCCM5H.js +20 -0
  40. package/dist/chunk-TZNCCM5H.js.map +1 -0
  41. package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
  42. package/dist/chunk-YGN7OMLT.js.map +1 -0
  43. package/dist/chunk-YKUUZPED.js +24 -0
  44. package/dist/chunk-YKUUZPED.js.map +1 -0
  45. package/dist/device-class.d.ts +62 -0
  46. package/dist/device-class.js +75 -0
  47. package/dist/device-class.js.map +1 -0
  48. package/dist/index.d.ts +3 -3
  49. package/dist/index.js +15 -15
  50. package/dist/menu-action-row.d.ts +3 -1
  51. package/dist/menu-action-row.js +2 -1
  52. package/dist/menu-header.d.ts +3 -1
  53. package/dist/menu-header.js +2 -1
  54. package/dist/menu-item.d.ts +3 -1
  55. package/dist/menu-item.js +2 -1
  56. package/dist/nav-stack.d.ts +38 -0
  57. package/dist/nav-stack.js +5 -0
  58. package/dist/nav-stack.js.map +1 -0
  59. package/dist/panel-header.d.ts +28 -0
  60. package/dist/panel-header.js +7 -0
  61. package/dist/panel-header.js.map +1 -0
  62. package/dist/segmented-control.d.ts +3 -1
  63. package/dist/segmented-control.js +2 -1
  64. package/dist/select.d.ts +5 -2
  65. package/dist/select.js +2 -1
  66. package/dist/skeleton.d.ts +24 -0
  67. package/dist/skeleton.js +3 -0
  68. package/dist/skeleton.js.map +1 -0
  69. package/dist/split-view.d.ts +42 -0
  70. package/dist/split-view.js +21 -0
  71. package/dist/split-view.js.map +1 -0
  72. package/dist/state-banner.d.ts +3 -1
  73. package/dist/state-banner.js +2 -1
  74. package/dist/styles/foundation.css +21 -0
  75. package/dist/styles/menu-header.css +19 -2
  76. package/dist/styles/nav-stack.css +112 -0
  77. package/dist/styles/panel-header.css +64 -0
  78. package/dist/styles/select.css +42 -0
  79. package/dist/styles/skeleton.css +31 -0
  80. package/dist/styles/split-view.css +35 -0
  81. package/dist/styles/styles.css +2 -2
  82. package/dist/styles/tab-scaffold.css +84 -0
  83. package/dist/styles/toolbar-control-group.css +52 -2
  84. package/dist/styles/toolbar-text.css +11 -0
  85. package/dist/styles/toolbar.css +9 -0
  86. package/dist/styles/workbench.css +94 -0
  87. package/dist/tab-scaffold.d.ts +30 -0
  88. package/dist/tab-scaffold.js +16 -0
  89. package/dist/tab-scaffold.js.map +1 -0
  90. package/dist/toolbar-text.d.ts +6 -2
  91. package/dist/toolbar-text.js +2 -1
  92. package/dist/value-table.d.ts +3 -1
  93. package/dist/value-table.js +2 -1
  94. package/dist/wire-nav-stack.d.ts +15 -0
  95. package/dist/wire-nav-stack.js +88 -0
  96. package/dist/wire-nav-stack.js.map +1 -0
  97. package/dist/wire-tab-scaffold.d.ts +11 -0
  98. package/dist/wire-tab-scaffold.js +16 -0
  99. package/dist/wire-tab-scaffold.js.map +1 -0
  100. package/dist/wire-token-search-fields.d.ts +46 -4
  101. package/dist/wire-token-search-fields.js +1 -1
  102. package/dist/workbench.d.ts +33 -0
  103. package/dist/workbench.js +17 -0
  104. package/dist/workbench.js.map +1 -0
  105. package/docs/accessibility.md +10 -10
  106. package/docs/app-layouts.md +57 -0
  107. package/docs/component-contract.md +46 -9
  108. package/docs/component-selection.md +57 -7
  109. package/docs/design-philosophy.md +23 -1
  110. package/docs/device-class.md +54 -0
  111. package/docs/layout.md +33 -8
  112. package/docs/nav-stack.md +47 -0
  113. package/docs/recipes.md +17 -5
  114. package/docs/split-view.md +49 -0
  115. package/docs/tab-scaffold.md +41 -0
  116. package/docs/ux-demo.md +3 -3
  117. package/docs/workbench.md +47 -0
  118. package/llms.txt +116 -43
  119. package/package.json +43 -11
  120. package/ux-demo/recipes/app-shell.tsx +3 -3
  121. package/ux-demo/recipes/composer-form.tsx +2 -2
  122. package/ux-demo/recipes/list-workspace-states.tsx +2 -2
  123. package/ux-demo/recipes/loaders.ts +2 -0
  124. package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
  125. package/ux-demo/recipes/mount-recipe.ts +3 -0
  126. package/ux-demo/recipes/navigation-stack.tsx +76 -0
  127. package/ux-demo/recipes/recipes.css +19 -8
  128. package/ux-demo/recipes/workspace-header.tsx +2 -2
  129. package/dist/browser/dialog-header.js +0 -5
  130. package/dist/browser/page-header.js +0 -3
  131. package/dist/chunk-2PES33HS.js +0 -13
  132. package/dist/chunk-2PES33HS.js.map +0 -1
  133. package/dist/chunk-CTQKI2XL.js.map +0 -1
  134. package/dist/chunk-EGHL5S4P.js.map +0 -1
  135. package/dist/chunk-GY5WH7TO.js +0 -10
  136. package/dist/chunk-GY5WH7TO.js.map +0 -1
  137. package/dist/chunk-H5AGGVU5.js +0 -75
  138. package/dist/chunk-H5AGGVU5.js.map +0 -1
  139. package/dist/chunk-K3G72I6D.js +0 -24
  140. package/dist/chunk-K3G72I6D.js.map +0 -1
  141. package/dist/chunk-NPPD2A6O.js.map +0 -1
  142. package/dist/chunk-R6JHHBKQ.js.map +0 -1
  143. package/dist/chunk-S3TPM2LP.js +0 -23
  144. package/dist/chunk-S3TPM2LP.js.map +0 -1
  145. package/dist/chunk-TRNBHPNW.js.map +0 -1
  146. package/dist/chunk-VHW5YNIB.js +0 -17
  147. package/dist/chunk-VHW5YNIB.js.map +0 -1
  148. package/dist/chunk-XZXRLY5Q.js.map +0 -1
  149. package/dist/dialog-header.d.ts +0 -15
  150. package/dist/dialog-header.js +0 -5
  151. package/dist/dialog-header.js.map +0 -1
  152. package/dist/page-header.d.ts +0 -9
  153. package/dist/page-header.js +0 -3
  154. package/dist/page-header.js.map +0 -1
  155. package/dist/styles/dialog-header.css +0 -87
  156. 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
- .kui-toolbar-control-group[data-single="false"]
137
- > :is(button, wa-button)[aria-pressed="true"] {
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;
@@ -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"]}
@@ -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 };
@@ -1,3 +1,4 @@
1
- export { ToolbarText } from './chunk-GY5WH7TO.js';
1
+ export { ToolbarText } from './chunk-KJYXOTG3.js';
2
+
2
3
 
3
4
  //# sourceMappingURL=toolbar-text.js.map
@@ -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;
@@ -1,3 +1,4 @@
1
- export { ValueTable, ValueTableRow } from './chunk-R6JHHBKQ.js';
1
+ export { ValueTable, ValueTableRow } from './chunk-PONXLV5U.js';
2
+
2
3
 
3
4
  //# sourceMappingURL=value-table.js.map
@@ -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: (submission: TokenSearchSubmit) => void;
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
- /** Keep TokenSearchField wrapping, submit Enter, and preserve its caret across controlled token deletion. */
9
- declare function wireTokenSearchFields(root: HTMLElement, { onSubmit }: WireTokenSearchFieldsOptions): () => void;
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 };
@@ -1,4 +1,4 @@
1
- export { wireTokenSearchFields } from './chunk-H5AGGVU5.js';
1
+ export { wireTokenSearchFields } from './chunk-2RJBFNB6.js';
2
2
 
3
3
 
4
4
 
@@ -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"]}
@@ -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
- ## DialogHeader
45
-
46
- `DialogHeader` keeps its icon and `h2` together in the leading borderless
47
- `ToolbarControlGroup`, with optional actions in a separate trailing group. Pass
48
- localized `actionsLabel` text when the action group needs an accessible name;
49
- the component does not synthesize an English label. The application connects
50
- `titleId` and an optional `summaryId` to the owning dialog through
51
- `aria-labelledby` and `aria-describedby`. Pass action children directly so the
52
- component can group them; existing pre-grouped actions remain compatible, but
53
- their inner groups retain their own labels and semantics.
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