skillui 1.1.2 → 1.1.4

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 (62) hide show
  1. package/README.md +20 -15
  2. package/dist/cli.js +105073 -194
  3. package/package.json +15 -6
  4. package/dist/cli.d.ts +0 -3
  5. package/dist/extractors/components.d.ts +0 -11
  6. package/dist/extractors/components.js +0 -455
  7. package/dist/extractors/framework.d.ts +0 -4
  8. package/dist/extractors/framework.js +0 -126
  9. package/dist/extractors/tokens/computed.d.ts +0 -7
  10. package/dist/extractors/tokens/computed.js +0 -249
  11. package/dist/extractors/tokens/css.d.ts +0 -3
  12. package/dist/extractors/tokens/css.js +0 -510
  13. package/dist/extractors/tokens/http-css.d.ts +0 -14
  14. package/dist/extractors/tokens/http-css.js +0 -1689
  15. package/dist/extractors/tokens/tailwind.d.ts +0 -3
  16. package/dist/extractors/tokens/tailwind.js +0 -353
  17. package/dist/extractors/tokens/tokens-file.d.ts +0 -3
  18. package/dist/extractors/tokens/tokens-file.js +0 -229
  19. package/dist/extractors/ultra/animations.d.ts +0 -21
  20. package/dist/extractors/ultra/animations.js +0 -527
  21. package/dist/extractors/ultra/components-dom.d.ts +0 -13
  22. package/dist/extractors/ultra/components-dom.js +0 -149
  23. package/dist/extractors/ultra/interactions.d.ts +0 -14
  24. package/dist/extractors/ultra/interactions.js +0 -222
  25. package/dist/extractors/ultra/layout.d.ts +0 -14
  26. package/dist/extractors/ultra/layout.js +0 -123
  27. package/dist/extractors/ultra/pages.d.ts +0 -16
  28. package/dist/extractors/ultra/pages.js +0 -228
  29. package/dist/font-resolver.d.ts +0 -10
  30. package/dist/font-resolver.js +0 -280
  31. package/dist/modes/dir.d.ts +0 -6
  32. package/dist/modes/dir.js +0 -213
  33. package/dist/modes/repo.d.ts +0 -6
  34. package/dist/modes/repo.js +0 -76
  35. package/dist/modes/ultra.d.ts +0 -22
  36. package/dist/modes/ultra.js +0 -281
  37. package/dist/modes/url.d.ts +0 -14
  38. package/dist/modes/url.js +0 -161
  39. package/dist/normalizer.d.ts +0 -11
  40. package/dist/normalizer.js +0 -867
  41. package/dist/playwright-loader.d.ts +0 -10
  42. package/dist/playwright-loader.js +0 -71
  43. package/dist/screenshot.d.ts +0 -9
  44. package/dist/screenshot.js +0 -94
  45. package/dist/types-ultra.d.ts +0 -157
  46. package/dist/types-ultra.js +0 -4
  47. package/dist/types.d.ts +0 -182
  48. package/dist/types.js +0 -4
  49. package/dist/writers/animations-md.d.ts +0 -17
  50. package/dist/writers/animations-md.js +0 -313
  51. package/dist/writers/components-md.d.ts +0 -8
  52. package/dist/writers/components-md.js +0 -151
  53. package/dist/writers/design-md.d.ts +0 -7
  54. package/dist/writers/design-md.js +0 -704
  55. package/dist/writers/interactions-md.d.ts +0 -8
  56. package/dist/writers/interactions-md.js +0 -146
  57. package/dist/writers/layout-md.d.ts +0 -8
  58. package/dist/writers/layout-md.js +0 -120
  59. package/dist/writers/skill.d.ts +0 -12
  60. package/dist/writers/skill.js +0 -1006
  61. package/dist/writers/tokens-json.d.ts +0 -11
  62. package/dist/writers/tokens-json.js +0 -164
@@ -1,1689 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.extractHttpCSSTokens = extractHttpCSSTokens;
37
- const csstree = __importStar(require("css-tree"));
38
- async function extractHttpCSSTokens(url, maxPages = 3) {
39
- const tokens = {
40
- colors: [],
41
- fonts: [],
42
- spacingValues: [],
43
- shadows: [],
44
- cssVariables: [],
45
- breakpoints: [],
46
- borderRadii: [],
47
- gradients: [],
48
- fontVarMap: {},
49
- animations: [],
50
- darkModeVars: [],
51
- zIndexValues: [],
52
- containerMaxWidth: null,
53
- fontSources: [],
54
- pageSections: [],
55
- transitionDurations: [],
56
- transitionEasings: [],
57
- };
58
- const visited = new Set();
59
- const toVisit = [url];
60
- let origin;
61
- const allHtml = [];
62
- const allCssContent = [];
63
- try {
64
- origin = new URL(url).origin;
65
- }
66
- catch {
67
- console.error(' Invalid URL');
68
- return { tokens, components: [] };
69
- }
70
- while (toVisit.length > 0 && visited.size < maxPages) {
71
- const currentUrl = toVisit.shift();
72
- if (visited.has(currentUrl))
73
- continue;
74
- visited.add(currentUrl);
75
- try {
76
- const html = await fetchText(currentUrl);
77
- if (!html)
78
- continue;
79
- allHtml.push(html);
80
- // Extract all linked CSS URLs from HTML
81
- const cssUrls = extractCSSUrls(html, currentUrl);
82
- // Extract inline <style> blocks (and resolve @import URLs within them)
83
- const inlineStyles = extractInlineStyles(html);
84
- // Extract inline style= attributes for color sampling
85
- extractInlineAttributeColors(html, tokens);
86
- // Extract meta theme-color
87
- extractMetaColors(html, tokens);
88
- // Detect fonts from <link> to Google Fonts / other font CDNs
89
- extractFontLinks(html, tokens);
90
- // Detect page structure for pseudo-components
91
- extractDOMStructure(html, tokens);
92
- // Detect and decode SPA modules (React, base64-encoded JS)
93
- extractSPAModules(html, tokens, allCssContent);
94
- // Collect @import URLs from inline styles
95
- for (const style of inlineStyles) {
96
- const importUrls = extractImportUrls(style, currentUrl);
97
- for (const importUrl of importUrls) {
98
- if (!cssUrls.includes(importUrl))
99
- cssUrls.push(importUrl);
100
- }
101
- }
102
- // Fetch and parse each CSS file (including @import targets)
103
- const fetchedCss = new Set();
104
- const cssQueue = [...cssUrls];
105
- while (cssQueue.length > 0) {
106
- const cssUrl = cssQueue.shift();
107
- if (fetchedCss.has(cssUrl))
108
- continue;
109
- fetchedCss.add(cssUrl);
110
- try {
111
- const cssContent = await fetchText(cssUrl);
112
- if (cssContent) {
113
- allCssContent.push(cssContent);
114
- parseCSS(cssContent, tokens, cssUrl);
115
- // Follow nested @import URLs
116
- const nestedImports = extractImportUrls(cssContent, cssUrl);
117
- for (const ni of nestedImports) {
118
- if (!fetchedCss.has(ni))
119
- cssQueue.push(ni);
120
- }
121
- }
122
- }
123
- catch {
124
- // Skip unreachable CSS files
125
- }
126
- }
127
- // Parse inline styles
128
- for (const style of inlineStyles) {
129
- allCssContent.push(style);
130
- parseCSS(style, tokens, currentUrl);
131
- }
132
- // Find additional page links for crawling (same origin only)
133
- if (visited.size < maxPages) {
134
- const links = extractPageLinks(html, currentUrl, origin);
135
- for (const link of links.slice(0, 5)) {
136
- if (!visited.has(link))
137
- toVisit.push(link);
138
- }
139
- }
140
- }
141
- catch (err) {
142
- // Skip pages that fail to load
143
- }
144
- }
145
- // Post-process: build dark mode pairs
146
- buildDarkModePairs(tokens);
147
- // Decode SPA modules for component detection
148
- let decodedJS = '';
149
- const combinedHtml = allHtml.join('\n');
150
- const base64Modules = combinedHtml.matchAll(/data:application\/javascript;base64,([A-Za-z0-9+/=]+)/g);
151
- for (const m of base64Modules) {
152
- try {
153
- decodedJS += '\n' + Buffer.from(m[1], 'base64').toString('utf-8');
154
- }
155
- catch { }
156
- }
157
- // Detect real UI components from HTML + decoded JS + CSS
158
- const combinedCss = allCssContent.join('\n');
159
- const components = detectHTMLComponents(combinedHtml + '\n' + decodedJS, combinedCss);
160
- return { tokens, components };
161
- }
162
- // ── HTML Parsing Helpers ──────────────────────────────────────────────
163
- function extractCSSUrls(html, baseUrl) {
164
- const urls = [];
165
- // <link rel="stylesheet" href="...">
166
- const linkMatches = html.matchAll(/<link[^>]+rel\s*=\s*["']stylesheet["'][^>]*href\s*=\s*["']([^"']+)["']/gi);
167
- for (const m of linkMatches) {
168
- urls.push(resolveUrl(m[1], baseUrl));
169
- }
170
- // Also catch <link href="..." rel="stylesheet">
171
- const linkMatches2 = html.matchAll(/<link[^>]+href\s*=\s*["']([^"']+)["'][^>]*rel\s*=\s*["']stylesheet["']/gi);
172
- for (const m of linkMatches2) {
173
- const resolved = resolveUrl(m[1], baseUrl);
174
- if (!urls.includes(resolved))
175
- urls.push(resolved);
176
- }
177
- // CSS @import from inline styles will be caught by css-tree
178
- return urls;
179
- }
180
- function extractImportUrls(css, baseUrl) {
181
- const urls = [];
182
- // @import url("...") or @import url(...) or @import "..."
183
- const importMatches = css.matchAll(/@import\s+(?:url\(\s*["']?([^"')]+)["']?\s*\)|["']([^"']+)["'])/gi);
184
- for (const m of importMatches) {
185
- const importUrl = m[1] || m[2];
186
- if (importUrl) {
187
- urls.push(resolveUrl(importUrl, baseUrl));
188
- }
189
- }
190
- return urls;
191
- }
192
- function extractInlineStyles(html) {
193
- const styles = [];
194
- const styleMatches = html.matchAll(/<style[^>]*>([\s\S]*?)<\/style>/gi);
195
- for (const m of styleMatches) {
196
- if (m[1].trim())
197
- styles.push(m[1]);
198
- }
199
- return styles;
200
- }
201
- function extractInlineAttributeColors(html, tokens) {
202
- // Extract colors from style="" attributes
203
- const styleAttrMatches = html.matchAll(/style\s*=\s*["']([^"']+)["']/gi);
204
- for (const m of styleAttrMatches) {
205
- const val = m[1];
206
- // Hex colors
207
- const hexMatches = val.matchAll(/#([0-9a-fA-F]{3,8})\b/g);
208
- for (const h of hexMatches) {
209
- const hex = normalizeHex(h[0]);
210
- addColor(tokens, hex, 'css');
211
- }
212
- // RGB colors
213
- const rgbMatches = val.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g);
214
- for (const r of rgbMatches) {
215
- addColor(tokens, rgbToHex(parseInt(r[1]), parseInt(r[2]), parseInt(r[3])), 'css');
216
- }
217
- }
218
- // Parse light-dark() CSS function — extract the LIGHT value (first arg)
219
- // light-dark(white, black) → use "white" as default
220
- parseLightDarkColors(html, tokens);
221
- }
222
- function parseLightDarkColors(content, tokens) {
223
- // Parse property: light-dark(lightVal, darkVal) — context-aware
224
- // background: light-dark(white, black) → white is background
225
- // color: light-dark(black, white) → black is text
226
- const propMatches = content.matchAll(/(background|color|border-color)\s*:\s*light-dark\(\s*([^,]+)\s*,\s*([^)]+)\s*\)/gi);
227
- for (const m of propMatches) {
228
- const prop = m[1].toLowerCase();
229
- const lightVal = m[2].trim();
230
- const hex = tryParseColor(lightVal) || namedColorToHex(lightVal);
231
- if (hex) {
232
- if (prop === 'background') {
233
- addColor(tokens, hex, 'css', 'light-bg');
234
- }
235
- else if (prop === 'color') {
236
- addColor(tokens, hex, 'css', 'light-text');
237
- }
238
- else {
239
- addColor(tokens, hex, 'css', 'light-default');
240
- }
241
- }
242
- }
243
- // Also detect color-scheme to understand if site is light or dark
244
- const colorSchemeMatch = content.match(/color-scheme\s*:\s*([^;}\n]+)/i);
245
- if (colorSchemeMatch) {
246
- const scheme = colorSchemeMatch[1].trim().toLowerCase();
247
- if (scheme.startsWith('light')) {
248
- tokens.cssVariables.push({
249
- name: '--color-scheme-default',
250
- value: 'light',
251
- property: 'color',
252
- });
253
- }
254
- }
255
- }
256
- /**
257
- * Detect color-scheme from CSS file content (not HTML).
258
- * Called per-CSS-file so we catch :root { color-scheme: light dark; } in stylesheets.
259
- * Guards against false positives from inside @media (prefers-color-scheme: dark) blocks.
260
- */
261
- function detectColorSchemeFromCSS(css, tokens) {
262
- // Already detected — skip
263
- if (tokens.cssVariables.some(v => v.name === '--color-scheme-default'))
264
- return;
265
- // Strip dark-mode media query blocks before searching, so we don't pick up
266
- // color-scheme: dark from inside @media (prefers-color-scheme: dark) { ... }
267
- const withoutDarkMedia = css.replace(/@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{[\s\S]*?\}\s*\}/g, '');
268
- const match = withoutDarkMedia.match(/color-scheme\s*:\s*([^;}\n]+)/i);
269
- if (!match)
270
- return;
271
- const scheme = match[1].trim().toLowerCase();
272
- // "light", "light dark", "only light" → all indicate light as primary
273
- if (scheme.startsWith('light') || scheme === 'only light') {
274
- tokens.cssVariables.push({
275
- name: '--color-scheme-default',
276
- value: 'light',
277
- property: 'color',
278
- });
279
- }
280
- }
281
- /**
282
- * Returns true if a font family name looks like an icon/symbol font.
283
- * These should be excluded from the design typography documentation.
284
- */
285
- function isIconFont(family) {
286
- return /^(apple\s*(icons?|legacy|sf\s*symbols?)|material\s*(icons?|symbols?)|font\s*awesome|fontawesome|glyphicons?|ionicons?|feather|remixicon|octicons?|bootstrap\s*icons?|hero\s*icons?|phosphor|tabler|lucide)\b/i.test(family)
287
- || /^apple\s*icons?\s*\d+/i.test(family)
288
- || /^apple\s*legacy\s*chevron/i.test(family);
289
- }
290
- /**
291
- * Returns true if a font family is a system font that cannot be downloaded.
292
- */
293
- function isSystemFont(family) {
294
- return /^(-apple-system|blinkmacsystemfont|system-ui|ui-sans-serif|ui-serif|ui-monospace|segoe\s*ui|\.sf\s*(pro|compact))/i.test(family);
295
- }
296
- /**
297
- * Returns true if a font family is an emoji font (not useful for UI design docs).
298
- */
299
- function isEmojiFont(family) {
300
- return /^(apple\s*color\s*emoji|noto\s*color\s*emoji|segoe\s*ui\s*emoji|android\s*emoji|twemoji|emoji)/i.test(family);
301
- }
302
- /**
303
- * From a CSS font-family value string (possibly with fallbacks),
304
- * extract the first real, non-generic, non-system, non-emoji font name.
305
- * Used to resolve --font-* CSS variable values.
306
- */
307
- function extractFirstRealFont(value) {
308
- const candidates = value.split(',').map(f => f.replace(/["']/g, '').trim());
309
- for (const family of candidates) {
310
- if (!family)
311
- continue;
312
- if (family.startsWith('var('))
313
- continue;
314
- if (isGenericFont(family))
315
- continue;
316
- if (isSystemFont(family))
317
- continue;
318
- if (isIconFont(family))
319
- continue;
320
- if (isEmojiFont(family))
321
- continue;
322
- if (!isValidFontName(family))
323
- continue;
324
- return family;
325
- }
326
- return null;
327
- }
328
- function extractMetaColors(html, tokens) {
329
- // <meta name="theme-color" content="#...">
330
- const themeColor = html.match(/<meta[^>]+name\s*=\s*["']theme-color["'][^>]+content\s*=\s*["']([^"']+)["']/i);
331
- if (themeColor) {
332
- const hex = tryParseColor(themeColor[1]);
333
- if (hex)
334
- addColor(tokens, hex, 'css', 'theme-color');
335
- }
336
- // <meta name="msapplication-TileColor">
337
- const tileColor = html.match(/<meta[^>]+name\s*=\s*["']msapplication-TileColor["'][^>]+content\s*=\s*["']([^"']+)["']/i);
338
- if (tileColor) {
339
- const hex = tryParseColor(tileColor[1]);
340
- if (hex)
341
- addColor(tokens, hex, 'css', 'tile-color');
342
- }
343
- // Favicon: <link rel="icon"> / <link rel="shortcut icon"> / <link rel="apple-touch-icon">
344
- if (!tokens.favicon) {
345
- const faviconPatterns = [
346
- /<link[^>]+rel\s*=\s*["'][^"']*\bicon\b[^"']*["'][^>]+href\s*=\s*["']([^"']+)["']/i,
347
- /<link[^>]+href\s*=\s*["']([^"']+)["'][^>]+rel\s*=\s*["'][^"']*\bicon\b[^"']*["']/i,
348
- ];
349
- for (const pat of faviconPatterns) {
350
- const m = html.match(pat);
351
- if (m && m[1]) {
352
- // Strip query strings from favicon URL — keep just the path/URL
353
- let faviconHref = m[1].split('?')[0].trim();
354
- // Prefer standard extensions
355
- if (/\.(ico|png|svg|jpg|webp)$/i.test(faviconHref)) {
356
- tokens.favicon = faviconHref;
357
- }
358
- else {
359
- tokens.favicon = faviconHref || '/favicon.ico';
360
- }
361
- break;
362
- }
363
- }
364
- // Default to /favicon.ico if none found
365
- if (!tokens.favicon)
366
- tokens.favicon = '/favicon.ico';
367
- }
368
- // Site title from <title> tag or og:title
369
- if (!tokens.siteTitle) {
370
- const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i);
371
- if (titleMatch)
372
- tokens.siteTitle = titleMatch[1].trim();
373
- }
374
- if (!tokens.siteTitle) {
375
- const ogTitle = html.match(/<meta[^>]+property\s*=\s*["']og:title["'][^>]+content\s*=\s*["']([^"']+)["']/i);
376
- if (ogTitle)
377
- tokens.siteTitle = ogTitle[1].trim();
378
- }
379
- }
380
- function extractFontLinks(html, tokens) {
381
- // Google Fonts: <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&...">
382
- const googleFontMatches = html.matchAll(/fonts\.googleapis\.com\/css2?\?[^"']*family=([^"'&]+)/gi);
383
- for (const m of googleFontMatches) {
384
- const families = decodeURIComponent(m[1]).split('|');
385
- for (const f of families) {
386
- const familyName = f.split(':')[0].replace(/\+/g, ' ').trim();
387
- if (familyName) {
388
- tokens.fonts.push({ family: familyName, source: 'css' });
389
- }
390
- }
391
- }
392
- // Detect font from @font-face in CSS (handled in parseCSS)
393
- // Also check for common font CDN patterns
394
- const fontCDNMatches = html.matchAll(/fonts\.[^"']+["']/gi);
395
- // Already handled above
396
- }
397
- function extractDOMStructure(html, tokens) {
398
- // Detect viewport meta for responsive
399
- const viewportMatch = html.match(/<meta[^>]+name\s*=\s*["']viewport["'][^>]+content\s*=\s*["']([^"']+)["']/i);
400
- if (viewportMatch && viewportMatch[1].includes('width=device-width')) {
401
- // Responsive site
402
- }
403
- // Detect page sections by semantic HTML tags and class patterns
404
- detectPageSections(html, tokens);
405
- }
406
- function extractSPAModules(html, tokens, allCssContent) {
407
- // Decode base64-encoded JS modules from importmap or inline scripts
408
- // These contain CSS classes, inline styles, and component structure
409
- const base64Matches = html.matchAll(/data:application\/javascript;base64,([A-Za-z0-9+/=]+)/g);
410
- for (const m of base64Matches) {
411
- try {
412
- const decoded = Buffer.from(m[1], 'base64').toString('utf-8');
413
- // Extract CSS-in-JS styles and class names
414
- extractColorsFromJSModule(decoded, tokens);
415
- extractClassNamesFromJS(decoded, tokens);
416
- // Also treat any inline CSS strings as CSS content
417
- const cssStrings = decoded.matchAll(/`([^`]*(?:background|color|font|padding|margin|border|display|flex)[^`]*)`/g);
418
- for (const cs of cssStrings) {
419
- allCssContent.push(cs[1]);
420
- }
421
- }
422
- catch {
423
- // Skip unparseable modules
424
- }
425
- }
426
- // Also check for esm.sh imports to detect frameworks
427
- if (/esm\.sh\/react@/i.test(html) || /from\s+["']react["']/i.test(html)) {
428
- // React SPA detected — fonts and components are rendered client-side
429
- }
430
- }
431
- function extractColorsFromJSModule(js, tokens) {
432
- // Extract hex colors from JS string literals
433
- const hexMatches = js.matchAll(/["'`](#[0-9a-fA-F]{3,8})["'`]/g);
434
- for (const m of hexMatches) {
435
- const hex = normalizeHex(m[1]);
436
- if (isValidHex(hex)) {
437
- addColor(tokens, hex, 'css');
438
- }
439
- }
440
- // Extract rgb/rgba from JS
441
- const rgbMatches = js.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g);
442
- for (const m of rgbMatches) {
443
- addColor(tokens, rgbToHex(parseInt(m[1]), parseInt(m[2]), parseInt(m[3])), 'css');
444
- }
445
- // Extract CSS variable definitions from JS template literals
446
- const cssVarMatches = js.matchAll(/(--[\w-]+)\s*:\s*([^;}\n"'`]+)/g);
447
- for (const m of cssVarMatches) {
448
- const name = m[1];
449
- const value = m[2].trim();
450
- if (!tokens.cssVariables.find(v => v.name === name)) {
451
- tokens.cssVariables.push({ name, value, property: guessPropertyType(name) });
452
- }
453
- if (isColorVarName(name)) {
454
- const hex = tryParseColor(value);
455
- if (hex)
456
- addColor(tokens, hex, 'css', name.replace(/^--/, ''));
457
- }
458
- }
459
- // Extract SVG colors (fill, stroke attributes)
460
- const svgColorMatches = js.matchAll(/(?:fill|stroke)\s*[:=]\s*["']([^"']+)["']/g);
461
- for (const m of svgColorMatches) {
462
- const val = m[1].trim();
463
- const hex = tryParseColor(val) || namedColorToHex(val);
464
- if (hex)
465
- addColor(tokens, hex, 'css');
466
- }
467
- }
468
- function extractClassNamesFromJS(js, tokens) {
469
- // Collect ALL Tailwind classes from className patterns
470
- const allClasses = [];
471
- const classNameMatches = js.matchAll(/className\s*[:=]\s*["'`]([^"'`]+)["'`]/g);
472
- for (const m of classNameMatches) {
473
- const classes = m[1].split(/\s+/).filter(Boolean);
474
- allClasses.push(...classes);
475
- }
476
- // Also from template literal className with interpolation
477
- const templateMatches = js.matchAll(/className\s*[:=]\s*`([^`]+)`/g);
478
- for (const m of templateMatches) {
479
- // Remove template expressions ${...} and split
480
- const cleaned = m[1].replace(/\$\{[^}]+\}/g, ' ');
481
- allClasses.push(...cleaned.split(/\s+/).filter(Boolean));
482
- }
483
- // Count class frequencies to determine primary patterns
484
- const classFreq = new Map();
485
- for (const cls of allClasses) {
486
- classFreq.set(cls, (classFreq.get(cls) || 0) + 1);
487
- }
488
- // ── Colors from Tailwind classes ──
489
- const twColorMap = {
490
- 'black': '#000000', 'white': '#ffffff',
491
- 'gray-100': '#f3f4f6', 'gray-200': '#e5e7eb', 'gray-300': '#d1d5db',
492
- 'gray-400': '#9ca3af', 'gray-500': '#6b7280', 'gray-600': '#4b5563',
493
- 'gray-700': '#374151', 'gray-800': '#1f2937', 'gray-900': '#111827',
494
- 'red-500': '#ef4444', 'red-600': '#dc2626',
495
- 'blue-500': '#3b82f6', 'green-500': '#22c55e',
496
- };
497
- for (const cls of allClasses) {
498
- // Tailwind arbitrary colors: bg-[#ACD8C9], text-[#ACD8C9], border-[#hex]
499
- // Also with opacity: bg-[#ACD8C9]/95
500
- const arbColorMatch = cls.match(/^(?:bg|text|border|fill|stroke)-\[#([0-9a-fA-F]{3,8})\]/);
501
- if (arbColorMatch) {
502
- const hex = normalizeHex('#' + arbColorMatch[1]);
503
- if (isValidHex(hex))
504
- addColor(tokens, hex, 'css');
505
- }
506
- // Named Tailwind colors: bg-black, text-white, text-black, border-black, bg-white
507
- const namedColorMatch = cls.match(/^(?:bg|text|border)-(black|white|gray-\d+|red-\d+|blue-\d+|green-\d+)(?:\/\d+)?$/);
508
- if (namedColorMatch && twColorMap[namedColorMatch[1]]) {
509
- addColor(tokens, twColorMap[namedColorMatch[1]], 'css');
510
- }
511
- // Arbitrary font family: font-['Custom Font']
512
- if (cls.startsWith('font-[') && cls.includes("'")) {
513
- const fontMatch = cls.match(/font-\[\s*'([^']+)'\s*\]/);
514
- if (fontMatch && !tokens.fonts.find(f => f.family === fontMatch[1])) {
515
- tokens.fonts.push({ family: fontMatch[1], source: 'css' });
516
- }
517
- }
518
- // Tailwind font classes: font-mono, font-sans, font-serif, font-doto (custom)
519
- const fontClassMatch = cls.match(/^font-(mono|sans|serif|doto|bold|black|medium|semibold|light|thin|extrabold)$/);
520
- if (fontClassMatch) {
521
- const fontType = fontClassMatch[1];
522
- if (fontType === 'mono' && !tokens.fonts.find(f => f.family === 'monospace')) {
523
- tokens.fonts.push({ family: 'monospace', source: 'css' });
524
- }
525
- // Custom font class like font-doto → Doto font
526
- if (!['mono', 'sans', 'serif', 'bold', 'black', 'medium', 'semibold', 'light', 'thin', 'extrabold'].includes(fontType)) {
527
- const capName = fontType.charAt(0).toUpperCase() + fontType.slice(1);
528
- if (!tokens.fonts.find(f => f.family.toLowerCase() === fontType)) {
529
- tokens.fonts.push({ family: capName, source: 'css' });
530
- }
531
- }
532
- }
533
- // Tailwind text sizes: text-xs, text-sm, text-[9px], text-[10px], text-2xl, text-4xl
534
- const textSizeMatch = cls.match(/^text-\[(\d+(?:px|rem))\]$/);
535
- if (textSizeMatch) {
536
- tokens.fonts.push({ family: '', size: textSizeMatch[1], source: 'css' });
537
- }
538
- const twSizeMap = {
539
- 'text-xs': '12px', 'text-sm': '14px', 'text-base': '16px',
540
- 'text-lg': '18px', 'text-xl': '20px', 'text-2xl': '24px',
541
- 'text-3xl': '30px', 'text-4xl': '36px', 'text-5xl': '48px',
542
- };
543
- if (twSizeMap[cls]) {
544
- tokens.fonts.push({ family: '', size: twSizeMap[cls], source: 'css' });
545
- }
546
- // Tailwind rounded-none → border-radius: 0px
547
- if (cls === 'rounded-none') {
548
- if (!tokens.borderRadii.includes('0px'))
549
- tokens.borderRadii.push('0px');
550
- }
551
- // Standard Tailwind rounded classes
552
- const twRadiusMap = {
553
- 'rounded-sm': '2px', 'rounded': '4px', 'rounded-md': '6px',
554
- 'rounded-lg': '8px', 'rounded-xl': '12px', 'rounded-2xl': '16px',
555
- 'rounded-3xl': '24px', 'rounded-full': '9999px',
556
- };
557
- if (twRadiusMap[cls] && !tokens.borderRadii.includes(twRadiusMap[cls])) {
558
- tokens.borderRadii.push(twRadiusMap[cls]);
559
- }
560
- // Tailwind arbitrary shadows: shadow-[2px_2px_0px_0px_rgba(0,0,0,1)]
561
- const shadowMatch = cls.match(/^shadow-\[(.+)\]$/);
562
- if (shadowMatch) {
563
- const shadowVal = shadowMatch[1].replace(/_/g, ' ');
564
- if (!tokens.shadows.find(s => s.value === shadowVal)) {
565
- tokens.shadows.push({ value: shadowVal });
566
- }
567
- }
568
- // Tailwind spacing: p-1, px-2, py-6, m-4, gap-3, space-y-4
569
- const spacingMatch = cls.match(/^(?:p|px|py|pt|pb|pl|pr|m|mx|my|mt|mb|ml|mr|gap|space-[xy])-(\d+(?:\.\d+)?)$/);
570
- if (spacingMatch) {
571
- const twUnit = parseFloat(spacingMatch[1]) * 4; // Tailwind spacing: 1 = 4px
572
- if (twUnit > 0 && twUnit <= 200)
573
- tokens.spacingValues.push(Math.round(twUnit));
574
- }
575
- // Arbitrary spacing: p-[40px], px-10
576
- const arbSpacingMatch = cls.match(/^(?:p|px|py|m|mx|my|gap)-\[(\d+)px\]$/);
577
- if (arbSpacingMatch) {
578
- const px = parseInt(arbSpacingMatch[1]);
579
- if (px > 0 && px <= 200)
580
- tokens.spacingValues.push(px);
581
- }
582
- }
583
- // ── Detect font dominance ──
584
- // Count font-mono vs font-sans occurrences to determine primary font style
585
- const monoCount = classFreq.get('font-mono') || 0;
586
- const sansCount = classFreq.get('font-sans') || 0;
587
- if (monoCount > sansCount && monoCount >= 3) {
588
- // Monospace is the primary UI font — increase its frequency
589
- const monoFont = tokens.fonts.find(f => f.family === 'monospace');
590
- if (monoFont) {
591
- // Already added
592
- }
593
- else {
594
- tokens.fonts.push({ family: 'monospace', source: 'css' });
595
- }
596
- // Add extra frequency entries to make monospace the primary
597
- for (let i = 0; i < monoCount; i++) {
598
- tokens.fonts.push({ family: 'monospace', source: 'css' });
599
- }
600
- }
601
- // ── Detect uppercase/tracking patterns ──
602
- const uppercaseCount = classFreq.get('uppercase') || 0;
603
- const trackingCount = (classFreq.get('tracking-widest') || 0) + (classFreq.get('tracking-wider') || 0) + (classFreq.get('tracking-tight') || 0);
604
- // If heavy uppercase usage, this indicates a brutalist/terminal style
605
- // We'll capture this in antiPatterns or similar
606
- if (uppercaseCount >= 5) {
607
- // Uppercase-heavy UI — this is a style signal
608
- }
609
- // Also extract font-family from inline style objects in JSX
610
- const fontFamilyMatches = js.matchAll(/fontFamily\s*:\s*["'`]([^"'`]+)["'`]/g);
611
- for (const m of fontFamilyMatches) {
612
- const family = m[1].split(',')[0].replace(/["']/g, '').trim();
613
- if (family && family.length > 1 && family.length < 50 && !isGenericFont(family)) {
614
- if (!tokens.fonts.find(f => f.family === family)) {
615
- tokens.fonts.push({ family, source: 'css' });
616
- }
617
- }
618
- }
619
- // Extract border-radius from inline style objects
620
- const radiusMatches = js.matchAll(/borderRadius\s*:\s*["'`]?(\d+(?:px|rem|%)?)/g);
621
- for (const m of radiusMatches) {
622
- const val = m[1].includes('px') || m[1].includes('rem') || m[1].includes('%') ? m[1] : m[1] + 'px';
623
- if (!tokens.borderRadii.includes(val))
624
- tokens.borderRadii.push(val);
625
- }
626
- // Extract spacing from inline style objects
627
- const spacingProps = ['padding', 'margin', 'gap', 'paddingTop', 'paddingBottom', 'paddingLeft', 'paddingRight', 'marginTop', 'marginBottom'];
628
- for (const prop of spacingProps) {
629
- const regex = new RegExp(`${prop}\\s*:\\s*["'\`]?(\\d+(?:px)?)`, 'g');
630
- const matches = js.matchAll(regex);
631
- for (const m of matches) {
632
- const px = parseInt(m[1]);
633
- if (px > 0 && px <= 200)
634
- tokens.spacingValues.push(px);
635
- }
636
- }
637
- }
638
- function detectPageSections(html, tokens) {
639
- // Navigation
640
- const navMatch = html.match(/<nav[^>]*class\s*=\s*["']([^"']*)["'][^>]*>/i);
641
- if (navMatch || /<nav[\s>]/i.test(html)) {
642
- tokens.pageSections.push({
643
- type: 'navigation',
644
- tag: 'nav',
645
- classes: navMatch ? navMatch[1].split(/\s+/).filter(Boolean) : [],
646
- childCount: countChildLinks(html, 'nav'),
647
- description: 'Top navigation bar',
648
- });
649
- }
650
- // Hero section — look for common class patterns near the top
651
- const heroPatterns = [
652
- /class\s*=\s*["'][^"']*\b(hero|banner|jumbotron|masthead)\b[^"']*["']/i,
653
- ];
654
- for (const pattern of heroPatterns) {
655
- const heroMatch = html.match(pattern);
656
- if (heroMatch) {
657
- tokens.pageSections.push({
658
- type: 'hero',
659
- tag: 'section',
660
- classes: [heroMatch[1]],
661
- childCount: 0,
662
- description: 'Hero/banner section with headline and CTAs',
663
- });
664
- break;
665
- }
666
- }
667
- // If no explicit hero class, detect by structure: first large heading
668
- if (!tokens.pageSections.find(s => s.type === 'hero')) {
669
- const hasLargeHeading = /<(h1|h2)[^>]*>[\s\S]{5,}<\/(h1|h2)>/i.test(html);
670
- if (hasLargeHeading) {
671
- tokens.pageSections.push({
672
- type: 'hero',
673
- tag: 'section',
674
- classes: [],
675
- childCount: 0,
676
- description: 'Hero section (detected from heading structure)',
677
- });
678
- }
679
- }
680
- // Features/cards section
681
- const featurePatterns = /class\s*=\s*["'][^"']*\b(features?|benefits?|cards?-grid|card-container)\b[^"']*["']/i;
682
- const featureMatch = html.match(featurePatterns);
683
- if (featureMatch) {
684
- tokens.pageSections.push({
685
- type: 'features',
686
- tag: 'section',
687
- classes: [featureMatch[1]],
688
- childCount: countRepeatedElements(html, 'card'),
689
- description: 'Feature/benefit cards grid',
690
- });
691
- }
692
- // FAQ section
693
- const faqMatch = html.match(/class\s*=\s*["'][^"']*\b(faq|accordion|questions)\b[^"']*["']/i);
694
- if (faqMatch || /FAQ|Frequently Asked/i.test(html)) {
695
- tokens.pageSections.push({
696
- type: 'faq',
697
- tag: 'section',
698
- classes: faqMatch ? [faqMatch[1]] : ['faq'],
699
- childCount: 0,
700
- description: 'FAQ/accordion section',
701
- });
702
- }
703
- // Footer
704
- const footerMatch = html.match(/<footer[^>]*class\s*=\s*["']([^"']*)["'][^>]*>/i);
705
- if (footerMatch || /<footer[\s>]/i.test(html)) {
706
- tokens.pageSections.push({
707
- type: 'footer',
708
- tag: 'footer',
709
- classes: footerMatch ? footerMatch[1].split(/\s+/).filter(Boolean) : [],
710
- childCount: countChildLinks(html, 'footer'),
711
- description: 'Page footer with links and info',
712
- });
713
- }
714
- // CTA sections
715
- const ctaMatch = html.match(/class\s*=\s*["'][^"']*\b(cta|call-to-action|signup)\b[^"']*["']/i);
716
- if (ctaMatch) {
717
- tokens.pageSections.push({
718
- type: 'cta',
719
- tag: 'section',
720
- classes: [ctaMatch[1]],
721
- childCount: 0,
722
- description: 'Call-to-action section',
723
- });
724
- }
725
- // Stats section
726
- const statsMatch = html.match(/class\s*=\s*["'][^"']*\b(stats|metrics|numbers|counters)\b[^"']*["']/i);
727
- if (statsMatch) {
728
- tokens.pageSections.push({
729
- type: 'stats',
730
- tag: 'section',
731
- classes: [statsMatch[1]],
732
- childCount: 0,
733
- description: 'Statistics/metrics display',
734
- });
735
- }
736
- // Testimonials
737
- const testimonialMatch = html.match(/class\s*=\s*["'][^"']*\b(testimonials?|reviews?|quotes?)\b[^"']*["']/i);
738
- if (testimonialMatch) {
739
- tokens.pageSections.push({
740
- type: 'testimonials',
741
- tag: 'section',
742
- classes: [testimonialMatch[1]],
743
- childCount: 0,
744
- description: 'Testimonials/reviews section',
745
- });
746
- }
747
- // Detect card patterns anywhere (repeated elements with "card" in class)
748
- if (!tokens.pageSections.find(s => s.type === 'cards' || s.type === 'features')) {
749
- const cardCount = countRepeatedElements(html, 'card');
750
- if (cardCount >= 3) {
751
- tokens.pageSections.push({
752
- type: 'cards',
753
- tag: 'div',
754
- classes: ['card'],
755
- childCount: cardCount,
756
- description: `Grid of ${cardCount} card elements`,
757
- });
758
- }
759
- }
760
- }
761
- function countChildLinks(html, tag) {
762
- const tagMatch = html.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i'));
763
- if (!tagMatch)
764
- return 0;
765
- const links = tagMatch[1].match(/<a\s/gi);
766
- return links ? links.length : 0;
767
- }
768
- function countRepeatedElements(html, classPattern) {
769
- const regex = new RegExp(`class\\s*=\\s*["'][^"']*\\b${classPattern}\\b`, 'gi');
770
- const matches = html.match(regex);
771
- return matches ? matches.length : 0;
772
- }
773
- function extractTransitionParts(value, tokens) {
774
- // Parse durations from transition shorthand (e.g., "all 150ms ease", "opacity 0.3s ease-out")
775
- const durationMatches = value.matchAll(/([\d.]+)(ms|s)\b/g);
776
- for (const m of durationMatches) {
777
- let dur = m[2] === 's' ? `${parseFloat(m[1]) * 1000}ms` : `${m[1]}ms`;
778
- if (!tokens.transitionDurations.includes(dur)) {
779
- tokens.transitionDurations.push(dur);
780
- }
781
- }
782
- // Parse easings
783
- const easingPatterns = [
784
- /\b(ease-in-out|ease-in|ease-out|ease|linear)\b/g,
785
- /cubic-bezier\([^)]+\)/g,
786
- ];
787
- for (const pattern of easingPatterns) {
788
- const matches = value.matchAll(pattern);
789
- for (const m of matches) {
790
- const easing = m[0];
791
- if (!tokens.transitionEasings.includes(easing)) {
792
- tokens.transitionEasings.push(easing);
793
- }
794
- }
795
- }
796
- }
797
- function extractPageLinks(html, baseUrl, origin) {
798
- const links = [];
799
- const hrefMatches = html.matchAll(/<a[^>]+href\s*=\s*["']([^"'#]+)["']/gi);
800
- for (const m of hrefMatches) {
801
- try {
802
- const resolved = new URL(m[1], baseUrl).href;
803
- if (resolved.startsWith(origin) && !resolved.includes('#') && !links.includes(resolved)) {
804
- links.push(resolved);
805
- }
806
- }
807
- catch {
808
- // Invalid URL
809
- }
810
- }
811
- return links;
812
- }
813
- // ── CSS Parsing ───────────────────────────────────────────────────────
814
- function parseCSS(content, tokens, cssBaseUrl) {
815
- // First, extract dark mode blocks via regex (before AST parsing may fail)
816
- extractDarkModeBlocks(content, tokens);
817
- // Detect color-scheme in CSS (e.g. :root { color-scheme: light dark; })
818
- detectColorSchemeFromCSS(content, tokens);
819
- // Extract @keyframes
820
- extractKeyframes(content, tokens);
821
- // Extract @font-face (with URL resolution)
822
- extractFontFace(content, tokens, cssBaseUrl);
823
- // Extract @media breakpoints
824
- extractMediaBreakpoints(content, tokens);
825
- // Extract z-index
826
- extractZIndex(content, tokens);
827
- // Extract container widths
828
- extractContainerWidth(content, tokens);
829
- // AST-based parsing
830
- try {
831
- const ast = csstree.parse(content, {
832
- parseCustomProperty: true,
833
- parseAtrulePrelude: true,
834
- });
835
- csstree.walk(ast, {
836
- visit: 'Declaration',
837
- enter(node) {
838
- const prop = node.property;
839
- const rawValue = csstree.generate(node.value);
840
- // CSS custom properties
841
- if (prop.startsWith('--')) {
842
- const trimmedValue = rawValue.trim();
843
- tokens.cssVariables.push({
844
- name: prop,
845
- value: trimmedValue,
846
- property: guessPropertyType(prop),
847
- });
848
- // Extract color from variable value
849
- if (isColorVarName(prop)) {
850
- const hex = tryParseColor(trimmedValue);
851
- if (hex) {
852
- const name = prop.replace(/^--/, '');
853
- addColor(tokens, hex, 'css', name);
854
- }
855
- }
856
- // Populate fontVarMap: --font-xxx, --font-sans, --font-mono, --default-font-family
857
- // e.g. --font-sans: 'delight' → fontVarMap['--font-sans'] = 'delight'
858
- if (/^--(font|default[-_]font)/i.test(prop)) {
859
- // Extract first real font from the value (skip generics/system/emoji)
860
- const fontVal = extractFirstRealFont(trimmedValue);
861
- if (fontVal) {
862
- tokens.fontVarMap[prop] = fontVal;
863
- }
864
- }
865
- }
866
- // Direct color properties
867
- if (isColorProperty(prop)) {
868
- extractColorsFromValue(rawValue, tokens);
869
- }
870
- // Font family — walk all comma-separated families, resolve vars, skip system/icon/emoji/generic
871
- if (prop === 'font-family') {
872
- const candidates = rawValue.split(',').map(f => f.replace(/["']/g, '').trim());
873
- for (let family of candidates) {
874
- if (!family)
875
- continue;
876
- // Resolve CSS variable reference: var(--font-delight) → 'delight'
877
- if (family.startsWith('var(')) {
878
- const varName = family.replace(/^var\(\s*/, '').replace(/\s*(?:,[^)]+)?\)$/, '').trim();
879
- const resolved = tokens.fontVarMap[varName] || tokens.fontVarMap[family];
880
- if (resolved) {
881
- family = resolved;
882
- }
883
- else {
884
- continue; // can't resolve yet — skip
885
- }
886
- }
887
- if (isGenericFont(family))
888
- continue;
889
- if (isSystemFont(family))
890
- continue;
891
- if (isIconFont(family))
892
- continue;
893
- if (isEmojiFont(family))
894
- continue;
895
- if (!isValidFontName(family))
896
- continue;
897
- if (!tokens.fonts.find(f => f.family === family)) {
898
- tokens.fonts.push({ family, source: 'css' });
899
- }
900
- break; // use the first real (non-system) font in the stack
901
- }
902
- }
903
- // Font size
904
- if (prop === 'font-size') {
905
- const size = rawValue.trim();
906
- const existingNoSize = tokens.fonts.find(f => !f.size);
907
- if (existingNoSize) {
908
- existingNoSize.size = size;
909
- }
910
- else {
911
- tokens.fonts.push({ family: '', size, source: 'css' });
912
- }
913
- }
914
- // Font weight
915
- if (prop === 'font-weight') {
916
- const existingNoWeight = tokens.fonts.find(f => !f.weight);
917
- if (existingNoWeight) {
918
- existingNoWeight.weight = rawValue.trim();
919
- }
920
- }
921
- // Spacing
922
- if (isSpacingProperty(prop)) {
923
- extractSpacingValues(rawValue, tokens);
924
- }
925
- // Box shadow
926
- if (prop === 'box-shadow' && rawValue.trim() !== 'none') {
927
- if (!tokens.shadows.find(s => s.value === rawValue.trim())) {
928
- tokens.shadows.push({ value: rawValue.trim() });
929
- }
930
- }
931
- // Border radius — only collect single-value radii, skip CSS shorthand like "0 0 18px 18px"
932
- if (prop === 'border-radius' || (prop.startsWith('border-') && prop.endsWith('-radius'))) {
933
- const val = rawValue.trim();
934
- // Skip shorthand values (contain spaces between lengths) and slash syntax
935
- if (!val.includes(' ') && !val.includes('/')) {
936
- if (!tokens.borderRadii.includes(val)) {
937
- tokens.borderRadii.push(val);
938
- }
939
- }
940
- }
941
- // Gradients
942
- if (rawValue.includes('gradient(')) {
943
- tokens.gradients.push(rawValue.trim());
944
- }
945
- // Transitions
946
- if (prop === 'transition') {
947
- tokens.animations.push({
948
- name: 'css-transition',
949
- type: 'css-transition',
950
- value: rawValue.trim(),
951
- source: 'css',
952
- });
953
- // Parse durations and easings from shorthand
954
- extractTransitionParts(rawValue, tokens);
955
- }
956
- // Transition sub-properties (skip var() references — normalizer will handle)
957
- if (prop === 'transition-duration' || prop === 'animation-duration') {
958
- const dur = rawValue.trim();
959
- if (dur && !dur.includes('var(') && /^[\d.]+m?s$/.test(dur) && !tokens.transitionDurations.includes(dur)) {
960
- tokens.transitionDurations.push(dur);
961
- }
962
- }
963
- if (prop === 'transition-timing-function' || prop === 'animation-timing-function') {
964
- const ease = rawValue.trim();
965
- if (ease && !ease.includes('var(') && !tokens.transitionEasings.includes(ease)) {
966
- tokens.transitionEasings.push(ease);
967
- }
968
- }
969
- },
970
- });
971
- }
972
- catch {
973
- // Fallback: regex-based extraction for files css-tree can't parse
974
- extractWithRegex(content, tokens);
975
- }
976
- }
977
- function extractDarkModeBlocks(content, tokens) {
978
- const rootVars = new Map();
979
- const darkVars = new Map();
980
- const rootBlocks = content.matchAll(/:root\s*\{([^}]+)\}/g);
981
- for (const m of rootBlocks) {
982
- const vars = m[1].matchAll(/(--[\w-]+)\s*:\s*([^;}\n]+)/g);
983
- for (const v of vars) {
984
- rootVars.set(v[1], v[2].trim());
985
- }
986
- }
987
- const darkPatterns = [
988
- /\.dark\s*\{([^}]+)\}/g,
989
- /\[data-theme\s*=\s*["']dark["']\]\s*\{([^}]+)\}/g,
990
- /\.dark\s+:root\s*\{([^}]+)\}/g,
991
- /:root\.dark\s*\{([^}]+)\}/g,
992
- /@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{[^{]*:root\s*\{([^}]+)\}/g,
993
- /@media\s*\(\s*prefers-color-scheme\s*:\s*dark\s*\)\s*\{([^}]+)\}/g,
994
- ];
995
- for (const pattern of darkPatterns) {
996
- const matches = content.matchAll(pattern);
997
- for (const m of matches) {
998
- const vars = m[1].matchAll(/(--[\w-]+)\s*:\s*([^;}\n]+)/g);
999
- for (const v of vars) {
1000
- darkVars.set(v[1], v[2].trim());
1001
- }
1002
- }
1003
- }
1004
- for (const [name, lightVal] of rootVars) {
1005
- const darkVal = darkVars.get(name);
1006
- if (darkVal && darkVal !== lightVal) {
1007
- if (!tokens.darkModeVars.find(d => d.variable === name)) {
1008
- tokens.darkModeVars.push({
1009
- variable: name,
1010
- lightValue: lightVal,
1011
- darkValue: darkVal,
1012
- });
1013
- }
1014
- }
1015
- }
1016
- }
1017
- function extractKeyframes(content, tokens) {
1018
- const keyframeMatches = content.matchAll(/@keyframes\s+([\w-]+)\s*\{([^}]*(?:\{[^}]*\}[^}]*)*)\}/g);
1019
- for (const m of keyframeMatches) {
1020
- if (!tokens.animations.find(a => a.name === m[1])) {
1021
- tokens.animations.push({
1022
- name: m[1],
1023
- type: 'css-keyframe',
1024
- value: m[2].trim().slice(0, 200),
1025
- source: 'css',
1026
- });
1027
- }
1028
- }
1029
- }
1030
- function extractFontFace(content, tokens, cssBaseUrl) {
1031
- const fontFaceMatches = content.matchAll(/@font-face\s*\{([^}]+)\}/g);
1032
- for (const m of fontFaceMatches) {
1033
- const familyMatch = m[1].match(/font-family\s*:\s*["']?([^"';,]+)["']?/);
1034
- const weightMatch = m[1].match(/font-weight\s*:\s*(\d+)/);
1035
- if (familyMatch) {
1036
- const family = familyMatch[1].trim();
1037
- if (!isValidFontName(family))
1038
- continue;
1039
- // Skip icon fonts — they pollute typography documentation
1040
- if (isIconFont(family))
1041
- continue;
1042
- // Skip system fonts — they can't be downloaded and are implicit
1043
- if (isSystemFont(family))
1044
- continue;
1045
- if (!tokens.fonts.find(f => f.family === family)) {
1046
- tokens.fonts.push({
1047
- family,
1048
- weight: weightMatch ? weightMatch[1] : undefined,
1049
- source: 'css',
1050
- });
1051
- }
1052
- // Extract font source URLs — skip icon/symbol font sources
1053
- const srcMatches = m[1].matchAll(/url\(\s*["']?([^"')]+)["']?\s*\)/g);
1054
- for (const srcMatch of srcMatches) {
1055
- let srcUrl = srcMatch[1].trim();
1056
- // Resolve relative URLs to absolute
1057
- if (cssBaseUrl && !srcUrl.startsWith('http') && !srcUrl.startsWith('data:')) {
1058
- srcUrl = resolveUrl(srcUrl, cssBaseUrl);
1059
- }
1060
- // Detect format
1061
- const formatMatch = m[1].match(new RegExp(`url\\([^)]*${escapeRegex(srcUrl)}[^)]*\\)\\s*format\\(\\s*["']?([^"')]+)["']?\\s*\\)`));
1062
- const format = formatMatch ? formatMatch[1] : guessFormatFromUrl(srcUrl);
1063
- if (!tokens.fontSources.find(fs => fs.family === family && fs.src === srcUrl)) {
1064
- tokens.fontSources.push({
1065
- family,
1066
- src: srcUrl,
1067
- format,
1068
- weight: weightMatch ? weightMatch[1] : undefined,
1069
- });
1070
- }
1071
- }
1072
- }
1073
- }
1074
- }
1075
- function escapeRegex(s) {
1076
- return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1077
- }
1078
- function guessFormatFromUrl(url) {
1079
- if (url.endsWith('.woff2'))
1080
- return 'woff2';
1081
- if (url.endsWith('.woff'))
1082
- return 'woff';
1083
- if (url.endsWith('.ttf'))
1084
- return 'truetype';
1085
- if (url.endsWith('.otf'))
1086
- return 'opentype';
1087
- if (url.endsWith('.eot'))
1088
- return 'embedded-opentype';
1089
- return undefined;
1090
- }
1091
- function extractMediaBreakpoints(content, tokens) {
1092
- const mediaMatches = content.matchAll(/@media[^{]*\(\s*min-width\s*:\s*([\d.]+)(px|em|rem)\s*\)/g);
1093
- for (const m of mediaMatches) {
1094
- const value = `${m[1]}${m[2]}`;
1095
- if (!tokens.breakpoints.find(b => b.value === value)) {
1096
- tokens.breakpoints.push({
1097
- name: nameBreakpoint(parseFloat(m[1]), m[2]),
1098
- value,
1099
- source: 'css',
1100
- });
1101
- }
1102
- }
1103
- // Also max-width breakpoints
1104
- const maxMediaMatches = content.matchAll(/@media[^{]*\(\s*max-width\s*:\s*([\d.]+)(px|em|rem)\s*\)/g);
1105
- for (const m of maxMediaMatches) {
1106
- const value = `${m[1]}${m[2]}`;
1107
- if (!tokens.breakpoints.find(b => b.value === value)) {
1108
- tokens.breakpoints.push({
1109
- name: nameBreakpoint(parseFloat(m[1]), m[2]),
1110
- value,
1111
- source: 'css',
1112
- });
1113
- }
1114
- }
1115
- }
1116
- function nameBreakpoint(px, unit) {
1117
- if (unit === 'em' || unit === 'rem')
1118
- px *= 16;
1119
- if (px <= 480)
1120
- return 'xs';
1121
- if (px <= 640)
1122
- return 'sm';
1123
- if (px <= 768)
1124
- return 'md';
1125
- if (px <= 1024)
1126
- return 'lg';
1127
- if (px <= 1280)
1128
- return 'xl';
1129
- return '2xl';
1130
- }
1131
- function extractZIndex(content, tokens) {
1132
- const zMatches = content.matchAll(/z-index\s*:\s*(\d+)/g);
1133
- for (const m of zMatches) {
1134
- const val = parseInt(m[1]);
1135
- if (!tokens.zIndexValues.includes(val)) {
1136
- tokens.zIndexValues.push(val);
1137
- }
1138
- }
1139
- }
1140
- function extractContainerWidth(content, tokens) {
1141
- const maxWidthMatches = content.matchAll(/max-width\s*:\s*([\d.]+)(px|rem|em|%)/g);
1142
- for (const m of maxWidthMatches) {
1143
- let px = parseFloat(m[1]);
1144
- if (m[2] === 'rem' || m[2] === 'em')
1145
- px *= 16;
1146
- if (px >= 960 && px <= 1600) {
1147
- tokens.containerMaxWidth = `${m[1]}${m[2]}`;
1148
- }
1149
- }
1150
- }
1151
- function extractWithRegex(content, tokens) {
1152
- // Hex colors
1153
- const hexMatches = content.matchAll(/#([0-9a-fA-F]{3,8})\b/g);
1154
- for (const m of hexMatches) {
1155
- addColor(tokens, normalizeHex(m[0]), 'css');
1156
- }
1157
- // RGB/RGBA
1158
- const rgbMatches = content.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g);
1159
- for (const m of rgbMatches) {
1160
- addColor(tokens, rgbToHex(parseInt(m[1]), parseInt(m[2]), parseInt(m[3])), 'css');
1161
- }
1162
- // HSL
1163
- const hslMatches = content.matchAll(/hsla?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%/g);
1164
- for (const m of hslMatches) {
1165
- addColor(tokens, hslToHex(parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3])), 'css');
1166
- }
1167
- // CSS variables
1168
- const varMatches = content.matchAll(/(--[\w-]+)\s*:\s*([^;}\n]+)/g);
1169
- for (const m of varMatches) {
1170
- tokens.cssVariables.push({
1171
- name: m[1],
1172
- value: m[2].trim(),
1173
- property: guessPropertyType(m[1]),
1174
- });
1175
- if (isColorVarName(m[1])) {
1176
- const hex = tryParseColor(m[2].trim());
1177
- if (hex)
1178
- addColor(tokens, hex, 'css', m[1].replace(/^--/, ''));
1179
- }
1180
- }
1181
- // Font families
1182
- const fontMatches = content.matchAll(/font-family\s*:\s*([^;}\n]+)/g);
1183
- for (const m of fontMatches) {
1184
- const family = m[1].replace(/["']/g, '').split(',')[0].trim();
1185
- if (family && !isGenericFont(family) && isValidFontName(family) && !tokens.fonts.find(f => f.family === family)) {
1186
- tokens.fonts.push({ family, source: 'css' });
1187
- }
1188
- }
1189
- // Box shadows
1190
- const shadowMatches = content.matchAll(/box-shadow\s*:\s*([^;}\n]+)/g);
1191
- for (const m of shadowMatches) {
1192
- const val = m[1].trim();
1193
- if (val !== 'none' && !tokens.shadows.find(s => s.value === val)) {
1194
- tokens.shadows.push({ value: val });
1195
- }
1196
- }
1197
- // Border radius
1198
- const radiusMatches = content.matchAll(/border-radius\s*:\s*([^;}\n]+)/g);
1199
- for (const m of radiusMatches) {
1200
- const val = m[1].trim();
1201
- if (!tokens.borderRadii.includes(val)) {
1202
- tokens.borderRadii.push(val);
1203
- }
1204
- }
1205
- }
1206
- function extractColorsFromValue(value, tokens) {
1207
- const hexMatches = value.matchAll(/#([0-9a-fA-F]{3,8})\b/g);
1208
- for (const m of hexMatches) {
1209
- addColor(tokens, normalizeHex(m[0]), 'css');
1210
- }
1211
- const rgbMatches = value.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g);
1212
- for (const m of rgbMatches) {
1213
- addColor(tokens, rgbToHex(parseInt(m[1]), parseInt(m[2]), parseInt(m[3])), 'css');
1214
- }
1215
- }
1216
- function extractSpacingValues(value, tokens) {
1217
- const nums = value.matchAll(/([\d.]+)(px|rem|em)/g);
1218
- for (const m of nums) {
1219
- let px = parseFloat(m[1]);
1220
- if (m[2] === 'rem' || m[2] === 'em')
1221
- px *= 16;
1222
- if (px > 0 && px <= 200) {
1223
- tokens.spacingValues.push(Math.round(px));
1224
- }
1225
- }
1226
- }
1227
- // ── HTML Component Detection ──────────────────────────────────────────
1228
- function detectHTMLComponents(html, css) {
1229
- const components = [];
1230
- const emptyTailwind = { backgrounds: [], borders: [], spacing: [], typography: [], effects: [], layout: [], interactive: [] };
1231
- // Build a CSS rule lookup: class name → style declarations
1232
- const cssRules = buildCSSRuleLookup(css);
1233
- // 1. Buttons — detect from HTML tags, JSX, and class patterns
1234
- const buttonPatterns = [
1235
- /<button[^>]*class\s*=\s*["']([^"']*)["'][^>]*>([\s\S]*?)<\/button>/gi,
1236
- /<a[^>]*class\s*=\s*["'][^"']*\b(btn|button|cta)\b[^"']*["'][^>]*>/gi,
1237
- ];
1238
- const buttonClasses = new Set();
1239
- for (const pattern of buttonPatterns) {
1240
- const matches = html.matchAll(pattern);
1241
- for (const m of matches) {
1242
- const classes = (m[1] || '').split(/\s+/).filter(Boolean);
1243
- classes.forEach(c => buttonClasses.add(c));
1244
- }
1245
- }
1246
- // Also detect JSX button patterns: _jsx("button", ...) or createElement("button", ...)
1247
- const jsxButtonPattern = /(?:_jsx|createElement)\s*\(\s*["']button["']/gi;
1248
- const hasJSXButtons = jsxButtonPattern.test(html);
1249
- if (buttonClasses.size > 0 || /<button[\s>]/i.test(html) || hasJSXButtons) {
1250
- const btnStyles = extractMatchingStyles(cssRules, buttonClasses, ['btn', 'button', 'cta']);
1251
- components.push({
1252
- name: 'Button',
1253
- filePath: 'html',
1254
- variants: findVariants(buttonClasses, 'btn'),
1255
- cssClasses: [...buttonClasses].slice(0, 10),
1256
- jsxSnippet: '',
1257
- props: [],
1258
- category: 'data-input',
1259
- hasAnimation: btnStyles.includes('transition'),
1260
- animationDetails: [],
1261
- statePatterns: [],
1262
- tailwindPatterns: emptyTailwind,
1263
- });
1264
- }
1265
- // 2. Inputs
1266
- const hasInputs = /<input[^>]*type\s*=\s*["'](text|email|password|search|url|tel|number)["']/i.test(html)
1267
- || /<textarea/i.test(html)
1268
- || /<select/i.test(html)
1269
- || /(?:_jsx|createElement)\s*\(\s*["']input["']/i.test(html)
1270
- || /(?:_jsx|createElement)\s*\(\s*["']textarea["']/i.test(html);
1271
- if (hasInputs) {
1272
- const inputClasses = extractElementClasses(html, 'input');
1273
- const inputStyles = extractMatchingStyles(cssRules, inputClasses, ['input', 'field', 'form']);
1274
- components.push({
1275
- name: 'Input',
1276
- filePath: 'html',
1277
- variants: [],
1278
- cssClasses: [...inputClasses].slice(0, 10),
1279
- jsxSnippet: '',
1280
- props: [],
1281
- category: 'data-input',
1282
- hasAnimation: false,
1283
- animationDetails: [],
1284
- statePatterns: [':focus', ':placeholder'],
1285
- tailwindPatterns: emptyTailwind,
1286
- });
1287
- }
1288
- // 3. Cards
1289
- const cardClasses = extractClassesByPattern(html, /\bcard\b/i);
1290
- if (cardClasses.size >= 2) {
1291
- components.push({
1292
- name: 'Card',
1293
- filePath: 'html',
1294
- variants: findVariants(cardClasses, 'card'),
1295
- cssClasses: [...cardClasses].slice(0, 10),
1296
- jsxSnippet: '',
1297
- props: [],
1298
- category: 'data-display',
1299
- hasAnimation: false,
1300
- animationDetails: [],
1301
- statePatterns: [],
1302
- tailwindPatterns: emptyTailwind,
1303
- });
1304
- }
1305
- // 4. Navigation / Header
1306
- if (/<nav[\s>]/i.test(html) || /<header[\s>]/i.test(html) || /(?:_jsx|createElement)\s*\(\s*["']nav["']/i.test(html)) {
1307
- const navClasses = new Set();
1308
- extractElementClasses(html, 'nav').forEach(c => navClasses.add(c));
1309
- extractElementClasses(html, 'header').forEach(c => navClasses.add(c));
1310
- const linkCount = countChildLinks(html, 'nav') || countChildLinks(html, 'header');
1311
- components.push({
1312
- name: 'Navigation',
1313
- filePath: 'html',
1314
- variants: [],
1315
- cssClasses: [...navClasses].slice(0, 10),
1316
- jsxSnippet: '',
1317
- props: [],
1318
- category: 'navigation',
1319
- hasAnimation: false,
1320
- animationDetails: [],
1321
- statePatterns: [],
1322
- tailwindPatterns: emptyTailwind,
1323
- });
1324
- }
1325
- // 5. Chips / Tags / Badges
1326
- const chipClasses = extractClassesByPattern(html, /\b(chip|tag|badge|label|pill)\b/i);
1327
- if (chipClasses.size > 0) {
1328
- components.push({
1329
- name: 'Badge',
1330
- filePath: 'html',
1331
- variants: [],
1332
- cssClasses: [...chipClasses].slice(0, 10),
1333
- jsxSnippet: '',
1334
- props: [],
1335
- category: 'data-display',
1336
- hasAnimation: false,
1337
- animationDetails: [],
1338
- statePatterns: [],
1339
- tailwindPatterns: emptyTailwind,
1340
- });
1341
- }
1342
- // 6. Modal / Dialog
1343
- const modalClasses = extractClassesByPattern(html, /\b(modal|dialog|drawer|overlay|popup)\b/i);
1344
- if (modalClasses.size > 0) {
1345
- components.push({
1346
- name: 'Modal',
1347
- filePath: 'html',
1348
- variants: [],
1349
- cssClasses: [...modalClasses].slice(0, 10),
1350
- jsxSnippet: '',
1351
- props: [],
1352
- category: 'overlay',
1353
- hasAnimation: false,
1354
- animationDetails: [],
1355
- statePatterns: [],
1356
- tailwindPatterns: emptyTailwind,
1357
- });
1358
- }
1359
- // 7. Footer
1360
- if (/<footer[\s>]/i.test(html) || /(?:_jsx|createElement)\s*\(\s*["']footer["']/i.test(html)) {
1361
- const footerClasses = extractElementClasses(html, 'footer');
1362
- components.push({
1363
- name: 'Footer',
1364
- filePath: 'html',
1365
- variants: [],
1366
- cssClasses: [...footerClasses].slice(0, 10),
1367
- jsxSnippet: '',
1368
- props: [],
1369
- category: 'layout',
1370
- hasAnimation: false,
1371
- animationDetails: [],
1372
- statePatterns: [],
1373
- tailwindPatterns: emptyTailwind,
1374
- });
1375
- }
1376
- // 8. Images / Media (hero images, avatars)
1377
- const imgCount = (html.match(/<img\s/gi) || []).length;
1378
- if (imgCount >= 3) {
1379
- components.push({
1380
- name: 'Image',
1381
- filePath: 'html',
1382
- variants: [],
1383
- cssClasses: [],
1384
- jsxSnippet: '',
1385
- props: [],
1386
- category: 'media',
1387
- hasAnimation: false,
1388
- animationDetails: [],
1389
- statePatterns: [],
1390
- tailwindPatterns: emptyTailwind,
1391
- });
1392
- }
1393
- // 9. SVG / Icon patterns
1394
- const svgCount = (html.match(/<svg[\s>]/gi) || []).length + (html.match(/(?:_jsx|createElement)\s*\(\s*["']svg["']/gi) || []).length;
1395
- if (svgCount >= 3 || /lucide-react|heroicons|@phosphor|react-icons/i.test(html)) {
1396
- components.push({
1397
- name: 'Icon',
1398
- filePath: 'html',
1399
- variants: [],
1400
- cssClasses: [],
1401
- jsxSnippet: '',
1402
- props: [],
1403
- category: 'media',
1404
- hasAnimation: false,
1405
- animationDetails: [],
1406
- statePatterns: [],
1407
- tailwindPatterns: emptyTailwind,
1408
- });
1409
- }
1410
- // 10. Lists / Timeline
1411
- const listClasses = extractClassesByPattern(html, /\b(list|timeline|steps|progress)\b/i);
1412
- if (listClasses.size > 0) {
1413
- components.push({
1414
- name: 'List',
1415
- filePath: 'html',
1416
- variants: [],
1417
- cssClasses: [...listClasses].slice(0, 10),
1418
- jsxSnippet: '',
1419
- props: [],
1420
- category: 'data-display',
1421
- hasAnimation: false,
1422
- animationDetails: [],
1423
- statePatterns: [],
1424
- tailwindPatterns: emptyTailwind,
1425
- });
1426
- }
1427
- // 11. Map / Canvas (interactive visualization)
1428
- if (/<canvas[\s>]/i.test(html) || /mapbox|leaflet|google.*maps/i.test(html) || /(?:_jsx|createElement)\s*\(\s*["']canvas["']/i.test(html) || /\bd3\b.*select|topojson/i.test(html)) {
1429
- components.push({
1430
- name: 'Map/Canvas',
1431
- filePath: 'html',
1432
- variants: [],
1433
- cssClasses: [],
1434
- jsxSnippet: '',
1435
- props: [],
1436
- category: 'media',
1437
- hasAnimation: false,
1438
- animationDetails: [],
1439
- statePatterns: [],
1440
- tailwindPatterns: emptyTailwind,
1441
- });
1442
- }
1443
- return components;
1444
- }
1445
- function buildCSSRuleLookup(css) {
1446
- const rules = new Map();
1447
- // Simple regex-based extraction: .class-name { ... }
1448
- const ruleMatches = css.matchAll(/\.([\w-]+)\s*\{([^}]*)\}/g);
1449
- for (const m of ruleMatches) {
1450
- const className = m[1];
1451
- const declarations = m[2].trim();
1452
- if (!rules.has(className)) {
1453
- rules.set(className, []);
1454
- }
1455
- rules.get(className).push(declarations);
1456
- }
1457
- return rules;
1458
- }
1459
- function extractMatchingStyles(cssRules, classes, patterns) {
1460
- const allDecls = [];
1461
- for (const cls of classes) {
1462
- const decls = cssRules.get(cls);
1463
- if (decls)
1464
- allDecls.push(...decls);
1465
- }
1466
- for (const pattern of patterns) {
1467
- for (const [cls, decls] of cssRules) {
1468
- if (cls.includes(pattern))
1469
- allDecls.push(...decls);
1470
- }
1471
- }
1472
- return allDecls.join('; ');
1473
- }
1474
- function extractElementClasses(html, tag) {
1475
- const classes = new Set();
1476
- // HTML: <tag class="...">
1477
- const regex = new RegExp(`<${tag}[^>]*class\\s*=\\s*["']([^"']*)["']`, 'gi');
1478
- const matches = html.matchAll(regex);
1479
- for (const m of matches) {
1480
- m[1].split(/\s+/).filter(Boolean).forEach(c => classes.add(c));
1481
- }
1482
- // JSX: _jsx("tag", { className: "..." })
1483
- const jsxRegex = new RegExp(`(?:_jsx|createElement)\\s*\\(\\s*["']${tag}["'][^)]*className\\s*:\\s*["'\`]([^"'\`]*)["'\`]`, 'gi');
1484
- const jsxMatches = html.matchAll(jsxRegex);
1485
- for (const m of jsxMatches) {
1486
- m[1].split(/\s+/).filter(Boolean).forEach(c => classes.add(c));
1487
- }
1488
- return classes;
1489
- }
1490
- function extractClassesByPattern(html, pattern) {
1491
- const classes = new Set();
1492
- // Match HTML class="..." and JSX className="..."
1493
- const allClassAttrs = html.matchAll(/(?:class|className)\s*[:=]\s*["'`]([^"'`]*)["'`]/gi);
1494
- for (const m of allClassAttrs) {
1495
- const classList = m[1].split(/\s+/).filter(Boolean);
1496
- for (const cls of classList) {
1497
- if (pattern.test(cls)) {
1498
- classes.add(cls);
1499
- }
1500
- }
1501
- }
1502
- return classes;
1503
- }
1504
- function findVariants(classes, base) {
1505
- const variants = [];
1506
- for (const cls of classes) {
1507
- if (cls.includes(base) && cls !== base) {
1508
- const variant = cls.replace(new RegExp(`.*${base}[-_]?`, 'i'), '');
1509
- if (variant && variant.length < 20)
1510
- variants.push(variant);
1511
- }
1512
- }
1513
- return [...new Set(variants)].slice(0, 5);
1514
- }
1515
- function buildDarkModePairs(tokens) {
1516
- const seen = new Set();
1517
- tokens.darkModeVars = tokens.darkModeVars.filter(v => {
1518
- if (seen.has(v.variable))
1519
- return false;
1520
- seen.add(v.variable);
1521
- return true;
1522
- });
1523
- }
1524
- // ── Utility Helpers ───────────────────────────────────────────────────
1525
- async function fetchText(url) {
1526
- try {
1527
- const controller = new AbortController();
1528
- const timeout = setTimeout(() => controller.abort(), 15000);
1529
- const response = await fetch(url, {
1530
- signal: controller.signal,
1531
- headers: {
1532
- 'User-Agent': 'Mozilla/5.0 (compatible; skillui/1.0; +https://github.com/amaanbuilds/skillui)',
1533
- 'Accept': 'text/html,text/css,application/xhtml+xml,*/*',
1534
- },
1535
- redirect: 'follow',
1536
- });
1537
- clearTimeout(timeout);
1538
- if (!response.ok)
1539
- return null;
1540
- return await response.text();
1541
- }
1542
- catch {
1543
- return null;
1544
- }
1545
- }
1546
- function resolveUrl(href, base) {
1547
- try {
1548
- return new URL(href, base).href;
1549
- }
1550
- catch {
1551
- return href;
1552
- }
1553
- }
1554
- function addColor(tokens, hex, source, name) {
1555
- if (!hex || !isValidHex(hex))
1556
- return;
1557
- const existing = tokens.colors.find(c => c.value === hex);
1558
- if (existing) {
1559
- existing.frequency++;
1560
- if (name && !existing.name)
1561
- existing.name = name;
1562
- }
1563
- else {
1564
- tokens.colors.push({ value: hex, frequency: 1, source, name });
1565
- }
1566
- }
1567
- function tryParseColor(value) {
1568
- // Hex
1569
- const hexMatch = value.match(/^#([0-9a-fA-F]{3,8})$/);
1570
- if (hexMatch)
1571
- return normalizeHex(value);
1572
- // rgb()
1573
- const rgbMatch = value.match(/^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
1574
- if (rgbMatch)
1575
- return rgbToHex(parseInt(rgbMatch[1]), parseInt(rgbMatch[2]), parseInt(rgbMatch[3]));
1576
- // hsl()
1577
- const hslMatch = value.match(/^hsla?\(\s*([\d.]+)\s*[,\s]\s*([\d.]+)%\s*[,\s]\s*([\d.]+)%/);
1578
- if (hslMatch)
1579
- return hslToHex(parseFloat(hslMatch[1]), parseFloat(hslMatch[2]), parseFloat(hslMatch[3]));
1580
- // Bare HSL: "220 20% 10%"
1581
- const bareHslMatch = value.match(/^\s*([\d.]+)\s+([\d.]+)%\s+([\d.]+)%\s*$/);
1582
- if (bareHslMatch)
1583
- return hslToHex(parseFloat(bareHslMatch[1]), parseFloat(bareHslMatch[2]), parseFloat(bareHslMatch[3]));
1584
- return null;
1585
- }
1586
- function isColorProperty(prop) {
1587
- return /^(color|background-color|background|border-color|outline-color|fill|stroke|accent-color|caret-color|text-decoration-color|column-rule-color)$/i.test(prop);
1588
- }
1589
- function isSpacingProperty(prop) {
1590
- return /^(margin|padding|gap|row-gap|column-gap|margin-top|margin-right|margin-bottom|margin-left|padding-top|padding-right|padding-bottom|padding-left|top|right|bottom|left)$/i.test(prop);
1591
- }
1592
- function guessPropertyType(varName) {
1593
- if (/color|bg|background|foreground/i.test(varName))
1594
- return 'color';
1595
- if (/font|family|typeface/i.test(varName))
1596
- return 'font';
1597
- if (/size|spacing|gap|padding|margin|radius/i.test(varName))
1598
- return 'spacing';
1599
- if (/shadow|elevation/i.test(varName))
1600
- return 'shadow';
1601
- return 'unknown';
1602
- }
1603
- function isColorVarName(name) {
1604
- return /color|background|foreground|primary|secondary|accent|muted|destructive|border|card|popover|ring|input|chart|surface|text|brand|success|danger|warning|error|info/i.test(name) &&
1605
- !/font|size|spacing|radius|shadow|width|height|duration|delay|family|weight|line/i.test(name);
1606
- }
1607
- function isGenericFont(f) {
1608
- return /^(sans-serif|serif|monospace|cursive|fantasy|system-ui|ui-sans-serif|ui-serif|ui-monospace|inherit|initial|unset)$/i.test(f);
1609
- }
1610
- function isValidFontName(f) {
1611
- // Reject font names that contain CSS syntax artifacts
1612
- // These indicate a broken/concatenated CSS rule was parsed as a font name
1613
- if (/[{};()]/.test(f))
1614
- return false;
1615
- // Reject names containing newlines or HTML tags
1616
- if (/[\n\r<>]/.test(f))
1617
- return false;
1618
- // Reject CSS property names mistakenly captured
1619
- if (/^\s*(font-family|font-size|font-weight|color|background|margin|padding)\b/i.test(f))
1620
- return false;
1621
- // Reject names starting with var(
1622
- if (/^var\(/i.test(f))
1623
- return false;
1624
- // Reject names that are just numbers
1625
- if (/^\d+(\.\d+)?(px|rem|em|%)?$/.test(f))
1626
- return false;
1627
- // Reject names longer than 50 chars (no real font name is that long)
1628
- if (f.length > 50)
1629
- return false;
1630
- return true;
1631
- }
1632
- function isValidHex(hex) {
1633
- return /^#[0-9a-f]{6}$/i.test(hex);
1634
- }
1635
- function namedColorToHex(name) {
1636
- const map = {
1637
- 'black': '#000000', 'white': '#ffffff', 'red': '#ff0000', 'green': '#008000',
1638
- 'blue': '#0000ff', 'yellow': '#ffff00', 'orange': '#ffa500', 'purple': '#800080',
1639
- 'gray': '#808080', 'grey': '#808080', 'pink': '#ffc0cb', 'brown': '#a52a2a',
1640
- 'cyan': '#00ffff', 'magenta': '#ff00ff', 'transparent': null, 'none': null,
1641
- 'currentcolor': null, 'inherit': null,
1642
- };
1643
- return map[name.toLowerCase()] ?? null;
1644
- }
1645
- function normalizeHex(v) {
1646
- const match = v.match(/^#([0-9a-fA-F]{3})$/);
1647
- if (match) {
1648
- const [r, g, b] = match[1].split('');
1649
- return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
1650
- }
1651
- return v.toLowerCase().slice(0, 7);
1652
- }
1653
- function rgbToHex(r, g, b) {
1654
- return '#' + [r, g, b].map(c => Math.max(0, Math.min(255, c)).toString(16).padStart(2, '0')).join('').toLowerCase();
1655
- }
1656
- function hslToHex(h, s, l) {
1657
- s /= 100;
1658
- l /= 100;
1659
- const c = (1 - Math.abs(2 * l - 1)) * s;
1660
- const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
1661
- const m = l - c / 2;
1662
- let r = 0, g = 0, b = 0;
1663
- if (h < 60) {
1664
- r = c;
1665
- g = x;
1666
- }
1667
- else if (h < 120) {
1668
- r = x;
1669
- g = c;
1670
- }
1671
- else if (h < 180) {
1672
- g = c;
1673
- b = x;
1674
- }
1675
- else if (h < 240) {
1676
- g = x;
1677
- b = c;
1678
- }
1679
- else if (h < 300) {
1680
- r = x;
1681
- b = c;
1682
- }
1683
- else {
1684
- r = c;
1685
- b = x;
1686
- }
1687
- return rgbToHex(Math.round((r + m) * 255), Math.round((g + m) * 255), Math.round((b + m) * 255));
1688
- }
1689
- //# sourceMappingURL=http-css.js.map