@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.
@@ -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 theme variant (.et-theme--{name})
379
+ // Generate main color variant (.et-color--{name})
375
380
  if (isDefault) {
376
- // Default theme gets :root and .et-theme--default selectors
377
- 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}`];
378
383
  themeVars.push(`${selectors.join(', ')} {`);
379
384
  }
380
385
  else if (!isDefaultAlt) {
381
- // Regular themes just get their own class
382
- themeVars.push(`.${prefix}-theme--${name} {`);
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 theme variant (.et-theme-alt--{name})
393
+ // Generate alt color variant (.et-color-alt--{name})
389
394
  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}`];
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 themes (including default and regular) get an alt variant
396
- themeVars.push(`.${prefix}-theme-alt--${name} {`);
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
  }
@@ -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.6",
3
+ "version": "5.0.0-next.8",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {