@kerfjs/ui 5.0.0-beta.18 → 5.0.0-beta.22

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 (170) 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-QIRXYKCE.js → chunk-DSNIYOZ3.js} +3 -2
  42. package/dist/chunk-DSNIYOZ3.js.map +1 -0
  43. package/dist/{chunk-ENVTNLYC.js → chunk-GP5DP5DL.js} +55 -15
  44. package/dist/chunk-GP5DP5DL.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/templates/panel-header/icon-actions-dark.svg +1 -1
  106. package/docs/design/templates/panel-header/icon-actions.svg +1 -1
  107. package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -1
  108. package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -1
  109. package/docs/design/templates/panel-header/no-icon-dark.svg +1 -1
  110. package/docs/design/templates/panel-header/no-icon.svg +1 -1
  111. package/docs/design/templates/panel-header/page-heading-dark.svg +1 -1
  112. package/docs/design/templates/panel-header/page-heading.svg +1 -1
  113. package/docs/design/templates/panel-header/title-only-dark.svg +1 -1
  114. package/docs/design/templates/panel-header/title-only.svg +1 -1
  115. package/docs/design/templates/panel-header-dark.svg +5 -5
  116. package/docs/design/templates/panel-header.svg +5 -5
  117. package/docs/design/templates/segmented-control/equal-dark.svg +1 -1
  118. package/docs/design/templates/segmented-control/equal.svg +1 -1
  119. package/docs/design/templates/segmented-control-dark.svg +1 -1
  120. package/docs/design/templates/segmented-control.svg +1 -1
  121. package/docs/design/templates/toolbar/title-and-actions-dark.svg +1 -1
  122. package/docs/design/templates/toolbar/title-and-actions.svg +1 -1
  123. package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -1
  124. package/docs/design/templates/toolbar-control-group/with-text.svg +1 -1
  125. package/docs/design/templates/toolbar-control-group-dark.svg +1 -1
  126. package/docs/design/templates/toolbar-control-group.svg +1 -1
  127. package/docs/design/templates/toolbar-dark.svg +1 -1
  128. package/docs/design/templates/toolbar-text/default-dark.svg +1 -1
  129. package/docs/design/templates/toolbar-text/default.svg +1 -1
  130. package/docs/design/templates/toolbar-text/large-dark.svg +1 -1
  131. package/docs/design/templates/toolbar-text/large.svg +1 -1
  132. package/docs/design/templates/toolbar-text/small-dark.svg +1 -1
  133. package/docs/design/templates/toolbar-text/small.svg +1 -1
  134. package/docs/design/templates/toolbar-text-dark.svg +3 -3
  135. package/docs/design/templates/toolbar-text.svg +3 -3
  136. package/docs/design/templates/toolbar.svg +1 -1
  137. package/docs/design/templates.md +11 -6
  138. package/docs/design-philosophy.md +2 -4
  139. package/docs/examples/application-ui-profile.json +60 -0
  140. package/docs/examples/component-catalog-extension-v2.json +81 -0
  141. package/docs/layout.md +42 -23
  142. package/docs/sunken-panel.md +43 -0
  143. package/docs/type-contracts.md +99 -0
  144. package/docs/ui-analyzer.md +81 -0
  145. package/docs/ui-doctor.md +77 -0
  146. package/docs/ui-evaluator.md +122 -0
  147. package/doctor/cli.mjs +115 -0
  148. package/doctor/config.schema.json +68 -0
  149. package/doctor/index.d.mts +116 -0
  150. package/doctor/index.mjs +1256 -0
  151. package/doctor/report.schema.json +118 -0
  152. package/evaluator/cli.mjs +89 -0
  153. package/evaluator/index.d.mts +90 -0
  154. package/evaluator/index.mjs +1138 -0
  155. package/evaluator/report.schema.json +152 -0
  156. package/llms.txt +40 -3
  157. package/package.json +66 -8
  158. package/ux-demo/recipes/compact-toolbar.tsx +2 -2
  159. package/ux-demo/recipes/composer-form.tsx +1 -1
  160. package/dist/chunk-4TRDV6RL.js.map +0 -1
  161. package/dist/chunk-6ETZKKAE.js.map +0 -1
  162. package/dist/chunk-ENVTNLYC.js.map +0 -1
  163. package/dist/chunk-JTHUXDN2.js.map +0 -1
  164. package/dist/chunk-NVWXKU37.js.map +0 -1
  165. package/dist/chunk-QIRXYKCE.js.map +0 -1
  166. package/dist/chunk-S3HKPI7I.js.map +0 -1
  167. package/dist/chunk-S3PCZ7NV.js.map +0 -1
  168. package/dist/chunk-U5IFGQCD.js.map +0 -1
  169. package/dist/chunk-ZK2G3VR5.js.map +0 -1
  170. package/dist/chunk-ZVI5YST2.js.map +0 -1
@@ -0,0 +1,529 @@
1
+ 'use strict';
2
+
3
+ const { existsSync, readFileSync } = require('node:fs');
4
+ const { dirname, isAbsolute, relative, resolve } = require('node:path');
5
+
6
+ const record = (provenance, source, prefix, value) => {
7
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return;
8
+ for (const key of Object.keys(value)) provenance[`${prefix}.${key}`] = source;
9
+ };
10
+
11
+ function mergeApplicationUiProfiles(layers) {
12
+ if (!layers.length)
13
+ throw new Error('At least one application UI profile is required.');
14
+ const profile = { schemaVersion: 1, scope: 'package' };
15
+ const provenance = {};
16
+ const catalogs = new Map();
17
+ const exceptions = new Map();
18
+ for (const { source, profile: layer } of layers) {
19
+ if (!layer || typeof layer !== 'object' || Array.isArray(layer)) continue;
20
+ profile.schemaVersion = layer.schemaVersion;
21
+ profile.scope = layer.scope;
22
+ provenance.$schemaVersion = source;
23
+ provenance.$scope = source;
24
+ if (layer.$schema) profile.$schema = layer.$schema;
25
+ for (const catalog of Array.isArray(layer.catalogs) ? layer.catalogs : []) {
26
+ if (!catalog || typeof catalog !== 'object' || Array.isArray(catalog))
27
+ continue;
28
+ catalogs.set(catalog.package, catalog);
29
+ provenance[`$catalogs.${catalog.package}`] = source;
30
+ }
31
+ for (const field of ['preferences', 'theme', 'tokens', 'layout'])
32
+ if (
33
+ layer[field] &&
34
+ typeof layer[field] === 'object' &&
35
+ !Array.isArray(layer[field])
36
+ ) {
37
+ profile[field] = { ...profile[field], ...layer[field] };
38
+ record(provenance, source, `$${field}`, layer[field]);
39
+ }
40
+ for (const exception of Array.isArray(layer.exceptions)
41
+ ? layer.exceptions
42
+ : []) {
43
+ if (
44
+ !exception ||
45
+ typeof exception !== 'object' ||
46
+ Array.isArray(exception)
47
+ )
48
+ continue;
49
+ exceptions.set(exception.id, exception);
50
+ provenance[`$exceptions.${exception.id}`] = source;
51
+ }
52
+ }
53
+ if (catalogs.size) profile.catalogs = [...catalogs.values()];
54
+ if (
55
+ exceptions.size ||
56
+ layers.some(({ profile: layer }) => Array.isArray(layer?.exceptions))
57
+ )
58
+ profile.exceptions = [...exceptions.values()];
59
+ return { profile, provenance };
60
+ }
61
+
62
+ function validateApplicationUiProfileLayers(layers) {
63
+ const diagnostics = [];
64
+ const ranks = { package: 0, workspace: 1, directory: 2 };
65
+ const counts = new Map();
66
+ let previousRank = -1;
67
+ let previousDirectory;
68
+ for (const { source, profile } of layers) {
69
+ const rank = ranks[profile?.scope];
70
+ counts.set(profile?.scope, (counts.get(profile?.scope) ?? 0) + 1);
71
+ if (rank === undefined || rank < previousRank)
72
+ diagnostics.push({
73
+ code: 'KUI-P027',
74
+ source,
75
+ path: '$.scope',
76
+ message:
77
+ 'Profile layers must be ordered package, workspace, then parent-to-child directory.',
78
+ });
79
+ if (rank !== undefined) previousRank = Math.max(previousRank, rank);
80
+ if (profile?.scope === 'directory') {
81
+ const currentDirectory = dirname(source);
82
+ if (
83
+ previousDirectory &&
84
+ relative(previousDirectory, currentDirectory).startsWith('..')
85
+ )
86
+ diagnostics.push({
87
+ code: 'KUI-P029',
88
+ source,
89
+ path: '$.scope',
90
+ message: 'Directory profiles must be ordered from parent to child.',
91
+ });
92
+ previousDirectory = currentDirectory;
93
+ }
94
+ }
95
+ for (const scope of ['package', 'workspace'])
96
+ if ((counts.get(scope) ?? 0) > 1)
97
+ diagnostics.push({
98
+ code: 'KUI-P028',
99
+ source: layers.findLast(({ profile }) => profile.scope === scope)
100
+ .source,
101
+ path: '$.scope',
102
+ message: `At most one ${scope} profile layer is allowed.`,
103
+ });
104
+ if ((counts.get('package') ?? 0) !== 1)
105
+ diagnostics.push({
106
+ code: 'KUI-P028',
107
+ source: layers[0]?.source ?? '<profiles>',
108
+ path: '$.scope',
109
+ message: 'Exactly one package profile must be the first layer.',
110
+ });
111
+ return diagnostics;
112
+ }
113
+
114
+ const stringList = (value) =>
115
+ Array.isArray(value) &&
116
+ value.every((item) => typeof item === 'string' && item.length > 0) &&
117
+ new Set(value).size === value.length;
118
+
119
+ function validateApplicationUiProfile(profile, options = {}) {
120
+ const {
121
+ source = '<profile>',
122
+ knownComponents,
123
+ knownTokens,
124
+ knownRules,
125
+ } = options;
126
+ const diagnostics = [];
127
+ const componentSet = knownComponents && new Set(knownComponents);
128
+ const tokenSet = knownTokens && new Set(knownTokens);
129
+ const ruleSet = knownRules && new Set(knownRules);
130
+ const add = (code, path, message) =>
131
+ diagnostics.push({ code, source, path, message });
132
+ const objectAt = (value, path) => {
133
+ if (value === undefined) return undefined;
134
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
135
+ add('KUI-P030', path, 'Expected an object.');
136
+ return undefined;
137
+ }
138
+ return value;
139
+ };
140
+ const arrayAt = (value, path) => {
141
+ if (value === undefined) return [];
142
+ if (!Array.isArray(value)) {
143
+ add('KUI-P030', path, 'Expected an array.');
144
+ return [];
145
+ }
146
+ return value;
147
+ };
148
+ if (!profile || typeof profile !== 'object' || Array.isArray(profile)) {
149
+ add('KUI-P030', '$', 'Expected a profile object.');
150
+ return diagnostics;
151
+ }
152
+ const rejectUnknown = (value, allowed, path) => {
153
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return;
154
+ for (const key of Object.keys(value))
155
+ if (!allowed.includes(key))
156
+ add('KUI-P023', `${path}.${key}`, `Unknown profile property ${key}.`);
157
+ };
158
+ rejectUnknown(
159
+ profile,
160
+ [
161
+ '$schema',
162
+ 'schemaVersion',
163
+ 'scope',
164
+ 'catalogs',
165
+ 'preferences',
166
+ 'theme',
167
+ 'tokens',
168
+ 'layout',
169
+ 'exceptions',
170
+ ],
171
+ '$',
172
+ );
173
+ if (profile.schemaVersion !== 1)
174
+ add('KUI-P001', '$.schemaVersion', 'schemaVersion must equal 1.');
175
+ if (!['package', 'workspace', 'directory'].includes(profile.scope))
176
+ add(
177
+ 'KUI-P002',
178
+ '$.scope',
179
+ 'scope must be package, workspace, or directory.',
180
+ );
181
+ const catalogs = arrayAt(profile.catalogs, '$.catalogs');
182
+ const preferences = objectAt(profile.preferences, '$.preferences') ?? {};
183
+ const theme = objectAt(profile.theme, '$.theme');
184
+ const tokens = objectAt(profile.tokens, '$.tokens') ?? {};
185
+ const layout = objectAt(profile.layout, '$.layout');
186
+ const exceptions = arrayAt(profile.exceptions, '$.exceptions');
187
+ const packages = new Set();
188
+ for (const [index, catalog] of catalogs.entries()) {
189
+ const path = `$.catalogs[${index}]`;
190
+ if (!objectAt(catalog, path)) continue;
191
+ rejectUnknown(catalog, ['package', 'selection', 'composition'], path);
192
+ if (!catalog.package)
193
+ add('KUI-P003', `${path}.package`, 'Catalog package is required.');
194
+ if (packages.has(catalog.package))
195
+ add(
196
+ 'KUI-P004',
197
+ `${path}.package`,
198
+ `Catalog package ${catalog.package} is duplicated.`,
199
+ );
200
+ packages.add(catalog.package);
201
+ if (catalog.package === '@kerfjs/ui' && !catalog.selection)
202
+ add(
203
+ 'KUI-P005',
204
+ `${path}.selection`,
205
+ 'The @kerfjs/ui catalog requires its v1 selection location.',
206
+ );
207
+ for (const kind of ['selection', 'composition']) {
208
+ if (kind === 'selection' && !catalog[kind]) continue;
209
+ rejectUnknown(
210
+ catalog[kind],
211
+ ['path', 'schemaVersion'],
212
+ `${path}.${kind}`,
213
+ );
214
+ if (
215
+ !catalog[kind]?.path ||
216
+ !Number.isInteger(catalog[kind]?.schemaVersion)
217
+ )
218
+ add(
219
+ 'KUI-P005',
220
+ `${path}.${kind}`,
221
+ `${kind} requires a path and integer schemaVersion.`,
222
+ );
223
+ }
224
+ }
225
+ for (const [concept, raw] of Object.entries(preferences)) {
226
+ const path = `$.preferences.${concept}`;
227
+ const preference = objectAt(raw, path);
228
+ rejectUnknown(preference, ['preferred', 'avoid', 'rationale'], path);
229
+ if (!/^[a-z][a-z0-9-]*$/.test(concept))
230
+ add(
231
+ 'KUI-P024',
232
+ path,
233
+ 'Preference concept keys must be lowercase kebab-case.',
234
+ );
235
+ if (!preference?.preferred)
236
+ add(
237
+ 'KUI-P006',
238
+ `${path}.preferred`,
239
+ 'A preferred package:id is required.',
240
+ );
241
+ if (!stringList(preference?.avoid ?? []))
242
+ add(
243
+ 'KUI-P007',
244
+ `${path}.avoid`,
245
+ 'avoid must contain unique package:id values.',
246
+ );
247
+ if ((preference?.avoid ?? []).includes(preference?.preferred))
248
+ add(
249
+ 'KUI-P008',
250
+ path,
251
+ `${preference.preferred} cannot be both preferred and avoided.`,
252
+ );
253
+ for (const [field, refs] of [
254
+ ['preferred', preference?.preferred ? [preference.preferred] : []],
255
+ ['avoid', preference?.avoid ?? []],
256
+ ])
257
+ for (const reference of refs)
258
+ if (!/^.+:.+$/.test(reference))
259
+ add(
260
+ 'KUI-P024',
261
+ `${path}.${field}`,
262
+ `${reference} must use package:id identity.`,
263
+ );
264
+ else if (componentSet && !componentSet.has(reference))
265
+ add(
266
+ 'KUI-P009',
267
+ `${path}.${field}`,
268
+ `Unknown or stale component reference ${reference}.`,
269
+ );
270
+ }
271
+ rejectUnknown(
272
+ theme,
273
+ ['colorScheme', 'allowedColorSchemes', 'density', 'allowedDensities'],
274
+ '$.theme',
275
+ );
276
+ const colorSchemes = ['light', 'dark', 'invert', 'system'];
277
+ const densities = ['standard', 'compact'];
278
+ if (
279
+ theme?.colorScheme &&
280
+ Array.isArray(theme.allowedColorSchemes) &&
281
+ !theme.allowedColorSchemes.includes(theme.colorScheme)
282
+ )
283
+ add(
284
+ 'KUI-P010',
285
+ '$.theme.colorScheme',
286
+ 'colorScheme must be included in allowedColorSchemes.',
287
+ );
288
+ if (
289
+ theme?.density &&
290
+ Array.isArray(theme.allowedDensities) &&
291
+ !theme.allowedDensities.includes(theme.density)
292
+ )
293
+ add(
294
+ 'KUI-P011',
295
+ '$.theme.density',
296
+ 'density must be included in allowedDensities.',
297
+ );
298
+ if (theme?.colorScheme && !colorSchemes.includes(theme.colorScheme))
299
+ add('KUI-P026', '$.theme.colorScheme', 'Unknown color scheme.');
300
+ if (theme?.density && !densities.includes(theme.density))
301
+ add('KUI-P026', '$.theme.density', 'Unknown density.');
302
+ for (const value of arrayAt(
303
+ theme?.allowedColorSchemes,
304
+ '$.theme.allowedColorSchemes',
305
+ ))
306
+ if (!colorSchemes.includes(value))
307
+ add(
308
+ 'KUI-P026',
309
+ '$.theme.allowedColorSchemes',
310
+ `Unknown color scheme ${value}.`,
311
+ );
312
+ for (const value of arrayAt(
313
+ theme?.allowedDensities,
314
+ '$.theme.allowedDensities',
315
+ ))
316
+ if (!densities.includes(value))
317
+ add('KUI-P026', '$.theme.allowedDensities', `Unknown density ${value}.`);
318
+ for (const token of Object.keys(tokens)) {
319
+ if (!token.startsWith('--kui-'))
320
+ add(
321
+ 'KUI-P012',
322
+ `$.tokens.${token}`,
323
+ 'Only --kui-* semantic tokens may be overridden.',
324
+ );
325
+ else if (tokenSet && !tokenSet.has(token))
326
+ add(
327
+ 'KUI-P013',
328
+ `$.tokens.${token}`,
329
+ `Unknown or private semantic token ${token}.`,
330
+ );
331
+ if (typeof tokens[token] !== 'string' || !tokens[token])
332
+ add(
333
+ 'KUI-P025',
334
+ `$.tokens.${token}`,
335
+ 'Token override must be a non-empty string.',
336
+ );
337
+ }
338
+ rejectUnknown(
339
+ layout,
340
+ [
341
+ 'shell',
342
+ 'pane',
343
+ 'responsiveStrategy',
344
+ 'compactNavigation',
345
+ 'scrollOwnership',
346
+ 'spacingScale',
347
+ ],
348
+ '$.layout',
349
+ );
350
+ for (const field of ['shell', 'pane']) {
351
+ const reference = layout?.[field];
352
+ if (reference && !/^.+:.+$/.test(reference))
353
+ add(
354
+ 'KUI-P024',
355
+ `$.layout.${field}`,
356
+ `${reference} must use package:id identity.`,
357
+ );
358
+ else if (reference && componentSet && !componentSet.has(reference))
359
+ add(
360
+ 'KUI-P014',
361
+ `$.layout.${field}`,
362
+ `Unknown or stale component reference ${reference}.`,
363
+ );
364
+ }
365
+ const ids = new Set();
366
+ for (const [index, exception] of exceptions.entries()) {
367
+ const path = `$.exceptions[${index}]`;
368
+ if (!objectAt(exception, path)) continue;
369
+ rejectUnknown(exception, ['id', 'rules', 'target', 'rationale'], path);
370
+ if (!exception.id || ids.has(exception.id))
371
+ add(
372
+ 'KUI-P015',
373
+ `${path}.id`,
374
+ 'Exception ids must be present and unique.',
375
+ );
376
+ ids.add(exception.id);
377
+ if (!stringList(exception.rules) || !exception.rules.length)
378
+ add(
379
+ 'KUI-P016',
380
+ `${path}.rules`,
381
+ 'An exception must name at least one exact rule id.',
382
+ );
383
+ for (const rule of exception.rules ?? [])
384
+ if (ruleSet && !ruleSet.has(rule))
385
+ add('KUI-P022', `${path}.rules`, `Unknown or stale rule id ${rule}.`);
386
+ if (
387
+ !exception.target ||
388
+ isAbsolute(exception.target) ||
389
+ exception.target.includes('..') ||
390
+ /[*?]/.test(exception.target) ||
391
+ ['.', 'src', 'ui'].includes(exception.target.replace(/\/$/, ''))
392
+ )
393
+ add(
394
+ 'KUI-P017',
395
+ `${path}.target`,
396
+ 'Exception target must be a narrow relative path without traversal or wildcards.',
397
+ );
398
+ if (!exception.rationale || exception.rationale.trim().length < 12)
399
+ add(
400
+ 'KUI-P018',
401
+ `${path}.rationale`,
402
+ 'Exception rationale must explain the narrow need.',
403
+ );
404
+ }
405
+ return diagnostics;
406
+ }
407
+
408
+ const unique = (diagnostics) => [
409
+ ...new Map(
410
+ diagnostics.map((item) => [
411
+ `${item.code}\0${item.source}\0${item.path}`,
412
+ item,
413
+ ]),
414
+ ).values(),
415
+ ];
416
+
417
+ function catalogFacts(resolved, fallbackCatalogs, seedRules) {
418
+ const diagnostics = [];
419
+ const knownComponents = new Set();
420
+ const knownTokens = new Set();
421
+ const knownRules = new Set(seedRules);
422
+ const addComposition = (artifact) => {
423
+ for (const entry of artifact?.entries ?? []) {
424
+ knownComponents.add(entry.key ?? `${artifact.package}:${entry.id}`);
425
+ for (const token of entry.boundaries?.publicTokens ?? [])
426
+ knownTokens.add(token);
427
+ for (const diagnostic of entry.diagnostics ?? [])
428
+ knownRules.add(diagnostic.id);
429
+ }
430
+ };
431
+ if (!(resolved.profile.catalogs ?? []).length)
432
+ for (const artifact of fallbackCatalogs) addComposition(artifact);
433
+ for (const [index, catalog] of (resolved.profile.catalogs ?? []).entries()) {
434
+ const owner = resolved.provenance[`$catalogs.${catalog.package}`];
435
+ if (!owner) continue;
436
+ for (const kind of ['selection', 'composition']) {
437
+ const location = catalog[kind];
438
+ if (!location?.path) continue;
439
+ const path = resolve(dirname(owner), location.path);
440
+ try {
441
+ const artifact = JSON.parse(readFileSync(path, 'utf8'));
442
+ if (
443
+ artifact.package !== catalog.package ||
444
+ artifact.schemaVersion !== location.schemaVersion
445
+ )
446
+ diagnostics.push({
447
+ code: 'KUI-P020',
448
+ source: owner,
449
+ path: `$.catalogs[${index}].${kind}`,
450
+ message: `Catalog ${path} does not match package ${catalog.package} schemaVersion ${location.schemaVersion}.`,
451
+ });
452
+ if (kind === 'composition') addComposition(artifact);
453
+ } catch (error) {
454
+ diagnostics.push({
455
+ code: 'KUI-P021',
456
+ source: owner,
457
+ path: `$.catalogs[${index}].${kind}.path`,
458
+ message: `Cannot load catalog ${path}: ${error.message}`,
459
+ });
460
+ }
461
+ }
462
+ }
463
+ return { diagnostics, knownComponents, knownTokens, knownRules };
464
+ }
465
+
466
+ function loadApplicationUiProfileSync(layers, options = {}) {
467
+ const diagnostics = validateApplicationUiProfileLayers(layers);
468
+ let facts;
469
+ for (let index = 0; index < layers.length; index += 1) {
470
+ const prefix = mergeApplicationUiProfiles(layers.slice(0, index + 1));
471
+ facts = catalogFacts(
472
+ prefix,
473
+ options.fallbackCatalogs ?? [],
474
+ options.knownRules ?? [],
475
+ );
476
+ diagnostics.push(
477
+ ...facts.diagnostics,
478
+ ...validateApplicationUiProfile(layers[index].profile, {
479
+ source: layers[index].source,
480
+ ...facts,
481
+ }),
482
+ );
483
+ }
484
+ const resolved = mergeApplicationUiProfiles(layers);
485
+ diagnostics.push(
486
+ ...validateApplicationUiProfile(resolved.profile, {
487
+ source: layers.at(-1).source,
488
+ ...facts,
489
+ }),
490
+ );
491
+ return { layers, ...resolved, diagnostics: unique(diagnostics) };
492
+ }
493
+
494
+ function discoverApplicationUiProfileFilesSync({
495
+ workspaceRoot,
496
+ startDirectory,
497
+ packageProfile,
498
+ }) {
499
+ const root = resolve(workspaceRoot);
500
+ const start = resolve(startDirectory);
501
+ const outside = relative(root, start);
502
+ if (
503
+ outside === '..' ||
504
+ outside.startsWith(`..${require('node:path').sep}`) ||
505
+ isAbsolute(outside)
506
+ )
507
+ throw new Error(`startDirectory must be inside workspaceRoot: ${start}`);
508
+ const files = [resolve(packageProfile)];
509
+ const directories = [];
510
+ let directory = start;
511
+ while (true) {
512
+ directories.push(directory);
513
+ if (directory === root) break;
514
+ directory = dirname(directory);
515
+ }
516
+ for (const candidate of directories.reverse()) {
517
+ const path = resolve(candidate, '.kerf-ui-profile.json');
518
+ if (existsSync(path) && !files.includes(path)) files.push(path);
519
+ }
520
+ return files;
521
+ }
522
+
523
+ module.exports = {
524
+ discoverApplicationUiProfileFilesSync,
525
+ loadApplicationUiProfileSync,
526
+ mergeApplicationUiProfiles,
527
+ validateApplicationUiProfile,
528
+ validateApplicationUiProfileLayers,
529
+ };
@@ -0,0 +1 @@
1
+ export * from './application-ui-profile.js';
@@ -0,0 +1,102 @@
1
+ export type ApplicationUiProfileScope = 'package' | 'workspace' | 'directory';
2
+ export type QualifiedCatalogKey = `${string}:${string}`;
3
+
4
+ export interface ApplicationUiCatalogLocation {
5
+ path: string;
6
+ schemaVersion: number;
7
+ }
8
+
9
+ export interface ApplicationUiProfile {
10
+ $schema?: string;
11
+ schemaVersion: 1;
12
+ scope: ApplicationUiProfileScope;
13
+ catalogs?: Array<{
14
+ package: string;
15
+ /** Required for @kerfjs/ui; composition-only consumer packages omit it. */
16
+ selection?: ApplicationUiCatalogLocation;
17
+ composition: ApplicationUiCatalogLocation;
18
+ }>;
19
+ preferences?: Record<
20
+ string,
21
+ {
22
+ preferred: QualifiedCatalogKey;
23
+ avoid?: QualifiedCatalogKey[];
24
+ rationale?: string;
25
+ }
26
+ >;
27
+ theme?: {
28
+ colorScheme?: 'light' | 'dark' | 'invert' | 'system';
29
+ allowedColorSchemes?: Array<'light' | 'dark' | 'invert' | 'system'>;
30
+ density?: 'standard' | 'compact';
31
+ allowedDensities?: Array<'standard' | 'compact'>;
32
+ };
33
+ tokens?: Record<`--kui-${string}`, string>;
34
+ layout?: {
35
+ shell?: QualifiedCatalogKey;
36
+ pane?: QualifiedCatalogKey;
37
+ responsiveStrategy?: 'device-class' | 'container' | 'application';
38
+ compactNavigation?: 'nav-stack' | 'tab-scaffold' | 'application';
39
+ scrollOwnership?: 'one-per-pane' | 'application';
40
+ spacingScale?: 'kerf-five-step' | 'application';
41
+ };
42
+ exceptions?: Array<{
43
+ id: string;
44
+ rules: string[];
45
+ target: string;
46
+ rationale: string;
47
+ }>;
48
+ }
49
+
50
+ export interface ApplicationUiProfileLayer {
51
+ source: string;
52
+ profile: ApplicationUiProfile;
53
+ }
54
+
55
+ export interface ApplicationUiProfileDiagnostic {
56
+ code: string;
57
+ source: string;
58
+ path: string;
59
+ message: string;
60
+ }
61
+
62
+ export interface ResolvedApplicationUiProfile {
63
+ profile: ApplicationUiProfile;
64
+ provenance: Record<string, string>;
65
+ }
66
+
67
+ export const APPLICATION_UI_PROFILE_FILENAME: '.kerf-ui-profile.json';
68
+ export function discoverApplicationUiProfileFiles(options: {
69
+ workspaceRoot: string;
70
+ startDirectory: string;
71
+ packageProfile?: string;
72
+ }): Promise<string[]>;
73
+ export function readApplicationUiProfile(
74
+ source: string,
75
+ ): Promise<ApplicationUiProfileLayer>;
76
+ export function mergeApplicationUiProfiles(
77
+ layers: ApplicationUiProfileLayer[],
78
+ ): ResolvedApplicationUiProfile;
79
+ export function validateApplicationUiProfileLayers(
80
+ layers: ApplicationUiProfileLayer[],
81
+ ): ApplicationUiProfileDiagnostic[];
82
+ export function validateApplicationUiProfile(
83
+ profile: ApplicationUiProfile,
84
+ options?: {
85
+ source?: string;
86
+ knownComponents?: Iterable<string>;
87
+ knownTokens?: Iterable<string>;
88
+ knownRules?: Iterable<string>;
89
+ },
90
+ ): ApplicationUiProfileDiagnostic[];
91
+ export function loadApplicationUiProfile(options: {
92
+ workspaceRoot: string;
93
+ startDirectory: string;
94
+ packageProfile?: string;
95
+ knownRules?: Iterable<string>;
96
+ }): Promise<{
97
+ files: string[];
98
+ layers: ApplicationUiProfileLayer[];
99
+ profile?: ApplicationUiProfile;
100
+ provenance?: Record<string, string>;
101
+ diagnostics: ApplicationUiProfileDiagnostic[];
102
+ }>;
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "./application-ui-profile.schema.json",
3
+ "schemaVersion": 1,
4
+ "scope": "package",
5
+ "catalogs": [
6
+ {
7
+ "package": "@kerfjs/ui",
8
+ "selection": { "path": "./component-catalog.json", "schemaVersion": 1 },
9
+ "composition": {
10
+ "path": "./component-catalog-v2.json",
11
+ "schemaVersion": 2
12
+ }
13
+ }
14
+ ],
15
+ "preferences": {
16
+ "application-shell": { "preferred": "@kerfjs/ui:recipe-app-shell" },
17
+ "exclusive-choice": {
18
+ "preferred": "@kerfjs/ui:segmented-control",
19
+ "avoid": ["@kerfjs/ui:wa-button-group"]
20
+ },
21
+ "navigation-row": { "preferred": "@kerfjs/ui:list-item" },
22
+ "panel-heading": { "preferred": "@kerfjs/ui:panel-header" },
23
+ "structured-search": { "preferred": "@kerfjs/ui:token-search-field" }
24
+ },
25
+ "theme": {
26
+ "colorScheme": "light",
27
+ "allowedColorSchemes": ["light", "dark", "invert"],
28
+ "density": "standard",
29
+ "allowedDensities": ["standard"]
30
+ },
31
+ "tokens": {},
32
+ "layout": {
33
+ "shell": "@kerfjs/ui:recipe-app-shell",
34
+ "pane": "@kerfjs/ui:pane",
35
+ "responsiveStrategy": "device-class",
36
+ "compactNavigation": "nav-stack",
37
+ "scrollOwnership": "one-per-pane",
38
+ "spacingScale": "kerf-five-step"
39
+ },
40
+ "exceptions": []
41
+ }