@ethlete/core 4.31.0 ā 5.0.0-next.0
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 +67 -0
- package/fesm2022/ethlete-core.mjs +3996 -4793
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +14 -0
- package/generators/migrate-to-v5/create-provider.js +158 -0
- package/generators/migrate-to-v5/migration.js +28 -0
- package/generators/migrate-to-v5/router-state-service.js +1064 -0
- package/generators/migrate-to-v5/schema.json +29 -0
- package/generators/migrate-to-v5/viewport-service.js +1678 -0
- package/generators/tailwind-4-theme/generator.js +490 -0
- package/generators/tailwind-4-theme/schema.json +32 -0
- package/package.json +18 -11
- package/types/ethlete-core.d.ts +2161 -0
- package/index.d.ts +0 -1975
|
@@ -0,0 +1,490 @@
|
|
|
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-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 'isDefaultAlt':
|
|
171
|
+
if (initializer.isKind(SyntaxKind.TrueKeyword)) {
|
|
172
|
+
theme.isDefaultAlt = true;
|
|
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
|
+
const defaultAltThemes = themes.filter((t) => t.isDefaultAlt);
|
|
192
|
+
const bothDefaultAndAlt = themes.filter((t) => t.isDefault && t.isDefaultAlt);
|
|
193
|
+
// Error: No default theme
|
|
194
|
+
if (defaultThemes.length === 0) {
|
|
195
|
+
throw new Error('No default theme found. At least one theme must have isDefault: true');
|
|
196
|
+
}
|
|
197
|
+
// Error: Multiple default themes
|
|
198
|
+
if (defaultThemes.length > 1) {
|
|
199
|
+
throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
|
|
200
|
+
}
|
|
201
|
+
// Error: Theme has both isDefault and isDefaultAlt
|
|
202
|
+
if (bothDefaultAndAlt.length > 0) {
|
|
203
|
+
throw new Error(`Theme "${bothDefaultAndAlt[0].name}" has both isDefault and isDefaultAlt set to true. A theme can only be one or the other`);
|
|
204
|
+
}
|
|
205
|
+
// Error: Multiple default alt themes
|
|
206
|
+
if (defaultAltThemes.length > 1) {
|
|
207
|
+
throw new Error(`Multiple default alt themes found: ${defaultAltThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefaultAlt: true`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function parseThemeSwatch(obj, sourceFile) {
|
|
211
|
+
const properties = obj.getProperties();
|
|
212
|
+
const swatch = {};
|
|
213
|
+
for (const prop of properties) {
|
|
214
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const propName = prop.getName();
|
|
218
|
+
const initializer = prop.getInitializer();
|
|
219
|
+
if (!initializer) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (propName === 'color' || propName === 'onColor') {
|
|
223
|
+
const colorMap = parseColorMap(initializer, sourceFile);
|
|
224
|
+
if (colorMap) {
|
|
225
|
+
swatch[propName] = colorMap;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!swatch.color || !swatch.onColor) {
|
|
230
|
+
throw new Error('ThemeSwatch must have color and onColor properties');
|
|
231
|
+
}
|
|
232
|
+
return swatch;
|
|
233
|
+
}
|
|
234
|
+
function parseColorMap(initializer, sourceFile) {
|
|
235
|
+
// Handle spread expressions by resolving references
|
|
236
|
+
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
237
|
+
const colorMap = {};
|
|
238
|
+
const properties = initializer.getProperties();
|
|
239
|
+
for (const prop of properties) {
|
|
240
|
+
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
241
|
+
const propName = prop.getName();
|
|
242
|
+
const propValue = prop.getInitializer();
|
|
243
|
+
if (propValue?.isKind(SyntaxKind.StringLiteral)) {
|
|
244
|
+
colorMap[propName] = propValue.getLiteralValue();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else if (prop.isKind(SyntaxKind.SpreadAssignment)) {
|
|
248
|
+
// Handle spread: { ...onColorDark, disabled: '...' }
|
|
249
|
+
const spreadExpr = prop.getExpression();
|
|
250
|
+
if (spreadExpr.isKind(SyntaxKind.Identifier)) {
|
|
251
|
+
const referencedName = spreadExpr.getText();
|
|
252
|
+
const referencedDecl = sourceFile
|
|
253
|
+
.getVariableDeclarations()
|
|
254
|
+
.find((decl) => decl.getName() === referencedName);
|
|
255
|
+
if (referencedDecl) {
|
|
256
|
+
const referencedObj = referencedDecl.getInitializer();
|
|
257
|
+
if (referencedObj?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
258
|
+
const spreadColors = parseColorMap(referencedObj, sourceFile);
|
|
259
|
+
if (spreadColors) {
|
|
260
|
+
Object.assign(colorMap, spreadColors);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Apply fallbacks for required fields
|
|
268
|
+
if (colorMap.default) {
|
|
269
|
+
// Check if this is a ThemeColorMap (has hover, active, or disabled explicitly set)
|
|
270
|
+
const isThemeColorMap = colorMap.hover !== undefined || colorMap.active !== undefined || colorMap.disabled !== undefined;
|
|
271
|
+
if (isThemeColorMap) {
|
|
272
|
+
// For ThemeColorMap - apply fallbacks for all required fields
|
|
273
|
+
const defaultColor = colorMap.default;
|
|
274
|
+
const hoverColor = (colorMap.hover || defaultColor);
|
|
275
|
+
const result = {
|
|
276
|
+
default: defaultColor,
|
|
277
|
+
hover: hoverColor,
|
|
278
|
+
focus: colorMap.focus,
|
|
279
|
+
active: (colorMap.active || hoverColor),
|
|
280
|
+
disabled: (colorMap.disabled || defaultColor),
|
|
281
|
+
};
|
|
282
|
+
return result;
|
|
283
|
+
}
|
|
284
|
+
// For OnThemeColorMap - no fallbacks needed, all fields are optional except default
|
|
285
|
+
return colorMap;
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
function createCssThemeName(name) {
|
|
292
|
+
// Convert theme name to CSS-safe format (e.g., "Primary Blue" -> "primary-blue")
|
|
293
|
+
return name
|
|
294
|
+
.toLowerCase()
|
|
295
|
+
.replace(/\s+/g, '-')
|
|
296
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
297
|
+
}
|
|
298
|
+
function generateTailwindThemeCss(themes, prefix, schema) {
|
|
299
|
+
const tailwindVars = [];
|
|
300
|
+
const themeVars = [];
|
|
301
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
302
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
303
|
+
const header = `/*
|
|
304
|
+
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
305
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
306
|
+
*
|
|
307
|
+
* Generated from your theme definitions
|
|
308
|
+
* 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}` : ''}
|
|
310
|
+
*/
|
|
311
|
+
|
|
312
|
+
`;
|
|
313
|
+
// Validation is now done separately before this function is called
|
|
314
|
+
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
315
|
+
const defaultAltThemes = themes.filter((t) => t.isDefaultAlt);
|
|
316
|
+
const regularThemes = themes.filter((t) => !t.isDefault && !t.isDefaultAlt);
|
|
317
|
+
// Generate static Tailwind @theme block for each theme
|
|
318
|
+
for (const theme of themes) {
|
|
319
|
+
const name = createCssThemeName(theme.name);
|
|
320
|
+
// Add comment for theme section
|
|
321
|
+
tailwindVars.push(` /* ${theme.name} theme */`);
|
|
322
|
+
// Primary colors for Tailwind utilities
|
|
323
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-${name}`, theme.primary.color);
|
|
324
|
+
tailwindVars.push('');
|
|
325
|
+
// On colors for Tailwind utilities
|
|
326
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}`, theme.primary.onColor);
|
|
327
|
+
tailwindVars.push('');
|
|
328
|
+
// Secondary colors if present
|
|
329
|
+
if (theme.secondary) {
|
|
330
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-${name}-secondary`, theme.secondary.color);
|
|
331
|
+
tailwindVars.push('');
|
|
332
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}-secondary`, theme.secondary.onColor);
|
|
333
|
+
tailwindVars.push('');
|
|
334
|
+
}
|
|
335
|
+
// Tertiary colors if present
|
|
336
|
+
if (theme.tertiary) {
|
|
337
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-${name}-tertiary`, theme.tertiary.color);
|
|
338
|
+
tailwindVars.push('');
|
|
339
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}-tertiary`, theme.tertiary.onColor);
|
|
340
|
+
tailwindVars.push('');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
tailwindVars.push('');
|
|
344
|
+
// Generate dynamic theme variables - check main themes and alt themes separately
|
|
345
|
+
const mainThemes = [...defaultThemes, ...regularThemes];
|
|
346
|
+
const hasSecondary = mainThemes.some((t) => t.secondary);
|
|
347
|
+
const hasTertiary = mainThemes.some((t) => t.tertiary);
|
|
348
|
+
const hasAltSecondary = defaultAltThemes.some((t) => t.secondary);
|
|
349
|
+
const hasAltTertiary = defaultAltThemes.some((t) => t.tertiary);
|
|
350
|
+
// Main theme dynamic colors
|
|
351
|
+
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
352
|
+
addDynamicThemeColors(tailwindVars, prefix, 'theme', 'primary', true);
|
|
353
|
+
if (hasSecondary) {
|
|
354
|
+
addDynamicThemeColors(tailwindVars, prefix, 'theme-secondary', 'secondary', false);
|
|
355
|
+
}
|
|
356
|
+
if (hasTertiary) {
|
|
357
|
+
addDynamicThemeColors(tailwindVars, prefix, 'theme-tertiary', 'tertiary', false);
|
|
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);
|
|
367
|
+
}
|
|
368
|
+
// Generate runtime CSS for ALL themes (both main and alt variants)
|
|
369
|
+
themes.forEach((theme) => {
|
|
370
|
+
const name = createCssThemeName(theme.name);
|
|
371
|
+
// Determine if this is the default or default-alt theme
|
|
372
|
+
const isDefault = theme.isDefault;
|
|
373
|
+
const isDefaultAlt = theme.isDefaultAlt;
|
|
374
|
+
// Generate main theme variant (.et-theme--{name})
|
|
375
|
+
if (isDefault) {
|
|
376
|
+
// Default theme gets :root and .et-theme--default selectors
|
|
377
|
+
const selectors = [':root', `.${prefix}-theme--default`, `.${prefix}-theme--${name}`];
|
|
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}`];
|
|
392
|
+
themeVars.push(`${selectors.join(', ')} {`);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// All themes (including default and regular) get an alt variant
|
|
396
|
+
themeVars.push(`.${prefix}-theme-alt--${name} {`);
|
|
397
|
+
}
|
|
398
|
+
addThemeColorVariants(themeVars, prefix, 'alt-', theme);
|
|
399
|
+
themeVars.push('}\n');
|
|
400
|
+
});
|
|
401
|
+
return `${header}@theme {
|
|
402
|
+
${tailwindVars.join('\n')}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
${themeVars.join('\n')}`;
|
|
406
|
+
}
|
|
407
|
+
function addDynamicThemeColors(vars, prefix, tailwindName, cssVarName, addSpacingBefore) {
|
|
408
|
+
if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
|
|
409
|
+
vars.push('');
|
|
410
|
+
}
|
|
411
|
+
// Color variants
|
|
412
|
+
vars.push(` --color-${prefix}-${tailwindName}: rgb(var(--${prefix}-color-${cssVarName}));`);
|
|
413
|
+
vars.push(` --color-${prefix}-${tailwindName}-hover: rgb(var(--${prefix}-color-${cssVarName}-hover));`);
|
|
414
|
+
vars.push(` --color-${prefix}-${tailwindName}-focus: rgb(var(--${prefix}-color-${cssVarName}-focus));`);
|
|
415
|
+
vars.push(` --color-${prefix}-${tailwindName}-active: rgb(var(--${prefix}-color-${cssVarName}-active));`);
|
|
416
|
+
vars.push(` --color-${prefix}-${tailwindName}-disabled: rgb(var(--${prefix}-color-${cssVarName}-disabled));`);
|
|
417
|
+
vars.push('');
|
|
418
|
+
// On-color variants
|
|
419
|
+
vars.push(` --color-${prefix}-on-${tailwindName}: rgb(var(--${prefix}-color-on-${cssVarName}));`);
|
|
420
|
+
vars.push(` --color-${prefix}-on-${tailwindName}-hover: rgb(var(--${prefix}-color-on-${cssVarName}-hover));`);
|
|
421
|
+
vars.push(` --color-${prefix}-on-${tailwindName}-focus: rgb(var(--${prefix}-color-on-${cssVarName}-focus));`);
|
|
422
|
+
vars.push(` --color-${prefix}-on-${tailwindName}-active: rgb(var(--${prefix}-color-on-${cssVarName}-active));`);
|
|
423
|
+
vars.push(` --color-${prefix}-on-${tailwindName}-disabled: rgb(var(--${prefix}-color-on-${cssVarName}-disabled));`);
|
|
424
|
+
vars.push('');
|
|
425
|
+
}
|
|
426
|
+
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
427
|
+
// Tailwind 4 requires --color-* prefix and rgb() wrapper
|
|
428
|
+
// Always generate all variants with fallbacks
|
|
429
|
+
vars.push(` --color-${colorName}: rgb(${colorSet.default});`);
|
|
430
|
+
// For hover: use hover if exists, otherwise default
|
|
431
|
+
const hoverValue = 'hover' in colorSet && colorSet.hover ? colorSet.hover : colorSet.default;
|
|
432
|
+
vars.push(` --color-${colorName}-hover: rgb(${hoverValue});`);
|
|
433
|
+
// For focus: use focus if exists, otherwise hover, otherwise default
|
|
434
|
+
const focusValue = 'focus' in colorSet && colorSet.focus ? colorSet.focus : hoverValue;
|
|
435
|
+
vars.push(` --color-${colorName}-focus: rgb(${focusValue});`);
|
|
436
|
+
// For active: use active if exists, otherwise hover, otherwise default
|
|
437
|
+
const activeValue = 'active' in colorSet && colorSet.active ? colorSet.active : hoverValue;
|
|
438
|
+
vars.push(` --color-${colorName}-active: rgb(${activeValue});`);
|
|
439
|
+
// For disabled: use disabled if exists, otherwise default
|
|
440
|
+
const disabledValue = 'disabled' in colorSet && colorSet.disabled ? colorSet.disabled : colorSet.default;
|
|
441
|
+
vars.push(` --color-${colorName}-disabled: rgb(${disabledValue});`);
|
|
442
|
+
}
|
|
443
|
+
function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
444
|
+
const addSwatch = (level, swatch) => {
|
|
445
|
+
// Color variants with fallbacks
|
|
446
|
+
const defaultColor = swatch.color.default;
|
|
447
|
+
const hoverColor = swatch.color.hover || defaultColor;
|
|
448
|
+
const focusColor = swatch.color.focus || hoverColor;
|
|
449
|
+
const activeColor = swatch.color.active || hoverColor;
|
|
450
|
+
const disabledColor = swatch.color.disabled || defaultColor;
|
|
451
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}: ${defaultColor};`);
|
|
452
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-hover: ${hoverColor};`);
|
|
453
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-focus: ${focusColor};`);
|
|
454
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-active: ${activeColor};`);
|
|
455
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-disabled: ${disabledColor};`);
|
|
456
|
+
vars.push('');
|
|
457
|
+
// On color variants with fallbacks
|
|
458
|
+
const onDefaultColor = swatch.onColor.default;
|
|
459
|
+
const onHoverColor = swatch.onColor.hover || onDefaultColor;
|
|
460
|
+
const onFocusColor = swatch.onColor.focus || onHoverColor;
|
|
461
|
+
const onActiveColor = swatch.onColor.active || onDefaultColor;
|
|
462
|
+
const onDisabledColor = swatch.onColor.disabled || onDefaultColor;
|
|
463
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}: ${onDefaultColor};`);
|
|
464
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-hover: ${onHoverColor};`);
|
|
465
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
466
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
467
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
468
|
+
if (theme.secondary || theme.tertiary) {
|
|
469
|
+
vars.push('');
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
addSwatch('primary', theme.primary);
|
|
473
|
+
if (theme.secondary) {
|
|
474
|
+
addSwatch('secondary', theme.secondary);
|
|
475
|
+
}
|
|
476
|
+
if (theme.tertiary) {
|
|
477
|
+
addSwatch('tertiary', theme.tertiary);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
function findMainStylesFile(tree) {
|
|
481
|
+
const potentialFiles = [];
|
|
482
|
+
visitNotIgnoredFiles(tree, '', (path) => {
|
|
483
|
+
if (path.match(/styles\.(css|scss)$/) && !path.includes('node_modules') && !path.includes('dist')) {
|
|
484
|
+
potentialFiles.push(path);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
return potentialFiles;
|
|
488
|
+
}
|
|
489
|
+
//#endregion
|
|
490
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "tailwind-4-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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0-next.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
7
|
-
"@angular
|
|
8
|
-
"@angular/
|
|
9
|
-
"@angular/
|
|
10
|
-
"@angular/
|
|
11
|
-
"@angular/
|
|
12
|
-
"@angular/
|
|
7
|
+
"@analogjs/vite-plugin-angular": "2.3.1",
|
|
8
|
+
"@angular/cdk": "21.2.1",
|
|
9
|
+
"@angular/common": "21.2.1",
|
|
10
|
+
"@angular/core": "21.2.1",
|
|
11
|
+
"@angular/forms": "21.2.1",
|
|
12
|
+
"@angular/platform-browser": "21.2.1",
|
|
13
|
+
"@angular/router": "21.2.1",
|
|
13
14
|
"@ethlete/types": "^1.6.2",
|
|
14
|
-
"@floating-ui/dom": "1.7.
|
|
15
|
-
"
|
|
15
|
+
"@floating-ui/dom": "1.7.6",
|
|
16
|
+
"@nx/devkit": "22.5.4",
|
|
17
|
+
"@nx/vite": "22.5.4",
|
|
18
|
+
"rxjs": "7.8.2",
|
|
19
|
+
"ts-morph": "21.0.1",
|
|
20
|
+
"typescript": "5.9.3",
|
|
21
|
+
"vite": "^7.0.0"
|
|
16
22
|
},
|
|
23
|
+
"generators": "./generators/generators.json",
|
|
17
24
|
"module": "fesm2022/ethlete-core.mjs",
|
|
18
|
-
"typings": "
|
|
25
|
+
"typings": "types/ethlete-core.d.ts",
|
|
19
26
|
"exports": {
|
|
20
27
|
"./package.json": {
|
|
21
28
|
"default": "./package.json"
|
|
22
29
|
},
|
|
23
30
|
".": {
|
|
24
|
-
"types": "./
|
|
31
|
+
"types": "./types/ethlete-core.d.ts",
|
|
25
32
|
"default": "./fesm2022/ethlete-core.mjs"
|
|
26
33
|
}
|
|
27
34
|
},
|