@buoy-design/scanners 0.3.38 → 0.4.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-design/scanners",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Source scanners for Buoy (React, Vue, Svelte, Angular, Figma, Storybook)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -51,10 +51,11 @@
|
|
|
51
51
|
}
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@google/design.md": "0.1.1",
|
|
54
55
|
"glob": "^11.0.0",
|
|
55
56
|
"typescript": "^5.7.2",
|
|
56
57
|
"zod": "^3.24.1",
|
|
57
|
-
"@buoy-design/core": "0.
|
|
58
|
+
"@buoy-design/core": "0.4.1"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
61
|
"@types/node": "^22.10.2",
|
|
@@ -1,1381 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { resolve, relative } from 'path';
|
|
3
|
-
import { glob } from 'glob';
|
|
4
|
-
import { createTokenId } from '@buoy-design/core';
|
|
5
|
-
export class TailwindConfigParser {
|
|
6
|
-
projectRoot;
|
|
7
|
-
options;
|
|
8
|
-
constructor(projectRoot, options = {}) {
|
|
9
|
-
this.projectRoot = projectRoot;
|
|
10
|
-
this.options = options;
|
|
11
|
-
}
|
|
12
|
-
async parse() {
|
|
13
|
-
// Try v3 JS/TS config first
|
|
14
|
-
const v3ConfigPath = this.findV3ConfigFile();
|
|
15
|
-
if (v3ConfigPath) {
|
|
16
|
-
try {
|
|
17
|
-
const theme = await this.extractThemeFromJS(v3ConfigPath);
|
|
18
|
-
const tokens = this.themeToTokens(theme, v3ConfigPath);
|
|
19
|
-
return {
|
|
20
|
-
theme,
|
|
21
|
-
tokens,
|
|
22
|
-
configPath: v3ConfigPath,
|
|
23
|
-
version: 3,
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
catch (err) {
|
|
27
|
-
console.error('Failed to parse Tailwind v3 config:', err);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
// Try v4 CSS config
|
|
31
|
-
const v4ConfigPath = await this.findV4CSSFile();
|
|
32
|
-
if (v4ConfigPath) {
|
|
33
|
-
try {
|
|
34
|
-
const { theme, tokens } = await this.extractFromCSS(v4ConfigPath);
|
|
35
|
-
return {
|
|
36
|
-
theme,
|
|
37
|
-
tokens,
|
|
38
|
-
configPath: v4ConfigPath,
|
|
39
|
-
version: 4,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
catch (err) {
|
|
43
|
-
console.error('Failed to parse Tailwind v4 config:', err);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
findV3ConfigFile() {
|
|
49
|
-
const configNames = [
|
|
50
|
-
'tailwind.config.js',
|
|
51
|
-
'tailwind.config.ts',
|
|
52
|
-
'tailwind.config.mjs',
|
|
53
|
-
'tailwind.config.cjs',
|
|
54
|
-
];
|
|
55
|
-
for (const name of configNames) {
|
|
56
|
-
const path = resolve(this.projectRoot, name);
|
|
57
|
-
if (existsSync(path)) {
|
|
58
|
-
return path;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
async findV4CSSFile() {
|
|
64
|
-
// If CSS paths are provided, use those
|
|
65
|
-
if (this.options.cssConfigPaths?.length) {
|
|
66
|
-
for (const cssPath of this.options.cssConfigPaths) {
|
|
67
|
-
if (existsSync(cssPath)) {
|
|
68
|
-
const content = readFileSync(cssPath, 'utf-8');
|
|
69
|
-
if (this.isTailwindV4CSS(content)) {
|
|
70
|
-
return cssPath;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Auto-discover CSS files - check common locations first
|
|
76
|
-
const cssPatterns = [
|
|
77
|
-
'app/globals.css',
|
|
78
|
-
'src/app/globals.css',
|
|
79
|
-
'styles/globals.css',
|
|
80
|
-
'src/styles/globals.css',
|
|
81
|
-
'src/index.css',
|
|
82
|
-
'app/global.css',
|
|
83
|
-
'styles/global.css',
|
|
84
|
-
];
|
|
85
|
-
for (const pattern of cssPatterns) {
|
|
86
|
-
const path = resolve(this.projectRoot, pattern);
|
|
87
|
-
if (existsSync(path)) {
|
|
88
|
-
const content = readFileSync(path, 'utf-8');
|
|
89
|
-
if (this.isTailwindV4CSS(content)) {
|
|
90
|
-
return path;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
// Try glob search, prioritizing files with @theme blocks
|
|
95
|
-
try {
|
|
96
|
-
const files = await glob('**/*.css', {
|
|
97
|
-
cwd: this.projectRoot,
|
|
98
|
-
ignore: [
|
|
99
|
-
'**/node_modules/**',
|
|
100
|
-
'**/dist/**',
|
|
101
|
-
'**/build/**',
|
|
102
|
-
'**/templates/**',
|
|
103
|
-
'**/fixtures/**',
|
|
104
|
-
'**/test/**',
|
|
105
|
-
'**/tests/**',
|
|
106
|
-
'**/.next/**',
|
|
107
|
-
'**/coverage/**',
|
|
108
|
-
],
|
|
109
|
-
absolute: true,
|
|
110
|
-
});
|
|
111
|
-
// Sort candidates by content quality (prefer files with @theme blocks)
|
|
112
|
-
const candidates = [];
|
|
113
|
-
for (const file of files) {
|
|
114
|
-
try {
|
|
115
|
-
const content = readFileSync(file, 'utf-8');
|
|
116
|
-
if (this.isTailwindV4CSS(content)) {
|
|
117
|
-
let score = 0;
|
|
118
|
-
// Prefer files with @theme blocks
|
|
119
|
-
if (content.includes('@theme'))
|
|
120
|
-
score += 100;
|
|
121
|
-
// Prefer files with :root variables
|
|
122
|
-
if (content.includes(':root'))
|
|
123
|
-
score += 50;
|
|
124
|
-
// Prefer files with more content
|
|
125
|
-
score += Math.min(content.length / 100, 50);
|
|
126
|
-
// Prefer files in app/ or styles/ directories
|
|
127
|
-
if (file.includes('/app/') || file.includes('/styles/'))
|
|
128
|
-
score += 20;
|
|
129
|
-
candidates.push({ path: file, score });
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
// Skip unreadable files
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
// Return the highest scoring candidate
|
|
137
|
-
if (candidates.length > 0) {
|
|
138
|
-
candidates.sort((a, b) => b.score - a.score);
|
|
139
|
-
return candidates[0].path;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
// Glob failed, skip
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
isTailwindV4CSS(content) {
|
|
148
|
-
// Check for v4 indicators
|
|
149
|
-
return (content.includes('@import "tailwindcss"') ||
|
|
150
|
-
content.includes("@import 'tailwindcss'") ||
|
|
151
|
-
content.includes('@theme') ||
|
|
152
|
-
content.includes('@custom-variant') ||
|
|
153
|
-
content.includes('@utility'));
|
|
154
|
-
}
|
|
155
|
-
async extractFromCSS(cssPath) {
|
|
156
|
-
const content = readFileSync(cssPath, 'utf-8');
|
|
157
|
-
const relativePath = relative(this.projectRoot, cssPath);
|
|
158
|
-
const source = { type: 'css', path: relativePath };
|
|
159
|
-
const theme = {
|
|
160
|
-
colors: {},
|
|
161
|
-
spacing: {},
|
|
162
|
-
fontSize: {},
|
|
163
|
-
fontFamily: {},
|
|
164
|
-
borderRadius: {},
|
|
165
|
-
boxShadow: {},
|
|
166
|
-
customVariants: [],
|
|
167
|
-
utilities: [],
|
|
168
|
-
plugins: [],
|
|
169
|
-
imports: [],
|
|
170
|
-
breakpoints: {},
|
|
171
|
-
};
|
|
172
|
-
const tokens = [];
|
|
173
|
-
// Extract @plugin declarations
|
|
174
|
-
const plugins = this.extractPlugins(content);
|
|
175
|
-
theme.plugins = plugins;
|
|
176
|
-
// Extract @import declarations (excluding tailwindcss itself)
|
|
177
|
-
const imports = this.extractImports(content);
|
|
178
|
-
theme.imports = imports;
|
|
179
|
-
// Extract @theme inline blocks
|
|
180
|
-
const themeBlocks = this.extractThemeBlocks(content);
|
|
181
|
-
for (const block of themeBlocks) {
|
|
182
|
-
const blockTokens = this.parseThemeBlock(block, source, theme);
|
|
183
|
-
tokens.push(...blockTokens);
|
|
184
|
-
// Also populate theme object
|
|
185
|
-
for (const token of blockTokens) {
|
|
186
|
-
this.addToTheme(theme, token);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
// Extract :root CSS variables
|
|
190
|
-
const rootVars = this.extractRootVariables(content);
|
|
191
|
-
for (const [name, value] of Object.entries(rootVars)) {
|
|
192
|
-
const token = this.cssVarToToken(name, value, source, 'light');
|
|
193
|
-
if (token) {
|
|
194
|
-
tokens.push(token);
|
|
195
|
-
this.addToTheme(theme, token);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
// Extract .dark CSS variables
|
|
199
|
-
const darkVars = this.extractDarkVariables(content);
|
|
200
|
-
for (const [name, value] of Object.entries(darkVars)) {
|
|
201
|
-
const token = this.cssVarToToken(name, value, source, 'dark');
|
|
202
|
-
if (token) {
|
|
203
|
-
tokens.push(token);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Extract @custom-variant declarations
|
|
207
|
-
const customVariants = this.extractCustomVariants(content);
|
|
208
|
-
theme.customVariants = customVariants;
|
|
209
|
-
// Extract @utility declarations
|
|
210
|
-
const utilities = this.extractUtilities(content);
|
|
211
|
-
theme.utilities = utilities;
|
|
212
|
-
// Extract @source directives (v4 content paths)
|
|
213
|
-
const sources = this.extractSources(content);
|
|
214
|
-
theme.sources = sources;
|
|
215
|
-
return { theme, tokens };
|
|
216
|
-
}
|
|
217
|
-
extractThemeBlocks(content) {
|
|
218
|
-
const blocks = [];
|
|
219
|
-
// Match @theme inline { }, @theme { }, but NOT @theme reference { }
|
|
220
|
-
// The 'reference' keyword is used to reset theme values, not define new ones
|
|
221
|
-
const regex = /@theme\s+(?:inline\s*)?\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/gs;
|
|
222
|
-
let match;
|
|
223
|
-
while ((match = regex.exec(content)) !== null) {
|
|
224
|
-
// Skip @theme reference blocks
|
|
225
|
-
const fullMatch = match[0];
|
|
226
|
-
if (fullMatch.includes('@theme reference') || fullMatch.includes('@theme reference')) {
|
|
227
|
-
continue;
|
|
228
|
-
}
|
|
229
|
-
blocks.push(match[1] || '');
|
|
230
|
-
}
|
|
231
|
-
return blocks;
|
|
232
|
-
}
|
|
233
|
-
parseThemeBlock(block, source, theme) {
|
|
234
|
-
const tokens = [];
|
|
235
|
-
// Match CSS custom property declarations
|
|
236
|
-
const propRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
237
|
-
let match;
|
|
238
|
-
while ((match = propRegex.exec(block)) !== null) {
|
|
239
|
-
const name = match[1];
|
|
240
|
-
const value = match[2].trim();
|
|
241
|
-
// Extract breakpoints to theme.breakpoints
|
|
242
|
-
if (name.startsWith('breakpoint-')) {
|
|
243
|
-
const bpName = name.replace('breakpoint-', '');
|
|
244
|
-
theme.breakpoints[bpName] = value;
|
|
245
|
-
}
|
|
246
|
-
const token = this.themeVarToToken(name, value, source);
|
|
247
|
-
if (token) {
|
|
248
|
-
tokens.push(token);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
return tokens;
|
|
252
|
-
}
|
|
253
|
-
themeVarToToken(name, value, source) {
|
|
254
|
-
// Parse the variable name to determine category
|
|
255
|
-
const category = this.inferCategoryFromName(name);
|
|
256
|
-
// Handle var() references - store as raw with reference info in tags
|
|
257
|
-
if (value.startsWith('var(')) {
|
|
258
|
-
// Parse var() with optional fallback: var(--name, fallback)
|
|
259
|
-
const refMatch = value.match(/var\(--([^,)]+)(?:,\s*([^)]+))?\)/);
|
|
260
|
-
const refName = refMatch?.[1] || '';
|
|
261
|
-
const fallback = refMatch?.[2]?.trim();
|
|
262
|
-
const hasFallback = !!fallback;
|
|
263
|
-
const aliases = [name];
|
|
264
|
-
if (refName)
|
|
265
|
-
aliases.push(refName);
|
|
266
|
-
const tags = ['tailwind', 'v4', 'reference'];
|
|
267
|
-
if (hasFallback)
|
|
268
|
-
tags.push('fallback');
|
|
269
|
-
// Determine the appropriate category for the token
|
|
270
|
-
let tokenCategory = category;
|
|
271
|
-
if (category === 'other') {
|
|
272
|
-
tokenCategory = 'color'; // Default to color for unknown categories
|
|
273
|
-
}
|
|
274
|
-
return {
|
|
275
|
-
id: createTokenId(source, `tw-${name}`),
|
|
276
|
-
name: `tw-${name}`,
|
|
277
|
-
category: tokenCategory === 'other' ? 'color' : tokenCategory,
|
|
278
|
-
value: { type: 'raw', value },
|
|
279
|
-
source,
|
|
280
|
-
aliases,
|
|
281
|
-
usedBy: [],
|
|
282
|
-
metadata: { tags },
|
|
283
|
-
scannedAt: new Date(),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
// Parse color values (hex, rgb, rgba, hsl, oklch, etc.)
|
|
287
|
-
if (category === 'color') {
|
|
288
|
-
return this.createColorToken(name, value, source, 'v4');
|
|
289
|
-
}
|
|
290
|
-
// Parse spacing/sizing values
|
|
291
|
-
if (category === 'spacing' || category === 'border') {
|
|
292
|
-
return this.createV4SpacingToken(name, value, source, category);
|
|
293
|
-
}
|
|
294
|
-
// Handle typography tokens (font-*, text-*, etc.)
|
|
295
|
-
if (category === 'typography') {
|
|
296
|
-
return {
|
|
297
|
-
id: createTokenId(source, `tw-${name}`),
|
|
298
|
-
name: `tw-${name}`,
|
|
299
|
-
category: 'typography',
|
|
300
|
-
value: { type: 'raw', value },
|
|
301
|
-
source,
|
|
302
|
-
aliases: [name],
|
|
303
|
-
usedBy: [],
|
|
304
|
-
metadata: { tags: ['tailwind', 'v4'] },
|
|
305
|
-
scannedAt: new Date(),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
// Default raw token
|
|
309
|
-
return {
|
|
310
|
-
id: createTokenId(source, `tw-${name}`),
|
|
311
|
-
name: `tw-${name}`,
|
|
312
|
-
category: 'color',
|
|
313
|
-
value: { type: 'raw', value },
|
|
314
|
-
source,
|
|
315
|
-
aliases: [name],
|
|
316
|
-
usedBy: [],
|
|
317
|
-
metadata: { tags: ['tailwind', 'v4'] },
|
|
318
|
-
scannedAt: new Date(),
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
inferCategoryFromName(name) {
|
|
322
|
-
if (name.startsWith('color-') ||
|
|
323
|
-
name.includes('background') ||
|
|
324
|
-
name.includes('foreground') ||
|
|
325
|
-
name.includes('primary') ||
|
|
326
|
-
name.includes('secondary') ||
|
|
327
|
-
name.includes('accent') ||
|
|
328
|
-
name.includes('muted') ||
|
|
329
|
-
name.includes('destructive') ||
|
|
330
|
-
name.includes('border') ||
|
|
331
|
-
name.includes('input') ||
|
|
332
|
-
name.includes('ring') ||
|
|
333
|
-
name.includes('chart-') ||
|
|
334
|
-
name.includes('sidebar') ||
|
|
335
|
-
name.includes('surface') ||
|
|
336
|
-
name.includes('code') ||
|
|
337
|
-
name.includes('selection') ||
|
|
338
|
-
name.includes('popover') ||
|
|
339
|
-
name.includes('card')) {
|
|
340
|
-
return 'color';
|
|
341
|
-
}
|
|
342
|
-
if (name.startsWith('spacing-') ||
|
|
343
|
-
name.startsWith('breakpoint-') ||
|
|
344
|
-
name.includes('gap') ||
|
|
345
|
-
name.includes('padding') ||
|
|
346
|
-
name.includes('margin')) {
|
|
347
|
-
return 'spacing';
|
|
348
|
-
}
|
|
349
|
-
if (name.startsWith('radius-') || name.includes('border-radius')) {
|
|
350
|
-
return 'border';
|
|
351
|
-
}
|
|
352
|
-
if (name.startsWith('font-') ||
|
|
353
|
-
name.startsWith('text-') ||
|
|
354
|
-
name.includes('leading') ||
|
|
355
|
-
name.includes('tracking')) {
|
|
356
|
-
return 'typography';
|
|
357
|
-
}
|
|
358
|
-
return 'other';
|
|
359
|
-
}
|
|
360
|
-
createV4SpacingToken(name, value, source, category) {
|
|
361
|
-
const numMatch = value.match(/^([\d.]+)(rem|px|em|%)?$/);
|
|
362
|
-
if (numMatch) {
|
|
363
|
-
const num = parseFloat(numMatch[1]);
|
|
364
|
-
const rawUnit = numMatch[2] || 'px';
|
|
365
|
-
// Only use valid spacing units, fall back to raw for %
|
|
366
|
-
if (rawUnit === 'rem' || rawUnit === 'px' || rawUnit === 'em') {
|
|
367
|
-
return {
|
|
368
|
-
id: createTokenId(source, `tw-${name}`),
|
|
369
|
-
name: `tw-${name}`,
|
|
370
|
-
category,
|
|
371
|
-
value: { type: 'spacing', value: num, unit: rawUnit },
|
|
372
|
-
source,
|
|
373
|
-
aliases: [name],
|
|
374
|
-
usedBy: [],
|
|
375
|
-
metadata: { tags: ['tailwind', 'v4'] },
|
|
376
|
-
scannedAt: new Date(),
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// Handle calc() expressions
|
|
381
|
-
if (value.startsWith('calc(')) {
|
|
382
|
-
return {
|
|
383
|
-
id: createTokenId(source, `tw-${name}`),
|
|
384
|
-
name: `tw-${name}`,
|
|
385
|
-
category,
|
|
386
|
-
value: { type: 'raw', value },
|
|
387
|
-
source,
|
|
388
|
-
aliases: [name],
|
|
389
|
-
usedBy: [],
|
|
390
|
-
metadata: { tags: ['tailwind', 'v4', 'calc'] },
|
|
391
|
-
scannedAt: new Date(),
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
return {
|
|
395
|
-
id: createTokenId(source, `tw-${name}`),
|
|
396
|
-
name: `tw-${name}`,
|
|
397
|
-
category,
|
|
398
|
-
value: { type: 'raw', value },
|
|
399
|
-
source,
|
|
400
|
-
aliases: [name],
|
|
401
|
-
usedBy: [],
|
|
402
|
-
metadata: { tags: ['tailwind', 'v4'] },
|
|
403
|
-
scannedAt: new Date(),
|
|
404
|
-
};
|
|
405
|
-
}
|
|
406
|
-
extractRootVariables(content) {
|
|
407
|
-
const vars = {};
|
|
408
|
-
// Match :root { ... }
|
|
409
|
-
const rootMatch = content.match(/:root\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
|
|
410
|
-
if (rootMatch) {
|
|
411
|
-
const block = rootMatch[1];
|
|
412
|
-
const propRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
413
|
-
let match;
|
|
414
|
-
while ((match = propRegex.exec(block)) !== null) {
|
|
415
|
-
vars[match[1]] = match[2].trim();
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
return vars;
|
|
419
|
-
}
|
|
420
|
-
extractDarkVariables(content) {
|
|
421
|
-
const vars = {};
|
|
422
|
-
// Match .dark { ... }
|
|
423
|
-
const darkMatch = content.match(/\.dark\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/s);
|
|
424
|
-
if (darkMatch) {
|
|
425
|
-
const block = darkMatch[1];
|
|
426
|
-
const propRegex = /--([\w-]+):\s*([^;]+);/g;
|
|
427
|
-
let match;
|
|
428
|
-
while ((match = propRegex.exec(block)) !== null) {
|
|
429
|
-
vars[match[1]] = match[2].trim();
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
return vars;
|
|
433
|
-
}
|
|
434
|
-
cssVarToToken(name, value, source, mode) {
|
|
435
|
-
const category = this.inferCategoryFromCSSVar(name);
|
|
436
|
-
// Skip non-semantic variables
|
|
437
|
-
if (category === 'other') {
|
|
438
|
-
return null;
|
|
439
|
-
}
|
|
440
|
-
const tokenName = mode === 'dark' ? `tw-${name}-dark` : `tw-${name}`;
|
|
441
|
-
if (category === 'color') {
|
|
442
|
-
return this.createColorToken(name, value, source, 'v4', mode);
|
|
443
|
-
}
|
|
444
|
-
if (category === 'spacing' || category === 'border') {
|
|
445
|
-
return this.createV4SpacingToken(tokenName.replace('tw-', ''), value, source, category);
|
|
446
|
-
}
|
|
447
|
-
return {
|
|
448
|
-
id: createTokenId(source, tokenName),
|
|
449
|
-
name: tokenName,
|
|
450
|
-
category,
|
|
451
|
-
value: { type: 'raw', value },
|
|
452
|
-
source,
|
|
453
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
454
|
-
usedBy: [],
|
|
455
|
-
metadata: { tags: ['tailwind', 'v4', mode] },
|
|
456
|
-
scannedAt: new Date(),
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
inferCategoryFromCSSVar(name) {
|
|
460
|
-
// Common shadcn/tailwind v4 variable names
|
|
461
|
-
const colorVars = [
|
|
462
|
-
'background',
|
|
463
|
-
'foreground',
|
|
464
|
-
'primary',
|
|
465
|
-
'secondary',
|
|
466
|
-
'accent',
|
|
467
|
-
'muted',
|
|
468
|
-
'destructive',
|
|
469
|
-
'border',
|
|
470
|
-
'input',
|
|
471
|
-
'ring',
|
|
472
|
-
'card',
|
|
473
|
-
'popover',
|
|
474
|
-
'sidebar',
|
|
475
|
-
'surface',
|
|
476
|
-
'code',
|
|
477
|
-
'selection',
|
|
478
|
-
'chart-',
|
|
479
|
-
];
|
|
480
|
-
for (const cv of colorVars) {
|
|
481
|
-
if (name === cv || name.startsWith(`${cv}-`)) {
|
|
482
|
-
return 'color';
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
if (name.startsWith('radius')) {
|
|
486
|
-
return 'border';
|
|
487
|
-
}
|
|
488
|
-
if (name.startsWith('spacing') || name.startsWith('gap')) {
|
|
489
|
-
return 'spacing';
|
|
490
|
-
}
|
|
491
|
-
if (name.startsWith('font') || name.startsWith('text')) {
|
|
492
|
-
return 'typography';
|
|
493
|
-
}
|
|
494
|
-
return 'other';
|
|
495
|
-
}
|
|
496
|
-
extractCustomVariants(content) {
|
|
497
|
-
const variants = [];
|
|
498
|
-
// Match @custom-variant declarations
|
|
499
|
-
const customVariantRegex = /@custom-variant\s+([\w-]+)/g;
|
|
500
|
-
let match;
|
|
501
|
-
while ((match = customVariantRegex.exec(content)) !== null) {
|
|
502
|
-
variants.push(match[1]);
|
|
503
|
-
}
|
|
504
|
-
// Also match @variant declarations (Tailwind v4 syntax)
|
|
505
|
-
const variantRegex = /@variant\s+([\w-]+)/g;
|
|
506
|
-
while ((match = variantRegex.exec(content)) !== null) {
|
|
507
|
-
if (!variants.includes(match[1])) {
|
|
508
|
-
variants.push(match[1]);
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
return variants;
|
|
512
|
-
}
|
|
513
|
-
extractUtilities(content) {
|
|
514
|
-
const utilities = [];
|
|
515
|
-
const regex = /@utility\s+([\w-]+)/g;
|
|
516
|
-
let match;
|
|
517
|
-
while ((match = regex.exec(content)) !== null) {
|
|
518
|
-
utilities.push(match[1]);
|
|
519
|
-
}
|
|
520
|
-
return utilities;
|
|
521
|
-
}
|
|
522
|
-
extractPlugins(content) {
|
|
523
|
-
const plugins = [];
|
|
524
|
-
// Match @plugin 'package-name' or @plugin "package-name"
|
|
525
|
-
const regex = /@plugin\s+['"]([^'"]+)['"]/g;
|
|
526
|
-
let match;
|
|
527
|
-
while ((match = regex.exec(content)) !== null) {
|
|
528
|
-
plugins.push(match[1]);
|
|
529
|
-
}
|
|
530
|
-
return plugins;
|
|
531
|
-
}
|
|
532
|
-
extractImports(content) {
|
|
533
|
-
const imports = [];
|
|
534
|
-
// Match @import 'path' or @import "path" with optional layer()
|
|
535
|
-
const regex = /@import\s+['"]([^'"]+)['"](?:\s+layer\([^)]+\))?/g;
|
|
536
|
-
let match;
|
|
537
|
-
while ((match = regex.exec(content)) !== null) {
|
|
538
|
-
const importPath = match[1];
|
|
539
|
-
// Exclude tailwindcss itself
|
|
540
|
-
if (importPath !== 'tailwindcss') {
|
|
541
|
-
imports.push(importPath);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return imports;
|
|
545
|
-
}
|
|
546
|
-
/**
|
|
547
|
-
* Extract @source directives from v4 CSS config
|
|
548
|
-
* Handles both @source "path" and @source inline("path") syntax
|
|
549
|
-
*/
|
|
550
|
-
extractSources(content) {
|
|
551
|
-
const sources = [];
|
|
552
|
-
// Match @source "path"; or @source 'path';
|
|
553
|
-
const simpleRegex = /@source\s+['"]([^'"]+)['"];?/g;
|
|
554
|
-
let match;
|
|
555
|
-
while ((match = simpleRegex.exec(content)) !== null) {
|
|
556
|
-
sources.push(match[1]);
|
|
557
|
-
}
|
|
558
|
-
// Match @source inline("path"); or @source inline('path');
|
|
559
|
-
const inlineRegex = /@source\s+inline\(\s*['"]([^'"]+)['"]\s*\);?/g;
|
|
560
|
-
while ((match = inlineRegex.exec(content)) !== null) {
|
|
561
|
-
sources.push(match[1]);
|
|
562
|
-
}
|
|
563
|
-
return sources;
|
|
564
|
-
}
|
|
565
|
-
addToTheme(theme, token) {
|
|
566
|
-
if (token.category === 'color') {
|
|
567
|
-
const name = token.name.replace('tw-', '').replace('-dark', '');
|
|
568
|
-
if (typeof token.value === 'object' && 'hex' in token.value) {
|
|
569
|
-
theme.colors[name] = token.value.hex;
|
|
570
|
-
}
|
|
571
|
-
else if (typeof token.value === 'object' && token.value.type === 'raw') {
|
|
572
|
-
// Store raw value (including oklch) as-is
|
|
573
|
-
theme.colors[name] = token.value.value;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
else if (token.category === 'spacing') {
|
|
577
|
-
const name = token.name.replace('tw-spacing-', '').replace('tw-', '');
|
|
578
|
-
if (typeof token.value === 'object' && token.value.type === 'spacing') {
|
|
579
|
-
theme.spacing[name] = `${token.value.value}${token.value.unit || ''}`;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
else if (token.category === 'border') {
|
|
583
|
-
const name = token.name.replace('tw-radius-', '').replace('tw-', '');
|
|
584
|
-
if (typeof token.value === 'object' && token.value.type === 'spacing') {
|
|
585
|
-
theme.borderRadius[name] = `${token.value.value}${token.value.unit || ''}`;
|
|
586
|
-
}
|
|
587
|
-
else if (typeof token.value === 'object' && token.value.type === 'raw') {
|
|
588
|
-
theme.borderRadius[name] = token.value.value;
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
// ============ V3 JS Config Parsing ============
|
|
593
|
-
async extractThemeFromJS(configPath) {
|
|
594
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
595
|
-
const theme = {
|
|
596
|
-
colors: {},
|
|
597
|
-
spacing: {},
|
|
598
|
-
fontSize: {},
|
|
599
|
-
fontFamily: {},
|
|
600
|
-
borderRadius: {},
|
|
601
|
-
boxShadow: {},
|
|
602
|
-
dropShadow: {},
|
|
603
|
-
keyframes: [],
|
|
604
|
-
animation: {},
|
|
605
|
-
maxWidth: {},
|
|
606
|
-
plugins: [],
|
|
607
|
-
};
|
|
608
|
-
// Extract colors from theme.extend.colors or theme.colors
|
|
609
|
-
const colorMatches = this.extractObjectFromConfig(content, 'colors');
|
|
610
|
-
if (colorMatches) {
|
|
611
|
-
theme.colors = this.parseObjectLiteral(colorMatches);
|
|
612
|
-
}
|
|
613
|
-
// Extract spacing
|
|
614
|
-
const spacingMatches = this.extractObjectFromConfig(content, 'spacing');
|
|
615
|
-
if (spacingMatches) {
|
|
616
|
-
theme.spacing = this.parseObjectLiteral(spacingMatches);
|
|
617
|
-
}
|
|
618
|
-
// Extract fontSize (handles both simple values and tuples)
|
|
619
|
-
const fontSizeMatches = this.extractObjectFromConfig(content, 'fontSize');
|
|
620
|
-
if (fontSizeMatches) {
|
|
621
|
-
theme.fontSize = this.parseFontSizeObject(fontSizeMatches);
|
|
622
|
-
}
|
|
623
|
-
// Extract fontFamily (handles array syntax)
|
|
624
|
-
const fontFamilyMatches = this.extractObjectFromConfig(content, 'fontFamily');
|
|
625
|
-
if (fontFamilyMatches) {
|
|
626
|
-
theme.fontFamily = this.parseFontFamilyObject(fontFamilyMatches);
|
|
627
|
-
}
|
|
628
|
-
// Extract borderRadius
|
|
629
|
-
const borderRadiusMatches = this.extractObjectFromConfig(content, 'borderRadius');
|
|
630
|
-
if (borderRadiusMatches) {
|
|
631
|
-
theme.borderRadius = this.parseObjectLiteral(borderRadiusMatches);
|
|
632
|
-
}
|
|
633
|
-
// Extract boxShadow (handles multi-line values)
|
|
634
|
-
const boxShadowMatches = this.extractObjectFromConfig(content, 'boxShadow');
|
|
635
|
-
if (boxShadowMatches) {
|
|
636
|
-
theme.boxShadow = this.parseMultiLineObjectLiteral(boxShadowMatches);
|
|
637
|
-
}
|
|
638
|
-
// Extract dropShadow
|
|
639
|
-
const dropShadowMatches = this.extractObjectFromConfig(content, 'dropShadow');
|
|
640
|
-
if (dropShadowMatches) {
|
|
641
|
-
theme.dropShadow = this.parseMultiLineObjectLiteral(dropShadowMatches);
|
|
642
|
-
}
|
|
643
|
-
// Extract keyframes (just get the names)
|
|
644
|
-
const keyframesMatches = this.extractObjectFromConfig(content, 'keyframes');
|
|
645
|
-
if (keyframesMatches) {
|
|
646
|
-
theme.keyframes = this.extractKeyframeNames(keyframesMatches);
|
|
647
|
-
}
|
|
648
|
-
// Extract animation
|
|
649
|
-
const animationMatches = this.extractObjectFromConfig(content, 'animation');
|
|
650
|
-
if (animationMatches) {
|
|
651
|
-
theme.animation = this.parseObjectLiteral(animationMatches);
|
|
652
|
-
}
|
|
653
|
-
// Extract maxWidth
|
|
654
|
-
const maxWidthMatches = this.extractObjectFromConfig(content, 'maxWidth');
|
|
655
|
-
if (maxWidthMatches) {
|
|
656
|
-
theme.maxWidth = this.parseObjectLiteral(maxWidthMatches);
|
|
657
|
-
}
|
|
658
|
-
// Extract plugins from the plugins array
|
|
659
|
-
theme.plugins = this.extractPluginsFromJS(content);
|
|
660
|
-
// Extract container configuration
|
|
661
|
-
const containerMatches = this.extractObjectFromConfig(content, 'container');
|
|
662
|
-
if (containerMatches) {
|
|
663
|
-
theme.container = this.parseContainerConfig(containerMatches);
|
|
664
|
-
}
|
|
665
|
-
// Extract screens configuration - check both theme.screens and theme.extend.screens
|
|
666
|
-
theme.screens = {};
|
|
667
|
-
const allScreensMatches = this.extractAllObjectsFromConfig(content, 'screens');
|
|
668
|
-
for (const screensContent of allScreensMatches) {
|
|
669
|
-
const parsed = this.parseObjectLiteral(screensContent);
|
|
670
|
-
Object.assign(theme.screens, parsed);
|
|
671
|
-
}
|
|
672
|
-
// Extract darkMode configuration
|
|
673
|
-
theme.darkMode = this.extractDarkMode(content);
|
|
674
|
-
// Extract zIndex
|
|
675
|
-
const zIndexMatches = this.extractObjectFromConfig(content, 'zIndex');
|
|
676
|
-
if (zIndexMatches) {
|
|
677
|
-
theme.zIndex = this.parseObjectLiteral(zIndexMatches);
|
|
678
|
-
}
|
|
679
|
-
// Extract lineHeight
|
|
680
|
-
const lineHeightMatches = this.extractObjectFromConfig(content, 'lineHeight');
|
|
681
|
-
if (lineHeightMatches) {
|
|
682
|
-
theme.lineHeight = this.parseObjectLiteral(lineHeightMatches);
|
|
683
|
-
}
|
|
684
|
-
// Extract letterSpacing
|
|
685
|
-
const letterSpacingMatches = this.extractObjectFromConfig(content, 'letterSpacing');
|
|
686
|
-
if (letterSpacingMatches) {
|
|
687
|
-
theme.letterSpacing = this.parseObjectLiteral(letterSpacingMatches);
|
|
688
|
-
}
|
|
689
|
-
// Extract opacity
|
|
690
|
-
const opacityMatches = this.extractObjectFromConfig(content, 'opacity');
|
|
691
|
-
if (opacityMatches) {
|
|
692
|
-
theme.opacity = this.parseObjectLiteral(opacityMatches);
|
|
693
|
-
}
|
|
694
|
-
// Extract transitionDuration
|
|
695
|
-
const transitionDurationMatches = this.extractObjectFromConfig(content, 'transitionDuration');
|
|
696
|
-
if (transitionDurationMatches) {
|
|
697
|
-
theme.transitionDuration = this.parseObjectLiteral(transitionDurationMatches);
|
|
698
|
-
}
|
|
699
|
-
// Extract backgroundImage
|
|
700
|
-
const backgroundImageMatches = this.extractObjectFromConfig(content, 'backgroundImage');
|
|
701
|
-
if (backgroundImageMatches) {
|
|
702
|
-
theme.backgroundImage = this.parseMultiLineObjectLiteral(backgroundImageMatches);
|
|
703
|
-
}
|
|
704
|
-
// Extract fontWeight
|
|
705
|
-
const fontWeightMatches = this.extractObjectFromConfig(content, 'fontWeight');
|
|
706
|
-
if (fontWeightMatches) {
|
|
707
|
-
theme.fontWeight = this.parseObjectLiteral(fontWeightMatches);
|
|
708
|
-
}
|
|
709
|
-
return theme;
|
|
710
|
-
}
|
|
711
|
-
/**
|
|
712
|
-
* Parse container configuration object
|
|
713
|
-
*/
|
|
714
|
-
parseContainerConfig(content) {
|
|
715
|
-
const config = {};
|
|
716
|
-
// Extract center: true/false
|
|
717
|
-
const centerMatch = content.match(/center\s*:\s*(true|false)/);
|
|
718
|
-
if (centerMatch) {
|
|
719
|
-
config.center = centerMatch[1] === 'true';
|
|
720
|
-
}
|
|
721
|
-
// Extract padding as string or object
|
|
722
|
-
const paddingStrMatch = content.match(/padding\s*:\s*["']([^"']+)["']/);
|
|
723
|
-
if (paddingStrMatch) {
|
|
724
|
-
config.padding = paddingStrMatch[1];
|
|
725
|
-
}
|
|
726
|
-
// Extract screens object inside container
|
|
727
|
-
const screensMatch = this.extractObjectFromConfig(content, 'screens');
|
|
728
|
-
if (screensMatch) {
|
|
729
|
-
config.screens = this.parseObjectLiteral(screensMatch);
|
|
730
|
-
}
|
|
731
|
-
return config;
|
|
732
|
-
}
|
|
733
|
-
/**
|
|
734
|
-
* Extract darkMode configuration
|
|
735
|
-
*/
|
|
736
|
-
extractDarkMode(content) {
|
|
737
|
-
// Match darkMode: ["class"] or darkMode: ["class", "[data-theme='dark']"]
|
|
738
|
-
const arrayMatch = content.match(/darkMode\s*:\s*\[([^\]]+)\]/);
|
|
739
|
-
if (arrayMatch) {
|
|
740
|
-
const items = [];
|
|
741
|
-
const itemRegex = /["']([^"']+)["']/g;
|
|
742
|
-
let match;
|
|
743
|
-
while ((match = itemRegex.exec(arrayMatch[1])) !== null) {
|
|
744
|
-
items.push(match[1]);
|
|
745
|
-
}
|
|
746
|
-
return items;
|
|
747
|
-
}
|
|
748
|
-
// Match darkMode: "class" or darkMode: "selector" or darkMode: 'media'
|
|
749
|
-
const stringMatch = content.match(/darkMode\s*:\s*["']([^"']+)["']/);
|
|
750
|
-
if (stringMatch) {
|
|
751
|
-
return stringMatch[1];
|
|
752
|
-
}
|
|
753
|
-
return undefined;
|
|
754
|
-
}
|
|
755
|
-
extractObjectFromConfig(content, key) {
|
|
756
|
-
// For deeply nested structures like keyframes, use balanced brace matching
|
|
757
|
-
const keyPatterns = [
|
|
758
|
-
new RegExp(`${key}:\\s*\\{`, 'g'),
|
|
759
|
-
new RegExp(`['"]${key}['"]:\\s*\\{`, 'g'),
|
|
760
|
-
];
|
|
761
|
-
for (const pattern of keyPatterns) {
|
|
762
|
-
const keyMatch = pattern.exec(content);
|
|
763
|
-
if (keyMatch) {
|
|
764
|
-
const startIdx = keyMatch.index + keyMatch[0].length;
|
|
765
|
-
let depth = 1;
|
|
766
|
-
let endIdx = startIdx;
|
|
767
|
-
// Find matching closing brace
|
|
768
|
-
for (let i = startIdx; i < content.length && depth > 0; i++) {
|
|
769
|
-
if (content[i] === '{')
|
|
770
|
-
depth++;
|
|
771
|
-
else if (content[i] === '}')
|
|
772
|
-
depth--;
|
|
773
|
-
endIdx = i;
|
|
774
|
-
}
|
|
775
|
-
if (depth === 0) {
|
|
776
|
-
return content.substring(startIdx, endIdx);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
/**
|
|
783
|
-
* Extract ALL occurrences of an object with a given key from config
|
|
784
|
-
* This handles cases where same key appears in both theme and theme.extend
|
|
785
|
-
*/
|
|
786
|
-
extractAllObjectsFromConfig(content, key) {
|
|
787
|
-
const results = [];
|
|
788
|
-
const keyPatterns = [
|
|
789
|
-
new RegExp(`${key}:\\s*\\{`, 'g'),
|
|
790
|
-
new RegExp(`['"]${key}['"]:\\s*\\{`, 'g'),
|
|
791
|
-
];
|
|
792
|
-
for (const pattern of keyPatterns) {
|
|
793
|
-
let keyMatch;
|
|
794
|
-
while ((keyMatch = pattern.exec(content)) !== null) {
|
|
795
|
-
const startIdx = keyMatch.index + keyMatch[0].length;
|
|
796
|
-
let depth = 1;
|
|
797
|
-
let endIdx = startIdx;
|
|
798
|
-
// Find matching closing brace
|
|
799
|
-
for (let i = startIdx; i < content.length && depth > 0; i++) {
|
|
800
|
-
if (content[i] === '{')
|
|
801
|
-
depth++;
|
|
802
|
-
else if (content[i] === '}')
|
|
803
|
-
depth--;
|
|
804
|
-
endIdx = i;
|
|
805
|
-
}
|
|
806
|
-
if (depth === 0) {
|
|
807
|
-
results.push(content.substring(startIdx, endIdx));
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
}
|
|
811
|
-
return results;
|
|
812
|
-
}
|
|
813
|
-
parseObjectLiteral(content) {
|
|
814
|
-
const result = {};
|
|
815
|
-
// Simple key-value extraction
|
|
816
|
-
const kvPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*['"]([^'"]+)['"]/g;
|
|
817
|
-
let match;
|
|
818
|
-
while ((match = kvPattern.exec(content)) !== null) {
|
|
819
|
-
result[match[1]] = match[2];
|
|
820
|
-
}
|
|
821
|
-
// Also try to match nested objects for color scales
|
|
822
|
-
const nestedPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\{([^{}]+)\}/g;
|
|
823
|
-
while ((match = nestedPattern.exec(content)) !== null) {
|
|
824
|
-
const nestedObj = {};
|
|
825
|
-
const nestedContent = match[2];
|
|
826
|
-
const nestedKv = /['"]?(\w+[-\w]*)['"]?\s*:\s*['"]([^'"]+)['"]/g;
|
|
827
|
-
let nestedMatch;
|
|
828
|
-
while ((nestedMatch = nestedKv.exec(nestedContent)) !== null) {
|
|
829
|
-
nestedObj[nestedMatch[1]] = nestedMatch[2];
|
|
830
|
-
}
|
|
831
|
-
if (Object.keys(nestedObj).length > 0) {
|
|
832
|
-
result[match[1]] = nestedObj;
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
return result;
|
|
836
|
-
}
|
|
837
|
-
themeToTokens(theme, configPath) {
|
|
838
|
-
const tokens = [];
|
|
839
|
-
const relativePath = configPath.replace(this.projectRoot + '/', '');
|
|
840
|
-
const source = {
|
|
841
|
-
type: 'json',
|
|
842
|
-
path: relativePath,
|
|
843
|
-
};
|
|
844
|
-
// Convert colors to tokens
|
|
845
|
-
if (theme.colors) {
|
|
846
|
-
for (const [name, value] of Object.entries(theme.colors)) {
|
|
847
|
-
if (typeof value === 'string') {
|
|
848
|
-
tokens.push(this.createColorToken(name, value, source, 'v3'));
|
|
849
|
-
}
|
|
850
|
-
else if (typeof value === 'object') {
|
|
851
|
-
for (const [shade, color] of Object.entries(value)) {
|
|
852
|
-
// Handle DEFAULT key - use just the parent name for the token
|
|
853
|
-
if (shade === 'DEFAULT') {
|
|
854
|
-
tokens.push(this.createColorToken(name, color, source, 'v3'));
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
tokens.push(this.createColorToken(`${name}-${shade}`, color, source, 'v3'));
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
// Convert spacing to tokens
|
|
864
|
-
if (theme.spacing) {
|
|
865
|
-
for (const [name, value] of Object.entries(theme.spacing)) {
|
|
866
|
-
tokens.push(this.createSpacingToken(name, value, source));
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
// Convert boxShadow to tokens
|
|
870
|
-
if (theme.boxShadow) {
|
|
871
|
-
for (const [name, value] of Object.entries(theme.boxShadow)) {
|
|
872
|
-
tokens.push(this.createShadowToken(name, value, source));
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
// Convert dropShadow to tokens
|
|
876
|
-
if (theme.dropShadow) {
|
|
877
|
-
for (const [name, value] of Object.entries(theme.dropShadow)) {
|
|
878
|
-
tokens.push(this.createDropShadowToken(name, value, source));
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
// Convert borderRadius to tokens
|
|
882
|
-
if (theme.borderRadius) {
|
|
883
|
-
for (const [name, value] of Object.entries(theme.borderRadius)) {
|
|
884
|
-
tokens.push(this.createBorderRadiusToken(name, value, source));
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
// Convert fontFamily to tokens
|
|
888
|
-
if (theme.fontFamily) {
|
|
889
|
-
for (const [name, fonts] of Object.entries(theme.fontFamily)) {
|
|
890
|
-
if (Array.isArray(fonts)) {
|
|
891
|
-
tokens.push(this.createFontFamilyToken(name, fonts, source));
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
|
-
// Convert backgroundImage to tokens
|
|
896
|
-
if (theme.backgroundImage) {
|
|
897
|
-
for (const [name, value] of Object.entries(theme.backgroundImage)) {
|
|
898
|
-
tokens.push(this.createBackgroundImageToken(name, value, source));
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
// Convert fontWeight to tokens
|
|
902
|
-
if (theme.fontWeight) {
|
|
903
|
-
for (const [name, value] of Object.entries(theme.fontWeight)) {
|
|
904
|
-
tokens.push(this.createFontWeightToken(name, value, source));
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
// Convert fontSize to tokens (handles both string and tuple values)
|
|
908
|
-
if (theme.fontSize) {
|
|
909
|
-
for (const [name, value] of Object.entries(theme.fontSize)) {
|
|
910
|
-
tokens.push(this.createFontSizeToken(name, value, source));
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
return tokens;
|
|
914
|
-
}
|
|
915
|
-
createColorToken(name, value, source, version, mode) {
|
|
916
|
-
const tokenName = mode === 'dark' ? `tw-${name}-dark` : `tw-${name}`;
|
|
917
|
-
const tags = ['tailwind', version];
|
|
918
|
-
if (mode)
|
|
919
|
-
tags.push(mode);
|
|
920
|
-
// Check for oklch colors (v4) - store as raw since schema doesn't support oklch
|
|
921
|
-
if (value.startsWith('oklch(')) {
|
|
922
|
-
const colorTags = [...tags, 'oklch'];
|
|
923
|
-
// Check for alpha values (oklch with / for alpha)
|
|
924
|
-
if (value.includes('/')) {
|
|
925
|
-
colorTags.push('alpha');
|
|
926
|
-
}
|
|
927
|
-
return {
|
|
928
|
-
id: createTokenId(source, tokenName),
|
|
929
|
-
name: tokenName,
|
|
930
|
-
category: 'color',
|
|
931
|
-
value: { type: 'raw', value },
|
|
932
|
-
source,
|
|
933
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
934
|
-
usedBy: [],
|
|
935
|
-
metadata: { tags: colorTags },
|
|
936
|
-
scannedAt: new Date(),
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
// Check for hsl colors - store as raw with hsl tag
|
|
940
|
-
if (value.startsWith('hsl(') || value.startsWith('hsla(')) {
|
|
941
|
-
const colorTags = [...tags, 'hsl'];
|
|
942
|
-
if (value.includes('/') || value.startsWith('hsla(')) {
|
|
943
|
-
colorTags.push('alpha');
|
|
944
|
-
}
|
|
945
|
-
// Check for var() inside hsl() - e.g., hsl(var(--border))
|
|
946
|
-
const hslVarMatch = value.match(/var\(--([^,)]+)\)/);
|
|
947
|
-
if (hslVarMatch) {
|
|
948
|
-
colorTags.push('reference');
|
|
949
|
-
const refName = hslVarMatch[1];
|
|
950
|
-
return {
|
|
951
|
-
id: createTokenId(source, tokenName),
|
|
952
|
-
name: tokenName,
|
|
953
|
-
category: 'color',
|
|
954
|
-
value: { type: 'raw', value },
|
|
955
|
-
source,
|
|
956
|
-
aliases: mode === 'dark' ? [`${name}-dark`, refName] : [name, refName],
|
|
957
|
-
usedBy: [],
|
|
958
|
-
metadata: { tags: colorTags },
|
|
959
|
-
scannedAt: new Date(),
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
id: createTokenId(source, tokenName),
|
|
964
|
-
name: tokenName,
|
|
965
|
-
category: 'color',
|
|
966
|
-
value: { type: 'raw', value },
|
|
967
|
-
source,
|
|
968
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
969
|
-
usedBy: [],
|
|
970
|
-
metadata: { tags: colorTags },
|
|
971
|
-
scannedAt: new Date(),
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
// Check for rgb colors - store as raw with rgb tag
|
|
975
|
-
if (value.startsWith('rgb(') || value.startsWith('rgba(')) {
|
|
976
|
-
const colorTags = [...tags, 'rgb'];
|
|
977
|
-
if (value.includes('/') || value.startsWith('rgba(') || value.includes('<alpha-value>')) {
|
|
978
|
-
colorTags.push('alpha');
|
|
979
|
-
}
|
|
980
|
-
return {
|
|
981
|
-
id: createTokenId(source, tokenName),
|
|
982
|
-
name: tokenName,
|
|
983
|
-
category: 'color',
|
|
984
|
-
value: { type: 'raw', value },
|
|
985
|
-
source,
|
|
986
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
987
|
-
usedBy: [],
|
|
988
|
-
metadata: { tags: colorTags },
|
|
989
|
-
scannedAt: new Date(),
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
// Check for var() references - store as raw with reference info in aliases
|
|
993
|
-
if (value.startsWith('var(')) {
|
|
994
|
-
const refMatch = value.match(/var\(--([^)]+)\)/);
|
|
995
|
-
const refName = refMatch?.[1] || '';
|
|
996
|
-
return {
|
|
997
|
-
id: createTokenId(source, tokenName),
|
|
998
|
-
name: tokenName,
|
|
999
|
-
category: 'color',
|
|
1000
|
-
value: { type: 'raw', value },
|
|
1001
|
-
source,
|
|
1002
|
-
aliases: mode === 'dark' ? [`${name}-dark`, refName] : [name, refName],
|
|
1003
|
-
usedBy: [],
|
|
1004
|
-
metadata: { tags: [...tags, 'reference'] },
|
|
1005
|
-
scannedAt: new Date(),
|
|
1006
|
-
};
|
|
1007
|
-
}
|
|
1008
|
-
// Check for color-mix() expressions (CSS Color Level 5)
|
|
1009
|
-
if (value.startsWith('color-mix(')) {
|
|
1010
|
-
const colorTags = [...tags, 'color-mix'];
|
|
1011
|
-
return {
|
|
1012
|
-
id: createTokenId(source, tokenName),
|
|
1013
|
-
name: tokenName,
|
|
1014
|
-
category: 'color',
|
|
1015
|
-
value: { type: 'raw', value },
|
|
1016
|
-
source,
|
|
1017
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
1018
|
-
usedBy: [],
|
|
1019
|
-
metadata: { tags: colorTags },
|
|
1020
|
-
scannedAt: new Date(),
|
|
1021
|
-
};
|
|
1022
|
-
}
|
|
1023
|
-
// Check for other CSS color functions that should be raw
|
|
1024
|
-
// This includes light-dark(), lab(), lch(), color(), hwb(), etc.
|
|
1025
|
-
const cssColorFunctions = [
|
|
1026
|
-
'light-dark(',
|
|
1027
|
-
'lab(',
|
|
1028
|
-
'lch(',
|
|
1029
|
-
'color(',
|
|
1030
|
-
'hwb(',
|
|
1031
|
-
];
|
|
1032
|
-
for (const fn of cssColorFunctions) {
|
|
1033
|
-
if (value.startsWith(fn)) {
|
|
1034
|
-
return {
|
|
1035
|
-
id: createTokenId(source, tokenName),
|
|
1036
|
-
name: tokenName,
|
|
1037
|
-
category: 'color',
|
|
1038
|
-
value: { type: 'raw', value },
|
|
1039
|
-
source,
|
|
1040
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
1041
|
-
usedBy: [],
|
|
1042
|
-
metadata: { tags: [...tags, fn.replace('(', '')] },
|
|
1043
|
-
scannedAt: new Date(),
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
// Only treat as hex if it actually starts with #
|
|
1048
|
-
if (value.startsWith('#')) {
|
|
1049
|
-
return {
|
|
1050
|
-
id: createTokenId(source, tokenName),
|
|
1051
|
-
name: tokenName,
|
|
1052
|
-
category: 'color',
|
|
1053
|
-
value: { type: 'color', hex: value },
|
|
1054
|
-
source,
|
|
1055
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
1056
|
-
usedBy: [],
|
|
1057
|
-
metadata: { tags },
|
|
1058
|
-
scannedAt: new Date(),
|
|
1059
|
-
};
|
|
1060
|
-
}
|
|
1061
|
-
// Default to raw for any other color format we don't recognize
|
|
1062
|
-
return {
|
|
1063
|
-
id: createTokenId(source, tokenName),
|
|
1064
|
-
name: tokenName,
|
|
1065
|
-
category: 'color',
|
|
1066
|
-
value: { type: 'raw', value },
|
|
1067
|
-
source,
|
|
1068
|
-
aliases: mode === 'dark' ? [`${name}-dark`] : [name],
|
|
1069
|
-
usedBy: [],
|
|
1070
|
-
metadata: { tags },
|
|
1071
|
-
scannedAt: new Date(),
|
|
1072
|
-
};
|
|
1073
|
-
}
|
|
1074
|
-
createSpacingToken(name, value, source) {
|
|
1075
|
-
const numMatch = value.match(/^([\d.]+)(rem|px|em)?$/);
|
|
1076
|
-
if (numMatch) {
|
|
1077
|
-
const num = parseFloat(numMatch[1]);
|
|
1078
|
-
const unit = numMatch[2] || 'px';
|
|
1079
|
-
return {
|
|
1080
|
-
id: createTokenId(source, `tw-spacing-${name}`),
|
|
1081
|
-
name: `tw-spacing-${name}`,
|
|
1082
|
-
category: 'spacing',
|
|
1083
|
-
value: { type: 'spacing', value: num, unit },
|
|
1084
|
-
source,
|
|
1085
|
-
aliases: [name],
|
|
1086
|
-
usedBy: [],
|
|
1087
|
-
metadata: { tags: ['tailwind'] },
|
|
1088
|
-
scannedAt: new Date(),
|
|
1089
|
-
};
|
|
1090
|
-
}
|
|
1091
|
-
return this.createRawToken(`spacing-${name}`, value, 'spacing', source);
|
|
1092
|
-
}
|
|
1093
|
-
createShadowToken(name, value, source) {
|
|
1094
|
-
return {
|
|
1095
|
-
id: createTokenId(source, `tw-shadow-${name}`),
|
|
1096
|
-
name: `tw-shadow-${name}`,
|
|
1097
|
-
category: 'shadow',
|
|
1098
|
-
value: { type: 'raw', value },
|
|
1099
|
-
source,
|
|
1100
|
-
aliases: [name],
|
|
1101
|
-
usedBy: [],
|
|
1102
|
-
metadata: { tags: ['tailwind'] },
|
|
1103
|
-
scannedAt: new Date(),
|
|
1104
|
-
};
|
|
1105
|
-
}
|
|
1106
|
-
/**
|
|
1107
|
-
* Create a border radius token with proper tags for calc() and var() expressions
|
|
1108
|
-
*/
|
|
1109
|
-
createBorderRadiusToken(name, value, source) {
|
|
1110
|
-
const tags = ['tailwind'];
|
|
1111
|
-
// Check for calc() expressions
|
|
1112
|
-
if (value.startsWith('calc(')) {
|
|
1113
|
-
tags.push('calc');
|
|
1114
|
-
}
|
|
1115
|
-
// Check for var() references
|
|
1116
|
-
if (value.includes('var(')) {
|
|
1117
|
-
tags.push('reference');
|
|
1118
|
-
}
|
|
1119
|
-
return {
|
|
1120
|
-
id: createTokenId(source, `tw-radius-${name}`),
|
|
1121
|
-
name: `tw-radius-${name}`,
|
|
1122
|
-
category: 'border',
|
|
1123
|
-
value: { type: 'raw', value },
|
|
1124
|
-
source,
|
|
1125
|
-
aliases: [name],
|
|
1126
|
-
usedBy: [],
|
|
1127
|
-
metadata: { tags },
|
|
1128
|
-
scannedAt: new Date(),
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
createRawToken(name, value, category, source) {
|
|
1132
|
-
return {
|
|
1133
|
-
id: createTokenId(source, `tw-${name}`),
|
|
1134
|
-
name: `tw-${name}`,
|
|
1135
|
-
category,
|
|
1136
|
-
value: { type: 'raw', value },
|
|
1137
|
-
source,
|
|
1138
|
-
aliases: [],
|
|
1139
|
-
usedBy: [],
|
|
1140
|
-
metadata: { tags: ['tailwind'] },
|
|
1141
|
-
scannedAt: new Date(),
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
/**
|
|
1145
|
-
* Parse font family objects that contain array values
|
|
1146
|
-
* e.g., mono: ["Menlo", "Consolas", "monospace"]
|
|
1147
|
-
*/
|
|
1148
|
-
parseFontFamilyObject(content) {
|
|
1149
|
-
const result = {};
|
|
1150
|
-
// Match key: [array values]
|
|
1151
|
-
const arrayPattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\[([^\]]+)\]/g;
|
|
1152
|
-
let match;
|
|
1153
|
-
while ((match = arrayPattern.exec(content)) !== null) {
|
|
1154
|
-
const key = match[1];
|
|
1155
|
-
const arrayContent = match[2];
|
|
1156
|
-
// Extract individual strings from the array
|
|
1157
|
-
const strings = [];
|
|
1158
|
-
const stringPattern = /['"]([^'"]+)['"]/g;
|
|
1159
|
-
let strMatch;
|
|
1160
|
-
while ((strMatch = stringPattern.exec(arrayContent)) !== null) {
|
|
1161
|
-
strings.push(strMatch[1]);
|
|
1162
|
-
}
|
|
1163
|
-
if (strings.length > 0) {
|
|
1164
|
-
result[key] = strings;
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
return result;
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Parse fontSize object which can contain:
|
|
1171
|
-
* - Simple string values: 'xs': '0.75rem'
|
|
1172
|
-
* - Tuple values: 'xs': ['0.75rem', { lineHeight: '1rem' }]
|
|
1173
|
-
* - Extended tuples: 'hero': ['4rem', { lineHeight: '1.1', letterSpacing: '-0.05em', fontWeight: '700' }]
|
|
1174
|
-
*/
|
|
1175
|
-
parseFontSizeObject(content) {
|
|
1176
|
-
const result = {};
|
|
1177
|
-
// First, extract simple string values: 'key': 'value'
|
|
1178
|
-
const simpleKvPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]\s*(?:,|}|$)/g;
|
|
1179
|
-
let match;
|
|
1180
|
-
while ((match = simpleKvPattern.exec(content)) !== null) {
|
|
1181
|
-
// Only add if not already parsed as tuple
|
|
1182
|
-
if (!result[match[1]]) {
|
|
1183
|
-
result[match[1]] = match[2];
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
// Then extract tuple values: 'key': ['size', { lineHeight: 'value', ... }]
|
|
1187
|
-
// Use balanced bracket matching for arrays with nested objects
|
|
1188
|
-
const tuplePattern = /['"]?([\w-]+)['"]?\s*:\s*\[/g;
|
|
1189
|
-
while ((match = tuplePattern.exec(content)) !== null) {
|
|
1190
|
-
const key = match[1];
|
|
1191
|
-
const startIdx = match.index + match[0].length;
|
|
1192
|
-
// Find the matching closing bracket for the array
|
|
1193
|
-
let depth = 1;
|
|
1194
|
-
let endIdx = startIdx;
|
|
1195
|
-
for (let i = startIdx; i < content.length && depth > 0; i++) {
|
|
1196
|
-
if (content[i] === '[')
|
|
1197
|
-
depth++;
|
|
1198
|
-
else if (content[i] === ']')
|
|
1199
|
-
depth--;
|
|
1200
|
-
endIdx = i;
|
|
1201
|
-
}
|
|
1202
|
-
if (depth === 0) {
|
|
1203
|
-
const arrayContent = content.substring(startIdx, endIdx);
|
|
1204
|
-
// Parse the tuple: first element is the size, second is the config object
|
|
1205
|
-
// Format: 'size', { lineHeight: 'value', ... }
|
|
1206
|
-
const sizeMatch = arrayContent.match(/^\s*['"]([^'"]+)['"]/);
|
|
1207
|
-
if (sizeMatch) {
|
|
1208
|
-
const size = sizeMatch[1];
|
|
1209
|
-
// Extract the config object
|
|
1210
|
-
const configMatch = arrayContent.match(/\{\s*([^}]+)\s*\}/);
|
|
1211
|
-
if (configMatch) {
|
|
1212
|
-
const configContent = configMatch[1];
|
|
1213
|
-
const config = {};
|
|
1214
|
-
// Parse each property in the config object
|
|
1215
|
-
const propPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]/g;
|
|
1216
|
-
let propMatch;
|
|
1217
|
-
while ((propMatch = propPattern.exec(configContent)) !== null) {
|
|
1218
|
-
config[propMatch[1]] = propMatch[2];
|
|
1219
|
-
}
|
|
1220
|
-
result[key] = [size, config];
|
|
1221
|
-
}
|
|
1222
|
-
else {
|
|
1223
|
-
// Array with just size value, no config object
|
|
1224
|
-
result[key] = size;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
return result;
|
|
1230
|
-
}
|
|
1231
|
-
/**
|
|
1232
|
-
* Parse object literals that may have multi-line string values
|
|
1233
|
-
* Handles cases like:
|
|
1234
|
-
* "md-dark":
|
|
1235
|
-
* "0 4px 6px...",
|
|
1236
|
-
*/
|
|
1237
|
-
parseMultiLineObjectLiteral(content) {
|
|
1238
|
-
const result = {};
|
|
1239
|
-
// First pass: simple key-value on same line
|
|
1240
|
-
const simpleKvPattern = /['"]?([\w-]+)['"]?\s*:\s*['"]([^'"]+)['"]/g;
|
|
1241
|
-
let match;
|
|
1242
|
-
while ((match = simpleKvPattern.exec(content)) !== null) {
|
|
1243
|
-
result[match[1]] = match[2];
|
|
1244
|
-
}
|
|
1245
|
-
// Second pass: key on one line, value on next line (multi-line)
|
|
1246
|
-
const multiLinePattern = /['"]?([\w-]+)['"]?\s*:\s*\n\s*['"]([^'"]+)['"]/g;
|
|
1247
|
-
while ((match = multiLinePattern.exec(content)) !== null) {
|
|
1248
|
-
result[match[1]] = match[2];
|
|
1249
|
-
}
|
|
1250
|
-
return result;
|
|
1251
|
-
}
|
|
1252
|
-
/**
|
|
1253
|
-
* Extract keyframe animation names from a keyframes object
|
|
1254
|
-
*/
|
|
1255
|
-
extractKeyframeNames(content) {
|
|
1256
|
-
const names = [];
|
|
1257
|
-
// Match keyframe names (keys that have objects as values)
|
|
1258
|
-
const keyframePattern = /['"]?(\w+[-\w]*)['"]?\s*:\s*\{/g;
|
|
1259
|
-
let match;
|
|
1260
|
-
while ((match = keyframePattern.exec(content)) !== null) {
|
|
1261
|
-
names.push(match[1]);
|
|
1262
|
-
}
|
|
1263
|
-
return names;
|
|
1264
|
-
}
|
|
1265
|
-
/**
|
|
1266
|
-
* Extract plugin names from plugins array in JS config
|
|
1267
|
-
*/
|
|
1268
|
-
extractPluginsFromJS(content) {
|
|
1269
|
-
const plugins = [];
|
|
1270
|
-
// Match require("plugin-name") or require('plugin-name')
|
|
1271
|
-
const requirePattern = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
1272
|
-
let match;
|
|
1273
|
-
while ((match = requirePattern.exec(content)) !== null) {
|
|
1274
|
-
plugins.push(match[1]);
|
|
1275
|
-
}
|
|
1276
|
-
return plugins;
|
|
1277
|
-
}
|
|
1278
|
-
/**
|
|
1279
|
-
* Create a drop shadow token
|
|
1280
|
-
*/
|
|
1281
|
-
createDropShadowToken(name, value, source) {
|
|
1282
|
-
return {
|
|
1283
|
-
id: createTokenId(source, `tw-drop-shadow-${name}`),
|
|
1284
|
-
name: `tw-drop-shadow-${name}`,
|
|
1285
|
-
category: 'shadow',
|
|
1286
|
-
value: { type: 'raw', value },
|
|
1287
|
-
source,
|
|
1288
|
-
aliases: [name],
|
|
1289
|
-
usedBy: [],
|
|
1290
|
-
metadata: { tags: ['tailwind', 'drop-shadow'] },
|
|
1291
|
-
scannedAt: new Date(),
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Create a font family token
|
|
1296
|
-
*/
|
|
1297
|
-
createFontFamilyToken(name, fonts, source) {
|
|
1298
|
-
return {
|
|
1299
|
-
id: createTokenId(source, `tw-font-${name}`),
|
|
1300
|
-
name: `tw-font-${name}`,
|
|
1301
|
-
category: 'typography',
|
|
1302
|
-
value: { type: 'raw', value: fonts.join(', ') },
|
|
1303
|
-
source,
|
|
1304
|
-
aliases: [name],
|
|
1305
|
-
usedBy: [],
|
|
1306
|
-
metadata: { tags: ['tailwind', 'font-family'] },
|
|
1307
|
-
scannedAt: new Date(),
|
|
1308
|
-
};
|
|
1309
|
-
}
|
|
1310
|
-
/**
|
|
1311
|
-
* Create a background image token
|
|
1312
|
-
*/
|
|
1313
|
-
createBackgroundImageToken(name, value, source) {
|
|
1314
|
-
return {
|
|
1315
|
-
id: createTokenId(source, `tw-bg-${name}`),
|
|
1316
|
-
name: `tw-bg-${name}`,
|
|
1317
|
-
category: 'other',
|
|
1318
|
-
value: { type: 'raw', value },
|
|
1319
|
-
source,
|
|
1320
|
-
aliases: [name],
|
|
1321
|
-
usedBy: [],
|
|
1322
|
-
metadata: { tags: ['tailwind', 'background-image'] },
|
|
1323
|
-
scannedAt: new Date(),
|
|
1324
|
-
};
|
|
1325
|
-
}
|
|
1326
|
-
/**
|
|
1327
|
-
* Create a font weight token
|
|
1328
|
-
*/
|
|
1329
|
-
createFontWeightToken(name, value, source) {
|
|
1330
|
-
return {
|
|
1331
|
-
id: createTokenId(source, `tw-font-weight-${name}`),
|
|
1332
|
-
name: `tw-font-weight-${name}`,
|
|
1333
|
-
category: 'typography',
|
|
1334
|
-
value: { type: 'raw', value },
|
|
1335
|
-
source,
|
|
1336
|
-
aliases: [name],
|
|
1337
|
-
usedBy: [],
|
|
1338
|
-
metadata: { tags: ['tailwind', 'font-weight'] },
|
|
1339
|
-
scannedAt: new Date(),
|
|
1340
|
-
};
|
|
1341
|
-
}
|
|
1342
|
-
/**
|
|
1343
|
-
* Create a font size token
|
|
1344
|
-
* Handles both simple string values and tuple values [size, { lineHeight, ... }]
|
|
1345
|
-
*/
|
|
1346
|
-
createFontSizeToken(name, value, source) {
|
|
1347
|
-
if (typeof value === 'string') {
|
|
1348
|
-
return {
|
|
1349
|
-
id: createTokenId(source, `tw-text-${name}`),
|
|
1350
|
-
name: `tw-text-${name}`,
|
|
1351
|
-
category: 'typography',
|
|
1352
|
-
value: { type: 'raw', value },
|
|
1353
|
-
source,
|
|
1354
|
-
aliases: [name],
|
|
1355
|
-
usedBy: [],
|
|
1356
|
-
metadata: { tags: ['tailwind', 'font-size'] },
|
|
1357
|
-
scannedAt: new Date(),
|
|
1358
|
-
};
|
|
1359
|
-
}
|
|
1360
|
-
// Handle tuple: [size, { lineHeight, letterSpacing, fontWeight }]
|
|
1361
|
-
const [size, config] = value;
|
|
1362
|
-
const configStr = Object.entries(config)
|
|
1363
|
-
.map(([k, v]) => `${k}: ${v}`)
|
|
1364
|
-
.join(', ');
|
|
1365
|
-
return {
|
|
1366
|
-
id: createTokenId(source, `tw-text-${name}`),
|
|
1367
|
-
name: `tw-text-${name}`,
|
|
1368
|
-
category: 'typography',
|
|
1369
|
-
value: { type: 'raw', value: `${size} [${configStr}]` },
|
|
1370
|
-
source,
|
|
1371
|
-
aliases: [name],
|
|
1372
|
-
usedBy: [],
|
|
1373
|
-
metadata: {
|
|
1374
|
-
tags: ['tailwind', 'font-size', 'tuple'],
|
|
1375
|
-
description: `Font size ${size} with ${configStr}`,
|
|
1376
|
-
},
|
|
1377
|
-
scannedAt: new Date(),
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
//# sourceMappingURL=config-parser.js.map
|