skillui 1.1.2 → 1.1.3

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 (61) hide show
  1. package/dist/cli.js +105073 -194
  2. package/package.json +15 -6
  3. package/dist/cli.d.ts +0 -3
  4. package/dist/extractors/components.d.ts +0 -11
  5. package/dist/extractors/components.js +0 -455
  6. package/dist/extractors/framework.d.ts +0 -4
  7. package/dist/extractors/framework.js +0 -126
  8. package/dist/extractors/tokens/computed.d.ts +0 -7
  9. package/dist/extractors/tokens/computed.js +0 -249
  10. package/dist/extractors/tokens/css.d.ts +0 -3
  11. package/dist/extractors/tokens/css.js +0 -510
  12. package/dist/extractors/tokens/http-css.d.ts +0 -14
  13. package/dist/extractors/tokens/http-css.js +0 -1689
  14. package/dist/extractors/tokens/tailwind.d.ts +0 -3
  15. package/dist/extractors/tokens/tailwind.js +0 -353
  16. package/dist/extractors/tokens/tokens-file.d.ts +0 -3
  17. package/dist/extractors/tokens/tokens-file.js +0 -229
  18. package/dist/extractors/ultra/animations.d.ts +0 -21
  19. package/dist/extractors/ultra/animations.js +0 -527
  20. package/dist/extractors/ultra/components-dom.d.ts +0 -13
  21. package/dist/extractors/ultra/components-dom.js +0 -149
  22. package/dist/extractors/ultra/interactions.d.ts +0 -14
  23. package/dist/extractors/ultra/interactions.js +0 -222
  24. package/dist/extractors/ultra/layout.d.ts +0 -14
  25. package/dist/extractors/ultra/layout.js +0 -123
  26. package/dist/extractors/ultra/pages.d.ts +0 -16
  27. package/dist/extractors/ultra/pages.js +0 -228
  28. package/dist/font-resolver.d.ts +0 -10
  29. package/dist/font-resolver.js +0 -280
  30. package/dist/modes/dir.d.ts +0 -6
  31. package/dist/modes/dir.js +0 -213
  32. package/dist/modes/repo.d.ts +0 -6
  33. package/dist/modes/repo.js +0 -76
  34. package/dist/modes/ultra.d.ts +0 -22
  35. package/dist/modes/ultra.js +0 -281
  36. package/dist/modes/url.d.ts +0 -14
  37. package/dist/modes/url.js +0 -161
  38. package/dist/normalizer.d.ts +0 -11
  39. package/dist/normalizer.js +0 -867
  40. package/dist/playwright-loader.d.ts +0 -10
  41. package/dist/playwright-loader.js +0 -71
  42. package/dist/screenshot.d.ts +0 -9
  43. package/dist/screenshot.js +0 -94
  44. package/dist/types-ultra.d.ts +0 -157
  45. package/dist/types-ultra.js +0 -4
  46. package/dist/types.d.ts +0 -182
  47. package/dist/types.js +0 -4
  48. package/dist/writers/animations-md.d.ts +0 -17
  49. package/dist/writers/animations-md.js +0 -313
  50. package/dist/writers/components-md.d.ts +0 -8
  51. package/dist/writers/components-md.js +0 -151
  52. package/dist/writers/design-md.d.ts +0 -7
  53. package/dist/writers/design-md.js +0 -704
  54. package/dist/writers/interactions-md.d.ts +0 -8
  55. package/dist/writers/interactions-md.js +0 -146
  56. package/dist/writers/layout-md.d.ts +0 -8
  57. package/dist/writers/layout-md.js +0 -120
  58. package/dist/writers/skill.d.ts +0 -12
  59. package/dist/writers/skill.js +0 -1006
  60. package/dist/writers/tokens-json.d.ts +0 -11
  61. package/dist/writers/tokens-json.js +0 -164
@@ -1,867 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.normalize = normalize;
4
- /**
5
- * Normalize raw extracted tokens into a clean DesignProfile.
6
- * Pure deterministic logic — no AI, no inference beyond rule-based heuristics.
7
- */
8
- function normalize(projectName, frameworks, rawTokens, components, libraries) {
9
- // Light scheme: explicit color-scheme declaration OR heuristic (most frequent high-lightness colors)
10
- const hasExplicitLight = rawTokens.cssVariables.some(v => v.name === '--color-scheme-default' && v.value === 'light');
11
- const isLightScheme = hasExplicitLight || detectLightSchemeHeuristic(rawTokens.colors);
12
- const colors = normalizeColors(rawTokens.colors, isLightScheme);
13
- const typography = normalizeTypography(rawTokens.fonts, rawTokens.fontVarMap);
14
- const spacing = normalizeSpacing(rawTokens.spacingValues);
15
- const shadows = normalizeShadows(rawTokens.shadows);
16
- const borderRadius = normalizeBorderRadius(rawTokens.borderRadii, components);
17
- const fontVarMap = rawTokens.fontVarMap || {};
18
- const animations = normalizeAnimations(rawTokens.animations);
19
- const darkModeVars = rawTokens.darkModeVars || [];
20
- // Detect anti-patterns from the codebase
21
- const antiPatterns = detectAntiPatterns(rawTokens, components, shadows);
22
- // Build component categories
23
- const componentCategories = buildComponentCategories(components);
24
- // z-index scale
25
- const zIndexScale = [...new Set(rawTokens.zIndexValues || [])].sort((a, b) => a - b);
26
- // Compute design traits
27
- const designTraits = computeDesignTraits(colors, typography, spacing, shadows, rawTokens, animations);
28
- // Build motion tokens
29
- const motionTokens = normalizeMotionTokens(rawTokens, animations);
30
- return {
31
- projectName,
32
- favicon: rawTokens.favicon,
33
- frameworks,
34
- colors,
35
- typography,
36
- spacing,
37
- shadows,
38
- components,
39
- breakpoints: deduplicateBreakpoints(rawTokens.breakpoints),
40
- cssVariables: rawTokens.cssVariables,
41
- borderRadius,
42
- fontVarMap,
43
- antiPatterns,
44
- designTraits,
45
- animations,
46
- darkModeVars,
47
- iconLibrary: libraries?.iconLibrary || null,
48
- stateLibrary: libraries?.stateLibrary || null,
49
- componentCategories,
50
- zIndexScale,
51
- containerMaxWidth: rawTokens.containerMaxWidth || null,
52
- fontSources: rawTokens.fontSources || [],
53
- pageSections: deduplicatePageSections(rawTokens.pageSections || []),
54
- motionTokens,
55
- };
56
- }
57
- /**
58
- * Heuristic: if the top-frequency colors skew light (lightness > 0.6),
59
- * treat as a light-scheme site even without an explicit color-scheme declaration.
60
- */
61
- function detectLightSchemeHeuristic(rawColors) {
62
- if (rawColors.length === 0)
63
- return false;
64
- // Look at the 10 most frequent colors
65
- const top = [...rawColors].sort((a, b) => b.frequency - a.frequency).slice(0, 10);
66
- let lightCount = 0;
67
- let darkCount = 0;
68
- for (const c of top) {
69
- const rgb = hexToRgb(c.value);
70
- if (!rgb)
71
- continue;
72
- const lightness = (Math.max(rgb.r, rgb.g, rgb.b) + Math.min(rgb.r, rgb.g, rgb.b)) / 2 / 255;
73
- if (lightness > 0.6)
74
- lightCount++;
75
- else if (lightness < 0.35)
76
- darkCount++;
77
- }
78
- return lightCount > darkCount;
79
- }
80
- // ── Colors ────────────────────────────────────────────────────────────
81
- function normalizeColors(rawColors, isLightScheme = false) {
82
- if (rawColors.length === 0)
83
- return [];
84
- const deduplicated = deduplicateColors(rawColors);
85
- deduplicated.sort((a, b) => b.frequency - a.frequency);
86
- return assignColorRoles(deduplicated, isLightScheme);
87
- }
88
- function deduplicateColors(colors) {
89
- const groups = [];
90
- for (const color of colors) {
91
- const hex = color.value;
92
- if (!isValidHex(hex))
93
- continue;
94
- const rgb = hexToRgb(hex);
95
- if (!rgb)
96
- continue;
97
- let merged = false;
98
- for (const group of groups) {
99
- const groupRgb = hexToRgb(group.representative);
100
- if (groupRgb && colorDistance(rgb, groupRgb) < 15) {
101
- group.frequency += color.frequency;
102
- if (color.name && !group.name)
103
- group.name = color.name;
104
- merged = true;
105
- break;
106
- }
107
- }
108
- if (!merged) {
109
- groups.push({
110
- representative: hex,
111
- name: color.name,
112
- frequency: color.frequency,
113
- source: color.source,
114
- });
115
- }
116
- }
117
- return groups.map(g => ({
118
- hex: g.representative,
119
- name: g.name,
120
- role: 'unknown',
121
- frequency: g.frequency,
122
- source: g.source,
123
- }));
124
- }
125
- function assignColorRoles(colors, isLightScheme = false) {
126
- if (colors.length === 0)
127
- return colors;
128
- const assigned = new Set();
129
- const assign = (index, role) => {
130
- if (index >= 0 && !assigned.has(index)) {
131
- colors[index].role = role;
132
- assigned.add(index);
133
- }
134
- };
135
- // First pass: assign from CSS variable names (most reliable signal)
136
- for (let i = 0; i < colors.length; i++) {
137
- const name = colors[i].name?.toLowerCase() || '';
138
- if (!name)
139
- continue;
140
- // light-bg comes from light-dark() on background property
141
- if (name === 'light-bg' && isLightScheme) {
142
- assign(i, 'background');
143
- // light-text comes from light-dark() on color property
144
- }
145
- else if (name === 'light-text' && isLightScheme) {
146
- assign(i, 'text-primary');
147
- }
148
- else if (/\b(surface|card|panel)\b/.test(name) && !assigned.has(i)) {
149
- assign(i, 'surface');
150
- }
151
- else if (/\b(accent|primary-action)\b/.test(name) && !/text|font/i.test(name)) {
152
- assign(i, 'accent');
153
- }
154
- else if (/\b(muted|subtle|secondary|placeholder|caption)\b/.test(name) && /text|fg|foreground|font|color/i.test(name)) {
155
- assign(i, 'text-muted');
156
- }
157
- else if (/\b(glow|highlight|hover)\b/.test(name)) {
158
- // Glows/highlights are extended palette, leave as unknown for now
159
- }
160
- }
161
- const withInfo = colors.map((c, i) => ({
162
- ...c,
163
- index: i,
164
- ...getColorInfo(c.hex),
165
- }));
166
- // For light-scheme sites: lightest = background, darkest = text
167
- if (isLightScheme) {
168
- // Background: lightest frequent color (if not already assigned)
169
- if (!colors.some(c => c.role === 'background')) {
170
- const lightColors = withInfo
171
- .filter(c => c.lightness > 0.7 && !assigned.has(c.index))
172
- .sort((a, b) => b.lightness - a.lightness || b.frequency - a.frequency);
173
- if (lightColors.length > 0)
174
- assign(lightColors[0].index, 'background');
175
- }
176
- // Text primary: darkest high-frequency color
177
- const darkColors = withInfo
178
- .filter(c => c.lightness < 0.2 && !assigned.has(c.index))
179
- .sort((a, b) => b.frequency - a.frequency);
180
- if (darkColors.length > 0)
181
- assign(darkColors[0].index, 'text-primary');
182
- // Surface: second lightest or a mid-tone
183
- const surfaceCandidates = withInfo
184
- .filter(c => c.lightness > 0.5 && !assigned.has(c.index))
185
- .sort((a, b) => b.lightness - a.lightness);
186
- if (surfaceCandidates.length > 0)
187
- assign(surfaceCandidates[0].index, 'surface');
188
- }
189
- else {
190
- // Dark theme: darkest = background, lightest = text
191
- // Background: darkest frequent color
192
- const darkColors = withInfo
193
- .filter(c => c.lightness < 0.25)
194
- .sort((a, b) => b.frequency - a.frequency);
195
- if (darkColors.length > 0)
196
- assign(darkColors[0].index, 'background');
197
- // Surface: second darkest
198
- if (darkColors.length > 1)
199
- assign(darkColors[1].index, 'surface');
200
- // Text primary: lightest high-frequency color
201
- const lightColors = withInfo
202
- .filter(c => c.lightness > 0.7 && !assigned.has(c.index))
203
- .sort((a, b) => b.frequency - a.frequency);
204
- if (lightColors.length > 0)
205
- assign(lightColors[0].index, 'text-primary');
206
- }
207
- // Text muted: medium lightness, low saturation — for dark sites use a wider range
208
- const mutedColors = withInfo
209
- .filter(c => c.lightness > 0.25 && c.lightness < 0.75 && c.saturation < 0.35 && !assigned.has(c.index))
210
- .sort((a, b) => b.frequency - a.frequency);
211
- if (mutedColors.length > 0)
212
- assign(mutedColors[0].index, 'text-muted');
213
- // Danger: red-ish
214
- const redish = withInfo
215
- .filter(c => c.saturation > 0.3 && (c.hue < 30 || c.hue > 330) && !assigned.has(c.index));
216
- if (redish.length > 0)
217
- assign(redish[0].index, 'danger');
218
- // Success: green-ish
219
- const greenish = withInfo
220
- .filter(c => c.saturation > 0.3 && c.hue > 90 && c.hue < 170 && !assigned.has(c.index));
221
- if (greenish.length > 0)
222
- assign(greenish[0].index, 'success');
223
- // Warning: yellow-orange
224
- const yellowish = withInfo
225
- .filter(c => c.saturation > 0.3 && c.hue > 30 && c.hue < 60 && !assigned.has(c.index));
226
- if (yellowish.length > 0)
227
- assign(yellowish[0].index, 'warning');
228
- // Info: blue-ish
229
- const blueish = withInfo
230
- .filter(c => c.saturation > 0.3 && c.hue > 180 && c.hue < 260 && !assigned.has(c.index));
231
- if (blueish.length > 0)
232
- assign(blueish[0].index, 'info');
233
- // Accent: most saturated mid-lightness color
234
- const accentCandidates = withInfo
235
- .filter(c => c.saturation > 0.15 && c.lightness > 0.3 && c.lightness < 0.85 && !assigned.has(c.index))
236
- .sort((a, b) => b.saturation - a.saturation || b.frequency - a.frequency);
237
- if (accentCandidates.length > 0)
238
- assign(accentCandidates[0].index, 'accent');
239
- // Border: low-saturation, medium value
240
- const borderCandidates = withInfo
241
- .filter(c => c.saturation < 0.2 && c.lightness > 0.1 && c.lightness < 0.4 && !assigned.has(c.index))
242
- .sort((a, b) => b.frequency - a.frequency);
243
- if (borderCandidates.length > 0)
244
- assign(borderCandidates[0].index, 'border');
245
- // Light theme fallback
246
- if (!colors.some(c => c.role === 'background')) {
247
- const lightBg = withInfo
248
- .filter(c => c.lightness > 0.9 && !assigned.has(c.index))
249
- .sort((a, b) => b.frequency - a.frequency);
250
- if (lightBg.length > 0)
251
- assign(lightBg[0].index, 'background');
252
- const darkText = withInfo
253
- .filter(c => c.lightness < 0.3 && !assigned.has(c.index))
254
- .sort((a, b) => b.frequency - a.frequency);
255
- if (darkText.length > 0)
256
- assign(darkText[0].index, 'text-primary');
257
- }
258
- return colors.slice(0, 20);
259
- }
260
- function getColorInfo(hex) {
261
- const rgb = hexToRgb(hex);
262
- if (!rgb)
263
- return { hue: 0, saturation: 0, lightness: 0 };
264
- const r = rgb.r / 255;
265
- const g = rgb.g / 255;
266
- const b = rgb.b / 255;
267
- const max = Math.max(r, g, b);
268
- const min = Math.min(r, g, b);
269
- const l = (max + min) / 2;
270
- let h = 0;
271
- let s = 0;
272
- if (max !== min) {
273
- const d = max - min;
274
- s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
275
- if (max === r)
276
- h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
277
- else if (max === g)
278
- h = ((b - r) / d + 2) * 60;
279
- else
280
- h = ((r - g) / d + 4) * 60;
281
- }
282
- return { hue: h, saturation: s, lightness: l };
283
- }
284
- function colorDistance(a, b) {
285
- return Math.sqrt(Math.pow(a.r - b.r, 2) +
286
- Math.pow(a.g - b.g, 2) +
287
- Math.pow(a.b - b.b, 2));
288
- }
289
- function hexToRgb(hex) {
290
- const match = hex.match(/^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})/i);
291
- if (!match)
292
- return null;
293
- return {
294
- r: parseInt(match[1], 16),
295
- g: parseInt(match[2], 16),
296
- b: parseInt(match[3], 16),
297
- };
298
- }
299
- function isValidHex(hex) {
300
- return /^#[0-9a-f]{6}$/i.test(hex);
301
- }
302
- /** Remove duplicate breakpoint values, keep unique pixel values sorted ascending. */
303
- function deduplicateBreakpoints(bps) {
304
- const seen = new Set();
305
- return bps
306
- .filter(bp => {
307
- if (seen.has(bp.value))
308
- return false;
309
- seen.add(bp.value);
310
- return true;
311
- })
312
- .sort((a, b) => {
313
- const av = parseFloat(a.value);
314
- const bv = parseFloat(b.value);
315
- return av - bv;
316
- });
317
- }
318
- /** Remove duplicate page sections (same type appearing from multiple crawled pages). */
319
- function deduplicatePageSections(sections) {
320
- const seen = new Map();
321
- for (const s of sections) {
322
- const key = `${s.type}:${s.description}`;
323
- if (!seen.has(key))
324
- seen.set(key, s);
325
- }
326
- return Array.from(seen.values());
327
- }
328
- // ── Typography ────────────────────────────────────────────────────────
329
- function normalizeTypography(rawFonts, fontVarMap) {
330
- if (rawFonts.length === 0)
331
- return [];
332
- const resolvedFonts = rawFonts.map(f => ({
333
- ...f,
334
- family: resolveFontFamily(f.family, fontVarMap),
335
- }));
336
- const familyFreq = new Map();
337
- for (const f of resolvedFonts) {
338
- if (!f.family)
339
- continue;
340
- const normalized = f.family.replace(/["']/g, '').trim();
341
- if (normalized && !isGenericFamily(normalized)) {
342
- familyFreq.set(normalized, (familyFreq.get(normalized) || 0) + 1);
343
- }
344
- }
345
- // Merge "Foo Fallback" entries into "Foo" if both exist
346
- // Browsers generate "X Fallback" names for @font-face fonts during loading
347
- const mergedFreq = new Map();
348
- for (const [family, freq] of familyFreq.entries()) {
349
- const baseName = family.replace(/\s+Fallback$/i, '');
350
- if (baseName !== family && familyFreq.has(baseName)) {
351
- // "Foo Fallback" exists alongside "Foo" — merge into "Foo"
352
- mergedFreq.set(baseName, (mergedFreq.get(baseName) || 0) + freq);
353
- }
354
- else if (baseName !== family && !familyFreq.has(baseName)) {
355
- // Only "Foo Fallback" exists — use the base name "Foo"
356
- mergedFreq.set(baseName, (mergedFreq.get(baseName) || 0) + freq);
357
- }
358
- else if (!family.endsWith(' Fallback') || !familyFreq.has(family.replace(/\s+Fallback$/i, ''))) {
359
- mergedFreq.set(family, (mergedFreq.get(family) || 0) + freq);
360
- }
361
- }
362
- const sortedFamilies = Array.from(mergedFreq.entries())
363
- .sort((a, b) => b[1] - a[1])
364
- .map(([family]) => family);
365
- const primaryFont = sortedFamilies[0] || 'sans-serif';
366
- const secondaryFont = sortedFamilies.find(f => f !== primaryFont && !isMonoFont(f));
367
- const monoFont = sortedFamilies.find(f => isMonoFont(f));
368
- const sizes = new Map();
369
- for (const f of resolvedFonts) {
370
- if (f.size) {
371
- sizes.set(f.size, (sizes.get(f.size) || 0) + 1);
372
- }
373
- }
374
- const sortedSizes = Array.from(sizes.entries())
375
- .sort((a, b) => parseSizeToPixels(b[0]) - parseSizeToPixels(a[0]));
376
- // Separate sizes into heading-range (>= 24px) and body-range (< 24px)
377
- const headingSizes = sortedSizes.filter(([s]) => parseSizeToPixels(s) >= 24);
378
- const bodySizes = sortedSizes.filter(([s]) => {
379
- const px = parseSizeToPixels(s);
380
- return px >= 10 && px < 24;
381
- });
382
- const tokens = [];
383
- const headingRoles = ['heading-1', 'heading-2', 'heading-3'];
384
- const bodyRoles = ['body', 'caption'];
385
- // If we have a good spread of both heading and body sizes, use smart assignment
386
- if (headingSizes.length >= 2 && bodySizes.length >= 1) {
387
- // Assign headings from largest down
388
- for (let i = 0; i < Math.min(headingRoles.length, headingSizes.length); i++) {
389
- tokens.push({
390
- role: headingRoles[i],
391
- fontFamily: secondaryFont || primaryFont,
392
- fontSize: headingSizes[i][0],
393
- fontWeight: '700',
394
- source: rawFonts[0]?.source || 'css',
395
- });
396
- }
397
- // Assign body/caption from the body-range sizes (most frequent first)
398
- const bodyByFreq = [...bodySizes].sort((a, b) => b[1] - a[1]);
399
- for (let i = 0; i < Math.min(bodyRoles.length, bodyByFreq.length); i++) {
400
- tokens.push({
401
- role: bodyRoles[i],
402
- fontFamily: primaryFont,
403
- fontSize: bodyByFreq[i][0],
404
- fontWeight: '400',
405
- source: rawFonts[0]?.source || 'css',
406
- });
407
- }
408
- }
409
- else if (sortedSizes.length >= 4) {
410
- // Fallback: simple assignment from largest to smallest
411
- const roles = ['heading-1', 'heading-2', 'heading-3', 'body', 'caption'];
412
- for (let i = 0; i < Math.min(roles.length, sortedSizes.length); i++) {
413
- const isHeading = roles[i].startsWith('heading');
414
- tokens.push({
415
- role: roles[i],
416
- fontFamily: isHeading && secondaryFont ? secondaryFont : primaryFont,
417
- fontSize: sortedSizes[i][0],
418
- fontWeight: isHeading ? '700' : '400',
419
- source: rawFonts[0]?.source || 'css',
420
- });
421
- }
422
- }
423
- else {
424
- const defaultScale = [
425
- { role: 'heading-1', size: '48px / 3rem', weight: '700' },
426
- { role: 'heading-2', size: '32px / 2rem', weight: '600' },
427
- { role: 'heading-3', size: '24px / 1.5rem', weight: '600' },
428
- { role: 'body', size: '16px / 1rem', weight: '400' },
429
- { role: 'caption', size: '12px / 0.75rem', weight: '400' },
430
- ];
431
- for (const item of defaultScale) {
432
- const isHeading = item.role.startsWith('heading');
433
- tokens.push({
434
- role: item.role,
435
- fontFamily: isHeading && secondaryFont ? secondaryFont : primaryFont,
436
- fontSize: item.size,
437
- fontWeight: item.weight,
438
- source: rawFonts[0]?.source || 'css',
439
- });
440
- }
441
- }
442
- if (monoFont) {
443
- tokens.push({
444
- role: 'code',
445
- fontFamily: monoFont,
446
- fontSize: '14px',
447
- fontWeight: '400',
448
- source: rawFonts[0]?.source || 'css',
449
- });
450
- }
451
- return tokens;
452
- }
453
- function resolveFontFamily(family, varMap) {
454
- if (!family)
455
- return family;
456
- if (family.startsWith('var(')) {
457
- const resolved = varMap[family];
458
- if (resolved)
459
- return resolved;
460
- const varName = family.replace(/^var\(/, '').replace(/\)$/, '').trim();
461
- if (varMap[varName])
462
- return varMap[varName];
463
- }
464
- if (varMap[family])
465
- return varMap[family];
466
- return family.replace(/["']/g, '').trim();
467
- }
468
- function isGenericFamily(f) {
469
- if (/^(sans-serif|serif|monospace|cursive|fantasy|system-ui|ui-sans-serif|ui-serif|ui-monospace)$/i.test(f))
470
- return true;
471
- // Filter out unresolved CSS variable references (e.g. "var(--default-font-family")
472
- if (/^var\(/.test(f))
473
- return true;
474
- // Filter out font names that are just numbers or very short
475
- if (f.length < 2)
476
- return true;
477
- // Filter out malformed font names with CSS syntax artifacts
478
- if (/[{};()\n\r<>]/.test(f))
479
- return true;
480
- // Filter out names that are too long to be real font names
481
- if (f.length > 50)
482
- return true;
483
- // Filter out icon/symbol fonts
484
- if (/^(apple\s*(icons?|legacy|sf\s*symbols?)|material\s*(icons?|symbols?)|font\s*awesome|fontawesome|glyphicons?|ionicons?)/i.test(f))
485
- return true;
486
- if (/^apple\s*icons?\s*\d+/i.test(f))
487
- return true;
488
- // Filter out system font names
489
- if (/^(-apple-system|blinkmacsystemfont|\.sf\s*(pro|compact))/i.test(f))
490
- return true;
491
- // Filter out emoji fonts
492
- if (/^(apple\s*color\s*emoji|noto\s*color\s*emoji|segoe\s*ui\s*emoji|android\s*emoji|twemoji)/i.test(f))
493
- return true;
494
- // Filter out font fallback auto-generated names (e.g. "delight Fallback")
495
- if (/\bfallback\b/i.test(f))
496
- return true;
497
- return false;
498
- }
499
- function isMonoFont(family) {
500
- return /mono|consolas|courier|fira code|jetbrains|sf mono|menlo|hack|source code/i.test(family);
501
- }
502
- function parseSizeToPixels(size) {
503
- const px = size.match(/([\d.]+)\s*px/);
504
- if (px)
505
- return parseFloat(px[1]);
506
- const rem = size.match(/([\d.]+)\s*rem/);
507
- if (rem)
508
- return parseFloat(rem[1]) * 16;
509
- return 0;
510
- }
511
- // ── Spacing ───────────────────────────────────────────────────────────
512
- function normalizeSpacing(values) {
513
- if (values.length === 0) {
514
- return { base: 4, values: [4, 8, 12, 16, 20, 24, 32, 40, 48, 64], unit: 'px' };
515
- }
516
- const unique = [...new Set(values)]
517
- .filter(v => v > 0 && v <= 200)
518
- .sort((a, b) => a - b);
519
- const base = detectBase(unique);
520
- const aligned = unique.filter(v => v % base === 0);
521
- const halfAligned = unique.filter(v => v % (base / 2) === 0 && !aligned.includes(v));
522
- const combined = [...new Set([...aligned, ...halfAligned])].sort((a, b) => a - b);
523
- let finalValues;
524
- if (combined.length >= 6) {
525
- finalValues = combined;
526
- }
527
- else if (aligned.length >= 4) {
528
- finalValues = aligned;
529
- }
530
- else {
531
- finalValues = [];
532
- for (let m = 1; m <= 24; m++) {
533
- const v = base * m;
534
- if (v <= 200)
535
- finalValues.push(v);
536
- }
537
- }
538
- return {
539
- base,
540
- values: finalValues.slice(0, 15),
541
- unit: 'px',
542
- };
543
- }
544
- function detectBase(values) {
545
- if (values.length < 2)
546
- return values[0] || 4;
547
- const candidates = [8, 4, 6, 5, 10];
548
- let bestBase = 4;
549
- let bestScore = 0;
550
- for (const base of candidates) {
551
- const divisible = values.filter(v => v % base === 0);
552
- const ratio = divisible.length / values.length;
553
- const score = ratio + (base >= 8 ? 0.05 : 0);
554
- if (score > bestScore) {
555
- bestScore = score;
556
- bestBase = base;
557
- }
558
- }
559
- return bestBase;
560
- }
561
- // ── Shadows ───────────────────────────────────────────────────────────
562
- function normalizeShadows(rawShadows) {
563
- if (rawShadows.length === 0)
564
- return [];
565
- const seen = new Set();
566
- const unique = [];
567
- for (const s of rawShadows) {
568
- const normalized = s.value.trim().toLowerCase();
569
- if (!normalized || normalized === 'none')
570
- continue;
571
- // Skip pure Tailwind CSS variable chains — they have no real shadow value
572
- // e.g. "var(--tw-inset-shadow),var(--tw-ring-shadow),var(--tw-shadow)"
573
- // These expand to 'none' unless Tailwind is running — useless in standalone CSS
574
- if (/^var\(--tw-/.test(normalized) && !normalized.match(/\d+px/))
575
- continue;
576
- if (!seen.has(normalized)) {
577
- seen.add(normalized);
578
- unique.push(s);
579
- }
580
- }
581
- return unique.map(s => ({
582
- value: s.value,
583
- level: classifyShadow(s.value),
584
- name: s.name,
585
- })).sort((a, b) => shadowLevelOrder(a.level) - shadowLevelOrder(b.level));
586
- }
587
- function classifyShadow(value) {
588
- const numbers = value.match(/(\d+(?:\.\d+)?)\s*px/g);
589
- if (!numbers)
590
- return 'raised';
591
- const pxValues = numbers.map(n => parseFloat(n));
592
- const maxBlur = Math.max(...pxValues);
593
- if (maxBlur <= 2)
594
- return 'flat';
595
- if (maxBlur <= 8)
596
- return 'raised';
597
- if (maxBlur <= 20)
598
- return 'floating';
599
- return 'overlay';
600
- }
601
- function shadowLevelOrder(level) {
602
- const order = { flat: 0, raised: 1, floating: 2, overlay: 3 };
603
- return order[level];
604
- }
605
- // ── Border Radius ─────────────────────────────────────────────────────
606
- function normalizeBorderRadius(radii, components) {
607
- const allRadii = [...radii];
608
- for (const comp of components) {
609
- for (const cls of comp.cssClasses) {
610
- if (cls.startsWith('rounded')) {
611
- const twRadius = tailwindRoundedToValue(cls);
612
- if (twRadius)
613
- allRadii.push(twRadius);
614
- }
615
- }
616
- }
617
- // Count frequency of each radius value to detect dominant patterns
618
- const radiusFreq = new Map();
619
- for (const r of allRadii) {
620
- const normalized = r === '0' ? '0px' : r;
621
- radiusFreq.set(normalized, (radiusFreq.get(normalized) || 0) + 1);
622
- }
623
- // Check if 0px (sharp corners) is the dominant radius
624
- const zeroCount = (radiusFreq.get('0px') || 0) + (radiusFreq.get('0') || 0);
625
- const totalCount = allRadii.length;
626
- const sharpCornersDesign = zeroCount > 0 && zeroCount >= totalCount * 0.5;
627
- const unique = [...new Set(allRadii)]
628
- .filter(r => {
629
- // Filter out CSS variable references (var(--...))
630
- if (r.includes('var('))
631
- return false;
632
- // Filter out pill/full radius
633
- if (r.includes('9999') || r === '50%')
634
- return false;
635
- // Filter out infinity values (e.g. 3.40282e38px from CSS)
636
- const numVal = parseFloat(r);
637
- if (!isNaN(numVal) && numVal > 1000)
638
- return false;
639
- // Keep 0px only if sharp corners dominate (brutalist/sharp design)
640
- if ((r === '0' || r === '0px') && !sharpCornersDesign)
641
- return false;
642
- return true;
643
- })
644
- .sort((a, b) => parseFloat(a) - parseFloat(b));
645
- return unique.length > 0 ? unique : ['8px'];
646
- }
647
- function tailwindRoundedToValue(cls) {
648
- const map = {
649
- 'rounded-none': '0px',
650
- 'rounded-sm': '2px',
651
- 'rounded': '4px',
652
- 'rounded-md': '6px',
653
- 'rounded-lg': '8px',
654
- 'rounded-xl': '12px',
655
- 'rounded-2xl': '16px',
656
- 'rounded-3xl': '24px',
657
- 'rounded-full': '9999px',
658
- };
659
- const baseClass = cls.replace(/-(t|b|l|r|tl|tr|bl|br)-/, '-');
660
- return map[baseClass] || null;
661
- }
662
- // ── Animations ────────────────────────────────────────────────────────
663
- function normalizeAnimations(rawAnimations) {
664
- const seen = new Set();
665
- const unique = [];
666
- for (const anim of rawAnimations) {
667
- const key = `${anim.type}:${anim.name}`;
668
- if (!seen.has(key)) {
669
- seen.add(key);
670
- unique.push(anim);
671
- }
672
- }
673
- return unique;
674
- }
675
- // ── Motion Tokens ─────────────────────────────────────────────────────
676
- function normalizeMotionTokens(rawTokens, animations) {
677
- // Collect durations — only clean values, no var() references
678
- const durations = new Set();
679
- for (const d of (rawTokens.transitionDurations || [])) {
680
- if (!d.includes('var(') && /^[\d.]+m?s$/.test(d.trim())) {
681
- durations.add(d.trim());
682
- }
683
- }
684
- // Also parse durations from transition shorthands
685
- for (const anim of animations) {
686
- if (anim.type === 'css-transition') {
687
- const durMatch = anim.value.matchAll(/([\d.]+)(ms|s)\b/g);
688
- for (const m of durMatch) {
689
- const dur = m[2] === 's' ? `${parseFloat(m[1]) * 1000}ms` : `${m[1]}ms`;
690
- durations.add(dur);
691
- }
692
- }
693
- }
694
- // Collect easings — only clean values, no var() references
695
- const easings = new Set();
696
- for (const e of (rawTokens.transitionEasings || [])) {
697
- if (!e.includes('var(') && (/^(ease|ease-in|ease-out|ease-in-out|linear)$/.test(e.trim()) ||
698
- /^cubic-bezier\(/.test(e.trim()))) {
699
- easings.add(e.trim());
700
- }
701
- }
702
- for (const anim of animations) {
703
- if (anim.type === 'css-transition') {
704
- const easingPatterns = [
705
- /\b(ease-in-out|ease-in|ease-out|ease|linear)\b/g,
706
- /cubic-bezier\([^)]+\)/g,
707
- ];
708
- for (const pattern of easingPatterns) {
709
- const matches = anim.value.matchAll(pattern);
710
- for (const m of matches) {
711
- easings.add(m[0]);
712
- }
713
- }
714
- }
715
- }
716
- // Collect animated properties
717
- const properties = new Set();
718
- for (const anim of animations) {
719
- if (anim.type === 'css-transition') {
720
- // Extract property names from "all 150ms ease", "opacity 0.3s", etc.
721
- const parts = anim.value.split(',');
722
- for (const part of parts) {
723
- const propMatch = part.trim().match(/^([\w-]+)\s/);
724
- if (propMatch && !['all'].includes(propMatch[1])) {
725
- properties.add(propMatch[1]);
726
- }
727
- }
728
- }
729
- }
730
- // Sort durations numerically
731
- const sortedDurations = [...durations].sort((a, b) => {
732
- const ams = parseFloat(a);
733
- const bms = parseFloat(b);
734
- return ams - bms;
735
- });
736
- return {
737
- durations: sortedDurations,
738
- easings: [...easings],
739
- properties: [...properties],
740
- };
741
- }
742
- // ── Component Categories ──────────────────────────────────────────────
743
- function buildComponentCategories(components) {
744
- const cats = {
745
- 'layout': [],
746
- 'navigation': [],
747
- 'data-display': [],
748
- 'data-input': [],
749
- 'feedback': [],
750
- 'overlay': [],
751
- 'typography': [],
752
- 'media': [],
753
- 'other': [],
754
- };
755
- for (const comp of components) {
756
- cats[comp.category].push(comp.name);
757
- }
758
- return cats;
759
- }
760
- // ── Anti-Patterns Detection ───────────────────────────────────────────
761
- function detectAntiPatterns(rawTokens, components, shadows) {
762
- const patterns = [];
763
- if (shadows.length === 0)
764
- patterns.push('no-shadows');
765
- if ((rawTokens.gradients || []).length === 0)
766
- patterns.push('no-gradients');
767
- let hasBlur = false;
768
- let hasSkeletonLoaders = false;
769
- let hasParallax = false;
770
- for (const comp of components) {
771
- for (const cls of comp.cssClasses) {
772
- if (cls.includes('blur') || cls.includes('backdrop-blur'))
773
- hasBlur = true;
774
- if (cls.includes('skeleton') || cls.includes('animate-pulse'))
775
- hasSkeletonLoaders = true;
776
- if (cls.includes('parallax'))
777
- hasParallax = true;
778
- }
779
- if (comp.jsxSnippet) {
780
- if (/skeleton|shimmer|pulse/i.test(comp.jsxSnippet))
781
- hasSkeletonLoaders = true;
782
- if (/toast|Toaster|sonner/i.test(comp.jsxSnippet))
783
- patterns.push('has-toasts');
784
- }
785
- }
786
- if (!hasBlur)
787
- patterns.push('no-blur');
788
- if (hasSkeletonLoaders)
789
- patterns.push('has-skeleton-loaders');
790
- if (hasParallax)
791
- patterns.push('has-parallax');
792
- let hasZebraStriping = false;
793
- for (const comp of components) {
794
- if (/even:|odd:|striped|zebra/i.test(comp.cssClasses.join(' '))) {
795
- hasZebraStriping = true;
796
- }
797
- }
798
- if (!hasZebraStriping)
799
- patterns.push('no-zebra-striping');
800
- return patterns;
801
- }
802
- // ── Design Traits ─────────────────────────────────────────────────────
803
- function computeDesignTraits(colors, typography, spacing, shadows, rawTokens, animations) {
804
- const bg = colors.find(c => c.role === 'background');
805
- const accent = colors.find(c => c.role === 'accent');
806
- const primaryFont = typography.find(t => t.role === 'body')?.fontFamily || 'sans-serif';
807
- const isDark = bg ? isColorDark(bg.hex) : false;
808
- const tempColor = accent?.hex || bg?.hex || '#333333';
809
- const tempRgb = hexToRgb(tempColor);
810
- let primaryColorTemp = 'neutral';
811
- if (tempRgb) {
812
- if (tempRgb.r > tempRgb.b + 30)
813
- primaryColorTemp = 'warm';
814
- else if (tempRgb.b > tempRgb.r + 30)
815
- primaryColorTemp = 'cool';
816
- }
817
- let fontStyle = 'sans-serif';
818
- if (isMonoFont(primaryFont))
819
- fontStyle = 'monospace';
820
- else if (/serif|georgia|times|garamond|merriweather|playfair/i.test(primaryFont) &&
821
- !/sans/i.test(primaryFont))
822
- fontStyle = 'serif';
823
- let density = 'standard';
824
- if (spacing.base <= 4)
825
- density = 'compact';
826
- else if (spacing.base >= 12)
827
- density = 'spacious';
828
- const radii = (rawTokens.borderRadii || []).map(r => parseFloat(r)).filter(n => !isNaN(n) && n < 9999);
829
- const maxBorderRadius = radii.length > 0 ? Math.max(...radii) : 8;
830
- const hasAnimations = animations.length > 0;
831
- // hasDarkMode = site has a TOGGLEABLE dark mode (light/dark switch), NOT just "is dark"
832
- // A dark-primary site with no light mode toggle should have hasDarkMode = false
833
- const hasDarkMode = (rawTokens.darkModeVars || []).length > 0;
834
- // Motion style — based on number and type of animations, NOT binary has/doesn't
835
- let motionStyle = 'none';
836
- if (animations.length > 0 || (rawTokens.transitionDurations || []).length > 0) {
837
- const hasFramerMotion = animations.some(a => a.type === 'framer-motion' || a.type === 'spring');
838
- const hasLayoutAnims = animations.some(a => a.value.includes('layout-animation'));
839
- if (hasFramerMotion || hasLayoutAnims || animations.length > 5) {
840
- motionStyle = 'expressive';
841
- }
842
- else {
843
- motionStyle = 'subtle';
844
- }
845
- }
846
- return {
847
- isDark,
848
- hasShadows: shadows.length > 0,
849
- hasGradients: (rawTokens.gradients || []).length > 0,
850
- hasRoundedFull: (rawTokens.borderRadii || []).some(r => r.includes('9999') || r === '50%'),
851
- maxBorderRadius,
852
- primaryColorTemp,
853
- fontStyle,
854
- density,
855
- hasAnimations,
856
- hasDarkMode,
857
- motionStyle,
858
- };
859
- }
860
- function isColorDark(hex) {
861
- const rgb = hexToRgb(hex);
862
- if (!rgb)
863
- return false;
864
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
865
- return luminance < 0.5;
866
- }
867
- //# sourceMappingURL=normalizer.js.map