@kerfjs/ui 5.0.0-beta.53 → 5.0.0-beta.55

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 (152) hide show
  1. package/README.md +4 -2
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/component-catalog-v2.json +138 -5
  4. package/ai/component-catalog-v2.schema.json +2 -1
  5. package/ai/component-catalog.json +208 -16
  6. package/ai/public-api-signatures-v1.md +462 -39
  7. package/ai/skill.md +28 -20
  8. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  9. package/analyzer/index.mjs +48 -0
  10. package/analyzer/loud-pairs.d.mts +49 -0
  11. package/analyzer/loud-pairs.mjs +357 -0
  12. package/dist/app-tab.d.ts +7 -5
  13. package/dist/app-tab.js +1 -1
  14. package/dist/browser/popup-menu.js +3 -0
  15. package/dist/catalog-resources.d.ts +1 -1
  16. package/dist/catalog.d.ts +10 -4
  17. package/dist/catalog.js +243 -300
  18. package/dist/catalog.js.map +1 -1
  19. package/dist/{chunk-7IUSV4XG.js → chunk-24PHKUR3.js} +2 -2
  20. package/dist/chunk-24PHKUR3.js.map +1 -0
  21. package/dist/{chunk-EKYZCRCX.js → chunk-3CGQ7ZNO.js} +54 -22
  22. package/dist/chunk-3CGQ7ZNO.js.map +1 -0
  23. package/dist/{chunk-UKKSMZYT.js → chunk-46263QLK.js} +51 -1
  24. package/dist/chunk-46263QLK.js.map +1 -0
  25. package/dist/chunk-4BIMLITI.js +92 -0
  26. package/dist/chunk-4BIMLITI.js.map +1 -0
  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-ZK5EGVFP.js → chunk-5OLVUEND.js} +12 -2
  30. package/dist/chunk-5OLVUEND.js.map +1 -0
  31. package/dist/{chunk-CMUIXJZM.js → chunk-BSKEH3UC.js} +2 -2
  32. package/dist/{chunk-CMUIXJZM.js.map → chunk-BSKEH3UC.js.map} +1 -1
  33. package/dist/chunk-EAX2GSS4.js +384 -0
  34. package/dist/chunk-EAX2GSS4.js.map +1 -0
  35. package/dist/chunk-KH6VUGHU.js +399 -0
  36. package/dist/chunk-KH6VUGHU.js.map +1 -0
  37. package/dist/{chunk-BJJKFVWW.js → chunk-LZXQVUGN.js} +2 -2
  38. package/dist/chunk-LZXQVUGN.js.map +1 -0
  39. package/dist/chunk-N7FHFNT6.js +55 -0
  40. package/dist/chunk-N7FHFNT6.js.map +1 -0
  41. package/dist/{chunk-CCMKJWLX.js → chunk-NE5JAOQS.js} +7 -7
  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-KPNTVB7A.js → chunk-R754MHXX.js} +3 -3
  46. package/dist/{chunk-KPNTVB7A.js.map → chunk-R754MHXX.js.map} +1 -1
  47. package/dist/{chunk-HJKRCKDR.js → chunk-RNRUVPJ3.js} +4 -2
  48. package/dist/chunk-RNRUVPJ3.js.map +1 -0
  49. package/dist/{chunk-VOCPXGQN.js → chunk-VHADGP3B.js} +9 -8
  50. package/dist/chunk-VHADGP3B.js.map +1 -0
  51. package/dist/chunk-XQV2B4IL.js +14 -0
  52. package/dist/chunk-XQV2B4IL.js.map +1 -0
  53. package/dist/collapsible-panel.d.ts +46 -15
  54. package/dist/collapsible-panel.js +42 -10
  55. package/dist/collapsible-panel.js.map +1 -1
  56. package/dist/floating-toolbar.d.ts +5 -2
  57. package/dist/floating-toolbar.js +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.js +20 -18
  60. package/dist/list-header.js +1 -1
  61. package/dist/list-inset-text.d.ts +7 -1
  62. package/dist/list-inset-text.js +2 -1
  63. package/dist/panel-toolbar-BFQxFITu.d.ts +52 -0
  64. package/dist/popup-menu-register.d.ts +9 -0
  65. package/dist/popup-menu-register.js +30 -0
  66. package/dist/popup-menu-register.js.map +1 -0
  67. package/dist/popup-menu.d.ts +71 -0
  68. package/dist/popup-menu.js +3 -0
  69. package/dist/popup-menu.js.map +1 -0
  70. package/dist/resizable-region.d.ts +4 -1
  71. package/dist/resizable-region.js +1 -1
  72. package/dist/split-view.js +1 -1
  73. package/dist/state-banner.js +1 -1
  74. package/dist/styles/catalog/components/catalog-example.css +0 -16
  75. package/dist/styles/catalog/components/catalog-stage.css +8 -13
  76. package/dist/styles/catalog/components/catalog.css +8 -18
  77. package/dist/styles/catalog.css +0 -5
  78. package/dist/styles/collapsible-panel.css +88 -10
  79. package/dist/styles/floating-toolbar.css +11 -6
  80. package/dist/styles/popup-menu.css +12 -0
  81. package/dist/styles/resizable-region.css +81 -13
  82. package/dist/styles/state-banner.css +13 -4
  83. package/dist/styles/styles.css +1 -0
  84. package/dist/styles/tab-bar.css +31 -3
  85. package/dist/styles/toolbar-control-group.css +13 -0
  86. package/dist/styles/toolbar.css +43 -2
  87. package/dist/styles/workbench.css +503 -10
  88. package/dist/text.js +1 -1
  89. package/dist/token-search-field.d.ts +10 -1
  90. package/dist/token-search-field.js +1 -1
  91. package/dist/{types-CEsieBZi.d.ts → types-D6vzj-VT.d.ts} +15 -0
  92. package/dist/wire-catalog.d.ts +13 -1
  93. package/dist/wire-catalog.js +18 -3
  94. package/dist/wire-catalog.js.map +1 -1
  95. package/dist/wire-resizable-regions.d.ts +2 -1
  96. package/dist/wire-resizable-regions.js +2 -1
  97. package/dist/wire-sidebar.js +6 -3
  98. package/dist/wire-sidebar.js.map +1 -1
  99. package/dist/wire-tab-bars.js +1 -1
  100. package/dist/wire-token-search-fields.js +1 -1
  101. package/dist/wire-workbench.d.ts +109 -0
  102. package/dist/wire-workbench.js +6 -0
  103. package/dist/wire-workbench.js.map +1 -0
  104. package/dist/workbench.d.ts +166 -6
  105. package/dist/workbench.js +11 -104
  106. package/dist/workbench.js.map +1 -1
  107. package/docs/accessibility.md +66 -10
  108. package/docs/app-layouts.md +12 -1
  109. package/docs/catalog.md +27 -9
  110. package/docs/collapsible-panel.md +114 -1
  111. package/docs/component-contract.md +2 -2
  112. package/docs/component-selection.md +41 -23
  113. package/docs/design/templates/state-banner/danger-dark.svg +1 -1
  114. package/docs/design/templates/state-banner/danger.svg +1 -1
  115. package/docs/design/templates/state-banner/info-dark.svg +1 -1
  116. package/docs/design/templates/state-banner/info.svg +1 -1
  117. package/docs/design/templates/state-banner/success-dark.svg +1 -1
  118. package/docs/design/templates/state-banner/success.svg +1 -1
  119. package/docs/design/templates/state-banner/warning-dark.svg +1 -1
  120. package/docs/design/templates/state-banner/warning.svg +1 -1
  121. package/docs/design/templates/state-banner-dark.svg +1 -1
  122. package/docs/design/templates/state-banner.svg +1 -1
  123. package/docs/document-baseline.md +4 -0
  124. package/docs/layout.md +10 -1
  125. package/docs/recipes.md +48 -39
  126. package/docs/surface-scaffold.md +1 -1
  127. package/docs/type-contracts.md +2 -1
  128. package/docs/ui-analyzer.md +94 -19
  129. package/docs/ui-doctor.md +6 -0
  130. package/docs/ux-demo.md +7 -3
  131. package/docs/webawesome-theme.md +11 -2
  132. package/docs/workbench.md +388 -8
  133. package/llms.txt +13 -8
  134. package/package.json +17 -2
  135. package/ux-demo/recipes/app-shell.tsx +141 -223
  136. package/ux-demo/recipes/collapsible-sidebar.tsx +48 -78
  137. package/ux-demo/recipes/list-detail-dialog.tsx +150 -54
  138. package/ux-demo/recipes/mount-recipe.ts +0 -5
  139. package/ux-demo/recipes/types.ts +0 -1
  140. package/dist/chunk-7IUSV4XG.js.map +0 -1
  141. package/dist/chunk-BJJKFVWW.js.map +0 -1
  142. package/dist/chunk-CCMKJWLX.js.map +0 -1
  143. package/dist/chunk-EKYZCRCX.js.map +0 -1
  144. package/dist/chunk-HJKRCKDR.js.map +0 -1
  145. package/dist/chunk-UKKSMZYT.js.map +0 -1
  146. package/dist/chunk-VOCPXGQN.js.map +0 -1
  147. package/dist/chunk-ZK5EGVFP.js.map +0 -1
  148. package/dist/styles/catalog/components/catalog-detail.css +0 -35
  149. package/dist/styles/catalog/components/catalog-resource-footer.css +0 -37
  150. package/dist/styles/catalog/components/catalog-secondary-sections.css +0 -38
  151. package/dist/styles/catalog/components/catalog-section-list.css +0 -22
  152. package/dist/styles/catalog/components/catalog-sidebar.css +0 -82
package/docs/ux-demo.md CHANGED
@@ -10,7 +10,11 @@ launch Chromium, Firefox, and WebKit. The build emits
10
10
  `source-freshness.json`; browser coverage compares it with an independent hash
11
11
  of the current `src/` and `ux-demo/` inputs, so a stale `dist-demo` cannot pass
12
12
  after a route or component edit. `npm run demo:serve` remains a preview-only
13
- command for an already-built catalog.
13
+ command for an already-built catalog. The demo-conformance preflight also
14
+ rejects a component route that wraps its `CatalogExampleStack` in a card or
15
+ other container (`catalog-demo/wrapped-example-stack`): single-component demos
16
+ sit directly on the catalog grid, and only composition and recipe routes frame
17
+ their content.
14
18
 
15
19
  Ten lazy production-composition routes live under the `Recipes` category.
16
20
  Each `?component=recipe-*` route uses public package primitives, real wiring,
@@ -18,14 +22,14 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
18
22
 
19
23
  - It imports public component subpaths, exercising their browser-selected reachable CSS rather than a catalog-only style path.
20
24
  - The `ListActionRow` route demonstrates sibling primary/trailing native controls, independent disabled states, controlled selection, first-line leading-icon alignment for wrapped labels, and an application-owned native popover relationship without nesting controls. The `ListItem` route demonstrates the same multiline leading-icon anchor alongside its single-control navigation contract.
21
- - The `AppTab` and `TabBar` routes are focused component specimens: AppTab shows its selected, compact, segmented, icon-only, truncation, placeholder, pending, metadata, and close-icon axes inside the required tablist context; TabBar shows its rail, segmented, inspector, allocation, placement, and accessory-zone axes, including an adjacent action plus a separately pinned end action whose tabs overflow at narrow widths, without application readouts. The separate `Application tabs` composition route demonstrates controlled selection, matching panel content, overflow, adding, closing, keyboard/pointer reordering, and application-owned ordering. The `ResizableRegion` route likewise replaces only the decorative handle glyph while retaining separator semantics and shared wiring.
25
+ - The `AppTab` and `TabBar` routes are focused component specimens: AppTab shows its selected, compact, segmented, icon-only, truncation, placeholder, pending, metadata, and close-icon axes inside the required tablist context; TabBar shows its rail, segmented, inspector, allocation, placement, and accessory-zone axes, including an adjacent action plus a separately pinned end action whose tabs overflow at narrow widths, without application readouts. The separate `Application tabs` composition route demonstrates controlled selection, matching panel content, overflow, adding (a new tab opens `pending` with a spinner and stays selectable while its panel shows a `Skeleton` heading until a simulated load completes), closing, keyboard/pointer reordering, and application-owned ordering. The `ResizableRegion` route likewise replaces only the decorative handle glyph while retaining separator semantics and shared wiring.
22
26
  - Every first-party visual component and every free Web Awesome 3.12 component has its own stable `?component=<id>` URL and focused demo. Composite header, menu, and feedback scenarios remain addressable alongside their subcomponents; the root opens the first Kerf component. The former aggregate Web Awesome theme specimen is intentionally omitted because its component families are covered by those focused routes.
23
27
  - `ai/component-catalog.json` is the canonical catalog metadata. A deterministic checked-in projection supplies the demo's unique id, category, kind (`component`, `composition`, or `recipe`), source (`kerf` or `webawesome`), name, description, recommendation, direct `uses` dependencies, existing documentation path, and demo-source path; first-party, recipe, and Web Awesome renderer maps are exhaustively typed against its generated id unions. Functional groups follow their authored importance order; components precede compositions inside each group, equal-importance entries use alphabetical order, and the Recipes group comes last. Composition names stay concise and project a visible `Composition` sidebar tag. Non-supported Web Awesome recommendations project to a visible `Discouraged` tag, with Popup as the deliberate conditional exception. Demo paths resolve to the matching `ui/ux-demo/demos/<id>.tsx` or `ui/ux-demo/recipes/*.tsx` file, or to `ui/ux-demo/webawesome-demos.tsx` for Web Awesome entries. `ui/ux-demo/main.tsx` owns only the catalog shell, shared state, routing, and interaction wiring. First-party component entries additionally derive `ui/src/<public-subpath>.tsx` from their canonical `delivery.browserImport` or opt-in `delivery.moduleImport`.
24
28
  - Every detail footer groups compact `Demo source` and `Guidance` actions; first-party component details also expose `Component source`. Web Awesome details say `Integration guidance` to distinguish the local integration contract from the upstream component implementation. Each button retains its repository-relative path in the DOM for verification and uses a deploy-safe absolute GitHub blob URL with a contextual new-tab name; the demo does not depend on a local source server or add a package runtime export. At phone widths, the resource group and related-component selector stack as full-width rows, with any resource overflow contained locally so labels cannot overlap the selector or widen the document.
25
29
  - The master/detail shell groups first-party routes by category with production `ListHeader` and `ListItem` components. Catalog entries can supply short reusable trailing tags; the Web Awesome projection uses them to label exactly the 15 discouraged choices. The 70 ecosystem components live in a separate `Web Awesome` disclosure with seven subgroups, so they remain discoverable without being presented as first-class `@kerfjs/ui` exports. Direct Web Awesome routes open the disclosure and reveal the selected row at wide sizes; narrow layouts keep the expanded list in document flow rather than introducing a competing nested scroller.
26
30
  - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. Its sidebar identity uses the Kerf logo beside a vertically centered title, with supporting copy in its own aligned row; the current-view heading is also a toolbar. Collapsing the catalog removes the pane completely and moves its restore action to the main toolbar's leading edge instead of leaving an icon-only rail. The selected view description remains inset below the page toolbar. The checkerboard belongs to the preview scroll owner rather than a viewport-height stage or nested card, and one explicitly repeated SVG tile keeps the pattern continuous through below-fold content. A lightly separated footer keeps live status above compact source, guidance, and relationship groups. The focused `ResizableRegion` specimen stretches its surface through the available stage height and reports committed width in that shared status footer; narrow layouts retain local horizontal access when the pane is resized wider than the stage, and the split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
27
31
  - The composer-form recipe demonstrates one coherent visible form surface with toolbar title/supporting-copy hierarchy, fields and actions on the shared 8px control gutter, and 24px major rhythm. Its conditional error or success `StateBanner` is the only nested semantic surface. Reset synchronizes the upgraded Web Awesome fields' live value properties with their empty controlled attributes and announces `Draft reset`; browser coverage preserves behavior and verifies wide, narrow, dark, validation, success, reset, 200% zoom, and forced-colors states.
28
- - The list-detail dialog recipe composes `DialogSurface`, the dialog's own labeled header and close control, `SplitView`, and footer actions. Its `SplitView` sits flush in the dialog body, both panes open with a `ListHeader` on one shared line, the dialog title and header text share the 17px text edge, the value table keeps the 8px outer margin, and the trailing actions keep the 16px footer gutter. Compact devices get a full-screen sheet with a list-to-detail drill-down. Browser geometry coverage protects those relationships at wide and narrow widths.
32
+ - The list-detail dialog recipe is laid out like a `Workbench` on roomy devices: a headerless `DialogSurface` modal whose `SplitView` holds a full-height list sidebar `Pane` with its own toolbar beside a full-height detail `Pane` that carries the primary xl title, the `data-dialog="close"` control, and the record actions in its footer. Compact devices get a full-screen sheet with its own labeled header, footer actions, and a list-to-detail drill-down. Browser geometry coverage protects those relationships at wide and narrow widths.
29
33
  - Catalog-only brand marks, preview dimensions, checkerboard tiles, and the compound display-settings control retain local geometry because they are specimen chrome or intrinsic control anatomy rather than reusable application spacing roles.
30
34
  - The menu composition route demonstrates toolbar/content/footer anatomy, 24px major gaps, 8/1/8 child geometry, the full-width `ListHeader` count-or-badge/action split, logical-end 44px action targets with 18px visuals, and 44px row/header/toolbar targets. Its one chevron is a production 18px `DisclosureArrow` inside a controlled Tools header that actually reveals and hides content; ordinary Projects navigation has no false disclosure affordance. Browser assertions cover stable naming, expanded/content transitions, visual rotation, node identity, reduced motion, RTL alignment and containment, and wide, narrow, dark, and 200% layouts. The focused ListHeader route separately shows the shared neutral count pill for Attachments, Notes (including zero), and Duplicates plus a non-count legacy badge, full-width logical-end actions, and no disclosure without content.
31
35
  - The `DisclosureArrow` route renders the production 18px root-scaled default without a catalog-only size override. Its two native buttons independently control the default right-to-down chevron and a recognizable right-facing replacement arrow with left-to-up custom directions; both use shortest-path rotation and their names stay stable while `aria-expanded` communicates state. Browser coverage verifies the custom clockwise path, pointer and native keyboard activation, focus, narrow and 200%-zoom stacking with contained labels, consumer sizing, and the independent `.5` Web Awesome glyph scale retained by Kerf `Select`.
@@ -292,19 +292,28 @@ smallest subtree that needs a different identity:
292
292
  ```css
293
293
  :root {
294
294
  --wa-color-brand-fill-loud: #7540a8;
295
+ --wa-color-brand-on-loud: #ffffff;
295
296
  --wa-color-focus: #7540a8;
296
297
  --wa-form-control-border-radius: 0.5rem;
297
298
  }
298
299
 
299
300
  .billing-workspace {
300
301
  --wa-color-success-fill-loud: #10a86b;
302
+ --wa-color-success-on-loud: #1d1d1f;
301
303
  }
302
304
  ```
303
305
 
304
306
  The shipped theme pairs every `--wa-color-{variant}-fill-loud` with an
305
307
  `--wa-color-{variant}-on-loud` that clears WCAG AA (4.5:1) in both color
306
- schemes. When you override a loud fill, override its `on-loud` alongside it if
307
- the pair no longer clears 4.5:1 (for example, white text on a bright fill).
308
+ schemes. When you override a loud fill, set its `on-loud` alongside it, in the
309
+ same selector and at-rule scope, to a foreground that clears 4.5:1 on the new
310
+ fill (white text on a bright fill usually does not: `#10a86b` above reaches
311
+ only 3.1:1 with white, 5.5:1 with `#1d1d1f`). `kerf-ui-analyze` and
312
+ `kerf-ui-doctor` report a loud fill override without a governing `on-loud`, or
313
+ a literal pair that measures below 4.5:1, as review finding
314
+ [`KUI-L018`](./ui-analyzer.md#loud-fill--on-loud-pairing-kui-l018); it cannot
315
+ measure a `var()` or `color-mix()` value, so choosing a readable one is still
316
+ yours.
308
317
 
309
318
  Use `.wa-light`, `.wa-dark`, and `.wa-invert` for explicit appearance scopes.
310
319
  Component-specific custom properties and documented `::part()` selectors from
package/docs/workbench.md CHANGED
@@ -22,7 +22,22 @@ it can also be embedded. See [Document baseline](document-baseline.md).
22
22
 
23
23
  `Workbench` is declarative and the collapse is **pure CSS** — no wire. The app
24
24
  owns each panel's `collapsed` flag (usually a signal) and toggles it; the panel
25
- animates itself.
25
+ animates itself. Panels are fixed-size by default; a panel can opt in to drag
26
+ and keyboard resizing (see [Resizable panels](#resizable-panels)), and
27
+ `wireWorkbench` can make overlay panels transient (see
28
+ [Transient overlays](#transient-overlays)).
29
+
30
+ A collapsed panel renders `inert` and `aria-hidden="true"` on the rail or
31
+ drawer itself, and `inert` on its content, straight from `collapsed`, so
32
+ neither Tab nor assistive technology reaches controls that have slid out of
33
+ view, none of them can scroll back into view, and no empty labeled landmark
34
+ stays behind. The slide-out still animates,
35
+ and the panel's `restoreControl` lives outside the inert content, so it stays
36
+ reachable. Expanding the panel makes its content reachable again in the same
37
+ render. When the panel closes with focus inside it, `wireWorkbench` (given the
38
+ panel's `collapsed` signal) moves focus back out (see
39
+ [Transient overlays](#transient-overlays)); without it, that focus move is the
40
+ app's.
26
41
 
27
42
  ```tsx
28
43
  const navCollapsed = signal(false);
@@ -50,7 +65,8 @@ const navCollapsed = signal(false);
50
65
  />;
51
66
  ```
52
67
 
53
- Each `WorkbenchPanel` takes `content`, an optional `collapsed`, an optional
68
+ Each `WorkbenchPanel` takes `content`, an optional `toolbar` and `footer` (see
69
+ [Panel toolbars](#panel-toolbars)), an optional `collapsed`, an optional
54
70
  `size` (rail width or drawer height in px, overriding the CSS default —
55
71
  `--kui-workbench-rail-width` 280px, `--kui-workbench-drawer-height` 220px), and
56
72
  an optional `label`. Common shell behavior is configured rather than restyled:
@@ -61,15 +77,372 @@ an optional `label`. Common shell behavior is configured rather than restyled:
61
77
  - `contentOverflow: "clip" | "auto" | "visible"` lets a drawer temporarily
62
78
  expose an open popup without a descendant override;
63
79
  - `presentation: "inline" | "overlay" | "hidden"` supports compact overlays
64
- or a responsive replacement. Overlay panels clamp both their track and fixed-size
65
- animated content to the configured viewport-relative maximum;
80
+ or a responsive replacement. An overlay rail spans the Workbench height at
81
+ its side and an overlay drawer spans the work-area column at the bottom,
82
+ each exactly at its configured size (its separator border included, as in
83
+ flow) up to the viewport-relative overlay maximum; the content fills the
84
+ panel inside its border, so it is clamped with it;
85
+ - `responsiveOverlayAt: "narrow" | "compact" | "never"` presents a rail or the
86
+ bottom drawer as an overlay below a Workbench container breakpoint — 704px
87
+ or less for `narrow`, 448px or less for `compact`, the breakpoints of
88
+ `ResizableRegion`'s `responsiveFillAt` — and inline above it. **Rails
89
+ default to `narrow`**, so on a small screen a sidebar covers the work area
90
+ (and an open drawer) instead of squeezing it; pass `never` to keep a rail
91
+ inline. The drawer stays inline unless it opts in. On a compact (448px or
92
+ less) Workbench an overlay rail fills the Workbench less a dismiss strip on
93
+ the side away from its edge — `--kui-workbench-overlay-dismiss-margin`,
94
+ 44px, one touch target — so a press beside it closes it;
95
+ `compactOverlay: "full"` (per rail) fills the Workbench instead, which
96
+ usually wants app-level handling of its own. Pass each rail's `collapsed`
97
+ signal to `wireWorkbench` so only one overlay is open at a time and a press
98
+ outside closes it. The CSS
99
+ decides, so the app needs no `deviceClass` check for presentation; a
100
+ collapsed overlay drops its surface and shadow so nothing covers the work
101
+ area, and the work area keeps the safe-area inset of the edge the overlay
102
+ covers. When overlays meet, the order is fixed rather than by which opened
103
+ last: both rails stack above the drawer, as inline rails span the
104
+ Workbench's full height beside the drawer's column, and the right rail
105
+ stacks above the left. Rails take `--kui-workbench-overlay-z` (41) and the
106
+ drawer one less. Restore controls take `--kui-workbench-restore-z`, which
107
+ defaults to two less than the overlay z-index (39), so they sit beneath
108
+ every overlay: an open overlay covers another panel's restore control as
109
+ it covers the rest of the work area, and a collapsed overlay, which drops
110
+ its pointer events, leaves the control beneath it usable;
66
111
  - `restoreControl` places an application-owned restore affordance in a
67
- safe-area-aware viewport corner (`restorePosition` chooses the corner).
112
+ corner of the Workbench itself while the panel is collapsed. Prefer a
113
+ `toolbar.toggle`, which the Workbench relocates for you; keep
114
+ `restoreControl` for a custom affordance. `restorePosition` chooses the
115
+ corner, inset by
116
+ `--kui-workbench-restore-inset` (16px) plus the unsafe area of each edge the
117
+ corner reaches. A rail's control sits in the Workbench's corner; the
118
+ drawer's sits in the corner of the work-area column it restores into, so it
119
+ never lands on an expanded rail. A collapsed rail's control floats the
120
+ same inset above the top edge of an expanded inline bottom drawer, which
121
+ spans the column reaching that corner once the rail collapses, so it never
122
+ covers the drawer's content; it returns to the corner when the drawer
123
+ collapses (the drawer publishes its edge as a CSS anchor scoped to its own
124
+ Workbench; without anchor positioning the control keeps the corner). An
125
+ overlay drawer publishes no edge: an open one covers the control, as every
126
+ open overlay does. Because it is anchored to the Workbench
127
+ rather than the viewport, an embedded Workbench never floats it over page
128
+ chrome outside it, and it scrolls with the Workbench. Compose it from the
129
+ package controls — a `single` `ToolbarControlGroup` around one icon button
130
+ carrying `collapsiblePanelToggleIcon(side, true)` — rather than a bare
131
+ button. To give it `FloatingToolbar`'s toolbar role and floating look, wrap
132
+ that group in a `FloatingToolbar` whose `position` matches the corner: the
133
+ corner owns the inset (it sets `--kui-floating-toolbar-inset` to 0), so the
134
+ toolbar floats from the corner instead of doubling it.
135
+
136
+ - `resizable: true | { min, max }` opts the panel in to drag and keyboard
137
+ resizing, driven by `wireWorkbench` (off by default).
68
138
 
69
139
  The same policy props are available on `ResizableRegion` and
70
140
  `CollapsiblePanel`, so a resizable application shell does not need to reach
71
141
  into `.kui-resizable-region__content`.
72
142
 
143
+ ## Panel toolbars
144
+
145
+ A panel's controls follow it open and closed when the Workbench composes the
146
+ toolbars. Give a panel a `toolbar` and the work area a `mainToolbar` (and, for
147
+ a drawer, optionally a `mainBottomToolbar`); the Workbench renders each as a
148
+ standard `Toolbar` over a `Pane`, and moves groups between them as panels open
149
+ and close. A panel's `toolbar` has four parts:
150
+
151
+ - `title` — the panel's `ToolbarText` (the quiet default size in a rail).
152
+ - `panelOnly` — groups that make sense only while the panel is open. They lead
153
+ its toolbar, after the title, and are unavailable while it is closed.
154
+ - `constant` — groups that stay available either way. They trail the panel's
155
+ toolbar while it is open and move to the work area's toolbar while it is
156
+ closed.
157
+ - `toggle: { action, name }` — the standard collapse toggle, which the
158
+ Workbench renders: the per-side panel glyph, `aria-controls` naming the
159
+ panel, `aria-expanded`, a "Show …"/"Hide …" label, and the `data-action`
160
+ the app handles. It is always the last group.
161
+
162
+ While a panel is closed, its `constant` groups and then its toggle go to:
163
+
164
+ | Panel | Where they go |
165
+ | ------------- | --------------------------------------------------------------------------------------------------------------- |
166
+ | Left rail | The leading edge of `mainToolbar`, before its `title` |
167
+ | Right rail | The trailing edge of `mainToolbar`, after its `trailing` groups, so the toggle is the toolbar's last group |
168
+ | Bottom drawer | The trailing edge of `mainBottomToolbar`; without one, a `FloatingToolbar` in the work area's bottom-end corner |
169
+
170
+ A rail with no `mainToolbar` floats its groups in its corner the same way, so
171
+ a closed panel can always be reopened. An open overlay panel carries its own
172
+ toggle, so it never depends on a control it covers. An app's own
173
+ `restoreControl` still wins over the floating fallback.
174
+
175
+ ```tsx
176
+ <Workbench
177
+ id="studio"
178
+ label="Studio"
179
+ mainToolbar={{ label: "Editor", title: <ToolbarText text="Editor" size="xlarge" /> }}
180
+ main={<Editor />}
181
+ leftRail={{
182
+ label: "Navigator",
183
+ toolbar: {
184
+ label: "Navigator",
185
+ title: <ToolbarText text="Navigator" />,
186
+ panelOnly: <NewFileGroup />,
187
+ constant: <SearchGroup />,
188
+ toggle: { action: "toggle-navigator", name: "navigator" },
189
+ },
190
+ content: <Files />,
191
+ collapsed: navCollapsed.value,
192
+ }}
193
+ />
194
+ ```
195
+
196
+ The work area's `Pane` can carry more fixed chrome: `mainHeader` renders under
197
+ `mainToolbar` (supporting copy) and `mainFooter` over `mainBottomToolbar` (a status
198
+ line or a resource toolbar), each divided from the scrolling `main`.
199
+ `mainHeaderPlacement` and `mainFooterPlacement` (`"fixed"` by default, or
200
+ `"scroll"`) let that header or footer chrome scroll away with `main` instead —
201
+ useful where large text would leave pinned chrome little room. `mainToolbar` also
202
+ takes the `Toolbar`'s `responsive` / `responsiveAt` policy; `responsive: "wrap"`
203
+ keeps a long title whole and wraps its actions below it.
204
+
205
+ Size a rail so its title and groups fit at its narrowest (a resizable rail's
206
+ `min`); a toolbar that cannot hold them drops the title rather than
207
+ truncating it. `constant` content renders in both places while the panel is
208
+ closed (the panel's copy is inert), so give it no `id`s. With `wireWorkbench`
209
+ given the panel's `collapsed` signal, focus follows the toggle: closing a
210
+ panel from its own toggle focuses the relocated toggle in the work area, and
211
+ opening it from there focuses the panel's own toggle. A panel's optional
212
+ `footer` renders below its content, for a sidebar's bottom toolbar. Without a
213
+ `toolbar` or `mainToolbar`, `content` and `main` render exactly as given.
214
+
215
+ ## Resizable panels
216
+
217
+ Resizing is **opt-in per panel**. Give a panel `resizable` — `true` for the
218
+ default limits (rails 180–480px, drawer 120–480px) or `{ min, max }` — and
219
+ render its `size` from an app-owned signal. The panel then owns a separator on
220
+ its inner edge (the left rail's end, the right rail's start, the drawer's top)
221
+ with the same focusable `role="separator"` contract as a `ResizableRegion`
222
+ handle. `wireWorkbench` drives it:
223
+
224
+ ```tsx
225
+ import { wireWorkbench } from "@kerfjs/ui/wire-workbench";
226
+
227
+ const navSize = signal(280);
228
+ const consoleSize = signal(200);
229
+
230
+ <Workbench
231
+ id="studio"
232
+ label="Studio"
233
+ main={<Editor />}
234
+ mainMinSize={320} // the default: the editor never drops below 320px wide
235
+ mainMinHeight={120} // the default: the drawer leaves it 120px of height
236
+ leftRail={{
237
+ content: <Navigator />,
238
+ label: "Navigator",
239
+ collapsed: navCollapsed.value,
240
+ size: navSize.value,
241
+ resizable: { min: 200, max: 420 },
242
+ responsiveOverlayAt: "narrow", // an overlay when the Workbench is 704px or narrower
243
+ }}
244
+ bottomDrawer={{
245
+ content: <Console />,
246
+ label: "Console",
247
+ size: consoleSize.value,
248
+ resizable: true,
249
+ }}
250
+ />;
251
+
252
+ // once, after mount:
253
+ const stop = wireWorkbench(root, {
254
+ id: "studio",
255
+ panels: {
256
+ leftRail: {
257
+ size: navSize,
258
+ storageKey: "studio.nav-width",
259
+ collapsed: navCollapsed, // a transient overlay: takes focus, closes on Escape / outside press
260
+ },
261
+ bottomDrawer: { size: consoleSize, storageKey: "studio.console-height" },
262
+ },
263
+ });
264
+ ```
265
+
266
+ - **Pointer:** dragging the separator resizes the panel live (the wire marks
267
+ it `data-resizing` and suppresses content motion) and commits the final
268
+ size to the signal on release.
269
+ - **Keyboard:** focus the separator; arrow keys resize by `step` (16px),
270
+ Shift+arrow by `largeStep` (64px), Home/End jump to `min`/`max`. The
271
+ separator's `aria-valuenow`/`aria-valuemin`/`aria-valuemax` report the size
272
+ and limits.
273
+ - **Limits:** every size — dragged, typed, restored from storage, or passed
274
+ by the app — is clamped to `min`/`max` when rendered.
275
+ - **Work-area minimum:** beside its inline rails, fixed or resizable, the work
276
+ area keeps at least `mainMinSize` px of width (a `Workbench` prop, default
277
+ 320; `0` turns it off), and above an inline bottom drawer at least
278
+ `mainMinHeight` px of height (default 120; `0` turns it off). A dragged or
279
+ keyed rail stops growing where the work area would drop below its width,
280
+ and the drawer where it would drop below its height, so wide rails or a tall
281
+ drawer cannot crowd out the editor; `aria-valuemax` reports that reachable
282
+ maximum. When the Workbench itself narrows, the rails shrink in proportion
283
+ to their sizes, and when it gets shorter the drawer shrinks (their content
284
+ follows the shown size) while the work area holds its minimum; a squeezed
285
+ panel can still be made smaller, never larger, and never below its own
286
+ `min`. Two fixed 280px rails in a 720px Workbench show 200px each rather
287
+ than leaving the editor 160px. Each minimum applies only to a Workbench with a
288
+ panel on its axis, and never exceeds the Workbench itself.
289
+ - **Squeezed below `min`:** when the Workbench is too narrow for a rail's
290
+ `min` beside the work-area minimum, the rail shows less than its `min` (a
291
+ 172px Navigator whose `min` is 180). Its separator then reports that shown
292
+ width in `aria-valuenow`, and `aria-valuemin`/`aria-valuemax` pin to the
293
+ same value: the WAI-ARIA window splitter's value is the separator's actual
294
+ position, and the range must contain it, so a screen reader hears the width
295
+ on screen rather than a configured limit the layout cannot honor — and a
296
+ range that does not move, because the separator cannot move a rail the
297
+ container holds. A pinned separator commits nothing: arrow keys, Home/End,
298
+ and drags leave the rail's remembered size (and its storage) alone, so a
299
+ rail remembered at 280px returns at 280px, with the configured range, once
300
+ there is room.
301
+ - **Small Workbenches:** inline panels have no shrink floor of their own, by
302
+ design; the work area's minimums win, so in a very small Workbench a rail or
303
+ the drawer can become a sliver. Do not add your own `min-width` /
304
+ `min-height` to hold a panel open. Give the panel
305
+ `responsiveOverlayAt: "narrow"` (704px of Workbench width or less) or
306
+ `"compact"` (448px or less) instead: below that breakpoint it presents as a
307
+ full-size overlay over the work area rather than squeezing beside it (see
308
+ the catalog's resizable and responsive-drawer Workbench examples).
309
+ - **Collapse:** `collapsed` never changes a size. A collapsed panel keeps its
310
+ size (its content slides out at that width), its separator leaves the tab
311
+ order, and expanding it returns it at the size it had.
312
+ - **Persistence:** with a `storageKey`, the size is loaded from `storage`
313
+ (default `localStorage`) at wire-up and saved on every change, like
314
+ `wireSidebar`'s collapsed state.
315
+ - **Overlays never resize:** an overlay or hidden panel — including a rail
316
+ whose `responsiveOverlayAt` breakpoint currently applies — hides its
317
+ separator and is never resizable, and it leaves the work-area minimum to the
318
+ rails still in flow. With `responsiveOverlayAt` no device check is needed;
319
+ otherwise pass `deviceClass` and resizing is suspended while `compact` is
320
+ true, where rails present as overlay drawers or are replaced.
321
+ - `wireWorkbench` drives only the panels it is given, matched by the
322
+ Workbench `id`, so it never double-drives a `ResizableRegion` (or another
323
+ Workbench) under the same root. `onResize({ panel, size, source })` reports
324
+ each committed resize.
325
+
326
+ ## Transient overlays
327
+
328
+ An overlay panel covers the work area, so it should open only when the user
329
+ asks for it and close as easily as it opened. Give `wireWorkbench` a panel's
330
+ app-owned `collapsed` signal (`size` is optional, so a panel need not be
331
+ resizable) and it treats the panel as a transient overlay whenever it
332
+ presents as one, mirroring `wireSidebar`'s compact overlay:
333
+
334
+ - **Collapsed on entering the breakpoint:** when a panel's
335
+ `responsiveOverlayAt` breakpoint begins to apply — at wire-up, when the
336
+ Workbench first renders, or when it narrows across the breakpoint — the
337
+ wiring remembers the panel's inline `collapsed` state and collapses it, so
338
+ nothing covers the work area until the user opens it. When the breakpoint
339
+ stops applying (and on disposal) the remembered inline state comes back.
340
+ These presentation changes skip the collapse motion.
341
+ - **Focus moves in, and Tab stays in:** a panel that opens while it presents
342
+ as an overlay takes focus on its first focusable control (typically the
343
+ close control in its header), because it covers the work area and the
344
+ control that opened it. While it is open, Tab and Shift+Tab cycle through
345
+ its controls and never reach the covered work area; Escape, its own close
346
+ control, or an outside press leaves it. This is the ARIA dialog pattern
347
+ `wireSidebar`'s compact overlay follows. A panel opening inline never moves
348
+ focus.
349
+ - **Escape** closes the open overlay panel that holds focus, else the most
350
+ recently opened one. An Escape another handler already handled
351
+ (`defaultPrevented`) is left alone.
352
+ - **Outside press:** a pointer press that starts and ends outside an open
353
+ overlay panel closes it. The app's own toggle still works: its click closes
354
+ the panel before the wiring looks, and the press that opens a panel never
355
+ closes it.
356
+ - **Focus:** when a panel closes with focus inside it — whatever closed it,
357
+ including the app's own control inside the panel — focus returns to the
358
+ control that had it when the panel opened (typically its toggle; one inside
359
+ another panel that has closed since is skipped), else to
360
+ the first focusable control in the panel's `restoreControl`, else to a
361
+ focusable control outside the panel whose `aria-controls` names the panel
362
+ (or an element inside it). The last is what a panel already open at
363
+ wire-up relies on, since nothing opened it: give the app's toggle
364
+ `aria-controls` naming the panel. A pointer press inside the panel that
365
+ drops focus to the page counts as focus inside: Safari never focuses a
366
+ clicked button, so pressing the panel's own Hide control blurs the focused
367
+ control before the click closes it, and focus still returns. Focus the user
368
+ has moved to another element since is left alone. Each panel's `id` derives from the
369
+ Workbench's — `<id>-left-rail`, `<id>-right-rail`, `<id>-bottom-drawer`:
370
+
371
+ ```tsx
372
+ <button
373
+ data-action="toggle-navigator"
374
+ aria-controls="editor-left-rail"
375
+ aria-expanded={String(!navCollapsed.value)}
376
+ aria-label={navCollapsed.value ? "Show navigator" : "Hide navigator"}
377
+ >
378
+ <LucideIcon {...collapsiblePanelToggleIcon("left", navCollapsed.value)} />
379
+ </button>
380
+ ```
381
+
382
+ An open overlay can cover the control that opened it: a narrow Workbench's
383
+ right-rail overlay spans the full height at the end edge, over the trailing
384
+ end of the work area's toolbar, where its toggle usually sits. Escape and an
385
+ outside press still close it, but neither is a visible, pointer-reachable
386
+ control. So give every panel that may present as an overlay its own close
387
+ control, as a `CollapsiblePanel` puts its collapse toggle in its own header:
388
+ render the panel content as a `Pane` whose header `Toolbar` holds the title
389
+ and a `single`, borderless `ToolbarControlGroup` around one icon button
390
+ carrying `collapsiblePanelToggleIcon(side, false)`. The work area's toggle
391
+ stays the always-visible way to show the panel. The catalog's resizable
392
+ example does this for both rails.
393
+
394
+ The drawer works the same way. The catalog's responsive overlay drawer
395
+ example is the whole pattern for a bottom drawer:
396
+
397
+ ```tsx
398
+ const outputCollapsed = signal(false); // open inline by default
399
+
400
+ <Workbench
401
+ id="editor"
402
+ label="Editor"
403
+ main={<Editor />} // its toolbar toggles `outputCollapsed`
404
+ bottomDrawer={{
405
+ label: "Output",
406
+ content: <OutputPane />, // a Pane whose header holds its close control
407
+ collapsed: outputCollapsed.value,
408
+ size: 180,
409
+ responsiveOverlayAt: "narrow",
410
+ }}
411
+ />;
412
+
413
+ wireWorkbench(root, {
414
+ id: "editor",
415
+ panels: { bottomDrawer: { collapsed: outputCollapsed } },
416
+ });
417
+ ```
418
+
419
+ Above 704px of Workbench width the drawer takes its own track below the
420
+ editor. At 704px or less it overlays the bottom of the full-height editor,
421
+ starts hidden, and closes from its own header, on Escape, or on a press
422
+ outside it; widening the Workbench again brings it back inline in the state
423
+ it had before.
424
+
425
+ Escape and outside-press dismissal apply to static `presentation: "overlay"`
426
+ panels too; entering and leaving the collapsed state around a breakpoint is
427
+ only for `responsiveOverlayAt`, because a static presentation is the app's
428
+ choice. Inline panels are never touched. Pass `dismissOverlays: false` to
429
+ leave every `collapsed` write, and all focus handling, to the app.
430
+
431
+ Overlays are exclusive by default, like `wireSidebar`'s compact overlays:
432
+ opening a panel while it presents as an overlay closes every other open
433
+ overlay panel, the bottom drawer included. At a narrow width two rails are
434
+ wider than the Workbench, so without this the right rail would cover the
435
+ navigator's own close control. Panels presenting inline are never closed by
436
+ it, so a wide Workbench keeps both rails open. Pass `exclusiveOverlays: false`
437
+ to let overlays stay open together; they then stack in the fixed order above,
438
+ and Tab stays in the one that holds focus (else the most recently opened
439
+ one).
440
+
441
+ Where the browser supports `overflow-clip-margin`, the 20px hit target
442
+ straddles the panel's separator line like a `ResizableRegion` handle;
443
+ elsewhere it sits just inside the panel's inner edge, because the panel clips
444
+ its sliding content.
445
+
73
446
  ## Resizable application-shell migration
74
447
 
75
448
  An application such as Hot Sheet can replace its shell descendant overrides
@@ -89,7 +462,13 @@ with state-derived props:
89
462
  collapseMotion="fade-slide"
90
463
  contentOverflow={createMenuOpen.value ? "visible" : "clip"}
91
464
  presentation={mobile.value ? "overlay" : "inline"}
92
- restoreControl={<button data-action="show-drawer">Show terminals</button>}
465
+ restoreControl={
466
+ <ToolbarControlGroup label="Terminals" single>
467
+ <button data-action="show-drawer" aria-label="Show terminals">
468
+ <LucideIcon {...collapsiblePanelToggleIcon("bottom", true)} />
469
+ </button>
470
+ </ToolbarControlGroup>
471
+ }
93
472
  >
94
473
  <TerminalDrawer />
95
474
  </ResizableRegion>
@@ -113,8 +492,9 @@ set `--kui-workbench-rail-width` and `--kui-workbench-drawer-height` on a
113
492
  Workbench instance. The supported composition classes are `.kui-workbench`,
114
493
  `.kui-workbench__rail`, `.kui-workbench__rail--left`,
115
494
  `.kui-workbench__rail--right`, `.kui-workbench__center`,
116
- `.kui-workbench__main`, `.kui-workbench__drawer`, and
117
- `.kui-workbench__panel-content`, and `.kui-workbench__restore`; these exact hooks are cataloged for tools that
495
+ `.kui-workbench__main`, `.kui-workbench__drawer`,
496
+ `.kui-workbench__panel-content`, `.kui-workbench__handle`,
497
+ `.kui-workbench__handle-icon`, and `.kui-workbench__restore`; these exact hooks are cataloged for tools that
118
498
  must classify public application selectors. Prefer the component props and two
119
499
  size tokens before selecting internal anatomy, and do not target its data
120
500
  attributes or descendant tags as styling contracts.
package/llms.txt CHANGED
@@ -80,8 +80,10 @@ the folder modules, and CSS only imports their owner stylesheets.
80
80
  [`kerf-ui-doctor`](./docs/ui-doctor.md). Its portable JSON report merges
81
81
  profile/catalog, TypeScript, Kerf ESLint, and static analyzer diagnostics;
82
82
  CSS-adjacent component props are checked from catalog `cssValueProps`, so
83
- prefer its canonical shorthand or typed helper over raw CSS strings;
84
- repair source-located errors until exit 0. Use full mode for release gates;
83
+ prefer its canonical shorthand or typed helper over raw CSS strings; a
84
+ `--wa-color-{tone}-fill-loud` override without a governing `on-loud` (same
85
+ or ancestor scope, possibly in a co-loaded stylesheet), or whose literal
86
+ pair measures below 4.5:1, is review finding `KUI-L018` (set both); repair source-located errors until exit 0. Use full mode for release gates;
85
87
  generated `dist`/`coverage`, dependency, cache, evidence, and nested-worktree
86
88
  directories are excluded consistently from every static stage.
87
89
  [`kerf-ui-analyze`](./docs/ui-analyzer.md) remains available when only static
@@ -185,10 +187,10 @@ the folder modules, and CSS only imports their owner stylesheets.
185
187
  `TokenSearchField`, Web Awesome controls — inside a `ToolbarControlGroup`. For an
186
188
  ordinary icon/action control inside the group use a **plain `<button>`** (the
187
189
  group styles it fully) — that is the default; reach for `wa-button` only when you
188
- need a Web Awesome feature, chiefly the `slot="trigger"` button of a `wa-dropdown`
189
- popup menu. A toolbar popup menu is a `single` `ToolbarControlGroup` around a
190
- `wa-dropdown` (its `slot="trigger"` `wa-button` is the toolbar button, kept under
191
- `data-morph-skip-children`). Let the group size the trigger from its icon,
190
+ need a Web Awesome feature. A toolbar popup menu is a `PopupMenu`
191
+ (`@kerfjs/ui/popup-menu`, registered once via `@kerfjs/ui/popup-menu/register`)
192
+ in a `single` `ToolbarControlGroup` with `nestedDropdown`; never hand-write
193
+ `wa-dropdown` markup for a menu. Let the group size the trigger from its icon,
192
194
  localized text, and caret; do not force a dropdown or button width. Compact
193
195
  mixed groups keep standard item padding and raised selected chrome without an
194
196
  internal separator; the selected item paints over the outer border. For an
@@ -199,7 +201,8 @@ the folder modules, and CSS only imports their owner stylesheets.
199
201
  optional icon in a `ToolbarControlGroup`, an extra-large `ToolbarText` directly
200
202
  in the leading zone, and actions in a trailing group. Supporting copy is
201
203
  app-owned content below. Do not create a private heading wrapper or restyle the
202
- toolbar.
204
+ toolbar. A narrow peripheral rail or drawer, where extra-large would truncate
205
+ at its minimum size, names itself with the default `ToolbarText` size.
203
206
  - **Use `Text` for ordinary semantic headings, paragraphs, and inline secondary
204
207
  text outside toolbar title zones.** Omit `variant` for the default `p`, pass
205
208
  `h1`–`h6` based on the document outline, or use `variant="span"` inside a row,
@@ -246,7 +249,8 @@ the folder modules, and CSS only imports their owner stylesheets.
246
249
  editor focus before the next input task, so shortcut-letter typing stays in search.
247
250
  Select All deletion does the same; deletion
248
251
  restoration finishes before the next keystroke rather than on a delayed frame.
249
- Persist both query and remaining tokens read from an edited field. The one richer case is `wireTokenSearchFields`, which manages the
252
+ Persist both query and remaining tokens read from an edited field; change
253
+ `revision` to replace its DOM-owned text programmatically. The one richer case is `wireTokenSearchFields`, which manages the
250
254
  collapsible field's expand/collapse/focus by default (on, per-behavior opt-out) and
251
255
  exposes the `expanded` signal on its handle — bind it in render or hand in your own
252
256
  via `collapsible.signals`. Do not hand-roll open/collapse/focus that helper
@@ -322,6 +326,7 @@ shell, `@kerfjs/ui/collapsible-panel` (`CollapsiblePanel`, `CollapsiblePanelTogg
322
326
  - [component-contract.md](./docs/component-contract.md): what a component owns vs. what the app owns.
323
327
  - [accessibility.md](./docs/accessibility.md): keyboard and ARIA contracts per component.
324
328
  - [recipes.md](./docs/recipes.md): production compositions (app shell, list-detail dialog, composer form, …) as copyable references.
329
+ - [workbench.md](./docs/workbench.md): the `Workbench` multi-panel shell, its collapse policies, and opt-in `resizable` panels driven by `wireWorkbench` (`@kerfjs/ui/wire-workbench`: drag, keyboard, min/max, persistence, compact suspension), the `mainMinSize` / `mainMinHeight` work-area minimums (default 320px wide beside any inline rail, 120px tall above the drawer) that resizable panels stop at and inline panels shrink around, and a panel's `responsiveOverlayAt` (`narrow` / `compact`, rails and the bottom drawer) that presents it as an overlay below a Workbench container breakpoint without a device-class check; a panel given its `collapsed` signal is a transient overlay (collapsed on entering the breakpoint, takes focus and keeps Tab inside while open, closed by Escape or an outside press, focus returned; `dismissOverlays`), and overlays are exclusive by default (opening one closes the others; `exclusiveOverlays: false` opts out).
325
330
  - [app-layouts.md](./docs/app-layouts.md): choosing `NavStack`/`SplitView`/`Workbench`/`TabScaffold` by data + interaction + device, and dialog presentation, plus the safe-area model (surfaces paint through unsafe areas; content pads only the edges it reaches; `Pane.safeAreaEdges` for app-owned arrangements, `Toolbar.safeAreaEdges` for an app bar or bottom bar at a screen edge).
326
331
  - [webawesome-theme.md](./docs/webawesome-theme.md): the optional Web Awesome theme; catalog coverage means themed-and-supported, not preferred (prefer the Kerf primitive when one exists). The UX sidebar visibly tags superseded and exceptional Web Awesome choices as `Discouraged`; Popup remains an available conditional positioning primitive.
327
332
  - [component-catalog.json](./ai/component-catalog.json) + [component-catalog-extension.schema.json](./ai/component-catalog-extension.schema.json) + [catalog-authoring.json](./ai/catalog-authoring.json) + [public-api-signatures-v1.md](./ai/public-api-signatures-v1.md) + [webawesome-jsx-signatures-v1.md](./ai/webawesome-jsx-signatures-v1.md): machine-readable selection facts, a reusable app-component extension contract, Catalog authoring discovery, and exact signatures.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.53",
3
+ "version": "5.0.0-beta.55",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -37,7 +37,8 @@
37
37
  "sideEffects": [
38
38
  "**/*.css",
39
39
  "./dist/browser/*.js",
40
- "./dist/select-register.js"
40
+ "./dist/select-register.js",
41
+ "./dist/popup-menu-register.js"
41
42
  ],
42
43
  "exports": {
43
44
  ".": {
@@ -230,6 +231,10 @@
230
231
  "types": "./dist/wire-tab-scaffold.d.ts",
231
232
  "import": "./dist/wire-tab-scaffold.js"
232
233
  },
234
+ "./wire-workbench": {
235
+ "types": "./dist/wire-workbench.d.ts",
236
+ "import": "./dist/wire-workbench.js"
237
+ },
233
238
  "./loading-spinner": {
234
239
  "types": "./dist/loading-spinner.d.ts",
235
240
  "browser": "./dist/browser/loading-spinner.js",
@@ -264,6 +269,15 @@
264
269
  "types": "./dist/wire-token-search-fields.d.ts",
265
270
  "import": "./dist/wire-token-search-fields.js"
266
271
  },
272
+ "./popup-menu": {
273
+ "types": "./dist/popup-menu.d.ts",
274
+ "browser": "./dist/browser/popup-menu.js",
275
+ "import": "./dist/popup-menu.js"
276
+ },
277
+ "./popup-menu/register": {
278
+ "types": "./dist/popup-menu-register.d.ts",
279
+ "import": "./dist/popup-menu-register.js"
280
+ },
267
281
  "./select/register": {
268
282
  "types": "./dist/select-register.d.ts",
269
283
  "import": "./dist/select-register.js"
@@ -326,6 +340,7 @@
326
340
  "./tab-scaffold.css": "./dist/styles/tab-scaffold.css",
327
341
  "./loading-spinner.css": "./dist/styles/loading-spinner.css",
328
342
  "./select.css": "./dist/styles/select.css",
343
+ "./popup-menu.css": "./dist/styles/popup-menu.css",
329
344
  "./skeleton.css": "./dist/styles/skeleton.css",
330
345
  "./sunken-panel.css": "./dist/styles/sunken-panel.css",
331
346
  "./segmented-control.css": "./dist/styles/segmented-control.css",