@ethlete/core 5.0.0-next.4 → 5.0.0-next.40
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 +483 -0
- package/README.md +28 -4
- package/fesm2022/ethlete-core.mjs +5620 -1670
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +14 -4
- package/generators/migrate-provider-shape/migration-scope.js +35 -0
- package/generators/migrate-provider-shape/migration.js +51 -0
- package/generators/migrate-provider-shape/provider-shape.js +217 -0
- package/generators/migrate-provider-shape/schema.json +26 -0
- package/generators/migrate-to-v5/color-naming.js +175 -0
- package/generators/migrate-to-v5/create-provider.js +0 -4
- package/generators/migrate-to-v5/migration.js +6 -0
- package/generators/migrate-to-v5/schema.json +5 -0
- package/generators/migrate-to-v5/viewport-service.js +0 -1
- package/generators/{tailwind-4-theme → tailwind-4-color-theme}/generator.js +212 -103
- package/generators/tailwind-4-color-theme/schema.json +44 -0
- package/generators/tailwind-4-surface-theme/generator.js +467 -0
- package/generators/tailwind-4-surface-theme/schema.json +48 -0
- package/package.json +30 -14
- package/types/ethlete-core.d.ts +2818 -1432
- 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,20 @@ 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
|
-
|
|
235
|
+
// Error: Duplicate theme types
|
|
236
|
+
const typeMap = new Map();
|
|
237
|
+
for (const theme of themes) {
|
|
238
|
+
if (!theme.type) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const existing = typeMap.get(theme.type) || [];
|
|
242
|
+
existing.push(theme.name);
|
|
243
|
+
typeMap.set(theme.type, existing);
|
|
204
244
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
245
|
+
for (const [type, names] of typeMap) {
|
|
246
|
+
if (names.length > 1) {
|
|
247
|
+
throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
|
|
248
|
+
}
|
|
208
249
|
}
|
|
209
250
|
}
|
|
210
251
|
function parseThemeSwatch(obj, sourceFile) {
|
|
@@ -219,11 +260,18 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
219
260
|
if (!initializer) {
|
|
220
261
|
continue;
|
|
221
262
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
263
|
+
const colorMap = parseColorMap(initializer, sourceFile);
|
|
264
|
+
if (!colorMap) {
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (propName === 'color') {
|
|
268
|
+
swatch.color = colorMap;
|
|
269
|
+
}
|
|
270
|
+
else if (propName === 'onColor') {
|
|
271
|
+
swatch.onColor = colorMap;
|
|
272
|
+
}
|
|
273
|
+
else if (propName === 'inkColor') {
|
|
274
|
+
swatch.inkColor = colorMap;
|
|
227
275
|
}
|
|
228
276
|
}
|
|
229
277
|
if (!swatch.color || !swatch.onColor) {
|
|
@@ -231,6 +279,8 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
231
279
|
}
|
|
232
280
|
return swatch;
|
|
233
281
|
}
|
|
282
|
+
const COLOR_MAP_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
283
|
+
const isColorMapKey = (name) => COLOR_MAP_KEYS.some((key) => key === name);
|
|
234
284
|
function parseColorMap(initializer, sourceFile) {
|
|
235
285
|
// Handle spread expressions by resolving references
|
|
236
286
|
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
@@ -240,7 +290,7 @@ function parseColorMap(initializer, sourceFile) {
|
|
|
240
290
|
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
241
291
|
const propName = prop.getName();
|
|
242
292
|
const propValue = prop.getInitializer();
|
|
243
|
-
if (propValue?.isKind(SyntaxKind.StringLiteral)) {
|
|
293
|
+
if (propValue?.isKind(SyntaxKind.StringLiteral) && isColorMapKey(propName)) {
|
|
244
294
|
colorMap[propName] = propValue.getLiteralValue();
|
|
245
295
|
}
|
|
246
296
|
}
|
|
@@ -249,9 +299,7 @@ function parseColorMap(initializer, sourceFile) {
|
|
|
249
299
|
const spreadExpr = prop.getExpression();
|
|
250
300
|
if (spreadExpr.isKind(SyntaxKind.Identifier)) {
|
|
251
301
|
const referencedName = spreadExpr.getText();
|
|
252
|
-
const referencedDecl = sourceFile
|
|
253
|
-
.getVariableDeclarations()
|
|
254
|
-
.find((decl) => decl.getName() === referencedName);
|
|
302
|
+
const referencedDecl = sourceFile.getVariableDeclarations().find((decl) => decl.getName() === referencedName);
|
|
255
303
|
if (referencedDecl) {
|
|
256
304
|
const referencedObj = referencedDecl.getInitializer();
|
|
257
305
|
if (referencedObj?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
@@ -265,19 +313,19 @@ function parseColorMap(initializer, sourceFile) {
|
|
|
265
313
|
}
|
|
266
314
|
}
|
|
267
315
|
// Apply fallbacks for required fields
|
|
268
|
-
|
|
316
|
+
const defaultColor = colorMap.default;
|
|
317
|
+
if (defaultColor) {
|
|
269
318
|
// Check if this is a ThemeColorMap (has hover, active, or disabled explicitly set)
|
|
270
319
|
const isThemeColorMap = colorMap.hover !== undefined || colorMap.active !== undefined || colorMap.disabled !== undefined;
|
|
271
320
|
if (isThemeColorMap) {
|
|
272
321
|
// For ThemeColorMap - apply fallbacks for all required fields
|
|
273
|
-
const
|
|
274
|
-
const hoverColor = (colorMap.hover || defaultColor);
|
|
322
|
+
const hoverColor = colorMap.hover || defaultColor;
|
|
275
323
|
const result = {
|
|
276
324
|
default: defaultColor,
|
|
277
325
|
hover: hoverColor,
|
|
278
326
|
focus: colorMap.focus,
|
|
279
|
-
active:
|
|
280
|
-
disabled:
|
|
327
|
+
active: colorMap.active || hoverColor,
|
|
328
|
+
disabled: colorMap.disabled || defaultColor,
|
|
281
329
|
};
|
|
282
330
|
return result;
|
|
283
331
|
}
|
|
@@ -295,132 +343,182 @@ function createCssThemeName(name) {
|
|
|
295
343
|
.replace(/\s+/g, '-')
|
|
296
344
|
.replace(/[^a-z0-9-]/g, '');
|
|
297
345
|
}
|
|
298
|
-
function
|
|
346
|
+
function generateColorThemeNameTypes(themes, schema) {
|
|
347
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
348
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
349
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
350
|
+
const names = themes.map((theme) => `'${theme.name}'`).join(' | ');
|
|
351
|
+
return `/*
|
|
352
|
+
* Auto-generated by @ethlete/core:tailwind-4-color-theme
|
|
353
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
354
|
+
*
|
|
355
|
+
* Regenerate by running:
|
|
356
|
+
* 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}` : ''}
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
declare module '@ethlete/core' {
|
|
360
|
+
interface EthleteColorThemeNameRegistry {
|
|
361
|
+
name: ${names};
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export {};
|
|
366
|
+
`;
|
|
367
|
+
}
|
|
368
|
+
function generateTailwindThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
299
369
|
const tailwindVars = [];
|
|
300
370
|
const themeVars = [];
|
|
301
371
|
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
302
372
|
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
303
|
-
const header = `/*
|
|
373
|
+
const header = `/*
|
|
304
374
|
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
305
375
|
* DO NOT EDIT THIS FILE MANUALLY
|
|
306
|
-
*
|
|
376
|
+
*
|
|
307
377
|
* Generated from your theme definitions
|
|
308
378
|
* 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}` : ''}
|
|
379
|
+
* 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
380
|
*/
|
|
311
381
|
|
|
312
382
|
`;
|
|
313
383
|
// Validation is now done separately before this function is called
|
|
314
384
|
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
315
|
-
const
|
|
316
|
-
const regularThemes = themes.filter((t) => !t.isDefault && !t.isDefaultAlt);
|
|
385
|
+
const regularThemes = themes.filter((t) => !t.isDefault);
|
|
317
386
|
// Generate static Tailwind @theme block for each theme
|
|
318
387
|
for (const theme of themes) {
|
|
319
388
|
const name = createCssThemeName(theme.name);
|
|
320
389
|
// Add comment for theme section
|
|
321
390
|
tailwindVars.push(` /* ${theme.name} theme */`);
|
|
322
391
|
// Primary colors for Tailwind utilities
|
|
323
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
392
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}`, theme.primary.color);
|
|
324
393
|
tailwindVars.push('');
|
|
325
394
|
// On colors for Tailwind utilities
|
|
326
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
395
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}`, theme.primary.onColor);
|
|
396
|
+
tailwindVars.push('');
|
|
397
|
+
// Ink colors for standalone foreground usage
|
|
398
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
|
|
327
399
|
tailwindVars.push('');
|
|
328
400
|
// Secondary colors if present
|
|
329
401
|
if (theme.secondary) {
|
|
330
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
402
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-secondary`, theme.secondary.color);
|
|
331
403
|
tailwindVars.push('');
|
|
332
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
404
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-secondary`, theme.secondary.onColor);
|
|
333
405
|
tailwindVars.push('');
|
|
334
406
|
}
|
|
335
407
|
// Tertiary colors if present
|
|
336
408
|
if (theme.tertiary) {
|
|
337
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
409
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-tertiary`, theme.tertiary.color);
|
|
338
410
|
tailwindVars.push('');
|
|
339
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
411
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-tertiary`, theme.tertiary.onColor);
|
|
340
412
|
tailwindVars.push('');
|
|
341
413
|
}
|
|
342
414
|
}
|
|
343
415
|
tailwindVars.push('');
|
|
344
|
-
//
|
|
416
|
+
// Main theme dynamic colors
|
|
345
417
|
const mainThemes = [...defaultThemes, ...regularThemes];
|
|
346
418
|
const hasSecondary = mainThemes.some((t) => t.secondary);
|
|
347
419
|
const hasTertiary = mainThemes.some((t) => t.tertiary);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
//
|
|
351
|
-
|
|
352
|
-
|
|
420
|
+
// Collect the dynamic theme colors once. They are emitted in `@theme` (so Tailwind generates
|
|
421
|
+
// the utilities) and ALSO re-declared on every color selector in the alias block below. A
|
|
422
|
+
// Tailwind `@theme` variable only lands on `:root`, so `rgb(var(--<runtime>-color-*))` resolves
|
|
423
|
+
// once against the root color scope and inherits that concrete color into descendants - which
|
|
424
|
+
// means `bg-<prefix>-theme-*` utilities would ignore nested `.<runtime>-color--*` scopes.
|
|
425
|
+
// Re-declaring them per color selector makes the utilities resolve against the nearest scope.
|
|
426
|
+
const dynamicColorVars = [];
|
|
427
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme', 'primary', false);
|
|
428
|
+
addDynamicInkColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-ink', 'primary-ink');
|
|
353
429
|
if (hasSecondary) {
|
|
354
|
-
addDynamicThemeColors(
|
|
430
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-secondary', 'secondary', false);
|
|
355
431
|
}
|
|
356
432
|
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);
|
|
433
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-tertiary', 'tertiary', false);
|
|
367
434
|
}
|
|
368
|
-
|
|
435
|
+
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
436
|
+
tailwindVars.push(...dynamicColorVars);
|
|
437
|
+
// Generate runtime CSS for all themes
|
|
369
438
|
themes.forEach((theme) => {
|
|
370
439
|
const name = createCssThemeName(theme.name);
|
|
371
|
-
// Determine if this is the default
|
|
440
|
+
// Determine if this is the default theme
|
|
372
441
|
const isDefault = theme.isDefault;
|
|
373
|
-
|
|
374
|
-
// Generate main theme variant (.et-theme--{name})
|
|
442
|
+
// Generate color variant (.<runtimePrefix>-color--{name})
|
|
375
443
|
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}`];
|
|
444
|
+
// Default color gets :root and .<runtimePrefix>-color--default selectors
|
|
445
|
+
const selectors = [':root', `.${runtimePrefix}-color--default`, `.${runtimePrefix}-color--${name}`];
|
|
392
446
|
themeVars.push(`${selectors.join(', ')} {`);
|
|
393
447
|
}
|
|
394
448
|
else {
|
|
395
|
-
//
|
|
396
|
-
themeVars.push(`.${
|
|
449
|
+
// Regular colors just get their own class
|
|
450
|
+
themeVars.push(`.${runtimePrefix}-color--${name} {`);
|
|
397
451
|
}
|
|
398
|
-
addThemeColorVariants(themeVars,
|
|
452
|
+
addThemeColorVariants(themeVars, runtimePrefix, '', theme);
|
|
399
453
|
themeVars.push('}\n');
|
|
400
454
|
});
|
|
455
|
+
// Re-indent the dynamic theme colors (from 2-space `@theme` indent to the 4-space alias-block
|
|
456
|
+
// indent) and drop trailing blank lines so they slot cleanly into the alias selector below.
|
|
457
|
+
const dynamicColorAliasLines = dynamicColorVars.map((line) => (line === '' ? '' : ` ${line}`));
|
|
458
|
+
while (dynamicColorAliasLines.length && dynamicColorAliasLines[dynamicColorAliasLines.length - 1] === '') {
|
|
459
|
+
dynamicColorAliasLines.pop();
|
|
460
|
+
}
|
|
461
|
+
// Convenience var aliases - available on any element with a color class (or root for default)
|
|
462
|
+
const aliasBlock = `/* Convenience aliases (rgb + solid + opacity variants) */
|
|
463
|
+
@layer base {
|
|
464
|
+
:root, :where([class*="${runtimePrefix}-color--"]) {
|
|
465
|
+
--${runtimePrefix}-theme-color-primary-rgb: var(--${runtimePrefix}-color-primary);
|
|
466
|
+
--${runtimePrefix}-theme-color-primary-opacity: 1;
|
|
467
|
+
--${runtimePrefix}-theme-color-primary-solid: rgb(var(--${runtimePrefix}-theme-color-primary-rgb));
|
|
468
|
+
--${runtimePrefix}-theme-color-primary: rgb(var(--${runtimePrefix}-theme-color-primary-rgb) / var(--${runtimePrefix}-theme-color-primary-opacity));
|
|
469
|
+
|
|
470
|
+
--${runtimePrefix}-theme-color-on-primary-rgb: var(--${runtimePrefix}-color-on-primary);
|
|
471
|
+
--${runtimePrefix}-theme-color-on-primary-opacity: 1;
|
|
472
|
+
--${runtimePrefix}-theme-color-on-primary-solid: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
473
|
+
--${runtimePrefix}-theme-color-on-primary: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
474
|
+
|
|
475
|
+
--${runtimePrefix}-theme-color-ink-rgb: var(--${runtimePrefix}-color-primary-ink, var(--${runtimePrefix}-color-primary));
|
|
476
|
+
--${runtimePrefix}-theme-color-ink-opacity: 1;
|
|
477
|
+
--${runtimePrefix}-theme-color-ink-solid: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
478
|
+
--${runtimePrefix}-theme-color-ink: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
479
|
+
|
|
480
|
+
/* Dynamic Tailwind theme colors - re-declared per color scope so that
|
|
481
|
+
bg-${utilityPrefix}-theme-* utilities resolve against the nearest .${runtimePrefix}-color--*
|
|
482
|
+
scope instead of the value computed once at :root. */
|
|
483
|
+
${dynamicColorAliasLines.join('\n')}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
`;
|
|
401
487
|
return `${header}@theme {
|
|
402
488
|
${tailwindVars.join('\n')}
|
|
403
489
|
}
|
|
404
490
|
|
|
405
|
-
|
|
491
|
+
@layer base {
|
|
492
|
+
${themeVars.join('\n')}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
${aliasBlock}`;
|
|
406
496
|
}
|
|
407
|
-
function addDynamicThemeColors(vars,
|
|
497
|
+
function addDynamicThemeColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName, addSpacingBefore) {
|
|
408
498
|
if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
|
|
409
499
|
vars.push('');
|
|
410
500
|
}
|
|
411
501
|
// Color variants
|
|
412
|
-
vars.push(` --color-${
|
|
413
|
-
vars.push(` --color-${
|
|
414
|
-
vars.push(` --color-${
|
|
415
|
-
vars.push(` --color-${
|
|
416
|
-
vars.push(` --color-${
|
|
502
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
503
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
504
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
505
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
506
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
417
507
|
vars.push('');
|
|
418
508
|
// On-color variants
|
|
419
|
-
vars.push(` --color-${
|
|
420
|
-
vars.push(` --color-${
|
|
421
|
-
vars.push(` --color-${
|
|
422
|
-
vars.push(` --color-${
|
|
423
|
-
vars.push(` --color-${
|
|
509
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}: rgb(var(--${runtimePrefix}-color-on-${cssVarName}));`);
|
|
510
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-hover));`);
|
|
511
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-focus));`);
|
|
512
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-active));`);
|
|
513
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-disabled));`);
|
|
514
|
+
vars.push('');
|
|
515
|
+
}
|
|
516
|
+
function addDynamicInkColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName) {
|
|
517
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
518
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
519
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
520
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
521
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
424
522
|
vars.push('');
|
|
425
523
|
}
|
|
426
524
|
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
@@ -465,6 +563,17 @@ function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
|
465
563
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
466
564
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
467
565
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
566
|
+
const inkDefaultColor = swatch.inkColor?.default || defaultColor;
|
|
567
|
+
const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
|
|
568
|
+
const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
|
|
569
|
+
const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
|
|
570
|
+
const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
|
|
571
|
+
vars.push('');
|
|
572
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
|
|
573
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
|
|
574
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
|
|
575
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
|
|
576
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
|
|
468
577
|
if (theme.secondary || theme.tertiary) {
|
|
469
578
|
vars.push('');
|
|
470
579
|
}
|
|
@@ -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
|
+
}
|