@ethlete/core 5.0.0-next.3 → 5.0.0-next.31
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/CHANGELOG.md +304 -0
- package/README.md +30 -4
- package/fesm2022/ethlete-core.mjs +3944 -1102
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +9 -4
- package/generators/migrate-to-v5/color-naming.js +175 -0
- package/generators/migrate-to-v5/migration.js +6 -0
- package/generators/migrate-to-v5/schema.json +5 -0
- package/generators/{tailwind-4-theme → tailwind-4-color-theme}/generator.js +191 -91
- package/generators/tailwind-4-color-theme/schema.json +44 -0
- package/generators/tailwind-4-surface-theme/generator.js +459 -0
- package/generators/tailwind-4-surface-theme/schema.json +48 -0
- package/package.json +13 -13
- package/types/ethlete-core.d.ts +1947 -999
- package/generators/tailwind-4-theme/schema.json +0 -32
|
@@ -5,11 +5,15 @@ export default async function generate(tree, schema) {
|
|
|
5
5
|
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
6
6
|
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
7
7
|
const prefix = schema.prefix || 'et';
|
|
8
|
+
// The runtime prefix drives the theme-swap CSS variables (--<runtimePrefix>-color-primary)
|
|
9
|
+
// and selectors (.<runtimePrefix>-color--<name>). It defaults to `prefix` so existing
|
|
10
|
+
// consumers who only ever passed one prefix see no change in output.
|
|
11
|
+
const runtimePrefix = schema.runtimePrefix || prefix;
|
|
8
12
|
// Step 1: Check if themes file exists
|
|
9
13
|
if (!tree.exists(themesPath)) {
|
|
10
14
|
logger.error(`❌ Themes file not found at: ${themesPath}`);
|
|
11
15
|
logger.log(`\nPlease specify the correct path using --themesPath option.`);
|
|
12
|
-
logger.log(`Example: nx g @ethlete/core:tailwind-4-theme --themesPath=src/app/themes.ts\n`);
|
|
16
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
|
|
13
17
|
return;
|
|
14
18
|
}
|
|
15
19
|
logger.log(`📁 Reading themes from: ${themesPath}`);
|
|
@@ -32,7 +36,21 @@ export default async function generate(tree, schema) {
|
|
|
32
36
|
logger.log(' export const THEMES = [...] satisfies Theme[];\n');
|
|
33
37
|
return;
|
|
34
38
|
}
|
|
35
|
-
// Step 4:
|
|
39
|
+
// Step 4: Apply the generation-time default override, if any. The shared theme
|
|
40
|
+
// definitions may mark a default via isDefault, but in a monorepo each app picks its
|
|
41
|
+
// own default at its generation invocation — the option wins over the definitions.
|
|
42
|
+
if (schema.defaultTheme) {
|
|
43
|
+
try {
|
|
44
|
+
const defaultName = applyDefaultThemeOverride(themes, schema.defaultTheme);
|
|
45
|
+
logger.log(`🎯 Default theme set at generation time: ${defaultName}`);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
logger.error('❌ Invalid --defaultTheme option');
|
|
49
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Step 5: Validate theme configuration
|
|
36
54
|
try {
|
|
37
55
|
validateThemeConfiguration(themes);
|
|
38
56
|
}
|
|
@@ -41,17 +59,24 @@ export default async function generate(tree, schema) {
|
|
|
41
59
|
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
42
60
|
return;
|
|
43
61
|
}
|
|
44
|
-
// Step
|
|
62
|
+
// Step 6: Generate Tailwind CSS
|
|
45
63
|
logger.log('\n🎨 Generating Tailwind theme CSS...');
|
|
46
|
-
const css = generateTailwindThemeCss(themes, prefix, schema);
|
|
47
|
-
// Step
|
|
64
|
+
const css = generateTailwindThemeCss(themes, prefix, runtimePrefix, schema);
|
|
65
|
+
// Step 7: Write the generated CSS file
|
|
48
66
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
49
67
|
if (outputDir && !tree.exists(outputDir)) {
|
|
50
68
|
logger.log(`📁 Creating directory: ${outputDir}`);
|
|
51
69
|
}
|
|
52
70
|
tree.write(outputPath, css);
|
|
53
71
|
logger.log(`✅ Generated Tailwind themes at: ${outputPath}`);
|
|
54
|
-
// Step
|
|
72
|
+
// Step 8: Generate the `EthleteColorThemeNameRegistry` augmentation, so `etProvideColor`
|
|
73
|
+
// (and anything else that accepts a `RegisteredColorThemeName`) is checked/autocompleted
|
|
74
|
+
// against this app's actual theme names, instead of a plain `string`.
|
|
75
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
76
|
+
const typesDts = generateColorThemeNameTypes(themes, schema);
|
|
77
|
+
tree.write(typesOutputPath, typesDts);
|
|
78
|
+
logger.log(`✅ Generated color theme name types at: ${typesOutputPath}`);
|
|
79
|
+
// Step 9: Try to find and update main styles file
|
|
55
80
|
const mainStylesFiles = findMainStylesFile(tree);
|
|
56
81
|
if (mainStylesFiles.length > 0) {
|
|
57
82
|
logger.log('\n📝 Found potential main styles files:');
|
|
@@ -95,12 +120,9 @@ function extractThemesFromContent(content, filePath) {
|
|
|
95
120
|
if (!initializer) {
|
|
96
121
|
throw new Error('THEMES export has no initializer');
|
|
97
122
|
}
|
|
98
|
-
// Handle 'satisfies'
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
// Handle 'as const': [array] as const
|
|
103
|
-
if (initializer.isKind(SyntaxKind.AsExpression)) {
|
|
123
|
+
// Handle 'satisfies X'/'as const' wrappers, in either order: [array] satisfies Type[],
|
|
124
|
+
// [array] as const, or both combined.
|
|
125
|
+
while (initializer.isKind(SyntaxKind.SatisfiesExpression) || initializer.isKind(SyntaxKind.AsExpression)) {
|
|
104
126
|
initializer = initializer.getExpression();
|
|
105
127
|
}
|
|
106
128
|
if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
@@ -122,8 +144,9 @@ function extractThemesFromContent(content, filePath) {
|
|
|
122
144
|
logger.warn(`⚠️ Theme ${name} has no initializer`);
|
|
123
145
|
continue;
|
|
124
146
|
}
|
|
125
|
-
// Handle 'as const' on individual theme objects
|
|
126
|
-
|
|
147
|
+
// Handle 'as const' and 'satisfies X' wrappers on individual theme objects, in
|
|
148
|
+
// either order (e.g. `{...} satisfies Theme`, `{...} as const`, or both combined).
|
|
149
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
127
150
|
themeObj = themeObj.getExpression();
|
|
128
151
|
}
|
|
129
152
|
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
@@ -167,9 +190,9 @@ function parseThemeObject(obj, sourceFile) {
|
|
|
167
190
|
theme.isDefault = true;
|
|
168
191
|
}
|
|
169
192
|
break;
|
|
170
|
-
case '
|
|
171
|
-
if (initializer.isKind(SyntaxKind.
|
|
172
|
-
theme.
|
|
193
|
+
case 'type':
|
|
194
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
195
|
+
theme.type = initializer.getLiteralValue();
|
|
173
196
|
}
|
|
174
197
|
break;
|
|
175
198
|
case 'primary':
|
|
@@ -186,10 +209,21 @@ function parseThemeObject(obj, sourceFile) {
|
|
|
186
209
|
}
|
|
187
210
|
return theme;
|
|
188
211
|
}
|
|
212
|
+
// Makes the named theme the sole default, overriding any isDefault flags in the theme
|
|
213
|
+
// definitions. Matches the theme's name or its CSS-safe form. Returns the resolved name.
|
|
214
|
+
function applyDefaultThemeOverride(themes, defaultTheme) {
|
|
215
|
+
const wanted = createCssThemeName(defaultTheme);
|
|
216
|
+
const target = themes.find((t) => t.name === defaultTheme || createCssThemeName(t.name) === wanted);
|
|
217
|
+
if (!target) {
|
|
218
|
+
throw new Error(`No theme named "${defaultTheme}" found. Available themes: ${themes.map((t) => t.name).join(', ')}`);
|
|
219
|
+
}
|
|
220
|
+
for (const theme of themes) {
|
|
221
|
+
theme.isDefault = theme === target;
|
|
222
|
+
}
|
|
223
|
+
return target.name;
|
|
224
|
+
}
|
|
189
225
|
function validateThemeConfiguration(themes) {
|
|
190
226
|
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
191
|
-
const defaultAltThemes = themes.filter((t) => t.isDefaultAlt);
|
|
192
|
-
const bothDefaultAndAlt = themes.filter((t) => t.isDefault && t.isDefaultAlt);
|
|
193
227
|
// Error: No default theme
|
|
194
228
|
if (defaultThemes.length === 0) {
|
|
195
229
|
throw new Error('No default theme found. At least one theme must have isDefault: true');
|
|
@@ -198,13 +232,18 @@ function validateThemeConfiguration(themes) {
|
|
|
198
232
|
if (defaultThemes.length > 1) {
|
|
199
233
|
throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
|
|
200
234
|
}
|
|
201
|
-
// Error:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
235
|
+
// Error: Duplicate theme types
|
|
236
|
+
const typedThemes = themes.filter((t) => t.type);
|
|
237
|
+
const typeMap = new Map();
|
|
238
|
+
for (const theme of typedThemes) {
|
|
239
|
+
const existing = typeMap.get(theme.type) || [];
|
|
240
|
+
existing.push(theme.name);
|
|
241
|
+
typeMap.set(theme.type, existing);
|
|
242
|
+
}
|
|
243
|
+
for (const [type, names] of typeMap) {
|
|
244
|
+
if (names.length > 1) {
|
|
245
|
+
throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
|
|
246
|
+
}
|
|
208
247
|
}
|
|
209
248
|
}
|
|
210
249
|
function parseThemeSwatch(obj, sourceFile) {
|
|
@@ -219,7 +258,7 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
219
258
|
if (!initializer) {
|
|
220
259
|
continue;
|
|
221
260
|
}
|
|
222
|
-
if (propName === 'color' || propName === 'onColor') {
|
|
261
|
+
if (propName === 'color' || propName === 'onColor' || propName === 'inkColor') {
|
|
223
262
|
const colorMap = parseColorMap(initializer, sourceFile);
|
|
224
263
|
if (colorMap) {
|
|
225
264
|
swatch[propName] = colorMap;
|
|
@@ -295,132 +334,182 @@ function createCssThemeName(name) {
|
|
|
295
334
|
.replace(/\s+/g, '-')
|
|
296
335
|
.replace(/[^a-z0-9-]/g, '');
|
|
297
336
|
}
|
|
298
|
-
function
|
|
337
|
+
function generateColorThemeNameTypes(themes, schema) {
|
|
338
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
339
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
340
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
341
|
+
const names = themes.map((theme) => `'${theme.name}'`).join(' | ');
|
|
342
|
+
return `/*
|
|
343
|
+
* Auto-generated by @ethlete/core:tailwind-4-color-theme
|
|
344
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
345
|
+
*
|
|
346
|
+
* Regenerate by running:
|
|
347
|
+
* nx g @ethlete/core:tailwind-4-color-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.typesOutputPath ? ` --typesOutputPath=${typesOutputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}${schema.runtimePrefix && schema.runtimePrefix !== schema.prefix ? ` --runtimePrefix=${schema.runtimePrefix}` : ''}${schema.defaultTheme ? ` --defaultTheme=${schema.defaultTheme}` : ''}
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
declare module '@ethlete/core' {
|
|
351
|
+
interface EthleteColorThemeNameRegistry {
|
|
352
|
+
name: ${names};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
export {};
|
|
357
|
+
`;
|
|
358
|
+
}
|
|
359
|
+
function generateTailwindThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
299
360
|
const tailwindVars = [];
|
|
300
361
|
const themeVars = [];
|
|
301
362
|
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
302
363
|
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
303
|
-
const header = `/*
|
|
364
|
+
const header = `/*
|
|
304
365
|
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
305
366
|
* DO NOT EDIT THIS FILE MANUALLY
|
|
306
|
-
*
|
|
367
|
+
*
|
|
307
368
|
* Generated from your theme definitions
|
|
308
369
|
* This file can be regenerated by running:
|
|
309
|
-
* nx g @ethlete/core:tailwind-4-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}
|
|
370
|
+
* nx g @ethlete/core:tailwind-4-color-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}${schema.runtimePrefix && schema.runtimePrefix !== utilityPrefix ? ` --runtimePrefix=${schema.runtimePrefix}` : ''}${schema.defaultTheme ? ` --defaultTheme=${schema.defaultTheme}` : ''}
|
|
310
371
|
*/
|
|
311
372
|
|
|
312
373
|
`;
|
|
313
374
|
// Validation is now done separately before this function is called
|
|
314
375
|
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
315
|
-
const
|
|
316
|
-
const regularThemes = themes.filter((t) => !t.isDefault && !t.isDefaultAlt);
|
|
376
|
+
const regularThemes = themes.filter((t) => !t.isDefault);
|
|
317
377
|
// Generate static Tailwind @theme block for each theme
|
|
318
378
|
for (const theme of themes) {
|
|
319
379
|
const name = createCssThemeName(theme.name);
|
|
320
380
|
// Add comment for theme section
|
|
321
381
|
tailwindVars.push(` /* ${theme.name} theme */`);
|
|
322
382
|
// Primary colors for Tailwind utilities
|
|
323
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
383
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}`, theme.primary.color);
|
|
324
384
|
tailwindVars.push('');
|
|
325
385
|
// On colors for Tailwind utilities
|
|
326
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
386
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}`, theme.primary.onColor);
|
|
387
|
+
tailwindVars.push('');
|
|
388
|
+
// Ink colors for standalone foreground usage
|
|
389
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
|
|
327
390
|
tailwindVars.push('');
|
|
328
391
|
// Secondary colors if present
|
|
329
392
|
if (theme.secondary) {
|
|
330
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
393
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-secondary`, theme.secondary.color);
|
|
331
394
|
tailwindVars.push('');
|
|
332
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
395
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-secondary`, theme.secondary.onColor);
|
|
333
396
|
tailwindVars.push('');
|
|
334
397
|
}
|
|
335
398
|
// Tertiary colors if present
|
|
336
399
|
if (theme.tertiary) {
|
|
337
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
400
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-tertiary`, theme.tertiary.color);
|
|
338
401
|
tailwindVars.push('');
|
|
339
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
402
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-tertiary`, theme.tertiary.onColor);
|
|
340
403
|
tailwindVars.push('');
|
|
341
404
|
}
|
|
342
405
|
}
|
|
343
406
|
tailwindVars.push('');
|
|
344
|
-
//
|
|
407
|
+
// Main theme dynamic colors
|
|
345
408
|
const mainThemes = [...defaultThemes, ...regularThemes];
|
|
346
409
|
const hasSecondary = mainThemes.some((t) => t.secondary);
|
|
347
410
|
const hasTertiary = mainThemes.some((t) => t.tertiary);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
411
|
+
// Collect the dynamic theme colors once. They are emitted in `@theme` (so Tailwind generates
|
|
412
|
+
// the utilities) and ALSO re-declared on every color selector in the alias block below. A
|
|
413
|
+
// Tailwind `@theme` variable only lands on `:root`, so `rgb(var(--<runtime>-color-*))` resolves
|
|
414
|
+
// once against the root color scope and inherits that concrete color into descendants — which
|
|
415
|
+
// means `bg-<prefix>-theme-*` utilities would ignore nested `.<runtime>-color--*` scopes.
|
|
416
|
+
// Re-declaring them per color selector makes the utilities resolve against the nearest scope.
|
|
417
|
+
const dynamicColorVars = [];
|
|
418
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme', 'primary', false);
|
|
419
|
+
addDynamicInkColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-ink', 'primary-ink');
|
|
353
420
|
if (hasSecondary) {
|
|
354
|
-
addDynamicThemeColors(
|
|
421
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-secondary', 'secondary', false);
|
|
355
422
|
}
|
|
356
423
|
if (hasTertiary) {
|
|
357
|
-
addDynamicThemeColors(
|
|
358
|
-
}
|
|
359
|
-
// Alt theme dynamic colors
|
|
360
|
-
tailwindVars.push(' /* Alt theme dynamic colors */');
|
|
361
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme', 'alt-primary', true);
|
|
362
|
-
if (hasAltSecondary) {
|
|
363
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme-secondary', 'alt-secondary', false);
|
|
364
|
-
}
|
|
365
|
-
if (hasAltTertiary) {
|
|
366
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme-tertiary', 'alt-tertiary', false);
|
|
424
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-tertiary', 'tertiary', false);
|
|
367
425
|
}
|
|
368
|
-
|
|
426
|
+
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
427
|
+
tailwindVars.push(...dynamicColorVars);
|
|
428
|
+
// Generate runtime CSS for all themes
|
|
369
429
|
themes.forEach((theme) => {
|
|
370
430
|
const name = createCssThemeName(theme.name);
|
|
371
|
-
// Determine if this is the default
|
|
431
|
+
// Determine if this is the default theme
|
|
372
432
|
const isDefault = theme.isDefault;
|
|
373
|
-
|
|
374
|
-
// Generate main theme variant (.et-theme--{name})
|
|
433
|
+
// Generate color variant (.<runtimePrefix>-color--{name})
|
|
375
434
|
if (isDefault) {
|
|
376
|
-
// Default
|
|
377
|
-
const selectors = [':root', `.${
|
|
378
|
-
themeVars.push(`${selectors.join(', ')} {`);
|
|
379
|
-
}
|
|
380
|
-
else if (!isDefaultAlt) {
|
|
381
|
-
// Regular themes just get their own class
|
|
382
|
-
themeVars.push(`.${prefix}-theme--${name} {`);
|
|
383
|
-
}
|
|
384
|
-
if (isDefault || !isDefaultAlt) {
|
|
385
|
-
addThemeColorVariants(themeVars, prefix, '', theme);
|
|
386
|
-
themeVars.push('}\n');
|
|
387
|
-
}
|
|
388
|
-
// Generate alt theme variant (.et-theme-alt--{name})
|
|
389
|
-
if (isDefaultAlt) {
|
|
390
|
-
// Default alt theme gets :root and .et-theme-alt--default selectors
|
|
391
|
-
const selectors = [':root', `.${prefix}-theme-alt--default`, `.${prefix}-theme-alt--${name}`];
|
|
435
|
+
// Default color gets :root and .<runtimePrefix>-color--default selectors
|
|
436
|
+
const selectors = [':root', `.${runtimePrefix}-color--default`, `.${runtimePrefix}-color--${name}`];
|
|
392
437
|
themeVars.push(`${selectors.join(', ')} {`);
|
|
393
438
|
}
|
|
394
439
|
else {
|
|
395
|
-
//
|
|
396
|
-
themeVars.push(`.${
|
|
440
|
+
// Regular colors just get their own class
|
|
441
|
+
themeVars.push(`.${runtimePrefix}-color--${name} {`);
|
|
397
442
|
}
|
|
398
|
-
addThemeColorVariants(themeVars,
|
|
443
|
+
addThemeColorVariants(themeVars, runtimePrefix, '', theme);
|
|
399
444
|
themeVars.push('}\n');
|
|
400
445
|
});
|
|
446
|
+
// Re-indent the dynamic theme colors (from 2-space `@theme` indent to the 4-space alias-block
|
|
447
|
+
// indent) and drop trailing blank lines so they slot cleanly into the alias selector below.
|
|
448
|
+
const dynamicColorAliasLines = dynamicColorVars.map((line) => (line === '' ? '' : ` ${line}`));
|
|
449
|
+
while (dynamicColorAliasLines.length && dynamicColorAliasLines[dynamicColorAliasLines.length - 1] === '') {
|
|
450
|
+
dynamicColorAliasLines.pop();
|
|
451
|
+
}
|
|
452
|
+
// Convenience var aliases — available on any element with a color class (or root for default)
|
|
453
|
+
const aliasBlock = `/* Convenience aliases (rgb + solid + opacity variants) */
|
|
454
|
+
@layer base {
|
|
455
|
+
:root, :where([class*="${runtimePrefix}-color--"]) {
|
|
456
|
+
--${runtimePrefix}-theme-color-primary-rgb: var(--${runtimePrefix}-color-primary);
|
|
457
|
+
--${runtimePrefix}-theme-color-primary-opacity: 1;
|
|
458
|
+
--${runtimePrefix}-theme-color-primary-solid: rgb(var(--${runtimePrefix}-theme-color-primary-rgb));
|
|
459
|
+
--${runtimePrefix}-theme-color-primary: rgb(var(--${runtimePrefix}-theme-color-primary-rgb) / var(--${runtimePrefix}-theme-color-primary-opacity));
|
|
460
|
+
|
|
461
|
+
--${runtimePrefix}-theme-color-on-primary-rgb: var(--${runtimePrefix}-color-on-primary);
|
|
462
|
+
--${runtimePrefix}-theme-color-on-primary-opacity: 1;
|
|
463
|
+
--${runtimePrefix}-theme-color-on-primary-solid: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
464
|
+
--${runtimePrefix}-theme-color-on-primary: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
465
|
+
|
|
466
|
+
--${runtimePrefix}-theme-color-ink-rgb: var(--${runtimePrefix}-color-primary-ink, var(--${runtimePrefix}-color-primary));
|
|
467
|
+
--${runtimePrefix}-theme-color-ink-opacity: 1;
|
|
468
|
+
--${runtimePrefix}-theme-color-ink-solid: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
469
|
+
--${runtimePrefix}-theme-color-ink: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
470
|
+
|
|
471
|
+
/* Dynamic Tailwind theme colors — re-declared per color scope so that
|
|
472
|
+
bg-${utilityPrefix}-theme-* utilities resolve against the nearest .${runtimePrefix}-color--*
|
|
473
|
+
scope instead of the value computed once at :root. */
|
|
474
|
+
${dynamicColorAliasLines.join('\n')}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
`;
|
|
401
478
|
return `${header}@theme {
|
|
402
479
|
${tailwindVars.join('\n')}
|
|
403
480
|
}
|
|
404
481
|
|
|
405
|
-
|
|
482
|
+
@layer base {
|
|
483
|
+
${themeVars.join('\n')}
|
|
406
484
|
}
|
|
407
|
-
|
|
485
|
+
|
|
486
|
+
${aliasBlock}`;
|
|
487
|
+
}
|
|
488
|
+
function addDynamicThemeColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName, addSpacingBefore) {
|
|
408
489
|
if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
|
|
409
490
|
vars.push('');
|
|
410
491
|
}
|
|
411
492
|
// Color variants
|
|
412
|
-
vars.push(` --color-${
|
|
413
|
-
vars.push(` --color-${
|
|
414
|
-
vars.push(` --color-${
|
|
415
|
-
vars.push(` --color-${
|
|
416
|
-
vars.push(` --color-${
|
|
493
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
494
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
495
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
496
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
497
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
417
498
|
vars.push('');
|
|
418
499
|
// On-color variants
|
|
419
|
-
vars.push(` --color-${
|
|
420
|
-
vars.push(` --color-${
|
|
421
|
-
vars.push(` --color-${
|
|
422
|
-
vars.push(` --color-${
|
|
423
|
-
vars.push(` --color-${
|
|
500
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}: rgb(var(--${runtimePrefix}-color-on-${cssVarName}));`);
|
|
501
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-hover));`);
|
|
502
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-focus));`);
|
|
503
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-active));`);
|
|
504
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-disabled));`);
|
|
505
|
+
vars.push('');
|
|
506
|
+
}
|
|
507
|
+
function addDynamicInkColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName) {
|
|
508
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
509
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
510
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
511
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
512
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
424
513
|
vars.push('');
|
|
425
514
|
}
|
|
426
515
|
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
@@ -465,6 +554,17 @@ function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
|
465
554
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
466
555
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
467
556
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
557
|
+
const inkDefaultColor = swatch.inkColor?.default || defaultColor;
|
|
558
|
+
const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
|
|
559
|
+
const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
|
|
560
|
+
const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
|
|
561
|
+
const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
|
|
562
|
+
vars.push('');
|
|
563
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
|
|
564
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
|
|
565
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
|
|
566
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
|
|
567
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
|
|
468
568
|
if (theme.secondary || theme.tertiary) {
|
|
469
569
|
vars.push('');
|
|
470
570
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
"typesOutputPath": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Where to output the generated `EthleteColorThemeNameRegistry` augmentation (a .d.ts that narrows `etProvideColor` and other theme-name APIs to your actual theme names). Defaults to `outputPath` with its extension swapped to `.d.ts`."
|
|
23
|
+
},
|
|
24
|
+
"prefix": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"description": "Prefix for Tailwind utility color variable names (e.g., 'et' creates --color-et-primary, usable as bg-et-primary)",
|
|
27
|
+
"default": "et"
|
|
28
|
+
},
|
|
29
|
+
"runtimePrefix": {
|
|
30
|
+
"type": "string",
|
|
31
|
+
"description": "Prefix for the runtime theme-swap mechanism: the CSS custom properties (--<runtimePrefix>-color-primary) and selectors (.<runtimePrefix>-color--<name>) that ProvideColorDirective (etProvideColor) toggles between. Defaults to the same value as 'prefix'. Set this separately when your Tailwind utility prefix needs to differ from your runtime theme-switching prefix (e.g. because '--color-primary' is already a documented public API elsewhere in your app)."
|
|
32
|
+
},
|
|
33
|
+
"defaultTheme": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Name of the theme to use as the default, overriding any isDefault flags in the theme definitions. Use this when multiple apps share one theme definition set (e.g. in a monorepo) but each app needs a different default."
|
|
36
|
+
},
|
|
37
|
+
"skipFormat": {
|
|
38
|
+
"type": "boolean",
|
|
39
|
+
"description": "Skip formatting files after generation",
|
|
40
|
+
"default": false
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"required": ["themesPath"]
|
|
44
|
+
}
|