foundry-design 0.2.0-beta.2 → 0.2.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.
- package/README.md +164 -17
- package/dist/companion.d.ts +24 -0
- package/dist/companion.d.ts.map +1 -0
- package/dist/companion.js +89 -0
- package/dist/companion.js.map +1 -0
- package/dist/delivery-export.d.ts +27 -0
- package/dist/delivery-export.d.ts.map +1 -0
- package/dist/delivery-export.js +200 -0
- package/dist/delivery-export.js.map +1 -0
- package/dist/doctor.d.ts +24 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +206 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.js +583 -124
- package/dist/index.js.map +1 -1
- package/dist/indexer.d.ts.map +1 -1
- package/dist/indexer.js +690 -30
- package/dist/indexer.js.map +1 -1
- package/dist/installer.d.ts +46 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +551 -100
- package/dist/installer.js.map +1 -1
- package/dist/project.d.ts +9 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +50 -2
- package/dist/project.js.map +1 -1
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +106 -0
- package/dist/proxy.js.map +1 -0
- package/dist/release.d.ts +5 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +12 -0
- package/dist/release.js.map +1 -0
- package/dist/skill/foundry-design-control/SKILL.md +41 -15
- package/dist/skill/foundry-design-control/references/apply-run-contract.md +13 -7
- package/dist/skill/foundry-design-control/references/change-contract.md +9 -6
- package/dist/skill/foundry-design-control/references/component-workshop.md +46 -0
- package/dist/skill/foundry-design-control/references/design-branches.md +47 -0
- package/dist/skill/foundry-design-control/references/design-system-intelligence.md +40 -0
- package/dist/skill/foundry-design-control/references/motion-studio.md +101 -0
- package/dist/skill/foundry-design-control/references/portable-branch-records.md +38 -0
- package/dist/skill/foundry-design-control/references/responsive-design-lab.md +34 -0
- package/dist/skill/foundry-design-control/references/typography-studio.md +14 -0
- package/dist/skill/foundry-design-control/references/visual-agent-conversation.md +14 -0
- package/dist/skill/foundry-design-control/references/visual-workbench.md +27 -4
- package/dist/skill/foundry-design-control/scripts/foundry.sh +1 -5
- package/package.json +3 -3
package/dist/indexer.js
CHANGED
|
@@ -14,7 +14,31 @@ const SOURCE_EXTENSIONS = new Set([
|
|
|
14
14
|
'.mjs',
|
|
15
15
|
'.cjs',
|
|
16
16
|
'.json',
|
|
17
|
+
'.html',
|
|
17
18
|
]);
|
|
19
|
+
const STYLESHEET_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.less']);
|
|
20
|
+
function stylesheetSegments(file, content) {
|
|
21
|
+
const extension = extname(file).toLowerCase();
|
|
22
|
+
if (STYLESHEET_EXTENSIONS.has(extension))
|
|
23
|
+
return [{ content, offset: 0 }];
|
|
24
|
+
const segments = [];
|
|
25
|
+
const appendMatches = (pattern) => {
|
|
26
|
+
for (const match of content.matchAll(pattern)) {
|
|
27
|
+
const body = match[1];
|
|
28
|
+
if (!body || match.index == null)
|
|
29
|
+
continue;
|
|
30
|
+
const bodyOffset = match[0].indexOf(body);
|
|
31
|
+
segments.push({ content: body, offset: match.index + Math.max(0, bodyOffset) });
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
if (extension === '.html') {
|
|
35
|
+
appendMatches(/<style\b[^>]*>([\s\S]*?)<\/style>/gi);
|
|
36
|
+
}
|
|
37
|
+
else if (/\.[cm]?[jt]sx?$/.test(file)) {
|
|
38
|
+
appendMatches(/\b(?:css|createGlobalStyle|styled(?:\.[A-Za-z_$][\w$]*|\([^)]*\)))\s*`([\s\S]*?)`/g);
|
|
39
|
+
}
|
|
40
|
+
return segments;
|
|
41
|
+
}
|
|
18
42
|
function stableId(prefix, value) {
|
|
19
43
|
return `${prefix}_${createHash('sha1').update(value).digest('hex').slice(0, 12)}`;
|
|
20
44
|
}
|
|
@@ -39,6 +63,290 @@ function tokenCategory(name, value) {
|
|
|
39
63
|
return 'size';
|
|
40
64
|
return 'other';
|
|
41
65
|
}
|
|
66
|
+
function declarationCategory(property, value) {
|
|
67
|
+
const name = property.toLowerCase();
|
|
68
|
+
if (/shadow/.test(name))
|
|
69
|
+
return 'shadow';
|
|
70
|
+
if (/border.*radius|radius/.test(name))
|
|
71
|
+
return 'radius';
|
|
72
|
+
if (/font|line-height|letter-spacing|tracking/.test(name))
|
|
73
|
+
return 'typography';
|
|
74
|
+
if (/transition|animation|duration|easing/.test(name))
|
|
75
|
+
return 'motion';
|
|
76
|
+
if (/gap|padding|margin|inset/.test(name))
|
|
77
|
+
return 'spacing';
|
|
78
|
+
if (/color|background|fill|stroke/.test(name) && /#|rgb|hsl|oklch|color\(/i.test(value))
|
|
79
|
+
return 'color';
|
|
80
|
+
if (/width|height|size/.test(name) && /-?[\d.]+(?:px|rem|em|%|vh|vw)/i.test(value))
|
|
81
|
+
return 'size';
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function comparable(value) {
|
|
85
|
+
return value
|
|
86
|
+
.trim()
|
|
87
|
+
.toLowerCase()
|
|
88
|
+
.replace(/\s*!important\s*$/, '')
|
|
89
|
+
.replaceAll(' ', '');
|
|
90
|
+
}
|
|
91
|
+
function numericValue(value) {
|
|
92
|
+
const match = /^(-?[\d.]+)(px|rem|em|ms|s|%)$/i.exec(comparable(value));
|
|
93
|
+
return match ? { amount: Number(match[1]), unit: match[2].toLowerCase() } : null;
|
|
94
|
+
}
|
|
95
|
+
function closeNumericToken(value, tokens) {
|
|
96
|
+
const parsed = numericValue(value);
|
|
97
|
+
if (!parsed)
|
|
98
|
+
return undefined;
|
|
99
|
+
return tokens
|
|
100
|
+
.map((token) => ({ token, parsed: numericValue(token.value) }))
|
|
101
|
+
.filter((entry) => entry.parsed?.unit === parsed.unit)
|
|
102
|
+
.map((entry) => ({ ...entry, distance: Math.abs(entry.parsed.amount - parsed.amount) }))
|
|
103
|
+
.filter((entry) => entry.distance > 0 && entry.distance <= Math.max(2, Math.abs(parsed.amount) * 0.25))
|
|
104
|
+
.sort((a, b) => a.distance - b.distance)[0]?.token;
|
|
105
|
+
}
|
|
106
|
+
function tokenComparableValue(token) {
|
|
107
|
+
return token.resolvedValue ?? token.value;
|
|
108
|
+
}
|
|
109
|
+
function aliasTarget(value) {
|
|
110
|
+
return /^var\(\s*(--[\w-]+)(?:\s*,[^)]*)?\s*\)$/.exec(value.trim())?.[1];
|
|
111
|
+
}
|
|
112
|
+
function resolveTokenAliases(tokens) {
|
|
113
|
+
const resolve = (token, trail = []) => {
|
|
114
|
+
const targetName = aliasTarget(token.value);
|
|
115
|
+
if (!targetName)
|
|
116
|
+
return { aliasChain: [token.name], aliasStatus: 'direct', resolvedValue: token.value };
|
|
117
|
+
if (trail.includes(token.name)) {
|
|
118
|
+
return { aliasChain: [...trail, token.name], aliasStatus: 'circular' };
|
|
119
|
+
}
|
|
120
|
+
const target = tokens.get(targetName);
|
|
121
|
+
if (!target)
|
|
122
|
+
return { aliasChain: [token.name, targetName], aliasStatus: 'broken' };
|
|
123
|
+
const resolved = resolve(target, [...trail, token.name]);
|
|
124
|
+
return {
|
|
125
|
+
aliasChain: [token.name, ...resolved.aliasChain.filter((name) => name !== token.name)],
|
|
126
|
+
aliasStatus: resolved.aliasStatus === 'direct' ? 'resolved' : resolved.aliasStatus,
|
|
127
|
+
resolvedValue: resolved.resolvedValue,
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
for (const token of tokens.values()) {
|
|
131
|
+
const targetName = aliasTarget(token.value);
|
|
132
|
+
if (targetName) {
|
|
133
|
+
token.aliasOfTokenName = targetName;
|
|
134
|
+
const target = tokens.get(targetName);
|
|
135
|
+
token.aliasOfTokenId = target?.id;
|
|
136
|
+
if (token.category === 'other' && target)
|
|
137
|
+
token.category = target.category;
|
|
138
|
+
}
|
|
139
|
+
Object.assign(token, resolve(token));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function promotionTokenName(category, property, value) {
|
|
143
|
+
const propertySlug = property
|
|
144
|
+
.toLowerCase()
|
|
145
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
146
|
+
.replace(/^-|-$/g, '');
|
|
147
|
+
const valueSlug = value
|
|
148
|
+
.toLowerCase()
|
|
149
|
+
.replace(/[^a-z0-9.]+/g, '-')
|
|
150
|
+
.replaceAll('.', '-')
|
|
151
|
+
.replace(/^-|-$/g, '')
|
|
152
|
+
.slice(0, 24);
|
|
153
|
+
return `--${category}-${propertySlug || 'value'}${valueSlug ? `-${valueSlug}` : ''}`;
|
|
154
|
+
}
|
|
155
|
+
function objectNumber(source, property) {
|
|
156
|
+
const match = new RegExp(`\\b${property}\\s*:\\s*(-?[\\d.]+)`).exec(source);
|
|
157
|
+
const value = Number(match?.[1]);
|
|
158
|
+
return Number.isFinite(value) ? value : undefined;
|
|
159
|
+
}
|
|
160
|
+
function objectString(source, property) {
|
|
161
|
+
const quoted = new RegExp(`\\b${property}\\s*:\\s*['\"]([^'\"]+)['\"]`).exec(source);
|
|
162
|
+
if (quoted?.[1])
|
|
163
|
+
return quoted[1];
|
|
164
|
+
const array = new RegExp(`\\b${property}\\s*:\\s*\\[([^\\]]+)\\]`).exec(source);
|
|
165
|
+
if (!array?.[1])
|
|
166
|
+
return undefined;
|
|
167
|
+
const values = array[1]
|
|
168
|
+
.split(',')
|
|
169
|
+
.map((value) => Number(value.trim()))
|
|
170
|
+
.filter(Number.isFinite);
|
|
171
|
+
return values.length === 4 ? `cubic-bezier(${values.join(', ')})` : undefined;
|
|
172
|
+
}
|
|
173
|
+
function nativeMotionPresets(file, content) {
|
|
174
|
+
const presets = [];
|
|
175
|
+
const push = (adapter, label, sourceProperty, index, body) => {
|
|
176
|
+
const seconds = adapter !== 'react-spring';
|
|
177
|
+
const duration = objectNumber(body, 'duration');
|
|
178
|
+
const delay = objectNumber(body, 'delay');
|
|
179
|
+
const easing = objectString(body, adapter === 'gsap' ? 'ease' : 'ease') ?? objectString(body, 'easing');
|
|
180
|
+
const configuration = Object.fromEntries([
|
|
181
|
+
'type',
|
|
182
|
+
'mass',
|
|
183
|
+
'stiffness',
|
|
184
|
+
'damping',
|
|
185
|
+
'tension',
|
|
186
|
+
'friction',
|
|
187
|
+
'velocity',
|
|
188
|
+
'repeat',
|
|
189
|
+
'repeatType',
|
|
190
|
+
'yoyo',
|
|
191
|
+
]
|
|
192
|
+
.map((property) => [property, objectNumber(body, property) ?? objectString(body, property)])
|
|
193
|
+
.filter((entry) => entry[1] != null));
|
|
194
|
+
presets.push({
|
|
195
|
+
id: stableId('mot', `${file}:${index}:${adapter}`),
|
|
196
|
+
label,
|
|
197
|
+
duration: duration == null ? undefined : seconds ? duration * 1000 : duration,
|
|
198
|
+
delay: delay == null ? undefined : seconds ? delay * 1000 : delay,
|
|
199
|
+
easing,
|
|
200
|
+
adapter,
|
|
201
|
+
sourceProperty,
|
|
202
|
+
configuration,
|
|
203
|
+
source: { file, line: lineAt(content, index) },
|
|
204
|
+
evidence: [`${adapter} source import`, `${sourceProperty} authoring site`],
|
|
205
|
+
});
|
|
206
|
+
};
|
|
207
|
+
if (/from\s+['\"](?:motion\/react|framer-motion)['\"]/.test(content)) {
|
|
208
|
+
for (const match of content.matchAll(/<motion\.([A-Za-z][\w]*)\b([\s\S]*?)>/g)) {
|
|
209
|
+
const body = match[2] ?? '';
|
|
210
|
+
if (!/\b(?:animate|transition|variants)\s*=/.test(body))
|
|
211
|
+
continue;
|
|
212
|
+
push('motion', `Motion ${match[1]}`, 'transition', match.index ?? 0, body);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (/from\s+['\"]gsap['\"]|require\(['\"]gsap['\"]\)/.test(content)) {
|
|
216
|
+
for (const match of content.matchAll(/gsap\.(to|from|fromTo|set)\s*\(([\s\S]{0,1200}?)\)\s*[;,]/g)) {
|
|
217
|
+
push('gsap', `GSAP ${match[1]} tween`, 'vars', match.index ?? 0, match[2] ?? '');
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (/from\s+['\"](?:@react-spring\/web|react-spring)['\"]/.test(content)) {
|
|
221
|
+
for (const match of content.matchAll(/\b(useSpring|useTransition|useTrail)\s*\(([\s\S]{0,1200}?)\)\s*[;,]/g)) {
|
|
222
|
+
push('react-spring', `React Spring ${match[1]}`, match[1] === 'useSpring' ? 'config' : (match[1] ?? 'config'), match.index ?? 0, match[2] ?? '');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return presets;
|
|
226
|
+
}
|
|
227
|
+
function balancedObject(content, openIndex) {
|
|
228
|
+
if (content[openIndex] !== '{')
|
|
229
|
+
return null;
|
|
230
|
+
let depth = 0;
|
|
231
|
+
let quote = '';
|
|
232
|
+
let escaped = false;
|
|
233
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
234
|
+
const char = content[index];
|
|
235
|
+
if (quote) {
|
|
236
|
+
if (escaped)
|
|
237
|
+
escaped = false;
|
|
238
|
+
else if (char === '\\')
|
|
239
|
+
escaped = true;
|
|
240
|
+
else if (char === quote)
|
|
241
|
+
quote = '';
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
245
|
+
quote = char;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (char === '{')
|
|
249
|
+
depth += 1;
|
|
250
|
+
if (char === '}')
|
|
251
|
+
depth -= 1;
|
|
252
|
+
if (depth === 0)
|
|
253
|
+
return { body: content.slice(openIndex + 1, index), start: openIndex, end: index };
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function objectProperty(content, property, offset = 0) {
|
|
258
|
+
const match = new RegExp(`(?:^|[,\\s])${property}\\s*:\\s*\\{`, 'm').exec(content.slice(offset));
|
|
259
|
+
if (!match)
|
|
260
|
+
return null;
|
|
261
|
+
const openIndex = offset + match.index + match[0].lastIndexOf('{');
|
|
262
|
+
return balancedObject(content, openIndex);
|
|
263
|
+
}
|
|
264
|
+
function topLevelObjectKeys(content) {
|
|
265
|
+
const keys = [];
|
|
266
|
+
let depth = 0;
|
|
267
|
+
let quote = '';
|
|
268
|
+
let escaped = false;
|
|
269
|
+
for (let index = 0; index < content.length; index += 1) {
|
|
270
|
+
const char = content[index];
|
|
271
|
+
if (quote) {
|
|
272
|
+
if (escaped)
|
|
273
|
+
escaped = false;
|
|
274
|
+
else if (char === '\\')
|
|
275
|
+
escaped = true;
|
|
276
|
+
else if (char === quote)
|
|
277
|
+
quote = '';
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
281
|
+
quote = char;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (char === '{' || char === '[' || char === '(')
|
|
285
|
+
depth += 1;
|
|
286
|
+
if (char === '}' || char === ']' || char === ')')
|
|
287
|
+
depth -= 1;
|
|
288
|
+
if (depth !== 0)
|
|
289
|
+
continue;
|
|
290
|
+
const match = /^\s*(?:['"]([^'"]+)['"]|([A-Za-z_$][\w$-]*))\s*:/.exec(content.slice(index));
|
|
291
|
+
const key = match?.[1] ?? match?.[2];
|
|
292
|
+
if (!key || keys.some((item) => item.key === key))
|
|
293
|
+
continue;
|
|
294
|
+
keys.push({ key, index });
|
|
295
|
+
index += (match?.[0].length ?? 1) - 1;
|
|
296
|
+
}
|
|
297
|
+
return keys;
|
|
298
|
+
}
|
|
299
|
+
function sourceVariantAxes(file, content) {
|
|
300
|
+
const axes = [];
|
|
301
|
+
for (const cva of content.matchAll(/\bcva\s*\(/g)) {
|
|
302
|
+
const configOpen = content.indexOf('{', cva.index ?? 0);
|
|
303
|
+
const config = configOpen >= 0 ? balancedObject(content, configOpen) : null;
|
|
304
|
+
if (!config)
|
|
305
|
+
continue;
|
|
306
|
+
const variants = objectProperty(config.body, 'variants');
|
|
307
|
+
if (!variants)
|
|
308
|
+
continue;
|
|
309
|
+
for (const axisKey of topLevelObjectKeys(variants.body)) {
|
|
310
|
+
const axisOpen = variants.body.indexOf('{', axisKey.index);
|
|
311
|
+
const axis = axisOpen >= 0 ? balancedObject(variants.body, axisOpen) : null;
|
|
312
|
+
if (!axis)
|
|
313
|
+
continue;
|
|
314
|
+
const values = topLevelObjectKeys(axis.body).map((item) => item.key);
|
|
315
|
+
if (!values.length)
|
|
316
|
+
continue;
|
|
317
|
+
const absolute = config.start + 1 + variants.start + 1 + axisOpen;
|
|
318
|
+
axes.push({
|
|
319
|
+
id: stableId('axis', `${file}:cva:${axisKey.key}`),
|
|
320
|
+
label: axisKey.key,
|
|
321
|
+
property: axisKey.key,
|
|
322
|
+
values,
|
|
323
|
+
adapter: 'cva',
|
|
324
|
+
source: { file, line: lineAt(content, absolute) },
|
|
325
|
+
sourceProperty: `variants.${axisKey.key}`,
|
|
326
|
+
canCreate: true,
|
|
327
|
+
evidence: ['CVA variants object', `Writable ${axisKey.key} option map`],
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const match of content.matchAll(/\b([A-Za-z_$][\w$]*)\??\s*:\s*((?:['"][^'"]+['"]\s*\|\s*)+['"][^'"]+['"])/g)) {
|
|
332
|
+
const property = match[1];
|
|
333
|
+
const values = [...match[2].matchAll(/['"]([^'"]+)['"]/g)].map((item) => item[1]);
|
|
334
|
+
if (values.length < 2 || axes.some((axis) => axis.property === property))
|
|
335
|
+
continue;
|
|
336
|
+
axes.push({
|
|
337
|
+
id: stableId('axis', `${file}:typescript:${property}`),
|
|
338
|
+
label: property,
|
|
339
|
+
property,
|
|
340
|
+
values,
|
|
341
|
+
adapter: 'typescript',
|
|
342
|
+
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
343
|
+
sourceProperty: property,
|
|
344
|
+
canCreate: true,
|
|
345
|
+
evidence: ['TypeScript string union', `Writable ${property} prop type`],
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return axes;
|
|
349
|
+
}
|
|
42
350
|
async function sourceFiles(root, exclusions) {
|
|
43
351
|
const files = [];
|
|
44
352
|
const excluded = new Set([
|
|
@@ -74,48 +382,137 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
74
382
|
const tokens = new Map();
|
|
75
383
|
const components = new Map();
|
|
76
384
|
const breakpoints = new Map();
|
|
77
|
-
const
|
|
385
|
+
const containerQueries = new Map();
|
|
386
|
+
const themes = new Map();
|
|
78
387
|
const motion = new Map();
|
|
79
388
|
const storyVariants = new Map();
|
|
389
|
+
const axesByFile = new Map();
|
|
390
|
+
const documents = [];
|
|
80
391
|
for (const path of files) {
|
|
81
392
|
const content = await readFile(path, 'utf8').catch(() => '');
|
|
82
393
|
const file = relative(root, path).replaceAll('\\', '/');
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
394
|
+
const stylesheets = stylesheetSegments(file, content);
|
|
395
|
+
documents.push({ file, content, stylesheets });
|
|
396
|
+
for (const preset of nativeMotionPresets(file, content))
|
|
397
|
+
motion.set(preset.id, preset);
|
|
398
|
+
for (const stylesheet of stylesheets) {
|
|
399
|
+
for (const match of stylesheet.content.matchAll(/(--[\w-]+)\s*:\s*([^;}\n]+)/g)) {
|
|
400
|
+
const name = match[1];
|
|
401
|
+
const value = match[2].trim();
|
|
402
|
+
if (!value || value.includes('${') || value.includes('\\s*'))
|
|
403
|
+
continue;
|
|
404
|
+
const index = stylesheet.offset + (match.index ?? 0);
|
|
405
|
+
tokens.set(name, {
|
|
406
|
+
id: stableId('tok', name),
|
|
407
|
+
name,
|
|
408
|
+
value,
|
|
409
|
+
category: tokenCategory(name, value),
|
|
410
|
+
cssVariable: name,
|
|
411
|
+
source: { file, line: lineAt(content, index) },
|
|
412
|
+
confidence: 'instrumented',
|
|
413
|
+
evidence: ['CSS custom property'],
|
|
414
|
+
});
|
|
415
|
+
if (/duration|motion/.test(name)) {
|
|
416
|
+
const duration = /([\d.]+)m?s/.exec(value);
|
|
417
|
+
motion.set(name, {
|
|
418
|
+
id: stableId('mot', name),
|
|
419
|
+
label: name.replace(/^--/, ''),
|
|
420
|
+
duration: duration
|
|
421
|
+
? Number(duration[1]) * (value.includes('ms') ? 1 : 1000)
|
|
422
|
+
: undefined,
|
|
423
|
+
adapter: 'css',
|
|
424
|
+
configuration: {},
|
|
425
|
+
source: { file, line: lineAt(content, index) },
|
|
426
|
+
evidence: ['CSS custom property'],
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
for (const match of stylesheet.content.matchAll(/@media\s*\([^)]*(?:min|max)-width\s*:\s*(\d+)px[^)]*\)/g)) {
|
|
431
|
+
const index = stylesheet.offset + (match.index ?? 0);
|
|
432
|
+
const width = Number(match[1]);
|
|
433
|
+
breakpoints.set(width, {
|
|
434
|
+
width,
|
|
435
|
+
source: { file, line: lineAt(content, index) },
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
for (const match of stylesheet.content.matchAll(/@container(?:\s+([\w-]+))?\s*\(([^)]*)\)/g)) {
|
|
439
|
+
const name = match[1];
|
|
440
|
+
const condition = match[2].trim();
|
|
441
|
+
const min = /min-(?:width|inline-size)\s*:\s*([\d.]+)px/i.exec(condition)?.[1] ??
|
|
442
|
+
/(?:width|inline-size)\s*>=?\s*([\d.]+)px/i.exec(condition)?.[1];
|
|
443
|
+
const max = /max-(?:width|inline-size)\s*:\s*([\d.]+)px/i.exec(condition)?.[1] ??
|
|
444
|
+
/(?:width|inline-size)\s*<=?\s*([\d.]+)px/i.exec(condition)?.[1];
|
|
445
|
+
if (!min && !max)
|
|
446
|
+
continue;
|
|
447
|
+
const axis = /block-size|height/i.test(condition)
|
|
448
|
+
? 'block-size'
|
|
449
|
+
: /\bsize\b/i.test(condition) && !/inline-size/i.test(condition)
|
|
450
|
+
? 'size'
|
|
451
|
+
: 'inline-size';
|
|
452
|
+
const id = stableId('container', `${file}:${name ?? 'anonymous'}:${condition}`);
|
|
453
|
+
const index = stylesheet.offset + (match.index ?? 0);
|
|
454
|
+
containerQueries.set(id, {
|
|
455
|
+
id,
|
|
456
|
+
label: `${name ?? 'Anonymous container'} · ${condition}`,
|
|
457
|
+
...(name ? { name } : {}),
|
|
458
|
+
condition,
|
|
459
|
+
axis,
|
|
460
|
+
...(min ? { minWidth: Number(min) } : {}),
|
|
461
|
+
...(max ? { maxWidth: Number(max) } : {}),
|
|
462
|
+
source: { file, line: lineAt(content, index) },
|
|
463
|
+
evidence: [
|
|
464
|
+
'CSS @container rule',
|
|
465
|
+
...(stylesheet.content.includes('container-type')
|
|
466
|
+
? ['CSS container-type declaration']
|
|
467
|
+
: []),
|
|
468
|
+
...(name && stylesheet.content.includes('container-name')
|
|
469
|
+
? ['CSS container-name declaration']
|
|
470
|
+
: []),
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
for (const match of content.matchAll(/\[data-theme\s*=\s*["']?([\w-]+)["']?\]/g)) {
|
|
476
|
+
const id = match[1];
|
|
477
|
+
themes.set(id, {
|
|
478
|
+
id,
|
|
479
|
+
label: id[0].toUpperCase() + id.slice(1),
|
|
480
|
+
selector: `[data-theme="${id}"]`,
|
|
481
|
+
attribute: 'data-theme',
|
|
482
|
+
value: id,
|
|
92
483
|
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
93
484
|
confidence: 'instrumented',
|
|
94
|
-
evidence: ['CSS
|
|
485
|
+
evidence: ['Indexed CSS data-theme selector'],
|
|
95
486
|
});
|
|
96
|
-
if (/duration|motion/.test(name)) {
|
|
97
|
-
const duration = /([\d.]+)m?s/.exec(value);
|
|
98
|
-
motion.set(name, {
|
|
99
|
-
id: stableId('mot', name),
|
|
100
|
-
label: name.replace(/^--/, ''),
|
|
101
|
-
duration: duration ? Number(duration[1]) * (value.includes('ms') ? 1 : 1000) : undefined,
|
|
102
|
-
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
487
|
}
|
|
106
|
-
for (const match of content.matchAll(
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
488
|
+
for (const match of content.matchAll(/\bdata-theme\s*=\s*["']([\w-]+)["']/g)) {
|
|
489
|
+
const id = match[1];
|
|
490
|
+
if (themes.has(id))
|
|
491
|
+
continue;
|
|
492
|
+
themes.set(id, {
|
|
493
|
+
id,
|
|
494
|
+
label: id[0].toUpperCase() + id.slice(1),
|
|
495
|
+
attribute: 'data-theme',
|
|
496
|
+
value: id,
|
|
110
497
|
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
498
|
+
confidence: 'instrumented',
|
|
499
|
+
evidence: ['Indexed HTML data-theme attribute'],
|
|
111
500
|
});
|
|
112
501
|
}
|
|
113
|
-
for (const match of content.matchAll(/(
|
|
114
|
-
themes.
|
|
502
|
+
for (const match of content.matchAll(/(?:^|\s)(\.dark)(?=\s|[{,:])/gm)) {
|
|
503
|
+
if (themes.has('dark'))
|
|
504
|
+
continue;
|
|
505
|
+
themes.set('dark', {
|
|
506
|
+
id: 'dark',
|
|
507
|
+
label: 'Dark',
|
|
508
|
+
selector: '.dark',
|
|
509
|
+
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
510
|
+
confidence: 'instrumented',
|
|
511
|
+
evidence: ['Indexed CSS class theme selector'],
|
|
512
|
+
});
|
|
115
513
|
}
|
|
116
|
-
if (/(?:^|\s)\.dark(?:\s|[{,:])/.test(content))
|
|
117
|
-
themes.add('dark');
|
|
118
514
|
if (/\.(?:tsx?|jsx?|mjs|cjs)$/.test(path)) {
|
|
515
|
+
axesByFile.set(file, sourceVariantAxes(file, content));
|
|
119
516
|
if (/\.stories\.[cm]?[jt]sx?$/.test(path)) {
|
|
120
517
|
const componentName = basename(path).replace(/\.stories\.[cm]?[jt]sx?$/, '');
|
|
121
518
|
const variants = [...content.matchAll(/export\s+const\s+([A-Z][A-Za-z0-9_]*)/g)].map((story) => ({
|
|
@@ -123,7 +520,10 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
123
520
|
label: story[1],
|
|
124
521
|
property: 'story',
|
|
125
522
|
value: story[1],
|
|
523
|
+
props: { story: story[1] },
|
|
126
524
|
source: { file, line: lineAt(content, story.index ?? 0) },
|
|
525
|
+
adapter: 'storybook',
|
|
526
|
+
sourceProperty: story[1],
|
|
127
527
|
}));
|
|
128
528
|
storyVariants.set(componentName, variants);
|
|
129
529
|
continue;
|
|
@@ -137,6 +537,7 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
137
537
|
source: { file, line: lineAt(content, match.index ?? 0) },
|
|
138
538
|
instances: 0,
|
|
139
539
|
variants: [],
|
|
540
|
+
variantAxes: [],
|
|
140
541
|
evidence: ['exported component'],
|
|
141
542
|
};
|
|
142
543
|
components.set(id, component);
|
|
@@ -147,6 +548,17 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
147
548
|
const component = [...components.values()].find((item) => item.name === componentName);
|
|
148
549
|
if (component) {
|
|
149
550
|
component.variants = variants;
|
|
551
|
+
component.variantAxes.push({
|
|
552
|
+
id: stableId('axis', `${variants[0]?.source?.file}:storybook:story`),
|
|
553
|
+
label: 'Story',
|
|
554
|
+
property: 'story',
|
|
555
|
+
values: variants.map((variant) => variant.value),
|
|
556
|
+
adapter: 'storybook',
|
|
557
|
+
source: variants[0].source,
|
|
558
|
+
sourceProperty: 'named export',
|
|
559
|
+
canCreate: true,
|
|
560
|
+
evidence: ['Storybook named exports', 'Writable story definition'],
|
|
561
|
+
});
|
|
150
562
|
component.evidence.push('Storybook story');
|
|
151
563
|
}
|
|
152
564
|
else if (variants[0]?.source) {
|
|
@@ -157,10 +569,250 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
157
569
|
source: variants[0].source,
|
|
158
570
|
instances: 0,
|
|
159
571
|
variants,
|
|
572
|
+
variantAxes: [
|
|
573
|
+
{
|
|
574
|
+
id: stableId('axis', `${variants[0].source.file}:storybook:story`),
|
|
575
|
+
label: 'Story',
|
|
576
|
+
property: 'story',
|
|
577
|
+
values: variants.map((variant) => variant.value),
|
|
578
|
+
adapter: 'storybook',
|
|
579
|
+
source: variants[0].source,
|
|
580
|
+
sourceProperty: 'named export',
|
|
581
|
+
canCreate: true,
|
|
582
|
+
evidence: ['Storybook named exports', 'Writable story definition'],
|
|
583
|
+
},
|
|
584
|
+
],
|
|
160
585
|
evidence: ['Storybook story'],
|
|
161
586
|
});
|
|
162
587
|
}
|
|
163
588
|
}
|
|
589
|
+
for (const component of components.values()) {
|
|
590
|
+
const nativeAxes = axesByFile.get(component.source?.file ?? '') ?? [];
|
|
591
|
+
const known = new Set(component.variantAxes.map((axis) => axis.property));
|
|
592
|
+
component.variantAxes.push(...nativeAxes.filter((axis) => !known.has(axis.property)));
|
|
593
|
+
for (const axis of nativeAxes) {
|
|
594
|
+
for (const value of axis.values) {
|
|
595
|
+
const id = stableId('var', `${component.id}:${axis.property}:${String(value)}`);
|
|
596
|
+
if (component.variants.some((variant) => variant.id === id))
|
|
597
|
+
continue;
|
|
598
|
+
component.variants.push({
|
|
599
|
+
id,
|
|
600
|
+
label: String(value),
|
|
601
|
+
property: axis.property,
|
|
602
|
+
value,
|
|
603
|
+
props: { [axis.property]: value },
|
|
604
|
+
source: axis.source,
|
|
605
|
+
adapter: axis.adapter,
|
|
606
|
+
sourceProperty: `${axis.sourceProperty}.${String(value)}`,
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (nativeAxes.length)
|
|
611
|
+
component.evidence.push('Source-backed variant axis');
|
|
612
|
+
}
|
|
613
|
+
resolveTokenAliases(tokens);
|
|
614
|
+
const tokenList = [...tokens.values()];
|
|
615
|
+
const tokenUsages = [];
|
|
616
|
+
const findings = [];
|
|
617
|
+
const recurringLiterals = new Map();
|
|
618
|
+
const componentForSource = (file, line) => [...components.values()]
|
|
619
|
+
.filter((component) => component.source?.file === file && (component.source.line ?? 0) <= line)
|
|
620
|
+
.sort((a, b) => (b.source?.line ?? 0) - (a.source?.line ?? 0))[0];
|
|
621
|
+
for (const { file, content, stylesheets } of documents) {
|
|
622
|
+
for (const stylesheet of stylesheets) {
|
|
623
|
+
for (const match of stylesheet.content.matchAll(/var\(\s*(--[\w-]+)(?:\s*,[^)]*)?\)/g)) {
|
|
624
|
+
const token = tokens.get(match[1]);
|
|
625
|
+
if (!token)
|
|
626
|
+
continue;
|
|
627
|
+
const index = stylesheet.offset + (match.index ?? 0);
|
|
628
|
+
const line = lineAt(content, index);
|
|
629
|
+
const lineStart = content.lastIndexOf('\n', index) + 1;
|
|
630
|
+
const declarationPrefix = content.slice(lineStart, index);
|
|
631
|
+
const alias = /--[\w-]+\s*:\s*$/.test(declarationPrefix);
|
|
632
|
+
const component = componentForSource(file, line);
|
|
633
|
+
tokenUsages.push({
|
|
634
|
+
id: stableId('use', `${file}:${index}:${token.id}`),
|
|
635
|
+
tokenId: token.id,
|
|
636
|
+
tokenName: token.name,
|
|
637
|
+
value: token.value,
|
|
638
|
+
category: token.category,
|
|
639
|
+
kind: alias ? 'alias' : 'reference',
|
|
640
|
+
componentId: component?.id,
|
|
641
|
+
source: { file, line },
|
|
642
|
+
evidence: [alias ? 'Token aliases this project value' : 'CSS variable reference'],
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
for (const match of stylesheet.content.matchAll(/([a-zA-Z][\w-]*)\s*:\s*([^;\n},]+)[;},]/g)) {
|
|
646
|
+
const property = match[1];
|
|
647
|
+
const value = match[2].trim();
|
|
648
|
+
if (property.startsWith('--') || value.includes('var('))
|
|
649
|
+
continue;
|
|
650
|
+
const category = declarationCategory(property, value);
|
|
651
|
+
if (!category)
|
|
652
|
+
continue;
|
|
653
|
+
const categoryTokens = tokenList.filter((token) => token.category === category);
|
|
654
|
+
const exact = categoryTokens
|
|
655
|
+
.filter((token) => comparable(tokenComparableValue(token)) === comparable(value))
|
|
656
|
+
.sort((a, b) => (b.aliasChain?.length ?? 0) - (a.aliasChain?.length ?? 0) ||
|
|
657
|
+
a.name.localeCompare(b.name))[0];
|
|
658
|
+
const suggested = exact ??
|
|
659
|
+
closeNumericToken(value, categoryTokens.map((token) => ({ ...token, value: tokenComparableValue(token) })));
|
|
660
|
+
const index = stylesheet.offset + (match.index ?? 0);
|
|
661
|
+
const line = lineAt(content, index);
|
|
662
|
+
const component = componentForSource(file, line);
|
|
663
|
+
const recurringKey = `${category}:${comparable(value)}`;
|
|
664
|
+
const recurring = recurringLiterals.get(recurringKey) ?? {
|
|
665
|
+
value,
|
|
666
|
+
category,
|
|
667
|
+
properties: [],
|
|
668
|
+
sources: [],
|
|
669
|
+
componentIds: [],
|
|
670
|
+
};
|
|
671
|
+
recurring.properties.push(property);
|
|
672
|
+
recurring.sources.push({ file, line });
|
|
673
|
+
if (component?.id)
|
|
674
|
+
recurring.componentIds.push(component.id);
|
|
675
|
+
recurringLiterals.set(recurringKey, recurring);
|
|
676
|
+
if (!suggested)
|
|
677
|
+
continue;
|
|
678
|
+
const usage = {
|
|
679
|
+
id: stableId('use', `${file}:${index}:${suggested.id}:literal`),
|
|
680
|
+
tokenId: suggested.id,
|
|
681
|
+
tokenName: suggested.name,
|
|
682
|
+
value,
|
|
683
|
+
category,
|
|
684
|
+
kind: 'literal',
|
|
685
|
+
property,
|
|
686
|
+
componentId: component?.id,
|
|
687
|
+
source: { file, line },
|
|
688
|
+
evidence: [
|
|
689
|
+
exact
|
|
690
|
+
? 'Literal equals an existing project token'
|
|
691
|
+
: 'Literal is close to a project token',
|
|
692
|
+
],
|
|
693
|
+
};
|
|
694
|
+
tokenUsages.push(usage);
|
|
695
|
+
findings.push({
|
|
696
|
+
id: stableId('finding', usage.id),
|
|
697
|
+
kind: component ? 'component-drift' : 'literal-drift',
|
|
698
|
+
severity: exact ? 'info' : 'warning',
|
|
699
|
+
title: exact ? `Use ${suggested.name}` : `${value} is close to ${suggested.name}`,
|
|
700
|
+
detail: exact
|
|
701
|
+
? `${property} repeats ${suggested.value} as a literal instead of its project token.`
|
|
702
|
+
: `${property} uses ${value}; the nearest ${category} token is ${suggested.name} at ${suggested.value}.`,
|
|
703
|
+
category,
|
|
704
|
+
tokenIds: [suggested.id],
|
|
705
|
+
usageIds: [usage.id],
|
|
706
|
+
componentIds: component ? [component.id] : [],
|
|
707
|
+
suggestedTokenId: suggested.id,
|
|
708
|
+
source: usage.source,
|
|
709
|
+
evidence: usage.evidence,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const tokenPromotions = [...recurringLiterals.entries()]
|
|
715
|
+
.filter(([, recurring]) => recurring.sources.length >= 2)
|
|
716
|
+
.map(([key, recurring]) => {
|
|
717
|
+
const categoryTokens = tokenList.filter((token) => token.category === recurring.category);
|
|
718
|
+
const exact = categoryTokens
|
|
719
|
+
.filter((token) => comparable(tokenComparableValue(token)) === comparable(recurring.value))
|
|
720
|
+
.sort((a, b) => (b.aliasChain?.length ?? 0) - (a.aliasChain?.length ?? 0) ||
|
|
721
|
+
a.name.localeCompare(b.name))[0];
|
|
722
|
+
const near = exact
|
|
723
|
+
? undefined
|
|
724
|
+
: closeNumericToken(recurring.value, categoryTokens.map((token) => ({ ...token, value: tokenComparableValue(token) })));
|
|
725
|
+
const suggested = exact ?? near;
|
|
726
|
+
const property = [...recurring.properties].sort((a, b) => recurring.properties.filter((item) => item === b).length -
|
|
727
|
+
recurring.properties.filter((item) => item === a).length || a.localeCompare(b))[0];
|
|
728
|
+
const sources = recurring.sources.filter((source, index, all) => all.findIndex((candidate) => candidate.file === source.file && candidate.line === source.line) === index);
|
|
729
|
+
const relation = exact ? 'exact' : near ? 'near' : 'new';
|
|
730
|
+
const proposedTokenName = promotionTokenName(recurring.category, property, recurring.value);
|
|
731
|
+
const suggestedTokenName = suggested?.name ??
|
|
732
|
+
(tokens.has(proposedTokenName)
|
|
733
|
+
? `${proposedTokenName}-${stableId('token-name', key).slice(-4)}`
|
|
734
|
+
: proposedTokenName);
|
|
735
|
+
return {
|
|
736
|
+
id: stableId('promotion', key),
|
|
737
|
+
value: recurring.value,
|
|
738
|
+
category: recurring.category,
|
|
739
|
+
property,
|
|
740
|
+
occurrenceCount: sources.length,
|
|
741
|
+
sources,
|
|
742
|
+
componentIds: [...new Set(recurring.componentIds)],
|
|
743
|
+
recommendation: suggested ? 'use-existing' : 'create-token',
|
|
744
|
+
relation,
|
|
745
|
+
suggestedTokenId: suggested?.id,
|
|
746
|
+
suggestedTokenName,
|
|
747
|
+
suggestedValue: suggested ? `var(${suggested.name})` : recurring.value,
|
|
748
|
+
aliasChain: suggested?.aliasChain ?? [],
|
|
749
|
+
canStage: sources.length > 0,
|
|
750
|
+
blockers: [],
|
|
751
|
+
evidence: [
|
|
752
|
+
`${sources.length} authored literal occurrences`,
|
|
753
|
+
...(suggested
|
|
754
|
+
? [
|
|
755
|
+
relation === 'exact'
|
|
756
|
+
? 'Resolved value matches an existing project token'
|
|
757
|
+
: 'Nearest compatible project token requires explicit review',
|
|
758
|
+
]
|
|
759
|
+
: ['No compatible project token exists; create one before replacing usages']),
|
|
760
|
+
...(suggested?.aliasStatus === 'resolved'
|
|
761
|
+
? [`Preserves alias chain ${(suggested.aliasChain ?? []).join(' → ')}`]
|
|
762
|
+
: []),
|
|
763
|
+
],
|
|
764
|
+
};
|
|
765
|
+
})
|
|
766
|
+
.sort((a, b) => b.occurrenceCount - a.occurrenceCount || a.value.localeCompare(b.value));
|
|
767
|
+
for (let index = 0; index < tokenList.length; index += 1) {
|
|
768
|
+
const token = tokenList[index];
|
|
769
|
+
const near = tokenList.slice(index + 1).find((candidate) => {
|
|
770
|
+
if (candidate.category !== token.category)
|
|
771
|
+
return false;
|
|
772
|
+
const first = numericValue(token.value);
|
|
773
|
+
const second = numericValue(candidate.value);
|
|
774
|
+
return Boolean(first &&
|
|
775
|
+
second &&
|
|
776
|
+
first.unit === second.unit &&
|
|
777
|
+
Math.abs(first.amount - second.amount) > 0 &&
|
|
778
|
+
Math.abs(first.amount - second.amount) <= Math.max(2, Math.abs(first.amount) * 0.2));
|
|
779
|
+
});
|
|
780
|
+
if (near) {
|
|
781
|
+
const relatedUsages = tokenUsages.filter((usage) => usage.tokenId === token.id || usage.tokenId === near.id);
|
|
782
|
+
findings.push({
|
|
783
|
+
id: stableId('finding', `near:${token.id}:${near.id}`),
|
|
784
|
+
kind: 'near-duplicate',
|
|
785
|
+
severity: 'warning',
|
|
786
|
+
title: `${token.name} and ${near.name} are unusually close`,
|
|
787
|
+
detail: `${token.value} and ${near.value} may represent the same ${token.category} decision.`,
|
|
788
|
+
category: token.category,
|
|
789
|
+
tokenIds: [token.id, near.id],
|
|
790
|
+
usageIds: relatedUsages.map((usage) => usage.id),
|
|
791
|
+
componentIds: [
|
|
792
|
+
...new Set(relatedUsages
|
|
793
|
+
.map((usage) => usage.componentId)
|
|
794
|
+
.filter((componentId) => Boolean(componentId))),
|
|
795
|
+
],
|
|
796
|
+
source: near.source ?? token.source,
|
|
797
|
+
evidence: ['Same category', 'Values fall within the project drift threshold'],
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
if (!tokenUsages.some((usage) => usage.tokenId === token.id)) {
|
|
801
|
+
findings.push({
|
|
802
|
+
id: stableId('finding', `unused:${token.id}`),
|
|
803
|
+
kind: 'unused-token',
|
|
804
|
+
severity: 'info',
|
|
805
|
+
title: `${token.name} has no indexed references`,
|
|
806
|
+
detail: 'Confirm whether this token is intentionally reserved before removing it.',
|
|
807
|
+
category: token.category,
|
|
808
|
+
tokenIds: [token.id],
|
|
809
|
+
usageIds: [],
|
|
810
|
+
componentIds: [],
|
|
811
|
+
source: token.source,
|
|
812
|
+
evidence: ['No references were found in indexed project files'],
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
}
|
|
164
816
|
const configuredViewports = config?.design?.viewports ?? [];
|
|
165
817
|
const graphBreakpoints = configuredViewports.length
|
|
166
818
|
? configuredViewports.map((item) => ({ ...item, height: item.height ?? 900 }))
|
|
@@ -182,8 +834,12 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
182
834
|
];
|
|
183
835
|
const configuredThemes = config?.design?.themes ?? [];
|
|
184
836
|
const graphThemes = configuredThemes.length
|
|
185
|
-
? configuredThemes
|
|
186
|
-
|
|
837
|
+
? configuredThemes.map((theme) => ({
|
|
838
|
+
...theme,
|
|
839
|
+
confidence: theme.confidence ?? 'instrumented',
|
|
840
|
+
evidence: theme.evidence ?? ['Configured Foundry theme hook'],
|
|
841
|
+
}))
|
|
842
|
+
: [...themes.values()];
|
|
187
843
|
return projectDesignGraphSchema.parse({
|
|
188
844
|
protocolVersion: PROTOCOL_VERSION,
|
|
189
845
|
projectRoot: root,
|
|
@@ -191,9 +847,13 @@ export async function indexProjectDesign(root, config, revision) {
|
|
|
191
847
|
tokens: [...tokens.values()],
|
|
192
848
|
components: [...components.values()],
|
|
193
849
|
breakpoints: graphBreakpoints,
|
|
850
|
+
containerQueries: [...containerQueries.values()].sort((a, b) => (a.minWidth ?? a.maxWidth ?? 0) - (b.minWidth ?? b.maxWidth ?? 0)),
|
|
194
851
|
themes: graphThemes,
|
|
195
852
|
states: config?.design?.states ?? [],
|
|
196
853
|
motionPresets: [...motion.values()],
|
|
854
|
+
tokenUsages,
|
|
855
|
+
designSystemFindings: findings,
|
|
856
|
+
tokenPromotions,
|
|
197
857
|
indexedAt: new Date().toISOString(),
|
|
198
858
|
});
|
|
199
859
|
}
|