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

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 (274) hide show
  1. package/README.md +14 -11
  2. package/ai/compile-time-contracts-v1.json +2 -2
  3. package/ai/component-catalog-v2.json +206 -39
  4. package/ai/component-catalog.json +306 -58
  5. package/ai/public-api-signatures-v1.md +671 -92
  6. package/ai/skill.md +43 -38
  7. package/ai/webawesome-jsx-signatures-v1.md +8 -2
  8. package/dist/app-tab.js +1 -1
  9. package/dist/badge.d.ts +35 -7
  10. package/dist/badge.js +1 -1
  11. package/dist/browser/content-item.js +3 -0
  12. package/dist/browser/popup-menu.js +3 -0
  13. package/dist/browser/select.js +1 -0
  14. package/dist/catalog-resources.d.ts +12 -1
  15. package/dist/catalog.d.ts +21 -4
  16. package/dist/catalog.js +260 -308
  17. package/dist/catalog.js.map +1 -1
  18. package/dist/{chunk-7IUSV4XG.js → chunk-24PHKUR3.js} +2 -2
  19. package/dist/chunk-24PHKUR3.js.map +1 -0
  20. package/dist/{chunk-WZ2GLEY4.js → chunk-2G2ZJQYK.js} +2 -14
  21. package/dist/chunk-2G2ZJQYK.js.map +1 -0
  22. package/dist/{chunk-D7GWXIAS.js → chunk-5I7QGQVT.js} +1 -1
  23. package/dist/chunk-5I7QGQVT.js.map +1 -0
  24. package/dist/{chunk-HJKRCKDR.js → chunk-63ZWBGE3.js} +6 -3
  25. package/dist/chunk-63ZWBGE3.js.map +1 -0
  26. package/dist/{chunk-44IG7PFV.js → chunk-6CINWX2E.js} +3 -3
  27. package/dist/{chunk-44IG7PFV.js.map → chunk-6CINWX2E.js.map} +1 -1
  28. package/dist/{chunk-C2FAYFDE.js → chunk-AGXQQFKZ.js} +2 -2
  29. package/dist/{chunk-C2FAYFDE.js.map → chunk-AGXQQFKZ.js.map} +1 -1
  30. package/dist/{chunk-ATHXW6RP.js → chunk-AHEG27M7.js} +2 -2
  31. package/dist/{chunk-ATHXW6RP.js.map → chunk-AHEG27M7.js.map} +1 -1
  32. package/dist/{chunk-7GUQYHKA.js → chunk-AK67F6QN.js} +4 -1
  33. package/dist/chunk-AK67F6QN.js.map +1 -0
  34. package/dist/chunk-BFPBS5Y4.js +250 -0
  35. package/dist/chunk-BFPBS5Y4.js.map +1 -0
  36. package/dist/chunk-DG6G5EPF.js +35 -0
  37. package/dist/chunk-DG6G5EPF.js.map +1 -0
  38. package/dist/{chunk-MTM43SJO.js → chunk-G33XC2NB.js} +1 -1
  39. package/dist/chunk-G33XC2NB.js.map +1 -0
  40. package/dist/{chunk-TZ5IHWB2.js → chunk-K6UNVKDK.js} +3 -3
  41. package/dist/{chunk-TZ5IHWB2.js.map → chunk-K6UNVKDK.js.map} +1 -1
  42. package/dist/chunk-KH6VUGHU.js +399 -0
  43. package/dist/chunk-KH6VUGHU.js.map +1 -0
  44. package/dist/{chunk-HMQ2ZDGG.js → chunk-KROD7KZK.js} +3 -2
  45. package/dist/chunk-KROD7KZK.js.map +1 -0
  46. package/dist/{chunk-5NDQABWK.js → chunk-M7QV3IHH.js} +3 -1
  47. package/dist/chunk-M7QV3IHH.js.map +1 -0
  48. package/dist/{chunk-RNZAKSME.js → chunk-MA6Z6EIM.js} +3 -3
  49. package/dist/{chunk-RNZAKSME.js.map → chunk-MA6Z6EIM.js.map} +1 -1
  50. package/dist/chunk-MA73TA6D.js +42 -0
  51. package/dist/chunk-MA73TA6D.js.map +1 -0
  52. package/dist/{chunk-ZMBU522F.js → chunk-MEY33VWB.js} +2 -2
  53. package/dist/{chunk-ZMBU522F.js.map → chunk-MEY33VWB.js.map} +1 -1
  54. package/dist/{chunk-T22EM4ET.js → chunk-MOBRFZH3.js} +81 -31
  55. package/dist/chunk-MOBRFZH3.js.map +1 -0
  56. package/dist/chunk-MP2GDBQZ.js +50 -0
  57. package/dist/chunk-MP2GDBQZ.js.map +1 -0
  58. package/dist/{chunk-3NBL3OUQ.js → chunk-NAPE7WUS.js} +2 -2
  59. package/dist/chunk-NAPE7WUS.js.map +1 -0
  60. package/dist/{chunk-CMUIXJZM.js → chunk-NBYTIVHS.js} +2 -2
  61. package/dist/{chunk-CMUIXJZM.js.map → chunk-NBYTIVHS.js.map} +1 -1
  62. package/dist/chunk-O3EXIBEZ.js +114 -0
  63. package/dist/chunk-O3EXIBEZ.js.map +1 -0
  64. package/dist/{chunk-KPNTVB7A.js → chunk-PK4N5QTO.js} +8 -7
  65. package/dist/chunk-PK4N5QTO.js.map +1 -0
  66. package/dist/chunk-QQOTQTDD.js +415 -0
  67. package/dist/chunk-QQOTQTDD.js.map +1 -0
  68. package/dist/{chunk-BJJKFVWW.js → chunk-T4UZTNZ4.js} +5 -5
  69. package/dist/chunk-T4UZTNZ4.js.map +1 -0
  70. package/dist/chunk-V65EOWNF.js +83 -0
  71. package/dist/chunk-V65EOWNF.js.map +1 -0
  72. package/dist/{chunk-GUIHMIT2.js → chunk-VNPQDCQZ.js} +3 -3
  73. package/dist/{chunk-GUIHMIT2.js.map → chunk-VNPQDCQZ.js.map} +1 -1
  74. package/dist/{chunk-5I22LMCV.js → chunk-VZOFZPSO.js} +2 -2
  75. package/dist/chunk-VZOFZPSO.js.map +1 -0
  76. package/dist/chunk-WLG6SSNT.js +94 -0
  77. package/dist/chunk-WLG6SSNT.js.map +1 -0
  78. package/dist/{chunk-NE5JAOQS.js → chunk-WOUF7YXM.js} +3 -3
  79. package/dist/{chunk-NE5JAOQS.js.map → chunk-WOUF7YXM.js.map} +1 -1
  80. package/dist/collapsible-panel.d.ts +51 -13
  81. package/dist/collapsible-panel.js +38 -10
  82. package/dist/collapsible-panel.js.map +1 -1
  83. package/dist/content-item.d.ts +46 -0
  84. package/dist/content-item.js +4 -0
  85. package/dist/content-item.js.map +1 -0
  86. package/dist/css-values.d.ts +5 -11
  87. package/dist/css-values.js +1 -1
  88. package/dist/disclosure-arrow.d.ts +4 -1
  89. package/dist/disclosure-arrow.js +1 -1
  90. package/dist/floating-toolbar.d.ts +17 -6
  91. package/dist/floating-toolbar.js +1 -1
  92. package/dist/grid.js +1 -1
  93. package/dist/help-tags-register.d.ts +13 -0
  94. package/dist/help-tags-register.js +9 -0
  95. package/dist/help-tags-register.js.map +1 -0
  96. package/dist/index.d.ts +7 -5
  97. package/dist/index.js +31 -29
  98. package/dist/list-action-row.js +1 -1
  99. package/dist/list-header.d.ts +14 -1
  100. package/dist/list-header.js +1 -1
  101. package/dist/list-inset-control.d.ts +1 -1
  102. package/dist/list-inset-control.js +1 -1
  103. package/dist/list-inset-text.d.ts +9 -10
  104. package/dist/list-inset-text.js +2 -1
  105. package/dist/list-item.js +1 -1
  106. package/dist/list.d.ts +10 -3
  107. package/dist/list.js +1 -1
  108. package/dist/nav-stack.d.ts +38 -5
  109. package/dist/nav-stack.js +4 -1
  110. package/dist/pane.d.ts +20 -2
  111. package/dist/pane.js +1 -1
  112. package/dist/panel-toolbar-C8sF-YZC.d.ts +63 -0
  113. package/dist/popup-menu-register.d.ts +9 -0
  114. package/dist/popup-menu-register.js +31 -0
  115. package/dist/popup-menu-register.js.map +1 -0
  116. package/dist/popup-menu.d.ts +71 -0
  117. package/dist/popup-menu.js +3 -0
  118. package/dist/popup-menu.js.map +1 -0
  119. package/dist/row.d.ts +1 -1
  120. package/dist/row.js +1 -1
  121. package/dist/select-register.js +70 -3
  122. package/dist/select-register.js.map +1 -1
  123. package/dist/select.d.ts +46 -8
  124. package/dist/select.js +2 -1
  125. package/dist/{divider-sides-BzB6rphT.d.ts → sides-BPSWde0A.d.ts} +1 -3
  126. package/dist/skeleton.js +1 -1
  127. package/dist/spacer.js +1 -1
  128. package/dist/split-view.d.ts +31 -3
  129. package/dist/split-view.js +33 -5
  130. package/dist/split-view.js.map +1 -1
  131. package/dist/state-banner.js +1 -1
  132. package/dist/styles/badge.css +13 -0
  133. package/dist/styles/catalog/components/catalog-example.css +1 -17
  134. package/dist/styles/catalog/components/catalog-stage.css +9 -14
  135. package/dist/styles/catalog/components/catalog.css +8 -18
  136. package/dist/styles/catalog.css +0 -5
  137. package/dist/styles/collapsible-panel.css +192 -39
  138. package/dist/styles/content-item.css +30 -0
  139. package/dist/styles/floating-toolbar.css +5 -0
  140. package/dist/styles/foundation.css +28 -19
  141. package/dist/styles/layout.css +1 -21
  142. package/dist/styles/list-action-row.css +13 -1
  143. package/dist/styles/list-inset-text.css +1 -2
  144. package/dist/styles/list-item.css +14 -1
  145. package/dist/styles/list.css +1 -1
  146. package/dist/styles/nav-stack.css +29 -64
  147. package/dist/styles/pane.css +33 -1
  148. package/dist/styles/popup-menu.css +90 -0
  149. package/dist/styles/resizable-region.css +53 -10
  150. package/dist/styles/select.css +39 -12
  151. package/dist/styles/split-view.css +21 -25
  152. package/dist/styles/state-banner.css +13 -4
  153. package/dist/styles/styles.css +1 -0
  154. package/dist/styles/tab-bar.css +31 -3
  155. package/dist/styles/tab-scaffold.css +36 -14
  156. package/dist/styles/text.css +1 -1
  157. package/dist/styles/token-search-field.css +4 -4
  158. package/dist/styles/toolbar-control-group.css +44 -12
  159. package/dist/styles/toolbar.css +57 -7
  160. package/dist/styles/value-table.css +19 -2
  161. package/dist/styles/webawesome.css +3 -1
  162. package/dist/styles/workbench.css +165 -31
  163. package/dist/tab-scaffold.d.ts +35 -1
  164. package/dist/tab-scaffold.js +50 -16
  165. package/dist/tab-scaffold.js.map +1 -1
  166. package/dist/text.js +1 -1
  167. package/dist/token-search-field.d.ts +13 -2
  168. package/dist/token-search-field.js +1 -1
  169. package/dist/toolbar-text.js +1 -1
  170. package/dist/toolbar.d.ts +12 -4
  171. package/dist/toolbar.js +1 -1
  172. package/dist/{types-CEsieBZi.d.ts → types-DXAGogOS.d.ts} +48 -0
  173. package/dist/value-table.js +1 -1
  174. package/dist/webawesome.d.ts +7 -1
  175. package/dist/wire-catalog.d.ts +24 -1
  176. package/dist/wire-catalog.js +26 -3
  177. package/dist/wire-catalog.js.map +1 -1
  178. package/dist/wire-sidebar.js +6 -3
  179. package/dist/wire-sidebar.js.map +1 -1
  180. package/dist/wire-token-search-fields.js +1 -1
  181. package/dist/wire-workbench.js +1 -350
  182. package/dist/wire-workbench.js.map +1 -1
  183. package/dist/workbench.d.ts +144 -6
  184. package/dist/workbench.js +11 -208
  185. package/dist/workbench.js.map +1 -1
  186. package/docs/accessibility.md +126 -6
  187. package/docs/app-layouts.md +9 -7
  188. package/docs/catalog.md +41 -9
  189. package/docs/collapsible-panel.md +79 -5
  190. package/docs/component-contract.md +38 -12
  191. package/docs/component-selection.md +64 -55
  192. package/docs/design/templates/empty-state/with-action-dark.svg +1 -1
  193. package/docs/design/templates/empty-state/with-action.svg +1 -1
  194. package/docs/design/templates/empty-state-dark.svg +1 -1
  195. package/docs/design/templates/empty-state.svg +1 -1
  196. package/docs/design/templates/list/compact-dark.svg +1 -1
  197. package/docs/design/templates/list/compact.svg +1 -1
  198. package/docs/design/templates/list/stack-dark.svg +1 -1
  199. package/docs/design/templates/list/stack.svg +1 -1
  200. package/docs/design/templates/list-action-row/default-dark.svg +1 -1
  201. package/docs/design/templates/list-action-row/default.svg +1 -1
  202. package/docs/design/templates/list-action-row/selected-dark.svg +1 -1
  203. package/docs/design/templates/list-action-row/selected.svg +1 -1
  204. package/docs/design/templates/list-action-row-dark.svg +1 -1
  205. package/docs/design/templates/list-action-row.svg +1 -1
  206. package/docs/design/templates/list-dark.svg +1 -1
  207. package/docs/design/templates/list-item/default-dark.svg +1 -1
  208. package/docs/design/templates/list-item/default.svg +1 -1
  209. package/docs/design/templates/list-item/selected-dark.svg +1 -1
  210. package/docs/design/templates/list-item/selected.svg +1 -1
  211. package/docs/design/templates/list-item/trailing-dark.svg +1 -1
  212. package/docs/design/templates/list-item/trailing.svg +1 -1
  213. package/docs/design/templates/list-item-dark.svg +1 -1
  214. package/docs/design/templates/list-item.svg +1 -1
  215. package/docs/design/templates/list.svg +1 -1
  216. package/docs/design/templates/state-banner/danger-dark.svg +1 -1
  217. package/docs/design/templates/state-banner/danger.svg +1 -1
  218. package/docs/design/templates/state-banner/info-dark.svg +1 -1
  219. package/docs/design/templates/state-banner/info.svg +1 -1
  220. package/docs/design/templates/state-banner/success-dark.svg +1 -1
  221. package/docs/design/templates/state-banner/success.svg +1 -1
  222. package/docs/design/templates/state-banner/warning-dark.svg +1 -1
  223. package/docs/design/templates/state-banner/warning.svg +1 -1
  224. package/docs/design/templates/state-banner-dark.svg +1 -1
  225. package/docs/design/templates/state-banner.svg +1 -1
  226. package/docs/design/templates/value-table/details-dark.svg +1 -1
  227. package/docs/design/templates/value-table/details.svg +1 -1
  228. package/docs/design/templates/value-table-dark.svg +1 -1
  229. package/docs/design/templates/value-table.svg +1 -1
  230. package/docs/design-philosophy.md +1 -1
  231. package/docs/examples/command-palette-adapter.tsx +3 -2
  232. package/docs/layout.md +74 -29
  233. package/docs/nav-stack.md +45 -1
  234. package/docs/recipes.md +48 -40
  235. package/docs/split-view.md +46 -2
  236. package/docs/surface-scaffold.md +1 -1
  237. package/docs/tab-scaffold.md +50 -1
  238. package/docs/type-contracts.md +17 -17
  239. package/docs/ux-demo.md +8 -4
  240. package/docs/webawesome-theme.md +1 -2
  241. package/docs/workbench.md +131 -10
  242. package/llms.txt +38 -9
  243. package/package.json +24 -2
  244. package/ux-demo/recipes/app-shell.tsx +141 -223
  245. package/ux-demo/recipes/collapsible-sidebar.tsx +47 -77
  246. package/ux-demo/recipes/list-detail-dialog.tsx +150 -54
  247. package/ux-demo/recipes/mount-recipe.ts +0 -5
  248. package/ux-demo/recipes/navigation-sidebar.tsx +3 -2
  249. package/ux-demo/recipes/navigation-stack.tsx +3 -2
  250. package/ux-demo/recipes/types.ts +0 -1
  251. package/dist/chunk-3NBL3OUQ.js.map +0 -1
  252. package/dist/chunk-5I22LMCV.js.map +0 -1
  253. package/dist/chunk-5NDQABWK.js.map +0 -1
  254. package/dist/chunk-7GUQYHKA.js.map +0 -1
  255. package/dist/chunk-7IUSV4XG.js.map +0 -1
  256. package/dist/chunk-BJJKFVWW.js.map +0 -1
  257. package/dist/chunk-D7GWXIAS.js.map +0 -1
  258. package/dist/chunk-DQA5FMOX.js +0 -85
  259. package/dist/chunk-DQA5FMOX.js.map +0 -1
  260. package/dist/chunk-HJKRCKDR.js.map +0 -1
  261. package/dist/chunk-HMQ2ZDGG.js.map +0 -1
  262. package/dist/chunk-KPNTVB7A.js.map +0 -1
  263. package/dist/chunk-MTM43SJO.js.map +0 -1
  264. package/dist/chunk-T22EM4ET.js.map +0 -1
  265. package/dist/chunk-WZ2GLEY4.js.map +0 -1
  266. package/dist/chunk-XD7DWYXG.js +0 -34
  267. package/dist/chunk-XD7DWYXG.js.map +0 -1
  268. package/dist/chunk-ZK5EGVFP.js +0 -27
  269. package/dist/chunk-ZK5EGVFP.js.map +0 -1
  270. package/dist/styles/catalog/components/catalog-detail.css +0 -35
  271. package/dist/styles/catalog/components/catalog-resource-footer.css +0 -37
  272. package/dist/styles/catalog/components/catalog-secondary-sections.css +0 -38
  273. package/dist/styles/catalog/components/catalog-section-list.css +0 -22
  274. package/dist/styles/catalog/components/catalog-sidebar.css +0 -82
package/docs/layout.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Import `Pane` from `@kerfjs/ui/pane`. Browser-aware bundlers receive its CSS
4
4
  automatically; other consumers can import `@kerfjs/ui/pane.css`. Import
5
- `@kerfjs/ui/layout.css` as well when using the related content-item and control
6
- layout classes. The vocabulary deliberately describes structure rather than
5
+ `@kerfjs/ui/layout.css` as well when using the `.kui-content` stack and control
6
+ layout classes; place `ContentItem` children from `@kerfjs/ui/content-item`. The vocabulary deliberately describes structure rather than
7
7
  location: the same pane can be placed anywhere.
8
8
 
9
9
  ## Anatomy
@@ -29,10 +29,17 @@ top toolbar followed by secondary toolbar or status rows. A main area or dialog
29
29
  often omits the footer; a navigation pane commonly uses all three. Fixed chrome
30
30
  stays outside `.kui-pane__content`, which is the pane's only scroll owner.
31
31
 
32
+ Tall fixed chrome can leave a short pane no room for its content (a small
33
+ window, or text at 200%). `chromePlacement="auto"` keeps the header and footer
34
+ pinned while the pane is at least 480px (30rem, so it scales with the text
35
+ size) tall, and below that lets the whole pane scroll as one column, header,
36
+ content, and footer together, so the content keeps its natural height. The
37
+ default `fixed` always pins them.
38
+
32
39
  Pass any combination of logical sides to `separators`: `block-start`,
33
40
  `block-end`, `inline-start`, and `inline-end`. Every line is off by default and
34
41
  each enabled side uses `--kui-pane-separator-width` (1px) and
35
- `--kui-pane-separator-color` (`--kui-color-border`). Logical sides keep pane
42
+ `--kui-pane-separator-color` (`--kui-color-neutral-border-normal`). Logical sides keep pane
36
43
  boundaries correct in both left-to-right and right-to-left layouts.
37
44
 
38
45
  A pane is safe-area aware. Its background and separators paint through a
@@ -45,18 +52,8 @@ same logical sides as `separators`; all four by default, `[]` to opt out). See
45
52
 
46
53
  `.kui-content` is a vertical stack with a 24px gap between major children.
47
54
  Sections may contain adjacent `ListItem` rows without adding another major gap.
48
- Ordinary surface-like children use `.kui-content-item` and own their complete
49
- geometry:
50
-
51
- - 8px inline margin from the pane edge
52
- - 1px border, transparent by default
53
- - 8px internal padding and 8px internal gap
54
- - `calc(1px + remify(11px))`, or 12px, rounded corners
55
-
56
- Use `.kui-content-item--pill` for the 22px pill radius expressed as
57
- `calc(1px + remify(21px))`. Consumers can make a content item visible without
58
- changing its geometry by setting `--kui-content-item-border` and
59
- `--kui-content-item-background`.
55
+ Ordinary surface-like children are `ContentItem`s and own their complete
56
+ geometry (see [Content items](#content-items)).
60
57
 
61
58
  Nested selection and hover highlights stay concentric and match their owning
62
59
  control's shape. Their inner radius is the outer radius minus the full inset,
@@ -72,20 +69,63 @@ inline gutter instead of nesting them inside another padded content item. A
72
69
  conditional `StateBanner` remains visibly distinct because it communicates
73
70
  semantic status.
74
71
 
72
+ ## Content items
73
+
74
+ Render an ordinary surface-like `.kui-content` child with `ContentItem` from
75
+ `@kerfjs/ui/content-item` (browser-aware bundlers receive its CSS automatically;
76
+ `@kerfjs/ui/layout.css` and `@kerfjs/ui/content-item.css` both deliver it). It
77
+ owns its complete geometry:
78
+
79
+ - 8px inline margin from the pane edge
80
+ - 1px border, transparent by default
81
+ - 8px internal padding
82
+ - `calc(1px + remify(11px))`, or 12px, rounded corners
83
+
84
+ ```tsx
85
+ import { ContentItem } from "@kerfjs/ui/content-item";
86
+
87
+ <Pane label="Inspector">
88
+ <ContentItem>{details}</ContentItem>
89
+ <ContentItem frame="framed">{pendingChanges}</ContentItem>
90
+ <ContentItem shape="pill">{summary}</ContentItem>
91
+ </Pane>;
92
+ ```
93
+
94
+ `frame="framed"` paints the standard neutral border in the 1px the item always
95
+ reserves, so framed and unframed items share the same geometry. Frame an item
96
+ only when it marks a real distinction and must read as visibly bounded; do not
97
+ frame it to make it "look contained". `shape="pill"` selects the 22px pill
98
+ radius expressed as `calc(1px + remify(21px))`. There is deliberately no filled
99
+ variant: a lowered, filled surface is `SunkenPanel`, and bare text that only
100
+ needs to align with neighboring items is `ListInsetText`. Safe `data-*`
101
+ metadata goes in `rootAttributes`. Pass `ariaLabel` only for a distinct named
102
+ region (`role="region"`), and `focusTarget` to make the item a programmatic
103
+ focus target (`tabindex="-1"`), for example a NavStack view's `data-nav-focus`
104
+ target.
105
+
106
+ The rendered classes — `.kui-content-item`, `.kui-content-item--framed`, and
107
+ `.kui-content-item--pill` — stay public for the rare element that must carry the
108
+ geometry itself (for example a `<ul>` list or a `Text` paragraph in an
109
+ application-owned adapter). Prefer `ContentItem` everywhere else so the framing
110
+ choice is typed. `npm run check:guidance` rejects a plain
111
+ `<div class="kui-content-item">` in the package's examples, browser fixtures,
112
+ and UX catalog, since that div is exactly what `ContentItem` renders.
113
+
75
114
  ## Public roles and tokens
76
115
 
77
- | Need | Class | Token / default |
78
- | ---------------------------------------- | ------------------------- | --------------------------------------------------------------------------- |
79
- | Unpadded header/content/footer structure | `Pane`, `.kui-pane` | — |
80
- | Scrolling pane content | `.kui-pane__content` | — |
81
- | Optional logical-edge separators | `Pane.separators` | `--kui-pane-separator-width: 1px` |
82
- | Safe-area sides a pane may pad | `Pane.safeAreaEdges` | `--kui-safe-area-*: env(safe-area-inset-*)`, routed as `--kui-edge-inset-*` |
83
- | Major vertical rhythm | `.kui-content` | `--kui-layout-content-gap: 24px` |
84
- | Self-contained child geometry | `.kui-content-item` | 8px margin + 1px border + 8px padding |
85
- | Pill child | `.kui-content-item--pill` | `--kui-layout-pill-radius: 22px` |
86
- | Related controls | `.kui-control-cluster` | `--kui-layout-control-gap: 8px` |
87
- | Inline metadata | `.kui-inline-metadata` | `--kui-layout-metadata-gap: 4px` |
88
- | Explicit scroll owner outside a pane | `.kui-scroll-owner` | `overflow: auto` |
116
+ | Need | Class | Token / default |
117
+ | ---------------------------------------- | ---------------------------- | ------------------------------------------------------------------ |
118
+ | Unpadded header/content/footer structure | `Pane`, `.kui-pane` | — |
119
+ | Scrolling pane content | `.kui-pane__content` | — |
120
+ | Optional logical-edge separators | `Pane.separators` | `--kui-pane-separator-width: 1px` |
121
+ | Safe-area sides a pane may pad | `Pane.safeAreaEdges` | `--kui-safe-area-*: env(safe-area-inset-*)`, routed by each layout |
122
+ | Major vertical rhythm | `.kui-content` | `--kui-layout-content-gap: 24px` |
123
+ | Self-contained child geometry | `ContentItem` | 8px margin + 1px border + 8px padding |
124
+ | Pill child | `ContentItem shape="pill"` | `--kui-layout-pill-radius: 22px` |
125
+ | Visibly framed child | `ContentItem frame="framed"` | neutral 1px border, same geometry |
126
+ | Related controls | `.kui-control-cluster` | `--kui-layout-control-gap: 8px` |
127
+ | Inline metadata | `.kui-inline-metadata` | `--kui-layout-metadata-gap: 4px` |
128
+ | Explicit scroll owner outside a pane | `.kui-scroll-owner` | `overflow: auto` |
89
129
 
90
130
  The component layer applies the same contract to `Toolbar`, `ListHeader`,
91
131
  `ListItem`, `Toolbar`, `StateBanner`, `ValueTable`,
@@ -96,6 +136,11 @@ also keeps 8px of root-scaled block padding independently of its semantic inline
96
136
  inset. Most interactive rows and toolbar groups are 44px tall.
97
137
  Toolbar groups reserve a real 1px outer border around a 42px inner area, even
98
138
  when their border and background are transparent.
139
+ A `Toolbar` lays its zones out against one 44px control band at its top: an
140
+ item no taller than the band is centered in it by its own size, and a taller
141
+ item (a wrapped title, a second trailing row) starts at the band's top and
142
+ grows down, so it never moves the other controls. A wrapped `ToolbarText`
143
+ centers its first line in the band.
99
144
  Trailing icon actions share one axis: a toolbar control centers its glyph in
100
145
  that 44px slot at the 8px inline margin, and a `ListHeader` action centers its
101
146
  fitted 36px square in a slot of the same size (a transparent hit layer extends
@@ -302,8 +347,8 @@ selected sides; the control wrapper applies its 8px outer margin only on
302
347
  selected sides. Every direct child of `ListInsetControl` grows to fill the row
303
348
  (a `Select`, a `wa-input`, or a `wa-button` spans the full inset width), and
304
349
  several children share the row with an 8px gap; stack controls vertically with
305
- a `List` using `controlInsets` instead. `ListInsetText.horizontalOnly` remains a deprecated alias for
306
- `sides="rl"`; an explicit `sides` value takes precedence.
350
+ a `List` using `controlInsets` instead. Pass `sides="rl"` for tight text that
351
+ keeps the horizontal inset without adding vertical box space.
307
352
 
308
353
  `--kui-space-s` (12px) and `--kui-space-xl` (32px) exist but are **off the
309
354
  canonical rhythm** — reach for them only as a deliberate exception, never as a
package/docs/nav-stack.md CHANGED
@@ -49,11 +49,55 @@ views.value = [
49
49
  `NavStack` renders every entry stacked, the last one active and the rest kept
50
50
  mounted (so their DOM state survives) but hidden. Each entry carries a
51
51
  `key` (stable identity), `content`, an optional `title`, optional per-view
52
- `toolbar` actions, and an optional per-view `bottomToolbar`. The component-level
52
+ `leading`, `center`, and trailing `toolbar` groups for the top toolbar, and an
53
+ optional per-view `bottomToolbar`. The component-level
53
54
  `bottomToolbar` remains a persistent fallback for views that do not provide one.
54
55
  The back control appears automatically once the stack has more than one entry;
55
56
  `wireNavStack`'s `onBack` is where the app pops its own signal.
56
57
 
58
+ ## The top toolbar
59
+
60
+ The top chrome is a real `Toolbar`, so it follows the same zone,
61
+ group, and responsive rules as every other toolbar. Its leading zone holds the
62
+ back control (a borderless `ToolbarControlGroup`, shown once the stack has
63
+ depth), then the active view's `leading` groups, then the view's `title` as a
64
+ `ToolbarText`. The view's `center` fills the center zone and its `toolbar`
65
+ fills the trailing zone. Give each zone `ToolbarControlGroup`s or `ToolbarText`,
66
+ as in any toolbar.
67
+
68
+ Configure the toolbar with `toolbarConfig` instead of styling it:
69
+
70
+ ```tsx
71
+ <NavStack
72
+ id="mail"
73
+ label="Mail"
74
+ views={views.value}
75
+ backText={previousTitle}
76
+ toolbarConfig={{
77
+ headingLevel: 1,
78
+ titleSize: "xlarge",
79
+ dividerSides: "b",
80
+ responsive: "wrap",
81
+ }}
82
+ />
83
+ ```
84
+
85
+ - `toolbarConfig` forwards the `Toolbar` configuration (`dividerSides`,
86
+ `centerAlign`, `responsive`, `responsiveAt`, `safeAreaEdges`) and adds
87
+ `label` (the toolbar's accessible name), `titleSize` (the title's
88
+ `ToolbarText` size, default `large`), and `headingLevel` (expose the title as
89
+ a heading; default a plain span). The defaults keep the stack's own chrome:
90
+ no divider, one 44px control band, and the top and side safe-area edges
91
+ claimed.
92
+ - `backIcon` replaces the default chevron-left icon. `backText` adds visible
93
+ text beside it (for example, the previous view's title); visible text names
94
+ the control, so `backLabel` applies only to the icon-only control.
95
+
96
+ The catalog's **Configured toolbar** example shows these together: a level-2
97
+ heading title over a bottom divider, leading and center groups on the root
98
+ view, a trailing group on the pushed view, and `backText` set to the previous
99
+ view's title.
100
+
57
101
  Focus follows the controlled stack automatically. On push, `wireNavStack`
58
102
  remembers the focused descendant of the departing view and moves focus into the
59
103
  new top view. On pop it restores that exact descendant when it still exists. A
package/docs/recipes.md CHANGED
@@ -9,9 +9,9 @@ ship no stylesheet of their own and are not new monolithic components.
9
9
  Copy the recipe source together with the catalog-independent
10
10
  [`mount-recipe.ts`](../ux-demo/recipes/mount-recipe.ts) adapter. It mounts the
11
11
  controller at one stable application root, uses `delegateActions()` for recipe
12
- commands, forwards form and dialog lifecycle events, wires resize commits with
13
- the public `onCommit` callback, retains every disposer, and returns one
14
- idempotent disposer:
12
+ commands, forwards form and dialog lifecycle events, runs the recipe's own
13
+ `wire()` (for example `wireWorkbench` or `wireSidebar`), retains every
14
+ disposer, and returns one idempotent disposer:
15
15
 
16
16
  ```ts
17
17
  import { createRecipe } from "./navigation-sidebar.js";
@@ -31,32 +31,34 @@ specific dispatch; either way, wire once at a stable root and retain disposal.
31
31
  [Open the recipe](../ux-demo/?component=recipe-app-shell) · [TSX source](../ux-demo/recipes/app-shell.tsx)
32
32
 
33
33
  A filling `List` (`fill`, with the recipe's `data-*` markers through
34
- `rootAttributes`) stacks the app-bar `Toolbar` above a `Row` that places
35
- controlled `ResizableRegion` navigation and inspector panes around a content
36
- `Pane` that a one-column `Grid` grows to fill. Each region's lone `Pane` fills
37
- the region's full height, so it reaches the bottom edge the separator does.
38
- Only those three real panes own a
39
- `.kui-pane__content` scroll owner; no frame `Pane` wraps the layout in a scroll
40
- owner that never scrolls. Each pane lists the screen edges it reaches in
41
- `safeAreaEdges` (the bottom plus its outer side, or both sides when it is the
42
- only pane shown), and the app-bar `Toolbar` claims the top edge and both sides
43
- with its own `safeAreaEdges`, so it clears the status area. Below desktop sizes the app reads `deviceClass()` and shows one
44
- pane at a time, switched by a pressed-state `ToolbarControlGroup`. The recipe
45
- owns the shell topology; the app owns routing, responsive pane visibility,
46
- sizes, persistence, and data.
47
- Keep each visible pane's collapse action in its own toolbar. Once hidden, put
48
- its restore action in the adjacent main toolbar on the same logical edge:
49
- leading for an inline-start sidebar and trailing for an inline-end inspector.
50
- Collapse the pane completely rather than preserving an empty icon rail.
51
- Adapt only public `--kui-layout-*` and component variables.
34
+ `rootAttributes`) stacks the app-bar `Toolbar` above a `Workbench`
35
+ (`@kerfjs/ui/workbench`): resizable navigation and inspector rails around the
36
+ task list. Each rail composes its own panel toolbar — a default-size title and
37
+ the standard hide toggle — and the work area's `mainToolbar` carries the xl
38
+ view title. While a rail is hidden, `Workbench` relocates its show toggle into
39
+ the work-area toolbar on the same logical edge (leading for the navigation
40
+ rail, trailing for the inspector), and `wireWorkbench` hands focus between the
41
+ two positions. Collapse a rail completely rather than preserving an empty icon
42
+ rail. Only the three real panes own a `.kui-pane__content` scroll owner; no
43
+ frame `Pane` wraps the layout in a scroll owner that never scrolls, and the
44
+ app-bar `Toolbar` claims the top edge and both sides with its own
45
+ `safeAreaEdges`.
46
+
47
+ On a narrow container the rails overlay the task list one at a time instead of
48
+ squeezing it, leaving a 44px strip on the far side that dismisses the overlay
49
+ (`responsiveOverlayAt`, on by default; `compactOverlay: 'full'` covers the
50
+ whole width). The app owns each rail's collapsed and size signals — which
51
+ `wireWorkbench` keeps in sync with resizing and overlay dismissal — plus
52
+ routing, persistence, and data. Adapt only public `--kui-layout-*` and
53
+ component variables.
52
54
 
53
55
  ## Navigation sidebar
54
56
 
55
57
  [Open the recipe](../ux-demo/?component=recipe-navigation-sidebar) · [TSX source](../ux-demo/recipes/navigation-sidebar.tsx)
56
58
 
57
59
  One unpadded `.kui-pane` owns toolbar/content/footer structure. Its
58
- `.kui-content` uses 24px major gaps; `ListHeader`, `ListItem`, and other
59
- `.kui-content-item` children own their 8px margin, 1px border, and 8px padding.
60
+ `.kui-content` uses 24px major gaps; `ListHeader`, `ListItem`, and
61
+ `ContentItem` children own their 8px margin, 1px border, and 8px padding.
60
62
  Rows and footer toolbar groups remain 44px tall. The app owns routes,
61
63
  permissions, labels, selection, valid section counts and their localized
62
64
  `countLabel` phrases, non-count badge content, disclosure state, and revealed
@@ -75,19 +77,23 @@ controls relocate without changing focus order.
75
77
 
76
78
  [Open the recipe](../ux-demo/?component=recipe-list-detail-dialog) · [TSX source](../ux-demo/recipes/list-detail-dialog.tsx)
77
79
 
78
- The production Web Awesome dialog owns modal focus, Escape, its labeled header,
79
- and its close control; the thin recipe adapter restores the invoking control
80
- after the hide event. `DialogSurface` chooses the large modal with no body
81
- inset, because `SplitView` and its rows own list geometry, and a comfortable
82
- footer inset for the record actions. On roomy devices `SplitView` shows the
83
- project list beside its detail; each pane opens with a `ListHeader` on one
84
- shared line, and the dialog title, both headers, and the list text share the
85
- 17px text edge while the `ValueTable` keeps the 8px outer margin. On compact
86
- devices the dialog becomes a full-screen sheet and `SplitView` becomes a
80
+ A list-detail dialog is laid out like a `Workbench` with a left sidebar: on
81
+ roomy devices the list is a full-height sidebar and the detail is a full-height
82
+ main column, each a `Pane` with its own top `Toolbar`. The sidebar's toolbar
83
+ carries its quiet default-size title (and it may add a bottom toolbar for
84
+ sidebar-only actions); the main column's toolbar carries the dialog's primary
85
+ extra-large title and the close control, and its footer carries the record
86
+ actions. The Web Awesome dialog therefore renders `without-header` there —
87
+ its label still names the modal — and the close button uses
88
+ `data-dialog="close"`. Web Awesome keeps modal focus and Escape, and the thin
89
+ recipe adapter restores the invoking control after the hide event.
90
+ `DialogSurface` chooses the large modal with no body inset, because the panes
91
+ own their geometry. On compact devices the dialog becomes a full-screen sheet
92
+ with its own labeled header and footer actions, and `SplitView` becomes a
87
93
  `NavStack` drill-down whose back control the application pops. The
88
94
  application owns open state, selection, the pushed detail, dismissal policy,
89
- and record actions. Do not rebuild the dialog or reach into private shadow
90
- parts.
95
+ and record actions. Do not put the primary title in a header spanning both
96
+ columns, rebuild the dialog, or reach into private shadow parts.
91
97
 
92
98
  ## Composer form
93
99
 
@@ -162,11 +168,12 @@ which values are still unknown.
162
168
 
163
169
  A mini app frame whose left navigation rail and bottom activity drawer are
164
170
  standalone `CollapsiblePanel`s (`@kerfjs/ui/collapsible-panel`) driven by
165
- `wireSidebar` (`@kerfjs/ui/wire-sidebar`). `CollapsiblePanelToggle` supplies the
166
- standard per-side glyph: a collapse toggle lives inside each panel, and the
167
- panel's expand toggle shows only while it is collapsed, so exactly one control
168
- owns each action and a collapsed panel is still reachable. The rail's expand
169
- toggle sits in the always-visible main header; the drawer's is a
171
+ `wireSidebar` (`@kerfjs/ui/wire-sidebar`). Each panel takes a `toolbar` — a
172
+ title plus the standard per-side toggle — so the hide control lives in the
173
+ panel's own header, and `CollapsiblePanelRelocated` renders the matching show
174
+ control only while the panel is collapsed, so exactly one control owns each
175
+ action and a collapsed panel is still reachable. The rail's relocated toggle
176
+ leads the always-visible main header; the drawer's sits in a
170
177
  `FloatingToolbar` passed as the drawer's own `restoreControl`, which the panel
171
178
  floats in its column's safe-area-aware bottom-end corner, next to where the
172
179
  drawer opens — do not hand-roll a conditional floating control beside the
@@ -198,7 +205,8 @@ Chromium, Firefox, and WebKit by `tests/browser/collapsible-sidebar-recipe.spec.
198
205
  `@kerfjs/ui/webawesome.css`.
199
206
  - Compose, don't style. A recipe ships no stylesheet, sets no inline style, and
200
207
  names no class outside the published layout vocabulary (`.kui-content`,
201
- `.kui-content-item`, …) and themed Web Awesome classes; it configures
208
+ `.kui-control-cluster`, …) and themed Web Awesome classes — a content item is
209
+ a `ContentItem`, not a hand-written `.kui-content-item`; it configures
202
210
  components through props, `rootAttributes`, and tokens. When a recipe needs a
203
211
  capability no component offers, record it as a component gap rather than
204
212
  papering over it with CSS. `npm run check:recipes` enforces these rules on
@@ -53,11 +53,55 @@ const selected = signal<string | null>(null);
53
53
  app's selection). This is the portrait-tablet / handset presentation; as a
54
54
  dialog the compact form is a full-screen or large partial-cover modal.
55
55
 
56
+ ## Configuring the list region and the compact stack
57
+
58
+ `resizable` forwards the list's `ResizableRegion` configuration alongside
59
+ `size`, `min`, and `max`: `separator` (`"hidden"` keeps the resize hit target
60
+ but paints no line), `handleIcon`, `contentOverflow`, and collapse —
61
+ `collapsed`, `transitioning`, `collapseMotion`, `restoreControl`, and
62
+ `restorePosition`. A collapsed list snaps to zero width, renders `inert` and
63
+ `aria-hidden`, and the detail fills the split; the app owns the `collapsed`
64
+ flag and renders the restore control (usually a `FloatingToolbar`). Omitted
65
+ options keep the region's defaults.
66
+
67
+ ```tsx
68
+ resizable={{
69
+ size: listWidth.value,
70
+ min: 220,
71
+ max: 480,
72
+ collapsed: listCollapsed.value,
73
+ restoreControl: <ShowThreadsToolbar />,
74
+ }}
75
+ ```
76
+
77
+ `compactStack` configures the compact `NavStack`: `toolbarConfig`, `backIcon`,
78
+ `backText`, `hideToolbar`, and the persistent `bottomToolbar` forward to the
79
+ stack, and `list` / `detail` give each view its own top-toolbar groups
80
+ (`leading`, `center`, trailing `toolbar`) and `bottomToolbar`:
81
+
82
+ ```tsx
83
+ compactStack={{
84
+ backText: "Threads",
85
+ toolbarConfig: { headingLevel: 1 },
86
+ list: { toolbar: <ComposeGroup /> },
87
+ detail: { toolbar: <ReplyGroup />, bottomToolbar: <MessageActions /> },
88
+ }}
89
+ ```
90
+
56
91
  The catalog's **Interactive compact drill-down** example exercises this exact
57
92
  controlled flow: a list action sets the selected message, `detailActive` pushes
58
93
  its detail, and the wired Back action clears the selection to reveal the
59
- preserved list. Use the same structure in application demos so compact examples
60
- show the interaction rather than rendering a disconnected detail state.
94
+ preserved list. It also forwards `compactStack`: a level-2 heading title over
95
+ a bottom divider, a Compose group on the list view, and a Reply group plus a
96
+ bottom toolbar on the detail. Use the same structure in application demos so
97
+ compact examples show the interaction rather than rendering a disconnected
98
+ detail state.
99
+
100
+ The catalog's **Resizable, collapsible list** example forwards `resizable`: a
101
+ hidden separator that still resizes the list (its committed width comes back
102
+ through `wireResizableRegions`' `onCommit` into the app's size signal), a Hide
103
+ control in the list's own toolbar, and a `FloatingToolbar` restore control in
104
+ the split's bottom-start corner.
61
105
 
62
106
  Compose the interactive wiring from the existing helpers — `SplitView` adds no
63
107
  wire of its own.
@@ -18,7 +18,7 @@ padding and aligns with bordered siblings. Reserve `compact` or `comfortable`
18
18
  body insets for exceptional content that does not already have list/content-item
19
19
  geometry.
20
20
 
21
- Wrap one `wa-dropdown` with `PopupSurface`. `inset="list-zero"` removes menu
21
+ Wrap one `PopupMenu` (or another single `wa-dropdown`) with `PopupSurface`. `inset="list-zero"` removes menu
22
22
  padding for a child that already owns row insets, `compact` uses 4px, and
23
23
  `standard` uses the shared 8px surface inset. Keep the dropdown trigger named and
24
24
  preserve `data-morph-skip-children` when its upgraded light-DOM items must retain
@@ -44,7 +44,8 @@ const dispose = wireTabScaffold(root, {
44
44
  });
45
45
  ```
46
46
 
47
- Each `TabScaffoldTab` has an `id`, `label`, optional `icon`, and `content`. The
47
+ Each `TabScaffoldTab` has an `id`, `label`, optional `icon`, optional `badge`
48
+ and `badgeLabel` (see [Tab badges](#tab-badges)), and `content`. The
48
49
  bottom bar paints through the home-indicator safe area and pads for it and for
49
50
  the side insets, while each scene pads for the top and side insets (a scene
50
51
  whose only child is a `NavStack` or `Pane` lets that child own them; see
@@ -52,3 +53,51 @@ whose only child is a `NavStack` or `Pane` lets that child own them; see
52
53
  truncate horizontally with an ellipsis when a destination name exceeds its
53
54
  share of the bar. On larger device classes, promote the tab set to a
54
55
  `Workbench` rail or a persistent sidebar instead of a bottom bar.
56
+
57
+ ## Tab badges
58
+
59
+ Give a tab a `badge` (a count or short string such as `3`, `"99+"`, or
60
+ `"New"`) to show the iOS tab-bar badge: a compact, solid `danger` `Badge` at the
61
+ top-trailing corner of the tab's icon, growing outward as the text lengthens
62
+ (without an icon it sits above the label). The badge is decorative
63
+ (`aria-hidden`); its meaning reaches assistive technology through `badgeLabel`,
64
+ which the tab folds into its accessible name as `"<label>, <badgeLabel>"`:
65
+
66
+ ```tsx
67
+ {
68
+ id: "inbox",
69
+ label: "Inbox",
70
+ icon: <LucideIcon icon={Inbox} name="inbox" />,
71
+ badge: unread.value,
72
+ badgeLabel: `${unread.value} unread`,
73
+ content: <InboxStack />,
74
+ }
75
+ ```
76
+
77
+ `badgeLabel` defaults to the badge text itself, so supply a localized phrase
78
+ whenever the bare text would be ambiguous. Omitting `badge` (or passing an
79
+ empty string or a non-finite number) renders no badge and leaves the tab's name
80
+ as its label; `0` renders, so pass `undefined` when a count should disappear.
81
+ Keep the text short — clamp large counts (`"99+"`) in the app. The tone follows
82
+ the platform convention and is fixed; do not restyle the badge.
83
+
84
+ ### Dot badges
85
+
86
+ For new content without a count, pass `badge: true`. The tab shows the iOS
87
+ tab-bar dot — an 8px solid `danger` `Badge` (`size="dot"`) centered on the
88
+ icon's top-trailing corner (above the label without an icon). A dot has no
89
+ text to fall back on, so `badgeLabel` is required by the types:
90
+
91
+ ```tsx
92
+ {
93
+ id: "feed",
94
+ label: "Feed",
95
+ icon: <LucideIcon icon={Rss} name="rss" />,
96
+ badge: hasNewPosts.value ? true : undefined,
97
+ badgeLabel: "New activity",
98
+ content: <FeedStack />,
99
+ }
100
+ ```
101
+
102
+ The tab's accessible name becomes `"Feed, New activity"`. Pass `undefined` to
103
+ clear the dot; `false` is not a badge value.
@@ -11,22 +11,22 @@ public-signature artifact.
11
11
 
12
12
  ## Strengthened contracts
13
13
 
14
- | ID | Compile-time guarantee |
15
- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
- | `KUI-T001` | `ListHeader` is a passive heading, a fully named/icon-bearing trailing action, or a controlled disclosure with `action` + `expanded`. Props ignored by the selected mode are rejected. |
17
- | `KUI-T002` | `Select` has a visible `label` or an `ariaLabel`. |
18
- | `KUI-T003` | A literal `Select.value` belongs to its literal `choices`. |
19
- | `KUI-T004` | A literal `SegmentedControl.value` belongs to its literal `choices`. |
20
- | `KUI-T005` | `expanded`, `expandAction`, and `expandLabel` exist only on `TokenSearchField({ collapsible: true })`. |
21
- | `KUI-T006` | `TokenSearchEditorAttributes` accepts application `data-*` metadata but rejects component-owned identity, morph, count, and placeholder attributes. |
22
- | `KUI-T007` | `ToolbarText.maxLines` requires `wrap: true`; it is never silently ignored in typed code. |
23
- | `KUI-T008` | A literal `TabScaffold.active` id belongs to its literal `tabs`. |
24
- | `KUI-T009` | Adjacent-token keyboard removal requires `onRemoveToken`; disabling removal rejects the now-meaningless callback. |
25
- | `KUI-T010` | Finite public variants have named exported union types, including divider sides, tab activation, banner urgency, and all AppTab, TabBar, ToolbarControlGroup, and Select presentation axes; the convenience root barrel re-exports them. |
26
- | `KUI-T011` | Semantic component zones use the recursive `KerfUiContent` type: `SafeHtml`, runtime-empty booleans/nullish values, and readonly nested arrays are valid; arbitrary strings, numbers, and signals are rejected. Explicit text positions such as `ListInsetText` retain their text exception. |
27
- | `KUI-T012` | `List.gap` accepts finite `UiSpaceName` shorthands or a complete branded `CssLength`; raw strings and non-standalone `CssLengthExpression` arithmetic are rejected. `calc(plus(...))` promotes a typed expression to a complete value. |
28
- | `KUI-T013` | CSS-valued props preserve property grammar: `List.flex` uses `CssFlex`, `Skeleton` dimensions use `CssSize`/`CssLength`, and choice icons use `CssForegroundColor` (foreground tokens via `uiColor()` or `foregroundColorVar()`; fill aliases such as `uiColor('success')` are rejected); row declaration strings are absent. |
29
- | `KUI-T014` | Stable single-root visual components accept the explicit native `slot` attribute. Multi-root `ResizableRegion` and `CollapsiblePanel` remain excluded because no single returned element can own the assignment, and other arbitrary native attributes remain rejected. |
14
+ | ID | Compile-time guarantee |
15
+ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
16
+ | `KUI-T001` | `ListHeader` is a passive heading, a fully named/icon-bearing trailing action, or a controlled disclosure with `action` + `expanded`. Props ignored by the selected mode are rejected. |
17
+ | `KUI-T002` | `Select` has a visible `label` or an `ariaLabel`. |
18
+ | `KUI-T003` | A literal `Select.value` belongs to its literal `choices`. |
19
+ | `KUI-T004` | A literal `SegmentedControl.value` belongs to its literal `choices`. |
20
+ | `KUI-T005` | `expanded`, `expandAction`, and `expandLabel` exist only on `TokenSearchField({ collapsible: true })`. |
21
+ | `KUI-T006` | `TokenSearchEditorAttributes` accepts application `data-*` metadata but rejects component-owned identity, morph, count, and placeholder attributes. |
22
+ | `KUI-T007` | `ToolbarText.maxLines` requires `wrap: true`; it is never silently ignored in typed code. |
23
+ | `KUI-T008` | A literal `TabScaffold.active` id belongs to its literal `tabs`. |
24
+ | `KUI-T009` | Adjacent-token keyboard removal requires `onRemoveToken`; disabling removal rejects the now-meaningless callback. |
25
+ | `KUI-T010` | Finite public variants have named exported union types, including divider sides, tab activation, banner urgency, and all AppTab, TabBar, ToolbarControlGroup, and Select presentation axes; the convenience root barrel re-exports them. |
26
+ | `KUI-T011` | Semantic component zones use the recursive `KerfUiContent` type: `SafeHtml`, runtime-empty booleans/nullish values, and readonly nested arrays are valid; arbitrary strings, numbers, and signals are rejected. Explicit text positions such as `ListInsetText` retain their text exception. |
27
+ | `KUI-T012` | `List.gap` accepts finite `UiSpaceName` shorthands or a complete branded `CssLength`; raw strings and non-standalone `CssLengthExpression` arithmetic are rejected. `calc(plus(...))` promotes a typed expression to a complete value. |
28
+ | `KUI-T013` | CSS-valued props preserve property grammar: `List.flex` uses `CssFlex`, `Skeleton` dimensions use `CssSize`/`CssLength`, and choice icons use `CssForegroundColor` (foreground tokens via `uiColor()` or `foregroundColorVar()`; fill tokens such as `uiColor('success-fill-quiet')` are rejected); row declaration strings are absent. |
29
+ | `KUI-T014` | Stable single-root visual components accept the explicit native `slot` attribute. Multi-root `ResizableRegion` and `CollapsiblePanel` remain excluded because no single returned element can own the assignment, and other arbitrary native attributes remain rejected. |
30
30
 
31
31
  `KerfUiContent` lets conditionals and mapped component collections be direct
32
32
  siblings without an otherwise-unnecessary `Fragment`:
@@ -123,7 +123,7 @@ move errors into casts without making the integration safer.
123
123
  keyword; replace raw Skeleton dimensions with `px`/`rem`/`em`/`pct` builders;
124
124
  and replace raw choice colors with a foreground token such as
125
125
  `uiColor('success-on-quiet')` or an app-owned `foregroundColorVar()`.
126
- `SelectChoice.color` rejects fill aliases (`uiColor('success')`) and
126
+ `SelectChoice.color` rejects fill tokens (`uiColor('success-fill-quiet')`) and
127
127
  unqualified `colorVar()` values because a fill paints a nearly invisible
128
128
  icon.
129
129
  - Remove `style` from `ListItem` and `ListActionRow`; use `className`, public
package/docs/ux-demo.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # UX catalog contract
2
2
 
3
- Run `npm run dev` from `ui/`. The catalog is a development and review surface, not a second implementation. The development server allows repo-owned assets used by the catalog, and its production bundle uses relative asset URLs so the complete catalog remains intact below preview and proxy paths.
3
+ Run `npm run dev` from `ui/`. The catalog is a development and review surface, not a second implementation. The development server reloads component source edits without a restart: component entries still load through their `dist/browser` wrappers, so each component's reachable CSS is exercised as consumers receive it, but the JavaScript those wrappers re-export (and every other `@kerfjs/ui` subpath) resolves to `src/`, and CSS resolves to `src/` as well. A component that gains a new CSS dependency needs a restart, because the wrapper listing its stylesheets is regenerated only by the build. Production demo builds, previews, and the browser tests use `dist/`. The development server allows repo-owned assets used by the catalog, and its production bundle uses relative asset URLs so the complete catalog remains intact below preview and proxy paths.
4
4
 
5
5
  Every Playwright entry point builds current source before the preview server
6
6
  starts. `npm run test:e2e -- tests/browser/row.spec.ts` and a direct focused
@@ -10,7 +10,11 @@ launch Chromium, Firefox, and WebKit. The build emits
10
10
  `source-freshness.json`; browser coverage compares it with an independent hash
11
11
  of the current `src/` and `ux-demo/` inputs, so a stale `dist-demo` cannot pass
12
12
  after a route or component edit. `npm run demo:serve` remains a preview-only
13
- command for an already-built catalog.
13
+ command for an already-built catalog. The demo-conformance preflight also
14
+ rejects a component route that wraps its `CatalogExampleStack` in a card or
15
+ other container (`catalog-demo/wrapped-example-stack`): single-component demos
16
+ sit directly on the catalog grid, and only composition and recipe routes frame
17
+ their content.
14
18
 
15
19
  Ten lazy production-composition routes live under the `Recipes` category.
16
20
  Each `?component=recipe-*` route uses public package primitives, real wiring,
@@ -25,7 +29,7 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
25
29
  - The master/detail shell groups first-party routes by category with production `ListHeader` and `ListItem` components. Catalog entries can supply short reusable trailing tags; the Web Awesome projection uses them to label exactly the 15 discouraged choices. The 70 ecosystem components live in a separate `Web Awesome` disclosure with seven subgroups, so they remain discoverable without being presented as first-class `@kerfjs/ui` exports. Direct Web Awesome routes open the disclosure and reveal the selected row at wide sizes; narrow layouts keep the expanded list in document flow rather than introducing a competing nested scroller.
26
30
  - The shell itself uses `@kerfjs/ui/layout.css`: sidebars, details, previews, dialogs, and resize specimens share unpadded panes plus self-contained content items. Its sidebar identity uses the Kerf logo beside a vertically centered title, with supporting copy in its own aligned row; the current-view heading is also a toolbar. Collapsing the catalog removes the pane completely and moves its restore action to the main toolbar's leading edge instead of leaving an icon-only rail. The selected view description remains inset below the page toolbar. The checkerboard belongs to the preview scroll owner rather than a viewport-height stage or nested card, and one explicitly repeated SVG tile keeps the pattern continuous through below-fold content. A lightly separated footer keeps live status above compact source, guidance, and relationship groups. The focused `ResizableRegion` specimen stretches its surface through the available stage height and reports committed width in that shared status footer; narrow layouts retain local horizontal access when the pane is resized wider than the stage, and the split remains readable at 200% root scaling. Geometry coverage checks wide, intermediate, narrow, and 200% zoom layouts.
27
31
  - The composer-form recipe demonstrates one coherent visible form surface with toolbar title/supporting-copy hierarchy, fields and actions on the shared 8px control gutter, and 24px major rhythm. Its conditional error or success `StateBanner` is the only nested semantic surface. Reset synchronizes the upgraded Web Awesome fields' live value properties with their empty controlled attributes and announces `Draft reset`; browser coverage preserves behavior and verifies wide, narrow, dark, validation, success, reset, 200% zoom, and forced-colors states.
28
- - The list-detail dialog recipe composes `DialogSurface`, the dialog's own labeled header and close control, `SplitView`, and footer actions. Its `SplitView` sits flush in the dialog body, both panes open with a `ListHeader` on one shared line, the dialog title and header text share the 17px text edge, the value table keeps the 8px outer margin, and the trailing actions keep the 16px footer gutter. Compact devices get a full-screen sheet with a list-to-detail drill-down. Browser geometry coverage protects those relationships at wide and narrow widths.
32
+ - The list-detail dialog recipe is laid out like a `Workbench` on roomy devices: a headerless `DialogSurface` modal whose `SplitView` holds a full-height list sidebar `Pane` with its own toolbar beside a full-height detail `Pane` that carries the primary xl title, the `data-dialog="close"` control, and the record actions in its footer. Compact devices get a full-screen sheet with its own labeled header, footer actions, and a list-to-detail drill-down. Browser geometry coverage protects those relationships at wide and narrow widths.
29
33
  - Catalog-only brand marks, preview dimensions, checkerboard tiles, and the compound display-settings control retain local geometry because they are specimen chrome or intrinsic control anatomy rather than reusable application spacing roles.
30
34
  - The menu composition route demonstrates toolbar/content/footer anatomy, 24px major gaps, 8/1/8 child geometry, the full-width `ListHeader` count-or-badge/action split, logical-end 44px action targets with 18px visuals, and 44px row/header/toolbar targets. Its one chevron is a production 18px `DisclosureArrow` inside a controlled Tools header that actually reveals and hides content; ordinary Projects navigation has no false disclosure affordance. Browser assertions cover stable naming, expanded/content transitions, visual rotation, node identity, reduced motion, RTL alignment and containment, and wide, narrow, dark, and 200% layouts. The focused ListHeader route separately shows the shared neutral count pill for Attachments, Notes (including zero), and Duplicates plus a non-count legacy badge, full-width logical-end actions, and no disclosure without content.
31
35
  - The `DisclosureArrow` route renders the production 18px root-scaled default without a catalog-only size override. Its two native buttons independently control the default right-to-down chevron and a recognizable right-facing replacement arrow with left-to-up custom directions; both use shortest-path rotation and their names stay stable while `aria-expanded` communicates state. Browser coverage verifies the custom clockwise path, pointer and native keyboard activation, focus, narrow and 200%-zoom stacking with contained labels, consumer sizing, and the independent `.5` Web Awesome glyph scale retained by Kerf `Select`.
@@ -47,6 +51,6 @@ deterministic state, and semantic layout owners from the [recipe guide](./recipe
47
51
  - The footer action log is an `aria-live` region, making interactions visible to both people and browser tests.
48
52
  - The focused Toast route calls Web Awesome's documented programmatic stack API and notes that Hot Sheet 2 still uses its own app-level toast implementation; the catalog demonstrates ecosystem support without implying current product adoption.
49
53
  - Browser coverage walks every Kerf, composition, and Web Awesome route, checks every listed Web Awesome custom element is registered in its focused specimen, exercises the ecosystem disclosure and both relationship directions, validates offline source/guidance hrefs and accessible wide/narrow/200%-zoom wrapping, and covers tab selection/reordering/overflow, semantic palettes and scoped overrides, light/dark rendering, toolbar popup and responsive find behavior, single/multiline search alignment, menu alignment, banner urgency, keyboard resizing, live custom-element values, and representative wide/narrow captures.
50
- - The production build keeps the catalog shell and Kerf routes in the entry chunk, then dynamically imports the complete Web Awesome specimen and registration surface when an ecosystem route or the ToolbarControlGroup popup specimen is selected. Directly addressed routes pass through the same loader.
54
+ - The production build keeps the catalog shell and Kerf routes in the entry chunk and moves the third-party code loaded at startup (the Web Awesome select and popup-menu registrations with their Lit and Floating UI runtime) into an eagerly preloaded `vendor` chunk, so neither nears the largest-chunk limit in `demo-bundle-budget.json`; only modules statically reachable from the entry qualify, so lazy-only dependencies stay in their lazy chunks. It then dynamically imports the complete Web Awesome specimen and registration surface when an ecosystem route or the ToolbarControlGroup popup specimen is selected. Directly addressed routes pass through the same loader.
51
55
 
52
56
  Before handoff, inspect the actual captures for readability, context, alignment, clipping, spacing, responsiveness, focus, and obvious defects. Fix and recapture rather than treating the screenshot as proof by itself.
@@ -176,8 +176,7 @@ their open state uses the independent `rotate` property.
176
176
 
177
177
  That scale applies to each Web Awesome component's intrinsic glyph and is
178
178
  independent of Kerf `DisclosureArrow`, whose default box is 18px relative to
179
- the root font size and whose consumer override is
180
- `--kui-disclosure-arrow-size`.
179
+ the root font size and whose consumer override is its typed `size` prop.
181
180
 
182
181
  Tooltip and Popover use arrowless floating surfaces by default, matching Hot
183
182
  Sheet 2. The theme sets Web Awesome's public `--wa-tooltip-arrow-size` token to