@kerfjs/ui 5.0.0-beta.106 → 5.0.0-beta.108

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 (283) hide show
  1. package/README.md +2 -1
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/application-ui-profile.defaults.json +6 -1
  4. package/ai/component-catalog.json +184 -39
  5. package/ai/component-composition-rules.json +20 -2
  6. package/ai/component-composition.d.ts +11 -0
  7. package/ai/component-composition.json +126 -21
  8. package/ai/component-composition.schema.json +32 -0
  9. package/ai/components/README.md +3 -2
  10. package/ai/components/button.md +9 -6
  11. package/ai/components/checkbox-group.md +89 -0
  12. package/ai/components/checkbox.md +3 -2
  13. package/ai/components/chip.md +1 -0
  14. package/ai/components/color-picker.md +10 -7
  15. package/ai/components/disclosure.md +6 -1
  16. package/ai/components/foundation.md +1 -1
  17. package/ai/components/list-detail-dialog.md +1 -1
  18. package/ai/components/list.md +1 -1
  19. package/ai/components/message-composer.md +2 -1
  20. package/ai/components/radio-group.md +4 -3
  21. package/ai/components/segmented-control.md +1 -0
  22. package/ai/components/split-button.md +5 -2
  23. package/ai/components/surface-scaffold.md +3 -1
  24. package/ai/components/switch.md +1 -1
  25. package/ai/components/text-field.md +3 -1
  26. package/ai/components/text.md +2 -0
  27. package/ai/components/toolbar-control-group.md +1 -0
  28. package/ai/components/toolbar.md +1 -1
  29. package/ai/components/wa-checkbox-group.md +5 -5
  30. package/ai/components/workbench-dialog.md +1 -1
  31. package/ai/public-api-signatures-v1.md +258 -35
  32. package/ai/skill.md +82 -65
  33. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  34. package/dist/browser/checkbox-group.js +5 -0
  35. package/dist/button.d.ts +18 -5
  36. package/dist/button.js +1 -1
  37. package/dist/catalog.js +11 -10
  38. package/dist/catalog.js.map +1 -1
  39. package/dist/checkbox-group.d.ts +70 -0
  40. package/dist/checkbox-group.js +7 -0
  41. package/dist/checkbox-group.js.map +1 -0
  42. package/dist/chip.d.ts +20 -2
  43. package/dist/chip.js +2 -1
  44. package/dist/{chunk-SQ2Q3OQI.js → chunk-2LM3PYHA.js} +2 -2
  45. package/dist/{chunk-SQ2Q3OQI.js.map → chunk-2LM3PYHA.js.map} +1 -1
  46. package/dist/{chunk-HYORKKYP.js → chunk-3VZ6SFD4.js} +1 -1
  47. package/dist/chunk-3VZ6SFD4.js.map +1 -0
  48. package/dist/{chunk-EKBMUXFV.js → chunk-3Z7N2NAZ.js} +141 -54
  49. package/dist/chunk-3Z7N2NAZ.js.map +1 -0
  50. package/dist/{chunk-23PPPNH6.js → chunk-4OV272LF.js} +2 -2
  51. package/dist/{chunk-23PPPNH6.js.map → chunk-4OV272LF.js.map} +1 -1
  52. package/dist/{chunk-2PXZBFEC.js → chunk-5J6Z4BWY.js} +1 -1
  53. package/dist/{chunk-2PXZBFEC.js.map → chunk-5J6Z4BWY.js.map} +1 -1
  54. package/dist/{chunk-TAHR457X.js → chunk-6CDILXF4.js} +5 -1
  55. package/dist/chunk-6CDILXF4.js.map +1 -0
  56. package/dist/{chunk-DDSCVEO5.js → chunk-CPSGUGAY.js} +6 -6
  57. package/dist/{chunk-DDSCVEO5.js.map → chunk-CPSGUGAY.js.map} +1 -1
  58. package/dist/{chunk-C3RLKKFQ.js → chunk-EYDXFOSH.js} +13 -8
  59. package/dist/chunk-EYDXFOSH.js.map +1 -0
  60. package/dist/{chunk-PE2KPZDW.js → chunk-HDXUHLTQ.js} +7 -2
  61. package/dist/chunk-HDXUHLTQ.js.map +1 -0
  62. package/dist/chunk-HU5IHXCI.js +73 -0
  63. package/dist/chunk-HU5IHXCI.js.map +1 -0
  64. package/dist/{chunk-DF2YKGIX.js → chunk-JCCN3U6X.js} +2 -2
  65. package/dist/{chunk-DF2YKGIX.js.map → chunk-JCCN3U6X.js.map} +1 -1
  66. package/dist/{chunk-UTCAOOXU.js → chunk-KPINJHIC.js} +8 -2
  67. package/dist/chunk-KPINJHIC.js.map +1 -0
  68. package/dist/{chunk-N7MBGONH.js → chunk-LMHN2TJL.js} +22 -2
  69. package/dist/chunk-LMHN2TJL.js.map +1 -0
  70. package/dist/{chunk-NS2TBYM4.js → chunk-LZUUUHNH.js} +7 -3
  71. package/dist/chunk-LZUUUHNH.js.map +1 -0
  72. package/dist/{chunk-GCAK23PU.js → chunk-MVKHSAKE.js} +2 -2
  73. package/dist/{chunk-GCAK23PU.js.map → chunk-MVKHSAKE.js.map} +1 -1
  74. package/dist/{chunk-OY2MXIBB.js → chunk-NB6POCFH.js} +3 -1
  75. package/dist/chunk-NB6POCFH.js.map +1 -0
  76. package/dist/{chunk-ECFCAKWE.js → chunk-NR6O74ST.js} +5 -1
  77. package/dist/chunk-NR6O74ST.js.map +1 -0
  78. package/dist/{chunk-7W3GKMN4.js → chunk-OQP3PFKQ.js} +7 -4
  79. package/dist/chunk-OQP3PFKQ.js.map +1 -0
  80. package/dist/{chunk-FKKJHGN5.js → chunk-P7EJ4CAD.js} +2 -2
  81. package/dist/{chunk-FKKJHGN5.js.map → chunk-P7EJ4CAD.js.map} +1 -1
  82. package/dist/{chunk-ZXZAECHZ.js → chunk-SOFPUW3O.js} +4 -4
  83. package/dist/{chunk-ZXZAECHZ.js.map → chunk-SOFPUW3O.js.map} +1 -1
  84. package/dist/{chunk-WJKHLOP5.js → chunk-TI2GV36S.js} +8 -2
  85. package/dist/chunk-TI2GV36S.js.map +1 -0
  86. package/dist/{chunk-QLA25V2K.js → chunk-WPNIKZMJ.js} +15 -1
  87. package/dist/chunk-WPNIKZMJ.js.map +1 -0
  88. package/dist/{chunk-FG6U7KQQ.js → chunk-WSEJ2FDE.js} +4 -4
  89. package/dist/{chunk-FG6U7KQQ.js.map → chunk-WSEJ2FDE.js.map} +1 -1
  90. package/dist/{chunk-ACUKTVS3.js → chunk-XWNXGFX3.js} +52 -42
  91. package/dist/chunk-XWNXGFX3.js.map +1 -0
  92. package/dist/{chunk-WU57BYYM.js → chunk-YVZCKXR3.js} +8 -2
  93. package/dist/chunk-YVZCKXR3.js.map +1 -0
  94. package/dist/{chunk-CMLPFIC5.js → chunk-ZN7KUMPY.js} +3 -1
  95. package/dist/chunk-ZN7KUMPY.js.map +1 -0
  96. package/dist/collapsible-panel.js +2 -2
  97. package/dist/color-picker.d.ts +30 -5
  98. package/dist/color-picker.js +1 -1
  99. package/dist/disclosure.d.ts +13 -4
  100. package/dist/disclosure.js.map +1 -1
  101. package/dist/help-tags-register.js +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.js +24 -23
  104. package/dist/list-action-row.js +1 -1
  105. package/dist/list-detail-dialog.d.ts +7 -1
  106. package/dist/list-detail-dialog.js +7 -5
  107. package/dist/list-detail-dialog.js.map +1 -1
  108. package/dist/list-header.d.ts +8 -1
  109. package/dist/list-header.js +1 -1
  110. package/dist/list.d.ts +7 -1
  111. package/dist/list.js +1 -1
  112. package/dist/message-composer.js +1 -1
  113. package/dist/nav-stack.js +1 -1
  114. package/dist/popup-menu-register.js +2 -2
  115. package/dist/popup-menu.d.ts +15 -5
  116. package/dist/popup-menu.js +1 -1
  117. package/dist/segmented-control.d.ts +3 -1
  118. package/dist/segmented-control.js +1 -1
  119. package/dist/select-register.js +3 -3
  120. package/dist/split-button.d.ts +14 -2
  121. package/dist/split-button.js +1 -1
  122. package/dist/split-view.js +1 -1
  123. package/dist/styles/anchored-surface.css +3 -0
  124. package/dist/styles/button.css +57 -2
  125. package/dist/styles/catalog/components/catalog-example.css +55 -18
  126. package/dist/styles/checkbox-group.css +63 -0
  127. package/dist/styles/checkbox.css +7 -6
  128. package/dist/styles/color-picker.css +69 -34
  129. package/dist/styles/components/actions/button/button.css +57 -2
  130. package/dist/styles/components/actions/split-button/split-button.css +51 -5
  131. package/dist/styles/components/actions/toolbar/toolbar.css +9 -0
  132. package/dist/styles/components/actions/toolbar-control-group/toolbar-control-group.css +29 -1
  133. package/dist/styles/components/collections/framed-list/framed-list.css +6 -0
  134. package/dist/styles/components/collections/list/list.css +6 -0
  135. package/dist/styles/components/collections/list-header/list-header.css +7 -0
  136. package/dist/styles/components/feedback/anchored-surface/anchored-surface.css +3 -0
  137. package/dist/styles/components/forms/checkbox/checkbox.css +7 -6
  138. package/dist/styles/components/forms/checkbox-group/checkbox-group.css +63 -0
  139. package/dist/styles/components/forms/color-picker/color-picker.css +69 -34
  140. package/dist/styles/components/forms/radio-group/radio-group.css +7 -5
  141. package/dist/styles/components/forms/select/select.css +6 -2
  142. package/dist/styles/components/forms/switch/switch.css +3 -3
  143. package/dist/styles/components/layout/pane/pane.css +24 -0
  144. package/dist/styles/components/layout/workbench/workbench.css +4 -0
  145. package/dist/styles/components/navigation/nav-stack/nav-stack.css +77 -1
  146. package/dist/styles/components/navigation/tab-bar/tab-bar.css +6 -0
  147. package/dist/styles/components/navigation/tab-navigator/tab-navigator.css +17 -0
  148. package/dist/styles/components/surfaces/content-item/content-item.css +30 -0
  149. package/dist/styles/components/surfaces/disclosure/disclosure.css +38 -0
  150. package/dist/styles/components/surfaces/sunken-panel/sunken-panel.css +3 -0
  151. package/dist/styles/components/surfaces/surface-scaffold/surface-scaffold.css +8 -0
  152. package/dist/styles/components/typography/text/text.css +10 -0
  153. package/dist/styles/content-item.css +30 -0
  154. package/dist/styles/disclosure.css +38 -0
  155. package/dist/styles/foundation.css +78 -13
  156. package/dist/styles/framed-list.css +6 -0
  157. package/dist/styles/list-header.css +7 -0
  158. package/dist/styles/list.css +6 -0
  159. package/dist/styles/nav-stack.css +77 -1
  160. package/dist/styles/pane.css +24 -0
  161. package/dist/styles/radio-group.css +7 -5
  162. package/dist/styles/select.css +6 -2
  163. package/dist/styles/split-button.css +51 -5
  164. package/dist/styles/styles.css +1 -0
  165. package/dist/styles/sunken-panel.css +3 -0
  166. package/dist/styles/surface-scaffold.css +8 -0
  167. package/dist/styles/switch.css +3 -3
  168. package/dist/styles/tab-bar.css +6 -0
  169. package/dist/styles/tab-navigator.css +17 -0
  170. package/dist/styles/text.css +10 -0
  171. package/dist/styles/toolbar-control-group.css +29 -1
  172. package/dist/styles/toolbar.css +9 -0
  173. package/dist/styles/webawesome.css +56 -12
  174. package/dist/styles/workbench.css +4 -0
  175. package/dist/surface-scaffold.d.ts +15 -1
  176. package/dist/surface-scaffold.js +1 -1
  177. package/dist/text-field.d.ts +7 -1
  178. package/dist/text-field.js +1 -1
  179. package/dist/toolbar-control-group.d.ts +8 -2
  180. package/dist/toolbar-control-group.js +1 -1
  181. package/dist/toolbar.d.ts +6 -1
  182. package/dist/toolbar.js +1 -1
  183. package/dist/wire-catalog.js +1 -1
  184. package/dist/wire-color-fields.js +2 -2
  185. package/dist/wire-color-pickers.d.ts +4 -3
  186. package/dist/wire-color-pickers.js +1 -1
  187. package/dist/wire-comboboxes.js +1 -1
  188. package/dist/wire-workbench.js +1 -1
  189. package/dist/workbench-dialog.d.ts +11 -2
  190. package/dist/workbench-dialog.js +6 -3
  191. package/dist/workbench-dialog.js.map +1 -1
  192. package/dist/workbench.js +1 -1
  193. package/docs/accessibility.md +58 -3
  194. package/docs/anchored-surface.md +6 -1
  195. package/docs/catalog.md +26 -1
  196. package/docs/color-picker.md +39 -8
  197. package/docs/component-contract.md +47 -3
  198. package/docs/component-selection.md +58 -21
  199. package/docs/design/templates/button/full-width-dark.svg +1 -0
  200. package/docs/design/templates/button/full-width.svg +1 -0
  201. package/docs/design/templates/button/tones-dark.svg +1 -1
  202. package/docs/design/templates/button/tones.svg +1 -1
  203. package/docs/design/templates/button-dark.svg +1 -1
  204. package/docs/design/templates/button.svg +1 -1
  205. package/docs/design/templates/checkbox/error-dark.svg +1 -1
  206. package/docs/design/templates/checkbox/error.svg +1 -1
  207. package/docs/design/templates/checkbox/states-dark.svg +1 -1
  208. package/docs/design/templates/checkbox/states.svg +1 -1
  209. package/docs/design/templates/checkbox-dark.svg +1 -1
  210. package/docs/design/templates/checkbox-group/horizontal-dark.svg +1 -0
  211. package/docs/design/templates/checkbox-group/horizontal.svg +1 -0
  212. package/docs/design/templates/checkbox-group/vertical-dark.svg +1 -0
  213. package/docs/design/templates/checkbox-group/vertical.svg +1 -0
  214. package/docs/design/templates/checkbox-group-dark.svg +1 -0
  215. package/docs/design/templates/checkbox-group.svg +1 -0
  216. package/docs/design/templates/checkbox.svg +1 -1
  217. package/docs/design/templates/color-picker/gray-dark.svg +1 -1
  218. package/docs/design/templates/color-picker/gray.svg +1 -1
  219. package/docs/design/templates/color-picker/hsb-dark.svg +1 -0
  220. package/docs/design/templates/color-picker/hsb.svg +1 -0
  221. package/docs/design/templates/color-picker/rgb-dark.svg +1 -1
  222. package/docs/design/templates/color-picker/rgb.svg +1 -1
  223. package/docs/design/templates/color-picker-dark.svg +1 -1
  224. package/docs/design/templates/color-picker.svg +1 -1
  225. package/docs/design/templates/disclosure/embedded-dark.svg +1 -0
  226. package/docs/design/templates/disclosure/embedded.svg +1 -0
  227. package/docs/design/templates/disclosure-dark.svg +1 -1
  228. package/docs/design/templates/disclosure.svg +1 -1
  229. package/docs/design/templates/radio-group/horizontal-dark.svg +1 -1
  230. package/docs/design/templates/radio-group/horizontal.svg +1 -1
  231. package/docs/design/templates/radio-group/vertical-dark.svg +1 -1
  232. package/docs/design/templates/radio-group/vertical.svg +1 -1
  233. package/docs/design/templates/radio-group-dark.svg +1 -1
  234. package/docs/design/templates/radio-group.svg +1 -1
  235. package/docs/design/templates/split-button/full-width-dark.svg +1 -0
  236. package/docs/design/templates/split-button/full-width.svg +1 -0
  237. package/docs/design/templates/split-button/pill-dark.svg +1 -0
  238. package/docs/design/templates/split-button/pill.svg +1 -0
  239. package/docs/design/templates/split-button/states-dark.svg +1 -0
  240. package/docs/design/templates/split-button/states.svg +1 -0
  241. package/docs/design/templates/split-button/tones-dark.svg +1 -0
  242. package/docs/design/templates/split-button/tones.svg +1 -0
  243. package/docs/design/templates/split-button-dark.svg +1 -0
  244. package/docs/design/templates/split-button.svg +1 -0
  245. package/docs/design/templates/switch/states-dark.svg +1 -1
  246. package/docs/design/templates/switch/states.svg +1 -1
  247. package/docs/design/templates/switch-dark.svg +1 -1
  248. package/docs/design/templates/switch.svg +1 -1
  249. package/docs/design/templates/text-field/number-dark.svg +1 -0
  250. package/docs/design/templates/text-field/number.svg +1 -0
  251. package/docs/design/templates/text-field-dark.svg +1 -1
  252. package/docs/design/templates/text-field.svg +1 -1
  253. package/docs/design/templates.md +10 -3
  254. package/docs/disclosure.md +34 -0
  255. package/docs/layout.md +16 -5
  256. package/docs/list-detail-dialog.md +4 -0
  257. package/docs/nav-stack.md +6 -2
  258. package/docs/surface-scaffold.md +6 -2
  259. package/docs/ui-doctor.md +16 -4
  260. package/docs/webawesome-migration.md +3 -1
  261. package/docs/webawesome-theme.md +56 -18
  262. package/docs/workbench-dialog.md +4 -1
  263. package/docs/workbench.md +7 -1
  264. package/docs/writing.md +3 -3
  265. package/doctor/copy-capitalization-checks.mjs +10 -7
  266. package/llms.txt +65 -23
  267. package/package.json +7 -1
  268. package/dist/chunk-7W3GKMN4.js.map +0 -1
  269. package/dist/chunk-ACUKTVS3.js.map +0 -1
  270. package/dist/chunk-C3RLKKFQ.js.map +0 -1
  271. package/dist/chunk-CMLPFIC5.js.map +0 -1
  272. package/dist/chunk-ECFCAKWE.js.map +0 -1
  273. package/dist/chunk-EKBMUXFV.js.map +0 -1
  274. package/dist/chunk-HYORKKYP.js.map +0 -1
  275. package/dist/chunk-N7MBGONH.js.map +0 -1
  276. package/dist/chunk-NS2TBYM4.js.map +0 -1
  277. package/dist/chunk-OY2MXIBB.js.map +0 -1
  278. package/dist/chunk-PE2KPZDW.js.map +0 -1
  279. package/dist/chunk-QLA25V2K.js.map +0 -1
  280. package/dist/chunk-TAHR457X.js.map +0 -1
  281. package/dist/chunk-UTCAOOXU.js.map +0 -1
  282. package/dist/chunk-WJKHLOP5.js.map +0 -1
  283. package/dist/chunk-WU57BYYM.js.map +0 -1
@@ -36,6 +36,10 @@ stylesheets, or `@kerfjs/ui/styles.css`.
36
36
  `list.footer`.
37
37
  - **The surface draws no header.** Both columns start at the dialog's top
38
38
  edge, so the divider between them runs the dialog's full height.
39
+ - **The description is yours to name.** The detail `description` describes the
40
+ selected record, not the dialog, so it is not the dialog's accessible
41
+ description. Pass `describedBy`, the id of app-owned summary copy, to render
42
+ `aria-describedby` on the dialog.
39
43
 
40
44
  Each column is a `Pane`: its header and footer stay pinned and its content
41
45
  scrolls on its own. Pass content that owns its geometry, usually a `List`.
package/docs/nav-stack.md CHANGED
@@ -241,8 +241,12 @@ scrolls visibly, blurred, beneath it.
241
241
  includes that padding, so views clear the chrome and the safe area exactly
242
242
  once.
243
243
  - **Transitions.** Only the live chrome is measured, never a cross-fading
244
- copy. The copy keeps painting over the top edge with the same material while
245
- it fades, and the incoming view settles at the incoming chrome's clearance.
244
+ copy. An overlay bar (top chrome or bottom toolbar) cross-fades its
245
+ contents, never its material: the live bar keeps the material at full
246
+ opacity while its new contents fade in, and the copy fading out above it
247
+ carries only the old contents, so the sliding views never show through the
248
+ chrome mid push or pop. The incoming view settles at the incoming chrome's
249
+ clearance.
246
250
  - **Classic scrollbars.** Where the active view's scroller (the view, or a
247
251
  sole header-less Pane's content) shows a classic, space-taking scrollbar,
248
252
  `wireNavStack` measures it and the chrome ends where its track begins, so
@@ -41,6 +41,11 @@ openDialog(document.getElementById('edit-workspace') as HTMLDialogElement);
41
41
  - **Name.** `label` names the dialog and is its title text. With `heading`
42
42
  (title content such as an xl `ToolbarText` with a description), pass
43
43
  `labelledBy`, the id of the element that names it, or keep `label`.
44
+ - **Description.** `describedBy` is the id of the element whose text is the
45
+ dialog's accessible description, rendered as `aria-describedby` on the
46
+ `<dialog>`: the `heading` ToolbarText's `descriptionId`, or app-owned summary
47
+ copy in the body. `rootAttributes` takes only `data-*` attributes, so it can
48
+ never set or override `aria-describedby`.
44
49
  - **Header.** The title, then `headerActions` and a close button (`closeLabel`,
45
50
  default `Close`). `withoutHeader` renders none, for content that owns its
46
51
  heading and close control.
@@ -447,8 +452,7 @@ pass `describedBy`, the id of the element that holds the summary. It renders as
447
452
  </PopoverDialogSurface>;
448
453
  ```
449
454
 
450
- `DialogSurface` has no `describedBy`: name it with `label` or `labelledBy`,
451
- and keep any description in the body or its `heading`'s description.
455
+ `DialogSurface` takes the same `describedBy` for a modal dialog.
452
456
 
453
457
  Open it with a control's `popoverTarget` (or `showPopover()`); the browser
454
458
  places it centered in the top layer, light-dismisses it, and restores nothing
package/docs/ui-doctor.md CHANGED
@@ -18,6 +18,16 @@ The catalog stage discovers the package-default application UI profile and works
18
18
 
19
19
  TypeScript uses the compiler API with `noEmit`. ESLint loads the installed `eslint-plugin-kerfjs` `recommended-ui` preset (`--eslint strict-ui` opts into advisory rules as errors). This is deliberately an isolated Kerf lint pass rather than the consumer's complete ESLint configuration. For each file, the doctor projects the applicable core ESLint rules and `linterOptions` from the consumer configuration so core-rule suppression directives retain the same used/unused semantics as the application's normal lint command. Consumer plugin rules are not executed, and inline directives for plugins such as `@typescript-eslint` therefore do not produce false "rule definition not found" diagnostics; the consumer's normal ESLint command remains authoritative for those rules. Unknown `kerfjs/*` directives still fail the doctor pass. Stable KUI identifiers come from the packaged `application-ui-diagnostic-ids-v1.json` registry; the doctor also reads installed ESLint message metadata for semantic conflict detection instead of duplicating rule definitions. The analyzer calls the public `@kerfjs/ui/analyzer` contract. None of these stages executes generated application code.
20
20
 
21
+ Composition findings come from the same catalogs. `KUI-L201`–`KUI-L203`
22
+ report a cataloged component under a disallowed parent, a zone child the zone
23
+ does not accept, and a provable zone cardinality violation. `KUI-L204` reports
24
+ a direct child that brings its own content-item inset (1px border + 8px
25
+ padding) into a parent configured to expect inset-free content, such as a
26
+ block `Text` inside `Disclosure appearance="embedded"`, which double-insets the
27
+ owner chrome; the message names the child's inset-free form (`Text flush`) or
28
+ inline content. Values the rule cannot read statically, such as a spread or an
29
+ expression, are not reported.
30
+
21
31
  Property-specific CSS values are evaluated from each first- or third-party
22
32
  catalog entry's `cssValueProps`. `KUI-L013`–`KUI-L016` are blocking grammar
23
33
  violations and name a preferred shorthand/helper; `KUI-L017` keeps an
@@ -182,9 +192,10 @@ covers these string literals:
182
192
  `section`, `nav`, `aside`, `main`, `header`, `footer`, `form`, or `search`
183
193
  element or an element with a landmark `role`;
184
194
  - field and group labels: the `label` and `ariaLabel` of `TextField`,
185
- `Select`, and `RadioGroup`, and the `label` of a raw `wa-input`,
186
- `wa-select`, `wa-textarea`, or `wa-radio-group` in an application using the
187
- optional Web Awesome integration;
195
+ `Select`, `CheckboxGroup`, and `RadioGroup`, and the `label` of a raw
196
+ `wa-input`, `wa-select`, `wa-textarea`, `wa-checkbox-group`, or
197
+ `wa-radio-group` in an application using the optional Web Awesome
198
+ integration;
188
199
  - the accessible names of icon-only controls, which the rule also gives title
189
200
  style: an icon-only `Button`'s `ariaLabel`, `PopupMenu` `label`,
190
201
  `SplitButton` `optionsLabel`, `SearchField` and `TextField`
@@ -203,7 +214,8 @@ The check is deliberately one-directional, to keep false positives rare:
203
214
  is. A word joined to an interpolation (`` `${count}s` ``) is skipped.
204
215
  - It ignores other dynamic values and every sentence-style surface: descriptions,
205
216
  details, placeholders, tooltips, status text, and option text, including
206
- `Checkbox` and `Switch` labels, `RadioGroup` choice labels, and
217
+ `Checkbox` and `Switch` labels, `CheckboxGroup` and `RadioGroup` choice
218
+ labels, and
207
219
  `wa-checkbox`, `wa-switch`, and `wa-radio` labels. It does not read
208
220
  `LoadingSpinner` or `Skeleton` labels, which announce a status.
209
221
  - It skips tokens containing digits, code, paths, or URLs, and lowercase-led
@@ -17,13 +17,15 @@ standard DOM events, and keeps its state in native form controls, so a
17
17
  delegated `input` / `change` / `click` handler usually keeps working once the
18
18
  selector changes.
19
19
 
20
+ For a positive action such as Price Check, migrate `<wa-button variant="success">` to `<Button tone="success">`. The semantic success palette also applies to `secondary` and `plain` emphasis; use the component prop rather than an application color override.
21
+
20
22
  | Web Awesome markup | Kerf replacement | Notes |
21
23
  | ------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
22
24
  | `<wa-button variant="brand">Save</wa-button>` | `<Button tone="brand" action="save">Save</Button>` (`@kerfjs/ui/button`) | `tone` replaces `variant`; `emphasis` replaces `appearance`. Use `href` for a link button, `icon` with `ariaLabel` and no label for an icon-only one, and `pressed` for a toggle. |
23
25
  | `<wa-input label="Name" hint="…" with-clear>` | `<TextField label="Name" hint="…" clearable />` (`@kerfjs/ui/text-field`) | Call `wireTextFields(root)` from `@kerfjs/ui/wire-text-fields` once for the clear button and a live `showCount`. Pass `error` for an invalid field. |
24
26
  | `<wa-textarea rows="4" resize="auto">` | `<TextField multiline rows={4} maxRows={10} />` | `maxRows` grows the field with its content up to that height. |
25
27
  | `<wa-checkbox>Email me</wa-checkbox>` | `<Checkbox label="Email me" action="toggle-email" />` (`@kerfjs/ui/checkbox`) | For a mixed parent, pass `indeterminate` and call `wireCheckboxes(root)` from `@kerfjs/ui/wire-checkboxes` once. |
26
- | `<wa-checkbox-group>` | `Checkbox` rows in a `List`, or `ChoiceChips` with `multiple="toggle"` | |
28
+ | `<wa-checkbox-group label="Notifications">` | `<CheckboxGroup label="Notifications" name="channels" value={channels.value} choices={[…]} />` (`@kerfjs/ui/checkbox-group`), or `ChoiceChips` with `multiple="toggle"` | |
27
29
  | `<wa-radio-group label="Density"><wa-radio value="compact">…` | `<RadioGroup label="Density" name="density" value={density.value} choices={[{ value: 'compact', label: 'Compact' }]} />` | `@kerfjs/ui/radio-group`. Use `orientation="horizontal"` for a row. Use `SegmentedControl` for a view switch and `ChoiceChips` for wrapping pills. |
28
30
  | `<wa-switch>Live updates</wa-switch>` | `<Switch label="Live updates" checked={live.value} action="toggle-live" />` (`@kerfjs/ui/switch`) | Only for a setting that applies immediately; use `Checkbox` for a submitted option. |
29
31
  | `<wa-select>` with `<wa-option>` children | `<Select label="Status" name="status" value={status.value} choices={[…]} />` (`@kerfjs/ui/select`) | Import `@kerfjs/ui/select/register` once. It emits standard `input` and `change` events. |
@@ -99,8 +99,8 @@ share Kerf's 44px control height — the height of a Kerf `Select`, a toolbar
99
99
  group, and a list row — so a `wa-input` or `wa-button` sits flush beside a Kerf
100
100
  `Select`. The theme sets `--wa-form-control-height` to `2.75em` (44px at the
101
101
  16px default) on Button, Color Picker, Input, Known Date, Number Input, Radio
102
- Group, Select, Textarea (its minimum height), and Time Input, so `size="large"`
103
- scales proportionally while `size="small"` keeps Web Awesome's compact secondary
102
+ Group, Select, Tag Input (its box's minimum height), Textarea (its minimum
103
+ height), and Time Input, so `size="large"` scales proportionally while `size="small"` keeps Web Awesome's compact secondary
104
104
  height. The token is scoped to those controls rather than the root because Web
105
105
  Awesome also derives tag, copy-button, and toast geometry from it. Buttons
106
106
  inside a `ToolbarControlGroup` keep the group's 40px (32px compact) control
@@ -114,26 +114,58 @@ uppercase 12px/650 treatment. Inline control labels (Checkbox, Switch) keep
114
114
  their natural sentence case.
115
115
 
116
116
  On coarse touch devices, the editable part of small and extra-small Web Awesome
117
- Input, Number Input, Time Input, Textarea, and Select has a 16px minimum text
118
- size. Small field chrome keeps its compact height and desktop typography stays
119
- small. This prevents Safari from magnifying the page just to focus text; it does
120
- not constrain a user's own pinch zoom. App-owned native fields need the same
117
+ Input, Number Input, Tag Input, Time Input, Textarea, and Select has a 16px
118
+ minimum text size. Small field chrome keeps its compact height and desktop
119
+ typography stays small. This prevents Safari from magnifying the page just to
120
+ focus text; it does not constrain a user's own pinch zoom. App-owned native fields need the same
121
121
  editable-text audit.
122
122
 
123
123
  Known Date's field captions and bordered text-like field hints use the same 9px
124
124
  inline inset, keeping secondary text aligned with the value rather than the
125
125
  field's outer border. This covers Input, Known Date, Number Input, OTP Input,
126
- Select, Textarea, and Time Input hints; unbordered group hints retain their own
127
- natural geometry. OTP Input's label and hint both use the 9px field-text inset.
128
-
129
- Checkbox Group and Radio Group option regions receive the shared 8px inline
130
- outer inset through `::part(form-control-input)`. Unlike bordered fields, these
131
- groups have no shell of their own, so the explicit margin places their controls
132
- on the same content-item geometry without adding padding to each option. The
133
- Color Picker trigger receives the same 8px inline outer inset because it also
134
- has no bordered field shell. Slider's complete interactive region receives the
126
+ Select, Tag Input, Textarea, and Time Input hints.
127
+ OTP Input's label and hint both use the 9px field-text inset. Its code cells
128
+ render whole in a shrink-to-fit host (a start-aligned grid or flex item):
129
+ upstream caps the segments' focus-ring-padded box at the host's width, which
130
+ there is the cells' width minus that padding and so clipped the last cell's
131
+ inline-end edge, and the theme caps it at the host's width plus the padding its
132
+ negative margins reserve. A host narrower than the code still scrolls its
133
+ segments; the `contained` appearance keeps upstream's cap.
134
+
135
+ Checkbox Group, Radio Group, and Color Picker follow Kerf's `Checkbox`,
136
+ `RadioGroup`, and `ColorField`: the control region stays flush, so each box,
137
+ circle, or swatch trigger sits on the host's inline-start edge, while the label
138
+ and hint use the same 9px text column as field labels. Stacked checkboxes and
139
+ radios therefore share one control column with Kerf's own toggle controls;
140
+ place the group where you would place a Kerf `Checkbox` or `RadioGroup` rather
141
+ than adding margin to its parts. Stacked
142
+ default-appearance Radios sit 8px apart (`0.25em` block margins instead of Web
143
+ Awesome's `0.375em`, the first and last radio still flush), so a Radio Group
144
+ keeps the same 28px pitch as a Kerf `RadioGroup` and a Checkbox Group's 8px
145
+ gap; horizontal and button radios keep Web Awesome's spacing.
146
+
147
+ Bordered fields (Input, Known Date, Number Input, OTP Input, Select, Tag
148
+ Input, Textarea, and Time Input) and Switch likewise draw their border or
149
+ track on the host's inline-start edge, exactly where Kerf's `TextField`, `Select`, and `Switch`
150
+ draw theirs, with the label and hint on the same 9px text column. Place a Web
151
+ Awesome field where you would place the Kerf field it stands in for, so a mixed
152
+ form shares one border column and one text column; the UX catalog gives these
153
+ specimens the same `inline-control` alignment as the Kerf field demos. Tag
154
+ Input is Discouraged in favor of Kerf's `TagField`, but an application that
155
+ still renders it gets the same label, hint, height, and border placement as the
156
+ other bordered fields.
157
+
158
+ Slider's complete interactive region receives the
135
159
  shared 8px logical inline outer inset through `::part(slider)`, keeping its
136
- track, markers, references, and hint together without moving the label.
160
+ track, markers, references, and hint together without moving the label. Placed
161
+ where a Kerf `Slider` goes, its label and track land on the Kerf Slider's text
162
+ column.
163
+
164
+ Known Date and Slider size to their container rather than to their content, so
165
+ give them a width (a form column, or the container's full width) instead of a
166
+ shrink-to-fit parent, where Known Date collapses to nothing and Slider to a
167
+ stub. The UX catalog gives Known Date a compact field column and Slider the
168
+ Kerf Slider demo's medium width.
137
169
 
138
170
  Non-field chrome follows explicit, overridable control, surface, and container
139
171
  tiers:
@@ -374,8 +406,14 @@ application off them.
374
406
 
375
407
  ## Customization
376
408
 
377
- Load application overrides after the package theme, or scope them to the
378
- smallest subtree that needs a different identity:
409
+ To change the brand, set Kerf's opt-in `--kui-color-brand` on `:root`: the
410
+ theme derives `--wa-color-brand-fill-loud`, `--wa-color-brand-on-quiet`, and
411
+ `--wa-color-focus` from it (with the matching Kerf tokens) at AA-safe
412
+ lightness, so brand text and the focus ring follow the new hue. See
413
+ [Brand color](./component-selection.md#brand-color).
414
+
415
+ For anything else, load application overrides after the package theme, or
416
+ scope them to the smallest subtree that needs a different identity:
379
417
 
380
418
  ```css
381
419
  :root {
@@ -50,7 +50,8 @@ The dialog draws no surface header and no body inset. Its body is a
50
50
  `Workbench` (`<id>-workbench`):
51
51
 
52
52
  - **The work area's toolbar** carries the dialog's primary title (an xl
53
- level-2 heading, id `<id>-title`) with an optional `description`, any
53
+ level-2 heading, id `<id>-title`) with an optional `description` (id
54
+ `<id>-description`, the dialog's accessible description), any
54
55
  `leading` groups, the `actions`, and the close control. Close stays the
55
56
  outermost trailing control, after a hidden right rail's toggle.
56
57
  - **The rails** (`leftRail`, `rightRail`) keep their own toolbars, with
@@ -74,6 +75,8 @@ Escape closes the dialog.
74
75
 
75
76
  - `id`, `label` (required): identity and the dialog's accessible name.
76
77
  - `title` (required), `description`, `leading`, `actions`.
78
+ - `describedBy`: the id of other content, such as summary copy in `main`,
79
+ that describes the dialog in place of `description`.
77
80
  - `main` (required), `mainFooter`, `mainBottomToolbar`.
78
81
  - `leftRail`, `rightRail`, `bottomDrawer`: `WorkbenchPanel`s.
79
82
  - `compact`, `closeLabel` (default `Close`), `lightDismiss`.
package/docs/workbench.md CHANGED
@@ -471,7 +471,13 @@ presents as one, mirroring `wireSidebar`'s compact overlay:
471
471
  - **Focus moves in, and Tab stays in:** a panel that opens while it presents
472
472
  as an overlay takes focus on its first focusable control (typically the
473
473
  close control in its header), because it covers the work area and the
474
- control that opened it. While it is open, Tab and Shift+Tab cycle through
474
+ control that opened it. That holds whichever control opens it: the
475
+ Workbench's own relocating toggle, or a persistent toggle outside the
476
+ Workbench (an app-bar button whose `aria-controls` names the panel) that
477
+ stays where it is. Focus moves in as soon as the panel can take it, even
478
+ when the app's render shows the panel a frame after the `collapsed` signal
479
+ changes; focus the person has moved elsewhere in the meantime stays put.
480
+ While it is open, Tab and Shift+Tab cycle through
475
481
  its controls and never reach the covered work area; Escape, its own close
476
482
  control, or an outside press leaves it. This is the ARIA dialog pattern
477
483
  `wireSidebar`'s compact overlay follows. A panel opening inline never moves
package/docs/writing.md CHANGED
@@ -115,9 +115,9 @@ interpolated value keeps its own spelling. The covered props are:
115
115
  the `aria-label` of a landmark element (`section`, `nav`, `aside`, `main`,
116
116
  `header`, `footer`, `form`, `search`) or landmark role;
117
117
  - field and group labels: the `label` (or `ariaLabel`) of `TextField`,
118
- `Select`, and `RadioGroup`, and, in an application using the optional Web
119
- Awesome integration, the `label` of a raw `wa-input`, `wa-select`,
120
- `wa-textarea`, or `wa-radio-group`;
118
+ `Select`, `CheckboxGroup`, and `RadioGroup`, and, in an application using
119
+ the optional Web Awesome integration, the `label` of a raw `wa-input`,
120
+ `wa-select`, `wa-textarea`, `wa-checkbox-group`, or `wa-radio-group`;
121
121
  - the accessible names of icon-only controls: an icon-only `Button`'s `ariaLabel`, `PopupMenu` `label`, `SplitButton`
122
122
  `optionsLabel`, `SearchField` and `TextField` `clearLabel`, and the `aria-label` of a
123
123
  `button` or `wa-button` whose only content is an icon.
@@ -14,7 +14,7 @@ import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
14
14
  // to known title-style props are read (a template literal's static words, never
15
15
  // its interpolated values), so prose, descriptions, placeholders, tooltips,
16
16
  // checkbox/switch option text, status lines, LoadingSpinner and Skeleton
17
- // labels, Checkbox/Switch labels and RadioGroup choice labels, and dynamic
17
+ // labels, Checkbox/Switch labels and CheckboxGroup/RadioGroup choice labels, and dynamic
18
18
  // data are never judged. The check reports lowercase words title style capitalizes; it
19
19
  // does not report capitalized short words, because Apple capitalizes a
20
20
  // preposition of any length inside a phrasal verb ("Log In to the Server").
@@ -179,9 +179,11 @@ const titleStyleProps = new Map([
179
179
  'Select',
180
180
  { props: ['label', 'ariaLabel'], entries: { choices: ['label'] } },
181
181
  ],
182
- // Field and group labels. A Checkbox or Switch label and a RadioGroup
183
- // choice label are option text, which is sentence style and not read.
182
+ // Field and group labels. A Checkbox or Switch label and a CheckboxGroup or
183
+ // RadioGroup choice label are option text, which is sentence style and not
184
+ // read.
184
185
  ['TextField', { props: ['label', 'ariaLabel', 'clearLabel'] }],
186
+ ['CheckboxGroup', { props: ['label', 'ariaLabel'] }],
185
187
  ['RadioGroup', { props: ['label', 'ariaLabel'] }],
186
188
  ['TabNavigator', { entries: { tabs: ['label'] } }],
187
189
  // Region and landmark names: the accessible name of a pane, toolbar,
@@ -243,14 +245,15 @@ const landmarkRoles = new Set([
243
245
  'form',
244
246
  'search',
245
247
  ]);
246
- // A raw Web Awesome field's or radio group's `label` (in an application using
247
- // the optional integration) is a field label. The option text of
248
- // `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style and is not
249
- // read.
248
+ // A raw Web Awesome field's, checkbox group's, or radio group's `label` (in an
249
+ // application using the optional integration) is a field label. The option
250
+ // text of `wa-checkbox`, `wa-switch`, and `wa-radio` is sentence style and is
251
+ // not read.
250
252
  const fieldLabelTags = new Set([
251
253
  'wa-input',
252
254
  'wa-select',
253
255
  'wa-textarea',
256
+ 'wa-checkbox-group',
254
257
  'wa-radio-group',
255
258
  ]);
256
259
  const titleToolbarTextSizes = new Set(['xlarge', 'xlarge-fixed', 'large']);
package/llms.txt CHANGED
@@ -324,7 +324,9 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
324
324
  Public ListItem tokens set row, icon, label, and trailing geometry;
325
325
  ListHeader `size="xs|sm|m|l|xl"` sets label scale independently of
326
326
  `headingLevel` (`sm` keeps the current FieldLabel-sized uppercase style;
327
- `m`/`l`/`xl` match themed h5/h4/h3). Its height, title padding, and border
327
+ `m`/`l`/`xl` match themed h5/h4/h3). The label is one ellipsized line;
328
+ set `wrap` where the whole label must stay readable in a narrow column
329
+ (it wraps instead of truncating). Its height, title padding, and border
328
330
  tokens tune section density. A semantic `count` requires `countLabel` and
329
331
  excludes other indicators; `badge` and `status` may render together, and
330
332
  either may be explicitly `undefined` in conditional app state.
@@ -398,9 +400,14 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
398
400
  When a dialog provides another dismissal affordance, add `hide-actions` to
399
401
  the `wa-dialog` host; the theme hides its directly exported `header-actions`
400
402
  part without an unsupported chained `::part()` selector.
401
- Checkbox and Radio Group option regions also receive the shared 8px inline
402
- outer inset because those groups have no bordered field shell. The Color
403
- Picker trigger receives the same inset for the same unbordered geometry.
403
+ Checkbox and Radio Group option regions and the Color Picker trigger stay
404
+ flush, so each box, circle, or swatch sits on the host's inline-start edge
405
+ like Kerf's `Checkbox`, `RadioGroup`, and `ColorField`, with the label and
406
+ hint on the 9px text column. Bordered fields and Switch draw their border or
407
+ track on that same edge like `TextField`, `Select`, and `Switch`, so place a
408
+ Web Awesome field exactly where the Kerf field would go.
409
+ Stacked radios sit 8px apart (`0.25em` block margins) for the same 28px
410
+ pitch as `RadioGroup`.
404
411
  Slider's complete interactive region uses the shared 8px logical inline outer
405
412
  inset without shifting its already-aligned label.
406
413
  Known Date's Month/Day/Year captions and bordered text-like field hints align
@@ -448,9 +455,18 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
448
455
  or `sizing="grow"` with a `growBasis` that fits), and if a useful search
449
456
  cannot fit beside the other controls, set aside those controls while the
450
457
  search is open: mark them `visibility="yield-to-expanded-sibling"` (they
451
- leave a 480px-or-narrower toolbar while a sibling is expanded) or gather
452
- them behind a "…" `PopupMenu` overflow group. The app decides which
458
+ leave a 480px-or-narrower toolbar while a sibling is expanded), or, in a
459
+ `responsive="trailing-priority"` Toolbar, `visibility="yield-to-priority-row"`
460
+ (they leave exactly while the expanded control has its own row, from the
461
+ Toolbar's `responsiveAt`; never restate that breakpoint with `hideBelow`,
462
+ whose strict "below" is one pixel off the Toolbar's inclusive width), or
463
+ gather them behind a "…" `PopupMenu` overflow group. The app decides which
453
464
  controls yield.
465
+ - **Change the brand with `--kui-color-brand`.** Set it once on `:root` (any
466
+ color): Kerf derives the brand fill, brand text and links, text on brand
467
+ fills, and the focus ring at WCAG AA, and Web Awesome's brand tokens follow.
468
+ Overriding `--kui-color-brand-fill-loud` alone leaves brand text and the
469
+ focus ring blue.
454
470
  - **Toolbars hold only `ToolbarText` and `ToolbarControlGroup`.** Never put a bare
455
471
  button, input, link, or loose markup in a `leading`/`center`/`trailing` zone.
456
472
  Wrap every control — `SegmentedControl`, `Select`, a collapsible
@@ -470,9 +486,12 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
470
486
  `detail.checked` is the state a checkbox choice asks for (the app owns and
471
487
  renders that state);
472
488
  use `context` with `anchor` + `openPopupMenu` (or `openPopupMenuAt`) and
473
- `closePopupMenu` for pointer-anchored row menus; the menu owns its
489
+ `closePopupMenu` for pointer-anchored row menus, whose `placement` applies
490
+ at the point (`top-start` opens above it); the menu owns its
474
491
  zero-footprint anchor host, so add no wrapper or CSS. `openPopupMenuAt`
475
- also opens a button-triggered menu at the pointer for one opening. A row's
492
+ also opens a button-triggered menu at the pointer for one opening
493
+ (`bottom-start`, or `openPopupMenuAt(menu, x, y, { placement: 'top-start' })`
494
+ to open above the point). A row's
476
495
  trailing "…" overflow menu is `ListActionRow`'s `trailingMenu` (`label`,
477
496
  `icon`, `items`, optional `placement` and `disabled`), which renders a
478
497
  `PopupMenu` as the row's sibling trailing control. Do not force a dropdown or button width. Nothing
@@ -676,8 +695,23 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
676
695
  the inset rounded card; never restyle a card into a strip.
677
696
  - **Use `SplitButton` for a primary action with options.** One loud semantic
678
697
  fill with a native primary button and an options trigger (`aria-expanded`,
679
- `aria-haspopup`); `tone`, `fill`, `busy`, and `disabled` are props. Never
680
- build one from `wa-button-group` or app CSS on native buttons.
698
+ `aria-haspopup`); `tone`, `fill`, `shape` (`rounded` or `pill`, as on
699
+ `Button`), `busy`, and `disabled` are props. Never build one from
700
+ `wa-button-group` or app CSS on native buttons.
701
+ - **Keep a Button's focus ring inside a clipping ancestor with
702
+ `focusRingInset`.** `Button` and `SplitButton` draw the ring 1px outside the
703
+ box; inside a container that clips overflow, set `focusRingInset` and the
704
+ ring moves inside the box with a surface-colored line inside it, so it keeps
705
+ 3:1 contrast even on a loud fill of its own hue. The line follows the
706
+ surface context sunken, quiet, raised, and highlighted surfaces, app
707
+ `background` colors, and glass chrome publish
708
+ (`--kui-color-surface-current`, `-region`, `-tint`), so it never reads as a
709
+ halo there. Never re-offset the outline from app CSS.
710
+ - **Span a full-width primary action with `fill`.** `<Button tone="brand" fill action="create">Create Project</Button>`
711
+ spans the parent's available inline width instead of shrinking to its
712
+ label, with the label centered and ellipsized once it no longer fits; a
713
+ link (`href`) fills the same way. It is the same `fill` `SplitButton`
714
+ takes. Never set `width: 100%` on a Button from app CSS.
681
715
  - **Make any `Button` a toggle with `pressed`.** In content or a pane footer,
682
716
  `<Button icon={…} ariaLabel="Show Conversation" emphasis="plain" pressed={open} action="toggle-chat" />`
683
717
  renders a native `aria-pressed` button whose pressed look draws on top of
@@ -724,14 +758,20 @@ headingLevel`, `SearchField clearIcon`, `ContentItem frame="framed"` for a
724
758
  button, `multiline` with `rows` and a growing `maxRows`, and `maxLength` with
725
759
  `showCount`, at the 44px height or `size="compact"`. Call
726
760
  `wireTextFields(root)` (`@kerfjs/ui/wire-text-fields`) once for clearing and
727
- counting. Never render `wa-input` or `wa-textarea` for a form field.
728
- - **Use `Checkbox`, `RadioGroup`, and `Switch` for native toggles.**
729
- `@kerfjs/ui/checkbox` (with `indeterminate` and `wireCheckboxes(root)` from
730
- `@kerfjs/ui/wire-checkboxes`), `@kerfjs/ui/radio-group` (`label`, `name`,
731
- `choices`, `value`; one Tab stop, arrow keys), and `@kerfjs/ui/switch`
732
- (`role="switch"`, only for a setting that applies immediately) draw Kerf
733
- boxes over native inputs with hint and error text. Never render
734
- `wa-checkbox`, `wa-radio-group`, or `wa-switch`.
761
+ counting. For `type="number"`, pass native `min`, `max`, and `step` as numbers
762
+ or strings (`step="any"` accepts arbitrary decimals). Never render `wa-input`
763
+ or `wa-textarea` for a form field.
764
+ - **Use `Checkbox`, `CheckboxGroup`, `RadioGroup`, and `Switch` for native
765
+ toggles.** `@kerfjs/ui/checkbox` (with `indeterminate` and
766
+ `wireCheckboxes(root)` from `@kerfjs/ui/wire-checkboxes`),
767
+ `@kerfjs/ui/checkbox-group` (`label`, `name`, `choices`, an array `value`;
768
+ a labeled set laid out like RadioGroup), `@kerfjs/ui/radio-group` (`label`,
769
+ `name`, `choices`, `value`; one Tab stop, arrow keys), and
770
+ `@kerfjs/ui/switch` (`role="switch"`, only for a setting that applies
771
+ immediately) draw Kerf boxes over native inputs with hint and error text.
772
+ Stack standalone Checkboxes in a `List gap="xs"`: 8px apart, the same 28px
773
+ pitch as `RadioGroup` and `CheckboxGroup` choices. Never render
774
+ `wa-checkbox`, `wa-checkbox-group`, `wa-radio-group`, or `wa-switch`.
735
775
  - **Use `Combobox` to pick or create a value by typing.** A themed native
736
776
  input on the ARIA combobox pattern with filtered suggestions and an optional
737
777
  create option; `wireComboboxes(root, { onCommit })` reports `{ id, value,
@@ -770,8 +810,9 @@ created, input }`. Never use a hand-styled `<input list>` + `<datalist>`.
770
810
  Never use `wa-tooltip`.
771
811
  - **Show and hide content with `Disclosure`** (`@kerfjs/ui/disclosure`): a
772
812
  native details element with a DisclosureArrow on content-item geometry,
773
- `plain` / `framed` / `sunken`; Disclosures sharing a `name` form an
774
- accordion. Never use `wa-details` or `wa-accordion`.
813
+ `plain` / `framed` / `sunken`, or `embedded` inside a `ContentItem` (no
814
+ margin, border, or padding of its own, a compact summary, `flush` `Text`
815
+ content); Disclosures sharing a `name` form an accordion. Never use `wa-details` or `wa-accordion`.
775
816
  - **Show progress with `ProgressBar` or `ProgressRing`** (`@kerfjs/ui/progress-bar`,
776
817
  `@kerfjs/ui/progress-ring`): `value` of `max` (omit `value` for
777
818
  indeterminate), a `label`, `valueText`, tones. Never use `wa-progress-bar` or
@@ -786,7 +827,8 @@ created, input }`. Never use a hand-styled `<input list>` + `<datalist>`.
786
827
  a native radio group of named round swatches, one Tab stop, a ring that
787
828
  does not depend on hue, and a `'transparent'` no-color swatch.
788
829
  - **Choose any color with `ColorPicker`** (`@kerfjs/ui/color-picker`): an area,
789
- hue and opacity sliders, RGB/HSL/HSB/Grayscale fields, hex, presets, and
830
+ channel sliders that follow the color model (R/G/B, H/S/L, H/S/B, or gray)
831
+ plus opacity, RGB/HSL/HSB/Grayscale fields, hex, presets, and
790
832
  samples; call `wireColorPickers(root, { onChange })` and re-render.
791
833
  - **Put a color in a form with `ColorField`** (`@kerfjs/ui/color-field`): a
792
834
  swatch-and-hex trigger that opens a ColorPicker below it; call
@@ -962,10 +1004,10 @@ width policy. Keep primary actions visible and offer a complementary overflow
962
1004
  group. See `docs/component-contract.md` "Toolbar width visibility" for lifecycle
963
1005
  details.
964
1006
 
965
- Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. Keep popup sizing and position repairs out of consumer CSS.
1007
+ Select menus use native viewport shifting with 10px gutters: default menus grow to content (up to 20rem), while `fitMenu` matches trigger width and long labels wrap. A listbox (like a PopupMenu menu) shifts only along its trigger's edge, never over the trigger: when neither side fits, it opens on the side with more room and scrolls inside its capped height, so a second press on the trigger still closes it. Keep popup sizing and position repairs out of consumer CSS.
966
1008
 
967
1009
  Key sibling `DialogSurface`s that render conditionally with `rootAttributes={{ "data-key": "…" }}` so a re-render morphs each dialog into itself; never wrap one in a keyed app `div`. Modal `DialogSurface` bounds use typed `viewportGutter` and `maxHeight` (`CssLength` or `"viewport"`); explicit caps stay within the dynamic viewport minus both gutters. Omitted props keep the default caps (20px clear of each edge, 80% of the viewport height at 420px wide or narrower). Application render state chooses responsive values, and preview layout remains application-owned. `height` sets a definite bounded modal size and `bodyLayout="fill"` lets one child fill the body; `bodyLayout="fill-last"` keeps leading rows at their height above one filling last child, and `footerPresentation="floating"` (with a typed `footerOffset`) floats a chat composer over the body while the body reserves its measured height; give the transcript `scrollClearance="block-end"` (a `scrollable` `List` or a `Pane`) to let it scroll beneath the composer, with the last message still resting above it, through the published `--kui-scroll-clearance-block-end` context (overlaying chrome publishes `--kui-scroll-clearance-block-start` / `-end`; one scroller per covered edge opts in; a composed Pane opts in through `PaneConfig.scrollClearance` on a Workbench `mainPane` or panel `pane`, a `CollapsiblePanel` `pane`, or a `NavStack` view's `pane`). `headerPresentation="floating"` lays the title bar over the body on the translucent `--kui-chrome-material-raised` (opaque under reduced transparency, increased contrast, and forced colors); the register measures it and the body reserves it, and a `fill` body's only child with `scrollClearance="both"` scrolls beneath both the title bar and a floating composer through the published `--kui-scroll-clearance-block-start`. `presentation="bottom-sheet"` attaches the dialog to the bottom edge with square bottom corners, safe-area and virtual-keyboard clearance behind the panel, and a slide in/out (`enterMotion` / `exitMotion`); `resizable` adds a top grabber separator bounded by `minHeight` and `maxHeight`. `PopoverDialogSurface` is the non-modal, light-dismiss counterpart on the native Popover API, with the same size props and an `embedded` in-flow presentation; `describedBy` sets its `aria-describedby` (usually a title `ToolbarText`'s `descriptionId`).
968
1010
 
969
- Render a modal dialog as `DialogSurface` itself, a native `<dialog>`: `label` (or `labelledBy` with a `heading`, such as an xl `ToolbarText` with a description) names it, `children` is its body, `footer` holds its buttons, and `headerActions` sit before its close button (`withoutHeader` drops the header for content that owns its heading and close control). Open it with `openDialog(dialog)` and close it with `closeDialog(dialog)` from `@kerfjs/ui/surface-scaffold`; never toggle its `open` attribute. The close button, any `data-dialog-close` element inside (a Cancel button), Escape, and, with `lightDismiss`, a backdrop press dispatch a cancelable `kui-request-close` (`detail.source`: `close-button`, `escape`, or `backdrop`); cancel it to keep the dialog open (the panel pulses). The native `close` event fires once it closes. `openDialog` installs the listeners and, while the dialog is open, measures its floating header/footer, follows a bottom sheet's keyboard and grabber, and keeps that state across re-renders; import `@kerfjs/ui/surface-scaffold/register` only when a dialog may open some other way. Do not write `wa-dialog` inside a `DialogSurface`.
1011
+ Render a modal dialog as `DialogSurface` itself, a native `<dialog>` (`alert` renders `role="alertdialog"` for a destructive confirmation; pair it with `describedBy` on the message): `label` (or `labelledBy` with a `heading`, such as an xl `ToolbarText` with a description) names it, `describedBy` (the heading ToolbarText's `descriptionId`, or app-owned summary copy) describes it as `aria-describedby`, `children` is its body, `footer` holds its buttons, and `headerActions` sit before its close button (`withoutHeader` drops the header for content that owns its heading and close control). Open it with `openDialog(dialog)` and close it with `closeDialog(dialog)` from `@kerfjs/ui/surface-scaffold`; never toggle its `open` attribute. The close button, any `data-dialog-close` element inside (a Cancel button), Escape, and, with `lightDismiss`, a backdrop press dispatch a cancelable `kui-request-close` (`detail.source`: `close-button`, `escape`, or `backdrop`); cancel it to keep the dialog open (the panel pulses). The native `close` event fires once it closes. `openDialog` installs the listeners and, while the dialog is open, measures its floating header/footer, follows a bottom sheet's keyboard and grabber, and keeps that state across re-renders; import `@kerfjs/ui/surface-scaffold/register` only when a dialog may open some other way. Do not write `wa-dialog` inside a `DialogSurface`.
970
1012
 
971
1013
  Toolbar visibility measurement probes and invisible state children are helper-owned and survive Kerf rerenders through `data-morph-preserve`. Actual CSS visibility stays stable while rendering removes transient parent markers, preventing geometry-driven render feedback and repeated probe recreation. Toolbar replacement, explicit internal-node removal and disposal retain their existing behavior.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.106",
3
+ "version": "5.0.0-beta.108",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -495,6 +495,11 @@
495
495
  "browser": "./dist/browser/checkbox.js",
496
496
  "import": "./dist/checkbox.js"
497
497
  },
498
+ "./checkbox-group": {
499
+ "types": "./dist/checkbox-group.d.ts",
500
+ "browser": "./dist/browser/checkbox-group.js",
501
+ "import": "./dist/checkbox-group.js"
502
+ },
498
503
  "./radio-group": {
499
504
  "types": "./dist/radio-group.d.ts",
500
505
  "browser": "./dist/browser/radio-group.js",
@@ -626,6 +631,7 @@
626
631
  "./file-picker.css": "./dist/styles/file-picker.css",
627
632
  "./text-field.css": "./dist/styles/text-field.css",
628
633
  "./checkbox.css": "./dist/styles/checkbox.css",
634
+ "./checkbox-group.css": "./dist/styles/checkbox-group.css",
629
635
  "./slider.css": "./dist/styles/slider.css",
630
636
  "./radio-group.css": "./dist/styles/radio-group.css",
631
637
  "./switch.css": "./dist/styles/switch.css",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/components/actions/popup-menu/internal/popup-menu-content-cache.ts","../src/components/actions/popup-menu/popup-menu.tsx"],"names":[],"mappings":"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","file":"chunk-7W3GKMN4.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport type { PopupMenuEntry } from '../popup-menu.js';\n\n/**\n * Streaming content hash: slot 0 is the FNV-1a digest that names\n * the menu's `data-key`; slot 1 is an independent check hash (a 31-multiplier\n * polynomial whose dependency chain runs in parallel with the digest's), so\n * the markup cache is keyed by about 64 bits and a digest collision cannot\n * serve another menu's markup. An `Int32Array` keeps both accumulators unboxed\n * between calls.\n */\nconst hashState = new Int32Array(2);\n\nfunction hashCode(code: number): void {\n hashState[0] = Math.imul(hashState[0]! ^ code, 0x01000193);\n hashState[1] = (Math.imul(hashState[1]!, 31) + code) | 0;\n}\n\n/**\n * Hash a string's code points. A surrogate pair is combined into one code\n * point, matching the former `for…of` iteration over the joined key, so the\n * digest, and with it the rendered `data-key`, is unchanged.\n */\nfunction hashText(text: string): void {\n let digest = hashState[0]!;\n let check = hashState[1]!;\n const length = text.length;\n for (let index = 0; index < length; index++) {\n let code = text.charCodeAt(index);\n if ((code & 0xfc00) === 0xd800 && index + 1 < length) {\n const low = text.charCodeAt(index + 1);\n if ((low & 0xfc00) === 0xdc00) {\n code = ((code - 0xd800) << 10) + (low - 0xdc00) + 0x10000;\n index++;\n }\n }\n digest = Math.imul(digest ^ code, 0x01000193);\n check = (Math.imul(check, 31) + code) | 0;\n }\n hashState[0] = digest;\n hashState[1] = check;\n}\n\n/**\n * Parts at least this long (icon and details markup) are hashed through\n * {@link hashLongText}: by a memoized digest of the whole string instead of\n * character by character, so a glyph repeated on every row costs a map lookup.\n */\nexport const LONG_PART_LENGTH = 64;\n\n/** Bound on memoized long-part digests; past it the oldest is evicted. */\nexport const PART_DIGEST_LIMIT = 4096;\nconst partDigests = new Map<string, readonly [number, number]>();\n\n/**\n * Feed a long part as its own two hashes. A Lucide SVG is a few hundred\n * characters and repeats across every row's menu, so hashing its characters\n * dominated an unchanged menu's cost; V8 hashes a string key natively (once\n * per string object) and compares equal strings quickly.\n */\nfunction hashLongText(text: string): void {\n let pair = partDigests.get(text);\n if (!pair) {\n const digest = hashState[0]!;\n const check = hashState[1]!;\n hashState[0] = 0x811c9dc5;\n hashState[1] = 0x9747b28c;\n hashText(text);\n pair = [hashState[0], hashState[1]];\n hashState[0] = digest;\n hashState[1] = check;\n if (partDigests.size >= PART_DIGEST_LIMIT)\n partDigests.delete(partDigests.keys().next().value!);\n partDigests.set(text, pair);\n }\n hashCode(pair[0]);\n hashCode(pair[1]);\n}\n\n/** Hash a part, through its memoized digest when it is long. */\nfunction hashPart(text: string): void {\n if (text.length >= LONG_PART_LENGTH) hashLongText(text);\n else hashText(text);\n}\n\nconst PART_SEPARATOR = 0x00;\nconst FIELD_SEPARATOR = 0x01;\n\n/**\n * Feed one entry's structure: the fields, in order, separated by U+0001, with\n * each submenu entry appended the same way.\n */\nfunction hashEntry(entry: PopupMenuEntry): void {\n if (entry.kind === 'divider') {\n hashCode(0x2d); // '-'\n return;\n }\n if (entry.kind === 'heading') {\n hashCode(0x23); // '#'\n hashText(entry.label);\n return;\n }\n hashText(entry.label);\n hashCode(FIELD_SEPARATOR);\n hashText(entry.action ?? '');\n hashCode(FIELD_SEPARATOR);\n hashText(entry.value ?? '');\n hashCode(FIELD_SEPARATOR);\n hashCode(entry.disabled ? 0x31 : 0x30);\n hashCode(FIELD_SEPARATOR);\n hashCode(entry.checked === undefined ? 0x2d : entry.checked ? 0x31 : 0x30);\n hashCode(FIELD_SEPARATOR);\n hashText(entry.tone ?? '');\n hashCode(FIELD_SEPARATOR);\n hashText(entry.disabledReason ?? '');\n hashCode(FIELD_SEPARATOR);\n // SafeHtml stringifies to its stored markup without copying, and the key\n // must change when an icon or details change.\n hashPart(String(entry.icon ?? ''));\n hashCode(FIELD_SEPARATOR);\n hashPart(String(entry.details ?? ''));\n hashCode(FIELD_SEPARATOR);\n hashText(entry.attributes ? JSON.stringify(entry.attributes) : '{}');\n for (const child of entry.submenu ?? []) {\n hashCode(FIELD_SEPARATOR);\n hashEntry(child);\n }\n}\n\n/**\n * Bounded, least-recently-used cache of each menu's trigger and entry markup,\n * keyed by its content hashes. A list that re-renders unchanged menus then\n * pays one hash pass per menu instead of rendering every entry again, which\n * the morph would discard under `data-morph-skip-children` anyway. Sized above\n * a large list's distinct menus; past it, renders fall back to fresh markup.\n */\nexport const MARKUP_CACHE_LIMIT = 1024;\nconst markupCache = new Map<string, SafeHtml>();\n\n/**\n * The menu's `data-key` and its cache key. The digest covers, in order and\n * separated by U+0000, the trigger parts and then each entry, whose fields are\n * separated by U+0001; it is the FNV-1a hash of that joined text, except that\n * a part of {@link LONG_PART_LENGTH} or more characters contributes its own\n * two hashes in place of its characters.\n */\nexport function popupMenuContentKey(\n triggerParts: readonly string[],\n items: readonly PopupMenuEntry[],\n): { key: string; cacheKey: string } {\n hashState[0] = 0x811c9dc5;\n hashState[1] = 0x9747b28c;\n for (let index = 0; index < triggerParts.length; index++) {\n if (index > 0) hashCode(PART_SEPARATOR);\n hashPart(triggerParts[index]!);\n }\n for (const entry of items) {\n hashCode(PART_SEPARATOR);\n hashEntry(entry);\n }\n const digest = (hashState[0]! >>> 0).toString(36);\n return {\n key: `kui-popup-menu-${digest}`,\n cacheKey: `${digest}:${(hashState[1]! >>> 0).toString(36)}`,\n };\n}\n\n/** Cached markup for a cache key, refreshed as most recently used. */\nexport function cachedPopupMenuContent(cacheKey: string): SafeHtml | undefined {\n const content = markupCache.get(cacheKey);\n if (content) {\n markupCache.delete(cacheKey);\n markupCache.set(cacheKey, content);\n }\n return content;\n}\n\n/**\n * Remember rendered markup, evicting the least recently used entry when full.\n * Only static markup is reusable: content that holds a keyed list must be\n * rendered in each pass for the list reconciler.\n */\nexport function cachePopupMenuContent(\n cacheKey: string,\n content: SafeHtml,\n): void {\n if (content.__segment.kind !== 'static') return;\n if (markupCache.size >= MARKUP_CACHE_LIMIT)\n markupCache.delete(markupCache.keys().next().value!);\n markupCache.set(cacheKey, content);\n}\n\n/** Test hook: drop cached menu markup and memoized part digests. */\nexport function _clearPopupMenuMarkupCache(): void {\n markupCache.clear();\n partDigests.clear();\n}\n\n/** Test hook: the number of memoized long-part digests. */\nexport function _popupMenuPartDigestCount(): number {\n return partDigests.size;\n}\n\n/** Test hook: the number of cached menu markups. */\nexport function _popupMenuMarkupCacheSize(): number {\n return markupCache.size;\n}\n","import type { SafeHtml } from 'kerfjs';\nimport { Check } from 'lucide';\n\nimport type { KerfUiContent } from '../../../shared/content/semantic-content.js';\nimport {\n PopupCaretGlyph,\n PopupSubmenuGlyph,\n} from '../../../shared/popup/popup-glyphs.js';\nimport { LucideIcon } from '../../media/lucide-icon/lucide-icon.js';\nimport {\n cachedPopupMenuContent,\n cachePopupMenuContent,\n popupMenuContentKey,\n} from './internal/popup-menu-content-cache.js';\nimport {\n POPUP_MENU_CLOSE_EVENT,\n POPUP_MENU_OPEN_EVENT,\n type PopupMenuOpenDetail,\n} from './internal/popup-menu-events.js';\n\nexport type PopupMenuPlacement =\n 'bottom-start' | 'bottom' | 'bottom-end' | 'top-start' | 'top' | 'top-end';\n\ntype PopupMenuDataAttributes = Readonly<\n Record<`data-${string}`, string | undefined> & {\n 'data-action'?: never;\n 'data-component'?: never;\n 'data-key'?: never;\n 'data-morph-skip-children'?: never;\n }\n>;\n\n/** One command in the menu. */\nexport interface PopupMenuItem {\n kind?: 'item';\n label: string;\n /** Delegated `data-action` the app handles when the item is chosen. */\n action?: string;\n /** The item's value, reported in the menu's `kui-menu-select` event. */\n value?: string;\n /** Leading icon, typically a `LucideIcon`, before the label. */\n icon?: SafeHtml;\n /** A checkbox choice, including nested choices; omit for a plain command. */\n checked?: boolean;\n /** Trailing safe content, such as secondary status. */\n details?: SafeHtml;\n /** Destructive command styling. */\n tone?: 'default' | 'danger';\n disabled?: boolean;\n /** Native tooltip text for a disabled command. */\n disabledReason?: string;\n /** Application `data-*` metadata such as a record id. */\n attributes?: PopupMenuDataAttributes;\n /** Child commands, headings, and dividers opened by hover or keyboard navigation. */\n submenu?: readonly PopupMenuEntry[];\n}\n\n/** A labeled group heading; items that follow it belong to the group. */\nexport interface PopupMenuHeading {\n kind: 'heading';\n label: string;\n}\n\n/** A separator between groups of items. */\nexport interface PopupMenuDivider {\n kind: 'divider';\n}\n\nexport type PopupMenuEntry =\n PopupMenuItem | PopupMenuHeading | PopupMenuDivider;\n\ntype PopupMenuTriggerName =\n | {\n /** Visible trigger text, which is also its accessible name. */\n text: string;\n label?: never;\n triggerContent?: never;\n }\n | {\n text?: never;\n /**\n * Accessible name of an icon-only trigger, rendered as visually hidden\n * text inside the trigger button.\n */\n label: string;\n triggerContent?: never;\n }\n | {\n text?: never;\n /**\n * The trigger's accessible name (\"Status: Started\"); the visible\n * content is hidden from assistive technology.\n */\n label: string;\n /**\n * Visible content that is the whole trigger, such as a `Badge` showing\n * the current status. Kerf owns the bare button around it (no chrome,\n * a pill focus ring, the pointer cursor, `aria-haspopup`), and the\n * caret is off unless `caret` is set.\n */\n triggerContent: KerfUiContent;\n icon?: never;\n };\n\n/** A context PopupMenu's viewport anchor point. */\nexport interface PopupMenuAnchor {\n x: number;\n y: number;\n}\n\ntype PopupMenuTrigger =\n | (PopupMenuTriggerName & { context?: false; anchor?: never })\n | {\n context: true;\n label: string;\n triggerContent?: never;\n /**\n * The viewport point (CSS pixels, as in `MouseEvent.clientX/Y`) the\n * menu opens from. Rendered into the menu itself, so it survives\n * re-renders and replacement; open it with `openPopupMenu`.\n */\n anchor?: PopupMenuAnchor;\n text?: never;\n icon?: never;\n caret?: never;\n disabled?: never;\n };\n\nexport type PopupMenuProps = PopupMenuTrigger & {\n /** Trigger icon, typically a `LucideIcon`, before any visible text. */\n icon?: KerfUiContent;\n items: readonly PopupMenuEntry[];\n /** Where the menu opens relative to its trigger. Defaults to `bottom-start`. */\n placement?: PopupMenuPlacement;\n /** Show the disclosure caret after the trigger content. Defaults to true. */\n caret?: boolean;\n disabled?: boolean;\n /** Application `data-*` metadata on the menu root. */\n rootAttributes?: PopupMenuDataAttributes;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n};\n\ntype PopupMenuCommand = PopupMenuItem & { kind?: 'item' };\n\nconst isCommand = (entry: PopupMenuEntry): entry is PopupMenuCommand =>\n entry.kind !== 'heading' && entry.kind !== 'divider';\n\n/** Whether a menu level holds a checkbox choice, so its plain items reserve the check column. */\nfunction hasChoice(entries: readonly PopupMenuEntry[]): boolean {\n return entries.some(\n (entry) => isCommand(entry) && entry.checked !== undefined,\n );\n}\n\nfunction renderItem(entry: PopupMenuCommand, levelHasChoice: boolean) {\n // A plain command beside checkbox choices keeps their check column, so its\n // icon and label line up with theirs.\n const choice = entry.checked !== undefined;\n const item = (\n <button\n {...(entry.attributes ?? {})}\n type=\"button\"\n class=\"kui-popup-menu__item\"\n role={choice ? 'menuitemcheckbox' : 'menuitem'}\n tabindex={-1}\n data-action={entry.action}\n value={entry.value}\n data-tone={entry.tone === 'danger' ? 'danger' : undefined}\n aria-checked={choice ? String(entry.checked) : undefined}\n aria-disabled={entry.disabled ? 'true' : undefined}\n aria-haspopup={entry.submenu ? 'menu' : undefined}\n aria-expanded={entry.submenu ? 'false' : undefined}\n title={entry.disabled ? entry.disabledReason : undefined}\n >\n {levelHasChoice ? (\n <span class=\"kui-popup-menu__check\" aria-hidden=\"true\">\n {choice ? <LucideIcon icon={Check} name=\"check\" /> : null}\n </span>\n ) : null}\n {entry.icon ? (\n <span class=\"kui-popup-menu__icon\" aria-hidden=\"true\">\n {entry.icon}\n </span>\n ) : null}\n <span class=\"kui-popup-menu__item-label\">{entry.label}</span>\n {entry.details ? (\n <span class=\"kui-popup-menu__details\">{entry.details}</span>\n ) : null}\n {entry.submenu ? (\n <span class=\"kui-popup-menu__submenu-icon\" aria-hidden=\"true\">\n <PopupSubmenuGlyph />\n </span>\n ) : null}\n </button>\n );\n if (!entry.submenu) return item;\n return (\n <div class=\"kui-popup-menu__submenu-host\" role=\"none\">\n {item}\n {renderMenu(entry.submenu, entry.label, true)}\n </div>\n );\n}\n\n/**\n * One menu level: its entries in order, with each heading opening a labeled\n * group that holds the items up to the next heading or divider.\n */\nfunction renderEntries(entries: readonly PopupMenuEntry[]): SafeHtml[] {\n const levelHasChoice = hasChoice(entries);\n const out: SafeHtml[] = [];\n let index = 0;\n while (index < entries.length) {\n const entry = entries[index]!;\n if (entry.kind === 'divider') {\n out.push(<div class=\"kui-popup-menu__divider\" role=\"separator\" />);\n index += 1;\n continue;\n }\n if (entry.kind === 'heading') {\n const members: SafeHtml[] = [];\n index += 1;\n while (index < entries.length) {\n const member = entries[index]!;\n if (!isCommand(member)) break;\n members.push(renderItem(member, levelHasChoice));\n index += 1;\n }\n out.push(\n <div\n class=\"kui-popup-menu__group\"\n role=\"group\"\n aria-label={entry.label}\n >\n <div class=\"kui-popup-menu__heading\" aria-hidden=\"true\">\n {entry.label}\n </div>\n {members}\n </div>,\n );\n continue;\n }\n out.push(renderItem(entry, levelHasChoice));\n index += 1;\n }\n return out;\n}\n\nfunction renderMenu(\n entries: readonly PopupMenuEntry[],\n name: string | undefined,\n submenu: boolean,\n) {\n return (\n <div\n class=\"kui-popup-menu__menu\"\n role=\"menu\"\n popover=\"manual\"\n tabindex={-1}\n aria-label={name}\n data-submenu={submenu ? '' : undefined}\n >\n {renderEntries(entries)}\n </div>\n );\n}\n\n/** A rendered PopupMenu root, the element the open and close helpers take. */\nexport type PopupMenuElement = HTMLElement;\n\nfunction requestOpen(menu: PopupMenuElement, detail: PopupMenuOpenDetail) {\n menu.dispatchEvent(\n new CustomEvent<PopupMenuOpenDetail>(POPUP_MENU_OPEN_EVENT, { detail }),\n );\n}\n\n/**\n * Open a PopupMenu at viewport pointer coordinates. A `context` menu rendered\n * without `anchor` moves its invisible anchor there (a menu rendered with\n * `anchor` opens with `openPopupMenu` instead). A button-triggered menu (such\n * as a `ListActionRow` trailing menu) opens `bottom-start` from the point,\n * like a context menu, for this opening only; its next opening returns to its\n * trigger and placement. Needs the `/register` entry.\n */\nexport function openPopupMenuAt(\n menu: PopupMenuElement,\n x: number,\n y: number,\n): void {\n menu.style.setProperty('--kui-popup-menu-context-x', `${x}px`);\n menu.style.setProperty('--kui-popup-menu-context-y', `${y}px`);\n requestOpen(menu, { point: { x, y } });\n}\n\n/**\n * Open a PopupMenu from its trigger, or a context PopupMenu at the `anchor`\n * it was rendered with (or the point a previous `openPopupMenuAt` set).\n * Needs the `/register` entry.\n */\nexport function openPopupMenu(menu: PopupMenuElement): void {\n requestOpen(menu, {});\n}\n\n/** Close an open PopupMenu, and any submenu it shows. */\nexport function closePopupMenu(menu: PopupMenuElement): void {\n menu.dispatchEvent(new CustomEvent(POPUP_MENU_CLOSE_EVENT));\n}\n\ninterface PopupMenuContentProps {\n context: boolean;\n label: string | undefined;\n icon: KerfUiContent;\n triggerContent: KerfUiContent;\n text: string | undefined;\n caret: boolean;\n disabled: boolean;\n items: readonly PopupMenuEntry[];\n}\n\n/** The trigger and the menu: everything inside the root. */\nfunction renderContent({\n context,\n label,\n icon,\n triggerContent,\n text,\n caret,\n disabled,\n items,\n}: PopupMenuContentProps): SafeHtml {\n return (\n <>\n {context ? (\n <button\n type=\"button\"\n class=\"kui-popup-menu__context-anchor\"\n aria-hidden=\"true\"\n tabindex={-1}\n />\n ) : (\n <button\n type=\"button\"\n class=\"kui-popup-menu__trigger\"\n aria-haspopup=\"menu\"\n aria-expanded=\"false\"\n disabled={disabled}\n >\n {icon}\n {triggerContent === undefined ? null : (\n <span class=\"kui-popup-menu__trigger-content\" aria-hidden=\"true\">\n {triggerContent}\n </span>\n )}\n {text ? (\n <span class=\"kui-popup-menu__text\">{text}</span>\n ) : (\n <span class=\"kui-popup-menu__label\">{label}</span>\n )}\n {caret ? (\n <span class=\"kui-popup-menu__caret\" aria-hidden=\"true\">\n <PopupCaretGlyph />\n </span>\n ) : null}\n </button>\n )}\n {renderMenu(items, text ?? label, false)}\n </>\n );\n}\n\n/**\n * An action menu: a trigger button that opens a list of commands in the top\n * layer. A `single` `ToolbarControlGroup` (with `nestedDropdown`) or a\n * `PopupSurface` sizes and insets it as usual. Items dispatch through\n * delegated `data-action`s, and the root reports each choice as a bubbling\n * `kui-menu-select` event; the app owns the commands and any open-state reaction.\n * Import `@kerfjs/ui/popup-menu/register` once to wire opening, keyboard\n * navigation, submenus, and dismissal. At viewport widths up to 480px,\n * submenus appear above or below their parent menu so their rows do not cover\n * the parent list.\n */\nexport function PopupMenu({\n text,\n label,\n icon,\n triggerContent,\n items,\n placement = 'bottom-start',\n caret = triggerContent === undefined,\n disabled = false,\n rootAttributes = {},\n slot,\n context = false,\n anchor,\n}: PopupMenuProps) {\n if (\n anchor !== undefined &&\n (!context || !Number.isFinite(anchor.x) || !Number.isFinite(anchor.y))\n )\n throw new RangeError(\n 'PopupMenu anchor requires context mode and finite x and y coordinates',\n );\n // The register owns the open menu's state (shown popovers, `aria-expanded`,\n // checked toggles, focus), so the morph skips the children; the content key\n // rebuilds the menu when they change. An unchanged menu reuses its cached\n // markup and costs one hash pass.\n const { key, cacheKey } = popupMenuContentKey(\n [\n text ?? '',\n label ?? '',\n String(icon ?? ''),\n String(caret),\n String(disabled),\n String(context),\n // Only a content trigger adds a part, so other menus keep their keys.\n ...(triggerContent === undefined ? [] : [String(triggerContent)]),\n ],\n items,\n );\n let content = cachedPopupMenuContent(cacheKey);\n if (!content) {\n content = renderContent({\n context,\n label,\n icon,\n triggerContent,\n text,\n caret,\n disabled,\n items,\n });\n cachePopupMenuContent(cacheKey, content);\n }\n return (\n <span\n {...rootAttributes}\n class=\"kui-popup-menu\"\n data-component=\"popup-menu\"\n data-key={key}\n data-morph-skip-children\n data-trigger={\n context\n ? 'context'\n : triggerContent === undefined\n ? 'button'\n : 'content'\n }\n data-placement={placement}\n style={\n anchor\n ? `--kui-popup-menu-context-x:${anchor.x}px;--kui-popup-menu-context-y:${anchor.y}px`\n : undefined\n }\n slot={slot}\n >\n {content}\n </span>\n );\n}\n"]}