@ethlete/core 5.0.0-next.2 ā 5.0.0-next.21
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 +150 -0
- package/fesm2022/ethlete-core.mjs +3047 -959
- package/fesm2022/ethlete-core.mjs.map +1 -1
- package/generators/generators.json +9 -4
- package/generators/migrate-to-v5/color-naming.js +175 -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 +141 -87
- package/generators/tailwind-4-color-theme/schema.json +40 -0
- package/generators/tailwind-4-surface-theme/generator.js +402 -0
- package/generators/tailwind-4-surface-theme/schema.json +40 -0
- package/package.json +14 -13
- package/types/ethlete-core.d.ts +1714 -1032
- package/generators/tailwind-4-theme/schema.json +0 -32
|
@@ -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,175 @@
|
|
|
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
|
+
['et-theme-alt--', 'et-color-alt--'],
|
|
30
|
+
['et-theme--', 'et-color--'],
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* CSS class name replacements.
|
|
34
|
+
*/
|
|
35
|
+
const CSS_REPLACEMENTS = [
|
|
36
|
+
['.et-color-themed', '.et-colored'],
|
|
37
|
+
['.et-surface-themed', '.et-surfaced'],
|
|
38
|
+
['.et-theme--', '.et-color--'],
|
|
39
|
+
['.et-theme-alt--', '.et-color-alt--'],
|
|
40
|
+
];
|
|
41
|
+
/**
|
|
42
|
+
* Host directive input alias replacements in TypeScript files.
|
|
43
|
+
*/
|
|
44
|
+
const HOST_DIRECTIVE_REPLACEMENTS = [
|
|
45
|
+
["'etProvideTheme:theme'", "'etProvideColor:color'"],
|
|
46
|
+
["'etProvideAltTheme:altTheme'", "'etProvideAltColor:altColor'"],
|
|
47
|
+
["'etProvideTheme:color'", "'etProvideColor:color'"],
|
|
48
|
+
["'etProvideAltTheme:altColor'", "'etProvideAltColor:altColor'"],
|
|
49
|
+
['"etProvideTheme:theme"', '"etProvideColor:color"'],
|
|
50
|
+
['"etProvideAltTheme:altTheme"', '"etProvideAltColor:altColor"'],
|
|
51
|
+
['"etProvideTheme"', '"etProvideColor"'],
|
|
52
|
+
['"etProvideAltTheme"', '"etProvideAltColor"'],
|
|
53
|
+
];
|
|
54
|
+
export default async function migrateColorNaming(tree) {
|
|
55
|
+
logger.log('\nš Migrating theme ā color naming...\n');
|
|
56
|
+
const tsFiles = [];
|
|
57
|
+
const htmlFiles = [];
|
|
58
|
+
const cssFiles = [];
|
|
59
|
+
findFiles('.', tree, tsFiles, htmlFiles, cssFiles);
|
|
60
|
+
let filesModified = 0;
|
|
61
|
+
for (const filePath of tsFiles) {
|
|
62
|
+
const wasModified = migrateTypeScriptFile(tree, filePath);
|
|
63
|
+
if (wasModified) {
|
|
64
|
+
filesModified++;
|
|
65
|
+
logger.log(` ā ${filePath}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
for (const filePath of htmlFiles) {
|
|
69
|
+
const wasModified = migrateTemplateFile(tree, filePath);
|
|
70
|
+
if (wasModified) {
|
|
71
|
+
filesModified++;
|
|
72
|
+
logger.log(` ā ${filePath}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
for (const filePath of cssFiles) {
|
|
76
|
+
const wasModified = migrateCssFile(tree, filePath);
|
|
77
|
+
if (wasModified) {
|
|
78
|
+
filesModified++;
|
|
79
|
+
logger.log(` ā ${filePath}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (filesModified > 0) {
|
|
83
|
+
logger.log(`\nā
Successfully migrated color naming in ${filesModified} file(s)\n`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
logger.log('\nā¹ļø No files needed migration\n');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function findFiles(dir, tree, tsFiles, htmlFiles, cssFiles) {
|
|
90
|
+
const children = tree.children(dir);
|
|
91
|
+
for (const child of children) {
|
|
92
|
+
const path = dir === '.' ? child : `${dir}/${child}`;
|
|
93
|
+
if (tree.isFile(path)) {
|
|
94
|
+
if (path.includes('node_modules'))
|
|
95
|
+
continue;
|
|
96
|
+
if (path.endsWith('.ts') && !path.endsWith('.spec.ts')) {
|
|
97
|
+
tsFiles.push(path);
|
|
98
|
+
}
|
|
99
|
+
else if (path.endsWith('.html')) {
|
|
100
|
+
htmlFiles.push(path);
|
|
101
|
+
}
|
|
102
|
+
else if (path.endsWith('.css') || path.endsWith('.scss')) {
|
|
103
|
+
cssFiles.push(path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
if (child === 'node_modules')
|
|
108
|
+
continue;
|
|
109
|
+
findFiles(path, tree, tsFiles, htmlFiles, cssFiles);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function migrateTypeScriptFile(tree, filePath) {
|
|
114
|
+
let content = tree.read(filePath, 'utf-8');
|
|
115
|
+
if (!content)
|
|
116
|
+
return false;
|
|
117
|
+
const original = content;
|
|
118
|
+
// Rename symbols
|
|
119
|
+
for (const [oldName, newName] of Object.entries(TS_SYMBOL_RENAMES)) {
|
|
120
|
+
content = replaceWholeWord(content, oldName, newName);
|
|
121
|
+
}
|
|
122
|
+
// Rename host directive input aliases
|
|
123
|
+
for (const [oldStr, newStr] of HOST_DIRECTIVE_REPLACEMENTS) {
|
|
124
|
+
content = content.replaceAll(oldStr, newStr);
|
|
125
|
+
}
|
|
126
|
+
// Handle inline templates in TypeScript files
|
|
127
|
+
for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
|
|
128
|
+
content = content.replaceAll(oldStr, newStr);
|
|
129
|
+
}
|
|
130
|
+
// Handle CSS in TypeScript files
|
|
131
|
+
for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
|
|
132
|
+
content = content.replaceAll(oldStr, newStr);
|
|
133
|
+
}
|
|
134
|
+
if (content !== original) {
|
|
135
|
+
tree.write(filePath, content);
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
function migrateTemplateFile(tree, filePath) {
|
|
141
|
+
let content = tree.read(filePath, 'utf-8');
|
|
142
|
+
if (!content)
|
|
143
|
+
return false;
|
|
144
|
+
const original = content;
|
|
145
|
+
for (const [oldStr, newStr] of TEMPLATE_REPLACEMENTS) {
|
|
146
|
+
content = content.replaceAll(oldStr, newStr);
|
|
147
|
+
}
|
|
148
|
+
if (content !== original) {
|
|
149
|
+
tree.write(filePath, content);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
function migrateCssFile(tree, filePath) {
|
|
155
|
+
let content = tree.read(filePath, 'utf-8');
|
|
156
|
+
if (!content)
|
|
157
|
+
return false;
|
|
158
|
+
const original = content;
|
|
159
|
+
for (const [oldStr, newStr] of CSS_REPLACEMENTS) {
|
|
160
|
+
content = content.replaceAll(oldStr, newStr);
|
|
161
|
+
}
|
|
162
|
+
if (content !== original) {
|
|
163
|
+
tree.write(filePath, content);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
function replaceWholeWord(content, oldWord, newWord) {
|
|
169
|
+
const regex = new RegExp(`\\b${escapeRegex(oldWord)}\\b`, 'g');
|
|
170
|
+
return content.replace(regex, newWord);
|
|
171
|
+
}
|
|
172
|
+
function escapeRegex(str) {
|
|
173
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
|
+
}
|
|
175
|
+
//# 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": []
|
|
@@ -5,11 +5,15 @@ export default async function generate(tree, schema) {
|
|
|
5
5
|
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
6
6
|
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
7
7
|
const prefix = schema.prefix || 'et';
|
|
8
|
+
// The runtime prefix drives the theme-swap CSS variables (--<runtimePrefix>-color-primary)
|
|
9
|
+
// and selectors (.<runtimePrefix>-color--<name>). It defaults to `prefix` so existing
|
|
10
|
+
// consumers who only ever passed one prefix see no change in output.
|
|
11
|
+
const runtimePrefix = schema.runtimePrefix || prefix;
|
|
8
12
|
// Step 1: Check if themes file exists
|
|
9
13
|
if (!tree.exists(themesPath)) {
|
|
10
14
|
logger.error(`ā Themes file not found at: ${themesPath}`);
|
|
11
15
|
logger.log(`\nPlease specify the correct path using --themesPath option.`);
|
|
12
|
-
logger.log(`Example: nx g @ethlete/core:tailwind-4-theme --themesPath=src/app/themes.ts\n`);
|
|
16
|
+
logger.log(`Example: nx g @ethlete/core:tailwind-4-color-theme --themesPath=src/app/themes.ts\n`);
|
|
13
17
|
return;
|
|
14
18
|
}
|
|
15
19
|
logger.log(`š Reading themes from: ${themesPath}`);
|
|
@@ -43,7 +47,7 @@ export default async function generate(tree, schema) {
|
|
|
43
47
|
}
|
|
44
48
|
// Step 5: Generate Tailwind CSS
|
|
45
49
|
logger.log('\nšØ Generating Tailwind theme CSS...');
|
|
46
|
-
const css = generateTailwindThemeCss(themes, prefix, schema);
|
|
50
|
+
const css = generateTailwindThemeCss(themes, prefix, runtimePrefix, schema);
|
|
47
51
|
// Step 6: Write the generated CSS file
|
|
48
52
|
const outputDir = outputPath.substring(0, outputPath.lastIndexOf('/'));
|
|
49
53
|
if (outputDir && !tree.exists(outputDir)) {
|
|
@@ -51,7 +55,14 @@ export default async function generate(tree, schema) {
|
|
|
51
55
|
}
|
|
52
56
|
tree.write(outputPath, css);
|
|
53
57
|
logger.log(`ā
Generated Tailwind themes at: ${outputPath}`);
|
|
54
|
-
// Step 7:
|
|
58
|
+
// Step 7: Generate the `EthleteColorThemeNameRegistry` augmentation, so `etProvideColor`
|
|
59
|
+
// (and anything else that accepts a `RegisteredColorThemeName`) is checked/autocompleted
|
|
60
|
+
// against this app's actual theme names, instead of a plain `string`.
|
|
61
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
62
|
+
const typesDts = generateColorThemeNameTypes(themes, schema);
|
|
63
|
+
tree.write(typesOutputPath, typesDts);
|
|
64
|
+
logger.log(`ā
Generated color theme name types at: ${typesOutputPath}`);
|
|
65
|
+
// Step 8: Try to find and update main styles file
|
|
55
66
|
const mainStylesFiles = findMainStylesFile(tree);
|
|
56
67
|
if (mainStylesFiles.length > 0) {
|
|
57
68
|
logger.log('\nš Found potential main styles files:');
|
|
@@ -95,12 +106,9 @@ function extractThemesFromContent(content, filePath) {
|
|
|
95
106
|
if (!initializer) {
|
|
96
107
|
throw new Error('THEMES export has no initializer');
|
|
97
108
|
}
|
|
98
|
-
// Handle 'satisfies'
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
// Handle 'as const': [array] as const
|
|
103
|
-
if (initializer.isKind(SyntaxKind.AsExpression)) {
|
|
109
|
+
// Handle 'satisfies X'/'as const' wrappers, in either order: [array] satisfies Type[],
|
|
110
|
+
// [array] as const, or both combined.
|
|
111
|
+
while (initializer.isKind(SyntaxKind.SatisfiesExpression) || initializer.isKind(SyntaxKind.AsExpression)) {
|
|
104
112
|
initializer = initializer.getExpression();
|
|
105
113
|
}
|
|
106
114
|
if (!initializer.isKind(SyntaxKind.ArrayLiteralExpression)) {
|
|
@@ -122,8 +130,9 @@ function extractThemesFromContent(content, filePath) {
|
|
|
122
130
|
logger.warn(`ā ļø Theme ${name} has no initializer`);
|
|
123
131
|
continue;
|
|
124
132
|
}
|
|
125
|
-
// Handle 'as const' on individual theme objects
|
|
126
|
-
|
|
133
|
+
// Handle 'as const' and 'satisfies X' wrappers on individual theme objects, in
|
|
134
|
+
// either order (e.g. `{...} satisfies Theme`, `{...} as const`, or both combined).
|
|
135
|
+
while (themeObj.isKind(SyntaxKind.AsExpression) || themeObj.isKind(SyntaxKind.SatisfiesExpression)) {
|
|
127
136
|
themeObj = themeObj.getExpression();
|
|
128
137
|
}
|
|
129
138
|
if (!themeObj.isKind(SyntaxKind.ObjectLiteralExpression)) {
|
|
@@ -167,9 +176,9 @@ function parseThemeObject(obj, sourceFile) {
|
|
|
167
176
|
theme.isDefault = true;
|
|
168
177
|
}
|
|
169
178
|
break;
|
|
170
|
-
case '
|
|
171
|
-
if (initializer.isKind(SyntaxKind.
|
|
172
|
-
theme.
|
|
179
|
+
case 'type':
|
|
180
|
+
if (initializer.isKind(SyntaxKind.StringLiteral)) {
|
|
181
|
+
theme.type = initializer.getLiteralValue();
|
|
173
182
|
}
|
|
174
183
|
break;
|
|
175
184
|
case 'primary':
|
|
@@ -188,8 +197,6 @@ function parseThemeObject(obj, sourceFile) {
|
|
|
188
197
|
}
|
|
189
198
|
function validateThemeConfiguration(themes) {
|
|
190
199
|
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
191
|
-
const defaultAltThemes = themes.filter((t) => t.isDefaultAlt);
|
|
192
|
-
const bothDefaultAndAlt = themes.filter((t) => t.isDefault && t.isDefaultAlt);
|
|
193
200
|
// Error: No default theme
|
|
194
201
|
if (defaultThemes.length === 0) {
|
|
195
202
|
throw new Error('No default theme found. At least one theme must have isDefault: true');
|
|
@@ -198,13 +205,18 @@ function validateThemeConfiguration(themes) {
|
|
|
198
205
|
if (defaultThemes.length > 1) {
|
|
199
206
|
throw new Error(`Multiple default themes found: ${defaultThemes.map((t) => t.name).join(', ')}. Only one theme can have isDefault: true`);
|
|
200
207
|
}
|
|
201
|
-
// Error:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
+
// Error: Duplicate theme types
|
|
209
|
+
const typedThemes = themes.filter((t) => t.type);
|
|
210
|
+
const typeMap = new Map();
|
|
211
|
+
for (const theme of typedThemes) {
|
|
212
|
+
const existing = typeMap.get(theme.type) || [];
|
|
213
|
+
existing.push(theme.name);
|
|
214
|
+
typeMap.set(theme.type, existing);
|
|
215
|
+
}
|
|
216
|
+
for (const [type, names] of typeMap) {
|
|
217
|
+
if (names.length > 1) {
|
|
218
|
+
throw new Error(`Multiple themes with type "${type}" found: ${names.join(', ')}. Only one theme can have a given type`);
|
|
219
|
+
}
|
|
208
220
|
}
|
|
209
221
|
}
|
|
210
222
|
function parseThemeSwatch(obj, sourceFile) {
|
|
@@ -219,7 +231,7 @@ function parseThemeSwatch(obj, sourceFile) {
|
|
|
219
231
|
if (!initializer) {
|
|
220
232
|
continue;
|
|
221
233
|
}
|
|
222
|
-
if (propName === 'color' || propName === 'onColor') {
|
|
234
|
+
if (propName === 'color' || propName === 'onColor' || propName === 'inkColor') {
|
|
223
235
|
const colorMap = parseColorMap(initializer, sourceFile);
|
|
224
236
|
if (colorMap) {
|
|
225
237
|
swatch[propName] = colorMap;
|
|
@@ -295,132 +307,163 @@ function createCssThemeName(name) {
|
|
|
295
307
|
.replace(/\s+/g, '-')
|
|
296
308
|
.replace(/[^a-z0-9-]/g, '');
|
|
297
309
|
}
|
|
298
|
-
function
|
|
310
|
+
function generateColorThemeNameTypes(themes, schema) {
|
|
311
|
+
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
312
|
+
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
313
|
+
const typesOutputPath = schema.typesOutputPath || outputPath.replace(/\.css$/, '.d.ts');
|
|
314
|
+
const names = themes.map((theme) => `'${theme.name}'`).join(' | ');
|
|
315
|
+
return `/*
|
|
316
|
+
* Auto-generated by @ethlete/core:tailwind-4-color-theme
|
|
317
|
+
* DO NOT EDIT THIS FILE MANUALLY
|
|
318
|
+
*
|
|
319
|
+
* Regenerate by running:
|
|
320
|
+
* 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}` : ''}
|
|
321
|
+
*/
|
|
322
|
+
|
|
323
|
+
declare module '@ethlete/core' {
|
|
324
|
+
interface EthleteColorThemeNameRegistry {
|
|
325
|
+
name: ${names};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export {};
|
|
330
|
+
`;
|
|
331
|
+
}
|
|
332
|
+
function generateTailwindThemeCss(themes, utilityPrefix, runtimePrefix, schema) {
|
|
299
333
|
const tailwindVars = [];
|
|
300
334
|
const themeVars = [];
|
|
301
335
|
const themesPath = schema.themesPath || 'src/themes.ts';
|
|
302
336
|
const outputPath = schema.outputPath || 'src/styles/generated-tailwind-themes.css';
|
|
303
|
-
const header = `/*
|
|
337
|
+
const header = `/*
|
|
304
338
|
* Auto-generated Tailwind 4 theme colors from @ethlete/core
|
|
305
339
|
* DO NOT EDIT THIS FILE MANUALLY
|
|
306
|
-
*
|
|
340
|
+
*
|
|
307
341
|
* Generated from your theme definitions
|
|
308
342
|
* 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}` : ''}
|
|
343
|
+
* 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}` : ''}
|
|
310
344
|
*/
|
|
311
345
|
|
|
312
346
|
`;
|
|
313
347
|
// Validation is now done separately before this function is called
|
|
314
348
|
const defaultThemes = themes.filter((t) => t.isDefault);
|
|
315
|
-
const
|
|
316
|
-
const regularThemes = themes.filter((t) => !t.isDefault && !t.isDefaultAlt);
|
|
349
|
+
const regularThemes = themes.filter((t) => !t.isDefault);
|
|
317
350
|
// Generate static Tailwind @theme block for each theme
|
|
318
351
|
for (const theme of themes) {
|
|
319
352
|
const name = createCssThemeName(theme.name);
|
|
320
353
|
// Add comment for theme section
|
|
321
354
|
tailwindVars.push(` /* ${theme.name} theme */`);
|
|
322
355
|
// Primary colors for Tailwind utilities
|
|
323
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
356
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}`, theme.primary.color);
|
|
324
357
|
tailwindVars.push('');
|
|
325
358
|
// On colors for Tailwind utilities
|
|
326
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
359
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}`, theme.primary.onColor);
|
|
360
|
+
tailwindVars.push('');
|
|
361
|
+
// Ink colors for standalone foreground usage
|
|
362
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-ink`, theme.primary.inkColor || theme.primary.color);
|
|
327
363
|
tailwindVars.push('');
|
|
328
364
|
// Secondary colors if present
|
|
329
365
|
if (theme.secondary) {
|
|
330
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
366
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-secondary`, theme.secondary.color);
|
|
331
367
|
tailwindVars.push('');
|
|
332
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
368
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-secondary`, theme.secondary.onColor);
|
|
333
369
|
tailwindVars.push('');
|
|
334
370
|
}
|
|
335
371
|
// Tertiary colors if present
|
|
336
372
|
if (theme.tertiary) {
|
|
337
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
373
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-${name}-tertiary`, theme.tertiary.color);
|
|
338
374
|
tailwindVars.push('');
|
|
339
|
-
addTailwindColorVariants(tailwindVars, `${
|
|
375
|
+
addTailwindColorVariants(tailwindVars, `${utilityPrefix}-on-${name}-tertiary`, theme.tertiary.onColor);
|
|
340
376
|
tailwindVars.push('');
|
|
341
377
|
}
|
|
342
378
|
}
|
|
343
379
|
tailwindVars.push('');
|
|
344
|
-
//
|
|
380
|
+
// Main theme dynamic colors
|
|
345
381
|
const mainThemes = [...defaultThemes, ...regularThemes];
|
|
346
382
|
const hasSecondary = mainThemes.some((t) => t.secondary);
|
|
347
383
|
const hasTertiary = mainThemes.some((t) => t.tertiary);
|
|
348
|
-
const hasAltSecondary = defaultAltThemes.some((t) => t.secondary);
|
|
349
|
-
const hasAltTertiary = defaultAltThemes.some((t) => t.tertiary);
|
|
350
|
-
// Main theme dynamic colors
|
|
351
384
|
tailwindVars.push(' /* Dynamic theme colors (references runtime CSS variables) */');
|
|
352
|
-
addDynamicThemeColors(tailwindVars,
|
|
385
|
+
addDynamicThemeColors(tailwindVars, utilityPrefix, runtimePrefix, 'theme', 'primary', true);
|
|
386
|
+
addDynamicInkColors(tailwindVars, utilityPrefix, runtimePrefix, 'theme-ink', 'primary-ink');
|
|
353
387
|
if (hasSecondary) {
|
|
354
|
-
addDynamicThemeColors(tailwindVars,
|
|
388
|
+
addDynamicThemeColors(tailwindVars, utilityPrefix, runtimePrefix, 'theme-secondary', 'secondary', false);
|
|
355
389
|
}
|
|
356
390
|
if (hasTertiary) {
|
|
357
|
-
addDynamicThemeColors(tailwindVars,
|
|
358
|
-
}
|
|
359
|
-
// Alt theme dynamic colors
|
|
360
|
-
tailwindVars.push(' /* Alt theme dynamic colors */');
|
|
361
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme', 'alt-primary', true);
|
|
362
|
-
if (hasAltSecondary) {
|
|
363
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme-secondary', 'alt-secondary', false);
|
|
391
|
+
addDynamicThemeColors(tailwindVars, utilityPrefix, runtimePrefix, 'theme-tertiary', 'tertiary', false);
|
|
364
392
|
}
|
|
365
|
-
|
|
366
|
-
addDynamicThemeColors(tailwindVars, prefix, 'alt-theme-tertiary', 'alt-tertiary', false);
|
|
367
|
-
}
|
|
368
|
-
// Generate runtime CSS for ALL themes (both main and alt variants)
|
|
393
|
+
// Generate runtime CSS for all themes
|
|
369
394
|
themes.forEach((theme) => {
|
|
370
395
|
const name = createCssThemeName(theme.name);
|
|
371
|
-
// Determine if this is the default
|
|
396
|
+
// Determine if this is the default theme
|
|
372
397
|
const isDefault = theme.isDefault;
|
|
373
|
-
|
|
374
|
-
// Generate main theme variant (.et-theme--{name})
|
|
398
|
+
// Generate color variant (.<runtimePrefix>-color--{name})
|
|
375
399
|
if (isDefault) {
|
|
376
|
-
// Default
|
|
377
|
-
const selectors = [':root', `.${
|
|
378
|
-
themeVars.push(`${selectors.join(', ')} {`);
|
|
379
|
-
}
|
|
380
|
-
else if (!isDefaultAlt) {
|
|
381
|
-
// Regular themes just get their own class
|
|
382
|
-
themeVars.push(`.${prefix}-theme--${name} {`);
|
|
383
|
-
}
|
|
384
|
-
if (isDefault || !isDefaultAlt) {
|
|
385
|
-
addThemeColorVariants(themeVars, prefix, '', theme);
|
|
386
|
-
themeVars.push('}\n');
|
|
387
|
-
}
|
|
388
|
-
// Generate alt theme variant (.et-theme-alt--{name})
|
|
389
|
-
if (isDefaultAlt) {
|
|
390
|
-
// Default alt theme gets :root and .et-theme-alt--default selectors
|
|
391
|
-
const selectors = [':root', `.${prefix}-theme-alt--default`, `.${prefix}-theme-alt--${name}`];
|
|
400
|
+
// Default color gets :root and .<runtimePrefix>-color--default selectors
|
|
401
|
+
const selectors = [':root', `.${runtimePrefix}-color--default`, `.${runtimePrefix}-color--${name}`];
|
|
392
402
|
themeVars.push(`${selectors.join(', ')} {`);
|
|
393
403
|
}
|
|
394
404
|
else {
|
|
395
|
-
//
|
|
396
|
-
themeVars.push(`.${
|
|
405
|
+
// Regular colors just get their own class
|
|
406
|
+
themeVars.push(`.${runtimePrefix}-color--${name} {`);
|
|
397
407
|
}
|
|
398
|
-
addThemeColorVariants(themeVars,
|
|
408
|
+
addThemeColorVariants(themeVars, runtimePrefix, '', theme);
|
|
399
409
|
themeVars.push('}\n');
|
|
400
410
|
});
|
|
411
|
+
// Convenience var aliases ā available on any element with a color class (or root for default)
|
|
412
|
+
const aliasBlock = `/* Convenience aliases (rgb + solid + opacity variants) */
|
|
413
|
+
@layer base {
|
|
414
|
+
:root, :where([class*="${runtimePrefix}-color--"]) {
|
|
415
|
+
--${runtimePrefix}-theme-color-primary-rgb: var(--${runtimePrefix}-color-primary);
|
|
416
|
+
--${runtimePrefix}-theme-color-primary-opacity: 1;
|
|
417
|
+
--${runtimePrefix}-theme-color-primary-solid: rgb(var(--${runtimePrefix}-theme-color-primary-rgb));
|
|
418
|
+
--${runtimePrefix}-theme-color-primary: rgb(var(--${runtimePrefix}-theme-color-primary-rgb) / var(--${runtimePrefix}-theme-color-primary-opacity));
|
|
419
|
+
|
|
420
|
+
--${runtimePrefix}-theme-color-on-primary-rgb: var(--${runtimePrefix}-color-on-primary);
|
|
421
|
+
--${runtimePrefix}-theme-color-on-primary-opacity: 1;
|
|
422
|
+
--${runtimePrefix}-theme-color-on-primary-solid: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
423
|
+
--${runtimePrefix}-theme-color-on-primary: rgb(var(--${runtimePrefix}-theme-color-on-primary-rgb) / var(--${runtimePrefix}-theme-color-on-primary-opacity));
|
|
424
|
+
|
|
425
|
+
--${runtimePrefix}-theme-color-ink-rgb: var(--${runtimePrefix}-color-primary-ink, var(--${runtimePrefix}-color-primary));
|
|
426
|
+
--${runtimePrefix}-theme-color-ink-opacity: 1;
|
|
427
|
+
--${runtimePrefix}-theme-color-ink-solid: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
428
|
+
--${runtimePrefix}-theme-color-ink: rgb(var(--${runtimePrefix}-theme-color-ink-rgb) / var(--${runtimePrefix}-theme-color-ink-opacity));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
`;
|
|
401
432
|
return `${header}@theme {
|
|
402
433
|
${tailwindVars.join('\n')}
|
|
403
434
|
}
|
|
404
435
|
|
|
405
|
-
|
|
436
|
+
@layer base {
|
|
437
|
+
${themeVars.join('\n')}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
${aliasBlock}`;
|
|
406
441
|
}
|
|
407
|
-
function addDynamicThemeColors(vars,
|
|
442
|
+
function addDynamicThemeColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName, addSpacingBefore) {
|
|
408
443
|
if (addSpacingBefore && vars.length > 0 && vars[vars.length - 1] !== '') {
|
|
409
444
|
vars.push('');
|
|
410
445
|
}
|
|
411
446
|
// Color variants
|
|
412
|
-
vars.push(` --color-${
|
|
413
|
-
vars.push(` --color-${
|
|
414
|
-
vars.push(` --color-${
|
|
415
|
-
vars.push(` --color-${
|
|
416
|
-
vars.push(` --color-${
|
|
447
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
448
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
449
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
450
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
451
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
417
452
|
vars.push('');
|
|
418
453
|
// On-color variants
|
|
419
|
-
vars.push(` --color-${
|
|
420
|
-
vars.push(` --color-${
|
|
421
|
-
vars.push(` --color-${
|
|
422
|
-
vars.push(` --color-${
|
|
423
|
-
vars.push(` --color-${
|
|
454
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}: rgb(var(--${runtimePrefix}-color-on-${cssVarName}));`);
|
|
455
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-hover));`);
|
|
456
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-focus));`);
|
|
457
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-active));`);
|
|
458
|
+
vars.push(` --color-${utilityPrefix}-on-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-on-${cssVarName}-disabled));`);
|
|
459
|
+
vars.push('');
|
|
460
|
+
}
|
|
461
|
+
function addDynamicInkColors(vars, utilityPrefix, runtimePrefix, tailwindName, cssVarName) {
|
|
462
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}: rgb(var(--${runtimePrefix}-color-${cssVarName}));`);
|
|
463
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-hover: rgb(var(--${runtimePrefix}-color-${cssVarName}-hover));`);
|
|
464
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-focus: rgb(var(--${runtimePrefix}-color-${cssVarName}-focus));`);
|
|
465
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-active: rgb(var(--${runtimePrefix}-color-${cssVarName}-active));`);
|
|
466
|
+
vars.push(` --color-${utilityPrefix}-${tailwindName}-disabled: rgb(var(--${runtimePrefix}-color-${cssVarName}-disabled));`);
|
|
424
467
|
vars.push('');
|
|
425
468
|
}
|
|
426
469
|
function addTailwindColorVariants(vars, colorName, colorSet) {
|
|
@@ -465,6 +508,17 @@ function addThemeColorVariants(vars, prefix, altPrefix, theme) {
|
|
|
465
508
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-focus: ${onFocusColor};`);
|
|
466
509
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-active: ${onActiveColor};`);
|
|
467
510
|
vars.push(` --${prefix}-color-${altPrefix}on-${level}-disabled: ${onDisabledColor};`);
|
|
511
|
+
const inkDefaultColor = swatch.inkColor?.default || defaultColor;
|
|
512
|
+
const inkHoverColor = swatch.inkColor?.hover || inkDefaultColor;
|
|
513
|
+
const inkFocusColor = swatch.inkColor?.focus || inkHoverColor;
|
|
514
|
+
const inkActiveColor = swatch.inkColor?.active || inkDefaultColor;
|
|
515
|
+
const inkDisabledColor = swatch.inkColor?.disabled || inkDefaultColor;
|
|
516
|
+
vars.push('');
|
|
517
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink: ${inkDefaultColor};`);
|
|
518
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-hover: ${inkHoverColor};`);
|
|
519
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-focus: ${inkFocusColor};`);
|
|
520
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-active: ${inkActiveColor};`);
|
|
521
|
+
vars.push(` --${prefix}-color-${altPrefix}${level}-ink-disabled: ${inkDisabledColor};`);
|
|
468
522
|
if (theme.secondary || theme.tertiary) {
|
|
469
523
|
vars.push('');
|
|
470
524
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
"skipFormat": {
|
|
34
|
+
"type": "boolean",
|
|
35
|
+
"description": "Skip formatting files after generation",
|
|
36
|
+
"default": false
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"required": ["themesPath"]
|
|
40
|
+
}
|