@ethlete/core 5.0.0-next.6 → 5.0.0-next.8
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 +12 -0
- package/fesm2022/ethlete-core.mjs +461 -49
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +9 -4
- package/generators/migrate-to-v5/color-naming.js +173 -0
- package/generators/migrate-to-v5/migration.js +6 -0
- package/generators/migrate-to-v5/schema.json +5 -0
- package/generators/{tailwind-4-theme → tailwind-4-color-theme}/generator.js +37 -13
- package/generators/{tailwind-4-theme → tailwind-4-color-theme}/schema.json +1 -1
- package/generators/tailwind-4-surface-theme/generator.js +267 -0
- package/generators/tailwind-4-surface-theme/schema.json +32 -0
- package/package.json +1 -1
- package/types/ethlete-core.d.ts +201 -29
|
@@ -5,10 +5,15 @@
|
|
|
5
5
|
"schema": "./migrate-to-v5/schema.json",
|
|
6
6
|
"description": "Migrate to v5"
|
|
7
7
|
},
|
|
8
|
-
"tailwind-4-theme": {
|
|
9
|
-
"factory": "./tailwind-4-theme/generator",
|
|
10
|
-
"schema": "./tailwind-4-theme/schema.json",
|
|
11
|
-
"description": "Generate theming for Tailwind CSS v4"
|
|
8
|
+
"tailwind-4-color-theme": {
|
|
9
|
+
"factory": "./tailwind-4-color-theme/generator",
|
|
10
|
+
"schema": "./tailwind-4-color-theme/schema.json",
|
|
11
|
+
"description": "Generate color theming for Tailwind CSS v4"
|
|
12
|
+
},
|
|
13
|
+
"tailwind-4-surface-theme": {
|
|
14
|
+
"factory": "./tailwind-4-surface-theme/generator",
|
|
15
|
+
"schema": "./tailwind-4-surface-theme/schema.json",
|
|
16
|
+
"description": "Generate surface theming for Tailwind CSS v4"
|
|
12
17
|
}
|
|
13
18
|
}
|
|
14
19
|
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { logger } from '@nx/devkit';
|
|
2
|
+
/**
|
|
3
|
+
* Symbol renames in TypeScript files (imports + usage).
|
|
4
|
+
*/
|
|
5
|
+
const TS_SYMBOL_RENAMES = {
|
|
6
|
+
ProvideThemeDirective: 'ProvideColorDirective',
|
|
7
|
+
THEME_PROVIDER: 'COLOR_PROVIDER',
|
|
8
|
+
ColorThemedDirective: 'ColoredDirective',
|
|
9
|
+
ColorThemedStylesComponent: 'ColoredStylesComponent',
|
|
10
|
+
SurfaceThemedDirective: 'SurfacedDirective',
|
|
11
|
+
SurfaceThemedStylesComponent: 'SurfacedStylesComponent',
|
|
12
|
+
injectThemesPrefix: 'injectColorThemesPrefix',
|
|
13
|
+
createCssThemeName: 'createCssColorThemeName',
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* String replacements applied to HTML templates and inline templates.
|
|
17
|
+
*/
|
|
18
|
+
const TEMPLATE_REPLACEMENTS = [
|
|
19
|
+
['[etProvideTheme]', '[etProvideColor]'],
|
|
20
|
+
['etProvideTheme', 'etProvideColor'],
|
|
21
|
+
['[etProvideAltTheme]', '[etProvideAltColor]'],
|
|
22
|
+
['etProvideAltTheme', 'etProvideAltColor'],
|
|
23
|
+
['[etColorThemed]', '[etColored]'],
|
|
24
|
+
['etColorThemed', 'etColored'],
|
|
25
|
+
['[etSurfaceThemed]', '[etSurfaced]'],
|
|
26
|
+
['etSurfaceThemed', 'etSurfaced'],
|
|
27
|
+
['[theme]=', '[color]='],
|
|
28
|
+
['[altTheme]=', '[altColor]='],
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* CSS class name replacements.
|
|
32
|
+
*/
|
|
33
|
+
const CSS_REPLACEMENTS = [
|
|
34
|
+
['.et-color-themed', '.et-colored'],
|
|
35
|
+
['.et-surface-themed', '.et-surfaced'],
|
|
36
|
+
['.et-theme--', '.et-color--'],
|
|
37
|
+
['.et-theme-alt--', '.et-color-alt--'],
|
|
38
|
+
];
|
|
39
|
+
/**
|
|
40
|
+
* Host directive input alias replacements in TypeScript files.
|
|
41
|
+
*/
|
|
42
|
+
const HOST_DIRECTIVE_REPLACEMENTS = [
|
|
43
|
+
["'etProvideTheme:theme'", "'etProvideColor:color'"],
|
|
44
|
+
["'etProvideAltTheme:altTheme'", "'etProvideAltColor:altColor'"],
|
|
45
|
+
["'etProvideTheme:color'", "'etProvideColor:color'"],
|
|
46
|
+
["'etProvideAltTheme:altColor'", "'etProvideAltColor:altColor'"],
|
|
47
|
+
['"etProvideTheme:theme"', '"etProvideColor:color"'],
|
|
48
|
+
['"etProvideAltTheme:altTheme"', '"etProvideAltColor:altColor"'],
|
|
49
|
+
['"etProvideTheme"', '"etProvideColor"'],
|
|
50
|
+
['"etProvideAltTheme"', '"etProvideAltColor"'],
|
|
51
|
+
];
|
|
52
|
+
export default async function migrateColorNaming(tree) {
|
|
53
|
+
logger.log('\n🔄 Migrating theme → color naming...\n');
|
|
54
|
+
const tsFiles = [];
|
|
55
|
+
const htmlFiles = [];
|
|
56
|
+
const cssFiles = [];
|
|
57
|
+
findFiles('.', tree, tsFiles, htmlFiles, cssFiles);
|
|
58
|
+
let filesModified = 0;
|
|
59
|
+
for (const filePath of tsFiles) {
|
|
60
|
+
const wasModified = migrateTypeScriptFile(tree, filePath);
|
|
61
|
+
if (wasModified) {
|
|
62
|
+
filesModified++;
|
|
63
|
+
logger.log(` ✓ ${filePath}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
for (const filePath of htmlFiles) {
|
|
67
|
+
const wasModified = migrateTemplateFile(tree, filePath);
|
|
68
|
+
if (wasModified) {
|
|
69
|
+
filesModified++;
|
|
70
|
+
logger.log(` ✓ ${filePath}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
for (const filePath of cssFiles) {
|
|
74
|
+
const wasModified = migrateCssFile(tree, filePath);
|
|
75
|
+
if (wasModified) {
|
|
76
|
+
filesModified++;
|
|
77
|
+
logger.log(` ✓ ${filePath}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (filesModified > 0) {
|
|
81
|
+
logger.log(`\n✅ Successfully migrated color naming in ${filesModified} file(s)\n`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
logger.log('\nℹ️ No files needed migration\n');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function findFiles(dir, tree, tsFiles, htmlFiles, cssFiles) {
|
|
88
|
+
const children = tree.children(dir);
|
|
89
|
+
for (const child of children) {
|
|
90
|
+
const path = dir === '.' ? child : `${dir}/${child}`;
|
|
91
|
+
if (tree.isFile(path)) {
|
|
92
|
+
if (path.includes('node_modules'))
|
|
93
|
+
continue;
|
|
94
|
+
if (path.endsWith('.ts') && !path.endsWith('.spec.ts')) {
|
|
95
|
+
tsFiles.push(path);
|
|
96
|
+
}
|
|
97
|
+
else if (path.endsWith('.html')) {
|
|
98
|
+
htmlFiles.push(path);
|
|
99
|
+
}
|
|
100
|
+
else if (path.endsWith('.css') || path.endsWith('.scss')) {
|
|
101
|
+
cssFiles.push(path);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (child === 'node_modules')
|
|
106
|
+
continue;
|
|
107
|
+
findFiles(path, tree, tsFiles, htmlFiles, cssFiles);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function migrateTypeScriptFile(tree, filePath) {
|
|
112
|
+
let content = tree.read(filePath, 'utf-8');
|
|
113
|
+
if (!content)
|
|
114
|
+
return false;
|
|
115
|
+
const original = content;
|
|
116
|
+
// Rename symbols
|
|
117
|
+
for (const [oldName, newName] of Object.entries(TS_SYMBOL_RENAMES)) {
|
|
118
|
+
content = replaceWholeWord(content, oldName, newName);
|
|
119
|
+
}
|
|
120
|
+
// Rename host directive input aliases
|
|
121
|
+
for (const [oldStr, newStr] of HOST_DIRECTIVE_REPLACEMENTS) {
|
|
122
|
+
content = content.replaceAll(oldStr, newStr);
|
|
123
|
+
}
|
|
124
|
+
// Handle inline templates in TypeScript files
|
|
125
|
+
for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
|
|
126
|
+
content = content.replaceAll(oldStr, newStr);
|
|
127
|
+
}
|
|
128
|
+
// Handle CSS in TypeScript files
|
|
129
|
+
for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
|
|
130
|
+
content = content.replaceAll(oldStr, newStr);
|
|
131
|
+
}
|
|
132
|
+
if (content !== original) {
|
|
133
|
+
tree.write(filePath, content);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
function migrateTemplateFile(tree, filePath) {
|
|
139
|
+
let content = tree.read(filePath, 'utf-8');
|
|
140
|
+
if (!content)
|
|
141
|
+
return false;
|
|
142
|
+
const original = content;
|
|
143
|
+
for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
|
|
144
|
+
content = content.replaceAll(oldStr, newStr);
|
|
145
|
+
}
|
|
146
|
+
if (content !== original) {
|
|
147
|
+
tree.write(filePath, content);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
function migrateCssFile(tree, filePath) {
|
|
153
|
+
let content = tree.read(filePath, 'utf-8');
|
|
154
|
+
if (!content)
|
|
155
|
+
return false;
|
|
156
|
+
const original = content;
|
|
157
|
+
for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
|
|
158
|
+
content = content.replaceAll(oldStr, newStr);
|
|
159
|
+
}
|
|
160
|
+
if (content !== original) {
|
|
161
|
+
tree.write(filePath, content);
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
function replaceWholeWord(content, oldWord, newWord) {
|
|
167
|
+
const regex = new RegExp(`\\b${escapeRegex(oldWord)}\\b`, 'g');
|
|
168
|
+
return content.replace(regex, newWord);
|
|
169
|
+
}
|
|
170
|
+
function escapeRegex(str) {
|
|
171
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=color-naming.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { formatFiles } from '@nx/devkit';
|
|
2
|
+
import migrateColorNaming from './color-naming.js';
|
|
2
3
|
import migrateCreateProvider from './create-provider.js';
|
|
3
4
|
import migrateRouterStateService from './router-state-service.js';
|
|
4
5
|
import migrateViewportService from './viewport-service.js';
|
|
@@ -18,6 +19,11 @@ export default async function migrate(tree, schema) {
|
|
|
18
19
|
if (shouldMigrateRouterStateService) {
|
|
19
20
|
await migrateRouterStateService(tree);
|
|
20
21
|
}
|
|
22
|
+
const shouldMigrateColorNaming = schema.migrateColorNaming !== false;
|
|
23
|
+
if (shouldMigrateColorNaming) {
|
|
24
|
+
console.log(' • Migrating theme → color naming...');
|
|
25
|
+
await migrateColorNaming(tree);
|
|
26
|
+
}
|
|
21
27
|
if (!schema.skipFormat) {
|
|
22
28
|
console.log(' • Formatting files...');
|
|
23
29
|
await formatFiles(tree);
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
"type": "boolean",
|
|
24
24
|
"description": "Migrate router state service to v5",
|
|
25
25
|
"default": true
|
|
26
|
+
},
|
|
27
|
+
"migrateColorNaming": {
|
|
28
|
+
"type": "boolean",
|
|
29
|
+
"description": "Migrate theme naming to color naming (ProvideThemeDirective -> ProvideColorDirective, etc.)",
|
|
30
|
+
"default": true
|
|
26
31
|
}
|
|
27
32
|
},
|
|
28
33
|
"required": []
|
|
@@ -9,7 +9,7 @@ export default async function generate(tree, schema) {
|
|
|
9
9
|
if (!tree.exists(themesPath)) {
|
|
10
10
|
logger.error(`❌ Themes file not found at: ${themesPath}`);
|
|
11
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`);
|
|
12
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
logger.log(`📁 Reading themes from: ${themesPath}`);
|
|
@@ -219,7 +219,7 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
219
219
|
if (!initializer) {
|
|
220
220
|
continue;
|
|
221
221
|
}
|
|
222
|
-
if (propName === 'color' || propName === 'onColor') {
|
|
222
|
+
if (propName === 'color' || propName === 'onColor' || propName === 'inkColor') {
|
|
223
223
|
const colorMap = parseColorMap(initializer, sourceFile);
|
|
224
224
|
if (colorMap) {
|
|
225
225
|
swatch[propName] = colorMap;
|
|
@@ -306,7 +306,7 @@ function generateTailwindThemeCss(themes, prefix, schema) {
|
|
|
306
306
|
*
|
|
307
307
|
* Generated from your theme definitions
|
|
308
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}` : ''}
|
|
309
|
+
* nx g @ethlete/core:tailwind-4-color-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}
|
|
310
310
|
*/
|
|
311
311
|
|
|
312
312
|
`;
|
|
@@ -325,6 +325,9 @@ function generateTailwindThemeCss(themes, prefix, schema) {
|
|
|
325
325
|
// On colors for Tailwind utilities
|
|
326
326
|
addTailwindColorVariants(tailwindVars, `${prefix}-on-${name}`, theme.primary.onColor);
|
|
327
327
|
tailwindVars.push('');
|
|
328
|
+
// Ink colors for standalone foreground usage
|
|
329
|
+
addTailwindColorVariants(tailwindVars, `${prefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
|
|
330
|
+
tailwindVars.push('');
|
|
328
331
|
// Secondary colors if present
|
|
329
332
|
if (theme.secondary) {
|
|
330
333
|
addTailwindColorVariants(tailwindVars, `${prefix}-${name}-secondary`, theme.secondary.color);
|
|
@@ -350,6 +353,7 @@ function generateTailwindThemeCss(themes, prefix, schema) {
|
|
|
350
353
|
// Main theme dynamic colors
|
|
351
354
|
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
352
355
|
addDynamicThemeColors(tailwindVars, prefix, 'theme', 'primary', true);
|
|
356
|
+
addDynamicInkColors(tailwindVars, prefix, 'theme-ink', 'primary-ink');
|
|
353
357
|
if (hasSecondary) {
|
|
354
358
|
addDynamicThemeColors(tailwindVars, prefix, 'theme-secondary', 'secondary', false);
|
|
355
359
|
}
|
|
@@ -359,6 +363,7 @@ function generateTailwindThemeCss(themes, prefix, schema) {
|
|
|
359
363
|
// Alt theme dynamic colors
|
|
360
364
|
tailwindVars.push(' /* Alt theme dynamic colors */');
|
|
361
365
|
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme', 'alt-primary', true);
|
|
366
|
+
addDynamicInkColors(tailwindVars, prefix, 'alt-theme-ink', 'alt-primary-ink');
|
|
362
367
|
if (hasAltSecondary) {
|
|
363
368
|
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme-secondary', 'alt-secondary', false);
|
|
364
369
|
}
|
|
@@ -371,29 +376,29 @@ function generateTailwindThemeCss(themes, prefix, schema) {
|
|
|
371
376
|
// Determine if this is the default or default-alt theme
|
|
372
377
|
const isDefault = theme.isDefault;
|
|
373
378
|
const isDefaultAlt = theme.isDefaultAlt;
|
|
374
|
-
// Generate main
|
|
379
|
+
// Generate main color variant (.et-color--{name})
|
|
375
380
|
if (isDefault) {
|
|
376
|
-
// Default
|
|
377
|
-
const selectors = [':root', `.${prefix}-
|
|
381
|
+
// Default color gets :root and .et-color--default selectors
|
|
382
|
+
const selectors = [':root', `.${prefix}-color--default`, `.${prefix}-color--${name}`];
|
|
378
383
|
themeVars.push(`${selectors.join(', ')} {`);
|
|
379
384
|
}
|
|
380
385
|
else if (!isDefaultAlt) {
|
|
381
|
-
// Regular
|
|
382
|
-
themeVars.push(`.${prefix}-
|
|
386
|
+
// Regular colors just get their own class
|
|
387
|
+
themeVars.push(`.${prefix}-color--${name} {`);
|
|
383
388
|
}
|
|
384
389
|
if (isDefault || !isDefaultAlt) {
|
|
385
390
|
addThemeColorVariants(themeVars, prefix, '', theme);
|
|
386
391
|
themeVars.push('}\n');
|
|
387
392
|
}
|
|
388
|
-
// Generate alt
|
|
393
|
+
// Generate alt color variant (.et-color-alt--{name})
|
|
389
394
|
if (isDefaultAlt) {
|
|
390
|
-
// Default alt
|
|
391
|
-
const selectors = [':root', `.${prefix}-
|
|
395
|
+
// Default alt color gets :root and .et-color-alt--default selectors
|
|
396
|
+
const selectors = [':root', `.${prefix}-color-alt--default`, `.${prefix}-color-alt--${name}`];
|
|
392
397
|
themeVars.push(`${selectors.join(', ')} {`);
|
|
393
398
|
}
|
|
394
399
|
else {
|
|
395
|
-
// All
|
|
396
|
-
themeVars.push(`.${prefix}-
|
|
400
|
+
// All colors (including default and regular) get an alt variant
|
|
401
|
+
themeVars.push(`.${prefix}-color-alt--${name} {`);
|
|
397
402
|
}
|
|
398
403
|
addThemeColorVariants(themeVars, prefix, 'alt-', theme);
|
|
399
404
|
themeVars.push('}\n');
|
|
@@ -423,6 +428,14 @@ function addDynamicThemeColors(vars, prefix, tailwindName, cssVarName, addSpacin
|
|
|
423
428
|
vars.push(` --color-${prefix}-on-${tailwindName}-disabled: rgb(var(--${prefix}-color-on-${cssVarName}-disabled));`);
|
|
424
429
|
vars.push('');
|
|
425
430
|
}
|
|
431
|
+
function addDynamicInkColors(vars, prefix, tailwindName, cssVarName) {
|
|
432
|
+
vars.push(` --color-${prefix}-${tailwindName}: rgb(var(--${prefix}-color-${cssVarName}));`);
|
|
433
|
+
vars.push(` --color-${prefix}-${tailwindName}-hover: rgb(var(--${prefix}-color-${cssVarName}-hover));`);
|
|
434
|
+
vars.push(` --color-${prefix}-${tailwindName}-focus: rgb(var(--${prefix}-color-${cssVarName}-focus));`);
|
|
435
|
+
vars.push(` --color-${prefix}-${tailwindName}-active: rgb(var(--${prefix}-color-${cssVarName}-active));`);
|
|
436
|
+
vars.push(` --color-${prefix}-${tailwindName}-disabled: rgb(var(--${prefix}-color-${cssVarName}-disabled));`);
|
|
437
|
+
vars.push('');
|
|
438
|
+
}
|
|
426
439
|
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
427
440
|
// Tailwind 4 requires --color-* prefix and rgb() wrapper
|
|
428
441
|
// Always generate all variants with fallbacks
|
|
@@ -465,6 +478,17 @@ function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
|
465
478
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
466
479
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
467
480
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
481
|
+
const inkDefaultColor = swatch.inkColor?.default || defaultColor;
|
|
482
|
+
const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
|
|
483
|
+
const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
|
|
484
|
+
const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
|
|
485
|
+
const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
|
|
486
|
+
vars.push('');
|
|
487
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
|
|
488
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
|
|
489
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
|
|
490
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
|
|
491
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
|
|
468
492
|
if (theme.secondary || theme.tertiary) {
|
|
469
493
|
vars.push('');
|
|
470
494
|
}
|
|
@@ -0,0 +1,267 @@
|
|
|
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 surface theme generator...\n');
|
|
5
|
+
const themesPath = schema.themesPath || 'src/surface-themes.ts';
|
|
6
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-surface-themes.css';
|
|
7
|
+
const prefix = schema.prefix || 'et';
|
|
8
|
+
if (!tree.exists(themesPath)) {
|
|
9
|
+
logger.error(`❌ Surface themes file not found at: ${themesPath}`);
|
|
10
|
+
logger.log(`\nPlease specify the correct path using --themesPath option.`);
|
|
11
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-surface-theme --themesPath=src/app/surface-themes.ts\n`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
logger.log(`📁 Reading surface themes from: ${themesPath}`);
|
|
15
|
+
const themesContent = tree.read(themesPath, 'utf-8');
|
|
16
|
+
if (!themesContent) {
|
|
17
|
+
logger.error('❌ Failed to read surface themes file');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
let themes;
|
|
21
|
+
try {
|
|
22
|
+
themes = extractSurfaceThemesFromContent(themesContent, themesPath);
|
|
23
|
+
logger.log(`✅ Found ${themes.length} surface theme(s)`);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
logger.error('❌ Failed to parse surface themes file');
|
|
27
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
logger.log('\nThe surface themes file must export themes as:');
|
|
29
|
+
logger.log(' export const SURFACE_THEMES = [...] satisfies SurfaceTheme[];\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
validateSurfaceThemeConfiguration(themes);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
logger.error('❌ Surface theme configuration error');
|
|
37
|
+
logger.error(` ${error instanceof Error ? error.message : String(error)}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
logger.log('\n🎨 Generating Tailwind surface theme CSS...');
|
|
41
|
+
const css = generateSurfaceThemeCss(themes, prefix, schema);
|
|
42
|
+
tree.write(outputPath, css);
|
|
43
|
+
logger.log(`✅ Generated Tailwind surface themes at: ${outputPath}`);
|
|
44
|
+
const mainStylesFiles = findMainStylesFile(tree);
|
|
45
|
+
if (mainStylesFiles.length > 0) {
|
|
46
|
+
logger.log('\n📝 Found potential main styles files:');
|
|
47
|
+
mainStylesFiles.forEach((file) => logger.log(` - ${file}`));
|
|
48
|
+
logger.log('\n⚠️ Please manually import the generated themes:');
|
|
49
|
+
logger.log(` @import './${outputPath.replace('src/styles/', '')}';`);
|
|
50
|
+
}
|
|
51
|
+
if (!schema.skipFormat) {
|
|
52
|
+
await formatFiles(tree);
|
|
53
|
+
}
|
|
54
|
+
logger.log('\n✅ Generation completed successfully!\n');
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region Parsing
|
|
58
|
+
function extractSurfaceThemesFromContent(content, filePath) {
|
|
59
|
+
const project = new Project({
|
|
60
|
+
useInMemoryFileSystem: true,
|
|
61
|
+
compilerOptions: {
|
|
62
|
+
target: 99,
|
|
63
|
+
module: 99,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
67
|
+
const themes = [];
|
|
68
|
+
const exportedDeclarations = sourceFile.getVariableDeclarations().filter((decl) => {
|
|
69
|
+
const statement = decl.getVariableStatement();
|
|
70
|
+
return statement?.isExported();
|
|
71
|
+
});
|
|
72
|
+
const themesArray = exportedDeclarations.find((decl) => {
|
|
73
|
+
const name = decl.getName();
|
|
74
|
+
return name === 'SURFACE_THEMES' || name === 'surfaceThemes';
|
|
75
|
+
});
|
|
76
|
+
if (!themesArray) {
|
|
77
|
+
throw new Error('Could not find SURFACE_THEMES or surfaceThemes export');
|
|
78
|
+
}
|
|
79
|
+
let initializer = themesArray.getInitializer();
|
|
80
|
+
if (!initializer) {
|
|
81
|
+
throw new Error('SURFACE_THEMES export has no initializer');
|
|
82
|
+
}
|
|
83
|
+
if (initializer.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
84
|
+
initializer = initializer.getExpression();
|
|
85
|
+
}
|
|
86
|
+
if (initializer.isKind(SyntaxKind.AsExpression)) {
|
|
87
|
+
initializer = initializer.getExpression();
|
|
88
|
+
}
|
|
89
|
+
if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
90
|
+
throw new Error('SURFACE_THEMES export must be an array literal');
|
|
91
|
+
}
|
|
92
|
+
const elements = initializer.getElements();
|
|
93
|
+
for (const element of elements) {
|
|
94
|
+
if (element.isKind(SyntaxKind.Identifier)) {
|
|
95
|
+
const name = element.getText();
|
|
96
|
+
const themeDecl = exportedDeclarations.find((decl) => decl.getName() === name);
|
|
97
|
+
if (!themeDecl) {
|
|
98
|
+
logger.warn(`⚠️ Could not find declaration for surface theme: ${name}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
let themeObj = themeDecl.getInitializer();
|
|
102
|
+
if (!themeObj) {
|
|
103
|
+
logger.warn(`⚠️ Surface theme ${name} has no initializer`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (themeObj.isKind(SyntaxKind.AsExpression)) {
|
|
107
|
+
themeObj = themeObj.getExpression();
|
|
108
|
+
}
|
|
109
|
+
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
110
|
+
logger.warn(`⚠️ Surface theme ${name} is not an object literal`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const theme = parseSurfaceThemeObject(themeObj);
|
|
115
|
+
themes.push(theme);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.warn(`⚠️ Failed to parse surface theme ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (themes.length === 0) {
|
|
123
|
+
throw new Error('No valid surface themes found in SURFACE_THEMES array');
|
|
124
|
+
}
|
|
125
|
+
return themes;
|
|
126
|
+
}
|
|
127
|
+
function parseSurfaceThemeObject(obj) {
|
|
128
|
+
const properties = obj.getProperties();
|
|
129
|
+
const theme = {};
|
|
130
|
+
const STRING_PROPS = ['name', 'background', 'color', 'colorMuted', 'colorSubtle', 'border'];
|
|
131
|
+
for (const prop of properties) {
|
|
132
|
+
if (!prop.isKind(SyntaxKind.PropertyAssignment)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const propName = prop.getName();
|
|
136
|
+
const initializer = prop.getInitializer();
|
|
137
|
+
if (!initializer) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (propName === 'isDefault') {
|
|
141
|
+
if (initializer.isKind(SyntaxKind.TrueKeyword)) {
|
|
142
|
+
theme.isDefault = true;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (propName === 'elevation') {
|
|
147
|
+
if (initializer.isKind(SyntaxKind.NumericLiteral)) {
|
|
148
|
+
theme.elevation = Number(initializer.getLiteralValue());
|
|
149
|
+
}
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (propName === 'type' && initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
153
|
+
theme.type = initializer.getLiteralValue();
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (STRING_PROPS.includes(propName) && initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
157
|
+
theme[propName] = initializer.getLiteralValue();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (!theme.name ||
|
|
161
|
+
!theme.type ||
|
|
162
|
+
theme.elevation === undefined ||
|
|
163
|
+
!theme.background ||
|
|
164
|
+
!theme.color ||
|
|
165
|
+
!theme.colorMuted ||
|
|
166
|
+
!theme.colorSubtle ||
|
|
167
|
+
!theme.border) {
|
|
168
|
+
throw new Error('SurfaceTheme must have name, type, elevation, background, color, colorMuted, colorSubtle, and border properties');
|
|
169
|
+
}
|
|
170
|
+
return theme;
|
|
171
|
+
}
|
|
172
|
+
function validateSurfaceThemeConfiguration(themes) {
|
|
173
|
+
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
174
|
+
if (defaultThemes.length === 0) {
|
|
175
|
+
throw new Error('No default surface theme found. Each type (light/dark) must have exactly one theme with isDefault: true');
|
|
176
|
+
}
|
|
177
|
+
const types = new Set(themes.map((t) => t.type));
|
|
178
|
+
for (const type of types) {
|
|
179
|
+
const defaultsForType = defaultThemes.filter((t) => t.type === type);
|
|
180
|
+
if (defaultsForType.length === 0) {
|
|
181
|
+
throw new Error(`No default surface theme found for type '${type}'. Add isDefault: true to one '${type}' theme`);
|
|
182
|
+
}
|
|
183
|
+
if (defaultsForType.length > 1) {
|
|
184
|
+
throw new Error(`Multiple default surface themes found for type '${type}': ${defaultsForType.map((t) => t.name).join(', ')}. Only one theme per type can have isDefault: true`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region CSS Generation
|
|
190
|
+
function createCssSurfaceName(name) {
|
|
191
|
+
return name
|
|
192
|
+
.toLowerCase()
|
|
193
|
+
.replace(/\s+/g, '-')
|
|
194
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
195
|
+
}
|
|
196
|
+
function generateSurfaceThemeCss(themes, prefix, schema) {
|
|
197
|
+
const tailwindVars = [];
|
|
198
|
+
const themeVars = [];
|
|
199
|
+
const themesPath = schema.themesPath || 'src/surface-themes.ts';
|
|
200
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-surface-themes.css';
|
|
201
|
+
const header = `/*
|
|
202
|
+
* Auto-generated Tailwind 4 surface theme colors from @ethlete/core
|
|
203
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
204
|
+
*
|
|
205
|
+
* Generated from your surface theme definitions
|
|
206
|
+
* This file can be regenerated by running:
|
|
207
|
+
* nx g @ethlete/core:tailwind-4-surface-theme --themesPath=${themesPath}${schema.outputPath ? ` --outputPath=${outputPath}` : ''}${schema.prefix && schema.prefix !== 'et' ? ` --prefix=${schema.prefix}` : ''}
|
|
208
|
+
*/
|
|
209
|
+
|
|
210
|
+
`;
|
|
211
|
+
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
212
|
+
const regularThemes = themes.filter((t) => !t.isDefault);
|
|
213
|
+
// Static Tailwind @theme block — one color set per surface theme
|
|
214
|
+
for (const theme of themes) {
|
|
215
|
+
const name = createCssSurfaceName(theme.name);
|
|
216
|
+
tailwindVars.push(` /* ${theme.name} surface */`);
|
|
217
|
+
tailwindVars.push(` --color-${prefix}-surface-${name}-bg: rgb(${theme.background});`);
|
|
218
|
+
tailwindVars.push(` --color-${prefix}-surface-${name}: rgb(${theme.color});`);
|
|
219
|
+
tailwindVars.push(` --color-${prefix}-surface-${name}-muted: rgb(${theme.colorMuted});`);
|
|
220
|
+
tailwindVars.push(` --color-${prefix}-surface-${name}-subtle: rgb(${theme.colorSubtle});`);
|
|
221
|
+
tailwindVars.push(` --color-${prefix}-surface-${name}-border: rgb(${theme.border});`);
|
|
222
|
+
tailwindVars.push('');
|
|
223
|
+
}
|
|
224
|
+
// Dynamic surface colors (reference runtime CSS variables)
|
|
225
|
+
tailwindVars.push(' /* Dynamic surface colors (references runtime CSS variables) */');
|
|
226
|
+
tailwindVars.push(` --color-${prefix}-surface-bg: rgb(var(--${prefix}-surface-background));`);
|
|
227
|
+
tailwindVars.push(` --color-${prefix}-surface: rgb(var(--${prefix}-surface-color));`);
|
|
228
|
+
tailwindVars.push(` --color-${prefix}-surface-muted: rgb(var(--${prefix}-surface-color-muted));`);
|
|
229
|
+
tailwindVars.push(` --color-${prefix}-surface-subtle: rgb(var(--${prefix}-surface-color-subtle));`);
|
|
230
|
+
tailwindVars.push(` --color-${prefix}-surface-border: rgb(var(--${prefix}-surface-border));`);
|
|
231
|
+
// Runtime CSS — theme selector classes
|
|
232
|
+
for (const theme of themes) {
|
|
233
|
+
const name = createCssSurfaceName(theme.name);
|
|
234
|
+
const isDefault = theme.isDefault;
|
|
235
|
+
if (isDefault) {
|
|
236
|
+
const selectors = [`.${prefix}-surface--default-${theme.type}`, `.${prefix}-surface--${name}`];
|
|
237
|
+
themeVars.push(`${selectors.join(', ')} {`);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
themeVars.push(`.${prefix}-surface--${name} {`);
|
|
241
|
+
}
|
|
242
|
+
themeVars.push(` --${prefix}-surface-background: ${theme.background};`);
|
|
243
|
+
themeVars.push(` --${prefix}-surface-color: ${theme.color};`);
|
|
244
|
+
themeVars.push(` --${prefix}-surface-color-muted: ${theme.colorMuted};`);
|
|
245
|
+
themeVars.push(` --${prefix}-surface-color-subtle: ${theme.colorSubtle};`);
|
|
246
|
+
themeVars.push(` --${prefix}-surface-border: ${theme.border};`);
|
|
247
|
+
themeVars.push(` --${prefix}-surface-type: ${theme.type};`);
|
|
248
|
+
themeVars.push(` --${prefix}-surface-elevation: ${theme.elevation};`);
|
|
249
|
+
themeVars.push('}\n');
|
|
250
|
+
}
|
|
251
|
+
return `${header}@theme {
|
|
252
|
+
${tailwindVars.join('\n')}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
${themeVars.join('\n')}`;
|
|
256
|
+
}
|
|
257
|
+
function findMainStylesFile(tree) {
|
|
258
|
+
const potentialFiles = [];
|
|
259
|
+
visitNotIgnoredFiles(tree, '', (path) => {
|
|
260
|
+
if (path.match(/styles\.(css|scss)$/) && !path.includes('node_modules') && !path.includes('dist')) {
|
|
261
|
+
potentialFiles.push(path);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
return potentialFiles;
|
|
265
|
+
}
|
|
266
|
+
//#endregion
|
|
267
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "tailwind-4-surface-theme",
|
|
4
|
+
"title": "Tailwind 4 Surface Theming Generator",
|
|
5
|
+
"description": "Generates Tailwind 4 compatible CSS from your Ethlete surface theme definitions",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"themesPath": {
|
|
9
|
+
"type": "string",
|
|
10
|
+
"description": "Path to your surface themes file (relative to workspace root)",
|
|
11
|
+
"default": "src/surface-themes.ts",
|
|
12
|
+
"x-prompt": "Where is your surface themes file located?"
|
|
13
|
+
},
|
|
14
|
+
"outputPath": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Where to output the generated Tailwind surface themes CSS",
|
|
17
|
+
"default": "src/styles/generated-tailwind-surface-themes.css",
|
|
18
|
+
"x-prompt": "Where should the generated CSS be saved?"
|
|
19
|
+
},
|
|
20
|
+
"prefix": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"description": "Prefix for surface variable names (e.g., 'et' creates --color-et-surface-*)",
|
|
23
|
+
"default": "et"
|
|
24
|
+
},
|
|
25
|
+
"skipFormat": {
|
|
26
|
+
"type": "boolean",
|
|
27
|
+
"description": "Skip formatting files after generation",
|
|
28
|
+
"default": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"required": []
|
|
32
|
+
}
|