@nuasite/cms-marker 0.0.64 → 0.0.66

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 (38) hide show
  1. package/dist/types/build-processor.d.ts +2 -1
  2. package/dist/types/build-processor.d.ts.map +1 -1
  3. package/dist/types/component-registry.d.ts.map +1 -1
  4. package/dist/types/config.d.ts +19 -0
  5. package/dist/types/config.d.ts.map +1 -0
  6. package/dist/types/dev-middleware.d.ts +10 -2
  7. package/dist/types/dev-middleware.d.ts.map +1 -1
  8. package/dist/types/error-collector.d.ts +56 -0
  9. package/dist/types/error-collector.d.ts.map +1 -0
  10. package/dist/types/html-processor.d.ts.map +1 -1
  11. package/dist/types/index.d.ts +2 -1
  12. package/dist/types/index.d.ts.map +1 -1
  13. package/dist/types/manifest-writer.d.ts +11 -2
  14. package/dist/types/manifest-writer.d.ts.map +1 -1
  15. package/dist/types/source-finder.d.ts +18 -3
  16. package/dist/types/source-finder.d.ts.map +1 -1
  17. package/dist/types/tailwind-colors.d.ts +6 -30
  18. package/dist/types/tailwind-colors.d.ts.map +1 -1
  19. package/dist/types/tsconfig.tsbuildinfo +1 -1
  20. package/dist/types/types.d.ts +25 -9
  21. package/dist/types/types.d.ts.map +1 -1
  22. package/dist/types/vite-plugin.d.ts.map +1 -1
  23. package/package.json +2 -1
  24. package/src/build-processor.ts +73 -19
  25. package/src/component-registry.ts +2 -0
  26. package/src/config.ts +29 -0
  27. package/src/dev-middleware.ts +12 -4
  28. package/src/error-collector.ts +106 -0
  29. package/src/html-processor.ts +55 -37
  30. package/src/index.ts +20 -4
  31. package/src/manifest-writer.ts +38 -7
  32. package/src/source-finder.ts +1003 -295
  33. package/src/tailwind-colors.ts +511 -121
  34. package/src/types.ts +27 -9
  35. package/src/vite-plugin.ts +4 -12
  36. package/dist/types/astro-transform.d.ts +0 -21
  37. package/dist/types/astro-transform.d.ts.map +0 -1
  38. package/src/astro-transform.ts +0 -205
@@ -1,10 +1,10 @@
1
1
  import fs from 'node:fs/promises'
2
2
  import path from 'node:path'
3
- import type { AvailableColors, ColorClasses, TailwindColor } from './types'
3
+ import { getProjectRoot } from './config'
4
+ import type { AvailableColors, AvailableTextStyles, ColorClasses, TailwindColor, TextStyleValue } from './types'
4
5
 
5
6
  /**
6
7
  * Default Tailwind CSS v4 color names.
7
- * These are available by default in Tailwind v4.
8
8
  */
9
9
  export const DEFAULT_TAILWIND_COLORS = [
10
10
  'slate',
@@ -41,50 +41,384 @@ export const STANDARD_SHADES = ['50', '100', '200', '300', '400', '500', '600',
41
41
  */
42
42
  export const SPECIAL_COLORS = ['transparent', 'current', 'inherit', 'white', 'black'] as const
43
43
 
44
+ /**
45
+ * Complete Tailwind v4 default color palette with all shade values.
46
+ */
47
+ const DEFAULT_COLOR_VALUES: Record<string, Record<string, string>> = {
48
+ slate: {
49
+ '50': '#f8fafc',
50
+ '100': '#f1f5f9',
51
+ '200': '#e2e8f0',
52
+ '300': '#cbd5e1',
53
+ '400': '#94a3b8',
54
+ '500': '#64748b',
55
+ '600': '#475569',
56
+ '700': '#334155',
57
+ '800': '#1e293b',
58
+ '900': '#0f172a',
59
+ '950': '#020617',
60
+ },
61
+ gray: {
62
+ '50': '#f9fafb',
63
+ '100': '#f3f4f6',
64
+ '200': '#e5e7eb',
65
+ '300': '#d1d5db',
66
+ '400': '#9ca3af',
67
+ '500': '#6b7280',
68
+ '600': '#4b5563',
69
+ '700': '#374151',
70
+ '800': '#1f2937',
71
+ '900': '#111827',
72
+ '950': '#030712',
73
+ },
74
+ zinc: {
75
+ '50': '#fafafa',
76
+ '100': '#f4f4f5',
77
+ '200': '#e4e4e7',
78
+ '300': '#d4d4d8',
79
+ '400': '#a1a1aa',
80
+ '500': '#71717a',
81
+ '600': '#52525b',
82
+ '700': '#3f3f46',
83
+ '800': '#27272a',
84
+ '900': '#18181b',
85
+ '950': '#09090b',
86
+ },
87
+ neutral: {
88
+ '50': '#fafafa',
89
+ '100': '#f5f5f5',
90
+ '200': '#e5e5e5',
91
+ '300': '#d4d4d4',
92
+ '400': '#a3a3a3',
93
+ '500': '#737373',
94
+ '600': '#525252',
95
+ '700': '#404040',
96
+ '800': '#262626',
97
+ '900': '#171717',
98
+ '950': '#0a0a0a',
99
+ },
100
+ stone: {
101
+ '50': '#fafaf9',
102
+ '100': '#f5f5f4',
103
+ '200': '#e7e5e4',
104
+ '300': '#d6d3d1',
105
+ '400': '#a8a29e',
106
+ '500': '#78716c',
107
+ '600': '#57534e',
108
+ '700': '#44403c',
109
+ '800': '#292524',
110
+ '900': '#1c1917',
111
+ '950': '#0c0a09',
112
+ },
113
+ red: {
114
+ '50': '#fef2f2',
115
+ '100': '#fee2e2',
116
+ '200': '#fecaca',
117
+ '300': '#fca5a5',
118
+ '400': '#f87171',
119
+ '500': '#ef4444',
120
+ '600': '#dc2626',
121
+ '700': '#b91c1c',
122
+ '800': '#991b1b',
123
+ '900': '#7f1d1d',
124
+ '950': '#450a0a',
125
+ },
126
+ orange: {
127
+ '50': '#fff7ed',
128
+ '100': '#ffedd5',
129
+ '200': '#fed7aa',
130
+ '300': '#fdba74',
131
+ '400': '#fb923c',
132
+ '500': '#f97316',
133
+ '600': '#ea580c',
134
+ '700': '#c2410c',
135
+ '800': '#9a3412',
136
+ '900': '#7c2d12',
137
+ '950': '#431407',
138
+ },
139
+ amber: {
140
+ '50': '#fffbeb',
141
+ '100': '#fef3c7',
142
+ '200': '#fde68a',
143
+ '300': '#fcd34d',
144
+ '400': '#fbbf24',
145
+ '500': '#f59e0b',
146
+ '600': '#d97706',
147
+ '700': '#b45309',
148
+ '800': '#92400e',
149
+ '900': '#78350f',
150
+ '950': '#451a03',
151
+ },
152
+ yellow: {
153
+ '50': '#fefce8',
154
+ '100': '#fef9c3',
155
+ '200': '#fef08a',
156
+ '300': '#fde047',
157
+ '400': '#facc15',
158
+ '500': '#eab308',
159
+ '600': '#ca8a04',
160
+ '700': '#a16207',
161
+ '800': '#854d0e',
162
+ '900': '#713f12',
163
+ '950': '#422006',
164
+ },
165
+ lime: {
166
+ '50': '#f7fee7',
167
+ '100': '#ecfccb',
168
+ '200': '#d9f99d',
169
+ '300': '#bef264',
170
+ '400': '#a3e635',
171
+ '500': '#84cc16',
172
+ '600': '#65a30d',
173
+ '700': '#4d7c0f',
174
+ '800': '#3f6212',
175
+ '900': '#365314',
176
+ '950': '#1a2e05',
177
+ },
178
+ green: {
179
+ '50': '#f0fdf4',
180
+ '100': '#dcfce7',
181
+ '200': '#bbf7d0',
182
+ '300': '#86efac',
183
+ '400': '#4ade80',
184
+ '500': '#22c55e',
185
+ '600': '#16a34a',
186
+ '700': '#15803d',
187
+ '800': '#166534',
188
+ '900': '#14532d',
189
+ '950': '#052e16',
190
+ },
191
+ emerald: {
192
+ '50': '#ecfdf5',
193
+ '100': '#d1fae5',
194
+ '200': '#a7f3d0',
195
+ '300': '#6ee7b7',
196
+ '400': '#34d399',
197
+ '500': '#10b981',
198
+ '600': '#059669',
199
+ '700': '#047857',
200
+ '800': '#065f46',
201
+ '900': '#064e3b',
202
+ '950': '#022c22',
203
+ },
204
+ teal: {
205
+ '50': '#f0fdfa',
206
+ '100': '#ccfbf1',
207
+ '200': '#99f6e4',
208
+ '300': '#5eead4',
209
+ '400': '#2dd4bf',
210
+ '500': '#14b8a6',
211
+ '600': '#0d9488',
212
+ '700': '#0f766e',
213
+ '800': '#115e59',
214
+ '900': '#134e4a',
215
+ '950': '#042f2e',
216
+ },
217
+ cyan: {
218
+ '50': '#ecfeff',
219
+ '100': '#cffafe',
220
+ '200': '#a5f3fc',
221
+ '300': '#67e8f9',
222
+ '400': '#22d3ee',
223
+ '500': '#06b6d4',
224
+ '600': '#0891b2',
225
+ '700': '#0e7490',
226
+ '800': '#155e75',
227
+ '900': '#164e63',
228
+ '950': '#083344',
229
+ },
230
+ sky: {
231
+ '50': '#f0f9ff',
232
+ '100': '#e0f2fe',
233
+ '200': '#bae6fd',
234
+ '300': '#7dd3fc',
235
+ '400': '#38bdf8',
236
+ '500': '#0ea5e9',
237
+ '600': '#0284c7',
238
+ '700': '#0369a1',
239
+ '800': '#075985',
240
+ '900': '#0c4a6e',
241
+ '950': '#082f49',
242
+ },
243
+ blue: {
244
+ '50': '#eff6ff',
245
+ '100': '#dbeafe',
246
+ '200': '#bfdbfe',
247
+ '300': '#93c5fd',
248
+ '400': '#60a5fa',
249
+ '500': '#3b82f6',
250
+ '600': '#2563eb',
251
+ '700': '#1d4ed8',
252
+ '800': '#1e40af',
253
+ '900': '#1e3a8a',
254
+ '950': '#172554',
255
+ },
256
+ indigo: {
257
+ '50': '#eef2ff',
258
+ '100': '#e0e7ff',
259
+ '200': '#c7d2fe',
260
+ '300': '#a5b4fc',
261
+ '400': '#818cf8',
262
+ '500': '#6366f1',
263
+ '600': '#4f46e5',
264
+ '700': '#4338ca',
265
+ '800': '#3730a3',
266
+ '900': '#312e81',
267
+ '950': '#1e1b4b',
268
+ },
269
+ violet: {
270
+ '50': '#f5f3ff',
271
+ '100': '#ede9fe',
272
+ '200': '#ddd6fe',
273
+ '300': '#c4b5fd',
274
+ '400': '#a78bfa',
275
+ '500': '#8b5cf6',
276
+ '600': '#7c3aed',
277
+ '700': '#6d28d9',
278
+ '800': '#5b21b6',
279
+ '900': '#4c1d95',
280
+ '950': '#2e1065',
281
+ },
282
+ purple: {
283
+ '50': '#faf5ff',
284
+ '100': '#f3e8ff',
285
+ '200': '#e9d5ff',
286
+ '300': '#d8b4fe',
287
+ '400': '#c084fc',
288
+ '500': '#a855f7',
289
+ '600': '#9333ea',
290
+ '700': '#7e22ce',
291
+ '800': '#6b21a8',
292
+ '900': '#581c87',
293
+ '950': '#3b0764',
294
+ },
295
+ fuchsia: {
296
+ '50': '#fdf4ff',
297
+ '100': '#fae8ff',
298
+ '200': '#f5d0fe',
299
+ '300': '#f0abfc',
300
+ '400': '#e879f9',
301
+ '500': '#d946ef',
302
+ '600': '#c026d3',
303
+ '700': '#a21caf',
304
+ '800': '#86198f',
305
+ '900': '#701a75',
306
+ '950': '#4a044e',
307
+ },
308
+ pink: {
309
+ '50': '#fdf2f8',
310
+ '100': '#fce7f3',
311
+ '200': '#fbcfe8',
312
+ '300': '#f9a8d4',
313
+ '400': '#f472b6',
314
+ '500': '#ec4899',
315
+ '600': '#db2777',
316
+ '700': '#be185d',
317
+ '800': '#9d174d',
318
+ '900': '#831843',
319
+ '950': '#500724',
320
+ },
321
+ rose: {
322
+ '50': '#fff1f2',
323
+ '100': '#ffe4e6',
324
+ '200': '#fecdd3',
325
+ '300': '#fda4af',
326
+ '400': '#fb7185',
327
+ '500': '#f43f5e',
328
+ '600': '#e11d48',
329
+ '700': '#be123c',
330
+ '800': '#9f1239',
331
+ '900': '#881337',
332
+ '950': '#4c0519',
333
+ },
334
+ }
335
+
336
+ /**
337
+ * Special color values.
338
+ */
339
+ const SPECIAL_COLOR_VALUES: Record<string, string> = {
340
+ transparent: 'transparent',
341
+ current: 'currentColor',
342
+ inherit: 'inherit',
343
+ white: '#ffffff',
344
+ black: '#000000',
345
+ }
346
+
347
+ /**
348
+ * Default Tailwind v4 font weight values.
349
+ */
350
+ const DEFAULT_FONT_WEIGHTS: TextStyleValue[] = [
351
+ { class: 'font-thin', label: 'Thin', css: { fontWeight: '100' } },
352
+ { class: 'font-extralight', label: 'Extra Light', css: { fontWeight: '200' } },
353
+ { class: 'font-light', label: 'Light', css: { fontWeight: '300' } },
354
+ { class: 'font-normal', label: 'Normal', css: { fontWeight: '400' } },
355
+ { class: 'font-medium', label: 'Medium', css: { fontWeight: '500' } },
356
+ { class: 'font-semibold', label: 'Semibold', css: { fontWeight: '600' } },
357
+ { class: 'font-bold', label: 'Bold', css: { fontWeight: '700' } },
358
+ { class: 'font-extrabold', label: 'Extra Bold', css: { fontWeight: '800' } },
359
+ { class: 'font-black', label: 'Black', css: { fontWeight: '900' } },
360
+ ]
361
+
362
+ /**
363
+ * Default Tailwind v4 font size values.
364
+ */
365
+ const DEFAULT_FONT_SIZES: TextStyleValue[] = [
366
+ { class: 'text-xs', label: 'XS', css: { fontSize: '0.75rem', lineHeight: '1rem' } },
367
+ { class: 'text-sm', label: 'SM', css: { fontSize: '0.875rem', lineHeight: '1.25rem' } },
368
+ { class: 'text-base', label: 'Base', css: { fontSize: '1rem', lineHeight: '1.5rem' } },
369
+ { class: 'text-lg', label: 'LG', css: { fontSize: '1.125rem', lineHeight: '1.75rem' } },
370
+ { class: 'text-xl', label: 'XL', css: { fontSize: '1.25rem', lineHeight: '1.75rem' } },
371
+ { class: 'text-2xl', label: '2XL', css: { fontSize: '1.5rem', lineHeight: '2rem' } },
372
+ { class: 'text-3xl', label: '3XL', css: { fontSize: '1.875rem', lineHeight: '2.25rem' } },
373
+ { class: 'text-4xl', label: '4XL', css: { fontSize: '2.25rem', lineHeight: '2.5rem' } },
374
+ { class: 'text-5xl', label: '5XL', css: { fontSize: '3rem', lineHeight: '1' } },
375
+ { class: 'text-6xl', label: '6XL', css: { fontSize: '3.75rem', lineHeight: '1' } },
376
+ { class: 'text-7xl', label: '7XL', css: { fontSize: '4.5rem', lineHeight: '1' } },
377
+ { class: 'text-8xl', label: '8XL', css: { fontSize: '6rem', lineHeight: '1' } },
378
+ { class: 'text-9xl', label: '9XL', css: { fontSize: '8rem', lineHeight: '1' } },
379
+ ]
380
+
381
+ /**
382
+ * Default text decoration values.
383
+ */
384
+ const DEFAULT_TEXT_DECORATIONS: TextStyleValue[] = [
385
+ { class: 'no-underline', label: 'None', css: { textDecoration: 'none' } },
386
+ { class: 'underline', label: 'Underline', css: { textDecoration: 'underline' } },
387
+ { class: 'overline', label: 'Overline', css: { textDecoration: 'overline' } },
388
+ { class: 'line-through', label: 'Strikethrough', css: { textDecoration: 'line-through' } },
389
+ ]
390
+
391
+ /**
392
+ * Default font style values.
393
+ */
394
+ const DEFAULT_FONT_STYLES: TextStyleValue[] = [
395
+ { class: 'not-italic', label: 'Normal', css: { fontStyle: 'normal' } },
396
+ { class: 'italic', label: 'Italic', css: { fontStyle: 'italic' } },
397
+ ]
398
+
44
399
  /**
45
400
  * Build a regex pattern for matching color classes.
46
- * Matches either:
47
- * - Known default/special colors (e.g., bg-red, text-white)
48
- * - Any color name followed by a shade number (e.g., bg-primary-500)
49
401
  */
50
402
  function buildColorPattern(prefix: string): RegExp {
51
403
  const colorNames = [...DEFAULT_TAILWIND_COLORS, ...SPECIAL_COLORS].join('|')
52
- // Match either: known-color (with optional shade) OR any-name-with-shade (to support custom colors)
53
404
  return new RegExp(`^${prefix}-((?:${colorNames})(?:-(\\d+))?|([a-z]+)-(\\d+))$`)
54
405
  }
55
406
 
56
407
  /**
57
408
  * Regex patterns to match Tailwind color classes.
58
- * These patterns are specific to color utilities and won't match other utilities
59
- * like text-lg, text-center, bg-fixed, etc.
60
409
  */
61
410
  const COLOR_CLASS_PATTERNS = {
62
- // Matches: bg-red-500, bg-primary-500, bg-white, bg-transparent
63
411
  bg: buildColorPattern('bg'),
64
- // Matches: text-red-500, text-primary-500, text-white (NOT text-lg, text-center)
65
412
  text: buildColorPattern('text'),
66
- // Matches: border-red-500, border-primary-500
67
413
  border: buildColorPattern('border'),
68
- // Matches: hover:bg-red-500
69
414
  hoverBg: buildColorPattern('hover:bg'),
70
- // Matches: hover:text-red-500
71
415
  hoverText: buildColorPattern('hover:text'),
72
416
  }
73
417
 
74
418
  /**
75
- * Parse Tailwind v4 CSS config to extract available colors.
76
- * Tailwind v4 uses CSS-based configuration with @theme directive.
77
- *
78
- * Example CSS:
79
- * ```css
80
- * @theme {
81
- * --color-primary-50: #eff6ff;
82
- * --color-primary-500: #3b82f6;
83
- * --color-accent: #f59e0b;
84
- * }
85
- * ```
419
+ * Parse Tailwind v4 CSS config to extract available colors with their values.
86
420
  */
87
- export async function parseTailwindConfig(projectRoot: string = process.cwd()): Promise<AvailableColors> {
421
+ export async function parseTailwindConfig(projectRoot: string = getProjectRoot()): Promise<AvailableColors> {
88
422
  // Tailwind v4 CSS files to search
89
423
  const cssFiles = [
90
424
  'src/styles/global.css',
@@ -112,17 +446,17 @@ export async function parseTailwindConfig(projectRoot: string = process.cwd()):
112
446
  }
113
447
  }
114
448
 
115
- // Build default colors list
449
+ // Build default colors with values
116
450
  const defaultColors: TailwindColor[] = DEFAULT_TAILWIND_COLORS.map(name => ({
117
451
  name,
118
- shades: [...STANDARD_SHADES],
452
+ values: DEFAULT_COLOR_VALUES[name] || {},
119
453
  isCustom: false,
120
454
  }))
121
455
 
122
- // Add special colors (no shades)
456
+ // Add special colors
123
457
  const specialColors: TailwindColor[] = SPECIAL_COLORS.map(name => ({
124
458
  name,
125
- shades: [],
459
+ values: { '': SPECIAL_COLOR_VALUES[name] || name },
126
460
  isCustom: false,
127
461
  }))
128
462
 
@@ -135,95 +469,192 @@ export async function parseTailwindConfig(projectRoot: string = process.cwd()):
135
469
 
136
470
  /**
137
471
  * Extract custom colors from Tailwind v4 CSS @theme block.
138
- *
139
- * Looks for patterns like:
140
- * - --color-primary-50: #value;
141
- * - --color-primary: #value;
142
- * - --color-accent-500: oklch(...);
472
+ * Extracts both color names and their actual CSS values.
143
473
  */
144
474
  function extractColorsFromCss(content: string): TailwindColor[] {
145
- const colors = new Map<string, Set<string>>()
475
+ const colors = new Map<string, Record<string, string>>()
146
476
 
147
- // Find @theme blocks
148
- const themeBlockPattern = /@theme\s*\{([^}]+)\}/gs
477
+ // Find @theme blocks (including inline)
478
+ const themeBlockPattern = /@theme(?:\s+inline)?\s*\{([^}]+)\}/gs
149
479
  let themeMatch: RegExpExecArray | null
150
480
 
151
481
  while ((themeMatch = themeBlockPattern.exec(content)) !== null) {
152
482
  const themeContent = themeMatch[1]
153
483
  if (!themeContent) continue
154
484
 
155
- // Find all --color-* definitions
485
+ // Find all --color-* definitions with their values
156
486
  // Pattern: --color-{name}-{shade}: value; or --color-{name}: value;
157
- const colorVarPattern = /--color-([a-z]+)(?:-(\d+))?:/gi
487
+ const colorVarPattern = /--color-([a-z]+)(?:-(\d+))?:\s*([^;]+);/gi
158
488
  let colorMatch: RegExpExecArray | null
159
489
 
160
490
  while ((colorMatch = colorVarPattern.exec(themeContent)) !== null) {
161
491
  const colorName = colorMatch[1]?.toLowerCase()
162
- const shade = colorMatch[2]
492
+ const shade = colorMatch[2] || ''
493
+ const value = colorMatch[3]?.trim()
163
494
 
164
- if (!colorName) continue
495
+ if (!colorName || !value) continue
165
496
 
166
- // Skip if it's a default color
497
+ // Skip if it's a default color (we already have values for those)
167
498
  if (DEFAULT_TAILWIND_COLORS.includes(colorName as any)) {
168
499
  continue
169
500
  }
170
501
 
171
502
  if (!colors.has(colorName)) {
172
- colors.set(colorName, new Set())
503
+ colors.set(colorName, {})
173
504
  }
174
505
 
175
- if (shade) {
176
- colors.get(colorName)!.add(shade)
177
- }
506
+ colors.get(colorName)![shade] = value
178
507
  }
179
508
  }
180
509
 
181
- // Also check for inline @theme definitions (Tailwind v4 can be inline too)
182
- // Pattern: @theme inline { ... }
183
- const inlineThemePattern = /@theme\s+inline\s*\{([^}]+)\}/gs
184
- let inlineMatch: RegExpExecArray | null
185
-
186
- while ((inlineMatch = inlineThemePattern.exec(content)) !== null) {
187
- const themeContent = inlineMatch[1]
188
- if (!themeContent) continue
510
+ // Convert to TailwindColor array
511
+ const result: TailwindColor[] = []
512
+ for (const [name, values] of colors) {
513
+ result.push({
514
+ name,
515
+ values,
516
+ isCustom: true,
517
+ })
518
+ }
189
519
 
190
- const colorVarPattern = /--color-([a-z]+)(?:-(\d+))?:/gi
191
- let colorMatch: RegExpExecArray | null
520
+ return result
521
+ }
192
522
 
193
- while ((colorMatch = colorVarPattern.exec(themeContent)) !== null) {
194
- const colorName = colorMatch[1]?.toLowerCase()
195
- const shade = colorMatch[2]
523
+ /**
524
+ * Parse Tailwind v4 CSS config to extract available text styles.
525
+ */
526
+ export async function parseTextStyles(projectRoot: string = getProjectRoot()): Promise<AvailableTextStyles> {
527
+ // Tailwind v4 CSS files to search
528
+ const cssFiles = [
529
+ 'src/styles/global.css',
530
+ 'src/styles/tailwind.css',
531
+ 'src/styles/app.css',
532
+ 'src/app.css',
533
+ 'src/global.css',
534
+ 'src/index.css',
535
+ 'app/globals.css',
536
+ 'styles/globals.css',
537
+ ]
196
538
 
197
- if (!colorName) continue
539
+ let customTextStyles: Partial<AvailableTextStyles> = {}
198
540
 
199
- if (DEFAULT_TAILWIND_COLORS.includes(colorName as any)) {
200
- continue
541
+ for (const cssFile of cssFiles) {
542
+ const fullPath = path.join(projectRoot, cssFile)
543
+ try {
544
+ const content = await fs.readFile(fullPath, 'utf-8')
545
+ customTextStyles = extractTextStylesFromCss(content)
546
+ // If we found any custom styles, use this file
547
+ if (Object.values(customTextStyles).some(arr => arr && arr.length > 0)) {
548
+ break
201
549
  }
550
+ } catch {
551
+ // File doesn't exist, continue
552
+ }
553
+ }
202
554
 
203
- if (!colors.has(colorName)) {
204
- colors.set(colorName, new Set())
205
- }
555
+ // Merge custom styles with defaults (custom overrides default)
556
+ return {
557
+ fontWeight: mergeTextStyles(DEFAULT_FONT_WEIGHTS, customTextStyles.fontWeight),
558
+ fontSize: mergeTextStyles(DEFAULT_FONT_SIZES, customTextStyles.fontSize),
559
+ textDecoration: mergeTextStyles(DEFAULT_TEXT_DECORATIONS, customTextStyles.textDecoration),
560
+ fontStyle: mergeTextStyles(DEFAULT_FONT_STYLES, customTextStyles.fontStyle),
561
+ }
562
+ }
206
563
 
207
- if (shade) {
208
- colors.get(colorName)!.add(shade)
209
- }
564
+ /**
565
+ * Merge custom text styles with defaults.
566
+ * Custom styles with matching class names override defaults.
567
+ */
568
+ function mergeTextStyles(defaults: TextStyleValue[], custom?: TextStyleValue[]): TextStyleValue[] {
569
+ if (!custom || custom.length === 0) {
570
+ return defaults
571
+ }
572
+
573
+ const customByClass = new Map(custom.map(s => [s.class, s]))
574
+ const result: TextStyleValue[] = []
575
+
576
+ // Update defaults with custom overrides
577
+ for (const def of defaults) {
578
+ const customStyle = customByClass.get(def.class)
579
+ if (customStyle) {
580
+ result.push(customStyle)
581
+ customByClass.delete(def.class)
582
+ } else {
583
+ result.push(def)
210
584
  }
211
585
  }
212
586
 
213
- // Convert to TailwindColor array
214
- const result: TailwindColor[] = []
215
- for (const [name, shades] of colors) {
216
- const sortedShades = Array.from(shades).sort((a, b) => parseInt(a) - parseInt(b))
217
- result.push({
218
- name,
219
- shades: sortedShades.length > 0 ? sortedShades : [],
220
- isCustom: true,
221
- })
587
+ // Add any remaining custom styles that weren't overrides
588
+ for (const style of customByClass.values()) {
589
+ result.push(style)
222
590
  }
223
591
 
224
592
  return result
225
593
  }
226
594
 
595
+ /**
596
+ * Extract custom text styles from Tailwind v4 CSS @theme block.
597
+ */
598
+ function extractTextStylesFromCss(content: string): Partial<AvailableTextStyles> {
599
+ const fontWeights: TextStyleValue[] = []
600
+ const fontSizes: TextStyleValue[] = []
601
+
602
+ // Find @theme blocks (including inline)
603
+ const themeBlockPattern = /@theme(?:\s+inline)?\s*\{([^}]+)\}/gs
604
+ let themeMatch: RegExpExecArray | null
605
+
606
+ while ((themeMatch = themeBlockPattern.exec(content)) !== null) {
607
+ const themeContent = themeMatch[1]
608
+ if (!themeContent) continue
609
+
610
+ // Extract font-weight overrides: --font-weight-{name}: value;
611
+ const fontWeightPattern = /--font-weight-([a-z]+):\s*([^;]+);/gi
612
+ let weightMatch: RegExpExecArray | null
613
+ while ((weightMatch = fontWeightPattern.exec(themeContent)) !== null) {
614
+ const name = weightMatch[1]?.toLowerCase()
615
+ const value = weightMatch[2]?.trim()
616
+ if (!name || !value) continue
617
+
618
+ fontWeights.push({
619
+ class: `font-${name}`,
620
+ label: name.charAt(0).toUpperCase() + name.slice(1),
621
+ css: { fontWeight: value },
622
+ })
623
+ }
624
+
625
+ // Extract font-size overrides: --font-size-{name}: value;
626
+ // Also look for corresponding line-height: --line-height-{name}: value;
627
+ const fontSizePattern = /--font-size-([a-z0-9]+):\s*([^;]+);/gi
628
+ let sizeMatch: RegExpExecArray | null
629
+ while ((sizeMatch = fontSizePattern.exec(themeContent)) !== null) {
630
+ const name = sizeMatch[1]?.toLowerCase()
631
+ const value = sizeMatch[2]?.trim()
632
+ if (!name || !value) continue
633
+
634
+ // Try to find matching line-height
635
+ const lineHeightPattern = new RegExp(`--line-height-${name}:\\s*([^;]+);`, 'i')
636
+ const lineHeightMatch = themeContent.match(lineHeightPattern)
637
+ const lineHeight = lineHeightMatch?.[1]?.trim()
638
+
639
+ const css: Record<string, string> = { fontSize: value }
640
+ if (lineHeight) {
641
+ css.lineHeight = lineHeight
642
+ }
643
+
644
+ fontSizes.push({
645
+ class: `text-${name}`,
646
+ label: name.toUpperCase(),
647
+ css,
648
+ })
649
+ }
650
+ }
651
+
652
+ return {
653
+ fontWeight: fontWeights.length > 0 ? fontWeights : undefined,
654
+ fontSize: fontSizes.length > 0 ? fontSizes : undefined,
655
+ }
656
+ }
657
+
227
658
  /**
228
659
  * Extract color classes from an element's class attribute.
229
660
  */
@@ -235,14 +666,13 @@ export function extractColorClasses(classAttr: string | null | undefined): Color
235
666
  const allColorClasses: string[] = []
236
667
 
237
668
  for (const cls of classes) {
238
- // Check each pattern
239
669
  for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
240
670
  const match = cls.match(pattern)
241
671
  if (match) {
242
672
  allColorClasses.push(cls)
243
- // Assign to appropriate field
244
- if (!(key in colorClasses)) {
245
- ;(colorClasses as any)[key] = cls
673
+ const colorKey = key as keyof Omit<ColorClasses, 'allColorClasses'>
674
+ if (!(colorKey in colorClasses)) {
675
+ colorClasses[colorKey] = cls
246
676
  }
247
677
  break
248
678
  }
@@ -266,10 +696,6 @@ export function isColorClass(className: string): boolean {
266
696
 
267
697
  /**
268
698
  * Generate a new class string with a color class replaced.
269
- * @param currentClasses - Current class attribute value
270
- * @param oldColorClass - The color class to replace (e.g., 'bg-blue-500')
271
- * @param newColorClass - The new color class (e.g., 'bg-red-500')
272
- * @returns New class string with the replacement
273
699
  */
274
700
  export function replaceColorClass(
275
701
  currentClasses: string,
@@ -283,8 +709,6 @@ export function replaceColorClass(
283
709
 
284
710
  /**
285
711
  * Get the color type from a color class.
286
- * @param colorClass - e.g., 'bg-blue-500', 'text-white', 'hover:bg-red-600'
287
- * @returns The type: 'bg', 'text', 'border', 'hoverBg', 'hoverText', or undefined
288
712
  */
289
713
  export function getColorType(colorClass: string): keyof ColorClasses | undefined {
290
714
  for (const [key, pattern] of Object.entries(COLOR_CLASS_PATTERNS)) {
@@ -297,8 +721,6 @@ export function getColorType(colorClass: string): keyof ColorClasses | undefined
297
721
 
298
722
  /**
299
723
  * Parse a color class into its components.
300
- * @param colorClass - e.g., 'bg-blue-500', 'text-white', 'hover:bg-red-600'
301
- * @returns Object with prefix, colorName, and shade (if any)
302
724
  */
303
725
  export function parseColorClass(colorClass: string): {
304
726
  prefix: string
@@ -306,11 +728,9 @@ export function parseColorClass(colorClass: string): {
306
728
  shade?: string
307
729
  isHover: boolean
308
730
  } | undefined {
309
- // Handle hover prefix
310
731
  const isHover = colorClass.startsWith('hover:')
311
732
  const classWithoutHover = isHover ? colorClass.slice(6) : colorClass
312
733
 
313
- // Match prefix-color-shade or prefix-color
314
734
  const match = classWithoutHover.match(/^(bg|text|border)-([a-z]+)(?:-(\d+))?$/)
315
735
 
316
736
  if (!match) return undefined
@@ -336,33 +756,3 @@ export function buildColorClass(
336
756
  }
337
757
  return `${prefix}-${colorName}`
338
758
  }
339
-
340
- /**
341
- * Generate a hidden HTML string containing all color classes for Tailwind safelist.
342
- * This ensures Tailwind includes all color utility classes during build,
343
- * even if they're only used dynamically via the CMS color editor.
344
- *
345
- * @param availableColors - The available colors from Tailwind config
346
- * @returns HTML string with a hidden div containing all color classes
347
- */
348
- export function generateColorSafelistHtml(availableColors: AvailableColors): string {
349
- const classes: string[] = []
350
-
351
- for (const color of availableColors.colors) {
352
- if (color.shades.length === 0) {
353
- // Special colors without shades (white, black, transparent, etc.)
354
- if (color.name !== 'transparent' && color.name !== 'current' && color.name !== 'inherit') {
355
- classes.push(`bg-${color.name}`)
356
- classes.push(`text-${color.name}`)
357
- }
358
- } else {
359
- // Colors with shades
360
- for (const shade of color.shades) {
361
- classes.push(`bg-${color.name}-${shade}`)
362
- classes.push(`text-${color.name}-${shade}`)
363
- }
364
- }
365
- }
366
-
367
- return `<div aria-hidden="true" class="!hidden ${classes.join(' ')}"></div>`
368
- }