@kerfjs/ui 4.4.1 → 5.0.0-beta.10

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 (292) hide show
  1. package/README.md +240 -10
  2. package/ai/component-catalog.json +4766 -0
  3. package/ai/component-catalog.schema.json +79 -0
  4. package/ai/public-api-signatures-v1.md +1327 -0
  5. package/ai/skill.md +73 -11
  6. package/ai/webawesome-jsx-signatures-v1.md +106 -0
  7. package/dist/app-tab.d.ts +14 -2
  8. package/dist/app-tab.js +3 -1
  9. package/dist/browser/app-tab.js +3 -2
  10. package/dist/browser/disclosure-arrow.js +4 -0
  11. package/dist/browser/empty-state.js +3 -3
  12. package/dist/browser/list-action-row.js +4 -0
  13. package/dist/browser/list-header.js +6 -0
  14. package/dist/browser/list-item.js +4 -0
  15. package/dist/browser/loading-spinner.js +2 -2
  16. package/dist/browser/lucide-icon.js +2 -2
  17. package/dist/browser/panel-header.js +7 -0
  18. package/dist/browser/resizable-region.js +2 -2
  19. package/dist/browser/segmented-control.js +3 -2
  20. package/dist/browser/select.js +4 -3
  21. package/dist/browser/skeleton.js +3 -0
  22. package/dist/browser/state-banner.js +3 -2
  23. package/dist/browser/tab-bar.js +2 -2
  24. package/dist/browser/token-search-field.js +4 -0
  25. package/dist/browser/toolbar-control-group.js +2 -2
  26. package/dist/browser/toolbar-text.js +3 -2
  27. package/dist/browser/toolbar.js +2 -2
  28. package/dist/browser/value-table.js +3 -2
  29. package/dist/catalog.d.ts +119 -0
  30. package/dist/catalog.js +143 -0
  31. package/dist/catalog.js.map +1 -0
  32. package/dist/chunk-2Z5TWHKQ.js +34 -0
  33. package/dist/chunk-2Z5TWHKQ.js.map +1 -0
  34. package/dist/chunk-37T3VNIZ.js +19 -0
  35. package/dist/chunk-37T3VNIZ.js.map +1 -0
  36. package/dist/chunk-56RJXPHG.js +43 -0
  37. package/dist/chunk-56RJXPHG.js.map +1 -0
  38. package/dist/chunk-7BR3DO2A.js +31 -0
  39. package/dist/chunk-7BR3DO2A.js.map +1 -0
  40. package/dist/chunk-7JB7VPRI.js +34 -0
  41. package/dist/chunk-7JB7VPRI.js.map +1 -0
  42. package/dist/chunk-7LDCTU7D.js +134 -0
  43. package/dist/chunk-7LDCTU7D.js.map +1 -0
  44. package/dist/{chunk-4HHCYSGT.js → chunk-AQNKJRFH.js} +2 -2
  45. package/dist/{chunk-4HHCYSGT.js.map → chunk-AQNKJRFH.js.map} +1 -1
  46. package/dist/{chunk-MBR3TYNL.js → chunk-AVZHJN6C.js} +3 -3
  47. package/dist/chunk-AVZHJN6C.js.map +1 -0
  48. package/dist/chunk-B24C4A6T.js +22 -0
  49. package/dist/chunk-B24C4A6T.js.map +1 -0
  50. package/dist/chunk-BLK3UEEI.js +23 -0
  51. package/dist/chunk-BLK3UEEI.js.map +1 -0
  52. package/dist/chunk-CVZYNJJ5.js +265 -0
  53. package/dist/chunk-CVZYNJJ5.js.map +1 -0
  54. package/dist/chunk-I7INRJO4.js +17 -0
  55. package/dist/chunk-I7INRJO4.js.map +1 -0
  56. package/dist/chunk-L4OKFDJY.js +24 -0
  57. package/dist/chunk-L4OKFDJY.js.map +1 -0
  58. package/dist/chunk-LC3YSAZC.js +39 -0
  59. package/dist/chunk-LC3YSAZC.js.map +1 -0
  60. package/dist/{chunk-3O7ZEUW2.js → chunk-M5JRI6MN.js} +3 -3
  61. package/dist/chunk-M5JRI6MN.js.map +1 -0
  62. package/dist/chunk-RUNWEPR4.js +10 -0
  63. package/dist/chunk-RUNWEPR4.js.map +1 -0
  64. package/dist/chunk-W5EQ3HYQ.js +42 -0
  65. package/dist/chunk-W5EQ3HYQ.js.map +1 -0
  66. package/dist/{chunk-MQIZAD4A.js → chunk-XXMOARTH.js} +5 -3
  67. package/dist/chunk-XXMOARTH.js.map +1 -0
  68. package/dist/chunk-Y5MRDTLC.js +20 -0
  69. package/dist/chunk-Y5MRDTLC.js.map +1 -0
  70. package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
  71. package/dist/chunk-YGN7OMLT.js.map +1 -0
  72. package/dist/collapsible-panel.d.ts +66 -0
  73. package/dist/collapsible-panel.js +22 -0
  74. package/dist/collapsible-panel.js.map +1 -0
  75. package/dist/device-class.d.ts +62 -0
  76. package/dist/device-class.js +75 -0
  77. package/dist/device-class.js.map +1 -0
  78. package/dist/disclosure-arrow.d.ts +14 -0
  79. package/dist/disclosure-arrow.js +4 -0
  80. package/dist/disclosure-arrow.js.map +1 -0
  81. package/dist/index.d.ts +10 -6
  82. package/dist/index.js +21 -16
  83. package/dist/list-action-row.d.ts +52 -0
  84. package/dist/list-action-row.js +5 -0
  85. package/dist/list-action-row.js.map +1 -0
  86. package/dist/list-header.d.ts +43 -0
  87. package/dist/list-header.js +7 -0
  88. package/dist/list-header.js.map +1 -0
  89. package/dist/list-item.d.ts +33 -0
  90. package/dist/list-item.js +5 -0
  91. package/dist/list-item.js.map +1 -0
  92. package/dist/nav-stack.d.ts +38 -0
  93. package/dist/nav-stack.js +5 -0
  94. package/dist/nav-stack.js.map +1 -0
  95. package/dist/panel-header.d.ts +36 -0
  96. package/dist/panel-header.js +7 -0
  97. package/dist/panel-header.js.map +1 -0
  98. package/dist/resizable-region.d.ts +3 -1
  99. package/dist/resizable-region.js +1 -1
  100. package/dist/segmented-control.d.ts +3 -1
  101. package/dist/segmented-control.js +2 -1
  102. package/dist/select-register.d.ts +5 -1
  103. package/dist/select-register.js +3 -0
  104. package/dist/select-register.js.map +1 -1
  105. package/dist/select.d.ts +5 -2
  106. package/dist/select.js +2 -1
  107. package/dist/skeleton.d.ts +24 -0
  108. package/dist/skeleton.js +3 -0
  109. package/dist/skeleton.js.map +1 -0
  110. package/dist/split-view.d.ts +42 -0
  111. package/dist/split-view.js +21 -0
  112. package/dist/split-view.js.map +1 -0
  113. package/dist/state-banner.d.ts +3 -1
  114. package/dist/state-banner.js +2 -1
  115. package/dist/styles/app-tab.css +152 -0
  116. package/dist/styles/catalog.css +498 -0
  117. package/dist/styles/collapsible-panel.css +146 -0
  118. package/dist/styles/disclosure-arrow.css +23 -0
  119. package/dist/styles/empty-state.css +47 -0
  120. package/dist/styles/foundation.css +347 -0
  121. package/dist/styles/layout.css +73 -0
  122. package/dist/styles/list-action-row.css +160 -0
  123. package/dist/styles/list-header.css +160 -0
  124. package/dist/styles/list-item.css +123 -0
  125. package/dist/styles/loading-spinner.css +21 -0
  126. package/dist/styles/lucide-icon.css +6 -0
  127. package/dist/styles/nav-stack.css +111 -0
  128. package/dist/styles/panel-header.css +64 -0
  129. package/dist/styles/resizable-region.css +111 -0
  130. package/{src → dist/styles}/segmented-control.css +62 -31
  131. package/dist/styles/select.css +138 -0
  132. package/dist/styles/skeleton.css +31 -0
  133. package/dist/styles/split-view.css +35 -0
  134. package/dist/styles/state-banner.css +173 -0
  135. package/dist/styles/styles.css +23 -0
  136. package/dist/styles/tab-bar.css +45 -0
  137. package/dist/styles/tab-scaffold.css +84 -0
  138. package/dist/styles/token-search-field.css +289 -0
  139. package/dist/styles/toolbar-control-group.css +211 -0
  140. package/dist/styles/toolbar-text.css +40 -0
  141. package/dist/styles/toolbar.css +61 -0
  142. package/dist/styles/value-table.css +75 -0
  143. package/{src → dist/styles}/webawesome.css +119 -29
  144. package/dist/styles/workbench.css +94 -0
  145. package/dist/tab-bar.d.ts +8 -1
  146. package/dist/tab-bar.js +1 -1
  147. package/dist/tab-scaffold.d.ts +30 -0
  148. package/dist/tab-scaffold.js +16 -0
  149. package/dist/tab-scaffold.js.map +1 -0
  150. package/dist/token-search-field.d.ts +43 -0
  151. package/dist/token-search-field.js +4 -0
  152. package/dist/token-search-field.js.map +1 -0
  153. package/dist/toolbar-text.d.ts +15 -3
  154. package/dist/toolbar-text.js +2 -1
  155. package/dist/value-table.d.ts +12 -2
  156. package/dist/value-table.js +2 -1
  157. package/dist/webawesome.d.ts +100 -0
  158. package/dist/webawesome.js +3 -0
  159. package/dist/webawesome.js.map +1 -0
  160. package/dist/wire-catalog.d.ts +26 -0
  161. package/dist/wire-catalog.js +48 -0
  162. package/dist/wire-catalog.js.map +1 -0
  163. package/dist/wire-nav-stack.d.ts +15 -0
  164. package/dist/wire-nav-stack.js +88 -0
  165. package/dist/wire-nav-stack.js.map +1 -0
  166. package/dist/wire-resizable-regions.js +1 -1
  167. package/dist/wire-sidebar.d.ts +43 -0
  168. package/dist/wire-sidebar.js +117 -0
  169. package/dist/wire-sidebar.js.map +1 -0
  170. package/dist/wire-tab-bars.d.ts +15 -2
  171. package/dist/wire-tab-bars.js +1 -1
  172. package/dist/wire-tab-scaffold.d.ts +11 -0
  173. package/dist/wire-tab-scaffold.js +16 -0
  174. package/dist/wire-tab-scaffold.js.map +1 -0
  175. package/dist/wire-token-search-fields.d.ts +97 -0
  176. package/dist/wire-token-search-fields.js +5 -0
  177. package/dist/wire-token-search-fields.js.map +1 -0
  178. package/dist/workbench.d.ts +33 -0
  179. package/dist/workbench.js +17 -0
  180. package/dist/workbench.js.map +1 -0
  181. package/docs/accessibility.md +101 -3
  182. package/docs/app-layouts.md +61 -0
  183. package/docs/catalog.md +155 -0
  184. package/docs/collapsible-panel.md +102 -0
  185. package/docs/component-contract.md +169 -4
  186. package/docs/component-selection.md +185 -0
  187. package/docs/design/templates/panel-header/icon-actions-dark.svg +1 -0
  188. package/docs/design/templates/panel-header/icon-actions.svg +1 -0
  189. package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -0
  190. package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -0
  191. package/docs/design/templates/panel-header/no-icon-dark.svg +1 -0
  192. package/docs/design/templates/panel-header/no-icon.svg +1 -0
  193. package/docs/design/templates/panel-header/page-heading-dark.svg +1 -0
  194. package/docs/design/templates/panel-header/page-heading.svg +1 -0
  195. package/docs/design/templates/panel-header/title-only-dark.svg +1 -0
  196. package/docs/design/templates/panel-header/title-only.svg +1 -0
  197. package/docs/design/templates/panel-header-dark.svg +13 -0
  198. package/docs/design/templates/panel-header.svg +13 -0
  199. package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
  200. package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
  201. package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
  202. package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
  203. package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
  204. package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
  205. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
  206. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
  207. package/docs/design/templates/toolbar-control-group-dark.svg +11 -0
  208. package/docs/design/templates/toolbar-control-group.svg +11 -0
  209. package/docs/design/templates.md +83 -0
  210. package/docs/design-philosophy.md +61 -1
  211. package/docs/device-class.md +54 -0
  212. package/docs/examples/command-palette-adapter.tsx +70 -0
  213. package/docs/layout.md +131 -0
  214. package/docs/nav-stack.md +47 -0
  215. package/docs/recipes.md +167 -0
  216. package/docs/split-view.md +49 -0
  217. package/docs/tab-scaffold.md +41 -0
  218. package/docs/ux-demo.md +23 -9
  219. package/docs/webawesome-theme.md +15 -0
  220. package/docs/workbench.md +47 -0
  221. package/llms.txt +123 -14
  222. package/package.json +139 -44
  223. package/ux-demo/recipes/app-shell.tsx +32 -0
  224. package/ux-demo/recipes/collapsible-sidebar.tsx +100 -0
  225. package/ux-demo/recipes/compact-toolbar.tsx +43 -0
  226. package/ux-demo/recipes/composer-form.tsx +63 -0
  227. package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
  228. package/ux-demo/recipes/list-workspace-states.tsx +26 -0
  229. package/ux-demo/recipes/loaders.ts +33 -0
  230. package/ux-demo/recipes/loading-inspector.tsx +81 -0
  231. package/ux-demo/recipes/mount-recipe.ts +43 -0
  232. package/ux-demo/recipes/navigation-sidebar.tsx +26 -0
  233. package/ux-demo/recipes/navigation-stack.tsx +76 -0
  234. package/ux-demo/recipes/recipes.css +448 -0
  235. package/ux-demo/recipes/types.ts +17 -0
  236. package/ux-demo/recipes/workspace-header.tsx +12 -0
  237. package/dist/browser/dialog-header.js +0 -3
  238. package/dist/browser/menu-header.js +0 -3
  239. package/dist/browser/menu-item.js +0 -3
  240. package/dist/browser/page-header.js +0 -3
  241. package/dist/chunk-2PES33HS.js +0 -13
  242. package/dist/chunk-2PES33HS.js.map +0 -1
  243. package/dist/chunk-3O7ZEUW2.js.map +0 -1
  244. package/dist/chunk-723YNJBH.js +0 -17
  245. package/dist/chunk-723YNJBH.js.map +0 -1
  246. package/dist/chunk-GY5WH7TO.js +0 -10
  247. package/dist/chunk-GY5WH7TO.js.map +0 -1
  248. package/dist/chunk-MBR3TYNL.js.map +0 -1
  249. package/dist/chunk-MQIZAD4A.js.map +0 -1
  250. package/dist/chunk-NHXX4JI5.js +0 -28
  251. package/dist/chunk-NHXX4JI5.js.map +0 -1
  252. package/dist/chunk-OPCJRCYO.js +0 -19
  253. package/dist/chunk-OPCJRCYO.js.map +0 -1
  254. package/dist/chunk-T5XNMVOI.js +0 -24
  255. package/dist/chunk-T5XNMVOI.js.map +0 -1
  256. package/dist/chunk-TRNBHPNW.js.map +0 -1
  257. package/dist/chunk-VHW5YNIB.js +0 -17
  258. package/dist/chunk-VHW5YNIB.js.map +0 -1
  259. package/dist/chunk-VKH2JLDZ.js +0 -10
  260. package/dist/chunk-VKH2JLDZ.js.map +0 -1
  261. package/dist/chunk-WY4CAEPG.js +0 -14
  262. package/dist/chunk-WY4CAEPG.js.map +0 -1
  263. package/dist/dialog-header.d.ts +0 -14
  264. package/dist/dialog-header.js +0 -3
  265. package/dist/dialog-header.js.map +0 -1
  266. package/dist/menu-header.d.ts +0 -15
  267. package/dist/menu-header.js +0 -3
  268. package/dist/menu-header.js.map +0 -1
  269. package/dist/menu-item.d.ts +0 -22
  270. package/dist/menu-item.js +0 -3
  271. package/dist/menu-item.js.map +0 -1
  272. package/dist/page-header.d.ts +0 -9
  273. package/dist/page-header.js +0 -3
  274. package/dist/page-header.js.map +0 -1
  275. package/src/app-tab.css +0 -20
  276. package/src/dialog-header.css +0 -9
  277. package/src/empty-state.css +0 -6
  278. package/src/foundation.css +0 -106
  279. package/src/loading-spinner.css +0 -4
  280. package/src/lucide-icon.css +0 -1
  281. package/src/menu-header.css +0 -13
  282. package/src/menu-item.css +0 -12
  283. package/src/page-header.css +0 -3
  284. package/src/resizable-region.css +0 -19
  285. package/src/select.css +0 -18
  286. package/src/state-banner.css +0 -31
  287. package/src/styles.css +0 -18
  288. package/src/tab-bar.css +0 -4
  289. package/src/toolbar-control-group.css +0 -25
  290. package/src/toolbar-text.css +0 -4
  291. package/src/toolbar.css +0 -9
  292. package/src/value-table.css +0 -5
package/llms.txt CHANGED
@@ -1,22 +1,131 @@
1
1
  # @kerfjs/ui
2
2
 
3
- Accessible, production-backed UI primitives for kerf applications.
3
+ > Accessible, production-backed UI primitives for [kerf](https://github.com/brianwestphal/kerf)
4
+ > applications. Components are plain functions returning `SafeHtml`; the app owns
5
+ > state and wires stable `data-action` hooks once. This file orients an LLM to
6
+ > generate correct, good-looking kerf UI — it is not a catalog to browse.
4
7
 
5
- - [Package guide](./README.md)
6
- - [AI skill](./ai/skill.md)
7
- - [Component contract](./docs/component-contract.md)
8
- - [Web Awesome theme contract](./docs/webawesome-theme.md)
9
- - [Accessibility and keyboard contracts](./docs/accessibility.md)
10
- - [Design philosophy](./docs/design-philosophy.md)
11
- - [Apple HIG translation](./docs/apple-hig.md)
12
- - [UX catalog](./docs/ux-demo.md)
8
+ ```bash
9
+ npm install kerfjs @kerfjs/ui # kerfjs is a peer; Web Awesome is an optional peer
10
+ ```
13
11
 
14
- The UX catalog is a category-grouped master/detail review surface with a focused route for every public visual component plus composition demos. Its collapsible ecosystem section also lists all 70 free Web Awesome 3.12 components, each with a focused themed route. Related entries use one grouped `Uses` / `Used by` selector across both sets; the shell, display preferences, and selected component share one semantic theme.
12
+ Import each component from its own subpath (`@kerfjs/ui/list-item`, `@kerfjs/ui/toolbar`,
13
+ …); the browser build pulls that component's reachable CSS automatically, so do not
14
+ maintain a central style list. Configure JSX with `"jsxImportSource": "kerfjs"`.
15
15
 
16
- Public components: `LucideIcon`, `Toolbar`, `ToolbarControlGroup`, `ToolbarText`, `SegmentedControl`, `MenuItem`, `MenuHeader`, `ResizableRegion`, `wireResizableRegions`, `AppTab`, `TabBar`, `wireTabBars`, `reorderTabs`, `PageHeader`, `LoadingSpinner`, `Select`, `StateBanner`, `EmptyState`, `DialogHeader`, and `ValueTable`. `SegmentedControl` is controlled and supports toolbar, rounded-rectangle, and pill presentations with native pressed buttons in sequential Tab order. Semantic state palettes are opinionated, Web Awesome-compatible, and overridable globally, by tone, or per instance.
16
+ ## How to build a screen (retrieval order)
17
17
 
18
- Import visual components from explicit JavaScript subpaths. A CSS-aware browser bundler selects generated wrappers that bring in the foundation, the component CSS, and styles for reachable UI subcomponents; unrelated CSS stays out. The root barrel and `@kerfjs/ui/unstyled` are CSS-free; use `styles.css` with the root barrel only when the complete layer is intentional. Manual CSS exports remain available for custom pipelines, and application overrides follow package styles in the cascade. `@kerfjs/ui/webawesome.css` is an optional CSS-only theme for Web Awesome's free components; it includes Web Awesome's base stylesheet, registers no elements, and remains overridable through public `--wa-*` tokens. CSS, its generated browser wrappers, and `@kerfjs/ui/select/register` are the declared side effects. `kerfjs` is an external peer. The Web Awesome peer is optional and needed for its components. `wireTabBars` includes proximity-based horizontal edge autoscroll during same-bar drag reordering.
18
+ 1. **Pick the component from the interface need**, not by appearance read
19
+ [component-selection.md](./docs/component-selection.md) (the problem→component
20
+ matrix, with imports, wiring, ownership, and alternatives).
21
+ 2. **Get exact signatures before emitting code** — read
22
+ [public-api-signatures-v1.md](./ai/public-api-signatures-v1.md) (real prop names,
23
+ callback shapes, disposer return types).
24
+ 3. **For exhaustive, structured facts** (ids, exports, variants, `publicClasses`,
25
+ relationships, side effects) load the
26
+ [Machine-readable component catalog](./ai/component-catalog.json) — the
27
+ canonical checked-in source; its schema is alongside it.
28
+ 4. **Apply the rules below and in** [skill.md](./ai/skill.md).
19
29
 
20
- The Web Awesome theme renders Tooltip and Popover without pointer arrows by default. Restore them only when the anchor relationship needs the pointer, using `--wa-tooltip-arrow-size`, `--kui-wa-popover-arrow-size`, or one popover's public `--arrow-size`; `without-arrow` remains available for explicit local intent.
30
+ ## Core rules (do the right thing)
21
31
 
22
- Web Awesome catalog coverage means themed and inspectable, not preferred. Consider Popup when it replaces custom anchored positioning. Prefer Kerf Select over direct Dropdown/Dropdown Item/Select/Option composition, SegmentedControl over Button Group, TabBar or SegmentedControl over Web Awesome Tabs, LucideIcon over Web Awesome Icon, and ResizableRegion over Split Panel. ResizableRegion carries Hot Sheet 2's 1px separator plus hover/focus grip. Use Tree/Tree Item, Animated Image, and Comparison only for a specific required behavior; avoid Zoomable Frame.
32
+ - **Reach for the primitive, not for CSS.** The package is built to look right
33
+ *unstyled*. Before adding `padding`, `margin`, `width`, `height`, `border`,
34
+ `background`, a wrapper card, or a decoration, check whether the component, the
35
+ pane, or the content-item already owns it — it almost always does. Prefer a
36
+ component prop or a `--kui-*` token; write CSS only for genuinely new structure,
37
+ and then only join a component's documented `publicClasses` (never reach in by
38
+ tag/id/attribute or an unlisted class).
39
+ - **Never double-inset.** A `.kui-pane` has no padding; its `.kui-content` children
40
+ own the geometry (8px inline margin + 1px border + 8px padding). Do not add a
41
+ container padding on top of a content-item's own margin — pick one owner. Use
42
+ `.kui-content` for 24px major vertical rhythm.
43
+ - **Trust the defaults.** Render a control at its natural size and color (a
44
+ `LucideIcon` is 24px); if it looks wrong, fix the surrounding layout, not the
45
+ control. Never force a component's width/height to size it — that leaves a halo
46
+ or a stretched oval.
47
+ - **Toolbars hold only `ToolbarText` and `ToolbarControlGroup`.** Never put a bare
48
+ button, input, link, or loose markup in a `leading`/`center`/`trailing` zone.
49
+ Wrap every control — `SegmentedControl`, `Select`, a collapsible
50
+ `TokenSearchField`, Web Awesome controls — inside a `ToolbarControlGroup`. A
51
+ toolbar popup menu is a `single` `ToolbarControlGroup` around a `wa-dropdown`
52
+ (its `slot="trigger"` `wa-button` is the toolbar button, kept under
53
+ `data-morph-skip-children`).
54
+ - **`PanelHeader` is *the* panel/dialog/page heading.** A plain toolbar with an
55
+ optional bordered icon group, an extra-large `ToolbarText` title, the trailing
56
+ controls you pass, and an optional subtitle below — do not hand-build a heading
57
+ row or restyle a toolbar into one.
58
+ - **Space by the five-step scale, by relationship not by eye.** `0`
59
+ `--kui-space-none`, `4px` `--kui-space-2xs`, `8px` `--kui-space-xs` (inside a
60
+ group), `16px` `--kui-space-m`, `24px` `--kui-space-l` (between major differing
61
+ regions). 12px/32px are off-scale exceptions.
62
+ - **Panes share one child-owned geometry.** Sidebars, main areas, inspectors, and
63
+ dialogs all use an unpadded `.kui-pane` (optional `.kui-pane__toolbar`, one
64
+ scrolling `.kui-pane__content`, optional `.kui-pane__footer`). A visible pane owns
65
+ its collapse control; a hidden pane's restore moves to the adjacent main
66
+ toolbar's matching edge — no icon-only rail. Keep one scroll owner per region.
67
+ - **State and behavior live in the app.** Components take semantic `data-action`
68
+ strings and controlled values; wire them once with `delegate()`/`delegateActions()`
69
+ or the disposer-returning `wire…` helpers, and own domain copy, transport, and
70
+ policy in a thin adapter. Every element must earn its place — cut chrome, labels,
71
+ and readouts that do not help a person decide or act.
72
+ - **App state is domain/persisted; helpers own only ephemeral mechanics.** Keep
73
+ navigation stacks, tab selection and order, pane sizes, and `collapsed`/`expanded`
74
+ disclosure flags in the app — they are data or persisted layout, and the app reads
75
+ them to render. The `wire…` helpers own the *ephemeral* parts: `wireNavStack` the
76
+ push/pop animation, `wireTabBars` the overflow autoscroll and drag preview,
77
+ `wireResizableRegions` the live drag preview; they report committed changes via
78
+ callbacks. The one richer case is `wireTokenSearchFields`, which manages the
79
+ collapsible field's expand/collapse/focus by default (on, per-behavior opt-out) and
80
+ exposes the `expanded` signal on its handle — bind it in render or hand in your own
81
+ via `collapsible.signals`. Do not hand-roll open/collapse/focus that helper
82
+ provides; but do keep a plain disclosure toggle (a one-line boolean the app already
83
+ renders from) in the app. For real-app composition it also offers a focusout
84
+ keep-open exception (`data-token-search-keep-open` regions or `collapsible.keepOpenOn`,
85
+ so a sibling suggestions/date popover does not collapse an empty field), opt-in
86
+ atomic-chip keyboard (`keyboard`: Backspace/Delete remove the adjacent chip via
87
+ `onRemoveToken`, ArrowRight moves the caret past a trailing chip), and an
88
+ `onEdit({id, editor})` input callback — so the app can drop its own keydown/input
89
+ boilerplate while still owning parsing and controlled token state.
90
+ - **Placeholder loading state, don't rebuild it.** For a value or component that is
91
+ still loading, set `placeholder` on the component (`Select`, `ListHeader`,
92
+ `ListItem`, `ValueTableRow`, `PanelHeader`, `SegmentedControl`, `StateBanner`,
93
+ `AppTab`, `ToolbarText`, `ListActionRow`): it renders its real chrome with value
94
+ slots as subtle **unanimated** skeleton blocks and interactivity disabled, so a
95
+ parent composes a faithful loading view (e.g. an inspector) without hand-rebuilding
96
+ markup. Use the standalone `Skeleton` for a custom slot. Prefer it over `wa-skeleton`
97
+ (the pure-Kerf primitives avoid Web Awesome), and never animate it — use
98
+ `LoadingSpinner` for known busy activity instead.
99
+
100
+ ## Public exports
101
+
102
+ `LucideIcon`, `DisclosureArrow`, `Toolbar`, `ToolbarControlGroup`, `ToolbarText`,
103
+ `PanelHeader`, `ListItem`, `ListActionRow`, `ListHeader`, `SegmentedControl`,
104
+ `Select`, `TokenSearchField` (+ `readTokenSearchField`, `placeTokenSearchCaret`,
105
+ `wireTokenSearchFields`), `AppTab`, `TabBar` (+ `wireTabBars`, `reorderTabs`),
106
+ `ResizableRegion` (+ `wireResizableRegions`), `StateBanner`, `EmptyState`,
107
+ `LoadingSpinner`, `Skeleton`, `ValueTable`, `ValueTableRow`. Whole-screen layouts (opt-in,
108
+ tree-shakeable, declarative with `wire…` helpers, each with a companion CSS import):
109
+ `NavStack`, `SplitView`, `Workbench`, `TabScaffold`, driven by `deviceClass()` from
110
+ `@kerfjs/ui/device-class`. For a standalone collapsible rail/drawer outside a full
111
+ shell, `@kerfjs/ui/collapsible-panel` (`CollapsiblePanel`, `CollapsiblePanelToggle`)
112
+ + `@kerfjs/ui/wire-sidebar` (`wireSidebar`: toggle, focus, compact overlay, persistence).
113
+ `@kerfjs/ui/catalog` is another opt-in shell — a
114
+ `Catalog` component + `wireCatalog` helper for building a component-gallery catalog
115
+ (collapsible category sidebar + preview stage + resources footer) from your own
116
+ entries; controlled/stateless like the layouts. `@kerfjs/ui/webawesome` is a
117
+ type-only boundary for `wa-*` JSX; `@kerfjs/ui/select/register` and the CSS are the
118
+ declared side effects.
119
+
120
+ ## Reference
121
+
122
+ - [skill.md](./ai/skill.md): the full rule set + a common-mistakes table (the shipped AI skill).
123
+ - [design-philosophy.md](./docs/design-philosophy.md): why kerf UI is shaped this way — the decision principles behind the rules above.
124
+ - [layout.md](./docs/layout.md): the pane/content-item geometry, public roles/tokens, and the spacing scale.
125
+ - [component-contract.md](./docs/component-contract.md): what a component owns vs. what the app owns.
126
+ - [accessibility.md](./docs/accessibility.md): keyboard and ARIA contracts per component.
127
+ - [recipes.md](./docs/recipes.md): production compositions (app shell, list-detail dialog, composer form, …) as copyable references.
128
+ - [app-layouts.md](./docs/app-layouts.md): choosing `NavStack`/`SplitView`/`Workbench`/`TabScaffold` by data + interaction + device, and dialog presentation.
129
+ - [webawesome-theme.md](./docs/webawesome-theme.md): the optional Web Awesome theme; catalog coverage means themed-and-supported, not preferred (prefer the Kerf primitive when one exists).
130
+ - [component-catalog.json](./ai/component-catalog.json) + [public-api-signatures-v1.md](./ai/public-api-signatures-v1.md) + [webawesome-jsx-signatures-v1.md](./ai/webawesome-jsx-signatures-v1.md): machine-readable facts and exact signatures.
131
+ - [README.md](./README.md): package overview and the component subpath table.
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "4.4.1",
3
+ "version": "5.0.0-beta.10",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Brian Westphal <brian.westphal@bleugris.com>",
8
- "homepage": "https://brianwestphal.github.io/kerf/docs/ui-package/",
8
+ "homepage": "https://brianwestphal.github.io/kerf/docs/component-packages/",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "https://github.com/brianwestphal/kerf",
@@ -44,6 +44,11 @@
44
44
  "browser": "./dist/browser/lucide-icon.js",
45
45
  "import": "./dist/lucide-icon.js"
46
46
  },
47
+ "./disclosure-arrow": {
48
+ "types": "./dist/disclosure-arrow.d.ts",
49
+ "browser": "./dist/browser/disclosure-arrow.js",
50
+ "import": "./dist/disclosure-arrow.js"
51
+ },
47
52
  "./toolbar": {
48
53
  "types": "./dist/toolbar.d.ts",
49
54
  "browser": "./dist/browser/toolbar.js",
@@ -59,15 +64,20 @@
59
64
  "browser": "./dist/browser/toolbar-text.js",
60
65
  "import": "./dist/toolbar-text.js"
61
66
  },
62
- "./menu-item": {
63
- "types": "./dist/menu-item.d.ts",
64
- "browser": "./dist/browser/menu-item.js",
65
- "import": "./dist/menu-item.js"
67
+ "./list-action-row": {
68
+ "types": "./dist/list-action-row.d.ts",
69
+ "browser": "./dist/browser/list-action-row.js",
70
+ "import": "./dist/list-action-row.js"
71
+ },
72
+ "./list-item": {
73
+ "types": "./dist/list-item.d.ts",
74
+ "browser": "./dist/browser/list-item.js",
75
+ "import": "./dist/list-item.js"
66
76
  },
67
- "./menu-header": {
68
- "types": "./dist/menu-header.d.ts",
69
- "browser": "./dist/browser/menu-header.js",
70
- "import": "./dist/menu-header.js"
77
+ "./list-header": {
78
+ "types": "./dist/list-header.d.ts",
79
+ "browser": "./dist/browser/list-header.js",
80
+ "import": "./dist/list-header.js"
71
81
  },
72
82
  "./resizable-region": {
73
83
  "types": "./dist/resizable-region.d.ts",
@@ -78,11 +88,23 @@
78
88
  "types": "./dist/wire-resizable-regions.d.ts",
79
89
  "import": "./dist/wire-resizable-regions.js"
80
90
  },
91
+ "./device-class": {
92
+ "types": "./dist/device-class.d.ts",
93
+ "import": "./dist/device-class.js"
94
+ },
81
95
  "./app-tab": {
82
96
  "types": "./dist/app-tab.d.ts",
83
97
  "browser": "./dist/browser/app-tab.js",
84
98
  "import": "./dist/app-tab.js"
85
99
  },
100
+ "./catalog": {
101
+ "types": "./dist/catalog.d.ts",
102
+ "import": "./dist/catalog.js"
103
+ },
104
+ "./wire-catalog": {
105
+ "types": "./dist/wire-catalog.d.ts",
106
+ "import": "./dist/wire-catalog.js"
107
+ },
86
108
  "./tab-bar": {
87
109
  "types": "./dist/tab-bar.d.ts",
88
110
  "browser": "./dist/browser/tab-bar.js",
@@ -92,10 +114,37 @@
92
114
  "types": "./dist/wire-tab-bars.d.ts",
93
115
  "import": "./dist/wire-tab-bars.js"
94
116
  },
95
- "./page-header": {
96
- "types": "./dist/page-header.d.ts",
97
- "browser": "./dist/browser/page-header.js",
98
- "import": "./dist/page-header.js"
117
+ "./nav-stack": {
118
+ "types": "./dist/nav-stack.d.ts",
119
+ "import": "./dist/nav-stack.js"
120
+ },
121
+ "./wire-nav-stack": {
122
+ "types": "./dist/wire-nav-stack.d.ts",
123
+ "import": "./dist/wire-nav-stack.js"
124
+ },
125
+ "./split-view": {
126
+ "types": "./dist/split-view.d.ts",
127
+ "import": "./dist/split-view.js"
128
+ },
129
+ "./workbench": {
130
+ "types": "./dist/workbench.d.ts",
131
+ "import": "./dist/workbench.js"
132
+ },
133
+ "./collapsible-panel": {
134
+ "types": "./dist/collapsible-panel.d.ts",
135
+ "import": "./dist/collapsible-panel.js"
136
+ },
137
+ "./wire-sidebar": {
138
+ "types": "./dist/wire-sidebar.d.ts",
139
+ "import": "./dist/wire-sidebar.js"
140
+ },
141
+ "./tab-scaffold": {
142
+ "types": "./dist/tab-scaffold.d.ts",
143
+ "import": "./dist/tab-scaffold.js"
144
+ },
145
+ "./wire-tab-scaffold": {
146
+ "types": "./dist/wire-tab-scaffold.d.ts",
147
+ "import": "./dist/wire-tab-scaffold.js"
99
148
  },
100
149
  "./loading-spinner": {
101
150
  "types": "./dist/loading-spinner.d.ts",
@@ -107,15 +156,33 @@
107
156
  "browser": "./dist/browser/select.js",
108
157
  "import": "./dist/select.js"
109
158
  },
159
+ "./skeleton": {
160
+ "types": "./dist/skeleton.d.ts",
161
+ "browser": "./dist/browser/skeleton.js",
162
+ "import": "./dist/skeleton.js"
163
+ },
110
164
  "./segmented-control": {
111
165
  "types": "./dist/segmented-control.d.ts",
112
166
  "browser": "./dist/browser/segmented-control.js",
113
167
  "import": "./dist/segmented-control.js"
114
168
  },
169
+ "./token-search-field": {
170
+ "types": "./dist/token-search-field.d.ts",
171
+ "browser": "./dist/browser/token-search-field.js",
172
+ "import": "./dist/token-search-field.js"
173
+ },
174
+ "./wire-token-search-fields": {
175
+ "types": "./dist/wire-token-search-fields.d.ts",
176
+ "import": "./dist/wire-token-search-fields.js"
177
+ },
115
178
  "./select/register": {
116
179
  "types": "./dist/select-register.d.ts",
117
180
  "import": "./dist/select-register.js"
118
181
  },
182
+ "./webawesome": {
183
+ "types": "./dist/webawesome.d.ts",
184
+ "import": "./dist/webawesome.js"
185
+ },
119
186
  "./state-banner": {
120
187
  "types": "./dist/state-banner.d.ts",
121
188
  "browser": "./dist/browser/state-banner.js",
@@ -126,10 +193,10 @@
126
193
  "browser": "./dist/browser/empty-state.js",
127
194
  "import": "./dist/empty-state.js"
128
195
  },
129
- "./dialog-header": {
130
- "types": "./dist/dialog-header.d.ts",
131
- "browser": "./dist/browser/dialog-header.js",
132
- "import": "./dist/dialog-header.js"
196
+ "./panel-header": {
197
+ "types": "./dist/panel-header.d.ts",
198
+ "browser": "./dist/browser/panel-header.js",
199
+ "import": "./dist/panel-header.js"
133
200
  },
134
201
  "./value-table": {
135
202
  "types": "./dist/value-table.d.ts",
@@ -140,55 +207,80 @@
140
207
  "types": "./dist/index.d.ts",
141
208
  "import": "./dist/index.js"
142
209
  },
143
- "./foundation.css": "./src/foundation.css",
144
- "./webawesome.css": "./src/webawesome.css",
145
- "./styles.css": "./src/styles.css",
146
- "./lucide-icon.css": "./src/lucide-icon.css",
147
- "./toolbar.css": "./src/toolbar.css",
148
- "./toolbar-control-group.css": "./src/toolbar-control-group.css",
149
- "./toolbar-text.css": "./src/toolbar-text.css",
150
- "./menu-item.css": "./src/menu-item.css",
151
- "./menu-header.css": "./src/menu-header.css",
152
- "./resizable-region.css": "./src/resizable-region.css",
153
- "./app-tab.css": "./src/app-tab.css",
154
- "./tab-bar.css": "./src/tab-bar.css",
155
- "./page-header.css": "./src/page-header.css",
156
- "./loading-spinner.css": "./src/loading-spinner.css",
157
- "./select.css": "./src/select.css",
158
- "./segmented-control.css": "./src/segmented-control.css",
159
- "./state-banner.css": "./src/state-banner.css",
160
- "./empty-state.css": "./src/empty-state.css",
161
- "./dialog-header.css": "./src/dialog-header.css",
162
- "./value-table.css": "./src/value-table.css",
210
+ "./foundation.css": "./dist/styles/foundation.css",
211
+ "./layout.css": "./dist/styles/layout.css",
212
+ "./webawesome.css": "./dist/styles/webawesome.css",
213
+ "./styles.css": "./dist/styles/styles.css",
214
+ "./lucide-icon.css": "./dist/styles/lucide-icon.css",
215
+ "./disclosure-arrow.css": "./dist/styles/disclosure-arrow.css",
216
+ "./toolbar.css": "./dist/styles/toolbar.css",
217
+ "./toolbar-control-group.css": "./dist/styles/toolbar-control-group.css",
218
+ "./toolbar-text.css": "./dist/styles/toolbar-text.css",
219
+ "./list-action-row.css": "./dist/styles/list-action-row.css",
220
+ "./list-item.css": "./dist/styles/list-item.css",
221
+ "./list-header.css": "./dist/styles/list-header.css",
222
+ "./resizable-region.css": "./dist/styles/resizable-region.css",
223
+ "./app-tab.css": "./dist/styles/app-tab.css",
224
+ "./catalog.css": "./dist/styles/catalog.css",
225
+ "./tab-bar.css": "./dist/styles/tab-bar.css",
226
+ "./nav-stack.css": "./dist/styles/nav-stack.css",
227
+ "./split-view.css": "./dist/styles/split-view.css",
228
+ "./workbench.css": "./dist/styles/workbench.css",
229
+ "./collapsible-panel.css": "./dist/styles/collapsible-panel.css",
230
+ "./tab-scaffold.css": "./dist/styles/tab-scaffold.css",
231
+ "./loading-spinner.css": "./dist/styles/loading-spinner.css",
232
+ "./select.css": "./dist/styles/select.css",
233
+ "./skeleton.css": "./dist/styles/skeleton.css",
234
+ "./segmented-control.css": "./dist/styles/segmented-control.css",
235
+ "./token-search-field.css": "./dist/styles/token-search-field.css",
236
+ "./state-banner.css": "./dist/styles/state-banner.css",
237
+ "./empty-state.css": "./dist/styles/empty-state.css",
238
+ "./panel-header.css": "./dist/styles/panel-header.css",
239
+ "./value-table.css": "./dist/styles/value-table.css",
163
240
  "./ai/*": "./ai/*",
164
241
  "./docs/*": "./docs/*"
165
242
  },
166
243
  "files": [
167
244
  "dist",
168
- "src/*.css",
169
245
  "ai",
170
246
  "docs",
247
+ "ux-demo/recipes",
171
248
  "llms.txt",
172
249
  "README.md",
173
250
  "LICENSE"
174
251
  ],
175
252
  "scripts": {
176
- "build": "tsup && node ../scripts/clean-dist-output.mjs dist && node scripts/build-browser-entries.mjs",
253
+ "build": "tsup && node ../scripts/clean-dist-output.mjs dist && node scripts/build-css.mjs && node scripts/build-browser-entries.mjs",
177
254
  "dev": "npm run build && vite --config ux-demo/vite.config.ts",
178
255
  "demo:build": "npm run build && vite build --config ux-demo/vite.config.ts && node scripts/check-demo-bundle.mjs",
179
256
  "demo:serve": "vite preview --config ux-demo/vite.config.ts",
180
- "lint": "eslint src tests ux-demo --max-warnings 0",
257
+ "format:css": "prettier --write \"src/**/*.css\" \"ux-demo/**/*.css\"",
258
+ "check:format": "prettier --check \"src/**/*.css\" \"ux-demo/**/*.css\"",
259
+ "catalog:sync": "node scripts/sync-component-catalog.mjs",
260
+ "check:catalog": "node scripts/sync-component-catalog.mjs --check && node scripts/check-component-catalog.mjs",
261
+ "check:recipes": "node scripts/check-recipes.mjs",
262
+ "check:ai-regressions": "node scripts/check-ai-regressions.mjs && node scripts/audit-ai-regression-results.mjs",
263
+ "check:ai-signatures": "node scripts/sync-ai-public-signatures.mjs --check",
264
+ "ai:signatures:sync": "npm run build && node scripts/sync-ai-public-signatures.mjs",
265
+ "ai:regressions:compile": "node scripts/compile-ai-regression-response.mjs",
266
+ "ai:regressions:prepare": "node scripts/prepare-ai-regression.mjs",
267
+ "ai:regressions:record": "node scripts/record-ai-regression-run.mjs",
268
+ "ai:regressions:score": "node scripts/score-ai-regression.mjs",
269
+ "check:guidance": "node scripts/check-decision-guidance.mjs",
270
+ "lint": "eslint src tests ux-demo docs/examples --max-warnings 0",
181
271
  "typecheck": "node node_modules/typescript7/bin/tsc --noEmit",
182
272
  "test": "npm run test:unit && npm run test:bundle",
183
273
  "test:unit": "vitest run tests/unit --coverage",
184
274
  "test:bundle": "npm run build && vitest run tests/bundle",
275
+ "test:consumer-types": "npm run build && node node_modules/typescript7/bin/tsc -p tests/consumer-types/webawesome/tsconfig.json",
185
276
  "test:e2e": "npm run demo:build && playwright test",
186
- "check": "npm run lint && npm run typecheck && npm test && npm run demo:build && npm pack --dry-run",
187
- "prepublishOnly": "npm run check"
277
+ "check": "npm run check:format && npm run check:catalog && npm run check:recipes && npm run check:guidance && npm run check:ai-regressions && npm run lint && npm run typecheck && npm test && npm run test:consumer-types && npm run check:ai-signatures && npm run demo:build && npm pack --dry-run",
278
+ "prepublishOnly": "npm run check",
279
+ "design-templates:build": "node scripts/build-design-templates.mjs"
188
280
  },
189
281
  "peerDependencies": {
190
282
  "@awesome.me/webawesome": "^3.12.0",
191
- "kerfjs": "^4.0.0",
283
+ "kerfjs": "^5.0.0-0",
192
284
  "lucide": "^1.43.0"
193
285
  },
194
286
  "peerDependenciesMeta": {
@@ -204,11 +296,14 @@
204
296
  "@typescript-eslint/eslint-plugin": "^8.65.0",
205
297
  "@typescript-eslint/parser": "^8.65.0",
206
298
  "@vitest/coverage-v8": "4.1.11",
299
+ "domotion-svg": "^0.29.0",
207
300
  "esbuild": "^0.25.0",
208
301
  "eslint": "^10.8.0",
209
302
  "eslint-plugin-simple-import-sort": "^12.1.1",
210
303
  "happy-dom": "^20.9.0",
211
304
  "lucide": "1.43.0",
305
+ "postcss": "^8.5.28",
306
+ "prettier": "^3.9.6",
212
307
  "tsup": "^8.3.0",
213
308
  "typescript": "^6.0.3",
214
309
  "typescript7": "npm:typescript@^7.0.2",
@@ -0,0 +1,32 @@
1
+ import '@kerfjs/ui/layout.css';
2
+ import './recipes.css';
3
+
4
+ import { ListHeader } from '@kerfjs/ui/list-header';
5
+ import { ListItem } from '@kerfjs/ui/list-item';
6
+ import { LucideIcon } from '@kerfjs/ui/lucide-icon';
7
+ import { PanelHeader } from '@kerfjs/ui/panel-header';
8
+ import { ResizableRegion } from '@kerfjs/ui/resizable-region';
9
+ import { Toolbar } from '@kerfjs/ui/toolbar';
10
+ import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
11
+ import { ToolbarText } from '@kerfjs/ui/toolbar-text';
12
+ import { ValueTable, ValueTableRow } from '@kerfjs/ui/value-table';
13
+ import { signal } from 'kerfjs';
14
+ import { Bell, Folder, Inbox, Settings } from 'lucide';
15
+
16
+ import type { RecipeFactory } from './types.js';
17
+
18
+ export const createRecipe: RecipeFactory = (announce) => {
19
+ const selected = signal('inbox');
20
+ const navigationSize = signal(224);
21
+ const inspectorSize = signal(240);
22
+ const responsivePane = signal<'content' | 'navigation' | 'inspector'>('content');
23
+ const render = () => <section class="kui-recipe recipe-shell kui-recipe__surface kui-layout" data-recipe="recipe-app-shell" data-responsive-pane={responsivePane.value}>
24
+ <Toolbar label="Atlas workspace" leading={<ToolbarControlGroup appearance="borderless" single><ToolbarText text="Atlas" /></ToolbarControlGroup>} trailing={<div class="recipe-shell__toolbar-actions"><ToolbarControlGroup className="recipe-shell__responsive-controls" appearance="borderless" label="Workspace panes"><button type="button" aria-label="Show navigation" aria-controls="recipe-shell-navigation" aria-pressed={String(responsivePane.value === 'navigation')} data-action="recipe-action" data-recipe-command="show-navigation"><LucideIcon icon={Folder} name="folder" /></button><button type="button" aria-label="Show content" aria-controls="recipe-shell-content" aria-pressed={String(responsivePane.value === 'content')} data-action="recipe-action" data-recipe-command="show-content"><LucideIcon icon={Inbox} name="inbox" /></button><button type="button" aria-label="Show inspector" aria-controls="recipe-shell-inspector" aria-pressed={String(responsivePane.value === 'inspector')} data-action="recipe-action" data-recipe-command="show-inspector"><LucideIcon icon={Settings} name="settings" /></button></ToolbarControlGroup><ToolbarControlGroup appearance="borderless" label="Workspace controls"><button type="button" aria-label="Notifications" data-action="recipe-action" data-recipe-command="notify"><LucideIcon icon={Bell} name="bell" /></button><button type="button" aria-label="Settings" data-action="recipe-action" data-recipe-command="settings"><LucideIcon icon={Settings} name="settings" /></button></ToolbarControlGroup></div>} />
25
+ <div class="recipe-shell__body">
26
+ <div id="recipe-shell-navigation" class="recipe-shell__nav-region"><ResizableRegion id="recipe-navigation" label="Navigation" size={navigationSize.value} min={180} max={320}><aside class="recipe-shell__nav kui-pane"><ListHeader label="Workspace" /><nav class="recipe-shell__nav-list kui-pane__content kui-content" aria-label="Workspace"><section><ListItem action="recipe-action" itemId="inbox" label="Inbox" icon={<LucideIcon icon={Inbox} name="inbox" />} selected={selected.value === 'inbox'} /><ListItem action="recipe-action" itemId="projects" label="Projects with a deliberately wrapping title" icon={<LucideIcon icon={Folder} name="folder" />} selected={selected.value === 'projects'} multiline /></section></nav></aside></ResizableRegion></div>
27
+ <main id="recipe-shell-content" class="recipe-shell__main kui-pane"><PanelHeader title={selected.value === 'inbox' ? 'Inbox triage' : 'Active projects'} titleId="recipe-shell-main-title" actions={<button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="new">New task</button>} /><div class="recipe-shell__main-body kui-pane__content kui-content"><p class="kui-recipe__ownership kui-content-item">Recipe owns pane geometry and one scroll owner per pane. The app owns routing, data, pane visibility, sizes, and persistence.</p><div class="recipe-shell__cards">{['Release accessibility audit', 'Prepare tablet navigation', 'Review stale-data states', 'Confirm package boundaries'].map((title) => <article class="recipe-shell__card kui-content-item"><strong>{title}</strong><p class="kui-recipe__muted">Assigned to the interface systems team · due this week</p></article>)}</div></div></main>
28
+ <div id="recipe-shell-inspector" class="recipe-shell__inspector-region"><ResizableRegion id="recipe-inspector" label="Inspector" size={inspectorSize.value} min={200} max={360} edge="start"><aside class="recipe-shell__inspector kui-pane"><PanelHeader title="Inspector" titleId="recipe-shell-inspector-title" /><div class="recipe-shell__inspector-body kui-pane__content kui-content"><ValueTable label="Selected task"><ValueTableRow label="Status" value="In review" /><ValueTableRow label="Owner" value="Mara Chen" /><ValueTableRow label="Priority" value="High" /></ValueTable></div></aside></ResizableRegion></div>
29
+ </div>
30
+ </section>;
31
+ return { render, action(command, element) { const pane = command.match(/^show-(navigation|content|inspector)$/)?.[1] as typeof responsivePane.value | undefined; if (pane) responsivePane.value = pane; const id = element.dataset.itemId; if (id) selected.value = id; announce(id ? `Opened ${id}` : pane ? `Showing ${pane}` : command === 'new' ? 'New task requested' : `${command} requested`); }, resize(id, size) { if (id === 'recipe-navigation') navigationSize.value = size; if (id === 'recipe-inspector') inspectorSize.value = size; announce(`${id} resized to ${size}px`); } };
32
+ };
@@ -0,0 +1,100 @@
1
+ import '@kerfjs/ui/layout.css';
2
+ import '@kerfjs/ui/collapsible-panel.css';
3
+ import './recipes.css';
4
+
5
+ import { CollapsiblePanel, CollapsiblePanelToggle } from '@kerfjs/ui/collapsible-panel';
6
+ import { deviceClass } from '@kerfjs/ui/device-class';
7
+ import { ListHeader } from '@kerfjs/ui/list-header';
8
+ import { ListItem } from '@kerfjs/ui/list-item';
9
+ import { LucideIcon } from '@kerfjs/ui/lucide-icon';
10
+ import { PanelHeader } from '@kerfjs/ui/panel-header';
11
+ import { Toolbar } from '@kerfjs/ui/toolbar';
12
+ import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
13
+ import { ToolbarText } from '@kerfjs/ui/toolbar-text';
14
+ import { type SidebarStorage, wireSidebar } from '@kerfjs/ui/wire-sidebar';
15
+ import { signal } from 'kerfjs';
16
+ import { Bell, Folder, Inbox, Users } from 'lucide';
17
+
18
+ import type { RecipeFactory } from './types.js';
19
+
20
+ const RAIL_ACTION = 'recipe-sidebar-rail';
21
+ const DRAWER_ACTION = 'recipe-sidebar-drawer';
22
+
23
+ /**
24
+ * A memory-backed store so the recipe demonstrates the persistence hook without
25
+ * writing to the shared `localStorage` the catalog page runs in.
26
+ */
27
+ function memoryStorage(): SidebarStorage {
28
+ const map = new Map<string, string>();
29
+ return { getItem: (key) => map.get(key) ?? null, setItem: (key, value) => { map.set(key, value); } };
30
+ }
31
+
32
+ export const createRecipe: RecipeFactory = (announce) => {
33
+ const selected = signal('inbox');
34
+ const railCollapsed = signal(false);
35
+ const drawerCollapsed = signal(true);
36
+ const storage = memoryStorage();
37
+ // A shared window-backed device class: when the viewport is compact the wire
38
+ // switches the rail/drawer to a dismissable overlay presentation.
39
+ const device = deviceClass();
40
+
41
+ const railToggle = (inPanel: boolean) => <CollapsiblePanelToggle side="left" collapsed={railCollapsed.value} action={RAIL_ACTION} panelId="sidebar-rail" label={railCollapsed.value ? 'Show navigation' : 'Hide navigation'} className={inPanel ? '' : 'recipe-collapsible-sidebar__reveal'} />;
42
+ const drawerToggle = () => <CollapsiblePanelToggle side="bottom" collapsed={drawerCollapsed.value} action={DRAWER_ACTION} panelId="sidebar-console" label={drawerCollapsed.value ? 'Show activity' : 'Hide activity'} />;
43
+
44
+ const render = () => <section class="kui-recipe recipe-collapsible-sidebar kui-recipe__surface" data-recipe="recipe-collapsible-sidebar" data-rail-collapsed={String(railCollapsed.value)} data-drawer-collapsed={String(drawerCollapsed.value)}>
45
+ <CollapsiblePanel id="sidebar-rail" side="left" size={232} collapsed={railCollapsed.value} label="Workspace navigation" className="recipe-collapsible-sidebar__rail kui-pane">
46
+ <div class="recipe-collapsible-sidebar__rail-head">
47
+ <ToolbarText text="Atlas" />
48
+ <ToolbarControlGroup appearance="borderless" single>{railToggle(true)}</ToolbarControlGroup>
49
+ </div>
50
+ <nav class="recipe-collapsible-sidebar__nav kui-pane__content kui-content" aria-label="Workspace">
51
+ <section>
52
+ <ListHeader label="Workspace" />
53
+ <ListItem action="recipe-action" itemId="inbox" label="Inbox" icon={<LucideIcon icon={Inbox} name="inbox" />} trailing={<span>12</span>} selected={selected.value === 'inbox'} />
54
+ <ListItem action="recipe-action" itemId="projects" label="Projects" icon={<LucideIcon icon={Folder} name="folder" />} selected={selected.value === 'projects'} />
55
+ <ListItem action="recipe-action" itemId="shared" label="Shared with me" icon={<LucideIcon icon={Users} name="users" />} selected={selected.value === 'shared'} />
56
+ </section>
57
+ </nav>
58
+ </CollapsiblePanel>
59
+ <main class="recipe-collapsible-sidebar__main kui-pane">
60
+ <PanelHeader
61
+ title={selected.value === 'projects' ? 'Projects' : selected.value === 'shared' ? 'Shared with me' : 'Inbox'}
62
+ titleId="recipe-collapsible-main-title"
63
+ icon={railToggle(false)}
64
+ actions={<ToolbarControlGroup appearance="borderless" single>{drawerToggle()}</ToolbarControlGroup>}
65
+ />
66
+ <div class="recipe-collapsible-sidebar__body kui-pane__content kui-content">
67
+ <p class="kui-recipe__ownership kui-content-item">The app owns each panel's <code>collapsed</code> signal, sizes, and content; <code>wireSidebar</code> owns the toggle, focus move/restore, the compact overlay, and persistence.</p>
68
+ <article class="recipe-collapsible-sidebar__card kui-content-item"><strong>Narrow the window to a compact width</strong><p class="kui-recipe__muted">The rail and drawer become a dismissable overlay: a backdrop, Escape, and a trapped Tab ring, all managed by the wire.</p></article>
69
+ </div>
70
+ </main>
71
+ <CollapsiblePanel id="sidebar-console" side="bottom" size={168} collapsed={drawerCollapsed.value} label="Activity" className="recipe-collapsible-sidebar__drawer kui-pane">
72
+ <Toolbar label="Activity" divider leading={<ToolbarControlGroup appearance="borderless" single><ToolbarText text="Activity" size="small" /></ToolbarControlGroup>} trailing={<ToolbarControlGroup appearance="borderless" single>{drawerToggle()}</ToolbarControlGroup>} />
73
+ <div class="recipe-collapsible-sidebar__drawer-body kui-pane__content kui-content">
74
+ <ul class="recipe-collapsible-sidebar__log">
75
+ <li><LucideIcon icon={Bell} name="bell" />Deploy finished · 2m ago</li>
76
+ <li><LucideIcon icon={Bell} name="bell" />Review requested · 9m ago</li>
77
+ </ul>
78
+ </div>
79
+ </CollapsiblePanel>
80
+ </section>;
81
+
82
+ return {
83
+ render,
84
+ action(command, element) {
85
+ const id = element.dataset.itemId;
86
+ if (id) { selected.value = id; announce(`Opened ${id}`); return; }
87
+ announce(`${command} requested`);
88
+ },
89
+ wire(root) {
90
+ return wireSidebar(root, {
91
+ deviceClass: device,
92
+ storage,
93
+ panels: [
94
+ { id: 'sidebar-rail', collapsed: railCollapsed, toggleAction: RAIL_ACTION, storageKey: 'recipe-sidebar-rail' },
95
+ { id: 'sidebar-console', collapsed: drawerCollapsed, toggleAction: DRAWER_ACTION, storageKey: 'recipe-sidebar-console' },
96
+ ],
97
+ });
98
+ },
99
+ };
100
+ };