@ethlete/core 5.0.0-next.7 ā 5.0.0-next.70
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 +843 -0
- package/README.md +28 -4
- package/design/interactive-styles.ts +5 -0
- package/design/theme-css.js +392 -0
- package/design/theme-css.ts +547 -0
- package/fesm2022/ethlete-core.mjs +9338 -5667
- 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 +41 -5
- package/generators/migrate-color-theme-variants/color-theme-variants.js +136 -0
- package/generators/migrate-color-theme-variants/migration.js +46 -0
- package/generators/migrate-color-theme-variants/schema.json +26 -0
- package/generators/migrate-create-destroy/create-destroy.js +317 -0
- package/generators/migrate-create-destroy/migration.js +46 -0
- package/generators/migrate-create-destroy/schema.json +26 -0
- package/generators/migrate-legacy-color-themes/legacy-color-themes.js +123 -0
- package/generators/migrate-legacy-color-themes/migration.js +93 -0
- package/generators/migrate-legacy-color-themes/schema.json +26 -0
- 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 +161 -0
- package/generators/migrate-to-v5/router-state-service.js +125 -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/sync-observable-consumers.js +80 -0
- package/generators/migrate-to-v5/viewport-service.js +226 -491
- package/generators/tailwind-4-color-theme/generator.js +411 -0
- package/generators/tailwind-4-color-theme/schema.json +44 -0
- package/generators/tailwind-4-surface-theme/generator.js +379 -0
- package/generators/tailwind-4-surface-theme/schema.json +48 -0
- package/migrations/legacy-color-themes.md +38 -0
- package/migrations/seo-directive-removed.md +81 -0
- package/migrations/surface-theme-regenerate.md +20 -0
- package/migrations.json +59 -0
- package/package.json +38 -21
- package/src/lib/animations/easing-tokens-styles.component.css +155 -0
- package/src/lib/providers/visually-hidden-styles.component.css +13 -0
- package/src/lib/resize-handles/resize-handles.component.css +137 -0
- package/src/lib/theming/color-interactive-styles.component.css +147 -0
- package/src/lib/theming/surface-interactive-styles.component.css +42 -0
- package/types/ethlete-core.d.ts +3842 -2205
- package/generators/tailwind-4-theme/generator.js +0 -514
- package/generators/tailwind-4-theme/schema.json +0 -32
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { formatFiles, logger, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
import { posix } from 'path';
|
|
3
|
+
import { Project, SyntaxKind } from 'ts-morph';
|
|
4
|
+
import { colorThemeCss, cssThemeName } from '../../design/theme-css.js';
|
|
5
|
+
export default async function generate(tree, schema) {
|
|
6
|
+
logger.log('\nš Starting Tailwind 4 theme generator...\n');
|
|
7
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
8
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
9
|
+
const prefix = schema.prefix || 'et';
|
|
10
|
+
const runtimePrefix = schema.runtimePrefix || prefix;
|
|
11
|
+
if (!tree.exists(themesPath)) {
|
|
12
|
+
logger.error(`ā Themes file not found at: ${themesPath}`);
|
|
13
|
+
logger.log(`\nPlease specify the correct path using --themesPath option.`);
|
|
14
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
logger.log(`š Reading themes from: ${themesPath}`);
|
|
18
|
+
const themesContent = tree.read(themesPath, 'utf-8');
|
|
19
|
+
if (!themesContent) {
|
|
20
|
+
logger.error('ā Failed to read themes file');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
let themes;
|
|
24
|
+
try {
|
|
25
|
+
themes = extractThemesFromContent(tree, 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
|
+
// Apply the generation-time default override, if any. The shared theme
|
|
36
|
+
// definitions may mark a default via isDefault, but in a monorepo each app picks its
|
|
37
|
+
// own default at its generation invocation - the option wins over the definitions.
|
|
38
|
+
if (schema.defaultTheme) {
|
|
39
|
+
try {
|
|
40
|
+
const defaultName = applyDefaultThemeOverride(themes, schema.defaultTheme);
|
|
41
|
+
logger.log(`šÆ Default theme set at generation time: ${defaultName}`);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
logger.error('ā Invalid --defaultTheme option');
|
|
45
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
validateThemeConfiguration(themes);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
logger.error('ā Theme configuration error');
|
|
54
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
logger.log('\nšØ Generating Tailwind theme CSS...');
|
|
58
|
+
const css = generateTailwindThemeCss(themes, prefix, runtimePrefix, schema);
|
|
59
|
+
const typesOutputPath = schema.typesOutputPath ?? outputPath.replace(/\.css$/, '.d.ts');
|
|
60
|
+
if (typesOutputPath === outputPath) {
|
|
61
|
+
throw new Error('A custom outputPath must end in .css or be paired with a distinct typesOutputPath.');
|
|
62
|
+
}
|
|
63
|
+
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
64
|
+
if (outputDir && !tree.exists(outputDir)) {
|
|
65
|
+
logger.log(`š Creating directory: ${outputDir}`);
|
|
66
|
+
}
|
|
67
|
+
tree.write(outputPath, css);
|
|
68
|
+
logger.log(`ā
Generated Tailwind themes at: ${outputPath}`);
|
|
69
|
+
// Generate the `EthleteColorThemeNameRegistry` augmentation, so `etProvideColor`
|
|
70
|
+
// (and anything else that accepts a `RegisteredColorThemeName`) is checked/autocompleted
|
|
71
|
+
// against this app's actual theme names, instead of a plain `string`.
|
|
72
|
+
const typesDts = generateColorThemeNameTypes(themes, schema);
|
|
73
|
+
tree.write(typesOutputPath, typesDts);
|
|
74
|
+
logger.log(`ā
Generated color theme name types at: ${typesOutputPath}`);
|
|
75
|
+
const mainStylesFiles = findMainStylesFile(tree);
|
|
76
|
+
if (mainStylesFiles.length > 0) {
|
|
77
|
+
logger.log('\nš Found potential main styles files:');
|
|
78
|
+
mainStylesFiles.forEach((file) => logger.log(` - ${file}`));
|
|
79
|
+
logger.log('\nā ļø Please manually import the generated themes:');
|
|
80
|
+
logger.log(` @import './${outputPath.replace('src/styles/', '')}';`);
|
|
81
|
+
}
|
|
82
|
+
if (!schema.skipFormat) {
|
|
83
|
+
await formatFiles(tree);
|
|
84
|
+
}
|
|
85
|
+
logger.log('\nā
Generation completed successfully!\n');
|
|
86
|
+
}
|
|
87
|
+
function extractThemesFromContent(tree, content, filePath) {
|
|
88
|
+
const project = new Project({
|
|
89
|
+
useInMemoryFileSystem: true,
|
|
90
|
+
compilerOptions: {
|
|
91
|
+
target: 99,
|
|
92
|
+
module: 99,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
96
|
+
const context = { tree, project };
|
|
97
|
+
const themes = [];
|
|
98
|
+
const exportedDeclarations = sourceFile.getVariableDeclarations().filter((decl) => {
|
|
99
|
+
const statement = decl.getVariableStatement();
|
|
100
|
+
return statement?.isExported();
|
|
101
|
+
});
|
|
102
|
+
const themesArray = exportedDeclarations.find((decl) => {
|
|
103
|
+
const name = decl.getName();
|
|
104
|
+
return name === 'THEMES' || name === 'themes';
|
|
105
|
+
});
|
|
106
|
+
if (!themesArray) {
|
|
107
|
+
throw new Error('Could not find THEMES or themes export');
|
|
108
|
+
}
|
|
109
|
+
let initializer = themesArray.getInitializer();
|
|
110
|
+
if (!initializer) {
|
|
111
|
+
throw new Error('THEMES export has no initializer');
|
|
112
|
+
}
|
|
113
|
+
// Handle 'satisfies X'/'as const' wrappers, in either order: [array] satisfies Type[],
|
|
114
|
+
// [array] as const, or both combined.
|
|
115
|
+
while (initializer.isKind(SyntaxKind.SatisfiesExpression) || initializer.isKind(SyntaxKind.AsExpression)) {
|
|
116
|
+
initializer = initializer.getExpression();
|
|
117
|
+
}
|
|
118
|
+
if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
119
|
+
throw new Error('THEMES export must be an array literal');
|
|
120
|
+
}
|
|
121
|
+
const elements = initializer.getElements();
|
|
122
|
+
for (const element of elements) {
|
|
123
|
+
let themeObj;
|
|
124
|
+
let name = '<inline>';
|
|
125
|
+
if (element.isKind(SyntaxKind.Identifier)) {
|
|
126
|
+
name = element.getText();
|
|
127
|
+
const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
|
|
128
|
+
if (!themeDecl) {
|
|
129
|
+
logger.warn(`ā ļø Could not find declaration for theme: ${name}`);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
themeObj = themeDecl.getInitializer();
|
|
133
|
+
if (!themeObj) {
|
|
134
|
+
logger.warn(`ā ļø Theme ${name} has no initializer`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
themeObj = element;
|
|
140
|
+
}
|
|
141
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
142
|
+
themeObj = themeObj.getExpression();
|
|
143
|
+
}
|
|
144
|
+
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
145
|
+
logger.warn(`ā ļø Theme ${name} is not an object literal`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const theme = parseThemeObject(themeObj, sourceFile, context);
|
|
150
|
+
themes.push(theme);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
logger.warn(`ā ļø Failed to parse theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (themes.length === 0) {
|
|
157
|
+
throw new Error('No valid themes found in THEMES array');
|
|
158
|
+
}
|
|
159
|
+
return themes;
|
|
160
|
+
}
|
|
161
|
+
function parseThemeObject(obj, sourceFile, context) {
|
|
162
|
+
const properties = obj.getProperties();
|
|
163
|
+
const theme = {};
|
|
164
|
+
for (const prop of properties) {
|
|
165
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const propName = prop.getName();
|
|
169
|
+
const initializer = prop.getInitializer();
|
|
170
|
+
if (!initializer) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
switch (propName) {
|
|
174
|
+
case 'name':
|
|
175
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
176
|
+
theme.name = initializer.getLiteralValue();
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'isDefault':
|
|
180
|
+
if (initializer.isKind(SyntaxKind.TrueKeyword)) {
|
|
181
|
+
theme.isDefault = true;
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
case 'type':
|
|
185
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
186
|
+
theme.type = initializer.getLiteralValue();
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
case 'primary':
|
|
190
|
+
case 'secondary':
|
|
191
|
+
case 'tertiary':
|
|
192
|
+
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
193
|
+
theme[propName] = parseThemeSwatch(initializer, sourceFile, context);
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!theme.name || !theme.primary) {
|
|
199
|
+
throw new Error('Theme must have name and primary properties');
|
|
200
|
+
}
|
|
201
|
+
return theme;
|
|
202
|
+
}
|
|
203
|
+
function applyDefaultThemeOverride(themes, defaultTheme) {
|
|
204
|
+
const wanted = cssThemeName(defaultTheme);
|
|
205
|
+
const target = themes.find((t) => t.name === defaultTheme || cssThemeName(t.name) === wanted);
|
|
206
|
+
if (!target) {
|
|
207
|
+
throw new Error(`No theme named "${defaultTheme}" found. Available themes: ${themes.map((t) => t.name).join(', ')}`);
|
|
208
|
+
}
|
|
209
|
+
for (const theme of themes) {
|
|
210
|
+
theme.isDefault = theme === target;
|
|
211
|
+
}
|
|
212
|
+
return target.name;
|
|
213
|
+
}
|
|
214
|
+
function validateThemeConfiguration(themes) {
|
|
215
|
+
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
216
|
+
if (defaultThemes.length === 0) {
|
|
217
|
+
throw new Error('No default theme found. At least one theme must have isDefault: true');
|
|
218
|
+
}
|
|
219
|
+
if (defaultThemes.length > 1) {
|
|
220
|
+
throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
|
|
221
|
+
}
|
|
222
|
+
const typeMap = new Map();
|
|
223
|
+
for (const theme of themes) {
|
|
224
|
+
if (!theme.type) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const existing = typeMap.get(theme.type) || [];
|
|
228
|
+
existing.push(theme.name);
|
|
229
|
+
typeMap.set(theme.type, existing);
|
|
230
|
+
}
|
|
231
|
+
for (const [type, names] of typeMap) {
|
|
232
|
+
if (names.length > 1) {
|
|
233
|
+
throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function parseThemeSwatch(obj, sourceFile, context) {
|
|
238
|
+
const properties = obj.getProperties();
|
|
239
|
+
const swatch = {};
|
|
240
|
+
for (const prop of properties) {
|
|
241
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const propName = prop.getName();
|
|
245
|
+
const initializer = prop.getInitializer();
|
|
246
|
+
if (!initializer) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const colorMap = parseColorMap(initializer, sourceFile, context);
|
|
250
|
+
if (!colorMap) {
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (propName === 'color') {
|
|
254
|
+
swatch.color = colorMap;
|
|
255
|
+
}
|
|
256
|
+
else if (propName === 'onColor') {
|
|
257
|
+
swatch.onColor = colorMap;
|
|
258
|
+
}
|
|
259
|
+
else if (propName === 'inkColor') {
|
|
260
|
+
swatch.inkColor = colorMap;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!swatch.color || !swatch.onColor) {
|
|
264
|
+
throw new Error('ThemeSwatch must have color and onColor properties');
|
|
265
|
+
}
|
|
266
|
+
return swatch;
|
|
267
|
+
}
|
|
268
|
+
const COLOR_MAP_KEYS = ['default', 'hover', 'focus', 'active', 'disabled'];
|
|
269
|
+
const isColorMapKey = (name) => COLOR_MAP_KEYS.some((key) => key === name);
|
|
270
|
+
function unwrapTypeExpressions(expression) {
|
|
271
|
+
let current = expression;
|
|
272
|
+
while (current?.isKind(SyntaxKind.AsExpression) || current?.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
273
|
+
current = current.getExpression();
|
|
274
|
+
}
|
|
275
|
+
return current;
|
|
276
|
+
}
|
|
277
|
+
function resolveConstInitializer(name, sourceFile, context) {
|
|
278
|
+
const local = sourceFile.getVariableDeclarations().find((decl) => decl.getName() === name);
|
|
279
|
+
if (local) {
|
|
280
|
+
const initializer = unwrapTypeExpressions(local.getInitializer());
|
|
281
|
+
return initializer ? { initializer, sourceFile } : null;
|
|
282
|
+
}
|
|
283
|
+
const importDecl = sourceFile
|
|
284
|
+
.getImportDeclarations()
|
|
285
|
+
.find((decl) => decl.getNamedImports().some((named) => (named.getAliasNode()?.getText() ?? named.getName()) === name));
|
|
286
|
+
if (!importDecl) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const specifier = importDecl.getModuleSpecifierValue();
|
|
290
|
+
const filePath = sourceFile.getFilePath();
|
|
291
|
+
const importedName = importDecl
|
|
292
|
+
.getNamedImports()
|
|
293
|
+
.find((named) => (named.getAliasNode()?.getText() ?? named.getName()) === name)
|
|
294
|
+
?.getName() ?? name;
|
|
295
|
+
const warnUnresolved = (reason) => logger.warn(`ā ļø Could not resolve "${name}" imported from "${specifier}" in ${filePath} (${reason}); it is ignored.`);
|
|
296
|
+
if (!specifier.startsWith('.')) {
|
|
297
|
+
warnUnresolved('package imports are not followed');
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const base = posix.join(posix.dirname(filePath), specifier).replace(/^\//, '');
|
|
301
|
+
const candidates = [`${base}.ts`, posix.join(base, 'index.ts'), base];
|
|
302
|
+
const resolvedPath = candidates.find((candidate) => context.tree.isFile(candidate));
|
|
303
|
+
if (!resolvedPath) {
|
|
304
|
+
warnUnresolved('file not found in the workspace');
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
const importedFile = context.project.getSourceFile(resolvedPath) ??
|
|
308
|
+
context.project.createSourceFile(resolvedPath, context.tree.read(resolvedPath, 'utf-8') ?? '');
|
|
309
|
+
const declaration = importedFile.getVariableDeclarations().find((decl) => decl.getName() === importedName);
|
|
310
|
+
const initializer = unwrapTypeExpressions(declaration?.getInitializer());
|
|
311
|
+
if (!initializer?.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
312
|
+
warnUnresolved(`not an object literal in ${resolvedPath}`);
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
return { initializer, sourceFile: importedFile };
|
|
316
|
+
}
|
|
317
|
+
function parseColorMap(initializer, sourceFile, context) {
|
|
318
|
+
if (initializer.isKind(SyntaxKind.Identifier)) {
|
|
319
|
+
const resolved = resolveConstInitializer(initializer.getText(), sourceFile, context);
|
|
320
|
+
return resolved ? parseColorMap(resolved.initializer, resolved.sourceFile, context) : null;
|
|
321
|
+
}
|
|
322
|
+
if (initializer.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
323
|
+
const colorMap = {};
|
|
324
|
+
const properties = initializer.getProperties();
|
|
325
|
+
for (const prop of properties) {
|
|
326
|
+
if (prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
327
|
+
const propName = prop.getName();
|
|
328
|
+
const propValue = prop.getInitializer();
|
|
329
|
+
if (propValue?.isKind(SyntaxKind.StringLiteral) && isColorMapKey(propName)) {
|
|
330
|
+
colorMap[propName] = propValue.getLiteralValue();
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
else if (prop.isKind(SyntaxKind.SpreadAssignment)) {
|
|
334
|
+
const spreadExpr = prop.getExpression();
|
|
335
|
+
if (spreadExpr.isKind(SyntaxKind.Identifier)) {
|
|
336
|
+
const resolved = resolveConstInitializer(spreadExpr.getText(), sourceFile, context);
|
|
337
|
+
if (resolved) {
|
|
338
|
+
Object.assign(colorMap, parseColorMap(resolved.initializer, resolved.sourceFile, context));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
logger.warn(`ā ļø Unsupported spread "...${spreadExpr.getText()}" in ${sourceFile.getFilePath()}; it is ignored.`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const defaultColor = colorMap.default;
|
|
347
|
+
if (defaultColor) {
|
|
348
|
+
const isThemeColorMap = colorMap.hover !== undefined || colorMap.active !== undefined || colorMap.disabled !== undefined;
|
|
349
|
+
if (isThemeColorMap) {
|
|
350
|
+
const hoverColor = colorMap.hover || defaultColor;
|
|
351
|
+
const result = {
|
|
352
|
+
default: defaultColor,
|
|
353
|
+
hover: hoverColor,
|
|
354
|
+
focus: colorMap.focus,
|
|
355
|
+
active: colorMap.active || hoverColor,
|
|
356
|
+
disabled: colorMap.disabled || defaultColor,
|
|
357
|
+
};
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
return colorMap;
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
function generateColorThemeNameTypes(themes, schema) {
|
|
367
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
368
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
369
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
370
|
+
const names = themes.map((theme) => `'${theme.name}'`).join(' | ');
|
|
371
|
+
return `/*
|
|
372
|
+
* Auto-generated by @ethlete/core:tailwind-4-color-theme
|
|
373
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
374
|
+
*
|
|
375
|
+
* Regenerate by running:
|
|
376
|
+
* 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}` : ''}
|
|
377
|
+
*/
|
|
378
|
+
|
|
379
|
+
declare module '@ethlete/core' {
|
|
380
|
+
interface EthleteColorThemeNameRegistry {
|
|
381
|
+
name: ${names};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export {};
|
|
386
|
+
`;
|
|
387
|
+
}
|
|
388
|
+
function generateTailwindThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
389
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
390
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
391
|
+
const header = `/*
|
|
392
|
+
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
393
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
394
|
+
*
|
|
395
|
+
* Generated from your theme definitions
|
|
396
|
+
* This file can be regenerated by running:
|
|
397
|
+
* 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}` : ''}
|
|
398
|
+
*/
|
|
399
|
+
|
|
400
|
+
`;
|
|
401
|
+
return `${header}${colorThemeCss(themes, { prefix: utilityPrefix, runtimePrefix })}`;
|
|
402
|
+
}
|
|
403
|
+
function findMainStylesFile(tree) {
|
|
404
|
+
const potentialFiles = [];
|
|
405
|
+
visitNotIgnoredFiles(tree, '', (path) => {
|
|
406
|
+
if (path.match(/styles\.(css|scss)$/) && !path.includes('node_modules') && !path.includes('dist')) {
|
|
407
|
+
potentialFiles.push(path);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
return potentialFiles;
|
|
411
|
+
}
|
|
@@ -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
|
+
}
|