@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
@@ -55,25 +55,26 @@ policy, or analysis inputs to the containing application.
55
55
 
56
56
  ## Diagnostics and exit behavior
57
57
 
58
- | Rule | Kind | Meaning |
59
- | ---------- | ------ | ----------------------------------------------------------------- |
60
- | `KUI-L001` | error | A stylesheet reaches into a private or unknown `.kui-*` selector. |
61
- | `KUI-L002` | error | A stylesheet references an unknown or private `--kui-*` token. |
62
- | `KUI-L003` | error | One element combines public classes that claim the same geometry. |
63
- | `KUI-L004` | review | Nested literal classes both add a content inset. |
64
- | `KUI-L005` | review | Consumer CSS forces a public component's dimensions. |
65
- | `KUI-L006` | review | Literal spacing falls outside Kerf's approved scale. |
66
- | `KUI-L007` | error | A declared scroll owner is nested inside another scroll owner. |
67
- | `KUI-L008` | review | A dynamic class expression cannot be classified soundly. |
68
- | `KUI-L009` | error | A stylesheet cannot be parsed. |
69
- | `KUI-L010` | error | A selector reaches a private Kerf descendant from a public root. |
70
- | `KUI-L011` | error | A `::part()` target is not cataloged as a public extension point. |
71
- | `KUI-L012` | error | CSS assigns a Kerf token without a public configuration contract. |
72
- | `KUI-L013` | error | A CSS-adjacent prop uses an unknown shorthand or raw literal. |
73
- | `KUI-L014` | error | A typed helper produces the wrong property grammar. |
74
- | `KUI-L015` | error | An expression-only helper is passed without a composer. |
75
- | `KUI-L016` | error | A removed declaration-list escape hatch is used. |
76
- | `KUI-L017` | review | A valid but exceptional off-scale shorthand needs justification. |
58
+ | Rule | Kind | Meaning |
59
+ | ---------- | ------ | ------------------------------------------------------------------------------------- |
60
+ | `KUI-L001` | error | A stylesheet reaches into a private or unknown `.kui-*` selector. |
61
+ | `KUI-L002` | error | A stylesheet references an unknown or private `--kui-*` token. |
62
+ | `KUI-L003` | error | One element combines public classes that claim the same geometry. |
63
+ | `KUI-L004` | review | Nested literal classes both add a content inset. |
64
+ | `KUI-L005` | review | Consumer CSS forces a public component's dimensions. |
65
+ | `KUI-L006` | review | Literal spacing falls outside Kerf's approved scale. |
66
+ | `KUI-L007` | error | A declared scroll owner is nested inside another scroll owner. |
67
+ | `KUI-L008` | review | A dynamic class expression cannot be classified soundly. |
68
+ | `KUI-L009` | error | A stylesheet cannot be parsed. |
69
+ | `KUI-L010` | error | A selector reaches a private Kerf descendant from a public root. |
70
+ | `KUI-L011` | error | A `::part()` target is not cataloged as a public extension point. |
71
+ | `KUI-L012` | error | CSS assigns a Kerf token without a public configuration contract. |
72
+ | `KUI-L013` | error | A CSS-adjacent prop uses an unknown shorthand or raw literal. |
73
+ | `KUI-L014` | error | A typed helper produces the wrong property grammar. |
74
+ | `KUI-L015` | error | An expression-only helper is passed without a composer. |
75
+ | `KUI-L016` | error | A removed declaration-list escape hatch is used. |
76
+ | `KUI-L017` | review | A valid but exceptional off-scale shorthand needs justification. |
77
+ | `KUI-L018` | review | A loud fill override lacks a governing `on-loud`, or the literal pair is below 4.5:1. |
77
78
 
78
79
  Errors are provable contract violations and make the command exit 1. Review
79
80
  findings are deliberately heuristic and do not fail by default; pass
@@ -102,6 +103,80 @@ uses that same metadata for direct component calls and JSX, including nested
102
103
  paths such as `choices[].color`; consumer catalogs receive identical checks.
103
104
  Dynamic values remain a type-system responsibility rather than being guessed.
104
105
 
106
+ ## Loud fill / on-loud pairing (`KUI-L018`)
107
+
108
+ The shipped Web Awesome theme pairs every `--wa-color-{tone}-fill-loud` with a
109
+ `--wa-color-{tone}-on-loud` foreground that clears WCAG AA (4.5:1), for the
110
+ `neutral`, `brand`, `success`, `warning`, `danger`, and `pop` tones. A
111
+ stylesheet that overrides a loud fill without also setting that tone's
112
+ `on-loud` keeps the theme's foreground on an unknown fill, so the guarantee no
113
+ longer holds. `KUI-L018` reports such a fill and names each scope that lacks a
114
+ pair, and it reports a pair that is present but measurably fails.
115
+
116
+ **Which `on-loud` governs a fill.** Each individual selector of the fill
117
+ (after resolving CSS nesting and splitting selector lists) inside its chain of
118
+ at-rules is one scope. An `on-loud` of the same tone governs it when declared:
119
+
120
+ - in the same scope, in the same or a separate rule, in either order; or
121
+ - in an ancestor scope: a document-root selector (`:root`, `html`, `:host`,
122
+ `*`), or the leading part of a descendant or child chain (`.shell` governs
123
+ `.shell .panel` and `.frame > .tile`, but not `.shell-alt .panel` or
124
+ `.shell.dense`); and
125
+ - under no at-rule, or the same at-rule chain (an unconditional `on-loud`
126
+ applies inside `@media`, but one under `@media` or `@supports` is not proven
127
+ to apply to an unconditional fill).
128
+
129
+ That `on-loud` may live in the same stylesheet or in another one the same
130
+ entry loads: a source module that imports both stylesheets, or a stylesheet
131
+ that reaches both through `@import`. A stylesheet no entry loads with the fill
132
+ does not count. When several candidates apply, the same scope wins (this
133
+ stylesheet first), then the nearest ancestor.
134
+
135
+ **Contrast, when it can be proven.** When the fill and its governing `on-loud`
136
+ are both literal opaque colors (hex, `rgb()`/`rgba()`, `hsl()`/`hsla()`,
137
+ `white`/`black`, or a `light-dark()` of two of them), the analyzer computes the
138
+ WCAG contrast ratio in each color scheme and reports a pair below 4.5:1, with
139
+ the ratio and scheme in its evidence. An unpaired literal fill is accepted when
140
+ it clears 4.5:1 in both schemes against the theme's default `on-loud` for its
141
+ tone (`light-dark(#fff, #111113)` for `brand` and `danger`, `#1d1d1f` for
142
+ `success` and `warning`); `neutral` follows a surface token and `pop` has no
143
+ Web Awesome default, so their unpaired fills are always reported. Values it
144
+ cannot prove (`var()`, `color-mix()`, translucent colors, keywords) are not
145
+ measured: a present pair is accepted, and an unpaired fill is reported.
146
+
147
+ ```css
148
+ /* KUI-L018: brand fill moves, its foreground does not. */
149
+ .billing {
150
+ --wa-color-brand-fill-loud: #7540a8;
151
+ }
152
+
153
+ /* Paired: both in the same scope. */
154
+ .billing {
155
+ --wa-color-brand-fill-loud: #7540a8;
156
+ --wa-color-brand-on-loud: #ffffff;
157
+ }
158
+
159
+ /* Paired through an ancestor scope (6.87:1). */
160
+ :root {
161
+ --wa-color-brand-on-loud: #ffffff;
162
+ }
163
+ .billing {
164
+ --wa-color-brand-fill-loud: #7540a8;
165
+ }
166
+
167
+ /* KUI-L018: present, but white on this fill measures 2.14:1. */
168
+ .billing {
169
+ --wa-color-brand-fill-loud: #7fb3ff;
170
+ --wa-color-brand-on-loud: #ffffff;
171
+ }
172
+ ```
173
+
174
+ It is a review finding rather than an error because the analyzer cannot see
175
+ every cascade: an `on-loud` set inline, by script, or by a stylesheet it does
176
+ not analyze may still govern the fill. Set the `on-loud` explicitly beside the
177
+ fill, which also documents the pair; use `--fail-on-review` to gate it in CI.
178
+ See [the Web Awesome theme customization notes](./webawesome-theme.md#customization).
179
+
105
180
  The JSON report schema is exported as
106
181
  `@kerfjs/ui/analyzer/report.schema.json`.
107
182
 
package/docs/ui-doctor.md CHANGED
@@ -24,6 +24,12 @@ exceptional but valid spacing choice in the review queue. The ESLint and
24
24
  analyzer stages share these ids, so the normalized report can be consumed as
25
25
  one repair loop without tool-specific translations.
26
26
 
27
+ The analyzer stage also reads consumer stylesheets for theme overrides:
28
+ `KUI-L018` is a review finding for a `--wa-color-{tone}-fill-loud` override
29
+ that lacks a governing `--wa-color-{tone}-on-loud` (in the same or an ancestor
30
+ scope, in any stylesheet the same entry loads), or whose literal pair measures
31
+ below 4.5:1 (see [the analyzer rule](./ui-analyzer.md#loud-fill--on-loud-pairing-kui-l018)).
32
+
27
33
  The browser evaluator is different: it runs the application and is disabled by default. It only runs when configuration supplies `browser.url` or the command receives `--browser-url`. Start and authorize the target application separately.
28
34
 
29
35
  An unavailable or failed stage does not prevent independent stages from reporting. Its final exit is still a configuration failure, so a partial run cannot appear clean.
package/docs/ux-demo.md CHANGED
@@ -18,7 +18,7 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
18
18
 
19
19
  - It imports public component subpaths, exercising their browser-selected reachable CSS rather than a catalog-only style path.
20
20
  - 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.
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 (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
22
  - 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
23
  - `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
24
  - 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.
@@ -31,7 +31,7 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
31
31
  - 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`.
32
32
  - A detail with relationships renders one content-fitted `Components` footer selector: `Uses` links to direct dependencies and `Used by` links to reverse dependents. Its popup owns the standard surface inset so headings and even long component names stay clear of the menu border. Choosing either navigates to that component's demo. The relationship group is absent when both sets are empty; no duplicate summary is rendered.
33
33
  - Focused demos ship no local stylesheets, inline styles, or styling-only class names. They compose Kerf components first, Web Awesome surfaces where Kerf has no equivalent, and ordinary semantic HTML for the remainder. The catalog-owned `CatalogExample.viewport` contract supplies the few specimen constraints needed to demonstrate layout behavior. Recipes follow the same rule: they ship no stylesheets, inline styles, or styling-only classes, and the catalog frames each one in the `CatalogExample` viewport its exported `presentation` declares (full-width application arrangements span the canvas measure; narrower recipes sit in the centered example column). Demo-only CSS is limited to the document-shell `style.css`, which carries no recipe rules. Decorative chrome and Web Awesome controls share the same semantic system palette instead of carrying a separate demo identity.
34
- - Scenarios use deterministic data and cover real variants, long content, disabled state, selection, progress, feedback, overlays, media, helpers, and stateful interactions. Individual Web Awesome action, form, layout, navigation, feedback, media, and helper routes provide focused theme specimens without a redundant aggregate route; ListItem and ListHeader demonstrate typed domain metadata, protected native button/disclosure semantics, and a native popover trigger relationship without product props; the Headers composition demonstrates direct toolbar headings with an extra-large title, optional supporting copy, and grouped icon/actions; the Select route verifies that option icons survive unrelated Kerf rerenders and selected slot content follows value changes; the Animation route exposes preset, easing, duration, playback-rate, and transport controls with live lifecycle status; observer routes provide user-driven intersection, mutation, and resize events with live results; ToolbarControlGroup exposes all eight Hot Sheet 2 variants, loads the Web Awesome registrations needed by its popup specimen, and preserves the upgraded dropdown's managed light-DOM children across Kerf rerenders; the Toolbar route composes a collapsible TokenSearchField inside a group, animates between the empty iconic and complete field states, stays open while populated, and focuses the revealed editor; SegmentedControl covers toolbar, rounded-rectangle, pill, equal-width, and disabled presentations; TokenSearchField covers editable ordered chips, pointer and keyboard removal, caret-preserving controlled deletion, editing, clearing, free text, disabled state, and fixed first-line alignment while content wraps; StateBanner exposes every built-in tone with tone-tinted badges plus a scoped override; the Application tabs composition uses a Web Awesome card with semantic header/footer slots and Kerf `Text` while demonstrating overflow, edge autoscroll, and controlled pointer/keyboard reordering.
34
+ - Scenarios use deterministic data and cover real variants, long content, disabled state, selection, progress, feedback, overlays, media, helpers, and stateful interactions. Individual Web Awesome action, form, layout, navigation, feedback, media, and helper routes provide focused theme specimens without a redundant aggregate route; ListItem and ListHeader demonstrate typed domain metadata, protected native button/disclosure semantics, and a native popover trigger relationship without product props; the Headers composition demonstrates direct toolbar headings with an extra-large title, optional supporting copy, and grouped icon/actions; the Select route verifies that option icons survive unrelated Kerf rerenders and selected slot content follows value changes; the Animation route exposes preset, easing, duration, playback-rate, and transport controls with live lifecycle status; observer routes provide user-driven intersection, mutation, and resize events with live results; ToolbarControlGroup exposes all eight Hot Sheet 2 variants, loads the Web Awesome registrations needed by its popup specimen, and preserves the upgraded dropdown's managed light-DOM children across Kerf rerenders; the Toolbar route composes a collapsible TokenSearchField inside a group, animates between the empty iconic and complete field states, stays open while populated, and focuses the revealed editor; SegmentedControl covers toolbar, rounded-rectangle, pill, equal-width, and disabled presentations; TokenSearchField covers editable ordered chips, pointer and keyboard removal, caret-preserving controlled deletion, editing, clearing, free text, disabled state, and fixed first-line alignment while content wraps; StateBanner exposes every built-in tone with tone-tinted badges, a scoped override, and a placeholder with and without a badge, the badged placeholder in every tone so captures cover the solid badge skeleton; the Application tabs composition uses a Web Awesome card with semantic header/footer slots and Kerf `Text` while demonstrating overflow, edge autoscroll, and controlled pointer/keyboard reordering.
35
35
  - The ToolbarControlGroup route also demonstrates the public compact/tight,
36
36
  mixed-content, filled-selection, nested-dropdown, and avatar/scrim contracts
37
37
  added for consumers that previously restyled group descendants. The compact
@@ -143,8 +143,10 @@ short status or count, not a content item. A badge with `appearance="filled"` or
143
143
  `appearance="filled-outlined"` uses its variant's quiet fill with normal text by
144
144
  default so every shipped semantic variant meets WCAG AA for that small text;
145
145
  filled-outlined keeps its normal variant border. Accent badges keep their loud
146
- fills, with brand, success, and danger using a dark foreground that clears AA in
147
- both themes (neutral and warning retain their already-safe variant foregrounds).
146
+ fills and their variant's `on-loud` foreground, which the theme sets to clear AA
147
+ on that fill in both themes: white on the light brand (`#0075db`) and danger
148
+ (`#eb0005`) fills, dark text on the success and warning fills and on the pale
149
+ dark-mode brand and danger fills.
148
150
  Override
149
151
  `--kui-wa-badge-filled-background` and `--kui-wa-badge-filled-foreground` on a
150
152
  badge or containing scope when a product needs a different contrast-safe pair,
@@ -290,15 +292,29 @@ smallest subtree that needs a different identity:
290
292
  ```css
291
293
  :root {
292
294
  --wa-color-brand-fill-loud: #7540a8;
295
+ --wa-color-brand-on-loud: #ffffff;
293
296
  --wa-color-focus: #7540a8;
294
297
  --wa-form-control-border-radius: 0.5rem;
295
298
  }
296
299
 
297
300
  .billing-workspace {
298
301
  --wa-color-success-fill-loud: #10a86b;
302
+ --wa-color-success-on-loud: #1d1d1f;
299
303
  }
300
304
  ```
301
305
 
306
+ The shipped theme pairs every `--wa-color-{variant}-fill-loud` with an
307
+ `--wa-color-{variant}-on-loud` that clears WCAG AA (4.5:1) in both color
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.
317
+
302
318
  Use `.wa-light`, `.wa-dark`, and `.wa-invert` for explicit appearance scopes.
303
319
  Component-specific custom properties and documented `::part()` selectors from
304
320
  Web Awesome remain available when a semantic theme token is not sufficiently
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);
@@ -61,15 +76,288 @@ an optional `label`. Common shell behavior is configured rather than restyled:
61
76
  - `contentOverflow: "clip" | "auto" | "visible"` lets a drawer temporarily
62
77
  expose an open popup without a descendant override;
63
78
  - `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;
79
+ or a responsive replacement. An overlay rail spans the Workbench height at
80
+ its side and an overlay drawer spans the work-area column at the bottom,
81
+ each exactly at its configured size (its separator border included, as in
82
+ flow) up to the viewport-relative overlay maximum; the content fills the
83
+ panel inside its border, so it is clamped with it;
84
+ - `responsiveOverlayAt: "narrow" | "compact"` presents a rail or the bottom
85
+ drawer as an overlay below a Workbench container breakpoint — 704px or less
86
+ for `narrow`, 448px or less for `compact`, the breakpoints of
87
+ `ResizableRegion`'s `responsiveFillAt` — and inline above it. The CSS
88
+ decides, so the app needs no `deviceClass` check for presentation; a
89
+ collapsed overlay drops its surface and shadow so nothing covers the work
90
+ area, and the work area keeps the safe-area inset of the edge the overlay
91
+ covers. When overlays meet, the order is fixed rather than by which opened
92
+ last: both rails stack above the drawer, as inline rails span the
93
+ Workbench's full height beside the drawer's column, and the right rail
94
+ stacks above the left. Rails take `--kui-workbench-overlay-z` (41) and the
95
+ drawer one less. Restore controls take `--kui-workbench-restore-z`, which
96
+ defaults to two less than the overlay z-index (39), so they sit beneath
97
+ every overlay: an open overlay covers another panel's restore control as
98
+ it covers the rest of the work area, and a collapsed overlay, which drops
99
+ its pointer events, leaves the control beneath it usable;
66
100
  - `restoreControl` places an application-owned restore affordance in a
67
- safe-area-aware viewport corner (`restorePosition` chooses the corner).
101
+ corner of the Workbench itself while the panel is collapsed
102
+ (`restorePosition` chooses the corner), inset by
103
+ `--kui-workbench-restore-inset` (16px) plus the unsafe area of each edge the
104
+ corner reaches. A rail's control sits in the Workbench's corner; the
105
+ drawer's sits in the corner of the work-area column it restores into, so it
106
+ never lands on an expanded rail. A collapsed rail's control floats the
107
+ same inset above the top edge of an expanded inline bottom drawer, which
108
+ spans the column reaching that corner once the rail collapses, so it never
109
+ covers the drawer's content; it returns to the corner when the drawer
110
+ collapses (the drawer publishes its edge as a CSS anchor scoped to its own
111
+ Workbench; without anchor positioning the control keeps the corner). An
112
+ overlay drawer publishes no edge: an open one covers the control, as every
113
+ open overlay does. Because it is anchored to the Workbench
114
+ rather than the viewport, an embedded Workbench never floats it over page
115
+ chrome outside it, and it scrolls with the Workbench. Compose it from the
116
+ package controls — a `single` `ToolbarControlGroup` around one icon button
117
+ carrying `collapsiblePanelToggleIcon(side, true)` — rather than a bare
118
+ button. To give it `FloatingToolbar`'s toolbar role and floating look, wrap
119
+ that group in a `FloatingToolbar` whose `position` matches the corner: the
120
+ corner owns the inset (it sets `--kui-floating-toolbar-inset` to 0), so the
121
+ toolbar floats from the corner instead of doubling it.
122
+
123
+ - `resizable: true | { min, max }` opts the panel in to drag and keyboard
124
+ resizing, driven by `wireWorkbench` (off by default).
68
125
 
69
126
  The same policy props are available on `ResizableRegion` and
70
127
  `CollapsiblePanel`, so a resizable application shell does not need to reach
71
128
  into `.kui-resizable-region__content`.
72
129
 
130
+ ## Resizable panels
131
+
132
+ Resizing is **opt-in per panel**. Give a panel `resizable` — `true` for the
133
+ default limits (rails 180–480px, drawer 120–480px) or `{ min, max }` — and
134
+ render its `size` from an app-owned signal. The panel then owns a separator on
135
+ its inner edge (the left rail's end, the right rail's start, the drawer's top)
136
+ with the same focusable `role="separator"` contract as a `ResizableRegion`
137
+ handle. `wireWorkbench` drives it:
138
+
139
+ ```tsx
140
+ import { wireWorkbench } from "@kerfjs/ui/wire-workbench";
141
+
142
+ const navSize = signal(280);
143
+ const consoleSize = signal(200);
144
+
145
+ <Workbench
146
+ id="studio"
147
+ label="Studio"
148
+ main={<Editor />}
149
+ mainMinSize={320} // the default: the editor never drops below 320px wide
150
+ mainMinHeight={120} // the default: the drawer leaves it 120px of height
151
+ leftRail={{
152
+ content: <Navigator />,
153
+ label: "Navigator",
154
+ collapsed: navCollapsed.value,
155
+ size: navSize.value,
156
+ resizable: { min: 200, max: 420 },
157
+ responsiveOverlayAt: "narrow", // an overlay when the Workbench is 704px or narrower
158
+ }}
159
+ bottomDrawer={{
160
+ content: <Console />,
161
+ label: "Console",
162
+ size: consoleSize.value,
163
+ resizable: true,
164
+ }}
165
+ />;
166
+
167
+ // once, after mount:
168
+ const stop = wireWorkbench(root, {
169
+ id: "studio",
170
+ panels: {
171
+ leftRail: {
172
+ size: navSize,
173
+ storageKey: "studio.nav-width",
174
+ collapsed: navCollapsed, // a transient overlay: takes focus, closes on Escape / outside press
175
+ },
176
+ bottomDrawer: { size: consoleSize, storageKey: "studio.console-height" },
177
+ },
178
+ });
179
+ ```
180
+
181
+ - **Pointer:** dragging the separator resizes the panel live (the wire marks
182
+ it `data-resizing` and suppresses content motion) and commits the final
183
+ size to the signal on release.
184
+ - **Keyboard:** focus the separator; arrow keys resize by `step` (16px),
185
+ Shift+arrow by `largeStep` (64px), Home/End jump to `min`/`max`. The
186
+ separator's `aria-valuenow`/`aria-valuemin`/`aria-valuemax` report the size
187
+ and limits.
188
+ - **Limits:** every size — dragged, typed, restored from storage, or passed
189
+ by the app — is clamped to `min`/`max` when rendered.
190
+ - **Work-area minimum:** beside its inline rails, fixed or resizable, the work
191
+ area keeps at least `mainMinSize` px of width (a `Workbench` prop, default
192
+ 320; `0` turns it off), and above an inline bottom drawer at least
193
+ `mainMinHeight` px of height (default 120; `0` turns it off). A dragged or
194
+ keyed rail stops growing where the work area would drop below its width,
195
+ and the drawer where it would drop below its height, so wide rails or a tall
196
+ drawer cannot crowd out the editor; `aria-valuemax` reports that reachable
197
+ maximum. When the Workbench itself narrows, the rails shrink in proportion
198
+ to their sizes, and when it gets shorter the drawer shrinks (their content
199
+ follows the shown size) while the work area holds its minimum; a squeezed
200
+ panel can still be made smaller, never larger, and never below its own
201
+ `min`. Two fixed 280px rails in a 720px Workbench show 200px each rather
202
+ than leaving the editor 160px. Each minimum applies only to a Workbench with a
203
+ panel on its axis, and never exceeds the Workbench itself.
204
+ - **Squeezed below `min`:** when the Workbench is too narrow for a rail's
205
+ `min` beside the work-area minimum, the rail shows less than its `min` (a
206
+ 172px Navigator whose `min` is 180). Its separator then reports that shown
207
+ width in `aria-valuenow`, and `aria-valuemin`/`aria-valuemax` pin to the
208
+ same value: the WAI-ARIA window splitter's value is the separator's actual
209
+ position, and the range must contain it, so a screen reader hears the width
210
+ on screen rather than a configured limit the layout cannot honor — and a
211
+ range that does not move, because the separator cannot move a rail the
212
+ container holds. A pinned separator commits nothing: arrow keys, Home/End,
213
+ and drags leave the rail's remembered size (and its storage) alone, so a
214
+ rail remembered at 280px returns at 280px, with the configured range, once
215
+ there is room.
216
+ - **Small Workbenches:** inline panels have no shrink floor of their own, by
217
+ design; the work area's minimums win, so in a very small Workbench a rail or
218
+ the drawer can become a sliver. Do not add your own `min-width` /
219
+ `min-height` to hold a panel open. Give the panel
220
+ `responsiveOverlayAt: "narrow"` (704px of Workbench width or less) or
221
+ `"compact"` (448px or less) instead: below that breakpoint it presents as a
222
+ full-size overlay over the work area rather than squeezing beside it (see
223
+ the catalog's resizable and responsive-drawer Workbench examples).
224
+ - **Collapse:** `collapsed` never changes a size. A collapsed panel keeps its
225
+ size (its content slides out at that width), its separator leaves the tab
226
+ order, and expanding it returns it at the size it had.
227
+ - **Persistence:** with a `storageKey`, the size is loaded from `storage`
228
+ (default `localStorage`) at wire-up and saved on every change, like
229
+ `wireSidebar`'s collapsed state.
230
+ - **Overlays never resize:** an overlay or hidden panel — including a rail
231
+ whose `responsiveOverlayAt` breakpoint currently applies — hides its
232
+ separator and is never resizable, and it leaves the work-area minimum to the
233
+ rails still in flow. With `responsiveOverlayAt` no device check is needed;
234
+ otherwise pass `deviceClass` and resizing is suspended while `compact` is
235
+ true, where rails present as overlay drawers or are replaced.
236
+ - `wireWorkbench` drives only the panels it is given, matched by the
237
+ Workbench `id`, so it never double-drives a `ResizableRegion` (or another
238
+ Workbench) under the same root. `onResize({ panel, size, source })` reports
239
+ each committed resize.
240
+
241
+ ## Transient overlays
242
+
243
+ An overlay panel covers the work area, so it should open only when the user
244
+ asks for it and close as easily as it opened. Give `wireWorkbench` a panel's
245
+ app-owned `collapsed` signal (`size` is optional, so a panel need not be
246
+ resizable) and it treats the panel as a transient overlay whenever it
247
+ presents as one, mirroring `wireSidebar`'s compact overlay:
248
+
249
+ - **Collapsed on entering the breakpoint:** when a panel's
250
+ `responsiveOverlayAt` breakpoint begins to apply — at wire-up, when the
251
+ Workbench first renders, or when it narrows across the breakpoint — the
252
+ wiring remembers the panel's inline `collapsed` state and collapses it, so
253
+ nothing covers the work area until the user opens it. When the breakpoint
254
+ stops applying (and on disposal) the remembered inline state comes back.
255
+ These presentation changes skip the collapse motion.
256
+ - **Focus moves in, and Tab stays in:** a panel that opens while it presents
257
+ as an overlay takes focus on its first focusable control (typically the
258
+ close control in its header), because it covers the work area and the
259
+ control that opened it. While it is open, Tab and Shift+Tab cycle through
260
+ its controls and never reach the covered work area; Escape, its own close
261
+ control, or an outside press leaves it. This is the ARIA dialog pattern
262
+ `wireSidebar`'s compact overlay follows. A panel opening inline never moves
263
+ focus.
264
+ - **Escape** closes the open overlay panel that holds focus, else the most
265
+ recently opened one. An Escape another handler already handled
266
+ (`defaultPrevented`) is left alone.
267
+ - **Outside press:** a pointer press that starts and ends outside an open
268
+ overlay panel closes it. The app's own toggle still works: its click closes
269
+ the panel before the wiring looks, and the press that opens a panel never
270
+ closes it.
271
+ - **Focus:** when a panel closes with focus inside it — whatever closed it,
272
+ including the app's own control inside the panel — focus returns to the
273
+ control that had it when the panel opened (typically its toggle; one inside
274
+ another panel that has closed since is skipped), else to
275
+ the first focusable control in the panel's `restoreControl`, else to a
276
+ focusable control outside the panel whose `aria-controls` names the panel
277
+ (or an element inside it). The last is what a panel already open at
278
+ wire-up relies on, since nothing opened it: give the app's toggle
279
+ `aria-controls` naming the panel. A pointer press inside the panel that
280
+ drops focus to the page counts as focus inside: Safari never focuses a
281
+ clicked button, so pressing the panel's own Hide control blurs the focused
282
+ control before the click closes it, and focus still returns. Focus the user
283
+ has moved to another element since is left alone. Each panel's `id` derives from the
284
+ Workbench's — `<id>-left-rail`, `<id>-right-rail`, `<id>-bottom-drawer`:
285
+
286
+ ```tsx
287
+ <button
288
+ data-action="toggle-navigator"
289
+ aria-controls="editor-left-rail"
290
+ aria-expanded={String(!navCollapsed.value)}
291
+ aria-label={navCollapsed.value ? "Show navigator" : "Hide navigator"}
292
+ >
293
+ <LucideIcon {...collapsiblePanelToggleIcon("left", navCollapsed.value)} />
294
+ </button>
295
+ ```
296
+
297
+ An open overlay can cover the control that opened it: a narrow Workbench's
298
+ right-rail overlay spans the full height at the end edge, over the trailing
299
+ end of the work area's toolbar, where its toggle usually sits. Escape and an
300
+ outside press still close it, but neither is a visible, pointer-reachable
301
+ control. So give every panel that may present as an overlay its own close
302
+ control, as a `CollapsiblePanel` puts its collapse toggle in its own header:
303
+ render the panel content as a `Pane` whose header `Toolbar` holds the title
304
+ and a `single`, borderless `ToolbarControlGroup` around one icon button
305
+ carrying `collapsiblePanelToggleIcon(side, false)`. The work area's toggle
306
+ stays the always-visible way to show the panel. The catalog's resizable
307
+ example does this for both rails.
308
+
309
+ The drawer works the same way. The catalog's responsive overlay drawer
310
+ example is the whole pattern for a bottom drawer:
311
+
312
+ ```tsx
313
+ const outputCollapsed = signal(false); // open inline by default
314
+
315
+ <Workbench
316
+ id="editor"
317
+ label="Editor"
318
+ main={<Editor />} // its toolbar toggles `outputCollapsed`
319
+ bottomDrawer={{
320
+ label: "Output",
321
+ content: <OutputPane />, // a Pane whose header holds its close control
322
+ collapsed: outputCollapsed.value,
323
+ size: 180,
324
+ responsiveOverlayAt: "narrow",
325
+ }}
326
+ />;
327
+
328
+ wireWorkbench(root, {
329
+ id: "editor",
330
+ panels: { bottomDrawer: { collapsed: outputCollapsed } },
331
+ });
332
+ ```
333
+
334
+ Above 704px of Workbench width the drawer takes its own track below the
335
+ editor. At 704px or less it overlays the bottom of the full-height editor,
336
+ starts hidden, and closes from its own header, on Escape, or on a press
337
+ outside it; widening the Workbench again brings it back inline in the state
338
+ it had before.
339
+
340
+ Escape and outside-press dismissal apply to static `presentation: "overlay"`
341
+ panels too; entering and leaving the collapsed state around a breakpoint is
342
+ only for `responsiveOverlayAt`, because a static presentation is the app's
343
+ choice. Inline panels are never touched. Pass `dismissOverlays: false` to
344
+ leave every `collapsed` write, and all focus handling, to the app.
345
+
346
+ Overlays are exclusive by default, like `wireSidebar`'s compact overlays:
347
+ opening a panel while it presents as an overlay closes every other open
348
+ overlay panel, the bottom drawer included. At a narrow width two rails are
349
+ wider than the Workbench, so without this the right rail would cover the
350
+ navigator's own close control. Panels presenting inline are never closed by
351
+ it, so a wide Workbench keeps both rails open. Pass `exclusiveOverlays: false`
352
+ to let overlays stay open together; they then stack in the fixed order above,
353
+ and Tab stays in the one that holds focus (else the most recently opened
354
+ one).
355
+
356
+ Where the browser supports `overflow-clip-margin`, the 20px hit target
357
+ straddles the panel's separator line like a `ResizableRegion` handle;
358
+ elsewhere it sits just inside the panel's inner edge, because the panel clips
359
+ its sliding content.
360
+
73
361
  ## Resizable application-shell migration
74
362
 
75
363
  An application such as Hot Sheet can replace its shell descendant overrides
@@ -89,7 +377,13 @@ with state-derived props:
89
377
  collapseMotion="fade-slide"
90
378
  contentOverflow={createMenuOpen.value ? "visible" : "clip"}
91
379
  presentation={mobile.value ? "overlay" : "inline"}
92
- restoreControl={<button data-action="show-drawer">Show terminals</button>}
380
+ restoreControl={
381
+ <ToolbarControlGroup label="Terminals" single>
382
+ <button data-action="show-drawer" aria-label="Show terminals">
383
+ <LucideIcon {...collapsiblePanelToggleIcon("bottom", true)} />
384
+ </button>
385
+ </ToolbarControlGroup>
386
+ }
93
387
  >
94
388
  <TerminalDrawer />
95
389
  </ResizableRegion>
@@ -113,8 +407,9 @@ set `--kui-workbench-rail-width` and `--kui-workbench-drawer-height` on a
113
407
  Workbench instance. The supported composition classes are `.kui-workbench`,
114
408
  `.kui-workbench__rail`, `.kui-workbench__rail--left`,
115
409
  `.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
410
+ `.kui-workbench__main`, `.kui-workbench__drawer`,
411
+ `.kui-workbench__panel-content`, `.kui-workbench__handle`,
412
+ `.kui-workbench__handle-icon`, and `.kui-workbench__restore`; these exact hooks are cataloged for tools that
118
413
  must classify public application selectors. Prefer the component props and two
119
414
  size tokens before selecting internal anatomy, and do not target its data
120
415
  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
@@ -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,
@@ -262,9 +265,13 @@ the folder modules, and CSS only imports their owner stylesheets.
262
265
  still loading, set `placeholder` on the component (`Select`, `ListHeader`,
263
266
  `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`,
264
267
  `AppTab`, `ToolbarText`, `ListActionRow`): it renders its real chrome with value
265
- slots as subtle **unanimated** skeleton blocks and interactivity disabled, so a
266
- parent composes a faithful loading view (e.g. an inspector) without hand-rebuilding
267
- markup. Use the standalone `Skeleton` for a custom slot. Prefer it over `wa-skeleton`
268
+ slots as subtle **unanimated** skeleton blocks and interactivity disabled. Only
269
+ unknown values change: a placeholder is never dimmed or styled as disabled (a
270
+ control you disabled keeps its live disabled tone), so a parent composes a
271
+ faithful loading view (e.g. an inspector) without hand-rebuilding markup.
272
+ Component-drawn chrome stays live: a `ListItem` or `ListActionRow` keeps its
273
+ `busy` spinner while its `status` (and a `ListItem`'s `trailing`) content
274
+ become skeletons. Use the standalone `Skeleton` for a custom slot. Prefer it over `wa-skeleton`
268
275
  (the pure-Kerf primitives avoid Web Awesome), and never animate it — use
269
276
  `LoadingSpinner` for known busy activity instead.
270
277
  - **Keep ListItem content inside the row.** The rounded `ListItem` root clips
@@ -318,6 +325,7 @@ shell, `@kerfjs/ui/collapsible-panel` (`CollapsiblePanel`, `CollapsiblePanelTogg
318
325
  - [component-contract.md](./docs/component-contract.md): what a component owns vs. what the app owns.
319
326
  - [accessibility.md](./docs/accessibility.md): keyboard and ARIA contracts per component.
320
327
  - [recipes.md](./docs/recipes.md): production compositions (app shell, list-detail dialog, composer form, …) as copyable references.
328
+ - [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).
321
329
  - [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).
322
330
  - [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.
323
331
  - [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.52",
3
+ "version": "5.0.0-beta.54",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -230,6 +230,10 @@
230
230
  "types": "./dist/wire-tab-scaffold.d.ts",
231
231
  "import": "./dist/wire-tab-scaffold.js"
232
232
  },
233
+ "./wire-workbench": {
234
+ "types": "./dist/wire-workbench.d.ts",
235
+ "import": "./dist/wire-workbench.js"
236
+ },
233
237
  "./loading-spinner": {
234
238
  "types": "./dist/loading-spinner.d.ts",
235
239
  "browser": "./dist/browser/loading-spinner.js",