@kerfjs/ui 5.0.0-beta.52 → 5.0.0-beta.54

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 (188) hide show
  1. package/README.md +1 -0
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/component-catalog-v2.d.ts +3 -0
  4. package/ai/component-catalog-v2.json +85 -7
  5. package/ai/component-catalog-v2.schema.json +9 -1
  6. package/ai/component-catalog.json +108 -11
  7. package/ai/public-api-signatures-v1.md +215 -25
  8. package/ai/skill.md +23 -17
  9. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  10. package/analyzer/index.mjs +48 -0
  11. package/analyzer/loud-pairs.d.mts +49 -0
  12. package/analyzer/loud-pairs.mjs +357 -0
  13. package/dist/app-tab.d.ts +7 -5
  14. package/dist/app-tab.js +1 -1
  15. package/dist/browser/segmented-control.js +0 -1
  16. package/dist/catalog.js +4 -4
  17. package/dist/{chunk-EKYZCRCX.js → chunk-3CGQ7ZNO.js} +54 -22
  18. package/dist/chunk-3CGQ7ZNO.js.map +1 -0
  19. package/dist/{chunk-7XJO4D6A.js → chunk-3NBL3OUQ.js} +2 -2
  20. package/dist/{chunk-7XJO4D6A.js.map → chunk-3NBL3OUQ.js.map} +1 -1
  21. package/dist/{chunk-2S7OWAEV.js → chunk-44IG7PFV.js} +3 -3
  22. package/dist/{chunk-2S7OWAEV.js.map → chunk-44IG7PFV.js.map} +1 -1
  23. package/dist/{chunk-UKKSMZYT.js → chunk-46263QLK.js} +51 -1
  24. package/dist/chunk-46263QLK.js.map +1 -0
  25. package/dist/{chunk-DNCOFEUL.js → chunk-5I22LMCV.js} +2 -2
  26. package/dist/{chunk-DNCOFEUL.js.map → chunk-5I22LMCV.js.map} +1 -1
  27. package/dist/{chunk-NDUGJ4TU.js → chunk-5NDQABWK.js} +1 -1
  28. package/dist/{chunk-NDUGJ4TU.js.map → chunk-5NDQABWK.js.map} +1 -1
  29. package/dist/{chunk-KAQUUTZR.js → chunk-ATHXW6RP.js} +2 -2
  30. package/dist/{chunk-KAQUUTZR.js.map → chunk-ATHXW6RP.js.map} +1 -1
  31. package/dist/{chunk-JGFZ42ZF.js → chunk-BJJKFVWW.js} +14 -5
  32. package/dist/chunk-BJJKFVWW.js.map +1 -0
  33. package/dist/{chunk-T7C2VZUB.js → chunk-C2FAYFDE.js} +2 -2
  34. package/dist/{chunk-T7C2VZUB.js.map → chunk-C2FAYFDE.js.map} +1 -1
  35. package/dist/{chunk-SV22UIW4.js → chunk-DQA5FMOX.js} +2 -2
  36. package/dist/{chunk-SV22UIW4.js.map → chunk-DQA5FMOX.js.map} +1 -1
  37. package/dist/{chunk-LIOXPQ7N.js → chunk-GUIHMIT2.js} +12 -10
  38. package/dist/chunk-GUIHMIT2.js.map +1 -0
  39. package/dist/{chunk-3S7OUHUN.js → chunk-KPNTVB7A.js} +9 -4
  40. package/dist/chunk-KPNTVB7A.js.map +1 -0
  41. package/dist/{chunk-IR53BQUJ.js → chunk-NE5JAOQS.js} +9 -9
  42. package/dist/chunk-NE5JAOQS.js.map +1 -0
  43. package/dist/chunk-PSHUSWQH.js +18 -0
  44. package/dist/chunk-PSHUSWQH.js.map +1 -0
  45. package/dist/{chunk-OSCL5TKR.js → chunk-RNZAKSME.js} +11 -8
  46. package/dist/chunk-RNZAKSME.js.map +1 -0
  47. package/dist/{chunk-ABAXNUXU.js → chunk-T22EM4ET.js} +7 -5
  48. package/dist/chunk-T22EM4ET.js.map +1 -0
  49. package/dist/{chunk-YZV2PTY4.js → chunk-TZ5IHWB2.js} +3 -3
  50. package/dist/{chunk-YZV2PTY4.js.map → chunk-TZ5IHWB2.js.map} +1 -1
  51. package/dist/{chunk-VOCPXGQN.js → chunk-VHADGP3B.js} +9 -8
  52. package/dist/chunk-VHADGP3B.js.map +1 -0
  53. package/dist/{chunk-2CE22Q2K.js → chunk-WZ2GLEY4.js} +2 -1
  54. package/dist/chunk-WZ2GLEY4.js.map +1 -0
  55. package/dist/{chunk-M6GLI6A6.js → chunk-X5QSICJ3.js} +5 -5
  56. package/dist/chunk-X5QSICJ3.js.map +1 -0
  57. package/dist/chunk-XQV2B4IL.js +14 -0
  58. package/dist/chunk-XQV2B4IL.js.map +1 -0
  59. package/dist/{chunk-4XE2CDYB.js → chunk-ZMBU522F.js} +2 -2
  60. package/dist/{chunk-4XE2CDYB.js.map → chunk-ZMBU522F.js.map} +1 -1
  61. package/dist/collapsible-panel.d.ts +6 -2
  62. package/dist/collapsible-panel.js +1 -0
  63. package/dist/collapsible-panel.js.map +1 -1
  64. package/dist/css-values.d.ts +1 -1
  65. package/dist/css-values.js +1 -1
  66. package/dist/floating-toolbar.d.ts +5 -2
  67. package/dist/floating-toolbar.js +1 -1
  68. package/dist/grid.js +1 -1
  69. package/dist/index.js +20 -19
  70. package/dist/list-action-row.d.ts +2 -1
  71. package/dist/list-action-row.js +1 -1
  72. package/dist/list-header.d.ts +1 -0
  73. package/dist/list-header.js +1 -1
  74. package/dist/list-item.d.ts +5 -3
  75. package/dist/list-item.js +1 -1
  76. package/dist/list.js +1 -1
  77. package/dist/nav-stack.js +1 -1
  78. package/dist/resizable-region.d.ts +4 -1
  79. package/dist/resizable-region.js +1 -1
  80. package/dist/row.js +1 -1
  81. package/dist/segmented-control.d.ts +1 -1
  82. package/dist/segmented-control.js +1 -3
  83. package/dist/select.js +1 -1
  84. package/dist/skeleton.js +1 -1
  85. package/dist/spacer.js +1 -1
  86. package/dist/split-view.js +2 -2
  87. package/dist/state-banner.d.ts +1 -1
  88. package/dist/state-banner.js +1 -1
  89. package/dist/styles/badge.css +17 -6
  90. package/dist/styles/collapsible-panel.css +89 -11
  91. package/dist/styles/floating-toolbar.css +11 -6
  92. package/dist/styles/foundation.css +82 -26
  93. package/dist/styles/list-action-row.css +29 -8
  94. package/dist/styles/list-header.css +19 -7
  95. package/dist/styles/list-item.css +11 -3
  96. package/dist/styles/resizable-region.css +82 -14
  97. package/dist/styles/segmented-control.css +21 -1
  98. package/dist/styles/select.css +24 -13
  99. package/dist/styles/skeleton.css +33 -2
  100. package/dist/styles/state-banner.css +48 -12
  101. package/dist/styles/webawesome.css +47 -20
  102. package/dist/styles/workbench.css +473 -11
  103. package/dist/toolbar-text.js +1 -1
  104. package/dist/value-table.js +1 -1
  105. package/dist/wire-resizable-regions.d.ts +2 -1
  106. package/dist/wire-resizable-regions.js +2 -1
  107. package/dist/wire-tab-bars.js +1 -1
  108. package/dist/wire-workbench.d.ts +109 -0
  109. package/dist/wire-workbench.js +355 -0
  110. package/dist/wire-workbench.js.map +1 -0
  111. package/dist/workbench.d.ts +65 -6
  112. package/dist/workbench.js +180 -76
  113. package/dist/workbench.js.map +1 -1
  114. package/docs/accessibility.md +54 -9
  115. package/docs/app-layouts.md +12 -1
  116. package/docs/collapsible-panel.md +59 -1
  117. package/docs/component-contract.md +76 -4
  118. package/docs/component-selection.md +16 -6
  119. package/docs/design/templates/empty-state/with-action-dark.svg +1 -1
  120. package/docs/design/templates/empty-state-dark.svg +1 -1
  121. package/docs/design/templates/list/stack-dark.svg +1 -1
  122. package/docs/design/templates/list/stack.svg +1 -1
  123. package/docs/design/templates/list-action-row/selected.svg +1 -1
  124. package/docs/design/templates/list-action-row.svg +1 -1
  125. package/docs/design/templates/list-dark.svg +1 -1
  126. package/docs/design/templates/list-header/action-dark.svg +1 -1
  127. package/docs/design/templates/list-header/action.svg +1 -1
  128. package/docs/design/templates/list-header/count-dark.svg +1 -1
  129. package/docs/design/templates/list-header/count.svg +1 -1
  130. package/docs/design/templates/list-header/inline-action-dark.svg +1 -1
  131. package/docs/design/templates/list-header/inline-action.svg +1 -1
  132. package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -1
  133. package/docs/design/templates/list-header/toggle-expanded.svg +1 -1
  134. package/docs/design/templates/list-header-dark.svg +1 -1
  135. package/docs/design/templates/list-header.svg +1 -1
  136. package/docs/design/templates/list-item/selected.svg +1 -1
  137. package/docs/design/templates/list-item.svg +1 -1
  138. package/docs/design/templates/list.svg +1 -1
  139. package/docs/design/templates/segmented-control/equal-dark.svg +1 -1
  140. package/docs/design/templates/segmented-control/equal.svg +1 -1
  141. package/docs/design/templates/segmented-control/pill-small-dark.svg +1 -1
  142. package/docs/design/templates/segmented-control/pill-small.svg +1 -1
  143. package/docs/design/templates/segmented-control-dark.svg +1 -1
  144. package/docs/design/templates/segmented-control.svg +1 -1
  145. package/docs/design/templates/state-banner/danger-dark.svg +1 -1
  146. package/docs/design/templates/state-banner/danger.svg +1 -1
  147. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -1
  148. package/docs/design/templates/state-banner/info-badge.svg +1 -1
  149. package/docs/design/templates/state-banner/info-dark.svg +1 -1
  150. package/docs/design/templates/state-banner/info.svg +1 -1
  151. package/docs/design/templates/state-banner/success-dark.svg +1 -1
  152. package/docs/design/templates/state-banner/success.svg +1 -1
  153. package/docs/design/templates/state-banner/warning-dark.svg +1 -1
  154. package/docs/design/templates/state-banner/warning.svg +1 -1
  155. package/docs/design/templates/state-banner-dark.svg +1 -1
  156. package/docs/design/templates/state-banner.svg +1 -1
  157. package/docs/design/templates/tab-bar/workspace-tabs-dark.svg +1 -1
  158. package/docs/design/templates/tab-bar/workspace-tabs.svg +1 -1
  159. package/docs/design/templates/tab-bar-dark.svg +1 -1
  160. package/docs/design/templates/tab-bar.svg +1 -1
  161. package/docs/design/templates/token-search-field/tokens.svg +1 -1
  162. package/docs/design/templates/token-search-field.svg +1 -1
  163. package/docs/design/templates.md +11 -1
  164. package/docs/device-class.md +2 -1
  165. package/docs/document-baseline.md +4 -0
  166. package/docs/layout.md +5 -1
  167. package/docs/recipes.md +5 -3
  168. package/docs/type-contracts.md +2 -1
  169. package/docs/ui-analyzer.md +94 -19
  170. package/docs/ui-doctor.md +6 -0
  171. package/docs/ux-demo.md +2 -2
  172. package/docs/webawesome-theme.md +18 -2
  173. package/docs/workbench.md +302 -7
  174. package/llms.txt +14 -6
  175. package/package.json +5 -1
  176. package/ux-demo/recipes/collapsible-sidebar.tsx +8 -7
  177. package/ux-demo/recipes/loading-inspector.tsx +3 -2
  178. package/dist/chunk-2CE22Q2K.js.map +0 -1
  179. package/dist/chunk-3S7OUHUN.js.map +0 -1
  180. package/dist/chunk-ABAXNUXU.js.map +0 -1
  181. package/dist/chunk-EKYZCRCX.js.map +0 -1
  182. package/dist/chunk-IR53BQUJ.js.map +0 -1
  183. package/dist/chunk-JGFZ42ZF.js.map +0 -1
  184. package/dist/chunk-LIOXPQ7N.js.map +0 -1
  185. package/dist/chunk-M6GLI6A6.js.map +0 -1
  186. package/dist/chunk-OSCL5TKR.js.map +0 -1
  187. package/dist/chunk-UKKSMZYT.js.map +0 -1
  188. package/dist/chunk-VOCPXGQN.js.map +0 -1
@@ -2,24 +2,64 @@ import { SafeHtml } from 'kerfjs';
2
2
  import { ResizableRegionSeparator, ResizableRegionCollapseMotion, ResizableRegionContentOverflow, ResizableRegionPresentation, ResizableRegionRestorePosition } from './resizable-region.js';
3
3
  import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
4
4
 
5
+ /**
6
+ * The Workbench container breakpoint below which a panel presents as an
7
+ * overlay: `narrow` (704px or less) or `compact` (448px or less) — the same
8
+ * breakpoints as `ResizableRegion`'s `responsiveFillAt`.
9
+ */
10
+ type WorkbenchResponsiveOverlayAt = 'compact' | 'narrow';
11
+ /** Drag-resize limits for a resizable Workbench panel, in px. */
12
+ interface WorkbenchPanelResizable {
13
+ /** Smallest size (default 180 for a rail, 120 for the drawer). */
14
+ min?: number;
15
+ /** Largest size (default 480). */
16
+ max?: number;
17
+ }
5
18
  /** A collapsible Workbench panel — a side rail or the bottom drawer. */
6
19
  interface WorkbenchPanel {
7
20
  content: KerfUiContent;
8
21
  /** Whether the panel is currently collapsed (the app owns this). */
9
22
  collapsed?: boolean;
10
- /** Rail width, or drawer height, in px. Overrides the CSS default. */
23
+ /**
24
+ * Rail width, or drawer height, in px. Overrides the CSS default. For a
25
+ * resizable panel it is the current size (default 280 for a rail, 220 for
26
+ * the drawer), clamped to the limits; the app owns it and `wireWorkbench`
27
+ * reports each resize.
28
+ */
11
29
  size?: number;
30
+ /**
31
+ * Opt in to drag and keyboard resizing on the panel's inner separator:
32
+ * `true` for the default limits, or `{ min, max }`. Off by default. Pair it
33
+ * with `wireWorkbench` from `@kerfjs/ui/wire-workbench`.
34
+ */
35
+ resizable?: boolean | WorkbenchPanelResizable;
12
36
  /** Accessible name for the panel region. */
13
37
  label?: string;
14
38
  separator?: ResizableRegionSeparator;
15
39
  collapseMotion?: ResizableRegionCollapseMotion;
16
40
  contentOverflow?: ResizableRegionContentOverflow;
17
41
  presentation?: ResizableRegionPresentation;
18
- /** Control shown in a safe-area-aware viewport corner while collapsed. */
42
+ /**
43
+ * Present the panel as an overlay, without a separator, below a Workbench
44
+ * container breakpoint, and inline above it — the CSS decides, so the app
45
+ * needs no device-class check. A rail overlays from its side at full
46
+ * height; the bottom drawer overlays the bottom of the work-area column.
47
+ */
48
+ responsiveOverlayAt?: WorkbenchResponsiveOverlayAt;
49
+ /**
50
+ * Control shown while collapsed, in a safe-area-aware corner of the
51
+ * Workbench (not the viewport); the bottom drawer's sits in the work-area
52
+ * column.
53
+ */
19
54
  restoreControl?: SafeHtml;
20
55
  restorePosition?: ResizableRegionRestorePosition;
21
56
  }
22
57
  interface WorkbenchProps {
58
+ /**
59
+ * The Workbench's `id`. Each panel's `id` derives from it —
60
+ * `<id>-left-rail`, `<id>-right-rail`, `<id>-bottom-drawer` — so a toggle
61
+ * can name the panel it shows and hides with `aria-controls`.
62
+ */
23
63
  id: string;
24
64
  label: string;
25
65
  /** The central work area. */
@@ -27,6 +67,22 @@ interface WorkbenchProps {
27
67
  leftRail?: WorkbenchPanel;
28
68
  rightRail?: WorkbenchPanel;
29
69
  bottomDrawer?: WorkbenchPanel;
70
+ /**
71
+ * Minimum width, in px, the work area keeps beside the inline rails, fixed
72
+ * or `resizable` (default 320; `0` turns it off). Resizing a rail stops
73
+ * where the work area would drop below it, and inline rails shrink
74
+ * proportionally when the Workbench gets narrower. Workbenches without a
75
+ * rail ignore it.
76
+ */
77
+ mainMinSize?: number;
78
+ /**
79
+ * Minimum height, in px, the work area keeps above an inline bottom drawer,
80
+ * fixed or `resizable` (default 120; `0` turns it off) — the drawer's
81
+ * counterpart of `mainMinSize`. Resizing the drawer stops where the work
82
+ * area would drop below it, and the drawer shrinks when the Workbench gets
83
+ * shorter. Workbenches without a drawer ignore it.
84
+ */
85
+ mainMinHeight?: number;
30
86
  className?: string;
31
87
  /** Native named-slot assignment when composed inside a web component. */
32
88
  slot?: string;
@@ -38,9 +94,12 @@ interface WorkbenchProps {
38
94
  * a composited transform — the instant-width / sliding-content technique, so the
39
95
  * work area relayouts once, not per frame. Bottom-drawer content stays anchored
40
96
  * to the shell's stable bottom edge throughout that transition. The app owns
41
- * each `collapsed` flag; the collapse is pure CSS (no wire). See
42
- * `docs/23-app-layouts.md` §3.3.
97
+ * each `collapsed` flag; the collapse is pure CSS. A collapsed panel renders
98
+ * `inert` and `aria-hidden`, so neither Tab nor assistive technology reaches
99
+ * controls that have slid out of view or an empty landmark (its restore control
100
+ * lives outside it and stays reachable). A panel may opt in to drag and keyboard resizing with
101
+ * `resizable`, which `wireWorkbench` drives. See `docs/23-app-layouts.md` §3.3.
43
102
  */
44
- declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, className, slot, }: WorkbenchProps): SafeHtml;
103
+ declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, mainMinSize, mainMinHeight, className, slot, }: WorkbenchProps): SafeHtml;
45
104
 
46
- export { Workbench, type WorkbenchPanel, type WorkbenchProps };
105
+ export { Workbench, type WorkbenchPanel, type WorkbenchPanelResizable, type WorkbenchProps, type WorkbenchResponsiveOverlayAt };
package/dist/workbench.js CHANGED
@@ -1,6 +1,167 @@
1
+ import { WORKBENCH_MAIN_MIN_SIZE, WORKBENCH_MAIN_MIN_HEIGHT, workbenchRegionId, WORKBENCH_RESIZE_DEFAULTS } from './chunk-PSHUSWQH.js';
2
+ import { clampRegionSize, ResizeGrip } from './chunk-VHADGP3B.js';
1
3
  import { jsxs, jsx } from 'kerfjs/jsx-runtime';
2
4
 
3
- // src/workbench.tsx
5
+ var EDGES = {
6
+ leftRail: "end",
7
+ rightRail: "start",
8
+ bottomDrawer: "start"
9
+ };
10
+ var DEFAULT_LABELS = {
11
+ leftRail: "left rail",
12
+ rightRail: "right rail",
13
+ bottomDrawer: "bottom drawer"
14
+ };
15
+ function resolveResize(id, key, panel) {
16
+ if (!panel.resizable) return void 0;
17
+ const defaults = WORKBENCH_RESIZE_DEFAULTS[key];
18
+ const limits = panel.resizable === true ? {} : panel.resizable;
19
+ const min = limits.min ?? defaults.min;
20
+ const max = Math.max(min, limits.max ?? defaults.max);
21
+ return {
22
+ regionId: workbenchRegionId(id, key),
23
+ axis: key === "bottomDrawer" ? "vertical" : "horizontal",
24
+ edge: EDGES[key],
25
+ size: clampRegionSize(panel.size ?? defaults.size, min, max),
26
+ min,
27
+ max
28
+ };
29
+ }
30
+ var resizeAttributes = (resize) => resize ? {
31
+ "data-resizable": "true",
32
+ "data-region-id": resize.regionId,
33
+ "data-axis": resize.axis,
34
+ "data-edge": resize.edge
35
+ } : {};
36
+ function panelStyle(panel, resize, sizeProperty) {
37
+ if (resize) return `--kui-resizable-region-size:${resize.size}px`;
38
+ return panel.size ? `${sizeProperty}: ${panel.size}px` : void 0;
39
+ }
40
+ function PanelHandle({
41
+ panel,
42
+ resize,
43
+ label
44
+ }) {
45
+ const inert = Boolean(panel.collapsed) || (panel.presentation ?? "inline") !== "inline";
46
+ return /* @__PURE__ */ jsx(
47
+ "div",
48
+ {
49
+ class: "kui-workbench__handle",
50
+ role: "separator",
51
+ tabindex: inert ? "-1" : "0",
52
+ "aria-label": `Resize ${label}`,
53
+ "aria-orientation": resize.axis === "horizontal" ? "vertical" : "horizontal",
54
+ "aria-valuemin": resize.min,
55
+ "aria-valuemax": resize.max,
56
+ "aria-valuenow": resize.size,
57
+ "aria-hidden": inert ? "true" : void 0,
58
+ "data-kui-resize-handle": true,
59
+ "data-region-id": resize.regionId,
60
+ children: /* @__PURE__ */ jsx("span", { class: "kui-workbench__handle-icon", "aria-hidden": "true", children: /* @__PURE__ */ jsx(ResizeGrip, { axis: resize.axis }) })
61
+ }
62
+ );
63
+ }
64
+ function hiddenFromAccessibility(panel) {
65
+ return panel.collapsed || panel.presentation === "hidden" ? "true" : void 0;
66
+ }
67
+ function Rail({
68
+ id,
69
+ side,
70
+ panel
71
+ }) {
72
+ const key = side === "left" ? "leftRail" : "rightRail";
73
+ const resize = resolveResize(id, key, panel);
74
+ return /* @__PURE__ */ jsxs(
75
+ "aside",
76
+ {
77
+ id: workbenchRegionId(id, key),
78
+ class: `kui-workbench__rail kui-workbench__rail--${side}`,
79
+ "data-workbench-rail": side,
80
+ "data-collapsed": String(panel.collapsed ?? false),
81
+ "data-separator": panel.separator ?? "auto",
82
+ "data-collapse-motion": panel.collapseMotion ?? "slide",
83
+ "data-content-overflow": panel.contentOverflow ?? "clip",
84
+ "data-presentation": panel.presentation ?? "inline",
85
+ "data-responsive-overlay-at": panel.responsiveOverlayAt,
86
+ ...resizeAttributes(resize),
87
+ "aria-label": panel.label || void 0,
88
+ "aria-hidden": hiddenFromAccessibility(panel),
89
+ inert: Boolean(panel.collapsed),
90
+ style: panelStyle(panel, resize, "--kui-workbench-rail-width"),
91
+ children: [
92
+ /* @__PURE__ */ jsx(
93
+ "div",
94
+ {
95
+ class: "kui-workbench__panel-content",
96
+ inert: Boolean(panel.collapsed),
97
+ children: panel.content
98
+ }
99
+ ),
100
+ resize && /* @__PURE__ */ jsx(
101
+ PanelHandle,
102
+ {
103
+ panel,
104
+ resize,
105
+ label: panel.label || DEFAULT_LABELS[key]
106
+ }
107
+ )
108
+ ]
109
+ }
110
+ );
111
+ }
112
+ function Drawer({ id, panel }) {
113
+ const resize = resolveResize(id, "bottomDrawer", panel);
114
+ return /* @__PURE__ */ jsxs(
115
+ "section",
116
+ {
117
+ id: workbenchRegionId(id, "bottomDrawer"),
118
+ class: "kui-workbench__drawer",
119
+ "data-workbench-drawer": true,
120
+ "data-collapsed": String(panel.collapsed ?? false),
121
+ "data-separator": panel.separator ?? "auto",
122
+ "data-collapse-motion": panel.collapseMotion ?? "slide",
123
+ "data-content-overflow": panel.contentOverflow ?? "clip",
124
+ "data-presentation": panel.presentation ?? "inline",
125
+ "data-responsive-overlay-at": panel.responsiveOverlayAt,
126
+ ...resizeAttributes(resize),
127
+ "aria-label": panel.label || void 0,
128
+ "aria-hidden": hiddenFromAccessibility(panel),
129
+ inert: Boolean(panel.collapsed),
130
+ style: panelStyle(panel, resize, "--kui-workbench-drawer-height"),
131
+ children: [
132
+ /* @__PURE__ */ jsx(
133
+ "div",
134
+ {
135
+ class: "kui-workbench__panel-content",
136
+ inert: Boolean(panel.collapsed),
137
+ children: panel.content
138
+ }
139
+ ),
140
+ resize && /* @__PURE__ */ jsx(
141
+ PanelHandle,
142
+ {
143
+ panel,
144
+ resize,
145
+ label: panel.label || DEFAULT_LABELS.bottomDrawer
146
+ }
147
+ )
148
+ ]
149
+ }
150
+ );
151
+ }
152
+ function restore(panel, side, position) {
153
+ if (!panel?.collapsed || !panel.restoreControl || panel.presentation === "hidden")
154
+ return "";
155
+ return /* @__PURE__ */ jsx(
156
+ "div",
157
+ {
158
+ class: "kui-workbench__restore",
159
+ "data-panel": side,
160
+ "data-position": panel.restorePosition ?? position,
161
+ children: panel.restoreControl
162
+ }
163
+ );
164
+ }
4
165
  function Workbench({
5
166
  id,
6
167
  label,
@@ -8,96 +169,39 @@ function Workbench({
8
169
  leftRail,
9
170
  rightRail,
10
171
  bottomDrawer,
172
+ mainMinSize = WORKBENCH_MAIN_MIN_SIZE,
173
+ mainMinHeight = WORKBENCH_MAIN_MIN_HEIGHT,
11
174
  className = "",
12
175
  slot
13
176
  }) {
177
+ const px = (value) => Math.max(0, Math.round(value));
178
+ const mainMin = leftRail || rightRail ? px(mainMinSize) : void 0;
179
+ const mainMinBlock = bottomDrawer ? px(mainMinHeight) : void 0;
180
+ const style = [
181
+ mainMin === void 0 ? "" : `--_kui-workbench-main-min-width:${mainMin}px`,
182
+ mainMinBlock === void 0 ? "" : `--_kui-workbench-main-min-height:${mainMinBlock}px`
183
+ ].filter(Boolean).join(";");
14
184
  return /* @__PURE__ */ jsxs(
15
185
  "section",
16
186
  {
17
187
  class: `kui-workbench ${className}`.trim(),
18
188
  id,
19
189
  "data-component": "workbench",
190
+ "data-main-min-size": mainMin === void 0 ? void 0 : String(mainMin),
191
+ "data-main-min-height": mainMinBlock === void 0 ? void 0 : String(mainMinBlock),
192
+ style: style || void 0,
20
193
  "aria-label": label,
21
194
  slot,
22
195
  children: [
23
- leftRail && /* @__PURE__ */ jsx(
24
- "aside",
25
- {
26
- class: "kui-workbench__rail kui-workbench__rail--left",
27
- "data-workbench-rail": "left",
28
- "data-collapsed": String(leftRail.collapsed ?? false),
29
- "data-separator": leftRail.separator ?? "auto",
30
- "data-collapse-motion": leftRail.collapseMotion ?? "slide",
31
- "data-content-overflow": leftRail.contentOverflow ?? "clip",
32
- "data-presentation": leftRail.presentation ?? "inline",
33
- "aria-label": leftRail.label || void 0,
34
- "aria-hidden": leftRail.presentation === "hidden" ? "true" : void 0,
35
- style: leftRail.size ? `--kui-workbench-rail-width: ${leftRail.size}px` : void 0,
36
- children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: leftRail.content })
37
- }
38
- ),
39
- leftRail?.collapsed && leftRail.restoreControl && leftRail.presentation !== "hidden" && /* @__PURE__ */ jsx(
40
- "div",
41
- {
42
- class: "kui-workbench__restore",
43
- "data-panel": "left",
44
- "data-position": leftRail.restorePosition ?? "bottom-start",
45
- children: leftRail.restoreControl
46
- }
47
- ),
196
+ leftRail && /* @__PURE__ */ jsx(Rail, { id, side: "left", panel: leftRail }),
197
+ restore(leftRail, "left", "bottom-start"),
48
198
  /* @__PURE__ */ jsxs("div", { class: "kui-workbench__center", children: [
49
199
  /* @__PURE__ */ jsx("div", { class: "kui-workbench__main", "data-workbench-main": true, children: main }),
50
- bottomDrawer && /* @__PURE__ */ jsx(
51
- "section",
52
- {
53
- class: "kui-workbench__drawer",
54
- "data-workbench-drawer": true,
55
- "data-collapsed": String(bottomDrawer.collapsed ?? false),
56
- "data-separator": bottomDrawer.separator ?? "auto",
57
- "data-collapse-motion": bottomDrawer.collapseMotion ?? "slide",
58
- "data-content-overflow": bottomDrawer.contentOverflow ?? "clip",
59
- "data-presentation": bottomDrawer.presentation ?? "inline",
60
- "aria-label": bottomDrawer.label || void 0,
61
- "aria-hidden": bottomDrawer.presentation === "hidden" ? "true" : void 0,
62
- style: bottomDrawer.size ? `--kui-workbench-drawer-height: ${bottomDrawer.size}px` : void 0,
63
- children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: bottomDrawer.content })
64
- }
65
- ),
66
- bottomDrawer?.collapsed && bottomDrawer.restoreControl && bottomDrawer.presentation !== "hidden" && /* @__PURE__ */ jsx(
67
- "div",
68
- {
69
- class: "kui-workbench__restore",
70
- "data-panel": "bottom",
71
- "data-position": bottomDrawer.restorePosition ?? "bottom-end",
72
- children: bottomDrawer.restoreControl
73
- }
74
- )
200
+ bottomDrawer && /* @__PURE__ */ jsx(Drawer, { id, panel: bottomDrawer }),
201
+ restore(bottomDrawer, "bottom", "bottom-end")
75
202
  ] }),
76
- rightRail && /* @__PURE__ */ jsx(
77
- "aside",
78
- {
79
- class: "kui-workbench__rail kui-workbench__rail--right",
80
- "data-workbench-rail": "right",
81
- "data-collapsed": String(rightRail.collapsed ?? false),
82
- "data-separator": rightRail.separator ?? "auto",
83
- "data-collapse-motion": rightRail.collapseMotion ?? "slide",
84
- "data-content-overflow": rightRail.contentOverflow ?? "clip",
85
- "data-presentation": rightRail.presentation ?? "inline",
86
- "aria-label": rightRail.label || void 0,
87
- "aria-hidden": rightRail.presentation === "hidden" ? "true" : void 0,
88
- style: rightRail.size ? `--kui-workbench-rail-width: ${rightRail.size}px` : void 0,
89
- children: /* @__PURE__ */ jsx("div", { class: "kui-workbench__panel-content", children: rightRail.content })
90
- }
91
- ),
92
- rightRail?.collapsed && rightRail.restoreControl && rightRail.presentation !== "hidden" && /* @__PURE__ */ jsx(
93
- "div",
94
- {
95
- class: "kui-workbench__restore",
96
- "data-panel": "right",
97
- "data-position": rightRail.restorePosition ?? "bottom-end",
98
- children: rightRail.restoreControl
99
- }
100
- )
203
+ rightRail && /* @__PURE__ */ jsx(Rail, { id, side: "right", panel: rightRail }),
204
+ restore(rightRail, "right", "bottom-end")
101
205
  ]
102
206
  }
103
207
  );
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/workbench.tsx"],"names":[],"mappings":"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","file":"workbench.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport type {\n ResizableRegionCollapseMotion,\n ResizableRegionContentOverflow,\n ResizableRegionPresentation,\n ResizableRegionRestorePosition,\n ResizableRegionSeparator,\n} from './resizable-region.js';\nimport type { KerfUiContent } from './semantic-content.js';\n\n/** A collapsible Workbench panel — a side rail or the bottom drawer. */\nexport interface WorkbenchPanel {\n content: KerfUiContent;\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 separator?: ResizableRegionSeparator;\n collapseMotion?: ResizableRegionCollapseMotion;\n contentOverflow?: ResizableRegionContentOverflow;\n presentation?: ResizableRegionPresentation;\n /** Control shown in a safe-area-aware viewport corner while collapsed. */\n restoreControl?: SafeHtml;\n restorePosition?: ResizableRegionRestorePosition;\n}\n\nexport interface WorkbenchProps {\n id: string;\n label: string;\n /** The central work area. */\n main: KerfUiContent;\n leftRail?: WorkbenchPanel;\n rightRail?: WorkbenchPanel;\n bottomDrawer?: WorkbenchPanel;\n className?: string;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: 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. Bottom-drawer content stays anchored\n * to the shell's stable bottom edge throughout that transition. The app owns\n * each `collapsed` flag; the collapse is pure CSS (no wire). See\n * `docs/23-app-layouts.md` §3.3.\n */\nexport function Workbench({\n id,\n label,\n main,\n leftRail,\n rightRail,\n bottomDrawer,\n className = '',\n slot,\n}: WorkbenchProps) {\n return (\n <section\n class={`kui-workbench ${className}`.trim()}\n id={id}\n data-component=\"workbench\"\n aria-label={label}\n slot={slot}\n >\n {leftRail && (\n <aside\n class=\"kui-workbench__rail kui-workbench__rail--left\"\n data-workbench-rail=\"left\"\n data-collapsed={String(leftRail.collapsed ?? false)}\n data-separator={leftRail.separator ?? 'auto'}\n data-collapse-motion={leftRail.collapseMotion ?? 'slide'}\n data-content-overflow={leftRail.contentOverflow ?? 'clip'}\n data-presentation={leftRail.presentation ?? 'inline'}\n aria-label={leftRail.label || undefined}\n aria-hidden={leftRail.presentation === 'hidden' ? 'true' : undefined}\n style={\n leftRail.size\n ? `--kui-workbench-rail-width: ${leftRail.size}px`\n : undefined\n }\n >\n <div class=\"kui-workbench__panel-content\">{leftRail.content}</div>\n </aside>\n )}\n {leftRail?.collapsed &&\n leftRail.restoreControl &&\n leftRail.presentation !== 'hidden' && (\n <div\n class=\"kui-workbench__restore\"\n data-panel=\"left\"\n data-position={leftRail.restorePosition ?? 'bottom-start'}\n >\n {leftRail.restoreControl}\n </div>\n )}\n <div class=\"kui-workbench__center\">\n <div class=\"kui-workbench__main\" data-workbench-main>\n {main}\n </div>\n {bottomDrawer && (\n <section\n class=\"kui-workbench__drawer\"\n data-workbench-drawer\n data-collapsed={String(bottomDrawer.collapsed ?? false)}\n data-separator={bottomDrawer.separator ?? 'auto'}\n data-collapse-motion={bottomDrawer.collapseMotion ?? 'slide'}\n data-content-overflow={bottomDrawer.contentOverflow ?? 'clip'}\n data-presentation={bottomDrawer.presentation ?? 'inline'}\n aria-label={bottomDrawer.label || undefined}\n aria-hidden={\n bottomDrawer.presentation === 'hidden' ? 'true' : undefined\n }\n style={\n bottomDrawer.size\n ? `--kui-workbench-drawer-height: ${bottomDrawer.size}px`\n : undefined\n }\n >\n <div class=\"kui-workbench__panel-content\">\n {bottomDrawer.content}\n </div>\n </section>\n )}\n {bottomDrawer?.collapsed &&\n bottomDrawer.restoreControl &&\n bottomDrawer.presentation !== 'hidden' && (\n <div\n class=\"kui-workbench__restore\"\n data-panel=\"bottom\"\n data-position={bottomDrawer.restorePosition ?? 'bottom-end'}\n >\n {bottomDrawer.restoreControl}\n </div>\n )}\n </div>\n {rightRail && (\n <aside\n class=\"kui-workbench__rail kui-workbench__rail--right\"\n data-workbench-rail=\"right\"\n data-collapsed={String(rightRail.collapsed ?? false)}\n data-separator={rightRail.separator ?? 'auto'}\n data-collapse-motion={rightRail.collapseMotion ?? 'slide'}\n data-content-overflow={rightRail.contentOverflow ?? 'clip'}\n data-presentation={rightRail.presentation ?? 'inline'}\n aria-label={rightRail.label || undefined}\n aria-hidden={rightRail.presentation === 'hidden' ? 'true' : undefined}\n style={\n rightRail.size\n ? `--kui-workbench-rail-width: ${rightRail.size}px`\n : undefined\n }\n >\n <div class=\"kui-workbench__panel-content\">{rightRail.content}</div>\n </aside>\n )}\n {rightRail?.collapsed &&\n rightRail.restoreControl &&\n rightRail.presentation !== 'hidden' && (\n <div\n class=\"kui-workbench__restore\"\n data-panel=\"right\"\n data-position={rightRail.restorePosition ?? 'bottom-end'}\n >\n {rightRail.restoreControl}\n </div>\n )}\n </section>\n );\n}\n"]}
1
+ {"version":3,"sources":["../src/workbench.tsx"],"names":[],"mappings":"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","file":"workbench.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport {\n clampRegionSize,\n type ResizableRegionAxis,\n type ResizableRegionCollapseMotion,\n type ResizableRegionContentOverflow,\n type ResizableRegionEdge,\n type ResizableRegionPresentation,\n type ResizableRegionRestorePosition,\n type ResizableRegionSeparator,\n} from './resizable-region.js';\nimport { ResizeGrip } from './resize-grip.js';\nimport type { KerfUiContent } from './semantic-content.js';\nimport {\n WORKBENCH_MAIN_MIN_HEIGHT,\n WORKBENCH_MAIN_MIN_SIZE,\n WORKBENCH_RESIZE_DEFAULTS,\n type WorkbenchPanelKey,\n workbenchRegionId,\n} from './workbench-resize.js';\n\n/**\n * The Workbench container breakpoint below which a panel presents as an\n * overlay: `narrow` (704px or less) or `compact` (448px or less) — the same\n * breakpoints as `ResizableRegion`'s `responsiveFillAt`.\n */\nexport type WorkbenchResponsiveOverlayAt = 'compact' | 'narrow';\n\n/** Drag-resize limits for a resizable Workbench panel, in px. */\nexport interface WorkbenchPanelResizable {\n /** Smallest size (default 180 for a rail, 120 for the drawer). */\n min?: number;\n /** Largest size (default 480). */\n max?: number;\n}\n\n/** A collapsible Workbench panel — a side rail or the bottom drawer. */\nexport interface WorkbenchPanel {\n content: KerfUiContent;\n /** Whether the panel is currently collapsed (the app owns this). */\n collapsed?: boolean;\n /**\n * Rail width, or drawer height, in px. Overrides the CSS default. For a\n * resizable panel it is the current size (default 280 for a rail, 220 for\n * the drawer), clamped to the limits; the app owns it and `wireWorkbench`\n * reports each resize.\n */\n size?: number;\n /**\n * Opt in to drag and keyboard resizing on the panel's inner separator:\n * `true` for the default limits, or `{ min, max }`. Off by default. Pair it\n * with `wireWorkbench` from `@kerfjs/ui/wire-workbench`.\n */\n resizable?: boolean | WorkbenchPanelResizable;\n /** Accessible name for the panel region. */\n label?: string;\n separator?: ResizableRegionSeparator;\n collapseMotion?: ResizableRegionCollapseMotion;\n contentOverflow?: ResizableRegionContentOverflow;\n presentation?: ResizableRegionPresentation;\n /**\n * Present the panel as an overlay, without a separator, below a Workbench\n * container breakpoint, and inline above it — the CSS decides, so the app\n * needs no device-class check. A rail overlays from its side at full\n * height; the bottom drawer overlays the bottom of the work-area column.\n */\n responsiveOverlayAt?: WorkbenchResponsiveOverlayAt;\n /**\n * Control shown while collapsed, in a safe-area-aware corner of the\n * Workbench (not the viewport); the bottom drawer's sits in the work-area\n * column.\n */\n restoreControl?: SafeHtml;\n restorePosition?: ResizableRegionRestorePosition;\n}\n\nexport interface WorkbenchProps {\n /**\n * The Workbench's `id`. Each panel's `id` derives from it —\n * `<id>-left-rail`, `<id>-right-rail`, `<id>-bottom-drawer` — so a toggle\n * can name the panel it shows and hides with `aria-controls`.\n */\n id: string;\n label: string;\n /** The central work area. */\n main: KerfUiContent;\n leftRail?: WorkbenchPanel;\n rightRail?: WorkbenchPanel;\n bottomDrawer?: WorkbenchPanel;\n /**\n * Minimum width, in px, the work area keeps beside the inline rails, fixed\n * or `resizable` (default 320; `0` turns it off). Resizing a rail stops\n * where the work area would drop below it, and inline rails shrink\n * proportionally when the Workbench gets narrower. Workbenches without a\n * rail ignore it.\n */\n mainMinSize?: number;\n /**\n * Minimum height, in px, the work area keeps above an inline bottom drawer,\n * fixed or `resizable` (default 120; `0` turns it off) — the drawer's\n * counterpart of `mainMinSize`. Resizing the drawer stops where the work\n * area would drop below it, and the drawer shrinks when the Workbench gets\n * shorter. Workbenches without a drawer ignore it.\n */\n mainMinHeight?: number;\n className?: string;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\ninterface ResolvedResize {\n regionId: string;\n axis: ResizableRegionAxis;\n edge: ResizableRegionEdge;\n size: number;\n min: number;\n max: number;\n}\n\n/**\n * Each panel's separator is its inner edge. Growing the left rail moves that\n * edge toward the inline end; growing the right rail or the drawer moves it\n * toward the start — the same edge vocabulary `ResizableRegion` uses.\n */\nconst EDGES: Record<WorkbenchPanelKey, ResizableRegionEdge> = {\n leftRail: 'end',\n rightRail: 'start',\n bottomDrawer: 'start',\n};\n\nconst DEFAULT_LABELS: Record<WorkbenchPanelKey, string> = {\n leftRail: 'left rail',\n rightRail: 'right rail',\n bottomDrawer: 'bottom drawer',\n};\n\nfunction resolveResize(\n id: string,\n key: WorkbenchPanelKey,\n panel: WorkbenchPanel,\n): ResolvedResize | undefined {\n if (!panel.resizable) return undefined;\n const defaults = WORKBENCH_RESIZE_DEFAULTS[key];\n const limits = panel.resizable === true ? {} : panel.resizable;\n const min = limits.min ?? defaults.min;\n const max = Math.max(min, limits.max ?? defaults.max);\n return {\n regionId: workbenchRegionId(id, key),\n axis: key === 'bottomDrawer' ? 'vertical' : 'horizontal',\n edge: EDGES[key],\n size: clampRegionSize(panel.size ?? defaults.size, min, max),\n min,\n max,\n };\n}\n\n/** The attributes a resizable panel adds for the resize wiring to read. */\nconst resizeAttributes = (resize: ResolvedResize | undefined) =>\n resize\n ? {\n 'data-resizable': 'true',\n 'data-region-id': resize.regionId,\n 'data-axis': resize.axis,\n 'data-edge': resize.edge,\n }\n : {};\n\n/**\n * A resizable panel's size lives in the resize wiring's track property, so a\n * drag or key press resizes the panel before the app re-renders; the CSS maps\n * it onto the rail width or drawer height.\n */\nfunction panelStyle(\n panel: WorkbenchPanel,\n resize: ResolvedResize | undefined,\n sizeProperty: string,\n) {\n if (resize) return `--kui-resizable-region-size:${resize.size}px`;\n return panel.size ? `${sizeProperty}: ${panel.size}px` : undefined;\n}\n\n/**\n * The separator a resizable panel owns: the same focusable `role=\"separator\"`\n * contract a `ResizableRegion` handle carries, so one resize wiring drives\n * both. It leaves the tab order while the panel is collapsed or not inline.\n */\nfunction PanelHandle({\n panel,\n resize,\n label,\n}: {\n panel: WorkbenchPanel;\n resize: ResolvedResize;\n label: string;\n}) {\n const inert =\n Boolean(panel.collapsed) || (panel.presentation ?? 'inline') !== 'inline';\n return (\n <div\n class=\"kui-workbench__handle\"\n role=\"separator\"\n tabindex={inert ? '-1' : '0'}\n aria-label={`Resize ${label}`}\n aria-orientation={\n resize.axis === 'horizontal' ? 'vertical' : 'horizontal'\n }\n aria-valuemin={resize.min}\n aria-valuemax={resize.max}\n aria-valuenow={resize.size}\n aria-hidden={inert ? 'true' : undefined}\n data-kui-resize-handle\n data-region-id={resize.regionId}\n >\n <span class=\"kui-workbench__handle-icon\" aria-hidden=\"true\">\n <ResizeGrip axis={resize.axis} />\n </span>\n </div>\n );\n}\n\n/**\n * A collapsed panel leaves the accessibility tree as a whole, like a collapsed\n * `CollapsiblePanel`: its content and separator are already inert or hidden,\n * so the labeled landmark would otherwise stay behind, empty, for a screen\n * reader to land on. Its restore control renders outside it and stays\n * reachable.\n */\nfunction hiddenFromAccessibility(panel: WorkbenchPanel) {\n return panel.collapsed || panel.presentation === 'hidden'\n ? ('true' as const)\n : undefined;\n}\n\nfunction Rail({\n id,\n side,\n panel,\n}: {\n id: string;\n side: 'left' | 'right';\n panel: WorkbenchPanel;\n}) {\n const key: WorkbenchPanelKey = side === 'left' ? 'leftRail' : 'rightRail';\n const resize = resolveResize(id, key, panel);\n return (\n <aside\n id={workbenchRegionId(id, key)}\n class={`kui-workbench__rail kui-workbench__rail--${side}`}\n data-workbench-rail={side}\n data-collapsed={String(panel.collapsed ?? false)}\n data-separator={panel.separator ?? 'auto'}\n data-collapse-motion={panel.collapseMotion ?? 'slide'}\n data-content-overflow={panel.contentOverflow ?? 'clip'}\n data-presentation={panel.presentation ?? 'inline'}\n data-responsive-overlay-at={panel.responsiveOverlayAt}\n {...resizeAttributes(resize)}\n aria-label={panel.label || undefined}\n aria-hidden={hiddenFromAccessibility(panel)}\n inert={Boolean(panel.collapsed)}\n style={panelStyle(panel, resize, '--kui-workbench-rail-width')}\n >\n <div\n class=\"kui-workbench__panel-content\"\n inert={Boolean(panel.collapsed)}\n >\n {panel.content}\n </div>\n {resize && (\n <PanelHandle\n panel={panel}\n resize={resize}\n label={panel.label || DEFAULT_LABELS[key]}\n />\n )}\n </aside>\n );\n}\n\nfunction Drawer({ id, panel }: { id: string; panel: WorkbenchPanel }) {\n const resize = resolveResize(id, 'bottomDrawer', panel);\n return (\n <section\n id={workbenchRegionId(id, 'bottomDrawer')}\n class=\"kui-workbench__drawer\"\n data-workbench-drawer\n data-collapsed={String(panel.collapsed ?? false)}\n data-separator={panel.separator ?? 'auto'}\n data-collapse-motion={panel.collapseMotion ?? 'slide'}\n data-content-overflow={panel.contentOverflow ?? 'clip'}\n data-presentation={panel.presentation ?? 'inline'}\n data-responsive-overlay-at={panel.responsiveOverlayAt}\n {...resizeAttributes(resize)}\n aria-label={panel.label || undefined}\n aria-hidden={hiddenFromAccessibility(panel)}\n inert={Boolean(panel.collapsed)}\n style={panelStyle(panel, resize, '--kui-workbench-drawer-height')}\n >\n <div\n class=\"kui-workbench__panel-content\"\n inert={Boolean(panel.collapsed)}\n >\n {panel.content}\n </div>\n {resize && (\n <PanelHandle\n panel={panel}\n resize={resize}\n label={panel.label || DEFAULT_LABELS.bottomDrawer}\n />\n )}\n </section>\n );\n}\n\n/** A collapsed panel's restore control, outside the clipped panel. */\nfunction restore(\n panel: WorkbenchPanel | undefined,\n side: 'left' | 'right' | 'bottom',\n position: ResizableRegionRestorePosition,\n) {\n if (\n !panel?.collapsed ||\n !panel.restoreControl ||\n panel.presentation === 'hidden'\n )\n return '';\n return (\n <div\n class=\"kui-workbench__restore\"\n data-panel={side}\n data-position={panel.restorePosition ?? position}\n >\n {panel.restoreControl}\n </div>\n );\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. Bottom-drawer content stays anchored\n * to the shell's stable bottom edge throughout that transition. The app owns\n * each `collapsed` flag; the collapse is pure CSS. A collapsed panel renders\n * `inert` and `aria-hidden`, so neither Tab nor assistive technology reaches\n * controls that have slid out of view or an empty landmark (its restore control\n * lives outside it and stays reachable). A panel may opt in to drag and keyboard resizing with\n * `resizable`, which `wireWorkbench` drives. See `docs/23-app-layouts.md` §3.3.\n */\nexport function Workbench({\n id,\n label,\n main,\n leftRail,\n rightRail,\n bottomDrawer,\n mainMinSize = WORKBENCH_MAIN_MIN_SIZE,\n mainMinHeight = WORKBENCH_MAIN_MIN_HEIGHT,\n className = '',\n slot,\n}: WorkbenchProps) {\n // Each work-area minimum applies only on an axis a panel shares with it:\n // the width beside a rail, the height above the drawer.\n const px = (value: number) => Math.max(0, Math.round(value));\n const mainMin = leftRail || rightRail ? px(mainMinSize) : undefined;\n const mainMinBlock = bottomDrawer ? px(mainMinHeight) : undefined;\n const style = [\n mainMin === undefined ? '' : `--_kui-workbench-main-min-width:${mainMin}px`,\n mainMinBlock === undefined\n ? ''\n : `--_kui-workbench-main-min-height:${mainMinBlock}px`,\n ]\n .filter(Boolean)\n .join(';');\n return (\n <section\n class={`kui-workbench ${className}`.trim()}\n id={id}\n data-component=\"workbench\"\n data-main-min-size={mainMin === undefined ? undefined : String(mainMin)}\n data-main-min-height={\n mainMinBlock === undefined ? undefined : String(mainMinBlock)\n }\n style={style || undefined}\n aria-label={label}\n slot={slot}\n >\n {leftRail && <Rail id={id} side=\"left\" panel={leftRail} />}\n {restore(leftRail, 'left', 'bottom-start')}\n <div class=\"kui-workbench__center\">\n <div class=\"kui-workbench__main\" data-workbench-main>\n {main}\n </div>\n {bottomDrawer && <Drawer id={id} panel={bottomDrawer} />}\n {restore(bottomDrawer, 'bottom', 'bottom-end')}\n </div>\n {rightRail && <Rail id={id} side=\"right\" panel={rightRail} />}\n {restore(rightRail, 'right', 'bottom-end')}\n </section>\n );\n}\n"]}
@@ -12,6 +12,7 @@ These are normative contracts for package components and consuming applications.
12
12
  - `wireNavStack` moves focus into every newly active top view and remembers the last focused descendant of each mounted view for pop restoration. Mark a preferred initial heading or control with `data-nav-focus` (and `tabindex="-1"` when it is not normally tabbable); otherwise the helper uses the first enabled focusable descendant, then a programmatically focusable view-container fallback.
13
13
  - In any `.kui-pane`, menu rows, `ListHeader` actions, and toolbar groups keep a minimum 44px target in both dimensions. A `ListHeader` fills the available inline width and keeps that action at the logical end, including in RTL. Its action visual defaults to 18px through `--kui-list-header-action-icon-size` inside a fitted 36px hover square; a transparent hit layer extends the pointer target to at least 44 × 44 around that square without moving layout or enlarging the painted surface. Compact headers are 36px tall like the rest of the compact List family, so their action target is 44px wide and clamped to the 36px header height instead of reaching into a neighboring row. Do not reduce the target to the visible icon, and do not force the button itself to 44px.
14
14
  - `LoadingSpinner` is either labeled (`role="img"`) or hidden. Its rotation stops for reduced motion.
15
+ - An unlabeled `Skeleton` (including the value slots a component's `placeholder` prop renders) is decorative loading chrome: it is `aria-hidden`, conveys no information a user needs to act on, and is deliberately exempt from the WCAG 3:1 non-text-contrast target. Inside a solid `Badge` it stays subtle (about 2.1–4.4:1 in light, 2.4–2.8:1 in dark, depending on tone); `tests/browser/tone-contrast.spec.ts` holds those values as a regression floor rather than an accessibility threshold. Announce loading through a labeled `Skeleton`, `LoadingSpinner`, or `aria-busy` on the owning region, never through the skeleton's visibility.
15
16
  - `StateBanner` defaults to polite `role="status"`; use `urgency="alert"` only for an attention-requiring failure. Its optional `badge` is persistent inline status/count content beside the title and is announced as part of the banner; keep it terse and do not rely on color alone for its meaning.
16
17
  - `EmptyState` reports busy state through `aria-busy` and never relies on an illustration as its label.
17
18
  - `Select` follows the Web Awesome host's standard `input`/`change` events. Application tests verify the live `value`, focus, and events—not attributes alone. Its decorative option icons and value-dependent selected content remain present after controlled rerenders, so the visible choice does not silently lose its non-text cue.
@@ -120,6 +121,15 @@ The handle exposes separator role, orientation, name, minimum, maximum, and curr
120
121
  them after a re-render writes the rendered props back, so assistive
121
122
  technology never reads an unclamped size between interactions (the
122
123
  disposer disconnects both observers);
124
+ - a truthful value below the minimum: when the container shows the track at
125
+ less than `min` (a narrowing `Workbench` squeezes its resizable rails), the
126
+ handle reports that shown size in `aria-valuenow` and pins `aria-valuemin`
127
+ and `aria-valuemax` to it — WAI-ARIA requires the value to lie inside the
128
+ range, and the separator cannot move a track its container holds. A pinned
129
+ separator — squeezed, or with a reachable range collapsed to one size —
130
+ commits nothing: arrow keys, Home/End, and drags leave the app's size (and
131
+ its storage) as it was, so the track returns to it, with the rendered range,
132
+ once it has room;
123
133
  - a whole hit target and focus ring at a clamped edge: when the separator has
124
134
  less room past it than the handle's 10px overhang (a parent clamps the track
125
135
  to its own edge), the region carries `data-handle-inset` and the handle sits
@@ -162,7 +172,10 @@ forced to a dark color scheme, but it is **not** in the top layer, so it never
162
172
  covers dialogs, popovers, or other overlays. Its children are the app's controls
163
173
  (normally `ToolbarControlGroup`s), which keep their own names, focus, and
164
174
  keyboard behavior; the application owns their actions, the toolbar's visibility,
165
- and — via `position` and `--kui-floating-toolbar-inset` — where it sits.
175
+ and — via `position` and `--kui-floating-toolbar-inset`, set on the toolbar
176
+ or an ancestor — where it sits. Inside a `Workbench`, `CollapsiblePanel`, or
177
+ `ResizableRegion` `restoreControl` the restore corner owns the inset, so the
178
+ toolbar floats from that corner.
166
179
 
167
180
  ## Tabs
168
181
 
@@ -180,8 +193,11 @@ that geometry; applications must not override its anatomy to create the split.
180
193
  presentations. Icon-only tabs keep the required `name` as the tab button's
181
194
  accessible name while visually hiding the duplicate label. A `pending` tab (known,
182
195
  still opening) keeps its visible name as the tab button's accessible name, so a
183
- trailing spinner's own label is not folded into it; like `placeholder`, it is
184
- `aria-busy`, disabled, out of the tab order, and not draggable. These props own
196
+ trailing spinner's own label is not folded into it. It is `aria-busy` and stays
197
+ selectable (in the roving tab order like a live tab), so the application can
198
+ show placeholder content in its panel until loading completes; its close
199
+ button is disabled and hidden and it is not draggable. A `placeholder` tab
200
+ (unknown) is instead disabled, out of the tab order, and not draggable. These props own
185
201
  component appearance only; the application still owns the bar's outer placement.
186
202
  `AppTab.rootAttributes` accepts application `data-*` metadata only; runtime
187
203
  filtering rejects roles plus case variants of component- or wiring-owned
@@ -192,8 +208,11 @@ and must not contain interactive descendants.
192
208
  activation, same-bar pointer reordering, `Alt+Shift+ArrowLeft/ArrowRight`
193
209
  reordering, focus restoration, scroll-into-view, and pointer-proximity
194
210
  autoscroll at either horizontal edge. Edge autoscroll is direct manipulation,
195
- stops on drop/drag end/disposal, and does not change keyboard behavior. It
196
- returns a disposer.
211
+ stops on drop/drag end/disposal, and does not change keyboard behavior. When
212
+ the application changes a bar's selected tab without focus (adding and
213
+ selecting a tab), the wiring scrolls that tab's strip — never the page — so
214
+ the new selection is fully visible; a re-render that keeps the same selection
215
+ leaves a strip the user scrolled where it is. It returns a disposer.
197
216
 
198
217
  Arrow / Home / End follow the ARIA Tabs **automatic-activation** pattern by
199
218
  default: they move roving focus and select the focused tab. If selection synchronously
@@ -276,15 +295,26 @@ and focus deliberately moved to another control; no app-level reopen callback is
276
295
 
277
296
  ## CollapsiblePanel / sidebar
278
297
 
279
- `CollapsiblePanel` is a labeled `aside` region that is `aria-hidden` while
280
- collapsed; its content slides via transform (disabled under reduced motion).
298
+ `CollapsiblePanel` is a labeled `aside` region that renders `inert` and
299
+ `aria-hidden` while collapsed, straight from the app's `collapsed` flag, so its
300
+ controls leave the Tab order and the accessibility tree even while the content
301
+ is still sliding out, and focusing one can never scroll clipped content back into
302
+ view (`aria-hidden` alone left them focusable inside a hidden subtree). Its
303
+ content slides via transform (disabled under reduced motion); `inert` does not
304
+ affect rendering, so the slide still plays. A `restoreControl` renders outside
305
+ the panel and stays reachable. A collapsed `ResizableRegion` renders `inert` and
306
+ `aria-hidden` on the region itself the same way (its content wrapper is `inert`
307
+ too, and its separator is already hidden and not displayed), so no empty
308
+ labeled region landmark stays behind; its `restoreControl` stays outside the
309
+ region.
281
310
  `CollapsiblePanelToggle` is a named button carrying `aria-expanded` and the
282
311
  standard per-side collapse/expand glyph (`PanelLeft*` / `PanelRight*` /
283
312
  `PanelBottom*`); place a collapse toggle inside the panel and an expand toggle in
284
313
  an always-visible location so it is reachable while collapsed. `wireSidebar()`
285
314
  moves focus into the panel when it opens and restores it to the trigger when it
286
- closes; a trigger inside the now-hidden panel hands focus to the panel's toggle
287
- outside it instead, so an app may render the expand toggle only while the panel
315
+ closes; a trigger inside the now-inert panel hands focus to the panel's toggle
316
+ outside it instead (and focus stranded there by a wide → compact crossing is
317
+ rescued the same way), so an app may render the expand toggle only while the panel
288
318
  is collapsed. When a `deviceClass()` reports `compact`, an open panel becomes an overlay
289
319
  with a dismissable backdrop, Escape and backdrop-click collapse it, and Tab is
290
320
  trapped within it (the ARIA dialog pattern). That modal state only ever begins
@@ -293,6 +323,21 @@ crossing, every panel starts collapsed, so no page load or resize traps focus. T
293
323
  signal, the panels, their sizes, and content; the wire may persist the collapsed
294
324
  state per panel.
295
325
 
326
+ ## Workbench
327
+
328
+ Each `Workbench` rail and drawer is a labeled region (`aside` for a rail, `section`
329
+ for the drawer). While a panel is collapsed the region itself renders `inert` and
330
+ `aria-hidden="true"`, and so does not linger as an empty landmark, and its content
331
+ wrapper is `inert`, all rendered from the app's `collapsed` flag, so its controls
332
+ leave the Tab order and the accessibility tree even while the content is still
333
+ sliding out, and focusing one can never scroll clipped content back into view. The panel's `restoreControl` renders
334
+ outside that content and stays reachable; a resizable panel's separator also leaves
335
+ the Tab order while collapsed or not inline. `wireWorkbench`, given a panel's
336
+ `collapsed` signal, returns focus stranded in a closing panel to the control that
337
+ opened it, else the restore control, else a control whose `aria-controls` names the
338
+ panel; an open overlay panel takes focus and keeps Tab inside it (the ARIA dialog
339
+ pattern). See [`workbench.md`](workbench.md).
340
+
296
341
  ## Verification matrix
297
342
 
298
343
  For each changed component, inspect default, hover, focus, disabled, selected/pressed, busy/error, long-content, wide, narrow, light, dark, increased-contrast, reduced-motion, keyboard-only, and 200%-zoom states where applicable. DOM order must match reading and focus order, with no clipping or unreachable action.
@@ -113,7 +113,18 @@ device in tests.
113
113
  selection).
114
114
  - **IDE (complex tool):** `Workbench` with a left navigator rail, a right
115
115
  inspector rail, and a bottom console drawer, each `collapsed` bound to a
116
- signal. Only offer this `atLeast('desktop')`.
116
+ signal. Only offer this `atLeast('desktop')`. To let people size the
117
+ navigator or console, opt those panels in with `resizable` and call
118
+ `wireWorkbench` with their size signals and `deviceClass`; resizing is
119
+ suspended on `compact`, where the rails become overlay drawers — or give each
120
+ rail `responsiveOverlayAt: "narrow"` and let the Workbench's own width decide
121
+ when it overlays, with no device check; pass each rail's `collapsed` signal
122
+ to `wireWorkbench` so the overlay starts collapsed, takes focus and keeps Tab
123
+ inside it while open, and closes on Escape or an outside press. Inline
124
+ rails, fixed or resizable, leave the editor its `mainMinSize` (320px by
125
+ default) and shrink in proportion when the window narrows; the bottom drawer
126
+ leaves it `mainMinHeight` (120px by default) and shrinks when the window gets
127
+ shorter.
117
128
  - **Social app (tabbed):** `TabScaffold` with Home / Search / Profile tabs, each
118
129
  `content` a `NavStack`. On a tablet/desktop, render the same sections as a
119
130
  `Workbench` left rail instead of a bottom bar.