@kerfjs/ui 5.0.0-beta.17 → 5.0.0-beta.21

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 +221 -81
  2. package/ai/application-ui-diagnostic-ids-v1.json +92 -0
  3. package/ai/application-ui-profile-sync.cjs +529 -0
  4. package/ai/application-ui-profile.d.mts +1 -0
  5. package/ai/application-ui-profile.d.ts +102 -0
  6. package/ai/application-ui-profile.defaults.json +41 -0
  7. package/ai/application-ui-profile.mjs +626 -0
  8. package/ai/application-ui-profile.schema.json +130 -0
  9. package/ai/catalog-authoring.json +25 -0
  10. package/ai/catalog-authoring.schema.json +61 -0
  11. package/ai/compile-time-contracts-v1.json +101 -0
  12. package/ai/compile-time-contracts-v1.schema.json +49 -0
  13. package/ai/component-catalog-extension-v2.d.ts +7 -0
  14. package/ai/component-catalog-extension-v2.schema.json +7 -0
  15. package/ai/component-catalog-extension.schema.json +79 -0
  16. package/ai/component-catalog-v2-overrides.json +326 -0
  17. package/ai/component-catalog-v2.d.ts +67 -0
  18. package/ai/component-catalog-v2.json +4441 -0
  19. package/ai/component-catalog-v2.schema.json +246 -0
  20. package/ai/component-catalog.json +649 -6
  21. package/ai/component-catalog.schema.json +15 -0
  22. package/ai/public-api-signatures-v1.md +357 -91
  23. package/ai/skill.md +95 -50
  24. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  25. package/analyzer/cli.mjs +46 -0
  26. package/analyzer/index.d.mts +45 -0
  27. package/analyzer/index.mjs +743 -0
  28. package/analyzer/report.schema.json +66 -0
  29. package/dist/app-tab.d.ts +1 -1
  30. package/dist/app-tab.js +1 -1
  31. package/dist/browser/pane.js +3 -0
  32. package/dist/browser/sunken-panel.js +3 -0
  33. package/dist/catalog-resources.d.ts +34 -0
  34. package/dist/catalog-resources.js +22 -0
  35. package/dist/catalog-resources.js.map +1 -0
  36. package/dist/catalog.d.ts +29 -4
  37. package/dist/catalog.js +302 -84
  38. package/dist/catalog.js.map +1 -1
  39. package/dist/chunk-5VTB2BKP.js +35 -0
  40. package/dist/chunk-5VTB2BKP.js.map +1 -0
  41. package/dist/chunk-6WTETUZ6.js +21 -0
  42. package/dist/chunk-6WTETUZ6.js.map +1 -0
  43. package/dist/chunk-7KEUJIIC.js +218 -0
  44. package/dist/chunk-7KEUJIIC.js.map +1 -0
  45. package/dist/chunk-7L6EHUMV.js +31 -0
  46. package/dist/chunk-7L6EHUMV.js.map +1 -0
  47. package/dist/{chunk-XXMOARTH.js → chunk-AASLYRGE.js} +57 -16
  48. package/dist/chunk-AASLYRGE.js.map +1 -0
  49. package/dist/chunk-BBRXX6PF.js +108 -0
  50. package/dist/chunk-BBRXX6PF.js.map +1 -0
  51. package/dist/{chunk-6NLRBSVX.js → chunk-CN2XNTAM.js} +129 -72
  52. package/dist/chunk-CN2XNTAM.js.map +1 -0
  53. package/dist/chunk-DPDBVKNA.js +113 -0
  54. package/dist/chunk-DPDBVKNA.js.map +1 -0
  55. package/dist/chunk-DSNIYOZ3.js +114 -0
  56. package/dist/chunk-DSNIYOZ3.js.map +1 -0
  57. package/dist/chunk-ELSJX4W7.js +68 -0
  58. package/dist/chunk-ELSJX4W7.js.map +1 -0
  59. package/dist/{chunk-AHQI52QO.js → chunk-FEY65TBV.js} +6 -2
  60. package/dist/chunk-FEY65TBV.js.map +1 -0
  61. package/dist/chunk-JRKSK2HX.js +79 -0
  62. package/dist/chunk-JRKSK2HX.js.map +1 -0
  63. package/dist/chunk-K57KRAOV.js +117 -0
  64. package/dist/chunk-K57KRAOV.js.map +1 -0
  65. package/dist/chunk-LPOMRC4C.js +25 -0
  66. package/dist/chunk-LPOMRC4C.js.map +1 -0
  67. package/dist/chunk-M7JBER5U.js +41 -0
  68. package/dist/chunk-M7JBER5U.js.map +1 -0
  69. package/dist/chunk-NENBUYX5.js +79 -0
  70. package/dist/chunk-NENBUYX5.js.map +1 -0
  71. package/dist/chunk-OHU6CI22.js +83 -0
  72. package/dist/chunk-OHU6CI22.js.map +1 -0
  73. package/dist/{chunk-YGN7OMLT.js → chunk-OZWU5ZPZ.js} +2 -2
  74. package/dist/chunk-OZWU5ZPZ.js.map +1 -0
  75. package/dist/chunk-PIDTEQ3U.js +105 -0
  76. package/dist/chunk-PIDTEQ3U.js.map +1 -0
  77. package/dist/chunk-PX3YSS5C.js +32 -0
  78. package/dist/chunk-PX3YSS5C.js.map +1 -0
  79. package/dist/chunk-PZQVSDLH.js +61 -0
  80. package/dist/chunk-PZQVSDLH.js.map +1 -0
  81. package/dist/chunk-QO55FN2Y.js +20 -0
  82. package/dist/chunk-QO55FN2Y.js.map +1 -0
  83. package/dist/chunk-QQFJMRIB.js +45 -0
  84. package/dist/chunk-QQFJMRIB.js.map +1 -0
  85. package/dist/chunk-RYRIFZCT.js +58 -0
  86. package/dist/chunk-RYRIFZCT.js.map +1 -0
  87. package/dist/{chunk-7BR3DO2A.js → chunk-SRSJO5QE.js} +13 -4
  88. package/dist/chunk-SRSJO5QE.js.map +1 -0
  89. package/dist/chunk-VLYV65LK.js +38 -0
  90. package/dist/chunk-VLYV65LK.js.map +1 -0
  91. package/dist/chunk-W4CXQYSG.js +23 -0
  92. package/dist/chunk-W4CXQYSG.js.map +1 -0
  93. package/dist/{chunk-WXMGNX2Y.js → chunk-W7YD6E4N.js} +2 -2
  94. package/dist/chunk-W7YD6E4N.js.map +1 -0
  95. package/dist/{chunk-56RJXPHG.js → chunk-Y6LYKFXY.js} +1 -1
  96. package/dist/{chunk-56RJXPHG.js.map → chunk-Y6LYKFXY.js.map} +1 -1
  97. package/dist/collapsible-panel.d.ts +2 -2
  98. package/dist/collapsible-panel.js +47 -6
  99. package/dist/collapsible-panel.js.map +1 -1
  100. package/dist/device-class.js +33 -5
  101. package/dist/device-class.js.map +1 -1
  102. package/dist/disclosure-arrow.js +1 -1
  103. package/dist/empty-state.d.ts +1 -1
  104. package/dist/empty-state.js +1 -1
  105. package/dist/floating-toolbar.d.ts +1 -1
  106. package/dist/floating-toolbar.js +1 -1
  107. package/dist/index.d.ts +6 -4
  108. package/dist/index.js +27 -25
  109. package/dist/list-action-row.d.ts +1 -1
  110. package/dist/list-action-row.js +1 -1
  111. package/dist/list-header.d.ts +30 -9
  112. package/dist/list-header.js +1 -1
  113. package/dist/list-inset-control.d.ts +1 -1
  114. package/dist/list-inset-control.js +1 -1
  115. package/dist/list-inset-text.d.ts +1 -1
  116. package/dist/list-inset-text.js +1 -1
  117. package/dist/list-item.d.ts +1 -1
  118. package/dist/list-item.js +1 -1
  119. package/dist/loading-spinner.js +1 -1
  120. package/dist/nav-stack.d.ts +1 -1
  121. package/dist/nav-stack.js +1 -1
  122. package/dist/pane.d.ts +49 -0
  123. package/dist/pane.js +4 -0
  124. package/dist/pane.js.map +1 -0
  125. package/dist/panel-header.d.ts +10 -4
  126. package/dist/panel-header.js +1 -1
  127. package/dist/resizable-region.d.ts +1 -1
  128. package/dist/resizable-region.js +1 -1
  129. package/dist/segmented-control.d.ts +6 -6
  130. package/dist/segmented-control.js +1 -1
  131. package/dist/select.d.ts +11 -5
  132. package/dist/select.js +1 -1
  133. package/dist/skeleton.d.ts +1 -1
  134. package/dist/skeleton.js +1 -1
  135. package/dist/split-view.d.ts +1 -1
  136. package/dist/split-view.js +70 -10
  137. package/dist/split-view.js.map +1 -1
  138. package/dist/state-banner.d.ts +4 -3
  139. package/dist/state-banner.js +1 -1
  140. package/dist/styles/catalog.css +32 -6
  141. package/dist/styles/layout.css +2 -20
  142. package/dist/styles/pane.css +62 -0
  143. package/dist/styles/segmented-control.css +3 -7
  144. package/dist/styles/sunken-panel.css +18 -0
  145. package/dist/sunken-panel.d.ts +16 -0
  146. package/dist/sunken-panel.js +3 -0
  147. package/dist/sunken-panel.js.map +1 -0
  148. package/dist/tab-bar.d.ts +4 -3
  149. package/dist/tab-bar.js +1 -1
  150. package/dist/tab-scaffold.d.ts +6 -6
  151. package/dist/tab-scaffold.js +43 -8
  152. package/dist/tab-scaffold.js.map +1 -1
  153. package/dist/token-search-field.d.ts +25 -9
  154. package/dist/token-search-field.js +1 -1
  155. package/dist/toolbar-control-group.d.ts +10 -6
  156. package/dist/toolbar-control-group.js +1 -1
  157. package/dist/toolbar-text.d.ts +10 -11
  158. package/dist/toolbar-text.js +1 -1
  159. package/dist/toolbar.d.ts +1 -1
  160. package/dist/toolbar.js +1 -1
  161. package/dist/value-table.d.ts +2 -2
  162. package/dist/value-table.js +1 -1
  163. package/dist/wire-catalog.d.ts +31 -2
  164. package/dist/wire-catalog.js +203 -8
  165. package/dist/wire-catalog.js.map +1 -1
  166. package/dist/wire-nav-stack.js +22 -7
  167. package/dist/wire-nav-stack.js.map +1 -1
  168. package/dist/wire-resizable-regions.d.ts +1 -1
  169. package/dist/wire-resizable-regions.js +1 -1
  170. package/dist/wire-sidebar.js +52 -41
  171. package/dist/wire-sidebar.js.map +1 -1
  172. package/dist/wire-tab-bars.d.ts +5 -2
  173. package/dist/wire-tab-bars.js +1 -1
  174. package/dist/wire-tab-scaffold.js +9 -4
  175. package/dist/wire-tab-scaffold.js.map +1 -1
  176. package/dist/wire-token-search-fields.d.ts +12 -7
  177. package/dist/wire-token-search-fields.js +1 -1
  178. package/dist/workbench.d.ts +1 -1
  179. package/dist/workbench.js +56 -9
  180. package/dist/workbench.js.map +1 -1
  181. package/docs/app-layouts.md +7 -7
  182. package/docs/catalog.md +249 -46
  183. package/docs/collapsible-panel.md +38 -11
  184. package/docs/component-contract.md +132 -7
  185. package/docs/component-selection.md +79 -67
  186. package/docs/design/templates.md +1 -1
  187. package/docs/design-philosophy.md +7 -9
  188. package/docs/device-class.md +8 -8
  189. package/docs/examples/application-ui-profile.json +60 -0
  190. package/docs/examples/command-palette-adapter.tsx +67 -17
  191. package/docs/examples/component-catalog-extension-v2.json +81 -0
  192. package/docs/examples/component-catalog-extension.json +45 -0
  193. package/docs/layout.md +51 -32
  194. package/docs/nav-stack.md +15 -6
  195. package/docs/recipes.md +4 -4
  196. package/docs/split-view.md +3 -3
  197. package/docs/sunken-panel.md +43 -0
  198. package/docs/tab-scaffold.md +16 -7
  199. package/docs/type-contracts.md +99 -0
  200. package/docs/ui-analyzer.md +81 -0
  201. package/docs/ui-doctor.md +77 -0
  202. package/docs/ui-evaluator.md +122 -0
  203. package/docs/webawesome-theme.md +8 -8
  204. package/docs/workbench.md +18 -5
  205. package/doctor/cli.mjs +115 -0
  206. package/doctor/config.schema.json +68 -0
  207. package/doctor/index.d.mts +116 -0
  208. package/doctor/index.mjs +1256 -0
  209. package/doctor/report.schema.json +118 -0
  210. package/evaluator/cli.mjs +89 -0
  211. package/evaluator/index.d.mts +90 -0
  212. package/evaluator/index.mjs +1138 -0
  213. package/evaluator/report.schema.json +152 -0
  214. package/llms.txt +58 -15
  215. package/package.json +70 -10
  216. package/ux-demo/recipes/app-shell.tsx +206 -10
  217. package/ux-demo/recipes/collapsible-sidebar.tsx +167 -44
  218. package/ux-demo/recipes/compact-toolbar.tsx +114 -12
  219. package/ux-demo/recipes/composer-form.tsx +106 -18
  220. package/ux-demo/recipes/list-detail-dialog.tsx +144 -8
  221. package/ux-demo/recipes/list-workspace-states.tsx +165 -8
  222. package/ux-demo/recipes/loaders.ts +5 -2
  223. package/ux-demo/recipes/loading-inspector.tsx +124 -41
  224. package/ux-demo/recipes/mount-recipe.ts +27 -6
  225. package/ux-demo/recipes/navigation-sidebar.tsx +120 -9
  226. package/ux-demo/recipes/navigation-stack.tsx +61 -19
  227. package/ux-demo/recipes/types.ts +3 -1
  228. package/ux-demo/recipes/workspace-header.tsx +57 -2
  229. package/dist/chunk-2Z5TWHKQ.js +0 -34
  230. package/dist/chunk-2Z5TWHKQ.js.map +0 -1
  231. package/dist/chunk-37T3VNIZ.js +0 -19
  232. package/dist/chunk-37T3VNIZ.js.map +0 -1
  233. package/dist/chunk-55DATV3A.js +0 -10
  234. package/dist/chunk-55DATV3A.js.map +0 -1
  235. package/dist/chunk-5ATCDSQS.js +0 -42
  236. package/dist/chunk-5ATCDSQS.js.map +0 -1
  237. package/dist/chunk-5E2HVJ33.js +0 -10
  238. package/dist/chunk-5E2HVJ33.js.map +0 -1
  239. package/dist/chunk-6NLRBSVX.js.map +0 -1
  240. package/dist/chunk-7BR3DO2A.js.map +0 -1
  241. package/dist/chunk-7JB7VPRI.js +0 -34
  242. package/dist/chunk-7JB7VPRI.js.map +0 -1
  243. package/dist/chunk-7LDCTU7D.js +0 -134
  244. package/dist/chunk-7LDCTU7D.js.map +0 -1
  245. package/dist/chunk-AHQI52QO.js.map +0 -1
  246. package/dist/chunk-AQNKJRFH.js +0 -79
  247. package/dist/chunk-AQNKJRFH.js.map +0 -1
  248. package/dist/chunk-AVZHJN6C.js +0 -14
  249. package/dist/chunk-AVZHJN6C.js.map +0 -1
  250. package/dist/chunk-B24C4A6T.js +0 -22
  251. package/dist/chunk-B24C4A6T.js.map +0 -1
  252. package/dist/chunk-CH4SQ6H5.js +0 -39
  253. package/dist/chunk-CH4SQ6H5.js.map +0 -1
  254. package/dist/chunk-DAFFDO33.js +0 -14
  255. package/dist/chunk-DAFFDO33.js.map +0 -1
  256. package/dist/chunk-I7INRJO4.js +0 -17
  257. package/dist/chunk-I7INRJO4.js.map +0 -1
  258. package/dist/chunk-L4OKFDJY.js +0 -24
  259. package/dist/chunk-L4OKFDJY.js.map +0 -1
  260. package/dist/chunk-M5JRI6MN.js +0 -27
  261. package/dist/chunk-M5JRI6MN.js.map +0 -1
  262. package/dist/chunk-RNPGFQ33.js +0 -10
  263. package/dist/chunk-RNPGFQ33.js.map +0 -1
  264. package/dist/chunk-VPCTCUED.js +0 -10
  265. package/dist/chunk-VPCTCUED.js.map +0 -1
  266. package/dist/chunk-WRTAQ7BZ.js +0 -23
  267. package/dist/chunk-WRTAQ7BZ.js.map +0 -1
  268. package/dist/chunk-WVPYVDXY.js +0 -15
  269. package/dist/chunk-WVPYVDXY.js.map +0 -1
  270. package/dist/chunk-WXMGNX2Y.js.map +0 -1
  271. package/dist/chunk-XXMOARTH.js.map +0 -1
  272. package/dist/chunk-YGN7OMLT.js.map +0 -1
  273. package/dist/chunk-ZQQ6P4SV.js +0 -20
  274. package/dist/chunk-ZQQ6P4SV.js.map +0 -1
@@ -15,8 +15,13 @@ import type { RecipeFactory } from './types.js';
15
15
 
16
16
  type ValueField = HTMLElement & { value: string };
17
17
 
18
- const syncControlledFieldValues = (action: HTMLElement, values: Record<string, string>) => {
19
- const form = action.closest<HTMLElement>('[data-recipe="recipe-composer-form"]');
18
+ const syncControlledFieldValues = (
19
+ action: HTMLElement,
20
+ values: Record<string, string>,
21
+ ) => {
22
+ const form = action.closest<HTMLElement>(
23
+ '[data-recipe="recipe-composer-form"]',
24
+ );
20
25
  if (!form) return;
21
26
  for (const [name, value] of Object.entries(values)) {
22
27
  const field = form.querySelector<ValueField>(`[name="${name}"]`);
@@ -26,20 +31,93 @@ const syncControlledFieldValues = (action: HTMLElement, values: Record<string, s
26
31
 
27
32
  export const createRecipe: RecipeFactory = (announce) => {
28
33
  const title = signal('');
29
- const body = signal('The tablet layout now keeps navigation, content, and inspector focus order aligned.');
34
+ const body = signal(
35
+ 'The tablet layout now keeps navigation, content, and inspector focus order aligned.',
36
+ );
30
37
  const audience = signal('team');
31
38
  const status = signal<'idle' | 'error' | 'saved'>('idle');
32
- const render = () => <form class="kui-recipe recipe-form kui-recipe__surface kui-content" data-recipe="recipe-composer-form" aria-labelledby="recipe-composer-title" aria-describedby="recipe-composer-summary" noValidate>
33
- <PanelHeader title="Publish workspace update" titleId="recipe-composer-title" summary="Share a concise, actionable update with collaborators." summaryId="recipe-composer-summary" />
34
- {status.value === 'error' && <StateBanner title="Add a title before publishing" detail="The update body and audience are preserved." tone="danger" urgency="alert" />}
35
- {status.value === 'saved' && <StateBanner title="Update published" detail="The team audience can now read it." tone="success" />}
36
- <div class="recipe-form__section recipe-form__fields">
37
- <wa-input name="recipe-title" label="Update title" hint="Summarize the outcome in one line." required value={title.value}></wa-input>
38
- <wa-textarea name="recipe-body" label="Details" hint="Include decisions, owners, and the next checkpoint." rows="5" maxlength="400" with-count value={body.value}></wa-textarea>
39
- <Select name="recipe-audience" value={audience.value} label="Audience" choices={[{ value: 'team', label: 'Workspace team' }, { value: 'reviewers', label: 'Reviewers' }, { value: 'organization', label: 'Entire organization' }]} />
40
- </div>
41
- <footer class="recipe-form__section recipe-form__footer"><div class="recipe-form__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="reset">Reset</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="submit">Publish update</wa-button></div><p class="kui-recipe__ownership">The recipe owns field, message, and action rhythm. The app owns validation rules, draft persistence, permissions, and transport.</p></footer>
42
- </form>;
39
+ const render = () => (
40
+ <form
41
+ class="kui-recipe recipe-form kui-recipe__surface kui-content"
42
+ data-recipe="recipe-composer-form"
43
+ aria-labelledby="recipe-composer-title"
44
+ aria-describedby="recipe-composer-summary"
45
+ noValidate
46
+ >
47
+ <PanelHeader
48
+ title="Publish workspace update"
49
+ titleId="recipe-composer-title"
50
+ summary="Share a concise, actionable update with collaborators."
51
+ summaryId="recipe-composer-summary"
52
+ />
53
+ {status.value === 'error' && (
54
+ <StateBanner
55
+ title="Add a title before publishing"
56
+ detail="The update body and audience are preserved."
57
+ tone="danger"
58
+ urgency="alert"
59
+ />
60
+ )}
61
+ {status.value === 'saved' && (
62
+ <StateBanner
63
+ title="Update published"
64
+ detail="The team audience can now read it."
65
+ tone="success"
66
+ />
67
+ )}
68
+ <div class="recipe-form__section recipe-form__fields">
69
+ <wa-input
70
+ name="recipe-title"
71
+ label="Update title"
72
+ hint="Summarize the outcome in one line."
73
+ required
74
+ value={title.value}
75
+ ></wa-input>
76
+ <wa-textarea
77
+ name="recipe-body"
78
+ label="Details"
79
+ hint="Include decisions, owners, and the next checkpoint."
80
+ rows="5"
81
+ maxlength="400"
82
+ with-count
83
+ value={body.value}
84
+ ></wa-textarea>
85
+ <Select<string>
86
+ name="recipe-audience"
87
+ value={audience.value}
88
+ label="Audience"
89
+ choices={[
90
+ { value: 'team', label: 'Workspace team' },
91
+ { value: 'reviewers', label: 'Reviewers' },
92
+ { value: 'organization', label: 'Entire organization' },
93
+ ]}
94
+ />
95
+ </div>
96
+ <footer class="recipe-form__section recipe-form__footer">
97
+ <div class="recipe-form__actions kui-control-cluster">
98
+ <wa-button
99
+ appearance="outlined"
100
+ data-action="recipe-action"
101
+ data-recipe-command="reset"
102
+ >
103
+ Reset
104
+ </wa-button>
105
+ <wa-button
106
+ variant="brand"
107
+ appearance="accent"
108
+ data-action="recipe-action"
109
+ data-recipe-command="submit"
110
+ >
111
+ Publish update
112
+ </wa-button>
113
+ </div>
114
+ <p class="kui-recipe__ownership">
115
+ The recipe owns field, message, and action rhythm. The app owns
116
+ validation rules, draft persistence, permissions, and transport.
117
+ </p>
118
+ </footer>
119
+ </form>
120
+ );
43
121
  return {
44
122
  render,
45
123
  action(command, element) {
@@ -47,17 +125,27 @@ export const createRecipe: RecipeFactory = (announce) => {
47
125
  title.value = '';
48
126
  body.value = '';
49
127
  status.value = 'idle';
50
- syncControlledFieldValues(element, { 'recipe-title': title.value, 'recipe-body': body.value });
128
+ syncControlledFieldValues(element, {
129
+ 'recipe-title': title.value,
130
+ 'recipe-body': body.value,
131
+ });
51
132
  announce('Draft reset');
52
133
  return;
53
134
  }
54
- if (command === 'submit') { status.value = title.value.trim() ? 'saved' : 'error'; announce(status.value === 'saved' ? 'Update published' : 'Title required'); }
135
+ if (command === 'submit') {
136
+ status.value = title.value.trim() ? 'saved' : 'error';
137
+ announce(
138
+ status.value === 'saved' ? 'Update published' : 'Title required',
139
+ );
140
+ }
55
141
  },
56
142
  change(element) {
57
- const value = (element as HTMLElement & { value?: string | null }).value ?? '';
143
+ const value =
144
+ (element as HTMLElement & { value?: string | null }).value ?? '';
58
145
  if (element.getAttribute('name') === 'recipe-title') title.value = value;
59
146
  if (element.getAttribute('name') === 'recipe-body') body.value = value;
60
- if (element.getAttribute('name') === 'recipe-audience') audience.value = value;
147
+ if (element.getAttribute('name') === 'recipe-audience')
148
+ audience.value = value;
61
149
  },
62
150
  };
63
151
  };
@@ -15,9 +15,24 @@ import { FileText } from 'lucide';
15
15
  import type { RecipeFactory } from './types.js';
16
16
 
17
17
  const records = {
18
- alpha: { name: 'Northstar migration', owner: 'Mara Chen', state: 'In review', updated: 'Today, 09:42' },
19
- beta: { name: 'Tablet navigation', owner: 'Sam Rivera', state: 'Ready', updated: 'Yesterday, 16:20' },
20
- gamma: { name: 'Contrast audit', owner: 'Inez Okafor', state: 'Blocked', updated: 'Monday, 11:05' },
18
+ alpha: {
19
+ name: 'Northstar migration',
20
+ owner: 'Mara Chen',
21
+ state: 'In review',
22
+ updated: 'Today, 09:42',
23
+ },
24
+ beta: {
25
+ name: 'Tablet navigation',
26
+ owner: 'Sam Rivera',
27
+ state: 'Ready',
28
+ updated: 'Yesterday, 16:20',
29
+ },
30
+ gamma: {
31
+ name: 'Contrast audit',
32
+ owner: 'Inez Okafor',
33
+ state: 'Blocked',
34
+ updated: 'Monday, 11:05',
35
+ },
21
36
  } as const;
22
37
 
23
38
  interface RecipeDialogElement extends HTMLElement {
@@ -27,15 +42,136 @@ interface RecipeDialogElement extends HTMLElement {
27
42
  export const createRecipe: RecipeFactory = (announce) => {
28
43
  const selected = signal<keyof typeof records>('alpha');
29
44
  const open = { value: false };
30
- const render = () => <section class="kui-recipe kui-content" data-recipe="recipe-list-detail-dialog"><button class="kui-recipe__button kui-content-item" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="open">Open project details</button><p class="kui-recipe__ownership kui-content-item">Web Awesome owns modal focus and dismissal. The recipe owns header/body/list-detail anatomy; the app owns open state, selection, and policy.</p><wa-dialog class="recipe-dialog" label="Project details" without-header open={open.value}><div class="recipe-dialog__pane kui-pane"><div class="recipe-list-detail kui-pane__content"><nav class="recipe-list-detail__list kui-pane" aria-label="Projects"><ListHeader label="Recent projects" /><div class="kui-pane__content kui-content"><section>{Object.entries(records).map(([id, record]) => <ListItem action="recipe-action" itemId={id} label={record.name} selected={selected.value === id} multiline />)}</section></div></nav><section class="recipe-list-detail__detail kui-pane"><PanelHeader title="Project details" titleId="recipe-dialog-title" summary="Compare delivery state without leaving the workspace." summaryId="recipe-dialog-summary" icon={<LucideIcon icon={FileText} name="file-text" />} actions={<wa-button appearance="plain" data-action="recipe-action" data-recipe-command="close">Close</wa-button>} /><div class="recipe-list-detail__body kui-pane__content kui-content" aria-live="polite"><h3 class="recipe-list-detail__title kui-recipe__pane-title kui-content-item">{records[selected.value].name}</h3><ValueTable label="Project details"><ValueTableRow label="Owner" value={records[selected.value].owner} /><ValueTableRow label="Status" value={records[selected.value].state} /><ValueTableRow label="Updated" value={records[selected.value].updated} /></ValueTable><div class="recipe-list-detail__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="archive">Archive</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="open-record">Open project</wa-button></div></div></section></div></div></wa-dialog></section>;
45
+ const render = () => (
46
+ <section
47
+ class="kui-recipe kui-content"
48
+ data-recipe="recipe-list-detail-dialog"
49
+ >
50
+ <button
51
+ class="kui-recipe__button kui-content-item"
52
+ data-primary="true"
53
+ type="button"
54
+ data-action="recipe-action"
55
+ data-recipe-command="open"
56
+ >
57
+ Open project details
58
+ </button>
59
+ <p class="kui-recipe__ownership kui-content-item">
60
+ Web Awesome owns modal focus and dismissal. The recipe owns
61
+ header/body/list-detail anatomy; the app owns open state, selection, and
62
+ policy.
63
+ </p>
64
+ <wa-dialog
65
+ class="recipe-dialog"
66
+ label="Project details"
67
+ without-header
68
+ open={open.value}
69
+ >
70
+ <div class="recipe-dialog__pane kui-pane">
71
+ <div class="recipe-list-detail kui-pane__content">
72
+ <nav
73
+ class="recipe-list-detail__list kui-pane"
74
+ aria-label="Projects"
75
+ >
76
+ <ListHeader label="Recent projects" />
77
+ <div class="kui-pane__content kui-content">
78
+ <section>
79
+ {Object.entries(records).map(([id, record]) => (
80
+ <ListItem
81
+ action="recipe-action"
82
+ itemId={id}
83
+ label={record.name}
84
+ selected={selected.value === id}
85
+ multiline
86
+ />
87
+ ))}
88
+ </section>
89
+ </div>
90
+ </nav>
91
+ <section class="recipe-list-detail__detail kui-pane">
92
+ <PanelHeader
93
+ title="Project details"
94
+ titleId="recipe-dialog-title"
95
+ summary="Compare delivery state without leaving the workspace."
96
+ summaryId="recipe-dialog-summary"
97
+ icon={<LucideIcon icon={FileText} name="file-text" />}
98
+ actions={
99
+ <wa-button
100
+ appearance="plain"
101
+ data-action="recipe-action"
102
+ data-recipe-command="close"
103
+ >
104
+ Close
105
+ </wa-button>
106
+ }
107
+ />
108
+ <div
109
+ class="recipe-list-detail__body kui-pane__content kui-content"
110
+ aria-live="polite"
111
+ >
112
+ <h3 class="recipe-list-detail__title kui-recipe__pane-title kui-content-item">
113
+ {records[selected.value].name}
114
+ </h3>
115
+ <ValueTable label="Project details">
116
+ <ValueTableRow
117
+ label="Owner"
118
+ value={records[selected.value].owner}
119
+ />
120
+ <ValueTableRow
121
+ label="Status"
122
+ value={records[selected.value].state}
123
+ />
124
+ <ValueTableRow
125
+ label="Updated"
126
+ value={records[selected.value].updated}
127
+ />
128
+ </ValueTable>
129
+ <div class="recipe-list-detail__actions kui-control-cluster">
130
+ <wa-button
131
+ appearance="outlined"
132
+ data-action="recipe-action"
133
+ data-recipe-command="archive"
134
+ >
135
+ Archive
136
+ </wa-button>
137
+ <wa-button
138
+ variant="brand"
139
+ appearance="accent"
140
+ data-action="recipe-action"
141
+ data-recipe-command="open-record"
142
+ >
143
+ Open project
144
+ </wa-button>
145
+ </div>
146
+ </div>
147
+ </section>
148
+ </div>
149
+ </div>
150
+ </wa-dialog>
151
+ </section>
152
+ );
31
153
  return {
32
154
  render,
33
155
  action(command, element) {
34
156
  const id = element.dataset.itemId;
35
- if (id && id in records) { selected.value = id as keyof typeof records; announce(`Selected ${records[selected.value].name}`); return; }
36
- const dialog = element.closest('[data-recipe]')?.querySelector<RecipeDialogElement>('wa-dialog');
37
- if (command === 'open' && dialog) { element.focus(); open.value = true; dialog.open = true; return; }
38
- if (command === 'close' && dialog) { dialog.open = false; return; }
157
+ if (id && id in records) {
158
+ selected.value = id as keyof typeof records;
159
+ announce(`Selected ${records[selected.value].name}`);
160
+ return;
161
+ }
162
+ const dialog = element
163
+ .closest('[data-recipe]')
164
+ ?.querySelector<RecipeDialogElement>('wa-dialog');
165
+ if (command === 'open' && dialog) {
166
+ element.focus();
167
+ open.value = true;
168
+ dialog.open = true;
169
+ return;
170
+ }
171
+ if (command === 'close' && dialog) {
172
+ dialog.open = false;
173
+ return;
174
+ }
39
175
  announce(`${command} requested`);
40
176
  },
41
177
  afterHide() {
@@ -10,17 +10,174 @@ import { signal } from 'kerfjs';
10
10
  import type { RecipeFactory } from './types.js';
11
11
 
12
12
  type ListState = 'loading' | 'empty' | 'populated' | 'stale' | 'error';
13
- const items = ['Audit keyboard focus order', 'Verify 200% zoom layout', 'Publish package migration notes'];
13
+ const items = [
14
+ 'Audit keyboard focus order',
15
+ 'Verify 200% zoom layout',
16
+ 'Publish package migration notes',
17
+ ];
14
18
 
15
19
  export const createRecipe: RecipeFactory = (announce) => {
16
20
  const state = signal<ListState>('loading');
17
- const populated = () => <ul class="recipe-list__items" aria-label="Release tasks">{items.map((item, index) => <li><strong>{item}</strong><span class="kui-recipe__muted">{index === 0 ? 'In review · Mara' : index === 1 ? 'Ready · Sam' : 'Draft · Inez'}</span></li>)}</ul>;
21
+ const populated = () => (
22
+ <ul class="recipe-list__items" aria-label="Release tasks">
23
+ {items.map((item, index) => (
24
+ <li>
25
+ <strong>{item}</strong>
26
+ <span class="kui-recipe__muted">
27
+ {index === 0
28
+ ? 'In review · Mara'
29
+ : index === 1
30
+ ? 'Ready · Sam'
31
+ : 'Draft · Inez'}
32
+ </span>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ );
18
37
  const renderBody = () => {
19
- if (state.value === 'loading') return <EmptyState title="Loading release tasks" detail="The current workspace will appear when data is ready." busy action={<button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="load">Complete load</button>} />;
20
- if (state.value === 'empty') return <EmptyState title="No release tasks" detail="Create the first task for this milestone." action={<button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="create">Create task</button>} />;
21
- if (state.value === 'error') return <StateBanner title="Release tasks could not be refreshed" detail="Existing filters are preserved. Try again when the connection recovers." tone="danger" urgency="alert" action={<button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="retry">Retry</button>} />;
22
- return <>{state.value === 'stale' && <StateBanner title="Showing saved results" detail="Refreshing in the background." tone="warning" action={<span class="kui-inline-metadata"><LoadingSpinner label="Refreshing release tasks" /><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="finish">Finish refresh</button></span>} />}{populated()}</>;
38
+ if (state.value === 'loading')
39
+ return (
40
+ <EmptyState
41
+ title="Loading release tasks"
42
+ detail="The current workspace will appear when data is ready."
43
+ busy
44
+ action={
45
+ <button
46
+ class="kui-recipe__button"
47
+ type="button"
48
+ data-action="recipe-action"
49
+ data-recipe-command="load"
50
+ >
51
+ Complete load
52
+ </button>
53
+ }
54
+ />
55
+ );
56
+ if (state.value === 'empty')
57
+ return (
58
+ <EmptyState
59
+ title="No release tasks"
60
+ detail="Create the first task for this milestone."
61
+ action={
62
+ <button
63
+ class="kui-recipe__button"
64
+ data-primary="true"
65
+ type="button"
66
+ data-action="recipe-action"
67
+ data-recipe-command="create"
68
+ >
69
+ Create task
70
+ </button>
71
+ }
72
+ />
73
+ );
74
+ if (state.value === 'error')
75
+ return (
76
+ <StateBanner
77
+ title="Release tasks could not be refreshed"
78
+ detail="Existing filters are preserved. Try again when the connection recovers."
79
+ tone="danger"
80
+ urgency="alert"
81
+ action={
82
+ <button
83
+ class="kui-recipe__button"
84
+ type="button"
85
+ data-action="recipe-action"
86
+ data-recipe-command="retry"
87
+ >
88
+ Retry
89
+ </button>
90
+ }
91
+ />
92
+ );
93
+ return (
94
+ <>
95
+ {state.value === 'stale' && (
96
+ <StateBanner
97
+ title="Showing saved results"
98
+ detail="Refreshing in the background."
99
+ tone="warning"
100
+ action={
101
+ <span class="kui-inline-metadata">
102
+ <LoadingSpinner label="Refreshing release tasks" />
103
+ <button
104
+ class="kui-recipe__button"
105
+ type="button"
106
+ data-action="recipe-action"
107
+ data-recipe-command="finish"
108
+ >
109
+ Finish refresh
110
+ </button>
111
+ </span>
112
+ }
113
+ />
114
+ )}
115
+ {populated()}
116
+ </>
117
+ );
118
+ };
119
+ const render = () => (
120
+ <section
121
+ class="kui-recipe recipe-list kui-recipe__surface kui-pane"
122
+ data-recipe="recipe-list-workspace-states"
123
+ data-list-state={state.value}
124
+ >
125
+ <PanelHeader
126
+ title="Release tasks"
127
+ titleId="recipe-list-title"
128
+ actions={
129
+ <div class="kui-control-cluster">
130
+ <button
131
+ class="kui-recipe__button"
132
+ type="button"
133
+ data-action="recipe-action"
134
+ data-recipe-command="empty"
135
+ >
136
+ Clear
137
+ </button>
138
+ <button
139
+ class="kui-recipe__button"
140
+ type="button"
141
+ data-action="recipe-action"
142
+ data-recipe-command="fail"
143
+ >
144
+ Simulate failure
145
+ </button>
146
+ <button
147
+ class="kui-recipe__button"
148
+ data-primary="true"
149
+ type="button"
150
+ data-action="recipe-action"
151
+ data-recipe-command="refresh"
152
+ >
153
+ Refresh
154
+ </button>
155
+ </div>
156
+ }
157
+ />
158
+ <div class="recipe-list__body kui-pane__content kui-content">
159
+ {renderBody()}
160
+ <p class="kui-recipe__ownership kui-content-item">
161
+ The recipe owns feedback placement and stable content. The app owns
162
+ fetching, cache age, retry policy, and domain rows.
163
+ </p>
164
+ </div>
165
+ </section>
166
+ );
167
+ return {
168
+ render,
169
+ action(command) {
170
+ const next: Record<string, ListState> = {
171
+ load: 'populated',
172
+ empty: 'empty',
173
+ create: 'populated',
174
+ fail: 'error',
175
+ retry: 'populated',
176
+ refresh: 'stale',
177
+ finish: 'populated',
178
+ };
179
+ if (command in next) state.value = next[command]!;
180
+ announce(`List state: ${state.value}`);
181
+ },
23
182
  };
24
- const render = () => <section class="kui-recipe recipe-list kui-recipe__surface kui-pane" data-recipe="recipe-list-workspace-states" data-list-state={state.value}><PanelHeader title="Release tasks" titleId="recipe-list-title" actions={<div class="kui-control-cluster"><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="empty">Clear</button><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="fail">Simulate failure</button><button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="refresh">Refresh</button></div>} /><div class="recipe-list__body kui-pane__content kui-content">{renderBody()}<p class="kui-recipe__ownership kui-content-item">The recipe owns feedback placement and stable content. The app owns fetching, cache age, retry policy, and domain rows.</p></div></section>;
25
- return { render, action(command) { const next: Record<string, ListState> = { load: 'populated', empty: 'empty', create: 'populated', fail: 'error', retry: 'populated', refresh: 'stale', finish: 'populated' }; if (command in next) state.value = next[command]!; announce(`List state: ${state.value}`); } };
26
183
  };
@@ -13,9 +13,12 @@ export const recipeIds = [
13
13
  'recipe-collapsible-sidebar',
14
14
  ] as const;
15
15
 
16
- export type RecipeId = typeof recipeIds[number];
16
+ export type RecipeId = (typeof recipeIds)[number];
17
17
 
18
- export const recipeLoaders: Record<RecipeId, () => Promise<{ createRecipe: RecipeFactory }>> = {
18
+ export const recipeLoaders: Record<
19
+ RecipeId,
20
+ () => Promise<{ createRecipe: RecipeFactory }>
21
+ > = {
19
22
  'recipe-app-shell': () => import('./app-shell.js'),
20
23
  'recipe-navigation-sidebar': () => import('./navigation-sidebar.js'),
21
24
  'recipe-workspace-header': () => import('./workspace-header.js'),