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