@kerfjs/ui 5.0.0-beta.18 → 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 (137) hide show
  1. package/README.md +114 -3
  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-v2-overrides.json +326 -0
  16. package/ai/component-catalog-v2.d.ts +67 -0
  17. package/ai/component-catalog-v2.json +4441 -0
  18. package/ai/component-catalog-v2.schema.json +246 -0
  19. package/ai/component-catalog.json +150 -6
  20. package/ai/public-api-signatures-v1.md +294 -64
  21. package/ai/skill.md +41 -5
  22. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  23. package/analyzer/cli.mjs +46 -0
  24. package/analyzer/index.d.mts +45 -0
  25. package/analyzer/index.mjs +743 -0
  26. package/analyzer/report.schema.json +66 -0
  27. package/dist/browser/pane.js +3 -0
  28. package/dist/browser/sunken-panel.js +3 -0
  29. package/dist/catalog-resources.d.ts +34 -0
  30. package/dist/catalog-resources.js +22 -0
  31. package/dist/catalog-resources.js.map +1 -0
  32. package/dist/catalog.d.ts +21 -3
  33. package/dist/catalog.js +239 -210
  34. package/dist/catalog.js.map +1 -1
  35. package/dist/{chunk-ZK2G3VR5.js → chunk-5VTB2BKP.js} +1 -1
  36. package/dist/chunk-5VTB2BKP.js.map +1 -0
  37. package/dist/{chunk-S3HKPI7I.js → chunk-7KEUJIIC.js} +1 -1
  38. package/dist/chunk-7KEUJIIC.js.map +1 -0
  39. package/dist/{chunk-NVWXKU37.js → chunk-AASLYRGE.js} +1 -1
  40. package/dist/chunk-AASLYRGE.js.map +1 -0
  41. package/dist/{chunk-ENVTNLYC.js → chunk-CN2XNTAM.js} +6 -6
  42. package/dist/chunk-CN2XNTAM.js.map +1 -0
  43. package/dist/{chunk-QIRXYKCE.js → chunk-DSNIYOZ3.js} +3 -2
  44. package/dist/chunk-DSNIYOZ3.js.map +1 -0
  45. package/dist/chunk-JRKSK2HX.js +79 -0
  46. package/dist/chunk-JRKSK2HX.js.map +1 -0
  47. package/dist/{chunk-U5IFGQCD.js → chunk-K57KRAOV.js} +1 -1
  48. package/dist/chunk-K57KRAOV.js.map +1 -0
  49. package/dist/{chunk-ZVI5YST2.js → chunk-M7JBER5U.js} +1 -1
  50. package/dist/chunk-M7JBER5U.js.map +1 -0
  51. package/dist/{chunk-43FUQH35.js → chunk-OHU6CI22.js} +2 -2
  52. package/dist/{chunk-43FUQH35.js.map → chunk-OHU6CI22.js.map} +1 -1
  53. package/dist/{chunk-4TRDV6RL.js → chunk-OZWU5ZPZ.js} +1 -1
  54. package/dist/chunk-OZWU5ZPZ.js.map +1 -0
  55. package/dist/{chunk-JTHUXDN2.js → chunk-RYRIFZCT.js} +3 -3
  56. package/dist/chunk-RYRIFZCT.js.map +1 -0
  57. package/dist/{chunk-6ETZKKAE.js → chunk-VLYV65LK.js} +1 -1
  58. package/dist/chunk-VLYV65LK.js.map +1 -0
  59. package/dist/chunk-W4CXQYSG.js +23 -0
  60. package/dist/chunk-W4CXQYSG.js.map +1 -0
  61. package/dist/{chunk-S3PCZ7NV.js → chunk-W7YD6E4N.js} +1 -1
  62. package/dist/chunk-W7YD6E4N.js.map +1 -0
  63. package/dist/index.d.ts +6 -4
  64. package/dist/index.js +14 -12
  65. package/dist/list-header.d.ts +29 -8
  66. package/dist/list-header.js +1 -1
  67. package/dist/nav-stack.js +1 -1
  68. package/dist/pane.d.ts +49 -0
  69. package/dist/pane.js +4 -0
  70. package/dist/pane.js.map +1 -0
  71. package/dist/panel-header.d.ts +9 -3
  72. package/dist/panel-header.js +1 -1
  73. package/dist/segmented-control.d.ts +6 -6
  74. package/dist/segmented-control.js +1 -1
  75. package/dist/select.d.ts +10 -4
  76. package/dist/select.js +1 -1
  77. package/dist/split-view.js +2 -2
  78. package/dist/state-banner.d.ts +3 -2
  79. package/dist/state-banner.js +1 -1
  80. package/dist/styles/catalog.css +6 -6
  81. package/dist/styles/layout.css +2 -20
  82. package/dist/styles/pane.css +62 -0
  83. package/dist/styles/segmented-control.css +3 -7
  84. package/dist/styles/sunken-panel.css +18 -0
  85. package/dist/sunken-panel.d.ts +16 -0
  86. package/dist/sunken-panel.js +3 -0
  87. package/dist/sunken-panel.js.map +1 -0
  88. package/dist/tab-bar.d.ts +3 -2
  89. package/dist/tab-bar.js +1 -1
  90. package/dist/tab-scaffold.d.ts +6 -6
  91. package/dist/tab-scaffold.js.map +1 -1
  92. package/dist/token-search-field.d.ts +25 -9
  93. package/dist/token-search-field.js +1 -1
  94. package/dist/toolbar-control-group.d.ts +9 -5
  95. package/dist/toolbar-control-group.js +1 -1
  96. package/dist/toolbar-text.d.ts +10 -11
  97. package/dist/toolbar-text.js +1 -1
  98. package/dist/wire-tab-bars.d.ts +5 -2
  99. package/dist/wire-tab-bars.js +1 -1
  100. package/dist/wire-token-search-fields.d.ts +11 -6
  101. package/dist/wire-token-search-fields.js +1 -1
  102. package/docs/catalog.md +186 -67
  103. package/docs/component-contract.md +102 -5
  104. package/docs/component-selection.md +9 -5
  105. package/docs/design-philosophy.md +2 -4
  106. package/docs/examples/application-ui-profile.json +60 -0
  107. package/docs/examples/component-catalog-extension-v2.json +81 -0
  108. package/docs/layout.md +42 -23
  109. package/docs/sunken-panel.md +43 -0
  110. package/docs/type-contracts.md +99 -0
  111. package/docs/ui-analyzer.md +81 -0
  112. package/docs/ui-doctor.md +77 -0
  113. package/docs/ui-evaluator.md +122 -0
  114. package/doctor/cli.mjs +115 -0
  115. package/doctor/config.schema.json +68 -0
  116. package/doctor/index.d.mts +116 -0
  117. package/doctor/index.mjs +1256 -0
  118. package/doctor/report.schema.json +118 -0
  119. package/evaluator/cli.mjs +89 -0
  120. package/evaluator/index.d.mts +90 -0
  121. package/evaluator/index.mjs +1138 -0
  122. package/evaluator/report.schema.json +152 -0
  123. package/llms.txt +40 -3
  124. package/package.json +66 -8
  125. package/ux-demo/recipes/compact-toolbar.tsx +2 -2
  126. package/ux-demo/recipes/composer-form.tsx +1 -1
  127. package/dist/chunk-4TRDV6RL.js.map +0 -1
  128. package/dist/chunk-6ETZKKAE.js.map +0 -1
  129. package/dist/chunk-ENVTNLYC.js.map +0 -1
  130. package/dist/chunk-JTHUXDN2.js.map +0 -1
  131. package/dist/chunk-NVWXKU37.js.map +0 -1
  132. package/dist/chunk-QIRXYKCE.js.map +0 -1
  133. package/dist/chunk-S3HKPI7I.js.map +0 -1
  134. package/dist/chunk-S3PCZ7NV.js.map +0 -1
  135. package/dist/chunk-U5IFGQCD.js.map +0 -1
  136. package/dist/chunk-ZK2G3VR5.js.map +0 -1
  137. package/dist/chunk-ZVI5YST2.js.map +0 -1
package/docs/catalog.md CHANGED
@@ -40,47 +40,113 @@ Per-entry `resources` render as "open in new tab" links in the footer, and
40
40
  around a `wa-dropdown` (grouped by each entry's `group`), so register its elements
41
41
  with `@kerfjs/ui/select/register` when you use it.
42
42
 
43
- ## Preview examples
44
-
45
- Compose each entry's `content` from `CatalogExample` (and `CatalogExampleStack`)
46
- instead of hand-rolled example markup, so labels, notes, and left-edge alignment
47
- stay consistent:
43
+ ## Catalog demo authoring contract
44
+
45
+ This section is the single authoritative contract for tools and people that
46
+ author Catalog previews. The machine-readable discovery entry is
47
+ [`catalog-authoring.json`](../ai/catalog-authoring.json); exact props remain in
48
+ [`public-api-signatures-v1.md`](../ai/public-api-signatures-v1.md#kerfjsuicatalog).
49
+ The component catalog deliberately does not duplicate these rules: it describes
50
+ which component to choose, while this contract describes how to present the
51
+ chosen component.
52
+
53
+ ### Choose the demo mode
54
+
55
+ Classify every entry before rendering it:
56
+
57
+ | Entry kind | Preview purpose | Geometry overlay |
58
+ | --------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
59
+ | Focused component | Show one public component, its meaningful variants, and adverse states. | `true`; the overlay inspects each selected specimen. |
60
+ | Composition or recipe | Show several components cooperating as one product surface. | `false`; child geometry remains unmarked so the composition can be read as a whole. |
61
+
62
+ Keep `geometryOverlay` present and compute it from the active entry. Do not make
63
+ it a permanent catalog-wide `true`, and do not give individual specimens their
64
+ own overlay implementation.
65
+
66
+ ### Required structure
67
+
68
+ - `Catalog` is the one shell. The app owns active-entry state and passes one
69
+ active preview through `content`.
70
+ - `CatalogExampleStack` is the group for one preview's rows. Put route/test
71
+ metadata such as `data-demo` on its rendered root through `rootAttributes`.
72
+ Pass `label` when the stack needs an accessible name: the rendered `section`
73
+ is then exposed as a named region. An unlabeled stack remains an ordinary
74
+ grouping rather than adding an unnecessary landmark.
75
+ - `CatalogExample` is one row: optional generated `ListHeader` label, optional
76
+ generated note, then one specimen or one intentionally coupled specimen
77
+ cluster. Use one row per variant/state; do not hand-author the helper's private
78
+ classes.
79
+ - The specimen is an immediate child of `CatalogExample`. A focused component
80
+ row should place the component root there, without a decorative card or
81
+ spacing wrapper. A composition row may place the composition root there.
82
+
83
+ Use `align="glyph"` for a bare glyph/text specimen, `align="inline-control"`
84
+ for a control whose own inline padding contributes about 8px, and `align="none"`
85
+ (the default) for a content item or composition that owns its geometry.
48
86
 
49
87
  ```tsx
50
88
  import { CatalogExample, CatalogExampleStack } from "@kerfjs/ui/catalog";
51
89
 
52
90
  const buttonPreview = (
53
- <CatalogExampleStack label="Button variants">
91
+ <CatalogExampleStack
92
+ label="Button variants"
93
+ rootAttributes={{ "data-demo": "button" }}
94
+ >
54
95
  <CatalogExample label="Icon" note="A bare glyph." align="glyph">
55
96
  <LucideIcon icon={Plus} name="plus" />
56
97
  </CatalogExample>
57
98
  <CatalogExample label="Control" align="inline-control">
58
99
  <SegmentedControl id="view" label="View" value="list" choices={choices} />
59
100
  </CatalogExample>
60
- <CatalogExample label="In composition">
61
- <ValueTable label="Metadata">{rows}</ValueTable>
101
+ <CatalogExample
102
+ label="Authoring note"
103
+ note="Explanatory chrome is not a specimen."
104
+ rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}
105
+ >
106
+ <p>Use the public helper contract.</p>
62
107
  </CatalogExample>
63
108
  </CatalogExampleStack>
64
109
  );
65
110
  ```
66
111
 
67
- `label` is optional omit it for a bare specimen with no `ListHeader`. `align`
68
- lines a specimen's visible left edge up with its label text: `'glyph'`
69
- (16px) for a bare glyph/text specimen, `'inline-control'` (8px) for a control that
70
- already carries ~8px of its own inline padding, and `'none'` (the default) for a
71
- content-item/composition that already owns its geometry. The inset is published as
72
- the `--kui-catalog-example-align` custom property so a debug overlay can exclude it
73
- from a specimen's measured margin.
74
-
75
- ## Geometry inspection
76
-
77
- Pass `geometryOverlay` to `Catalog` when individual component previews should
78
- show otherwise-invisible geometry. The controlled boolean draws a dashed outer
79
- bound around specimens with transparent backgrounds and devtools-style orange
80
- bands over positive margins. Keep the prop present while switching entries so
81
- `wireCatalog` can reuse one overlay layer; set it to `true` for focused component
82
- previews and `false` for full compositions whose child geometry should remain
83
- unmarked.
112
+ The overlay selects every immediate child of a `CatalogExample` except the
113
+ helper-generated label and note. It does not recursively promote a nested child
114
+ to be the specimen. Outside an example row, it selects only top-level
115
+ `[data-component]` roots in the canvas and ignores nested component descendants.
116
+ These rules keep a row's label/group scaffolding out of the measurement and make
117
+ the authored nesting determine exactly what is inspected.
118
+
119
+ ### Metadata ownership
120
+
121
+ Use `rootAttributes` on either helper for authoring metadata such as `data-demo`
122
+ or `data-catalog-geometry-overlay-skip`; the metadata lands on that helper's
123
+ rendered root. The slot accepts only `data-*` strings. Structural
124
+ `data-catalog-example`, `data-catalog-example-stack`, and `data-align` semantics
125
+ remain helper-owned and are rejected case-insensitively at runtime, including
126
+ from structurally widened or JavaScript objects. Do not copy the helpers'
127
+ private `kui-catalog-*` classes into preview markup.
128
+
129
+ The app owns entry ids, `kind`, routing, sources, relationships, and test hooks.
130
+ The helpers own their structural markers, label/note anatomy, alignment marker,
131
+ and private classes. Component metadata such as margin/border/padding ownership
132
+ lives in `component-catalog.json`; do not infer or overwrite it from overlay
133
+ pixels.
134
+
135
+ ### Geometry overlay and legend
136
+
137
+ Pass the conditional `geometryOverlay` boolean to `Catalog`, then call
138
+ `wireCatalogGeometryOverlay(root)` once after the first render and retain its
139
+ disposer alongside `wireCatalog`'s.
140
+
141
+ | Overlay mark | Meaning | It is not |
142
+ | ----------------------- | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
143
+ | Orange translucent band | A positive computed margin on the selected specimen, after subtracting the helper's alignment inset. | Padding, gap, or empty content. Zero and negative margins are not drawn. |
144
+ | Quiet dashed outline | The border-box outer bound of a selected specimen whose computed background is transparent. | A real CSS border, focus ring, padding edge, or proof that the specimen owns its size. |
145
+
146
+ The overlay does not visualize padding, borders, gaps, negative/zero margins,
147
+ scroll overflow, hit targets, nested descendants, or geometry ownership. Inspect
148
+ computed styles and the machine-readable `geometry` metadata for those facts.
149
+ Opaque specimens do not receive the transparent-bound outline.
84
150
 
85
151
  ```tsx
86
152
  <Catalog
@@ -90,13 +156,12 @@ unmarked.
90
156
  />
91
157
  ```
92
158
 
93
- Call `wireCatalogGeometryOverlay(root)` after the first render to synchronize the
94
- opt-in layer across rerenders, theme changes, resizes, and scrolling, and retain
95
- its disposer alongside `wireCatalog`'s. `CatalogExample` labels and notes are
96
- excluded; its `align` inset is also subtracted so alignment scaffolding is not
97
- reported as intrinsic component margin. Put
98
- `data-catalog-geometry-overlay-skip` on a preview subtree that is intentionally
99
- explanatory chrome rather than a specimen.
159
+ Put `rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}` on a
160
+ `CatalogExample` or `CatalogExampleStack` only when that whole subtree is
161
+ explanatory chrome rather than a specimen. The marker excludes the marked root
162
+ and every descendant from selection; it does not merely hide one band. It is
163
+ normally unnecessary in a composition because the active entry already sets the
164
+ global overlay to `false`.
100
165
 
101
166
  Use the overlay together with machine-readable geometry ownership metadata; the
102
167
  overlay verifies what is rendered, while metadata tells people and AI tools
@@ -107,6 +172,36 @@ contract and can start from the checked
107
172
  [`component-catalog-extension.json`](./examples/component-catalog-extension.json)
108
173
  example; provide those entries beside Kerf's shipped catalog to AI tools.
109
174
 
175
+ ### Automated conformance and reviewed exceptions
176
+
177
+ Run `npm run check:demo-conformance` after changing a first-party demo, its
178
+ catalog kind, or the shell's overlay logic. The TypeScript-AST gate verifies
179
+ facts that source can prove without guessing at rendered intent:
180
+
181
+ - focused component routes import and use `CatalogExampleStack` and
182
+ `CatalogExample` from the public package;
183
+ - focused route metadata uses the helpers' `rootAttributes` slot, example rows
184
+ are not empty, and composition routes do not add redundant skip markers;
185
+ - every `@kerfjs/ui` import is a published package export and relative imports
186
+ do not reach into `ui/src`;
187
+ - demo JSX does not copy private `kui-catalog-*` structural classes; and
188
+ - the shell derives both geometry-overlay enablement and documented demo mode
189
+ from the active entry's source and kind.
190
+
191
+ The gate deliberately does not infer component ownership from arbitrary class
192
+ names, margins, borders, or nested descendants. Runtime selection and geometry
193
+ remain the browser suite's job.
194
+
195
+ A focused route may bypass the two public layout helpers only when the route's
196
+ stage geometry is itself the reviewed specimen. Add the narrow waiver to
197
+ [`catalog-conformance-exceptions.json`](../ux-demo/catalog-conformance-exceptions.json)
198
+ with the exact route, source file, stable diagnostic ids, a substantive reason,
199
+ and the reviewing `KF-*` ticket. Only helper/metadata rules are waivable;
200
+ private imports, private markup, empty examples, composition overlay drift, and
201
+ shell-mode drift always fail. The gate rejects duplicate, malformed, unused,
202
+ and stale exceptions, so delete a waiver when its route adopts the standard
203
+ helpers.
204
+
110
205
  ## Selection reveal
111
206
 
112
207
  Set `revealSelection: true` on `wireCatalog` for a long desktop sidebar. After
@@ -124,8 +219,13 @@ sizes. For an initial deep link that did not come through `wireCatalog`, call
124
219
  ## Complete example
125
220
 
126
221
  ```tsx
127
- import { mount, signal } from "kerfjs";
128
- import { Catalog, type CatalogSection } from "@kerfjs/ui/catalog";
222
+ import { mount, signal, type SafeHtml } from "kerfjs";
223
+ import {
224
+ Catalog,
225
+ CatalogExample,
226
+ CatalogExampleStack,
227
+ type CatalogSection,
228
+ } from "@kerfjs/ui/catalog";
129
229
  import {
130
230
  revealCatalogEntry,
131
231
  wireCatalog,
@@ -133,45 +233,63 @@ import {
133
233
  } from "@kerfjs/ui/wire-catalog";
134
234
  import "@kerfjs/ui/styles.css"; // or import each primitive's CSS + @kerfjs/ui/catalog.css
135
235
 
136
- // 1. Describe your components once.
137
- const sections: CatalogSection[] = [
236
+ type DemoKind = "component" | "composition";
237
+ type DemoEntry = CatalogSection["entries"][number] & { kind: DemoKind };
238
+
239
+ // 1. Describe selection and overlay mode once.
240
+ const entries: DemoEntry[] = [
138
241
  {
139
- category: "Controls",
140
- entries: [
141
- {
142
- id: "button",
143
- name: "Button",
144
- description: "A pressable control.",
145
- resources: [
146
- {
147
- label: "Source",
148
- href: "/src/button.tsx",
149
- detail: "src/button.tsx",
150
- },
151
- ],
152
- },
153
- {
154
- id: "field",
155
- name: "Field",
156
- description: "A labeled input.",
157
- tags: ["Discouraged"],
158
- related: [{ id: "button", name: "Button", group: "Used with" }],
159
- },
242
+ id: "button",
243
+ name: "Button",
244
+ kind: "component",
245
+ description: "A pressable control.",
246
+ resources: [
247
+ { label: "Source", href: "/src/button.tsx", detail: "src/button.tsx" },
160
248
  ],
161
249
  },
162
250
  {
163
- category: "Feedback",
164
- entries: [
165
- { id: "toast", name: "Toast", description: "A transient message." },
166
- ],
251
+ id: "profile-form",
252
+ name: "Profile form",
253
+ kind: "composition",
254
+ description: "A labeled field and save action working together.",
255
+ },
256
+ ];
257
+ const sections: CatalogSection[] = [
258
+ {
259
+ category: "Examples",
260
+ entries: entries.map(({ kind: _kind, ...entry }) => entry),
167
261
  },
168
262
  ];
169
263
 
170
- // 2. One preview render per entry id.
171
- const renderers: Record<string, () => ReturnType<typeof Button>> = {
172
- button: () => <Button label="Save" />,
173
- field: () => <Field label="Name" />,
174
- toast: () => <Toast>Saved</Toast>,
264
+ // 2. Every preview uses one public group and public example rows.
265
+ const renderers: Record<string, () => SafeHtml> = {
266
+ button: () => (
267
+ <CatalogExampleStack
268
+ label="Button states"
269
+ rootAttributes={{ "data-demo": "button" }}
270
+ >
271
+ <CatalogExample label="Default" align="inline-control">
272
+ <Button label="Save" />
273
+ </CatalogExample>
274
+ <CatalogExample
275
+ label="Authoring note"
276
+ note="This explanatory row is deliberately excluded from inspection."
277
+ rootAttributes={{ "data-catalog-geometry-overlay-skip": "" }}
278
+ >
279
+ <p>The application owns product copy and actions.</p>
280
+ </CatalogExample>
281
+ </CatalogExampleStack>
282
+ ),
283
+ "profile-form": () => (
284
+ <CatalogExampleStack
285
+ label="Profile form composition"
286
+ rootAttributes={{ "data-demo": "profile-form" }}
287
+ >
288
+ <CatalogExample label="Complete composition">
289
+ <ProfileForm />
290
+ </CatalogExample>
291
+ </CatalogExampleStack>
292
+ ),
175
293
  };
176
294
 
177
295
  // 3. App-owned state (domain: which entry; transient: collapsed; global: theme).
@@ -180,6 +298,7 @@ const initial =
180
298
  const active = signal(initial);
181
299
  const collapsed = signal(false);
182
300
  const theme = signal<"light" | "dark">("light");
301
+ const activeEntry = () => entries.find(({ id }) => id === active.value) ?? entries[0];
183
302
 
184
303
  const app = document.getElementById("app")!;
185
304
  mount(app, () => (
@@ -190,7 +309,7 @@ mount(app, () => (
190
309
  content={renderers[active.value]?.() ?? <></>}
191
310
  collapsed={collapsed.value}
192
311
  theme={theme.value}
193
- geometryOverlay={true}
312
+ geometryOverlay={activeEntry().kind === "component"}
194
313
  />
195
314
  ));
196
315
 
@@ -18,6 +18,94 @@ package delivery paths, Web Awesome's installed manifest, AI guidance, and
18
18
  documentation links. Nuanced prose in this contract and the selection guide
19
19
  remains authored rather than generated.
20
20
 
21
+ ### Composition catalog v2
22
+
23
+ [`component-catalog-v2.json`](../ai/component-catalog-v2.json) adds a formal,
24
+ machine-evaluable composition layer without changing the v1 selection and
25
+ delivery contract. Every v1 entry projects once, in order, under the stable
26
+ qualified key `package:id`. Each v2 entry explicitly covers parents/contexts,
27
+ named zones and cardinality, child concepts, state ownership, required wiring,
28
+ responsive ownership, layout and geometry, accessibility obligations, public
29
+ CSS boundaries, diagnostics, and provenance.
30
+
31
+ The generator starts with permissive defaults. An `any` mode records that the
32
+ catalog has no defensible prohibition; it does not claim every composition is
33
+ recommended. Objective rules live in
34
+ `component-catalog-v2-overrides.json` and may carry stable `KUI-C###`
35
+ diagnostics. A tool reports a diagnostic only after proving its exact `when`
36
+ condition. Subjective choice, product policy, and visual taste remain prose.
37
+
38
+ V1 consumers continue unchanged. V2 consumers read v1 for selection/delivery
39
+ and v2 for composition. `npm run catalog:sync` projects every new component,
40
+ recipe, and supported Web Awesome entry, and the completeness gate prevents
41
+ silent omissions. Downstream catalogs use the v2 extension schema and types,
42
+ retain their own package identity, and qualify cross-catalog references. See
43
+ the checked
44
+ [`component-catalog-extension-v2.json`](./examples/component-catalog-extension-v2.json)
45
+ application-owned example.
46
+
47
+ ### Compile-time contract boundary
48
+
49
+ The versioned
50
+ [`compile-time-contracts-v1.json`](../ai/compile-time-contracts-v1.json)
51
+ artifact maps stable `KUI-T###` ids to public imports, emitted symbols, and
52
+ catalog identities. Its positive/negative fixture compiles against both source
53
+ and a freshly packed package, so source declarations and shipped declarations
54
+ cannot silently diverge. See [Compile-time contracts](./type-contracts.md) for
55
+ the full audit, migration guidance, and the relationships deliberately left to
56
+ runtime/catalog checks because TypeScript cannot prove them.
57
+
58
+ ### Application UI profile
59
+
60
+ The component catalogs describe what packages provide; an application profile
61
+ describes which supported choices a project has approved. A workspace may
62
+ check in `.kerf-ui-profile.json` conforming to
63
+ [`application-ui-profile.schema.json`](../ai/application-ui-profile.schema.json).
64
+ Keep package-qualified catalog locations and recurring-concept preferences,
65
+ allowed color schemes and density, public semantic-token overrides,
66
+ layout/responsive conventions, and narrow rule exceptions there. Product
67
+ records, copy, permissions, user preferences, and transport state do not belong
68
+ in this policy file.
69
+
70
+ Every catalog location names a v2 composition artifact. `selection` is optional
71
+ for consumer packages whose generated metadata declares v1 selection guidance
72
+ not applicable; `@kerfjs/ui` retains its required v1 selection artifact. This
73
+ lets a generated `component-catalog-v2.json` participate directly without a
74
+ fabricated compatibility file.
75
+
76
+ Discovery and precedence are deterministic:
77
+
78
+ 1. Load `application-ui-profile.defaults.json` from `@kerfjs/ui`.
79
+ 2. Load `.kerf-ui-profile.json` at the workspace root when present.
80
+ 3. Walk from the workspace root toward the target directory and load each
81
+ directory-local profile in parent-to-child order.
82
+
83
+ Later scalar and object-map values win. Catalogs merge by package; preferences
84
+ and token overrides merge by key; exceptions merge by stable id. Lists such as
85
+ allowed themes/densities replace the earlier list rather than accumulating.
86
+ Every resolved field retains its source file. The shipped
87
+ `application-ui-profile.mjs` API implements discovery, merge, loading, and
88
+ validation and reports actionable originating file + JSON-path diagnostics for stale
89
+ catalogs, unknown component/rule/token references, preference conflicts, and
90
+ broad exceptions. Each raw layer is validated against the catalogs effective at
91
+ that exact precedence point before merge, so a broken parent catalog, stale
92
+ parent reference, invalid value, or unknown field cannot disappear merely
93
+ because a child profile replaces it. The shipped
94
+ `application-ui-profile-sync.cjs` projects the same merge and validation contract
95
+ for synchronous hosts such as ESLint rules; it deliberately performs no async
96
+ I/O.
97
+ See the
98
+ [`application-ui-profile.json`](./examples/application-ui-profile.json) example.
99
+
100
+ Catalog detail footers use one standard resource vocabulary and order. Build
101
+ them with `catalogResources()` from `@kerfjs/ui/catalog-resources`: `Demo source`
102
+ first, optional `Component source` and `Design template`, then `Guidance`.
103
+ Third-party entries use the typed `integrationGuidance` kind, which renders
104
+ `Integration guidance`, and omit `Component source` when the implementation is
105
+ not owned by the catalog's project. Prefer these semantic kinds over local labels
106
+ such as “View source” or “Read UI guidance”; the helper makes that convention a
107
+ compile-time choice while still returning ordinary `CatalogResource[]` values.
108
+
21
109
  ## Ownership boundaries
22
110
 
23
111
  ### Geometry ownership metadata
@@ -55,7 +143,8 @@ claim Kerf delivery paths for them.
55
143
  - Consumers style through `--kui-*` semantic tokens and public component classes. Foundation tokens provide opinionated neutral, brand/info, success, warning, and danger fill/border/foreground roles. Stateful components expose local override variables; prefer an equivalent prop or token before writing a selector.
56
144
 
57
145
  `ListItem.rootAttributes`, `ListActionRow.rootAttributes`,
58
- `ListHeader.rootAttributes`, and `AppTab.rootAttributes` accept
146
+ `ListHeader.rootAttributes`, `AppTab.rootAttributes`,
147
+ `CatalogExample.rootAttributes`, and `CatalogExampleStack.rootAttributes` accept
59
148
  application-owned `data-*` metadata without
60
149
  adding product fields to the shared API. A `ListActionRow` trailing action uses
61
150
  `trailingActionAttributes`, and a `ListHeader` action/disclosure uses
@@ -64,7 +153,7 @@ the corresponding `aria-controls`/`aria-haspopup` relationship. These slots do
64
153
  not accept roles or component-owned action, selection, disclosure, accessible
65
154
  name, disabled, or icon fields. Extension objects are filtered at runtime,
66
155
  including case-insensitive rejection of protected `data-*` names, and the
67
- component writes its protected attributes after the accepted metadata. Typed,
156
+ component or helper writes its protected attributes after the accepted metadata. Typed,
68
157
  structurally widened, and JavaScript callers therefore cannot replace its
69
158
  contract.
70
159
 
@@ -93,6 +182,11 @@ layout cannot be expressed by a prop or token. A class being public does not
93
182
  make copied component markup an invocation or transfer state and accessibility
94
183
  ownership to the application.
95
184
 
185
+ The v2 composition catalog additionally requires `boundaries.rootClass` to be
186
+ either one exact member of `publicClasses` or `null` when the entry has no
187
+ rendered class root. Runtime geometry tooling uses this explicit field; array
188
+ order never implies root ownership.
189
+
96
190
  Do not select a component's descendant by element name, id, attribute alone, or
97
191
  an unlisted implementation class. Selectors such as `.kui-state-banner span`,
98
192
  `.kui-list-item [data-state]`, and `.kui-list-item .local-label` depend on
@@ -131,9 +225,12 @@ separator with a compact grip that appears on hover or keyboard focus. Override
131
225
  `--kui-resizable-region-handle-active-color` on a region when its containing
132
226
  surface needs a different contrast level.
133
227
 
134
- `@kerfjs/ui/layout.css` defines one structural model for sidebars, main areas,
135
- inspectors, and dialogs. `.kui-pane` is unpadded and contains an optional
136
- toolbar, one scrolling `.kui-pane__content`, and an optional footer.
228
+ `Pane` defines one structural model for sidebars, main areas, inspectors, and
229
+ dialogs. Its `.kui-pane` root is unpadded and contains an optional vertical
230
+ header, one scrolling vertical `.kui-pane__content`, and an optional footer.
231
+ Logical-edge separator lines are independently opt-in and default off.
232
+ `@kerfjs/ui/layout.css` retains the pane roles and supplies the related content
233
+ geometry classes.
137
234
  `.kui-content` gives major children 24px vertical separation.
138
235
  `.kui-content-item` gives one child 8px inline margin, a real 1px border,
139
236
  8px padding, and 12px corners; border and background may be transparent without
@@ -45,6 +45,11 @@ application boundary:
45
45
  Recipes use public production exports and show ownership boundaries; they are
46
46
  copyable reference compositions, not new monolithic components.
47
47
 
48
+ When presenting a component or recipe in a Catalog, use the authoritative
49
+ [Catalog demo authoring contract](./catalog.md#catalog-demo-authoring-contract)
50
+ rather than deriving preview structure or geometry-overlay behavior from these
51
+ selection entries.
52
+
48
53
  ## Missing recurring concepts
49
54
 
50
55
  Kerf UI does not export a command-palette component. Do not invent a package
@@ -71,7 +76,8 @@ an upstream component or recipe request.
71
76
  | Navigation section heading — `ListHeader` | A menu section needs a full-width label, semantic count, non-count badge, logical-end action, or real disclosure state. | Do not concatenate counts into `label` or put numeric content in `badge`; use `count` with the localized full phrase in `countLabel`. Do not add a disclosure arrow to navigation that reveals nothing. Do not shrink its 44px action target to the 18px visual. Do not use it as a page, panel, or dialog title; use `PanelHeader`. | Delegate its optional action; the app controls expanded state and revealed content. Toggle mode supplies `DisclosureArrow` unless `actionIcon` replaces it. Use `triggerAttributes` only for domain `data-*` or a native popover relationship. | Section organization, valid count and localized count label, disclosure state and content, non-count badge content, popover target behavior, and policy. | `@kerfjs/ui/list-header` | [Pane geometry](../README.md#pane-and-content-geometry) |
72
77
  | Inset self-bordered control — `ListInsetControl` | A control that owns its own border and padding but no outer margin (a search input, a `SegmentedControl`) must line up inside a `.kui-content` list with the standard 8px inline margins and stretch to fill the row. | Do not wrap a `.kui-content-item` or a `ListItem`/`ListHeader` that already owns its inline margin — that double-insets it. Do not add ad-hoc `margin`/`padding` around a bare control to align it; use this instead. | Place the self-bordered control(s) as children; they stretch to fill. It applies only the inline margin, flex stretch, and 8px gap — the child owns its own border and padding. | The control(s), their state, and action wiring. | `@kerfjs/ui/list-inset-control` | [Pane geometry](../README.md#component-subpaths) |
73
78
  | Inset bare text — `ListInsetText` | A plain string or inline content with no margin, border, or padding of its own must sit in a `.kui-content` list with its text edge aligned to the bordered items around it. | Do not use it to wrap a component that already owns content-item geometry (`ListItem`, `StateBanner`, a `.kui-content-item`) — that double-insets it. Do not hand-roll the 8px margin / 1px border / 8px padding. | Pass the text or inline `SafeHtml` as children; it supplies the 8px inline margin, 1px transparent border, and 8px padding so the text edge lands at the standard 17px inset. Pass `horizontalOnly` to keep that horizontal inset but drop the vertical margin, border, and padding for tight text layout. | The text, copy, and localization. | `@kerfjs/ui/list-inset-text` | [Pane geometry](../README.md#component-subpaths) |
74
- | Application layout composition | A sidebar, main area, inspector, or dialog needs shared toolbar/content/footer and child geometry. | Do not pad the pane shell, wrap child-owned geometry in competing insets, invent unrelated centered measures, or leave an icon-only rail for a hidden pane. | Use `.kui-pane` and one `.kui-pane__content`; add `.kui-content` and `.kui-content-item` as needed. A visible pane owns collapse in its toolbar; move a hidden inline-start pane's restore control to the main toolbar leading edge and an inline-end pane's restore control to its trailing edge. | Layout hierarchy, reading width, scroll ownership, responsive relocation, and pane visibility state. | `@kerfjs/ui/layout.css` | [Pane anatomy](./layout.md#anatomy) |
79
+ | Application column — `Pane` | A sidebar, main area, inspector, or dialog needs shared vertical header/content/footer organization, one scroll owner, and optional edge separators. | Do not pad the pane shell, wrap child-owned geometry in competing insets, invent unrelated centered measures, or leave an icon-only rail for a hidden pane. | Import `Pane`, provide optional fixed header/footer slots around its primary vertical content, and opt into only the needed logical-edge separators; add `.kui-content-item` as needed. A visible pane owns collapse in its toolbar; move a hidden inline-start pane's restore control to the main toolbar leading edge and an inline-end pane's restore control to its trailing edge. | Layout hierarchy, reading width, responsive relocation, pane visibility state, slot semantics, labels, and separator placement. | `@kerfjs/ui/pane` | [Pane anatomy](./layout.md#anatomy) |
80
+ | Lowered content surface — `SunkenPanel` | A main work area or nested content group needs one visually lowered surface with a compact inset and vertical stack. | Do not use it merely to add padding, as a replacement for `Pane` anatomy, or around children that already own the same outer surface. | Compose the application-owned content as children and pass `ariaLabel` only when the surface is a distinct named region. | Region semantics, accessible name, scroll ownership, child ordering, and content. | `@kerfjs/ui/sunken-panel` | [SunkenPanel](./sunken-panel.md) |
75
81
  | Menu composition | Navigation sections need full-size rows and the same content-item geometry as every other pane. | Do not add sidebar-specific wrapper padding, shrink targets to icon size, nest an interactive trailing control in `ListItem`, or use a chevron on a row that does not disclose content. Use ordinary links for a different navigation contract. | Compose `ListHeader`, `ListItem`, and `ListActionRow` in `.kui-content`; use `ListHeader` toggle mode with real controlled content, and use `.kui-content-item` for other surfaces plus a pane footer for toolbar actions. | Information architecture, disclosure content and state, responsive drawer/shell behavior, and token overrides. | `@kerfjs/ui/layout.css` | [Pane geometry](../README.md#pane-and-content-geometry) |
76
82
  | Resizable application pane — `ResizableRegion`, `clampRegionSize`, `resizeRegionFromPointer` | A controlled split pane needs the Kerf separator, collapse state, pointer plus keyboard resizing, or a product-specific decorative grip. | Do not use it for a static two-column layout; use CSS grid. Prefer it over Web Awesome `wa-split-panel` unless that component's distinct API is required. Keep `handleIcon` noninteractive. | Call `wireResizableRegions` from `@kerfjs/ui/wire-resizable-regions` once and retain its disposer. | Size signal, min/max policy, collapse policy, persistence, and optional decorative handle icon. | `@kerfjs/ui/resizable-region` | [ResizableRegion contract](./accessibility.md#resizableregion) |
77
83
  | One application tab — `AppTab` | A controlled app tab needs selection, close, drag, leading/trailing anatomy, safe domain metadata, or a product-specific close glyph. | Do not render it alone or use it for a small settings choice; compose in `TabBar`, or use `SegmentedControl`. Keep `closeIcon` noninteractive. | Compose in `TabBar`; let `wireTabBars` manage interaction. Put only domain `data-*` values in `rootAttributes`. | Tab identity, order, selection, close policy, content, and domain metadata values. | `@kerfjs/ui/app-tab` | [Tabs contract](./accessibility.md#tabs) |
@@ -142,8 +148,7 @@ Correct: let the pane stay unpadded while its children own the shared 8/1/8
142
148
  geometry and 44px targets.
143
149
 
144
150
  ```tsx
145
- <aside class="kui-pane">
146
- <nav class="kui-pane__content kui-content">
151
+ <Pane element="aside" contentElement="nav" label="Workspace" contentLabel="Workspace pages">
147
152
  <section>
148
153
  <ListHeader label="Workspace" />
149
154
  <ListItem action="open" label="Inbox" icon={inboxIcon} />
@@ -156,8 +161,7 @@ geometry and 44px targets.
156
161
  />
157
162
  </section>
158
163
  <div class="kui-content-item">Workspace details</div>
159
- </nav>
160
- </aside>
164
+ </Pane>
161
165
  ```
162
166
 
163
167
  Incorrect: duplicating component-like rows and compensating for nested padding
@@ -50,16 +50,14 @@ toolbar groups use 8px. Rows and actions keep 44px targets.
50
50
  Use the package composition so the geometry has one owner:
51
51
 
52
52
  ```tsx
53
- <aside class="kui-pane">
54
- <nav class="kui-pane__content kui-content">
53
+ <Pane element="aside" contentElement="nav" label="Workspace" contentLabel="Workspace pages">
55
54
  <section>
56
55
  <ListHeader label="Workspace" />
57
56
  <ListItem action="open" label="Inbox" icon={inboxIcon} />
58
57
  <ListItem action="open" label="Drafts" />
59
58
  </section>
60
59
  <div class="kui-content-item">Panel contents</div>
61
- </nav>
62
- </aside>
60
+ </Pane>
63
61
  ```
64
62
 
65
63
  Do not pad the pane and then pad every wrapper. That duplicates the geometry
@@ -0,0 +1,60 @@
1
+ {
2
+ "$schema": "../../ai/application-ui-profile.schema.json",
3
+ "schemaVersion": 1,
4
+ "scope": "workspace",
5
+ "catalogs": [
6
+ {
7
+ "package": "@kerfjs/ui",
8
+ "selection": {
9
+ "path": "../../ai/component-catalog.json",
10
+ "schemaVersion": 1
11
+ },
12
+ "composition": {
13
+ "path": "../../ai/component-catalog-v2.json",
14
+ "schemaVersion": 2
15
+ }
16
+ },
17
+ {
18
+ "package": "@acme/ui",
19
+ "composition": {
20
+ "path": "./component-catalog-extension-v2.json",
21
+ "schemaVersion": 2
22
+ }
23
+ }
24
+ ],
25
+ "preferences": {
26
+ "application-shell": {
27
+ "preferred": "@kerfjs/ui:recipe-app-shell",
28
+ "rationale": "The workspace uses the standard resizable three-region shell."
29
+ },
30
+ "record-inspector": {
31
+ "preferred": "@acme/ui:inspector",
32
+ "rationale": "The application-owned inspector maps record policy onto Kerf panel primitives."
33
+ }
34
+ },
35
+ "theme": {
36
+ "colorScheme": "light",
37
+ "allowedColorSchemes": ["light", "dark"],
38
+ "density": "standard",
39
+ "allowedDensities": ["standard"]
40
+ },
41
+ "tokens": {
42
+ "--kui-color-brand-fill-normal": "oklch(0.62 0.16 255)"
43
+ },
44
+ "layout": {
45
+ "shell": "@kerfjs/ui:recipe-app-shell",
46
+ "pane": "@kerfjs/ui:pane",
47
+ "responsiveStrategy": "device-class",
48
+ "compactNavigation": "nav-stack",
49
+ "scrollOwnership": "one-per-pane",
50
+ "spacingScale": "kerf-five-step"
51
+ },
52
+ "exceptions": [
53
+ {
54
+ "id": "legacy-report-grid",
55
+ "rules": ["KUI-C101"],
56
+ "target": "src/features/legacy-report/report-grid.tsx",
57
+ "rationale": "The embedded report engine owns this isolated toolbar-compatible control row."
58
+ }
59
+ ]
60
+ }