@ethlete/core 5.0.0-next.7 → 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.
@@ -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}`);
@@ -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
  `;
@@ -376,29 +376,29 @@ function generateTailwindThemeCss(themes, prefix, schema) {
376
376
  // Determine if this is the default or default-alt theme
377
377
  const isDefault = theme.isDefault;
378
378
  const isDefaultAlt = theme.isDefaultAlt;
379
- // Generate main theme variant (.et-theme--{name})
379
+ // Generate main color variant (.et-color--{name})
380
380
  if (isDefault) {
381
- // Default theme gets :root and .et-theme--default selectors
382
- const selectors = [':root', `.${prefix}-theme--default`, `.${prefix}-theme--${name}`];
381
+ // Default color gets :root and .et-color--default selectors
382
+ const selectors = [':root', `.${prefix}-color--default`, `.${prefix}-color--${name}`];
383
383
  themeVars.push(`${selectors.join(', ')} {`);
384
384
  }
385
385
  else if (!isDefaultAlt) {
386
- // Regular themes just get their own class
387
- themeVars.push(`.${prefix}-theme--${name} {`);
386
+ // Regular colors just get their own class
387
+ themeVars.push(`.${prefix}-color--${name} {`);
388
388
  }
389
389
  if (isDefault || !isDefaultAlt) {
390
390
  addThemeColorVariants(themeVars, prefix, '', theme);
391
391
  themeVars.push('}\n');
392
392
  }
393
- // Generate alt theme variant (.et-theme-alt--{name})
393
+ // Generate alt color variant (.et-color-alt--{name})
394
394
  if (isDefaultAlt) {
395
- // Default alt theme gets :root and .et-theme-alt--default selectors
396
- const selectors = [':root', `.${prefix}-theme-alt--default`, `.${prefix}-theme-alt--${name}`];
395
+ // Default alt color gets :root and .et-color-alt--default selectors
396
+ const selectors = [':root', `.${prefix}-color-alt--default`, `.${prefix}-color-alt--${name}`];
397
397
  themeVars.push(`${selectors.join(', ')} {`);
398
398
  }
399
399
  else {
400
- // All themes (including default and regular) get an alt variant
401
- themeVars.push(`.${prefix}-theme-alt--${name} {`);
400
+ // All colors (including default and regular) get an alt variant
401
+ themeVars.push(`.${prefix}-color-alt--${name} {`);
402
402
  }
403
403
  addThemeColorVariants(themeVars, prefix, 'alt-', theme);
404
404
  themeVars.push('}\n');
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/schema",
3
- "$id": "tailwind-4-theme",
3
+ "$id": "tailwind-4-color-theme",
4
4
  "title": "Tailwind 4 Theming Generator",
5
5
  "description": "Generates Tailwind 4 compatible CSS from your Ethlete theme definitions",
6
6
  "type": "object",
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/core",
3
- "version": "5.0.0-next.7",
3
+ "version": "5.0.0-next.8",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {