@kerfjs/ui 5.0.0-beta.49 → 5.0.0-beta.51

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 (319) hide show
  1. package/README.md +69 -15
  2. package/ai/catalog-authoring.json +12 -1
  3. package/ai/catalog-authoring.schema.json +49 -1
  4. package/ai/compile-time-contracts-v1.json +19 -2
  5. package/ai/compile-time-contracts-v1.schema.json +1 -0
  6. package/ai/component-catalog-v2-overrides.json +7 -0
  7. package/ai/component-catalog-v2.d.ts +19 -1
  8. package/ai/component-catalog-v2.json +920 -149
  9. package/ai/component-catalog-v2.schema.json +33 -1
  10. package/ai/component-catalog.json +689 -153
  11. package/ai/component-catalog.schema.json +21 -1
  12. package/ai/public-api-signatures-v1.md +404 -184
  13. package/ai/skill.md +31 -14
  14. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  15. package/analyzer/index.mjs +105 -35
  16. package/dist/app-tab.d.ts +3 -1
  17. package/dist/app-tab.js +1 -1
  18. package/dist/badge.d.ts +24 -0
  19. package/dist/badge.js +3 -0
  20. package/dist/badge.js.map +1 -0
  21. package/dist/browser/badge.js +3 -0
  22. package/dist/browser/list-header.js +1 -0
  23. package/dist/browser/state-banner.js +1 -0
  24. package/dist/catalog-resources.d.ts +2 -2
  25. package/dist/catalog.d.ts +47 -125
  26. package/dist/catalog.js +429 -262
  27. package/dist/catalog.js.map +1 -1
  28. package/dist/{chunk-W2FK7ZNY.js → chunk-4R777DUZ.js} +10 -2
  29. package/dist/chunk-4R777DUZ.js.map +1 -0
  30. package/dist/{chunk-OMSRSL3B.js → chunk-5FZZMIMD.js} +6 -4
  31. package/dist/chunk-5FZZMIMD.js.map +1 -0
  32. package/dist/{chunk-AQFWBG23.js → chunk-5PMGQDG7.js} +5 -3
  33. package/dist/chunk-5PMGQDG7.js.map +1 -0
  34. package/dist/{chunk-SOK5EUF7.js → chunk-6ZXOQEAN.js} +4 -2
  35. package/dist/chunk-6ZXOQEAN.js.map +1 -0
  36. package/dist/{chunk-I4V3UDSA.js → chunk-7GUQYHKA.js} +25 -6
  37. package/dist/chunk-7GUQYHKA.js.map +1 -0
  38. package/dist/{chunk-XKFRMGMW.js → chunk-7IUSV4XG.js} +3 -1
  39. package/dist/chunk-7IUSV4XG.js.map +1 -0
  40. package/dist/{chunk-IU4QUY5I.js → chunk-AEPXGCNF.js} +20 -3
  41. package/dist/chunk-AEPXGCNF.js.map +1 -0
  42. package/dist/{chunk-VDUJZF5G.js → chunk-CMUIXJZM.js} +2 -2
  43. package/dist/{chunk-VDUJZF5G.js.map → chunk-CMUIXJZM.js.map} +1 -1
  44. package/dist/{chunk-DAO2VFTT.js → chunk-D547PH4L.js} +7 -5
  45. package/dist/chunk-D547PH4L.js.map +1 -0
  46. package/dist/{chunk-2TYUFZUC.js → chunk-D7GWXIAS.js} +4 -2
  47. package/dist/chunk-D7GWXIAS.js.map +1 -0
  48. package/dist/{chunk-E34TE2ZI.js → chunk-DVZCMMB3.js} +6 -4
  49. package/dist/chunk-DVZCMMB3.js.map +1 -0
  50. package/dist/{chunk-6NDBE2OK.js → chunk-E36EVCKG.js} +5 -3
  51. package/dist/chunk-E36EVCKG.js.map +1 -0
  52. package/dist/chunk-EKYZCRCX.js +256 -0
  53. package/dist/chunk-EKYZCRCX.js.map +1 -0
  54. package/dist/{chunk-TIGSG6BX.js → chunk-HJKRCKDR.js} +7 -3
  55. package/dist/chunk-HJKRCKDR.js.map +1 -0
  56. package/dist/{chunk-Y6LYKFXY.js → chunk-HMQ2ZDGG.js} +5 -3
  57. package/dist/chunk-HMQ2ZDGG.js.map +1 -0
  58. package/dist/{chunk-UDHHKZWO.js → chunk-JM53SMVF.js} +9 -5
  59. package/dist/chunk-JM53SMVF.js.map +1 -0
  60. package/dist/{chunk-OGTJNHRG.js → chunk-JMGKM7RK.js} +6 -4
  61. package/dist/chunk-JMGKM7RK.js.map +1 -0
  62. package/dist/{chunk-GYGM363P.js → chunk-KX7DP2GO.js} +17 -7
  63. package/dist/chunk-KX7DP2GO.js.map +1 -0
  64. package/dist/{chunk-OL4TC3U2.js → chunk-MO56ZZLG.js} +6 -2
  65. package/dist/chunk-MO56ZZLG.js.map +1 -0
  66. package/dist/{chunk-VXFGMDA7.js → chunk-MTM43SJO.js} +16 -2
  67. package/dist/chunk-MTM43SJO.js.map +1 -0
  68. package/dist/{chunk-ZPWZIVI6.js → chunk-N3GKMPDW.js} +7 -3
  69. package/dist/chunk-N3GKMPDW.js.map +1 -0
  70. package/dist/{chunk-DYBPI5O2.js → chunk-NDUGJ4TU.js} +4 -2
  71. package/dist/chunk-NDUGJ4TU.js.map +1 -0
  72. package/dist/{chunk-I45QO2IZ.js → chunk-NMGTHYW3.js} +7 -5
  73. package/dist/chunk-NMGTHYW3.js.map +1 -0
  74. package/dist/{chunk-WSXUPSID.js → chunk-NN6GYOA2.js} +7 -3
  75. package/dist/chunk-NN6GYOA2.js.map +1 -0
  76. package/dist/{chunk-OGNE3OFT.js → chunk-NZXWU4R5.js} +24 -3
  77. package/dist/chunk-NZXWU4R5.js.map +1 -0
  78. package/dist/{chunk-V6GQKOX2.js → chunk-O3RCCSQJ.js} +6 -4
  79. package/dist/chunk-O3RCCSQJ.js.map +1 -0
  80. package/dist/{chunk-WVLMZ5SG.js → chunk-S4QRSBFI.js} +25 -3
  81. package/dist/chunk-S4QRSBFI.js.map +1 -0
  82. package/dist/{chunk-CK6P5AUK.js → chunk-UBH743HV.js} +10 -7
  83. package/dist/chunk-UBH743HV.js.map +1 -0
  84. package/dist/{chunk-T7DOWS6T.js → chunk-ULCDZTWL.js} +7 -4
  85. package/dist/chunk-ULCDZTWL.js.map +1 -0
  86. package/dist/{chunk-V6HCTCVD.js → chunk-UPZ6EEJN.js} +9 -5
  87. package/dist/chunk-UPZ6EEJN.js.map +1 -0
  88. package/dist/{chunk-CAB6NQ2T.js → chunk-VOCPXGQN.js} +3 -1
  89. package/dist/chunk-VOCPXGQN.js.map +1 -0
  90. package/dist/chunk-XD7DWYXG.js +34 -0
  91. package/dist/chunk-XD7DWYXG.js.map +1 -0
  92. package/dist/{chunk-B6JYRX3P.js → chunk-ZIYOJFZD.js} +43 -4
  93. package/dist/chunk-ZIYOJFZD.js.map +1 -0
  94. package/dist/{chunk-EYALJQQD.js → chunk-ZK5EGVFP.js} +4 -2
  95. package/dist/chunk-ZK5EGVFP.js.map +1 -0
  96. package/dist/{chunk-6WTETUZ6.js → chunk-ZLWQZUGF.js} +8 -2
  97. package/dist/chunk-ZLWQZUGF.js.map +1 -0
  98. package/dist/collapsible-panel.d.ts +4 -2
  99. package/dist/collapsible-panel.js +4 -2
  100. package/dist/collapsible-panel.js.map +1 -1
  101. package/dist/css-values.d.ts +39 -3
  102. package/dist/css-values.js +1 -1
  103. package/dist/disclosure-arrow.d.ts +3 -1
  104. package/dist/disclosure-arrow.js +1 -1
  105. package/dist/empty-state.d.ts +3 -1
  106. package/dist/empty-state.js +1 -1
  107. package/dist/floating-toolbar.d.ts +3 -1
  108. package/dist/floating-toolbar.js +1 -1
  109. package/dist/grid.d.ts +17 -1
  110. package/dist/grid.js +2 -1
  111. package/dist/index.d.ts +4 -3
  112. package/dist/index.js +34 -33
  113. package/dist/list-action-row.d.ts +3 -1
  114. package/dist/list-action-row.js +1 -1
  115. package/dist/list-header.d.ts +6 -1
  116. package/dist/list-header.js +2 -1
  117. package/dist/list-inset-control.d.ts +3 -1
  118. package/dist/list-inset-control.js +1 -1
  119. package/dist/list-inset-text.d.ts +3 -1
  120. package/dist/list-inset-text.js +1 -1
  121. package/dist/list-item.d.ts +3 -1
  122. package/dist/list-item.js +1 -1
  123. package/dist/list.d.ts +23 -2
  124. package/dist/list.js +2 -1
  125. package/dist/loading-spinner.d.ts +3 -1
  126. package/dist/loading-spinner.js +1 -1
  127. package/dist/lucide-icon.d.ts +3 -1
  128. package/dist/lucide-icon.js +1 -1
  129. package/dist/nav-stack.d.ts +3 -1
  130. package/dist/nav-stack.js +1 -1
  131. package/dist/pane.d.ts +19 -2
  132. package/dist/pane.js +1 -1
  133. package/dist/resizable-region.d.ts +5 -2
  134. package/dist/resizable-region.js +1 -1
  135. package/dist/row.d.ts +21 -1
  136. package/dist/row.js +2 -1
  137. package/dist/segmented-control.d.ts +3 -1
  138. package/dist/segmented-control.js +1 -1
  139. package/dist/select.d.ts +16 -4
  140. package/dist/select.js +1 -1
  141. package/dist/skeleton.d.ts +3 -1
  142. package/dist/skeleton.js +1 -1
  143. package/dist/spacer.d.ts +3 -1
  144. package/dist/spacer.js +1 -1
  145. package/dist/split-view.d.ts +3 -1
  146. package/dist/split-view.js +24 -5
  147. package/dist/split-view.js.map +1 -1
  148. package/dist/state-banner.d.ts +3 -1
  149. package/dist/state-banner.js +2 -1
  150. package/dist/styles/badge.css +86 -0
  151. package/dist/styles/catalog/components/catalog-detail.css +35 -0
  152. package/dist/styles/catalog/components/catalog-example-stack.css +9 -0
  153. package/dist/styles/catalog/components/catalog-example.css +180 -0
  154. package/dist/styles/catalog/components/catalog-resource-footer.css +37 -0
  155. package/dist/styles/catalog/components/catalog-secondary-sections.css +38 -0
  156. package/dist/styles/catalog/components/catalog-section-list.css +22 -0
  157. package/dist/styles/catalog/components/catalog-sidebar.css +82 -0
  158. package/dist/styles/catalog/components/catalog-stage.css +60 -0
  159. package/dist/styles/catalog/components/catalog.css +22 -0
  160. package/dist/styles/catalog.css +10 -587
  161. package/dist/styles/collapsible-panel.css +132 -4
  162. package/dist/styles/document.css +23 -0
  163. package/dist/styles/foundation.css +21 -7
  164. package/dist/styles/grid.css +10 -0
  165. package/dist/styles/list-header.css +49 -60
  166. package/dist/styles/list-inset-control.css +8 -1
  167. package/dist/styles/list-item.css +6 -0
  168. package/dist/styles/list.css +11 -0
  169. package/dist/styles/nav-stack.css +89 -2
  170. package/dist/styles/pane.css +86 -1
  171. package/dist/styles/resizable-region.css +117 -5
  172. package/dist/styles/row.css +10 -0
  173. package/dist/styles/select.css +39 -7
  174. package/dist/styles/split-view.css +108 -2
  175. package/dist/styles/state-banner.css +0 -27
  176. package/dist/styles/styles.css +1 -0
  177. package/dist/styles/tab-bar.css +6 -1
  178. package/dist/styles/tab-scaffold.css +64 -1
  179. package/dist/styles/text.css +10 -0
  180. package/dist/styles/token-search-field.css +5 -40
  181. package/dist/styles/toolbar-control-group.css +198 -25
  182. package/dist/styles/toolbar-text.css +8 -1
  183. package/dist/styles/toolbar.css +169 -5
  184. package/dist/styles/webawesome.css +70 -9
  185. package/dist/styles/workbench.css +136 -4
  186. package/dist/sunken-panel.d.ts +3 -1
  187. package/dist/sunken-panel.js +1 -1
  188. package/dist/surface-scaffold.d.ts +6 -2
  189. package/dist/surface-scaffold.js +1 -1
  190. package/dist/tab-bar.d.ts +8 -1
  191. package/dist/tab-bar.js +1 -1
  192. package/dist/tab-scaffold.d.ts +3 -1
  193. package/dist/tab-scaffold.js +3 -1
  194. package/dist/tab-scaffold.js.map +1 -1
  195. package/dist/text.d.ts +9 -5
  196. package/dist/text.js +1 -1
  197. package/dist/token-search-field.d.ts +5 -1
  198. package/dist/token-search-field.js +1 -1
  199. package/dist/toolbar-control-group.d.ts +33 -3
  200. package/dist/toolbar-control-group.js +1 -1
  201. package/dist/toolbar-text.d.ts +3 -1
  202. package/dist/toolbar-text.js +1 -1
  203. package/dist/toolbar.d.ts +30 -1
  204. package/dist/toolbar.js +1 -1
  205. package/dist/types-CEsieBZi.d.ts +69 -0
  206. package/dist/value-table.d.ts +6 -2
  207. package/dist/value-table.js +1 -1
  208. package/dist/wire-resizable-regions.js +1 -1
  209. package/dist/wire-sidebar.d.ts +18 -3
  210. package/dist/wire-sidebar.js +95 -14
  211. package/dist/wire-sidebar.js.map +1 -1
  212. package/dist/wire-token-search-fields.js +1 -1
  213. package/dist/workbench.d.ts +3 -1
  214. package/dist/workbench.js +3 -1
  215. package/dist/workbench.js.map +1 -1
  216. package/docs/accessibility.md +56 -9
  217. package/docs/app-layouts.md +73 -0
  218. package/docs/catalog.md +48 -9
  219. package/docs/collapsible-panel.md +81 -8
  220. package/docs/component-contract.md +96 -28
  221. package/docs/component-integration.md +9 -4
  222. package/docs/component-selection.md +36 -15
  223. package/docs/design/templates/list/stack-dark.svg +1 -1
  224. package/docs/design/templates/list/stack.svg +1 -1
  225. package/docs/design/templates/list-dark.svg +1 -1
  226. package/docs/design/templates/list-header/action-dark.svg +1 -1
  227. package/docs/design/templates/list-header/action.svg +1 -1
  228. package/docs/design/templates/list-header/count-dark.svg +1 -1
  229. package/docs/design/templates/list-header/count.svg +1 -1
  230. package/docs/design/templates/list-header/inline-action-dark.svg +1 -1
  231. package/docs/design/templates/list-header/inline-action.svg +1 -1
  232. package/docs/design/templates/list-header/toggle-expanded-dark.svg +1 -1
  233. package/docs/design/templates/list-header/toggle-expanded.svg +1 -1
  234. package/docs/design/templates/list-header-dark.svg +1 -1
  235. package/docs/design/templates/list-header.svg +1 -1
  236. package/docs/design/templates/list.svg +1 -1
  237. package/docs/design/templates/state-banner/info-badge-dark.svg +1 -1
  238. package/docs/design/templates/state-banner/info-badge.svg +1 -1
  239. package/docs/design/templates/state-banner-dark.svg +1 -1
  240. package/docs/design/templates/state-banner.svg +1 -1
  241. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -1
  242. package/docs/design/templates/toolbar/title-and-actions.svg +1 -1
  243. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -1
  244. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -1
  245. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  246. package/docs/design/templates/toolbar-control-group.svg +1 -1
  247. package/docs/design/templates/toolbar-dark.svg +1 -1
  248. package/docs/design/templates/toolbar-text/default-dark.svg +1 -1
  249. package/docs/design/templates/toolbar-text/default.svg +1 -1
  250. package/docs/design/templates/toolbar-text/large-dark.svg +1 -1
  251. package/docs/design/templates/toolbar-text/large.svg +1 -1
  252. package/docs/design/templates/toolbar-text/small-dark.svg +1 -1
  253. package/docs/design/templates/toolbar-text/small.svg +1 -1
  254. package/docs/design/templates/toolbar-text-dark.svg +1 -1
  255. package/docs/design/templates/toolbar-text.svg +1 -1
  256. package/docs/design/templates/toolbar.svg +1 -1
  257. package/docs/design-philosophy.md +6 -5
  258. package/docs/document-baseline.md +31 -0
  259. package/docs/layout.md +86 -21
  260. package/docs/nav-stack.md +8 -0
  261. package/docs/recipes.md +78 -43
  262. package/docs/split-view.md +8 -0
  263. package/docs/tab-scaffold.md +4 -2
  264. package/docs/type-contracts.md +21 -16
  265. package/docs/ui-analyzer.md +19 -0
  266. package/docs/ui-evaluator.md +17 -0
  267. package/docs/ux-demo.md +8 -8
  268. package/docs/webawesome-theme.md +25 -6
  269. package/docs/workbench.md +20 -1
  270. package/evaluator/index.mjs +73 -8
  271. package/llms.txt +61 -23
  272. package/package.json +12 -3
  273. package/ux-demo/recipes/app-shell.tsx +244 -184
  274. package/ux-demo/recipes/collapsible-sidebar.tsx +175 -116
  275. package/ux-demo/recipes/compact-toolbar.tsx +119 -102
  276. package/ux-demo/recipes/composer-form.tsx +79 -69
  277. package/ux-demo/recipes/list-detail-dialog.tsx +120 -136
  278. package/ux-demo/recipes/list-workspace-states.tsx +85 -79
  279. package/ux-demo/recipes/loaders.ts +5 -2
  280. package/ux-demo/recipes/loading-inspector.tsx +151 -134
  281. package/ux-demo/recipes/navigation-sidebar.tsx +93 -80
  282. package/ux-demo/recipes/navigation-stack.tsx +33 -22
  283. package/ux-demo/recipes/types.ts +14 -0
  284. package/ux-demo/recipes/workspace-header.tsx +73 -57
  285. package/dist/chunk-2TYUFZUC.js.map +0 -1
  286. package/dist/chunk-6NDBE2OK.js.map +0 -1
  287. package/dist/chunk-6WTETUZ6.js.map +0 -1
  288. package/dist/chunk-AQFWBG23.js.map +0 -1
  289. package/dist/chunk-B6JYRX3P.js.map +0 -1
  290. package/dist/chunk-CAB6NQ2T.js.map +0 -1
  291. package/dist/chunk-CK6P5AUK.js.map +0 -1
  292. package/dist/chunk-DAO2VFTT.js.map +0 -1
  293. package/dist/chunk-DYBPI5O2.js.map +0 -1
  294. package/dist/chunk-E34TE2ZI.js.map +0 -1
  295. package/dist/chunk-EYALJQQD.js.map +0 -1
  296. package/dist/chunk-GYGM363P.js.map +0 -1
  297. package/dist/chunk-I45QO2IZ.js.map +0 -1
  298. package/dist/chunk-I4V3UDSA.js.map +0 -1
  299. package/dist/chunk-IU4QUY5I.js.map +0 -1
  300. package/dist/chunk-OGNE3OFT.js.map +0 -1
  301. package/dist/chunk-OGTJNHRG.js.map +0 -1
  302. package/dist/chunk-OL4TC3U2.js.map +0 -1
  303. package/dist/chunk-OMSRSL3B.js.map +0 -1
  304. package/dist/chunk-SOK5EUF7.js.map +0 -1
  305. package/dist/chunk-T7DOWS6T.js.map +0 -1
  306. package/dist/chunk-TIGSG6BX.js.map +0 -1
  307. package/dist/chunk-UDHHKZWO.js.map +0 -1
  308. package/dist/chunk-V6GQKOX2.js.map +0 -1
  309. package/dist/chunk-V6HCTCVD.js.map +0 -1
  310. package/dist/chunk-VXFGMDA7.js.map +0 -1
  311. package/dist/chunk-W2FK7ZNY.js.map +0 -1
  312. package/dist/chunk-WN52USHV.js +0 -115
  313. package/dist/chunk-WN52USHV.js.map +0 -1
  314. package/dist/chunk-WSXUPSID.js.map +0 -1
  315. package/dist/chunk-WVLMZ5SG.js.map +0 -1
  316. package/dist/chunk-XKFRMGMW.js.map +0 -1
  317. package/dist/chunk-Y6LYKFXY.js.map +0 -1
  318. package/dist/chunk-ZPWZIVI6.js.map +0 -1
  319. package/ux-demo/recipes/recipes.css +0 -453
@@ -10,7 +10,7 @@ These are normative contracts for package components and consuming applications.
10
10
  - `ListHeader` owns action naming, disabled state, and disclosure `aria-expanded`. Toggle mode supplies one decorative 18px `DisclosureArrow` when `actionIcon` is omitted; it mirrors `expanded` while the native button's accessible name stays stable. The application must update that controlled state and reveal or hide real content. A valid `count` is visually rendered in an `aria-hidden` neutral pill while the required localized `countLabel` becomes part of the owning heading or disclosure button's accessible name; zero remains a real count. Its narrow `triggerAttributes` slot may describe a native popover or controlled-content relationship with `popoverTarget`, `popoverTargetAction`, `aria-controls`, and `aria-haspopup`; the application owns the target surface and its focus/dismissal behavior.
11
11
  - Dispose `wireTabBars` and `wireTokenSearchFields` with their owning view. Disposal cancels queued controlled-render focus restoration, so a torn-down view cannot reclaim focus from its successor.
12
12
  - `wireNavStack` moves focus into every newly active top view and remembers the last focused descendant of each mounted view for pop restoration. Mark a preferred initial heading or control with `data-nav-focus` (and `tabindex="-1"` when it is not normally tabbable); otherwise the helper uses the first enabled focusable descendant, then a programmatically focusable view-container fallback.
13
- - In any `.kui-pane`, menu rows, `ListHeader` actions, and toolbar groups keep a minimum 44px target in both dimensions. A `ListHeader` fills the available inline width and keeps that action at the logical end, including in RTL. Its action visual defaults to 18px through `--kui-list-header-action-icon-size`; do not reduce the target to the visible icon.
13
+ - In any `.kui-pane`, menu rows, `ListHeader` actions, and toolbar groups keep a minimum 44px target in both dimensions. A `ListHeader` fills the available inline width and keeps that action at the logical end, including in RTL. Its action visual defaults to 18px through `--kui-list-header-action-icon-size` inside a fitted 36px hover square; a transparent hit layer extends the pointer target to at least 44 × 44 around that square without moving layout or enlarging the painted surface. Compact headers are 36px tall like the rest of the compact List family, so their action target is 44px wide and clamped to the 36px header height instead of reaching into a neighboring row. Do not reduce the target to the visible icon, and do not force the button itself to 44px.
14
14
  - `LoadingSpinner` is either labeled (`role="img"`) or hidden. Its rotation stops for reduced motion.
15
15
  - `StateBanner` defaults to polite `role="status"`; use `urgency="alert"` only for an attention-requiring failure. Its optional `badge` is persistent inline status/count content beside the title and is announced as part of the banner; keep it terse and do not rely on color alone for its meaning.
16
16
  - `EmptyState` reports busy state through `aria-busy` and never relies on an illustration as its label.
@@ -29,10 +29,23 @@ adding height or spacing. This also applies to `renderSelected`: selected conten
29
29
  stays separate from the control's stable accessible name. Applications do not
30
30
  need shadow-DOM patches or extra label styling.
31
31
 
32
- Use `presentation="toolbar-borderless"` with `size="compact"` inside a
33
- `ToolbarControlGroup`; set `focusRingOwner="group"` when that parent paints the
34
- composed focus ring. `selectedPresentation="icon-only"` hides only the visible
35
- selected label—the required `label` or `ariaLabel` still names the combobox.
32
+ Use `presentation="toolbar-borderless"` inside a `ToolbarControlGroup`, with
33
+ `size="compact"` in a compact group; set `focusRingOwner="group"` when that
34
+ parent paints the composed focus ring. `selectedPresentation="icon-only"` hides
35
+ only the visible selected label—the required `label` or `ariaLabel` still names
36
+ the combobox. An icon-only trigger keeps its disclosure caret: the selected icon
37
+ and caret form one pill with the same geometry as a popup-menu `wa-dropdown`
38
+ trigger (8px inline padding on both sides, 12px from the icon to the caret, a
39
+ 40px default or 32px compact control height), so the caret still signals a
40
+ choice list. Inside a group the trigger is inset evenly on every side (2px in a
41
+ default group, 1px in a compact one) with a concentric radius; the group grows
42
+ to the trigger's width, so a single group is wider than it is tall and its
43
+ focus ring follows the pill while focused and while its listbox is open.
44
+ Beside other controls in one group, the icon-only trigger is one more segment:
45
+ it keeps the group's gap and its siblings' block inset, height, item radius,
46
+ and hover pill. In a group with per-control rings (`focusRing="control"`, the
47
+ default) it paints the same ring as its sibling buttons, even when
48
+ `focusRingOwner="group"` was set, because no group ring would show focus.
36
49
  Navigation selects can use intrinsic `presentation="navigation"` plus
37
50
  `labelMaxWidth` for component-owned ellipsis. These props own the control's
38
51
  appearance; its parent continues to own outer placement.
@@ -85,13 +98,38 @@ popover or context menu.
85
98
 
86
99
  ## ResizableRegion
87
100
 
101
+ Set `responsiveFillAt="narrow"` or `"compact"` when a responsive composition
102
+ shows the region as its only inline track. The component then owns its
103
+ full-width geometry and removes its dormant separator at that container
104
+ breakpoint; the application owns which pane is visible.
105
+
88
106
  The handle exposes separator role, orientation, name, minimum, maximum, and current values. `wireResizableRegions()` adds:
89
107
 
90
108
  - the axis arrow keys in 16 px steps by default;
91
109
  - Shift+Arrow in 64 px steps;
92
110
  - Home and End for minimum and maximum;
93
111
  - primary-pointer drag with clamping;
94
- - preview callbacks while dragging and one commit callback on release.
112
+ - a visible maximum: when a parent clamps the track below the requested size
113
+ (a `max-width` stage, a narrow container), pointer and keyboard resizing stop
114
+ at the size actually shown, and the handle announces that size in
115
+ `aria-valuenow` and that bound in `aria-valuemax`. Focusing the handle
116
+ reports a stale larger committed size as the shown size without committing
117
+ it, so the first key press moves the visible separator. The reported values
118
+ also stay current at rest: a `ResizeObserver` on each region and its parent
119
+ re-clamps them when the space changes, and a `MutationObserver` re-clamps
120
+ them after a re-render writes the rendered props back, so assistive
121
+ technology never reads an unclamped size between interactions (the
122
+ disposer disconnects both observers);
123
+ - a whole hit target and focus ring at a clamped edge: when the separator has
124
+ less room past it than the handle's 10px overhang (a parent clamps the track
125
+ to its own edge), the region carries `data-handle-inset` and the handle sits
126
+ wholly inside it, so a clipping ancestor cannot cut off half the handle or
127
+ its focus ring. The ring is drawn inside the handle's box, so the region's
128
+ ends meeting a clipping edge never shave it either;
129
+ - preview callbacks while dragging and one commit callback on release;
130
+ - a live size that also resizes slide-motion content, so the content tracks the
131
+ separator during a drag or key press instead of waiting for the app to
132
+ re-render the committed size.
95
133
 
96
134
  The application owns persistence and collapsed/expanded policy. Keep the last expanded size outside the component and restore it when reopening. An optional `handleIcon` replaces only decorative dormant content; it must not contain controls or interactive roles because the separator remains the sole focus and interaction owner.
97
135
 
@@ -129,10 +167,15 @@ and — via `position` and `--kui-floating-toolbar-inset` — where it sits.
129
167
  ## Tabs
130
168
 
131
169
  `AppTab` renders one controlled tab. `TabBar` supplies the containing tab list,
132
- fixed leading/trailing regions, and a horizontally scrollable strip.
170
+ fixed leading/trailing/end regions, and a horizontally scrollable strip.
133
171
  Use `TabBar.presentation` for rail, segmented, or inspector chrome,
134
172
  `allocation="fill"` when peers should divide the strip, and
135
173
  `trailingPlacement="adjacent"` when an action belongs beside the final tab.
174
+ When that local action must coexist with a workspace-level action, put the
175
+ workspace action in `end`; TabBar keeps the adjacent action immediately after
176
+ the shrinkable/scrolling tabs and pins `end` at the far edge. Leading,
177
+ trailing, and end actions remain visible and do not shrink. The component owns
178
+ that geometry; applications must not override its anatomy to create the split.
136
179
  `AppTab` provides compact 32px, segmented, truncating-label, and icon-only
137
180
  presentations. Icon-only tabs keep the required `name` as the tab button's
138
181
  accessible name while visually hiding the duplicate label. These props own
@@ -237,9 +280,13 @@ standard per-side collapse/expand glyph (`PanelLeft*` / `PanelRight*` /
237
280
  `PanelBottom*`); place a collapse toggle inside the panel and an expand toggle in
238
281
  an always-visible location so it is reachable while collapsed. `wireSidebar()`
239
282
  moves focus into the panel when it opens and restores it to the trigger when it
240
- closes. When a `deviceClass()` reports `compact`, an open panel becomes an overlay
283
+ closes; a trigger inside the now-hidden panel hands focus to the panel's toggle
284
+ outside it instead, so an app may render the expand toggle only while the panel
285
+ is collapsed. When a `deviceClass()` reports `compact`, an open panel becomes an overlay
241
286
  with a dismissable backdrop, Escape and backdrop-click collapse it, and Tab is
242
- trapped within it (the ARIA dialog pattern). The application owns each `collapsed`
287
+ trapped within it (the ARIA dialog pattern). That modal state only ever begins
288
+ from a user action: at wire-up on a compact device and on a wide → compact
289
+ crossing, every panel starts collapsed, so no page load or resize traps focus. The application owns each `collapsed`
243
290
  signal, the panels, their sizes, and content; the wire may persist the collapsed
244
291
  state per panel.
245
292
 
@@ -19,6 +19,79 @@ Derive responsiveness from `deviceClass()`: `compact` (a handset or portrait
19
19
  tablet) means "one pane at a time"; `atLeast('tablet')` / `atLeast('desktop')`
20
20
  gate the roomier presentations.
21
21
 
22
+ Full-height shells retain `height: 100%` so they remain embeddable. Give a
23
+ top-level shell a definite height chain by importing `@kerfjs/ui/document.css`
24
+ and applying `.kui-app-root` to the direct mount container; see the
25
+ [document baseline](document-baseline.md). Do not replace an embeddable shell's
26
+ height with `100dvh`.
27
+
28
+ ## Safe areas
29
+
30
+ On a device with unsafe areas (a notch, rounded corners, a home indicator) and a
31
+ page that opts into them with `<meta name="viewport" content="…, viewport-fit=cover">`,
32
+ the layouts handle the insets for you. There is nothing to configure:
33
+
34
+ - **Surfaces paint edge to edge.** Pane and panel backgrounds, separators, and
35
+ dividers run through the unsafe area. A collapsible rail or drawer grows by
36
+ the inset it reaches, so its content keeps its configured width or height.
37
+ - **Content is padded on the sides it touches.** Inline insets pad the sides a
38
+ region actually reaches. Block insets become padding _inside_ the scroll
39
+ container (with matching `scroll-padding`), so content scrolls under the
40
+ status bar or home indicator but its first and last items can always be
41
+ scrolled clear.
42
+ - **Interior edges get nothing.** A `Workbench` center next to an expanded rail,
43
+ a `SplitView` detail beside its list, or a `NavStack` view under its chrome is
44
+ not inset on that edge. Collapse the rail and the center picks up the edge.
45
+ - **No double inset.** Whichever region applies an inset clears it for its
46
+ descendants. A layout region whose only child is a `Pane` or another layout
47
+ lets that child own the insets. A `Pane` header or footer whose only child is
48
+ a `Toolbar` hands the inline insets to the toolbar, so the toolbar's dividers
49
+ still reach the edge.
50
+
51
+ A plain `Pane` follows the same rules. It assumes it may touch every screen edge
52
+ unless its layout says otherwise. When your own markup puts panes side by side,
53
+ say which sides each pane reaches with `safeAreaEdges`:
54
+
55
+ ```tsx
56
+ <div class="app-columns">
57
+ <Pane label="Library" safeAreaEdges={['block-start', 'block-end', 'inline-start']}>…</Pane>
58
+ <Pane label="Reader" safeAreaEdges={['block-start', 'block-end', 'inline-end']}>…</Pane>
59
+ </div>
60
+ ```
61
+
62
+ An app bar or bottom bar that sits directly at a screen edge, outside any
63
+ `Pane` header or footer, claims its edges the same way. A `Toolbar` only pads
64
+ the inline edges an owner hands it until you list the sides it reaches:
65
+
66
+ ```tsx
67
+ <List fill>
68
+ <Toolbar
69
+ label="App bar"
70
+ safeAreaEdges={['block-start', 'inline-start', 'inline-end']}
71
+ leading={<ToolbarText text="Atlas" />}
72
+ />
73
+ <Row gap="none" flex>…panes that list block-end and their outer side…</Row>
74
+ </List>
75
+ ```
76
+
77
+ Each claimed side pads by the edge context, or the full device inset when
78
+ nothing routes that edge, while the bar's surface and divider still reach the
79
+ screen edge. A claimed toolbar inside a `Pane` header or footer adds nothing
80
+ extra, because the pane already owns those edges.
81
+
82
+ Pass `safeAreaEdges={[]}` for a pane that never sits at a screen edge. For a
83
+ region you own, set `--kui-edge-inset-block-start`, `--kui-edge-inset-block-end`,
84
+ `--kui-edge-inset-inline-start`, or `--kui-edge-inset-inline-end` to `0px` on the
85
+ region for each edge it does not reach, and kerf descendants follow. A
86
+ `CollapsiblePanel` does this for its direct flex siblings automatically. A
87
+ panel wrapped in your own grid cell cannot see its siblings, so route those
88
+ edges yourself.
89
+
90
+ The device insets come from `--kui-safe-area-block-start`, `-block-end`,
91
+ `-inline-start`, and `-inline-end`, which default to `env(safe-area-inset-*)`.
92
+ Override them on `:root` to reserve room for app-owned chrome or to simulate a
93
+ device in tests.
94
+
22
95
  ## Decision matrix
23
96
 
24
97
  | Situation | Layout | Device threshold |
package/docs/catalog.md CHANGED
@@ -23,8 +23,8 @@ npm install @kerfjs/ui # kerfjs is a peer; @kerfjs/ui/select/register is needed
23
23
 
24
24
  - **`sections`** — category-grouped entries: `{ category, entries: [{ id, name,
25
25
  description?, tags?, resources?, related? }] }`. Each entry becomes a sidebar
26
- `ListItem` under a `ListHeader` for its category. Short `tags` render as quiet
27
- trailing pills for decision metadata such as `Discouraged`.
26
+ `ListItem` under a `ListHeader` for its category. Short `tags` render through
27
+ the row's quiet status treatment for decision metadata such as `Discouraged`.
28
28
  - **`content`** — the rendered preview for the active entry. Keep a map of `id →
29
29
  () => SafeHtml` in your app and call `renderers[active]()` in your render.
30
30
  - **`brand`** — `{ title, subtitle?, logoUrl? }` for the sidebar header.
@@ -54,10 +54,25 @@ chosen component.
54
54
 
55
55
  Classify every entry before rendering it:
56
56
 
57
- | Entry kind | Preview purpose | Geometry overlay |
58
- | --------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
59
- | Focused component | Show one public component, its meaningful variants, and adverse states. | `true`; the overlay inspects each selected specimen. |
60
- | Composition or recipe | Show several components cooperating as one product surface. | `false`; child geometry remains unmarked so the composition can be read as a whole. |
57
+ | Entry kind | Preview purpose | Geometry overlay |
58
+ | --------------- | ------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
59
+ | **Component** | Show one public component, its meaningful variants, and adverse states. | `true`; the overlay inspects each selected specimen. |
60
+ | **Composition** | Show components commonly used together in one focused container or surface. | `false`; child geometry remains unmarked so the composition can be read as a whole. |
61
+ | **Recipe** | Teach the recommended production approach to a specific task, including ownership and application glue. | `false`; child geometry remains unmarked so the complete approach can be read. |
62
+
63
+ These three kinds are exhaustive. Theme/foundation showcases use a functional
64
+ category, third-party specimens use source metadata, and state galleries retain
65
+ the kind of the thing they demonstrate; none needs another demo kind.
66
+
67
+ Organize the catalog by functional group, with groups in descending product
68
+ importance. Within every group, list all component demos before all composition
69
+ demos. Order each kind by descending importance where a meaningful distinction
70
+ exists, then alphabetically by display name to break ties. Put recipes after all
71
+ component/composition groups, ordered by the same importance-then-alphabetical
72
+ rule. Treat source order as the authored importance order when no explicit rank
73
+ field exists. Show a `Composition` tag rather than repeating the word in a
74
+ composition's display name; the generic `Catalog` accepts that through the
75
+ entry's `tags` array.
61
76
 
62
77
  Keep `geometryOverlay` present and compute it from the active entry. Do not make
63
78
  it a permanent catalog-wide `true`, and do not give individual specimens their
@@ -76,6 +91,18 @@ own overlay implementation.
76
91
  generated note, then one specimen or one intentionally coupled specimen
77
92
  cluster. Use one row per variant/state; do not hand-author the helper's private
78
93
  classes.
94
+ - Focused demos do not import local stylesheets, use inline `style`, or invent
95
+ styling-only `demo-*` classes. Prefer a public Kerf component, then a Web
96
+ Awesome surface when Kerf has no equivalent, then ordinary semantic HTML.
97
+ Use `CatalogExample.viewport` for bounded specimen width, height, frame,
98
+ surface, overflow, responsive visibility, and public custom-property values;
99
+ keep those constraints catalog-owned instead of recreating a demo stylesheet.
100
+ Component heights run `short` (220px) through `tall` (360px); frame an app
101
+ shell, whole-screen layout, or other application-sized recipe with
102
+ `height: "app"` (592px), and give its root `fill` rather than a frame `Pane`.
103
+ An app frame stands in for the screen: it is the containing block for
104
+ screen-fixed chrome, so a compact `CollapsiblePanel` overlay, its backdrop,
105
+ and restore controls dock to the frame's edges rather than the page's.
79
106
  - The specimen is an immediate child of `CatalogExample`. A focused component
80
107
  row should place the component root there, without a decorative card or
81
108
  spacing wrapper. A composition row may place the composition root there.
@@ -128,6 +155,9 @@ from structurally widened or JavaScript objects. Do not copy the helpers'
128
155
  private `kui-catalog-*` classes into preview markup.
129
156
 
130
157
  The app owns entry ids, `kind`, routing, sources, relationships, and test hooks.
158
+ `Catalog.stageRootAttributes` places app-owned `data-*` state on the
159
+ catalog-owned preview stage without an extra wrapper; `data-catalog-stage`
160
+ remains protected.
131
161
  The helpers own their structural markers, label/note anatomy, alignment marker,
132
162
  and private classes. Component metadata such as margin/border/padding ownership
133
163
  lives in `component-catalog.json`; do not infer or overwrite it from overlay
@@ -189,7 +219,13 @@ facts that source can prove without guessing at rendered intent:
189
219
  are not empty, and composition routes do not add redundant skip markers;
190
220
  - every `@kerfjs/ui` import is a published package export and relative imports
191
221
  do not reach into `ui/src`;
192
- - demo JSX does not copy private `kui-catalog-*` structural classes; and
222
+ - focused demos have no relative stylesheet imports, inline style attributes,
223
+ or styling-only `demo-*`, `wa-demo-*`, and token-search class names;
224
+ - demo JSX does not copy private `kui-catalog-*` structural classes;
225
+ - recipe sources (checked by `npm run check:recipes` with the same shared
226
+ analyzer) import no stylesheet other than public `@kerfjs/ui` CSS, set no
227
+ inline style, and name only published layout and Web Awesome classes; each
228
+ recipe declares its catalog frame through an exported `presentation`; and
193
229
  - the shell derives both geometry-overlay enablement and documented demo mode
194
230
  from the active entry's source and kind.
195
231
 
@@ -202,7 +238,7 @@ stage geometry is itself the reviewed specimen. Add the narrow waiver to
202
238
  [`catalog-conformance-exceptions.json`](../ux-demo/catalog-conformance-exceptions.json)
203
239
  with the exact route, source file, stable diagnostic ids, a substantive reason,
204
240
  and the reviewing `KF-*` ticket. Only helper/metadata rules are waivable;
205
- private imports, private markup, empty examples, composition overlay drift, and
241
+ private imports, local styling, private markup, empty examples, composition overlay drift, and
206
242
  shell-mode drift always fail. The gate rejects duplicate, malformed, unused,
207
243
  and stale exceptions, so delete a waiver when its route adopts the standard
208
244
  helpers.
@@ -262,7 +298,10 @@ const entries: DemoEntry[] = [
262
298
  const sections: CatalogSection[] = [
263
299
  {
264
300
  category: "Examples",
265
- entries: entries.map(({ kind: _kind, ...entry }) => entry),
301
+ entries: entries.map(({ kind, ...entry }) => ({
302
+ ...entry,
303
+ tags: kind === "composition" ? ["Composition"] : undefined,
304
+ })),
266
305
  },
267
306
  ];
268
307
 
@@ -37,25 +37,85 @@ Import the panel CSS (`@kerfjs/ui/collapsible-panel.css`) alongside `foundation.
37
37
  is the app's: put a collapse toggle in the panel's own header and an expand toggle
38
38
  somewhere always-visible (a toolbar) so it is reachable while collapsed.
39
39
  - **`wireSidebar(root, { panels, deviceClass?, storage? })`** — the interaction
40
- semantics. Each `panels` entry is `{ id, collapsed, toggleAction, storageKey? }`.
41
- It:
40
+ semantics. Each `panels` entry is
41
+ `{ id, collapsed, toggleAction, storageKey?, inlineCollapsed? }`. It:
42
42
  - **toggles** the panel's `collapsed` signal when any `[data-action=toggleAction]`
43
43
  button is clicked, and remembers the trigger;
44
44
  - **manages focus** — moves focus into the panel when it opens, and restores it
45
- to the trigger when it closes;
45
+ to the trigger when it closes. When that trigger is the collapse toggle inside
46
+ the now-hidden panel, focus goes to the panel's toggle outside it instead, so
47
+ an app may render its expand toggle only while the panel is collapsed (one
48
+ control per action, as the recipe does);
46
49
  - **presents a compact overlay** when `deviceClass.compact` is true (pass a
47
50
  `deviceClass()` signal): the open panel floats over the content with a
48
51
  dismissable backdrop, Escape and backdrop-click collapse it, and Tab is trapped
49
- within the panel (the ARIA dialog pattern);
52
+ within the panel (the ARIA dialog pattern). The overlay and its backdrop are
53
+ fixed to the screen; an ancestor that establishes a containing block for
54
+ fixed descendants (such as the catalog's `height: "app"` frame) docks them to
55
+ its edges instead;
50
56
  - accepts `compactPresentation: "hidden"` when a compact application replaces
51
57
  the panel with different navigation instead of overlaying it;
58
+ - **starts every compact overlay closed** (see
59
+ [Compact initial state](#compact-initial-state));
52
60
  - keeps compact overlays exclusive by default, collapsing another open panel
53
61
  when a new one opens (`exclusiveCompact: false` opts out);
54
- - **persists** the collapsed state to `storage` (default `localStorage`) under
55
- `storageKey`, seeding the signal on wire-up.
62
+ - **persists** the inline collapsed state to `storage` (default
63
+ `localStorage`) under `storageKey`, seeding the signal on wire-up. An
64
+ overlay's open/closed state is never persisted;
65
+ - **owns the root's `data-collapsible-responsive` / `data-collapsible-overlay`
66
+ attributes and the injected backdrop.** Your app may re-render (and morph)
67
+ the wired root for reasons that never touch a panel signal — a nav
68
+ selection, say — and the morph drops attributes and nodes its template
69
+ doesn't emit. The wire observes the root and re-applies them before the
70
+ next paint, so don't render these attributes yourself.
56
71
 
57
72
  Returns a disposer. Retain it and call it on teardown.
58
73
 
74
+ ## Compact initial state
75
+
76
+ A compact overlay is transient chrome the user summons, like a slide-over
77
+ sidebar on a phone: it covers the content, traps focus, and must be dismissed.
78
+ It therefore opens **only on a user action** (a toggle, or the app setting the
79
+ signal in response to one), never by itself:
80
+
81
+ - **Wire-up on a compact device.** Every panel starts collapsed, whatever its
82
+ signal, `inlineCollapsed`, or stored choice says. No backdrop, focus trap, or
83
+ Escape handling is active until the user opens a panel.
84
+ - **Wide → compact crossing.** Open panels collapse; nothing covers the page.
85
+ - **Compact → wide crossing.** Each panel returns to its remembered inline state
86
+ (open or collapsed), whatever the user did with the overlay meanwhile.
87
+ - **Disposal** hands the inline state back to the signals.
88
+ - **Persistence** records only the inline choice. A stored "open" rail restores
89
+ inline on a wide screen and is remembered, not opened, on a compact one.
90
+
91
+ These presentation changes never move focus, except to rescue focus that a
92
+ collapse would strand inside the hidden panel (it returns to the panel's last
93
+ trigger). `compactPresentation: "hidden"` leaves the signals alone.
94
+
95
+ `wireSidebar` runs after the first render, so an app that seeds an open inline
96
+ default would render the rail open for that first frame on a compact device.
97
+ Seed the signal from the device class instead, and declare the inline default
98
+ with `inlineCollapsed` so a later wide crossing still opens it:
99
+
100
+ ```ts
101
+ const device = deviceClass();
102
+ const navCollapsed = signal(device.value.compact);
103
+ // …
104
+ wireSidebar(app, {
105
+ panels: [
106
+ {
107
+ id: "nav",
108
+ collapsed: navCollapsed,
109
+ toggleAction: "toggle-nav",
110
+ inlineCollapsed: false,
111
+ },
112
+ ],
113
+ deviceClass: device,
114
+ });
115
+ ```
116
+
117
+ A stored inline choice takes precedence over `inlineCollapsed`.
118
+
59
119
  ## Example
60
120
 
61
121
  ```tsx
@@ -68,8 +128,10 @@ import {
68
128
  import { wireSidebar } from "@kerfjs/ui/wire-sidebar";
69
129
  import "@kerfjs/ui/collapsible-panel.css";
70
130
 
71
- const navCollapsed = signal(false);
72
131
  const device = deviceClass();
132
+ // Start collapsed on a compact device; `inlineCollapsed` keeps the open inline
133
+ // default for wide screens.
134
+ const navCollapsed = signal(device.value.compact);
73
135
 
74
136
  const app = document.querySelector("#app")!;
75
137
  mount(app, () => (
@@ -111,12 +173,22 @@ const stop = wireSidebar(app, {
111
173
  collapsed: navCollapsed,
112
174
  toggleAction: "toggle-nav",
113
175
  storageKey: "app.nav-collapsed",
176
+ inlineCollapsed: false,
114
177
  },
115
178
  ],
116
179
  deviceClass: device,
117
180
  });
118
181
  ```
119
182
 
183
+ ## Safe areas
184
+
185
+ A panel grows by the unsafe inset of the edge it docks to and pads its content
186
+ for every edge it touches except its interior edge. While expanded inline, it
187
+ also clears that edge for its direct flex siblings, so a neighboring pane gains
188
+ the inset when the panel collapses. When you wrap the panel in your own grid
189
+ cell, set `--kui-edge-inset-*: 0px` on the sibling regions yourself (or pass
190
+ `safeAreaEdges` to a sibling `Pane`). See [Choosing an app layout › Safe areas](app-layouts.md#safe-areas).
191
+
120
192
  ## When to use which
121
193
 
122
194
  - One or two independent rails / a drawer you place yourself → **`CollapsiblePanel` +
@@ -138,5 +210,6 @@ persistence: [open it](../ux-demo/?component=recipe-collapsible-sidebar) or read
138
210
  [`recipes.md`](recipes.md#collapsible-sidebar) · [TSX source](../ux-demo/recipes/collapsible-sidebar.tsx).
139
211
  It is covered end-to-end across Chromium, Firefox, and WebKit by
140
212
  `tests/browser/collapsible-sidebar-recipe.spec.ts` (collapse/expand, focus
141
- move/restore, the compact overlay + Escape/backdrop dismiss, and the Tab trap),
213
+ move/restore, the compact overlay + Escape/backdrop dismiss, the Tab trap, and
214
+ the compact initial state across first load and wide/compact crossings),
142
215
  alongside the component/wire unit tests.
@@ -36,6 +36,19 @@ bounded `items[].property` form. Consumer catalogs can publish the same facts;
36
36
  ESLint, `kerf-ui-analyze`, and `kerf-ui-doctor` then issue the same stable
37
37
  `KUI-L013`–`KUI-L017` feedback for application and third-party components.
38
38
 
39
+ `wiring.stateAttributes` lists the **wiring-owned state attributes**: the
40
+ `data-*` attributes a wiring helper writes at runtime, such as
41
+ `data-handle-inset` and `data-resizing`, which `wireResizableRegions` puts on a
42
+ `ResizableRegion`. Each entry names the attribute, the element that carries it
43
+ (`on`), the `helper` that writes it, and what it means. An application must not
44
+ render, remove, or treat these attributes as its own state; tools use the list
45
+ to tell them apart from app-authored attributes. They are declared once, on the
46
+ helper's wiring item in the v1 catalog, and v2 flattens them. `check:catalog`
47
+ reads each `wire*` helper's source and fails when its declarations differ from
48
+ the `data-*` attributes it actually writes. First-party entries always carry
49
+ the list, empty when their helpers write none; it is optional in extension
50
+ catalogs.
51
+
39
52
  The generator starts with permissive defaults. An `any` mode records that the
40
53
  catalog has no defensible prohibition; it does not claim every composition is
41
54
  recommended. Objective rules live in
@@ -54,6 +67,21 @@ explicitly. Zones without that binding remain guidance and are never guessed
54
67
  from their ids. Static tools inspect only visible JSX structure and leave
55
68
  dynamic `SafeHtml` expressions to runtime and accessibility evaluation.
56
69
 
70
+ ### Native named-slot assignment
71
+
72
+ Every public visual component with one stable conceptual root accepts an
73
+ explicit `slot?: string` prop and forwards it to that root on every render
74
+ branch. Use it when a Kerf component is a light-DOM child of a custom element,
75
+ for example `StateBanner({ title: 'Ready', slot: 'status' })`. Omitting the prop
76
+ omits the attribute.
77
+
78
+ This contract does not open a generic native-attribute bag: components still
79
+ accept only their documented props and narrow extension-attribute objects.
80
+ `ResizableRegion` and `CollapsiblePanel` are deliberately excluded because each
81
+ can return a region plus a sibling restore control, leaving no single root that
82
+ can truthfully own native slot assignment. Their single-root controls and all
83
+ alternate-root branches of supported components retain the contract.
84
+
57
85
  V1 consumers continue unchanged. V2 consumers read v1 for selection/delivery
58
86
  and v2 for composition. `npm run catalog:sync` projects every new component,
59
87
  recipe, and supported Web Awesome entry, and the completeness gate prevents
@@ -159,7 +187,7 @@ claim Kerf delivery paths for them.
159
187
  - Applications own signals/stores, product copy, domain-state mapping, persistence, routing, permissions, and transport.
160
188
  - Actions are `data-action` strings. Wire them at a stable root with `delegate()` or `delegateActions()` and retain the disposer.
161
189
  - A reusable component never owns per-instance mutable module state.
162
- - Consumers style through `--kui-*` semantic tokens and public component classes. Foundation tokens provide opinionated neutral, brand/info, non-status pop, success, warning, and danger fill/border/foreground roles. Stateful components expose local override variables; prefer an equivalent prop or token before writing a selector.
190
+ - Consumers configure presentation through component props and documented `--kui-*` semantic tokens. Foundation tokens provide opinionated neutral, brand/info, non-status pop, success, warning, and danger fill/border/foreground roles. Application stylesheets must not select package component classes or descendants. If genuinely new structure needs CSS, encapsulate that structure and its stylesheet in an application-owned component; configure any nested Kerf components through their public APIs.
163
191
 
164
192
  `ListItem.rootAttributes`, `ListActionRow.rootAttributes`,
165
193
  `ListHeader.rootAttributes`, `AppTab.rootAttributes`,
@@ -194,23 +222,29 @@ visual anchor.
194
222
  ### Public CSS anatomy
195
223
 
196
224
  The `publicClasses` array on each entry in
197
- [`component-catalog.json`](../ai/component-catalog.json) is the exact supported
198
- anatomy boundary. A scoped selector may join documented public classes, such as
199
- `.workspace .kui-toolbar .kui-toolbar__trailing`, when composition-specific
200
- layout cannot be expressed by a prop or token. A class being public does not
201
- make copied component markup an invocation or transfer state and accessibility
202
- ownership to the application.
225
+ [`component-catalog.json`](../ai/component-catalog.json) identifies stable
226
+ anatomy for diagnostics, tooling, browser assertions, and package-owned
227
+ composition. It is not an application customization API. Applications choose
228
+ documented props and tokens; package components may compose other components
229
+ only through those public contracts, never by styling a child's classes from a
230
+ parent stylesheet.
203
231
 
204
232
  The v2 composition catalog additionally requires `boundaries.rootClass` to be
205
233
  either one exact member of `publicClasses` or `null` when the entry has no
206
234
  rendered class root. Runtime geometry tooling uses this explicit field; array
207
235
  order never implies root ownership.
208
236
 
209
- Do not select a component's descendant by element name, id, attribute alone, or
210
- an unlisted implementation class. Selectors such as `.kui-state-banner span`,
211
- `.kui-list-item [data-state]`, and `.kui-list-item .local-label` depend on
212
- private structure. If no prop, token, or cataloged class expresses a recurring
213
- need, request a supported hook instead of inferring one from rendered markup.
237
+ Do not select a component root or descendant by class, element name, id, or
238
+ attribute. Selectors such as `.workspace .kui-toolbar`,
239
+ `.kui-state-banner span`, and `.kui-list-item [data-state]` cross the ownership
240
+ boundary. If no prop or token expresses a recurring need, add the configuration
241
+ to the owning component instead of inferring it from rendered markup.
242
+
243
+ `Badge` owns the complete visual treatment for compact status, count, category,
244
+ and metadata labels. Configure its semantic `tone`, `appearance`, `shape`, and
245
+ `size`; do not recreate or override badge anatomy in an application or consuming
246
+ component. Use `label` when visible content is abbreviated and `ariaHidden` only
247
+ when a surrounding component already includes the same value in its accessible name.
214
248
 
215
249
  `ListItem` renders its leading icon and nested SVG at a root-scaled 18px by
216
250
  default while retaining the row's 44px minimum interactive target. Multiline
@@ -240,9 +274,8 @@ the logical end while the visible glyph defaults to 18px through
240
274
 
241
275
  `StateBanner` exposes instance-level `--kui-state-banner-background`,
242
276
  `--kui-state-banner-border`, `--kui-state-banner-foreground`,
243
- `--kui-state-banner-detail`, `--kui-state-banner-badge-background`,
244
- `--kui-state-banner-badge-foreground`, and action background variables. Its optional
245
- badge is a compact pill beside the title and follows the banner tone by default. Its five built-in
277
+ `--kui-state-banner-detail` and action background variables. Its optional
278
+ badge composes `Badge` beside the title and follows the banner tone by default. Its five built-in
246
279
  tones can be rethemed globally with
247
280
  `--kui-state-banner-{tone}-{background|border|foreground}`. Toolbar control,
248
281
  segmented-control, app-tab, and tab-bar colors likewise use their public
@@ -286,8 +319,10 @@ props. Its opaque string brand improves authoring correctness; it is not a CSS
286
319
  sanitizer or security boundary.
287
320
  `Row.flex` and `List.flex` likewise accept their boolean default, finite keywords, or `CssFlex`
288
321
  from `flex(grow, shrink, basis)`. `Skeleton` width/height use `CssSize`, radius
289
- uses `CssLength`, and `SelectChoice.color` uses `CssColor` from `uiColor()` or
290
- `colorVar()`. Never exchange these property grammars. List rows expose
322
+ uses `CssLength`, and `SelectChoice.color` uses `CssForegroundColor` from
323
+ `uiColor()` with a foreground token such as `success-on-quiet`, or from
324
+ `foregroundColorVar()`. Fill tokens such as `success` are pale background tints
325
+ and do not type-check there. Never exchange these property grammars. List rows expose
291
326
  `className`, public tokens, and semantic props instead of unrestricted `style`
292
327
  declarations.
293
328
  `.kui-content` gives major children 24px vertical separation.
@@ -299,21 +334,39 @@ changing geometry. `.kui-content-item--pill` selects the 22px radius.
299
334
  text. Each group reserves `calc(2px + remify(42px))`, or 44px, with 8px between
300
335
  groups and inside items. For an ordinary icon/action control inside a group, use
301
336
  a plain `<button>` — the group styles `> button` fully, and it keeps the group
302
- free of a Web Awesome dependency and shadow DOM. Reach for `wa-button` only when
303
- you need a Web Awesome feature, chiefly the `slot="trigger"` button of a
304
- `wa-dropdown` popup menu. For a compact mixed-content group, set
305
- `nestedDropdown`; a text-and-caret trigger grows to its intrinsic width while an
306
- icon-only trigger can remain square. Compact mixed controls retain the standard
337
+ free of a Web Awesome dependency and shadow DOM. Use `ToolbarActionLink` when
338
+ the action must retain native anchor navigation; the group owns its geometry,
339
+ hover, and focus treatment. Set `overflow="scroll"` when a row of controls must
340
+ stay inside the available toolbar width. Set `visibility="compact-only"` for a
341
+ group that replaces wider toolbar controls below the Toolbar's compact container
342
+ breakpoint; do not hide the group with an application class. Reach for
343
+ `wa-button` only when you need
344
+ a Web Awesome feature, chiefly the `slot="trigger"` button of a `wa-dropdown`
345
+ popup menu. For a compact mixed-content group, set `nestedDropdown`; a
346
+ text-and-caret trigger grows to its intrinsic width while an icon-only trigger
347
+ can remain square. A compact icon `<button>` is a 32px circle, so a selected
348
+ compact segment never becomes an oval. An icon-only `Select` joins a group as
349
+ one more segment with its siblings' inset, radius, hover, and focus ring; only a
350
+ lone Select grows the group to its icon-and-caret pill. Set `menuInset` to configure the dropdown menu surface
351
+ without application `::part(menu)` CSS. Compact mixed controls retain the standard
307
352
  item padding, omit an internal separator, and let the raised selected item paint
308
- over the outer border instead of shrinking to an inset highlight. The short text
309
- trigger has an engine-stable 66px floor because WebKit
310
- does not include the shadow caret in the custom-element host's intrinsic width;
311
- raise `--kui-toolbar-dropdown-trigger-width` for longer localized copy.
353
+ over the outer border instead of shrinking to an inset highlight. The Web
354
+ Awesome trigger's shadow base owns max-content sizing, so icons, localized text,
355
+ and the shadow caret determine the width without a fixed host measurement.
356
+ The enclosing `Toolbar` owns zone alignment and responsive topology: use
357
+ `centerAlign="stretch"` when the center group should consume its track, and
358
+ choose `responsive="stack"` with `responsiveAt="compact" | "narrow"`,
359
+ `responsive="wrap"` for a heading whose title must stay whole, or
360
+ `responsive="center-priority"` instead of selecting its zone classes from a
361
+ parent stylesheet. No policy clips an action: the trailing zone, and a stacked
362
+ center zone, wrap whole groups onto another row.
312
363
  `ListHeader` similarly separates its dormant title and
313
364
  optional count or badge from its optional 44px action. Use the mutually
314
365
  exclusive `count`/`countLabel` pair for non-negative safe-integer section
315
366
  quantities; reserve `badge` for non-count `SafeHtml`. Do not concatenate counts
316
- into the section label. Do not add padding to pane shells,
367
+ into the section label. Set `width="content"` to shrink-wrap while retaining
368
+ normal header geometry; `inline` is the separate zero-outer-geometry mode. Do
369
+ not add padding to pane shells,
317
370
  double child-owned geometry with wrapper insets, or create competing scroll
318
371
  owners. The [layout contract](./layout.md) lists the public roles and tokens.
319
372
 
@@ -354,6 +407,14 @@ render, hands in its own via `collapsible.signals`, drives it through
354
407
  `handle.open`/`handle.close`, or disables any individual behavior — so transient
355
408
  UI is consistent by default without every app reinventing it.
356
409
 
410
+ A public subpath that exposes several visual components is a folder-backed
411
+ surface. Put each component in its own source file and give each visual
412
+ component exactly one owned stylesheet alongside it. Keep the public
413
+ compatibility entrypoints implementation-free: the JavaScript/TypeScript
414
+ entrypoint only re-exports the folder modules, and the CSS entrypoint only
415
+ imports their owner stylesheets. A shared public name is not permission to put
416
+ several component implementations or visual contracts back into one file.
417
+
357
418
  `wireTokenSearchFields` is a deliberate exception, not the rule for `wire…`
358
419
  helpers. Its collapse behavior was _rich and error-prone_ — reveal, focus
359
420
  transfer, Escape, empty-blur collapse, focus return — the kind of transient chrome
@@ -371,6 +432,13 @@ render the section body, so a managed helper would remove no real complexity. Re
371
432
  for a managed default only when the transient behavior is substantial enough that
372
433
  hand-rolling it produces genuine, inconsistent variation.
373
434
 
435
+ TabBar itself owns the inline geometry around those mechanics. Its tab list is
436
+ the only shrinking, horizontally scrolling region; `leading`, `trailing`, and
437
+ `end` action regions remain fixed. Set `trailingPlacement="adjacent"` when the
438
+ trailing action belongs immediately after the final tab and use `end` for a
439
+ separate action pinned to the far edge. Applications own the action content,
440
+ but must not restyle TabBar descendants to recreate that allocation.
441
+
374
442
  CSS, the generated wrappers that make it
375
443
  reachable, and the registration module are the package's only declared side
376
444
  effects.
@@ -421,4 +489,4 @@ Keep product adapters outside the package: connection-state maps, ticket empty-s
421
489
 
422
490
  ## Testing contract
423
491
 
424
- Each behavior has focused unit coverage and a real-browser flow through the production-backed catalog. Consumer bundle tests enforce subpath CSS reachability, transitive component styles, root/SSR isolation, optional registration boundaries, and the CSS-only Web Awesome theme boundary. Visual evidence supplements—never replaces—keyboard, focus, state, and event assertions.
492
+ Each behavior has focused unit coverage and a real-browser flow through the production-backed catalog. Consumer bundle tests enforce subpath CSS reachability, transitive component styles, root/SSR isolation, optional registration boundaries, and the CSS-only Web Awesome theme boundary. `npm run check:css-ownership` rejects application selectors into Kerf or Web Awesome components, a non-minimal catalog compatibility stylesheet, or a return of the shared recipe stylesheet. Visual evidence supplements—never replaces—keyboard, focus, state, and event assertions.