@ethlete/core 4.31.0 → 5.0.0-beta.11

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.
@@ -0,0 +1,513 @@
1
+ import { formatFiles, logger, visitNotIgnoredFiles } from '@nx/devkit';
2
+ import { Project, SyntaxKind } from 'ts-morph';
3
+ export default async function generate(tree, schema) {
4
+ logger.log('\nšŸ”„ Starting Tailwind 4 theme generator...\n');
5
+ const themesPath = schema.themesPath || 'src/themes.ts';
6
+ const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
7
+ const prefix = schema.prefix || 'et';
8
+ // Step 1: Check if themes file exists
9
+ if (!tree.exists(themesPath)) {
10
+ logger.error(`āŒ Themes file not found at: ${themesPath}`);
11
+ logger.log(`\nPlease specify the correct path using --themesPath option.`);
12
+ logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
13
+ return;
14
+ }
15
+ logger.log(`šŸ“ Reading themes from: ${themesPath}`);
16
+ // Step 2: Read and parse themes file
17
+ const themesContent = tree.read(themesPath, 'utf-8');
18
+ if (!themesContent) {
19
+ logger.error('āŒ Failed to read themes file');
20
+ return;
21
+ }
22
+ // Step 3: Try to extract themes using TypeScript
23
+ let themes;
24
+ try {
25
+ themes = extractThemesFromContent(themesContent, themesPath);
26
+ logger.log(`āœ… Found ${themes.length} theme(s)`);
27
+ }
28
+ catch (error) {
29
+ logger.error('āŒ Failed to parse themes file');
30
+ logger.error(` ${error instanceof Error ? error.message : String(error)}`);
31
+ logger.log('\nThe themes file must export themes as:');
32
+ logger.log(' export const THEMES = [...] satisfies Theme[];\n');
33
+ return;
34
+ }
35
+ // Step 4: Validate theme configuration
36
+ try {
37
+ validateThemeConfiguration(themes);
38
+ }
39
+ catch (error) {
40
+ logger.error('āŒ Theme configuration error');
41
+ logger.error(` ${error instanceof Error ? error.message : String(error)}`);
42
+ return;
43
+ }
44
+ // Step 5: Generate Tailwind CSS
45
+ logger.log('\nšŸŽØ Generating Tailwind theme CSS...');
46
+ const css = generateTailwindThemeCss(themes, prefix, schema);
47
+ // Step 6: Write the generated CSS file
48
+ const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
49
+ if (outputDir && !tree.exists(outputDir)) {
50
+ logger.log(`šŸ“ Creating directory: ${outputDir}`);
51
+ }
52
+ tree.write(outputPath, css);
53
+ logger.log(`āœ… Generated Tailwind themes at: ${outputPath}`);
54
+ // Step 7: Try to find and update main styles file
55
+ const mainStylesFiles = findMainStylesFile(tree);
56
+ if (mainStylesFiles.length > 0) {
57
+ logger.log('\nšŸ“ Found potential main styles files:');
58
+ mainStylesFiles.forEach((file) => logger.log(` - ${file}`));
59
+ logger.log('\nāš ļø Please manually import the generated themes:');
60
+ logger.log(` @import './${outputPath.replace('src/styles/', '')}';`);
61
+ }
62
+ if (!schema.skipFormat) {
63
+ await formatFiles(tree);
64
+ }
65
+ logger.log('\nāœ… Generation completed successfully!\n');
66
+ }
67
+ //#endregion
68
+ //#region Helper Functions
69
+ function extractThemesFromContent(content, filePath) {
70
+ // Create an in-memory TypeScript project
71
+ const project = new Project({
72
+ useInMemoryFileSystem: true,
73
+ compilerOptions: {
74
+ target: 99, // ESNext
75
+ module: 99, // ESNext
76
+ },
77
+ });
78
+ // Add the source file
79
+ const sourceFile = project.createSourceFile(filePath, content);
80
+ const themes = [];
81
+ // Find all exported const declarations
82
+ const exportedDeclarations = sourceFile.getVariableDeclarations().filter((decl) => {
83
+ const statement = decl.getVariableStatement();
84
+ return statement?.isExported();
85
+ });
86
+ // Look for the THEMES or themes array
87
+ const themesArray = exportedDeclarations.find((decl) => {
88
+ const name = decl.getName();
89
+ return name === 'THEMES' || name === 'themes';
90
+ });
91
+ if (!themesArray) {
92
+ throw new Error('Could not find THEMES or themes export');
93
+ }
94
+ let initializer = themesArray.getInitializer();
95
+ if (!initializer) {
96
+ throw new Error('THEMES export has no initializer');
97
+ }
98
+ // Handle 'satisfies' expression: [array] satisfies Type[]
99
+ if (initializer.isKind(SyntaxKind.SatisfiesExpression)) {
100
+ initializer = initializer.getExpression();
101
+ }
102
+ // Handle 'as const': [array] as const
103
+ if (initializer.isKind(SyntaxKind.AsExpression)) {
104
+ initializer = initializer.getExpression();
105
+ }
106
+ if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
107
+ throw new Error('THEMES export must be an array literal');
108
+ }
109
+ // Get the array elements (these are references to the theme const declarations)
110
+ const elements = initializer.getElements();
111
+ for (const element of elements) {
112
+ // Resolve the identifier to its declaration
113
+ if (element.isKind(SyntaxKind.Identifier)) {
114
+ const name = element.getText();
115
+ const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
116
+ if (!themeDecl) {
117
+ logger.warn(`āš ļø Could not find declaration for theme: ${name}`);
118
+ continue;
119
+ }
120
+ let themeObj = themeDecl.getInitializer();
121
+ if (!themeObj) {
122
+ logger.warn(`āš ļø Theme ${name} has no initializer`);
123
+ continue;
124
+ }
125
+ // Handle 'as const' on individual theme objects
126
+ if (themeObj.isKind(SyntaxKind.AsExpression)) {
127
+ themeObj = themeObj.getExpression();
128
+ }
129
+ if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
130
+ logger.warn(`āš ļø Theme ${name} is not an object literal`);
131
+ continue;
132
+ }
133
+ try {
134
+ const theme = parseThemeObject(themeObj, sourceFile);
135
+ themes.push(theme);
136
+ }
137
+ catch (error) {
138
+ logger.warn(`āš ļø Failed to parse theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
139
+ }
140
+ }
141
+ }
142
+ if (themes.length === 0) {
143
+ throw new Error('No valid themes found in THEMES array');
144
+ }
145
+ return themes;
146
+ }
147
+ function parseThemeObject(obj, sourceFile) {
148
+ const properties = obj.getProperties();
149
+ const theme = {};
150
+ for (const prop of properties) {
151
+ if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
152
+ continue;
153
+ }
154
+ const propName = prop.getName();
155
+ const initializer = prop.getInitializer();
156
+ if (!initializer) {
157
+ continue;
158
+ }
159
+ switch (propName) {
160
+ case 'name':
161
+ if (initializer.isKind(SyntaxKind.StringLiteral)) {
162
+ theme.name = initializer.getLiteralValue();
163
+ }
164
+ break;
165
+ case 'isDefault':
166
+ if (initializer.isKind(SyntaxKind.TrueKeyword)) {
167
+ theme.isDefault = true;
168
+ }
169
+ break;
170
+ case 'type':
171
+ if (initializer.isKind(SyntaxKind.StringLiteral)) {
172
+ theme.type = initializer.getLiteralValue();
173
+ }
174
+ break;
175
+ case 'primary':
176
+ case 'secondary':
177
+ case 'tertiary':
178
+ if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
179
+ theme[propName] = parseThemeSwatch(initializer, sourceFile);
180
+ }
181
+ break;
182
+ }
183
+ }
184
+ if (!theme.name || !theme.primary) {
185
+ throw new Error('Theme must have name and primary properties');
186
+ }
187
+ return theme;
188
+ }
189
+ function validateThemeConfiguration(themes) {
190
+ const defaultThemes = themes.filter((t) => t.isDefault);
191
+ // Error: No default theme
192
+ if (defaultThemes.length === 0) {
193
+ throw new Error('No default theme found. At least one theme must have isDefault: true');
194
+ }
195
+ // Error: Multiple default themes
196
+ if (defaultThemes.length > 1) {
197
+ throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
198
+ }
199
+ // Error: Duplicate theme types
200
+ const typedThemes = themes.filter((t) => t.type);
201
+ const typeMap = new Map();
202
+ for (const theme of typedThemes) {
203
+ const existing = typeMap.get(theme.type) || [];
204
+ existing.push(theme.name);
205
+ typeMap.set(theme.type, existing);
206
+ }
207
+ for (const [type, names] of typeMap) {
208
+ if (names.length > 1) {
209
+ throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
210
+ }
211
+ }
212
+ }
213
+ function parseThemeSwatch(obj, sourceFile) {
214
+ const properties = obj.getProperties();
215
+ const swatch = {};
216
+ for (const prop of properties) {
217
+ if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
218
+ continue;
219
+ }
220
+ const propName = prop.getName();
221
+ const initializer = prop.getInitializer();
222
+ if (!initializer) {
223
+ continue;
224
+ }
225
+ if (propName === 'color' || propName === 'onColor' || propName === 'inkColor') {
226
+ const colorMap = parseColorMap(initializer, sourceFile);
227
+ if (colorMap) {
228
+ swatch[propName] = colorMap;
229
+ }
230
+ }
231
+ }
232
+ if (!swatch.color || !swatch.onColor) {
233
+ throw new Error('ThemeSwatch must have color and onColor properties');
234
+ }
235
+ return swatch;
236
+ }
237
+ function parseColorMap(initializer, sourceFile) {
238
+ // Handle spread expressions by resolving references
239
+ if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
240
+ const colorMap = {};
241
+ const properties = initializer.getProperties();
242
+ for (const prop of properties) {
243
+ if (prop.isKind(SyntaxKind.PropertyAssignment)) {
244
+ const propName = prop.getName();
245
+ const propValue = prop.getInitializer();
246
+ if (propValue?.isKind(SyntaxKind.StringLiteral)) {
247
+ colorMap[propName] = propValue.getLiteralValue();
248
+ }
249
+ }
250
+ else if (prop.isKind(SyntaxKind.SpreadAssignment)) {
251
+ // Handle spread: { ...onColorDark, disabled: '...' }
252
+ const spreadExpr = prop.getExpression();
253
+ if (spreadExpr.isKind(SyntaxKind.Identifier)) {
254
+ const referencedName = spreadExpr.getText();
255
+ const referencedDecl = sourceFile
256
+ .getVariableDeclarations()
257
+ .find((decl) => decl.getName() === referencedName);
258
+ if (referencedDecl) {
259
+ const referencedObj = referencedDecl.getInitializer();
260
+ if (referencedObj?.isKind(SyntaxKind.ObjectLiteralExpression)) {
261
+ const spreadColors = parseColorMap(referencedObj, sourceFile);
262
+ if (spreadColors) {
263
+ Object.assign(colorMap, spreadColors);
264
+ }
265
+ }
266
+ }
267
+ }
268
+ }
269
+ }
270
+ // Apply fallbacks for required fields
271
+ if (colorMap.default) {
272
+ // Check if this is a ThemeColorMap (has hover, active, or disabled explicitly set)
273
+ const isThemeColorMap = colorMap.hover !== undefined || colorMap.active !== undefined || colorMap.disabled !== undefined;
274
+ if (isThemeColorMap) {
275
+ // For ThemeColorMap - apply fallbacks for all required fields
276
+ const defaultColor = colorMap.default;
277
+ const hoverColor = (colorMap.hover || defaultColor);
278
+ const result = {
279
+ default: defaultColor,
280
+ hover: hoverColor,
281
+ focus: colorMap.focus,
282
+ active: (colorMap.active || hoverColor),
283
+ disabled: (colorMap.disabled || defaultColor),
284
+ };
285
+ return result;
286
+ }
287
+ // For OnThemeColorMap - no fallbacks needed, all fields are optional except default
288
+ return colorMap;
289
+ }
290
+ return null;
291
+ }
292
+ return null;
293
+ }
294
+ function createCssThemeName(name) {
295
+ // Convert theme name to CSS-safe format (e.g., "Primary Blue" -> "primary-blue")
296
+ return name
297
+ .toLowerCase()
298
+ .replace(/\s+/g, '-')
299
+ .replace(/[^a-z0-9-]/g, '');
300
+ }
301
+ function generateTailwindThemeCss(themes, prefix, schema) {
302
+ const tailwindVars = [];
303
+ const themeVars = [];
304
+ const themesPath = schema.themesPath || 'src/themes.ts';
305
+ const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
306
+ const header = `/*
307
+ * Auto-generated Tailwind 4 theme colors from @ethlete/core
308
+ * DO NOT EDIT THIS FILE MANUALLY
309
+ *
310
+ * Generated from your theme definitions
311
+ * This file can be regenerated by running:
312
+ * nx g @ethlete/core:tailwind-4-color-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}
313
+ */
314
+
315
+ `;
316
+ // Validation is now done separately before this function is called
317
+ const defaultThemes = themes.filter((t) => t.isDefault);
318
+ const regularThemes = themes.filter((t) => !t.isDefault);
319
+ // Generate static Tailwind @theme block for each theme
320
+ for (const theme of themes) {
321
+ const name = createCssThemeName(theme.name);
322
+ // Add comment for theme section
323
+ tailwindVars.push(` /* ${theme.name} theme */`);
324
+ // Primary colors for Tailwind utilities
325
+ addTailwindColorVariants(tailwindVars, `${prefix}-${name}`, theme.primary.color);
326
+ tailwindVars.push('');
327
+ // On colors for Tailwind utilities
328
+ addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}`, theme.primary.onColor);
329
+ tailwindVars.push('');
330
+ // Ink colors for standalone foreground usage
331
+ addTailwindColorVariants(tailwindVars, `${prefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
332
+ tailwindVars.push('');
333
+ // Secondary colors if present
334
+ if (theme.secondary) {
335
+ addTailwindColorVariants(tailwindVars, `${prefix}-${name}-secondary`, theme.secondary.color);
336
+ tailwindVars.push('');
337
+ addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}-secondary`, theme.secondary.onColor);
338
+ tailwindVars.push('');
339
+ }
340
+ // Tertiary colors if present
341
+ if (theme.tertiary) {
342
+ addTailwindColorVariants(tailwindVars, `${prefix}-${name}-tertiary`, theme.tertiary.color);
343
+ tailwindVars.push('');
344
+ addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}-tertiary`, theme.tertiary.onColor);
345
+ tailwindVars.push('');
346
+ }
347
+ }
348
+ tailwindVars.push('');
349
+ // Main theme dynamic colors
350
+ const mainThemes = [...defaultThemes, ...regularThemes];
351
+ const hasSecondary = mainThemes.some((t) => t.secondary);
352
+ const hasTertiary = mainThemes.some((t) => t.tertiary);
353
+ tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
354
+ addDynamicThemeColors(tailwindVars, prefix, 'theme', 'primary', true);
355
+ addDynamicInkColors(tailwindVars, prefix, 'theme-ink', 'primary-ink');
356
+ if (hasSecondary) {
357
+ addDynamicThemeColors(tailwindVars, prefix, 'theme-secondary', 'secondary', false);
358
+ }
359
+ if (hasTertiary) {
360
+ addDynamicThemeColors(tailwindVars, prefix, 'theme-tertiary', 'tertiary', false);
361
+ }
362
+ // Generate runtime CSS for all themes
363
+ themes.forEach((theme) => {
364
+ const name = createCssThemeName(theme.name);
365
+ // Determine if this is the default theme
366
+ const isDefault = theme.isDefault;
367
+ // Generate color variant (.et-color--{name})
368
+ if (isDefault) {
369
+ // Default color gets :root and .et-color--default selectors
370
+ const selectors = [':root', `.${prefix}-color--default`, `.${prefix}-color--${name}`];
371
+ themeVars.push(`${selectors.join(', ')} {`);
372
+ }
373
+ else {
374
+ // Regular colors just get their own class
375
+ themeVars.push(`.${prefix}-color--${name} {`);
376
+ }
377
+ addThemeColorVariants(themeVars, prefix, '', theme);
378
+ themeVars.push('}\n');
379
+ });
380
+ // Convenience var aliases — available on any element with a color class (or root for default)
381
+ const aliasBlock = `/* Convenience aliases (rgb + solid + opacity variants) */
382
+ @layer base {
383
+ :root, :where([class*="${prefix}-color--"]) {
384
+ --${prefix}-theme-color-primary-rgb: var(--${prefix}-color-primary);
385
+ --${prefix}-theme-color-primary-opacity: 1;
386
+ --${prefix}-theme-color-primary-solid: rgb(var(--${prefix}-theme-color-primary-rgb));
387
+ --${prefix}-theme-color-primary: rgb(var(--${prefix}-theme-color-primary-rgb) / var(--${prefix}-theme-color-primary-opacity));
388
+
389
+ --${prefix}-theme-color-on-primary-rgb: var(--${prefix}-color-on-primary);
390
+ --${prefix}-theme-color-on-primary-opacity: 1;
391
+ --${prefix}-theme-color-on-primary-solid: rgb(var(--${prefix}-theme-color-on-primary-rgb) / var(--${prefix}-theme-color-on-primary-opacity));
392
+ --${prefix}-theme-color-on-primary: rgb(var(--${prefix}-theme-color-on-primary-rgb) / var(--${prefix}-theme-color-on-primary-opacity));
393
+
394
+ --${prefix}-theme-color-ink-rgb: var(--${prefix}-color-primary-ink, var(--${prefix}-color-primary));
395
+ --${prefix}-theme-color-ink-opacity: 1;
396
+ --${prefix}-theme-color-ink-solid: rgb(var(--${prefix}-theme-color-ink-rgb) / var(--${prefix}-theme-color-ink-opacity));
397
+ --${prefix}-theme-color-ink: rgb(var(--${prefix}-theme-color-ink-rgb) / var(--${prefix}-theme-color-ink-opacity));
398
+ }
399
+ }
400
+ `;
401
+ return `${header}@theme {
402
+ ${tailwindVars.join('\n')}
403
+ }
404
+
405
+ @layer base {
406
+ ${themeVars.join('\n')}
407
+ }
408
+
409
+ ${aliasBlock}`;
410
+ }
411
+ function addDynamicThemeColors(vars, prefix, tailwindName, cssVarName, addSpacingBefore) {
412
+ if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
413
+ vars.push('');
414
+ }
415
+ // Color variants
416
+ vars.push(` --color-${prefix}-${tailwindName}: rgb(var(--${prefix}-color-${cssVarName}));`);
417
+ vars.push(` --color-${prefix}-${tailwindName}-hover: rgb(var(--${prefix}-color-${cssVarName}-hover));`);
418
+ vars.push(` --color-${prefix}-${tailwindName}-focus: rgb(var(--${prefix}-color-${cssVarName}-focus));`);
419
+ vars.push(` --color-${prefix}-${tailwindName}-active: rgb(var(--${prefix}-color-${cssVarName}-active));`);
420
+ vars.push(` --color-${prefix}-${tailwindName}-disabled: rgb(var(--${prefix}-color-${cssVarName}-disabled));`);
421
+ vars.push('');
422
+ // On-color variants
423
+ vars.push(` --color-${prefix}-on-${tailwindName}: rgb(var(--${prefix}-color-on-${cssVarName}));`);
424
+ vars.push(` --color-${prefix}-on-${tailwindName}-hover: rgb(var(--${prefix}-color-on-${cssVarName}-hover));`);
425
+ vars.push(` --color-${prefix}-on-${tailwindName}-focus: rgb(var(--${prefix}-color-on-${cssVarName}-focus));`);
426
+ vars.push(` --color-${prefix}-on-${tailwindName}-active: rgb(var(--${prefix}-color-on-${cssVarName}-active));`);
427
+ vars.push(` --color-${prefix}-on-${tailwindName}-disabled: rgb(var(--${prefix}-color-on-${cssVarName}-disabled));`);
428
+ vars.push('');
429
+ }
430
+ function addDynamicInkColors(vars, prefix, tailwindName, cssVarName) {
431
+ vars.push(` --color-${prefix}-${tailwindName}: rgb(var(--${prefix}-color-${cssVarName}));`);
432
+ vars.push(` --color-${prefix}-${tailwindName}-hover: rgb(var(--${prefix}-color-${cssVarName}-hover));`);
433
+ vars.push(` --color-${prefix}-${tailwindName}-focus: rgb(var(--${prefix}-color-${cssVarName}-focus));`);
434
+ vars.push(` --color-${prefix}-${tailwindName}-active: rgb(var(--${prefix}-color-${cssVarName}-active));`);
435
+ vars.push(` --color-${prefix}-${tailwindName}-disabled: rgb(var(--${prefix}-color-${cssVarName}-disabled));`);
436
+ vars.push('');
437
+ }
438
+ function addTailwindColorVariants(vars, colorName, colorSet) {
439
+ // Tailwind 4 requires --color-* prefix and rgb() wrapper
440
+ // Always generate all variants with fallbacks
441
+ vars.push(` --color-${colorName}: rgb(${colorSet.default});`);
442
+ // For hover: use hover if exists, otherwise default
443
+ const hoverValue = 'hover' in colorSet && colorSet.hover ? colorSet.hover : colorSet.default;
444
+ vars.push(` --color-${colorName}-hover: rgb(${hoverValue});`);
445
+ // For focus: use focus if exists, otherwise hover, otherwise default
446
+ const focusValue = 'focus' in colorSet && colorSet.focus ? colorSet.focus : hoverValue;
447
+ vars.push(` --color-${colorName}-focus: rgb(${focusValue});`);
448
+ // For active: use active if exists, otherwise hover, otherwise default
449
+ const activeValue = 'active' in colorSet && colorSet.active ? colorSet.active : hoverValue;
450
+ vars.push(` --color-${colorName}-active: rgb(${activeValue});`);
451
+ // For disabled: use disabled if exists, otherwise default
452
+ const disabledValue = 'disabled' in colorSet && colorSet.disabled ? colorSet.disabled : colorSet.default;
453
+ vars.push(` --color-${colorName}-disabled: rgb(${disabledValue});`);
454
+ }
455
+ function addThemeColorVariants(vars, prefix, altPrefix, theme) {
456
+ const addSwatch = (level, swatch) => {
457
+ // Color variants with fallbacks
458
+ const defaultColor = swatch.color.default;
459
+ const hoverColor = swatch.color.hover || defaultColor;
460
+ const focusColor = swatch.color.focus || hoverColor;
461
+ const activeColor = swatch.color.active || hoverColor;
462
+ const disabledColor = swatch.color.disabled || defaultColor;
463
+ vars.push(` --${prefix}-color-${altPrefix}${level}: ${defaultColor};`);
464
+ vars.push(` --${prefix}-color-${altPrefix}${level}-hover: ${hoverColor};`);
465
+ vars.push(` --${prefix}-color-${altPrefix}${level}-focus: ${focusColor};`);
466
+ vars.push(` --${prefix}-color-${altPrefix}${level}-active: ${activeColor};`);
467
+ vars.push(` --${prefix}-color-${altPrefix}${level}-disabled: ${disabledColor};`);
468
+ vars.push('');
469
+ // On color variants with fallbacks
470
+ const onDefaultColor = swatch.onColor.default;
471
+ const onHoverColor = swatch.onColor.hover || onDefaultColor;
472
+ const onFocusColor = swatch.onColor.focus || onHoverColor;
473
+ const onActiveColor = swatch.onColor.active || onDefaultColor;
474
+ const onDisabledColor = swatch.onColor.disabled || onDefaultColor;
475
+ vars.push(` --${prefix}-color-${altPrefix}on-${level}: ${onDefaultColor};`);
476
+ vars.push(` --${prefix}-color-${altPrefix}on-${level}-hover: ${onHoverColor};`);
477
+ vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
478
+ vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
479
+ vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
480
+ const inkDefaultColor = swatch.inkColor?.default || defaultColor;
481
+ const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
482
+ const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
483
+ const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
484
+ const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
485
+ vars.push('');
486
+ vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
487
+ vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
488
+ vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
489
+ vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
490
+ vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
491
+ if (theme.secondary || theme.tertiary) {
492
+ vars.push('');
493
+ }
494
+ };
495
+ addSwatch('primary', theme.primary);
496
+ if (theme.secondary) {
497
+ addSwatch('secondary', theme.secondary);
498
+ }
499
+ if (theme.tertiary) {
500
+ addSwatch('tertiary', theme.tertiary);
501
+ }
502
+ }
503
+ function findMainStylesFile(tree) {
504
+ const potentialFiles = [];
505
+ visitNotIgnoredFiles(tree, '', (path) => {
506
+ if (path.match(/styles\.(css|scss)$/) && !path.includes('node_modules') && !path.includes('dist')) {
507
+ potentialFiles.push(path);
508
+ }
509
+ });
510
+ return potentialFiles;
511
+ }
512
+ //#endregion
513
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "$id": "tailwind-4-color-theme",
4
+ "title": "Tailwind 4 Theming Generator",
5
+ "description": "Generates Tailwind 4 compatible CSS from your Ethlete theme definitions",
6
+ "type": "object",
7
+ "properties": {
8
+ "themesPath": {
9
+ "type": "string",
10
+ "description": "Path to your themes file (relative to workspace root)",
11
+ "default": "src/themes.ts",
12
+ "x-prompt": "Where is your themes file located?"
13
+ },
14
+ "outputPath": {
15
+ "type": "string",
16
+ "description": "Where to output the generated Tailwind themes CSS",
17
+ "default": "src/styles/generated-tailwind-themes.css",
18
+ "x-prompt": "Where should the generated CSS be saved?"
19
+ },
20
+ "prefix": {
21
+ "type": "string",
22
+ "description": "Prefix for color variable names (e.g., 'et' creates --color-et-primary)",
23
+ "default": "et"
24
+ },
25
+ "skipFormat": {
26
+ "type": "boolean",
27
+ "description": "Skip formatting files after generation",
28
+ "default": false
29
+ }
30
+ },
31
+ "required": ["themesPath"]
32
+ }