@shohojdhara/atomix 0.3.2 → 0.3.4
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/README.md +58 -21
- package/dist/atomix.css +96 -121
- package/dist/atomix.min.css +3 -3
- package/dist/index.d.ts +7937 -7765
- package/dist/index.esm.js +3677 -4031
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +3648 -3952
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/package.json +44 -16
- package/scripts/atomix-cli.js +1764 -0
- package/scripts/build-themes.js +208 -0
- package/scripts/cli/interactive-init.js +520 -0
- package/scripts/cli/migration-tools.js +603 -0
- package/scripts/cli/theme-bridge.js +129 -0
- package/scripts/cli/token-manager.js +519 -0
- package/scripts/sync-theme-config.js +309 -0
- package/src/components/Button/Button.tsx +36 -1
- package/src/components/List/ListGroup.tsx +1 -2
- package/src/components/Popover/Popover.tsx +2 -2
- package/src/components/Tooltip/Tooltip.stories.tsx +49 -12
- package/src/components/Tooltip/Tooltip.tsx +32 -58
- package/src/lib/composables/useTooltip.ts +285 -0
- package/src/lib/config/index.ts +275 -0
- package/src/lib/config/loader.ts +105 -0
- package/src/lib/constants/cssVariables.ts +390 -0
- package/src/lib/hooks/__tests__/useComponentCustomization.test.ts +151 -0
- package/src/lib/hooks/index.ts +19 -0
- package/src/lib/hooks/useComponentCustomization.ts +175 -0
- package/src/lib/index.ts +14 -1
- package/src/lib/patterns/__tests__/slots.test.ts +108 -0
- package/src/lib/patterns/index.ts +35 -0
- package/src/lib/patterns/slots.tsx +421 -0
- package/src/lib/theme/composeTheme.ts +0 -5
- package/src/lib/theme/config/index.ts +1 -1
- package/src/lib/theme/config/loader.ts +75 -41
- package/src/lib/theme/config/types.ts +21 -7
- package/src/lib/theme/config/validator.ts +1 -1
- package/src/lib/theme/constants.ts +12 -2
- package/src/lib/theme/createTheme.ts +2 -135
- package/src/lib/theme/createThemeFromConfig.ts +132 -0
- package/src/lib/theme/cssVariableMapper.ts +261 -0
- package/src/lib/theme/devtools/CLI.ts +161 -76
- package/src/lib/theme/devtools/Comparator.tsx +343 -0
- package/src/lib/theme/devtools/IMPROVEMENTS.md +429 -0
- package/src/lib/theme/devtools/Inspector.tsx +21 -6
- package/src/lib/theme/devtools/LiveEditor.tsx +393 -0
- package/src/lib/theme/devtools/README.md +433 -0
- package/src/lib/theme/devtools/index.ts +12 -11
- package/src/lib/theme/generateCSSVariables.ts +79 -38
- package/src/lib/theme/index.ts +45 -246
- package/src/lib/theme/runtime/ThemeApplicator.ts +252 -0
- package/src/lib/theme/runtime/ThemeManager.test.ts +17 -1
- package/src/lib/theme/runtime/ThemeManager.ts +7 -7
- package/src/lib/theme/themeUtils.ts +27 -5
- package/src/lib/theme/types.ts +59 -1
- package/src/lib/theme-tools.ts +125 -0
- package/src/lib/types/components.ts +260 -72
- package/src/lib/types/partProps.ts +426 -0
- package/src/lib/utils/__tests__/componentUtils.test.ts +144 -0
- package/src/lib/utils/componentUtils.ts +163 -0
- package/src/lib/utils/index.ts +17 -57
- package/src/styles/01-settings/_settings.colors.scss +10 -10
- package/src/styles/01-settings/_settings.navbar.scss +1 -1
- package/src/styles/01-settings/_settings.tooltip.scss +1 -1
- package/src/styles/03-generic/_generated-root.css +5 -0
- package/src/styles/06-components/_components.navbar.scss +12 -5
- package/src/styles/06-components/_components.tooltip.scss +31 -81
- package/src/themes/README.md +442 -0
- package/src/themes/themes.config.js +35 -0
- package/src/lib/theme/errors.test.ts +0 -207
- package/src/lib/theme/generators/CSSGenerator.ts +0 -311
- package/src/lib/theme/generators/ConfigGenerator.ts +0 -287
- package/src/lib/theme/generators/TypeGenerator.ts +0 -228
- package/src/lib/theme/generators/index.ts +0 -21
- package/src/lib/theme/monitoring/ThemeAnalytics.ts +0 -409
- package/src/lib/theme/monitoring/index.ts +0 -17
- package/src/lib/theme/overrides/ComponentOverrides.ts +0 -243
- package/src/lib/theme/overrides/index.ts +0 -15
- package/src/lib/theme/studio/ThemeStudio.tsx +0 -312
- package/src/lib/theme/studio/index.ts +0 -8
- package/src/lib/theme/whitelabel/WhiteLabelManager.ts +0 -364
- package/src/lib/theme/whitelabel/index.ts +0 -13
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme CLI Bridge
|
|
3
|
+
*
|
|
4
|
+
* Bridges the TypeScript theme devtools CLI with the main JavaScript CLI
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { spawn } from 'child_process';
|
|
8
|
+
import { join, dirname } from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Execute theme CLI command
|
|
18
|
+
*/
|
|
19
|
+
export async function executeThemeCommand(command, args = [], options = {}) {
|
|
20
|
+
const spinner = options.spinner || ora(`Running theme ${command}...`).start();
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
// Path to the theme CLI
|
|
24
|
+
const themeCliPath = join(__dirname, '../../src/lib/theme/devtools/CLI.ts');
|
|
25
|
+
|
|
26
|
+
// Use ts-node to execute TypeScript CLI
|
|
27
|
+
const tsNodePath = join(__dirname, '../../node_modules/.bin/ts-node');
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const child = spawn(tsNodePath, [themeCliPath, command, ...args], {
|
|
31
|
+
stdio: 'inherit',
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
child.on('close', (code) => {
|
|
36
|
+
if (code === 0) {
|
|
37
|
+
spinner.succeed(chalk.green(`✓ Theme ${command} completed`));
|
|
38
|
+
resolve();
|
|
39
|
+
} else {
|
|
40
|
+
spinner.fail(chalk.red(`✗ Theme ${command} failed`));
|
|
41
|
+
reject(new Error(`Theme command failed with code ${code}`));
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
child.on('error', (error) => {
|
|
46
|
+
spinner.fail(chalk.red(`✗ Theme ${command} failed`));
|
|
47
|
+
reject(error);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
spinner.fail(chalk.red(`✗ Theme ${command} failed`));
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create theme CLI bridge
|
|
58
|
+
*/
|
|
59
|
+
export function createThemeCLIBridge() {
|
|
60
|
+
return {
|
|
61
|
+
/**
|
|
62
|
+
* Validate theme configuration
|
|
63
|
+
*/
|
|
64
|
+
async validate(options = {}) {
|
|
65
|
+
const args = [];
|
|
66
|
+
if (options.config) args.push('--config', options.config);
|
|
67
|
+
if (options.strict) args.push('--strict');
|
|
68
|
+
|
|
69
|
+
return executeThemeCommand('validate', args, options);
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* List all themes
|
|
74
|
+
*/
|
|
75
|
+
async list(options = {}) {
|
|
76
|
+
return executeThemeCommand('list', [], options);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Inspect a theme
|
|
81
|
+
*/
|
|
82
|
+
async inspect(themeName, options = {}) {
|
|
83
|
+
const args = ['--theme', themeName];
|
|
84
|
+
if (options.json) args.push('--json');
|
|
85
|
+
|
|
86
|
+
return executeThemeCommand('inspect', args, options);
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compare two themes
|
|
91
|
+
*/
|
|
92
|
+
async compare(theme1, theme2, options = {}) {
|
|
93
|
+
const args = ['--theme1', theme1, '--theme2', theme2];
|
|
94
|
+
|
|
95
|
+
return executeThemeCommand('compare', args, options);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Export a theme
|
|
100
|
+
*/
|
|
101
|
+
async export(themeName, options = {}) {
|
|
102
|
+
const args = ['--theme', themeName];
|
|
103
|
+
if (options.output) args.push('--output', options.output);
|
|
104
|
+
|
|
105
|
+
return executeThemeCommand('export', args, options);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check if theme CLI is available
|
|
112
|
+
*/
|
|
113
|
+
export async function isThemeCLIAvailable() {
|
|
114
|
+
try {
|
|
115
|
+
const themeCliPath = join(__dirname, '../../src/lib/theme/devtools/CLI.ts');
|
|
116
|
+
const { access } = await import('fs/promises');
|
|
117
|
+
await access(themeCliPath);
|
|
118
|
+
return true;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export default {
|
|
125
|
+
createThemeCLIBridge,
|
|
126
|
+
executeThemeCommand,
|
|
127
|
+
isThemeCLIAvailable,
|
|
128
|
+
};
|
|
129
|
+
|
|
@@ -0,0 +1,519 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Design Token Manager for Atomix Design System
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFile, writeFile, readdir } from 'fs/promises';
|
|
6
|
+
import { join, basename } from 'path';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import boxen from 'boxen';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Token categories in the design system
|
|
14
|
+
*/
|
|
15
|
+
const tokenCategories = {
|
|
16
|
+
colors: {
|
|
17
|
+
path: 'src/styles/01-settings/_settings.colors.scss',
|
|
18
|
+
prefix: '--atomix-color',
|
|
19
|
+
description: 'Color palette tokens'
|
|
20
|
+
},
|
|
21
|
+
typography: {
|
|
22
|
+
path: 'src/styles/01-settings/_settings.typography.scss',
|
|
23
|
+
prefix: '--atomix-font',
|
|
24
|
+
description: 'Typography scale tokens'
|
|
25
|
+
},
|
|
26
|
+
spacing: {
|
|
27
|
+
path: 'src/styles/01-settings/_settings.spacing.scss',
|
|
28
|
+
prefix: '--atomix-space',
|
|
29
|
+
description: 'Spacing scale tokens'
|
|
30
|
+
},
|
|
31
|
+
radius: {
|
|
32
|
+
path: 'src/styles/01-settings/_settings.radius.scss',
|
|
33
|
+
prefix: '--atomix-radius',
|
|
34
|
+
description: 'Border radius tokens'
|
|
35
|
+
},
|
|
36
|
+
shadows: {
|
|
37
|
+
path: 'src/styles/01-settings/_settings.shadows.scss',
|
|
38
|
+
prefix: '--atomix-shadow',
|
|
39
|
+
description: 'Box shadow tokens'
|
|
40
|
+
},
|
|
41
|
+
breakpoints: {
|
|
42
|
+
path: 'src/styles/01-settings/_settings.breakpoints.scss',
|
|
43
|
+
prefix: '--atomix-breakpoint',
|
|
44
|
+
description: 'Responsive breakpoint tokens'
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract tokens from SCSS file
|
|
50
|
+
*/
|
|
51
|
+
async function extractTokensFromFile(filePath) {
|
|
52
|
+
if (!existsSync(filePath)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const content = await readFile(filePath, 'utf8');
|
|
57
|
+
const tokens = {};
|
|
58
|
+
|
|
59
|
+
// Extract SCSS variables
|
|
60
|
+
const scssVarPattern = /\$([a-z-]+):\s*([^;!]+)(?:\s*!default)?;/gi;
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = scssVarPattern.exec(content)) !== null) {
|
|
63
|
+
tokens[`$${match[1]}`] = {
|
|
64
|
+
type: 'scss',
|
|
65
|
+
name: match[1],
|
|
66
|
+
value: match[2].trim(),
|
|
67
|
+
line: content.substring(0, match.index).split('\n').length
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Extract CSS custom properties
|
|
72
|
+
const cssVarPattern = /--(atomix-[a-z-]+):\s*([^;]+);/gi;
|
|
73
|
+
while ((match = cssVarPattern.exec(content)) !== null) {
|
|
74
|
+
tokens[`--${match[1]}`] = {
|
|
75
|
+
type: 'css',
|
|
76
|
+
name: match[1],
|
|
77
|
+
value: match[2].trim(),
|
|
78
|
+
line: content.substring(0, match.index).split('\n').length
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return tokens;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* List all design tokens
|
|
87
|
+
*/
|
|
88
|
+
export async function listTokens(category = null) {
|
|
89
|
+
const spinner = ora('Loading design tokens...').start();
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const results = {};
|
|
93
|
+
|
|
94
|
+
// Get tokens from specified category or all categories
|
|
95
|
+
const categories = category
|
|
96
|
+
? [category]
|
|
97
|
+
: Object.keys(tokenCategories);
|
|
98
|
+
|
|
99
|
+
for (const cat of categories) {
|
|
100
|
+
if (!tokenCategories[cat]) {
|
|
101
|
+
spinner.warn(chalk.yellow(`Unknown category: ${cat}`));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const { path, description } = tokenCategories[cat];
|
|
106
|
+
const fullPath = join(process.cwd(), path);
|
|
107
|
+
const tokens = await extractTokensFromFile(fullPath);
|
|
108
|
+
|
|
109
|
+
if (tokens) {
|
|
110
|
+
results[cat] = {
|
|
111
|
+
description,
|
|
112
|
+
path: path,
|
|
113
|
+
tokens: tokens,
|
|
114
|
+
count: Object.keys(tokens).length
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
spinner.stop();
|
|
120
|
+
|
|
121
|
+
// Display results
|
|
122
|
+
if (Object.keys(results).length === 0) {
|
|
123
|
+
console.log(chalk.yellow('\n⚠️ No design tokens found'));
|
|
124
|
+
console.log(chalk.gray('Make sure you are in an Atomix project directory'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(chalk.bold.cyan('\n📐 Design Tokens\n'));
|
|
129
|
+
|
|
130
|
+
for (const [category, data] of Object.entries(results)) {
|
|
131
|
+
console.log(boxen(
|
|
132
|
+
chalk.bold(category.toUpperCase()) + '\n' +
|
|
133
|
+
chalk.gray(data.description) + '\n\n' +
|
|
134
|
+
chalk.cyan(`Tokens: ${data.count}\n`) +
|
|
135
|
+
chalk.gray(`File: ${data.path}`),
|
|
136
|
+
{
|
|
137
|
+
padding: 1,
|
|
138
|
+
margin: { top: 0, bottom: 1, left: 0, right: 0 },
|
|
139
|
+
borderStyle: 'round',
|
|
140
|
+
borderColor: 'gray'
|
|
141
|
+
}
|
|
142
|
+
));
|
|
143
|
+
|
|
144
|
+
// Show first 5 tokens as examples
|
|
145
|
+
const tokenEntries = Object.entries(data.tokens).slice(0, 5);
|
|
146
|
+
tokenEntries.forEach(([name, info]) => {
|
|
147
|
+
const value = info.value.length > 30
|
|
148
|
+
? info.value.substring(0, 30) + '...'
|
|
149
|
+
: info.value;
|
|
150
|
+
console.log(` ${chalk.green(name)}: ${chalk.white(value)}`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (Object.keys(data.tokens).length > 5) {
|
|
154
|
+
console.log(chalk.gray(` ... and ${Object.keys(data.tokens).length - 5} more\n`));
|
|
155
|
+
} else {
|
|
156
|
+
console.log();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return results;
|
|
161
|
+
|
|
162
|
+
} catch (error) {
|
|
163
|
+
spinner.fail(chalk.red('Failed to load tokens'));
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Validate design tokens
|
|
170
|
+
*/
|
|
171
|
+
export async function validateTokens(options = {}) {
|
|
172
|
+
const spinner = ora('Validating design tokens...').start();
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const issues = [];
|
|
176
|
+
const warnings = [];
|
|
177
|
+
let totalTokens = 0;
|
|
178
|
+
|
|
179
|
+
for (const [category, config] of Object.entries(tokenCategories)) {
|
|
180
|
+
const fullPath = join(process.cwd(), config.path);
|
|
181
|
+
|
|
182
|
+
if (!existsSync(fullPath)) {
|
|
183
|
+
issues.push({
|
|
184
|
+
category,
|
|
185
|
+
issue: 'Token file missing',
|
|
186
|
+
file: config.path,
|
|
187
|
+
fix: `Create file: ${config.path}`
|
|
188
|
+
});
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tokens = await extractTokensFromFile(fullPath);
|
|
193
|
+
if (tokens) {
|
|
194
|
+
totalTokens += Object.keys(tokens).length;
|
|
195
|
+
|
|
196
|
+
// Check for hardcoded values
|
|
197
|
+
const content = await readFile(fullPath, 'utf8');
|
|
198
|
+
|
|
199
|
+
// Check for hardcoded colors
|
|
200
|
+
if (category === 'colors') {
|
|
201
|
+
const hardcodedColors = content.match(/#[0-9a-fA-F]{3,8}(?![0-9a-fA-F])/g);
|
|
202
|
+
if (hardcodedColors) {
|
|
203
|
+
const uniqueColors = [...new Set(hardcodedColors)];
|
|
204
|
+
warnings.push({
|
|
205
|
+
category,
|
|
206
|
+
issue: `Found ${uniqueColors.length} hardcoded color values`,
|
|
207
|
+
values: uniqueColors.slice(0, 3),
|
|
208
|
+
fix: 'Use color tokens or CSS custom properties'
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Check for hardcoded pixel values
|
|
214
|
+
if (category === 'spacing' || category === 'typography') {
|
|
215
|
+
const hardcodedPixels = content.match(/\d+px/g);
|
|
216
|
+
if (hardcodedPixels) {
|
|
217
|
+
const uniquePixels = [...new Set(hardcodedPixels)];
|
|
218
|
+
warnings.push({
|
|
219
|
+
category,
|
|
220
|
+
issue: `Found ${uniquePixels.length} hardcoded pixel values`,
|
|
221
|
+
values: uniquePixels.slice(0, 3),
|
|
222
|
+
fix: 'Use spacing tokens or rem units'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Check for missing !default flags
|
|
228
|
+
const scssVars = content.match(/\$[a-z-]+:\s*[^;]+;/gi);
|
|
229
|
+
const defaultFlags = content.match(/!default/g);
|
|
230
|
+
if (scssVars && (!defaultFlags || defaultFlags.length < scssVars.length)) {
|
|
231
|
+
warnings.push({
|
|
232
|
+
category,
|
|
233
|
+
issue: 'Some SCSS variables missing !default flag',
|
|
234
|
+
fix: 'Add !default to allow theme overrides'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check naming conventions
|
|
239
|
+
for (const [name, info] of Object.entries(tokens)) {
|
|
240
|
+
if (info.type === 'css' && !name.startsWith(config.prefix)) {
|
|
241
|
+
warnings.push({
|
|
242
|
+
category,
|
|
243
|
+
issue: `Token "${name}" doesn't follow naming convention`,
|
|
244
|
+
fix: `Should start with "${config.prefix}"`
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
spinner.stop();
|
|
252
|
+
|
|
253
|
+
// Display validation results
|
|
254
|
+
console.log(chalk.bold.cyan('\n🔍 Token Validation Report\n'));
|
|
255
|
+
|
|
256
|
+
if (issues.length === 0 && warnings.length === 0) {
|
|
257
|
+
console.log(boxen(
|
|
258
|
+
chalk.bold.green('✅ All tokens valid!\n\n') +
|
|
259
|
+
chalk.gray(`Total tokens: ${totalTokens}\n`) +
|
|
260
|
+
chalk.gray('All naming conventions followed\n') +
|
|
261
|
+
chalk.gray('No hardcoded values found'),
|
|
262
|
+
{
|
|
263
|
+
padding: 1,
|
|
264
|
+
borderStyle: 'round',
|
|
265
|
+
borderColor: 'green'
|
|
266
|
+
}
|
|
267
|
+
));
|
|
268
|
+
} else {
|
|
269
|
+
if (issues.length > 0) {
|
|
270
|
+
console.log(chalk.bold.red(`❌ Issues (${issues.length}):\n`));
|
|
271
|
+
issues.forEach((issue, i) => {
|
|
272
|
+
console.log(chalk.red(` ${i + 1}. [${issue.category}] ${issue.issue}`));
|
|
273
|
+
console.log(chalk.gray(` File: ${issue.file}`));
|
|
274
|
+
console.log(chalk.yellow(` Fix: ${issue.fix}\n`));
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (warnings.length > 0) {
|
|
279
|
+
console.log(chalk.bold.yellow(`⚠️ Warnings (${warnings.length}):\n`));
|
|
280
|
+
warnings.forEach((warning, i) => {
|
|
281
|
+
console.log(chalk.yellow(` ${i + 1}. [${warning.category}] ${warning.issue}`));
|
|
282
|
+
if (warning.values) {
|
|
283
|
+
console.log(chalk.gray(` Examples: ${warning.values.join(', ')}`));
|
|
284
|
+
}
|
|
285
|
+
console.log(chalk.cyan(` Fix: ${warning.fix}\n`));
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
290
|
+
console.log(chalk.cyan('\n💡 Run with --fix to attempt automatic fixes'));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { issues, warnings, totalTokens };
|
|
294
|
+
|
|
295
|
+
} catch (error) {
|
|
296
|
+
spinner.fail(chalk.red('Validation failed'));
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Export tokens to different formats
|
|
303
|
+
*/
|
|
304
|
+
export async function exportTokens(format = 'json', outputPath = null) {
|
|
305
|
+
const spinner = ora(`Exporting tokens as ${format.toUpperCase()}...`).start();
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const allTokens = {};
|
|
309
|
+
|
|
310
|
+
// Collect all tokens
|
|
311
|
+
for (const [category, config] of Object.entries(tokenCategories)) {
|
|
312
|
+
const fullPath = join(process.cwd(), config.path);
|
|
313
|
+
const tokens = await extractTokensFromFile(fullPath);
|
|
314
|
+
|
|
315
|
+
if (tokens) {
|
|
316
|
+
allTokens[category] = {};
|
|
317
|
+
for (const [name, info] of Object.entries(tokens)) {
|
|
318
|
+
allTokens[category][name] = info.value;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
let output;
|
|
324
|
+
let filename;
|
|
325
|
+
|
|
326
|
+
switch (format.toLowerCase()) {
|
|
327
|
+
case 'json':
|
|
328
|
+
output = JSON.stringify(allTokens, null, 2);
|
|
329
|
+
filename = 'atomix-tokens.json';
|
|
330
|
+
break;
|
|
331
|
+
|
|
332
|
+
case 'css':
|
|
333
|
+
output = ':root {\n';
|
|
334
|
+
for (const [category, tokens] of Object.entries(allTokens)) {
|
|
335
|
+
output += ` /* ${category} */\n`;
|
|
336
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
337
|
+
if (name.startsWith('--')) {
|
|
338
|
+
output += ` ${name}: ${value};\n`;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
output += '}';
|
|
343
|
+
filename = 'atomix-tokens.css';
|
|
344
|
+
break;
|
|
345
|
+
|
|
346
|
+
case 'scss':
|
|
347
|
+
output = '// Atomix Design Tokens\n\n';
|
|
348
|
+
for (const [category, tokens] of Object.entries(allTokens)) {
|
|
349
|
+
output += `// ${category}\n`;
|
|
350
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
351
|
+
if (name.startsWith('$')) {
|
|
352
|
+
output += `${name}: ${value} !default;\n`;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
output += '\n';
|
|
356
|
+
}
|
|
357
|
+
filename = 'atomix-tokens.scss';
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case 'js':
|
|
361
|
+
case 'javascript':
|
|
362
|
+
output = '// Atomix Design Tokens\n';
|
|
363
|
+
output += 'export const tokens = ';
|
|
364
|
+
output += JSON.stringify(allTokens, null, 2);
|
|
365
|
+
output += ';\n\nexport default tokens;';
|
|
366
|
+
filename = 'atomix-tokens.js';
|
|
367
|
+
break;
|
|
368
|
+
|
|
369
|
+
case 'ts':
|
|
370
|
+
case 'typescript':
|
|
371
|
+
output = '// Atomix Design Tokens\n\n';
|
|
372
|
+
output += 'export interface AtomixTokens {\n';
|
|
373
|
+
for (const category of Object.keys(allTokens)) {
|
|
374
|
+
output += ` ${category}: Record<string, string>;\n`;
|
|
375
|
+
}
|
|
376
|
+
output += '}\n\n';
|
|
377
|
+
output += 'export const tokens: AtomixTokens = ';
|
|
378
|
+
output += JSON.stringify(allTokens, null, 2);
|
|
379
|
+
output += ';\n\nexport default tokens;';
|
|
380
|
+
filename = 'atomix-tokens.ts';
|
|
381
|
+
break;
|
|
382
|
+
|
|
383
|
+
default:
|
|
384
|
+
throw new Error(`Unsupported format: ${format}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Write to file
|
|
388
|
+
const finalPath = outputPath || filename;
|
|
389
|
+
await writeFile(finalPath, output, 'utf8');
|
|
390
|
+
|
|
391
|
+
spinner.succeed(chalk.green(`✓ Exported tokens to ${finalPath}`));
|
|
392
|
+
|
|
393
|
+
// Show summary
|
|
394
|
+
const categoryCount = Object.keys(allTokens).length;
|
|
395
|
+
const tokenCount = Object.values(allTokens).reduce(
|
|
396
|
+
(sum, cat) => sum + Object.keys(cat).length,
|
|
397
|
+
0
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
console.log(chalk.gray(`\n Categories: ${categoryCount}`));
|
|
401
|
+
console.log(chalk.gray(` Total tokens: ${tokenCount}`));
|
|
402
|
+
console.log(chalk.gray(` Format: ${format.toUpperCase()}`));
|
|
403
|
+
|
|
404
|
+
return { path: finalPath, tokens: allTokens };
|
|
405
|
+
|
|
406
|
+
} catch (error) {
|
|
407
|
+
spinner.fail(chalk.red('Export failed'));
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Import tokens from file
|
|
414
|
+
*/
|
|
415
|
+
export async function importTokens(filePath, options = {}) {
|
|
416
|
+
const spinner = ora('Importing design tokens...').start();
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
if (!existsSync(filePath)) {
|
|
420
|
+
throw new Error(`File not found: ${filePath}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const content = await readFile(filePath, 'utf8');
|
|
424
|
+
const extension = filePath.split('.').pop().toLowerCase();
|
|
425
|
+
|
|
426
|
+
let tokens;
|
|
427
|
+
|
|
428
|
+
// Parse tokens based on file type
|
|
429
|
+
switch (extension) {
|
|
430
|
+
case 'json':
|
|
431
|
+
tokens = JSON.parse(content);
|
|
432
|
+
break;
|
|
433
|
+
|
|
434
|
+
case 'js':
|
|
435
|
+
case 'ts':
|
|
436
|
+
// Extract tokens from JS/TS export
|
|
437
|
+
const match = content.match(/export\s+(?:const|default)\s+\w*\s*=\s*({[\s\S]*})/);
|
|
438
|
+
if (match) {
|
|
439
|
+
// Simple eval (be careful in production)
|
|
440
|
+
tokens = eval(`(${match[1]})`);
|
|
441
|
+
} else {
|
|
442
|
+
throw new Error('Could not parse JavaScript/TypeScript tokens');
|
|
443
|
+
}
|
|
444
|
+
break;
|
|
445
|
+
|
|
446
|
+
default:
|
|
447
|
+
throw new Error(`Unsupported file type: ${extension}`);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
spinner.text = 'Updating token files...';
|
|
451
|
+
|
|
452
|
+
// Update token files
|
|
453
|
+
for (const [category, categoryTokens] of Object.entries(tokens)) {
|
|
454
|
+
if (!tokenCategories[category]) {
|
|
455
|
+
console.warn(chalk.yellow(`\n⚠️ Unknown category: ${category} (skipped)`));
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const config = tokenCategories[category];
|
|
460
|
+
const fullPath = join(process.cwd(), config.path);
|
|
461
|
+
|
|
462
|
+
// Generate SCSS content
|
|
463
|
+
let scssContent = `// ${config.description}\n`;
|
|
464
|
+
scssContent += `// Imported from: ${basename(filePath)}\n`;
|
|
465
|
+
scssContent += `// Date: ${new Date().toISOString()}\n\n`;
|
|
466
|
+
|
|
467
|
+
for (const [name, value] of Object.entries(categoryTokens)) {
|
|
468
|
+
if (name.startsWith('$')) {
|
|
469
|
+
scssContent += `${name}: ${value} !default;\n`;
|
|
470
|
+
} else if (name.startsWith('--')) {
|
|
471
|
+
scssContent += `:root {\n ${name}: ${value};\n}\n`;
|
|
472
|
+
} else {
|
|
473
|
+
// Convert to SCSS variable
|
|
474
|
+
scssContent += `$${name}: ${value} !default;\n`;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (options.dryRun) {
|
|
479
|
+
console.log(chalk.yellow(`\n Would update: ${config.path}`));
|
|
480
|
+
console.log(chalk.gray(scssContent.substring(0, 200) + '...'));
|
|
481
|
+
} else {
|
|
482
|
+
await writeFile(fullPath, scssContent, 'utf8');
|
|
483
|
+
console.log(chalk.green(` ✓ Updated ${config.path}`));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
spinner.succeed(chalk.green('✓ Tokens imported successfully'));
|
|
488
|
+
|
|
489
|
+
// Show summary
|
|
490
|
+
const categoryCount = Object.keys(tokens).length;
|
|
491
|
+
const tokenCount = Object.values(tokens).reduce(
|
|
492
|
+
(sum, cat) => sum + Object.keys(cat).length,
|
|
493
|
+
0
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
console.log(chalk.gray(`\n Categories imported: ${categoryCount}`));
|
|
497
|
+
console.log(chalk.gray(` Total tokens: ${tokenCount}`));
|
|
498
|
+
|
|
499
|
+
if (!options.dryRun) {
|
|
500
|
+
console.log(chalk.cyan('\n💡 Next steps:'));
|
|
501
|
+
console.log(chalk.gray(' 1. Review the updated token files'));
|
|
502
|
+
console.log(chalk.gray(' 2. Rebuild your themes: atomix build-theme <theme>'));
|
|
503
|
+
console.log(chalk.gray(' 3. Test your components with new tokens'));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return { tokens, categoryCount, tokenCount };
|
|
507
|
+
|
|
508
|
+
} catch (error) {
|
|
509
|
+
spinner.fail(chalk.red('Import failed'));
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export default {
|
|
515
|
+
listTokens,
|
|
516
|
+
validateTokens,
|
|
517
|
+
exportTokens,
|
|
518
|
+
importTokens
|
|
519
|
+
};
|