@ethlete/core 5.0.0-next.6 ā 5.0.0-next.61
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 +714 -0
- package/README.md +28 -4
- package/fesm2022/ethlete-core.mjs +8061 -3502
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/devtools-about/generator.js +122 -0
- package/generators/devtools-about/schema.json +25 -0
- package/generators/generators.json +26 -5
- package/generators/migrate-provider-shape/migration-scope.js +35 -0
- package/generators/migrate-provider-shape/migration.js +50 -0
- package/generators/migrate-provider-shape/provider-shape.js +284 -0
- package/generators/migrate-provider-shape/schema.json +26 -0
- package/generators/migrate-surface-interaction-swatch/interaction-swatch.js +63 -0
- package/generators/migrate-surface-interaction-swatch/migration.js +36 -0
- package/generators/migrate-surface-interaction-swatch/schema.json +26 -0
- package/generators/migrate-to-v5/apply-replacements.js +14 -0
- package/generators/migrate-to-v5/color-naming.js +124 -0
- package/generators/migrate-to-v5/create-provider.js +67 -55
- package/generators/migrate-to-v5/migration-files.js +13 -0
- package/generators/migrate-to-v5/migration.js +33 -19
- package/generators/migrate-to-v5/removed-exports.js +73 -0
- package/generators/migrate-to-v5/router-state-service.js +123 -330
- package/generators/migrate-to-v5/schema.json +22 -0
- package/generators/migrate-to-v5/service-classes.js +108 -0
- package/generators/migrate-to-v5/viewport-service.js +224 -491
- package/generators/tailwind-4-color-theme/generator.js +594 -0
- package/generators/tailwind-4-color-theme/schema.json +44 -0
- package/generators/tailwind-4-surface-theme/generator.js +599 -0
- package/generators/tailwind-4-surface-theme/schema.json +48 -0
- package/migrations/seo-directive-removed.md +80 -0
- package/migrations/surface-theme-regenerate.md +20 -0
- package/migrations.json +43 -0
- package/package.json +25 -14
- package/types/ethlete-core.d.ts +3337 -1775
- package/generators/tailwind-4-theme/generator.js +0 -490
- package/generators/tailwind-4-theme/schema.json +0 -32
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { formatFiles, logger, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
import { posix } from 'path';
|
|
3
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
4
|
+
export default async function generate(tree, schema) {
|
|
5
|
+
logger.log('\nš Starting Tailwind 4 theme generator...\n');
|
|
6
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
7
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
8
|
+
const prefix = schema.prefix || 'et';
|
|
9
|
+
const runtimePrefix = schema.runtimePrefix || prefix;
|
|
10
|
+
if (!tree.exists(themesPath)) {
|
|
11
|
+
logger.error(`ā Themes file not found at: ${themesPath}`);
|
|
12
|
+
logger.log(`\nPlease specify the correct path using --themesPath option.`);
|
|
13
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
logger.log(`š Reading themes from: ${themesPath}`);
|
|
17
|
+
const themesContent = tree.read(themesPath, 'utf-8');
|
|
18
|
+
if (!themesContent) {
|
|
19
|
+
logger.error('ā Failed to read themes file');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let themes;
|
|
23
|
+
try {
|
|
24
|
+
themes = extractThemesFromContent(tree, themesContent, themesPath);
|
|
25
|
+
logger.log(`ā
Found ${themes.length} theme(s)`);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
logger.error('ā Failed to parse themes file');
|
|
29
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
logger.log('\nThe themes file must export themes as:');
|
|
31
|
+
logger.log(' export const THEMES = [...] satisfies Theme[];\n');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Apply the generation-time default override, if any. The shared theme
|
|
35
|
+
// definitions may mark a default via isDefault, but in a monorepo each app picks its
|
|
36
|
+
// own default at its generation invocation - the option wins over the definitions.
|
|
37
|
+
if (schema.defaultTheme) {
|
|
38
|
+
try {
|
|
39
|
+
const defaultName = applyDefaultThemeOverride(themes, schema.defaultTheme);
|
|
40
|
+
logger.log(`šÆ Default theme set at generation time: ${defaultName}`);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger.error('ā Invalid --defaultTheme option');
|
|
44
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
validateThemeConfiguration(themes);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
logger.error('ā Theme configuration error');
|
|
53
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
logger.log('\nšØ Generating Tailwind theme CSS...');
|
|
57
|
+
const css = generateTailwindThemeCss(themes, prefix, runtimePrefix, schema);
|
|
58
|
+
const typesOutputPath = schema.typesOutputPath ?? outputPath.replace(/\.css$/, '.d.ts');
|
|
59
|
+
if (typesOutputPath === outputPath) {
|
|
60
|
+
throw new Error('A custom outputPath must end in .css or be paired with a distinct typesOutputPath.');
|
|
61
|
+
}
|
|
62
|
+
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
63
|
+
if (outputDir && !tree.exists(outputDir)) {
|
|
64
|
+
logger.log(`š Creating directory: ${outputDir}`);
|
|
65
|
+
}
|
|
66
|
+
tree.write(outputPath, css);
|
|
67
|
+
logger.log(`ā
Generated Tailwind themes at: ${outputPath}`);
|
|
68
|
+
// Generate the `EthleteColorThemeNameRegistry` augmentation, so `etProvideColor`
|
|
69
|
+
// (and anything else that accepts a `RegisteredColorThemeName`) is checked/autocompleted
|
|
70
|
+
// against this app's actual theme names, instead of a plain `string`.
|
|
71
|
+
const typesDts = generateColorThemeNameTypes(themes, schema);
|
|
72
|
+
tree.write(typesOutputPath, typesDts);
|
|
73
|
+
logger.log(`ā
Generated color theme name types at: ${typesOutputPath}`);
|
|
74
|
+
const mainStylesFiles = findMainStylesFile(tree);
|
|
75
|
+
if (mainStylesFiles.length > 0) {
|
|
76
|
+
logger.log('\nš Found potential main styles files:');
|
|
77
|
+
mainStylesFiles.forEach((file) => logger.log(` - ${file}`));
|
|
78
|
+
logger.log('\nā ļø Please manually import the generated themes:');
|
|
79
|
+
logger.log(` @import './${outputPath.replace('src/styles/', '')}';`);
|
|
80
|
+
}
|
|
81
|
+
if (!schema.skipFormat) {
|
|
82
|
+
await formatFiles(tree);
|
|
83
|
+
}
|
|
84
|
+
logger.log('\nā
Generation completed successfully!\n');
|
|
85
|
+
}
|
|
86
|
+
function extractThemesFromContent(tree, content, filePath) {
|
|
87
|
+
const project = new Project({
|
|
88
|
+
useInMemoryFileSystem: true,
|
|
89
|
+
compilerOptions: {
|
|
90
|
+
target: 99,
|
|
91
|
+
module: 99,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
95
|
+
const context = { tree, project };
|
|
96
|
+
const themes = [];
|
|
97
|
+
const exportedDeclarations = sourceFile.getVariableDeclarations().filter((decl) => {
|
|
98
|
+
const statement = decl.getVariableStatement();
|
|
99
|
+
return statement?.isExported();
|
|
100
|
+
});
|
|
101
|
+
const themesArray = exportedDeclarations.find((decl) => {
|
|
102
|
+
const name = decl.getName();
|
|
103
|
+
return name === 'THEMES' || name === 'themes';
|
|
104
|
+
});
|
|
105
|
+
if (!themesArray) {
|
|
106
|
+
throw new Error('Could not find THEMES or themes export');
|
|
107
|
+
}
|
|
108
|
+
let initializer = themesArray.getInitializer();
|
|
109
|
+
if (!initializer) {
|
|
110
|
+
throw new Error('THEMES export has no initializer');
|
|
111
|
+
}
|
|
112
|
+
// Handle 'satisfies X'/'as const' wrappers, in either order: [array] satisfies Type[],
|
|
113
|
+
// [array] as const, or both combined.
|
|
114
|
+
while (initializer.isKind(SyntaxKind.SatisfiesExpression) || initializer.isKind(SyntaxKind.AsExpression)) {
|
|
115
|
+
initializer = initializer.getExpression();
|
|
116
|
+
}
|
|
117
|
+
if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
118
|
+
throw new Error('THEMES export must be an array literal');
|
|
119
|
+
}
|
|
120
|
+
const elements = initializer.getElements();
|
|
121
|
+
for (const element of elements) {
|
|
122
|
+
let themeObj;
|
|
123
|
+
let name = '<inline>';
|
|
124
|
+
if (element.isKind(SyntaxKind.Identifier)) {
|
|
125
|
+
name = element.getText();
|
|
126
|
+
const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
|
|
127
|
+
if (!themeDecl) {
|
|
128
|
+
logger.warn(`ā ļø Could not find declaration for theme: ${name}`);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
themeObj = themeDecl.getInitializer();
|
|
132
|
+
if (!themeObj) {
|
|
133
|
+
logger.warn(`ā ļø Theme ${name} has no initializer`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
themeObj = element;
|
|
139
|
+
}
|
|
140
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
141
|
+
themeObj = themeObj.getExpression();
|
|
142
|
+
}
|
|
143
|
+
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
144
|
+
logger.warn(`ā ļø Theme ${name} is not an object literal`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const theme = parseThemeObject(themeObj, sourceFile, context);
|
|
149
|
+
themes.push(theme);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
logger.warn(`ā ļø Failed to parse theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (themes.length === 0) {
|
|
156
|
+
throw new Error('No valid themes found in THEMES array');
|
|
157
|
+
}
|
|
158
|
+
return themes;
|
|
159
|
+
}
|
|
160
|
+
function parseThemeObject(obj, sourceFile, context) {
|
|
161
|
+
const properties = obj.getProperties();
|
|
162
|
+
const theme = {};
|
|
163
|
+
for (const prop of properties) {
|
|
164
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const propName = prop.getName();
|
|
168
|
+
const initializer = prop.getInitializer();
|
|
169
|
+
if (!initializer) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
switch (propName) {
|
|
173
|
+
case 'name':
|
|
174
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
175
|
+
theme.name = initializer.getLiteralValue();
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case 'isDefault':
|
|
179
|
+
if (initializer.isKind(SyntaxKind.TrueKeyword)) {
|
|
180
|
+
theme.isDefault = true;
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
183
|
+
case 'type':
|
|
184
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
185
|
+
theme.type = initializer.getLiteralValue();
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
case 'primary':
|
|
189
|
+
case 'secondary':
|
|
190
|
+
case 'tertiary':
|
|
191
|
+
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
192
|
+
theme[propName] = parseThemeSwatch(initializer, sourceFile, context);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (!theme.name || !theme.primary) {
|
|
198
|
+
throw new Error('Theme must have name and primary properties');
|
|
199
|
+
}
|
|
200
|
+
return theme;
|
|
201
|
+
}
|
|
202
|
+
function applyDefaultThemeOverride(themes, defaultTheme) {
|
|
203
|
+
const wanted = createCssThemeName(defaultTheme);
|
|
204
|
+
const target = themes.find((t) => t.name === defaultTheme || createCssThemeName(t.name) === wanted);
|
|
205
|
+
if (!target) {
|
|
206
|
+
throw new Error(`No theme named "${defaultTheme}" found. Available themes: ${themes.map((t) => t.name).join(', ')}`);
|
|
207
|
+
}
|
|
208
|
+
for (const theme of themes) {
|
|
209
|
+
theme.isDefault = theme === target;
|
|
210
|
+
}
|
|
211
|
+
return target.name;
|
|
212
|
+
}
|
|
213
|
+
function validateThemeConfiguration(themes) {
|
|
214
|
+
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
215
|
+
if (defaultThemes.length === 0) {
|
|
216
|
+
throw new Error('No default theme found. At least one theme must have isDefault: true');
|
|
217
|
+
}
|
|
218
|
+
if (defaultThemes.length > 1) {
|
|
219
|
+
throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
|
|
220
|
+
}
|
|
221
|
+
const typeMap = new Map();
|
|
222
|
+
for (const theme of themes) {
|
|
223
|
+
if (!theme.type) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const existing = typeMap.get(theme.type) || [];
|
|
227
|
+
existing.push(theme.name);
|
|
228
|
+
typeMap.set(theme.type, existing);
|
|
229
|
+
}
|
|
230
|
+
for (const [type, names] of typeMap) {
|
|
231
|
+
if (names.length > 1) {
|
|
232
|
+
throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function parseThemeSwatch(obj, sourceFile, context) {
|
|
237
|
+
const properties = obj.getProperties();
|
|
238
|
+
const swatch = {};
|
|
239
|
+
for (const prop of properties) {
|
|
240
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const propName = prop.getName();
|
|
244
|
+
const initializer = prop.getInitializer();
|
|
245
|
+
if (!initializer) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const colorMap = parseColorMap(initializer, sourceFile, context);
|
|
249
|
+
if (!colorMap) {
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (propName === 'color') {
|
|
253
|
+
swatch.color = colorMap;
|
|
254
|
+
}
|
|
255
|
+
else if (propName === 'onColor') {
|
|
256
|
+
swatch.onColor = colorMap;
|
|
257
|
+
}
|
|
258
|
+
else if (propName === 'inkColor') {
|
|
259
|
+
swatch.inkColor = colorMap;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!swatch.color || !swatch.onColor) {
|
|
263
|
+
throw new Error('ThemeSwatch must have color and onColor properties');
|
|
264
|
+
}
|
|
265
|
+
return swatch;
|
|
266
|
+
}
|
|
267
|
+
const COLOR_MAP_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
268
|
+
const isColorMapKey = (name) => COLOR_MAP_KEYS.some((key) => key === name);
|
|
269
|
+
function unwrapTypeExpressions(expression) {
|
|
270
|
+
let current = expression;
|
|
271
|
+
while (current?.isKind(SyntaxKind.AsExpression) || current?.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
272
|
+
current = current.getExpression();
|
|
273
|
+
}
|
|
274
|
+
return current;
|
|
275
|
+
}
|
|
276
|
+
function resolveConstInitializer(name, sourceFile, context) {
|
|
277
|
+
const local = sourceFile.getVariableDeclarations().find((decl) => decl.getName() === name);
|
|
278
|
+
if (local) {
|
|
279
|
+
const initializer = unwrapTypeExpressions(local.getInitializer());
|
|
280
|
+
return initializer ? { initializer, sourceFile } : null;
|
|
281
|
+
}
|
|
282
|
+
const importDecl = sourceFile
|
|
283
|
+
.getImportDeclarations()
|
|
284
|
+
.find((decl) => decl.getNamedImports().some((named) => (named.getAliasNode()?.getText() ?? named.getName()) === name));
|
|
285
|
+
if (!importDecl) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
289
|
+
const filePath = sourceFile.getFilePath();
|
|
290
|
+
const importedName = importDecl
|
|
291
|
+
.getNamedImports()
|
|
292
|
+
.find((named) => (named.getAliasNode()?.getText() ?? named.getName()) === name)
|
|
293
|
+
?.getName() ?? name;
|
|
294
|
+
const warnUnresolved = (reason) => logger.warn(`ā ļø Could not resolve "${name}" imported from "${specifier}" in ${filePath} (${reason}); it is ignored.`);
|
|
295
|
+
if (!specifier.startsWith('.')) {
|
|
296
|
+
warnUnresolved('package imports are not followed');
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
const base = posix.join(posix.dirname(filePath), specifier).replace(/^\//, '');
|
|
300
|
+
const candidates = [`${base}.ts`, posix.join(base, 'index.ts'), base];
|
|
301
|
+
const resolvedPath = candidates.find((candidate) => context.tree.isFile(candidate));
|
|
302
|
+
if (!resolvedPath) {
|
|
303
|
+
warnUnresolved('file not found in the workspace');
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const importedFile = context.project.getSourceFile(resolvedPath) ??
|
|
307
|
+
context.project.createSourceFile(resolvedPath, context.tree.read(resolvedPath, 'utf-8') ?? '');
|
|
308
|
+
const declaration = importedFile.getVariableDeclarations().find((decl) => decl.getName() === importedName);
|
|
309
|
+
const initializer = unwrapTypeExpressions(declaration?.getInitializer());
|
|
310
|
+
if (!initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
311
|
+
warnUnresolved(`not an object literal in ${resolvedPath}`);
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
return { initializer, sourceFile: importedFile };
|
|
315
|
+
}
|
|
316
|
+
function parseColorMap(initializer, sourceFile, context) {
|
|
317
|
+
if (initializer.isKind(SyntaxKind.Identifier)) {
|
|
318
|
+
const resolved = resolveConstInitializer(initializer.getText(), sourceFile, context);
|
|
319
|
+
return resolved ? parseColorMap(resolved.initializer, resolved.sourceFile, context) : null;
|
|
320
|
+
}
|
|
321
|
+
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
322
|
+
const colorMap = {};
|
|
323
|
+
const properties = initializer.getProperties();
|
|
324
|
+
for (const prop of properties) {
|
|
325
|
+
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
326
|
+
const propName = prop.getName();
|
|
327
|
+
const propValue = prop.getInitializer();
|
|
328
|
+
if (propValue?.isKind(SyntaxKind.StringLiteral) && isColorMapKey(propName)) {
|
|
329
|
+
colorMap[propName] = propValue.getLiteralValue();
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
else if (prop.isKind(SyntaxKind.SpreadAssignment)) {
|
|
333
|
+
const spreadExpr = prop.getExpression();
|
|
334
|
+
if (spreadExpr.isKind(SyntaxKind.Identifier)) {
|
|
335
|
+
const resolved = resolveConstInitializer(spreadExpr.getText(), sourceFile, context);
|
|
336
|
+
if (resolved) {
|
|
337
|
+
Object.assign(colorMap, parseColorMap(resolved.initializer, resolved.sourceFile, context));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
logger.warn(`ā ļø Unsupported spread "...${spreadExpr.getText()}" in ${sourceFile.getFilePath()}; it is ignored.`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
const defaultColor = colorMap.default;
|
|
346
|
+
if (defaultColor) {
|
|
347
|
+
const isThemeColorMap = colorMap.hover !== undefined || colorMap.active !== undefined || colorMap.disabled !== undefined;
|
|
348
|
+
if (isThemeColorMap) {
|
|
349
|
+
const hoverColor = colorMap.hover || defaultColor;
|
|
350
|
+
const result = {
|
|
351
|
+
default: defaultColor,
|
|
352
|
+
hover: hoverColor,
|
|
353
|
+
focus: colorMap.focus,
|
|
354
|
+
active: colorMap.active || hoverColor,
|
|
355
|
+
disabled: colorMap.disabled || defaultColor,
|
|
356
|
+
};
|
|
357
|
+
return result;
|
|
358
|
+
}
|
|
359
|
+
return colorMap;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
function createCssThemeName(name) {
|
|
366
|
+
return name
|
|
367
|
+
.toLowerCase()
|
|
368
|
+
.replace(/\s+/g, '-')
|
|
369
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
370
|
+
}
|
|
371
|
+
function generateColorThemeNameTypes(themes, schema) {
|
|
372
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
373
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
374
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
375
|
+
const names = themes.map((theme) => `'${theme.name}'`).join(' | ');
|
|
376
|
+
return `/*
|
|
377
|
+
* Auto-generated by @ethlete/core:tailwind-4-color-theme
|
|
378
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
379
|
+
*
|
|
380
|
+
* Regenerate by running:
|
|
381
|
+
* 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}` : ''}
|
|
382
|
+
*/
|
|
383
|
+
|
|
384
|
+
declare module '@ethlete/core' {
|
|
385
|
+
interface EthleteColorThemeNameRegistry {
|
|
386
|
+
name: ${names};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export {};
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
function generateTailwindThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
394
|
+
const tailwindVars = [];
|
|
395
|
+
const themeVars = [];
|
|
396
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
397
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
398
|
+
const header = `/*
|
|
399
|
+
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
400
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
401
|
+
*
|
|
402
|
+
* Generated from your theme definitions
|
|
403
|
+
* This file can be regenerated by running:
|
|
404
|
+
* 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}` : ''}
|
|
405
|
+
*/
|
|
406
|
+
|
|
407
|
+
`;
|
|
408
|
+
for (const theme of themes) {
|
|
409
|
+
const name = createCssThemeName(theme.name);
|
|
410
|
+
tailwindVars.push(` /* ${theme.name} theme */`);
|
|
411
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}`, theme.primary.color);
|
|
412
|
+
tailwindVars.push('');
|
|
413
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}`, theme.primary.onColor);
|
|
414
|
+
tailwindVars.push('');
|
|
415
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
|
|
416
|
+
tailwindVars.push('');
|
|
417
|
+
if (theme.secondary) {
|
|
418
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-secondary`, theme.secondary.color);
|
|
419
|
+
tailwindVars.push('');
|
|
420
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-secondary`, theme.secondary.onColor);
|
|
421
|
+
tailwindVars.push('');
|
|
422
|
+
}
|
|
423
|
+
if (theme.tertiary) {
|
|
424
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-tertiary`, theme.tertiary.color);
|
|
425
|
+
tailwindVars.push('');
|
|
426
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-tertiary`, theme.tertiary.onColor);
|
|
427
|
+
tailwindVars.push('');
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
tailwindVars.push('');
|
|
431
|
+
const hasSecondary = themes.some((t) => t.secondary);
|
|
432
|
+
const hasTertiary = themes.some((t) => t.tertiary);
|
|
433
|
+
// Collect the dynamic theme colors once. They are emitted in `@theme` (so Tailwind generates
|
|
434
|
+
// the utilities) and ALSO re-declared on every color selector in the alias block below. A
|
|
435
|
+
// Tailwind `@theme` variable only lands on `:root`, so `rgb(var(--<runtime>-color-*))` resolves
|
|
436
|
+
// once against the root color scope and inherits that concrete color into descendants - which
|
|
437
|
+
// means `bg-<prefix>-theme-*` utilities would ignore nested `.<runtime>-color--*` scopes.
|
|
438
|
+
// Re-declaring them per color selector makes the utilities resolve against the nearest scope.
|
|
439
|
+
const dynamicColorVars = [];
|
|
440
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme', 'primary', false);
|
|
441
|
+
addDynamicInkColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-ink', 'primary-ink');
|
|
442
|
+
if (hasSecondary) {
|
|
443
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-secondary', 'secondary', false);
|
|
444
|
+
}
|
|
445
|
+
if (hasTertiary) {
|
|
446
|
+
addDynamicThemeColors(dynamicColorVars, utilityPrefix, runtimePrefix, 'theme-tertiary', 'tertiary', false);
|
|
447
|
+
}
|
|
448
|
+
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
449
|
+
tailwindVars.push(...dynamicColorVars);
|
|
450
|
+
themes.forEach((theme) => {
|
|
451
|
+
const name = createCssThemeName(theme.name);
|
|
452
|
+
const isDefault = theme.isDefault;
|
|
453
|
+
if (isDefault) {
|
|
454
|
+
const selectors = [':root', `.${runtimePrefix}-color--default`, `.${runtimePrefix}-color--${name}`];
|
|
455
|
+
themeVars.push(`${selectors.join(', ')} {`);
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
themeVars.push(`.${runtimePrefix}-color--${name} {`);
|
|
459
|
+
}
|
|
460
|
+
addThemeColorVariants(themeVars, runtimePrefix, '', theme);
|
|
461
|
+
themeVars.push('}\n');
|
|
462
|
+
});
|
|
463
|
+
// Re-indent the dynamic theme colors (from 2-space `@theme` indent to the 4-space alias-block
|
|
464
|
+
// indent) and drop trailing blank lines so they slot cleanly into the alias selector below.
|
|
465
|
+
const dynamicColorAliasLines = dynamicColorVars.map((line) => (line === '' ? '' : ` ${line}`));
|
|
466
|
+
while (dynamicColorAliasLines.length && dynamicColorAliasLines[dynamicColorAliasLines.length - 1] === '') {
|
|
467
|
+
dynamicColorAliasLines.pop();
|
|
468
|
+
}
|
|
469
|
+
const aliasBlock = `/* Convenience aliases (rgb + solid + opacity variants) */
|
|
470
|
+
@layer base {
|
|
471
|
+
:root, :where([class*="${runtimePrefix}-color--"]) {
|
|
472
|
+
--${runtimePrefix}-theme-color-primary-rgb: var(--${runtimePrefix}-color-primary);
|
|
473
|
+
--${runtimePrefix}-theme-color-primary-opacity: 1;
|
|
474
|
+
--${runtimePrefix}-theme-color-primary-solid: rgb(var(--${runtimePrefix}-theme-color-primary-rgb));
|
|
475
|
+
--${runtimePrefix}-theme-color-primary: rgb(var(--${runtimePrefix}-theme-color-primary-rgb) / var(--${runtimePrefix}-theme-color-primary-opacity));
|
|
476
|
+
|
|
477
|
+
--${runtimePrefix}-theme-color-on-primary-rgb: var(--${runtimePrefix}-color-on-primary);
|
|
478
|
+
--${runtimePrefix}-theme-color-on-primary-opacity: 1;
|
|
479
|
+
--${runtimePrefix}-theme-color-on-primary-solid: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
480
|
+
--${runtimePrefix}-theme-color-on-primary: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
481
|
+
|
|
482
|
+
--${runtimePrefix}-theme-color-ink-rgb: var(--${runtimePrefix}-color-primary-ink, var(--${runtimePrefix}-color-primary));
|
|
483
|
+
--${runtimePrefix}-theme-color-ink-opacity: 1;
|
|
484
|
+
--${runtimePrefix}-theme-color-ink-solid: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
485
|
+
--${runtimePrefix}-theme-color-ink: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
486
|
+
|
|
487
|
+
/* Dynamic Tailwind theme colors - re-declared per color scope so that
|
|
488
|
+
bg-${utilityPrefix}-theme-* utilities resolve against the nearest .${runtimePrefix}-color--*
|
|
489
|
+
scope instead of the value computed once at :root. */
|
|
490
|
+
${dynamicColorAliasLines.join('\n')}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
`;
|
|
494
|
+
return `${header}@theme {
|
|
495
|
+
${tailwindVars.join('\n')}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@layer base {
|
|
499
|
+
${themeVars.join('\n')}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
${aliasBlock}`;
|
|
503
|
+
}
|
|
504
|
+
function addDynamicThemeColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName, addSpacingBefore) {
|
|
505
|
+
if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
|
|
506
|
+
vars.push('');
|
|
507
|
+
}
|
|
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));`);
|
|
513
|
+
vars.push('');
|
|
514
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}: rgb(var(--${runtimePrefix}-color-on-${cssVarName}));`);
|
|
515
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-hover));`);
|
|
516
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-focus));`);
|
|
517
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-active));`);
|
|
518
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-disabled));`);
|
|
519
|
+
vars.push('');
|
|
520
|
+
}
|
|
521
|
+
function addDynamicInkColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName) {
|
|
522
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
523
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
524
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
525
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
526
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
527
|
+
vars.push('');
|
|
528
|
+
}
|
|
529
|
+
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
530
|
+
vars.push(` --color-${colorName}: rgb(${colorSet.default});`);
|
|
531
|
+
const hoverValue = 'hover' in colorSet && colorSet.hover ? colorSet.hover : colorSet.default;
|
|
532
|
+
vars.push(` --color-${colorName}-hover: rgb(${hoverValue});`);
|
|
533
|
+
const focusValue = 'focus' in colorSet && colorSet.focus ? colorSet.focus : hoverValue;
|
|
534
|
+
vars.push(` --color-${colorName}-focus: rgb(${focusValue});`);
|
|
535
|
+
const activeValue = 'active' in colorSet && colorSet.active ? colorSet.active : hoverValue;
|
|
536
|
+
vars.push(` --color-${colorName}-active: rgb(${activeValue});`);
|
|
537
|
+
const disabledValue = 'disabled' in colorSet && colorSet.disabled ? colorSet.disabled : colorSet.default;
|
|
538
|
+
vars.push(` --color-${colorName}-disabled: rgb(${disabledValue});`);
|
|
539
|
+
}
|
|
540
|
+
function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
541
|
+
const addSwatch = (level, swatch) => {
|
|
542
|
+
const defaultColor = swatch.color.default;
|
|
543
|
+
const hoverColor = swatch.color.hover || defaultColor;
|
|
544
|
+
const focusColor = swatch.color.focus || hoverColor;
|
|
545
|
+
const activeColor = swatch.color.active || hoverColor;
|
|
546
|
+
const disabledColor = swatch.color.disabled || defaultColor;
|
|
547
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}: ${defaultColor};`);
|
|
548
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-hover: ${hoverColor};`);
|
|
549
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-focus: ${focusColor};`);
|
|
550
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-active: ${activeColor};`);
|
|
551
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-disabled: ${disabledColor};`);
|
|
552
|
+
vars.push('');
|
|
553
|
+
const onDefaultColor = swatch.onColor.default;
|
|
554
|
+
const onHoverColor = swatch.onColor.hover || onDefaultColor;
|
|
555
|
+
const onFocusColor = swatch.onColor.focus || onHoverColor;
|
|
556
|
+
const onActiveColor = swatch.onColor.active || onDefaultColor;
|
|
557
|
+
const onDisabledColor = swatch.onColor.disabled || onDefaultColor;
|
|
558
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}: ${onDefaultColor};`);
|
|
559
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-hover: ${onHoverColor};`);
|
|
560
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
561
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
562
|
+
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
563
|
+
const inkDefaultColor = swatch.inkColor?.default || defaultColor;
|
|
564
|
+
const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
|
|
565
|
+
const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
|
|
566
|
+
const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
|
|
567
|
+
const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
|
|
568
|
+
vars.push('');
|
|
569
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
|
|
570
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
|
|
571
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
|
|
572
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
|
|
573
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
|
|
574
|
+
if (theme.secondary || theme.tertiary) {
|
|
575
|
+
vars.push('');
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
addSwatch('primary', theme.primary);
|
|
579
|
+
if (theme.secondary) {
|
|
580
|
+
addSwatch('secondary', theme.secondary);
|
|
581
|
+
}
|
|
582
|
+
if (theme.tertiary) {
|
|
583
|
+
addSwatch('tertiary', theme.tertiary);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
function findMainStylesFile(tree) {
|
|
587
|
+
const potentialFiles = [];
|
|
588
|
+
visitNotIgnoredFiles(tree, '', (path) => {
|
|
589
|
+
if (path.match(/styles\.(css|scss)$/) && !path.includes('node_modules') && !path.includes('dist')) {
|
|
590
|
+
potentialFiles.push(path);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
return potentialFiles;
|
|
594
|
+
}
|
|
@@ -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
|
+
}
|