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.
- package/dist/cli.js +105073 -194
- package/package.json +15 -6
- package/dist/cli.d.ts +0 -3
- package/dist/extractors/components.d.ts +0 -11
- package/dist/extractors/components.js +0 -455
- package/dist/extractors/framework.d.ts +0 -4
- package/dist/extractors/framework.js +0 -126
- package/dist/extractors/tokens/computed.d.ts +0 -7
- package/dist/extractors/tokens/computed.js +0 -249
- package/dist/extractors/tokens/css.d.ts +0 -3
- package/dist/extractors/tokens/css.js +0 -510
- package/dist/extractors/tokens/http-css.d.ts +0 -14
- package/dist/extractors/tokens/http-css.js +0 -1689
- package/dist/extractors/tokens/tailwind.d.ts +0 -3
- package/dist/extractors/tokens/tailwind.js +0 -353
- package/dist/extractors/tokens/tokens-file.d.ts +0 -3
- package/dist/extractors/tokens/tokens-file.js +0 -229
- package/dist/extractors/ultra/animations.d.ts +0 -21
- package/dist/extractors/ultra/animations.js +0 -527
- package/dist/extractors/ultra/components-dom.d.ts +0 -13
- package/dist/extractors/ultra/components-dom.js +0 -149
- package/dist/extractors/ultra/interactions.d.ts +0 -14
- package/dist/extractors/ultra/interactions.js +0 -222
- package/dist/extractors/ultra/layout.d.ts +0 -14
- package/dist/extractors/ultra/layout.js +0 -123
- package/dist/extractors/ultra/pages.d.ts +0 -16
- package/dist/extractors/ultra/pages.js +0 -228
- package/dist/font-resolver.d.ts +0 -10
- package/dist/font-resolver.js +0 -280
- package/dist/modes/dir.d.ts +0 -6
- package/dist/modes/dir.js +0 -213
- package/dist/modes/repo.d.ts +0 -6
- package/dist/modes/repo.js +0 -76
- package/dist/modes/ultra.d.ts +0 -22
- package/dist/modes/ultra.js +0 -281
- package/dist/modes/url.d.ts +0 -14
- package/dist/modes/url.js +0 -161
- package/dist/normalizer.d.ts +0 -11
- package/dist/normalizer.js +0 -867
- package/dist/playwright-loader.d.ts +0 -10
- package/dist/playwright-loader.js +0 -71
- package/dist/screenshot.d.ts +0 -9
- package/dist/screenshot.js +0 -94
- package/dist/types-ultra.d.ts +0 -157
- package/dist/types-ultra.js +0 -4
- package/dist/types.d.ts +0 -182
- package/dist/types.js +0 -4
- package/dist/writers/animations-md.d.ts +0 -17
- package/dist/writers/animations-md.js +0 -313
- package/dist/writers/components-md.d.ts +0 -8
- package/dist/writers/components-md.js +0 -151
- package/dist/writers/design-md.d.ts +0 -7
- package/dist/writers/design-md.js +0 -704
- package/dist/writers/interactions-md.d.ts +0 -8
- package/dist/writers/interactions-md.js +0 -146
- package/dist/writers/layout-md.d.ts +0 -8
- package/dist/writers/layout-md.js +0 -120
- package/dist/writers/skill.d.ts +0 -12
- package/dist/writers/skill.js +0 -1006
- package/dist/writers/tokens-json.d.ts +0 -11
- package/dist/writers/tokens-json.js +0 -164
package/dist/normalizer.js
DELETED
|
@@ -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
|