@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
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
* Command-line interface for theme management
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { generateConfigTemplate } from '../generators/ConfigGenerator';
|
|
8
|
-
import { generateCSS } from '../generators/CSSGenerator';
|
|
9
|
-
import { generateTypes } from '../generators/TypeGenerator';
|
|
10
7
|
import { loadThemeConfig } from '../config/loader';
|
|
11
8
|
import { validateConfig } from '../config/validator';
|
|
12
9
|
|
|
@@ -37,39 +34,49 @@ export class ThemeCLI {
|
|
|
37
34
|
*/
|
|
38
35
|
private registerDefaultCommands(): void {
|
|
39
36
|
this.register({
|
|
40
|
-
name: '
|
|
41
|
-
description: '
|
|
37
|
+
name: 'validate',
|
|
38
|
+
description: 'Validate theme configuration',
|
|
42
39
|
options: {
|
|
43
|
-
'--
|
|
44
|
-
'--
|
|
40
|
+
'--config': 'Path to config file',
|
|
41
|
+
'--strict': 'Enable strict validation',
|
|
45
42
|
},
|
|
46
|
-
handler: this.
|
|
43
|
+
handler: this.handleValidate.bind(this),
|
|
47
44
|
});
|
|
48
45
|
|
|
49
46
|
this.register({
|
|
50
|
-
name: '
|
|
51
|
-
description: '
|
|
52
|
-
handler: this.
|
|
47
|
+
name: 'list',
|
|
48
|
+
description: 'List all available themes',
|
|
49
|
+
handler: this.handleList.bind(this),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
this.register({
|
|
53
|
+
name: 'inspect',
|
|
54
|
+
description: 'Inspect a specific theme',
|
|
55
|
+
options: {
|
|
56
|
+
'--theme': 'Theme name to inspect',
|
|
57
|
+
'--json': 'Output as JSON',
|
|
58
|
+
},
|
|
59
|
+
handler: this.handleInspect.bind(this),
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
this.register({
|
|
56
|
-
name: '
|
|
57
|
-
description: '
|
|
63
|
+
name: 'compare',
|
|
64
|
+
description: 'Compare two themes',
|
|
58
65
|
options: {
|
|
59
|
-
'--
|
|
60
|
-
'--
|
|
66
|
+
'--theme1': 'First theme name',
|
|
67
|
+
'--theme2': 'Second theme name',
|
|
61
68
|
},
|
|
62
|
-
handler: this.
|
|
69
|
+
handler: this.handleCompare.bind(this),
|
|
63
70
|
});
|
|
64
71
|
|
|
65
72
|
this.register({
|
|
66
|
-
name: '
|
|
67
|
-
description: '
|
|
73
|
+
name: 'export',
|
|
74
|
+
description: 'Export theme to JSON',
|
|
68
75
|
options: {
|
|
69
|
-
'--
|
|
70
|
-
'--
|
|
76
|
+
'--theme': 'Theme name to export',
|
|
77
|
+
'--output': 'Output file path',
|
|
71
78
|
},
|
|
72
|
-
handler: this.
|
|
79
|
+
handler: this.handleExport.bind(this),
|
|
73
80
|
});
|
|
74
81
|
|
|
75
82
|
this.register({
|
|
@@ -139,29 +146,6 @@ export class ThemeCLI {
|
|
|
139
146
|
return { args: parsedArgs, options };
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
/**
|
|
143
|
-
* Handle init command
|
|
144
|
-
*/
|
|
145
|
-
private handleInit(args: string[], options: Record<string, any>): void {
|
|
146
|
-
const format = options.format || 'typescript';
|
|
147
|
-
const includeExamples = options.examples !== false;
|
|
148
|
-
|
|
149
|
-
const config = generateConfigTemplate({
|
|
150
|
-
format: format as any,
|
|
151
|
-
includeExamples,
|
|
152
|
-
includeComments: true,
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
const filename = format === 'json' ? 'theme.config.json' :
|
|
156
|
-
format === 'javascript' ? 'theme.config.js' :
|
|
157
|
-
'theme.config.ts';
|
|
158
|
-
|
|
159
|
-
console.log(`Generating ${filename}...`);
|
|
160
|
-
console.log(config);
|
|
161
|
-
console.log(`\\nTheme configuration template generated!`);
|
|
162
|
-
console.log(`Save this content to ${filename} in your project root.`);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
149
|
/**
|
|
166
150
|
* Handle validate command
|
|
167
151
|
*/
|
|
@@ -173,16 +157,16 @@ export class ThemeCLI {
|
|
|
173
157
|
if (result.valid) {
|
|
174
158
|
console.log('✅ Theme configuration is valid');
|
|
175
159
|
if (result.warnings.length > 0) {
|
|
176
|
-
console.log('
|
|
160
|
+
console.log('\n⚠️ Warnings:');
|
|
177
161
|
result.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
178
162
|
}
|
|
179
163
|
} else {
|
|
180
164
|
console.log('❌ Theme configuration is invalid');
|
|
181
|
-
console.log('
|
|
165
|
+
console.log('\nErrors:');
|
|
182
166
|
result.errors.forEach(error => console.log(` - ${error}`));
|
|
183
|
-
|
|
167
|
+
|
|
184
168
|
if (result.warnings.length > 0) {
|
|
185
|
-
console.log('
|
|
169
|
+
console.log('\nWarnings:');
|
|
186
170
|
result.warnings.forEach(warning => console.log(` - ${warning}`));
|
|
187
171
|
}
|
|
188
172
|
process.exit(1);
|
|
@@ -194,49 +178,150 @@ export class ThemeCLI {
|
|
|
194
178
|
}
|
|
195
179
|
|
|
196
180
|
/**
|
|
197
|
-
* Handle
|
|
181
|
+
* Handle list command
|
|
198
182
|
*/
|
|
199
|
-
private
|
|
200
|
-
console.log('Building themes...');
|
|
201
|
-
console.log('Note: This is a placeholder. Implement actual build logic based on your needs.');
|
|
202
|
-
|
|
183
|
+
private handleList(args: string[], options: Record<string, any>): void {
|
|
203
184
|
try {
|
|
204
185
|
const config = loadThemeConfig();
|
|
205
|
-
|
|
186
|
+
const themes = config.themes || {};
|
|
206
187
|
|
|
207
|
-
|
|
208
|
-
// 1. Load each theme
|
|
209
|
-
// 2. Generate CSS for CSS themes
|
|
210
|
-
// 3. Execute createTheme for JS themes and generate CSS
|
|
211
|
-
// 4. Write files to output directory
|
|
188
|
+
console.log('Available Themes:\n');
|
|
212
189
|
|
|
213
|
-
|
|
190
|
+
if (Object.keys(themes).length === 0) {
|
|
191
|
+
console.log('No themes found in configuration.');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const [id, theme] of Object.entries(themes)) {
|
|
196
|
+
console.log(` ${id}`);
|
|
197
|
+
console.log(` Name: ${theme.name}`);
|
|
198
|
+
if (theme.description) {
|
|
199
|
+
console.log(` Description: ${theme.description}`);
|
|
200
|
+
}
|
|
201
|
+
if (theme.version) {
|
|
202
|
+
console.log(` Version: ${theme.version}`);
|
|
203
|
+
}
|
|
204
|
+
if (theme.status) {
|
|
205
|
+
console.log(` Status: ${theme.status}`);
|
|
206
|
+
}
|
|
207
|
+
console.log();
|
|
208
|
+
}
|
|
214
209
|
} catch (error) {
|
|
215
|
-
console.error('
|
|
210
|
+
console.error('Failed to list themes:', error);
|
|
216
211
|
process.exit(1);
|
|
217
212
|
}
|
|
218
213
|
}
|
|
219
214
|
|
|
220
215
|
/**
|
|
221
|
-
* Handle
|
|
216
|
+
* Handle inspect command
|
|
222
217
|
*/
|
|
223
|
-
private
|
|
224
|
-
|
|
225
|
-
console.log('Note: This is a placeholder. Implement actual type generation logic.');
|
|
218
|
+
private handleInspect(args: string[], options: Record<string, any>): void {
|
|
219
|
+
const themeName = options.theme || args[0];
|
|
226
220
|
|
|
221
|
+
if (!themeName) {
|
|
222
|
+
console.error('Error: Theme name is required');
|
|
223
|
+
console.error('Usage: atomix-theme inspect --theme <theme-name>');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const config = loadThemeConfig();
|
|
229
|
+
const theme = config.themes?.[themeName];
|
|
230
|
+
|
|
231
|
+
if (!theme) {
|
|
232
|
+
console.error(`Error: Theme "${themeName}" not found`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (options.json) {
|
|
237
|
+
console.log(JSON.stringify(theme, null, 2));
|
|
238
|
+
} else {
|
|
239
|
+
console.log(`Theme: ${themeName}\n`);
|
|
240
|
+
console.log(JSON.stringify(theme, null, 2));
|
|
241
|
+
}
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error('Failed to inspect theme:', error);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Handle compare command
|
|
250
|
+
*/
|
|
251
|
+
private handleCompare(args: string[], options: Record<string, any>): void {
|
|
252
|
+
const theme1 = options.theme1 || args[0];
|
|
253
|
+
const theme2 = options.theme2 || args[1];
|
|
254
|
+
|
|
255
|
+
if (!theme1 || !theme2) {
|
|
256
|
+
console.error('Error: Two theme names are required');
|
|
257
|
+
console.error('Usage: atomix-theme compare --theme1 <name1> --theme2 <name2>');
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
227
261
|
try {
|
|
228
262
|
const config = loadThemeConfig();
|
|
229
|
-
const
|
|
263
|
+
const themeA = config.themes?.[theme1];
|
|
264
|
+
const themeB = config.themes?.[theme2];
|
|
265
|
+
|
|
266
|
+
if (!themeA) {
|
|
267
|
+
console.error(`Error: Theme "${theme1}" not found`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!themeB) {
|
|
272
|
+
console.error(`Error: Theme "${theme2}" not found`);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
console.log(`Comparing: ${theme1} vs ${theme2}\n`);
|
|
277
|
+
console.log('Differences:');
|
|
230
278
|
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
// 2. Generate TypeScript definitions
|
|
234
|
-
// 3. Write to output file
|
|
279
|
+
// Simple comparison (could be enhanced)
|
|
280
|
+
const keys = new Set([...Object.keys(themeA), ...Object.keys(themeB)]);
|
|
235
281
|
|
|
236
|
-
|
|
237
|
-
|
|
282
|
+
for (const key of keys) {
|
|
283
|
+
const valueA = (themeA as any)[key];
|
|
284
|
+
const valueB = (themeB as any)[key];
|
|
285
|
+
|
|
286
|
+
if (JSON.stringify(valueA) !== JSON.stringify(valueB)) {
|
|
287
|
+
console.log(`\n ${key}:`);
|
|
288
|
+
console.log(` ${theme1}: ${JSON.stringify(valueA)}`);
|
|
289
|
+
console.log(` ${theme2}: ${JSON.stringify(valueB)}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
238
292
|
} catch (error) {
|
|
239
|
-
console.error('
|
|
293
|
+
console.error('Failed to compare themes:', error);
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Handle export command
|
|
300
|
+
*/
|
|
301
|
+
private handleExport(args: string[], options: Record<string, any>): void {
|
|
302
|
+
const themeName = options.theme || args[0];
|
|
303
|
+
const outputPath = options.output || `${themeName}.json`;
|
|
304
|
+
|
|
305
|
+
if (!themeName) {
|
|
306
|
+
console.error('Error: Theme name is required');
|
|
307
|
+
console.error('Usage: atomix-theme export --theme <theme-name> [--output <path>]');
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const config = loadThemeConfig();
|
|
313
|
+
const theme = config.themes?.[themeName];
|
|
314
|
+
|
|
315
|
+
if (!theme) {
|
|
316
|
+
console.error(`Error: Theme "${themeName}" not found`);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const fs = require('fs');
|
|
321
|
+
fs.writeFileSync(outputPath, JSON.stringify(theme, null, 2));
|
|
322
|
+
console.log(`✅ Theme exported to: ${outputPath}`);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
console.error('Failed to export theme:', error);
|
|
240
325
|
process.exit(1);
|
|
241
326
|
}
|
|
242
327
|
}
|
|
@@ -245,13 +330,13 @@ export class ThemeCLI {
|
|
|
245
330
|
* Handle help command
|
|
246
331
|
*/
|
|
247
332
|
private handleHelp(args: string[], options: Record<string, any>): void {
|
|
248
|
-
console.log('Atomix Theme CLI
|
|
249
|
-
console.log('Usage: atomix-theme <command> [options]
|
|
333
|
+
console.log('Atomix Theme CLI\n');
|
|
334
|
+
console.log('Usage: atomix-theme <command> [options]\n');
|
|
250
335
|
console.log('Commands:');
|
|
251
336
|
|
|
252
337
|
for (const [name, command] of this.commands.entries()) {
|
|
253
338
|
console.log(` ${name.padEnd(12)} ${command.description}`);
|
|
254
|
-
|
|
339
|
+
|
|
255
340
|
if (command.options) {
|
|
256
341
|
for (const [option, description] of Object.entries(command.options)) {
|
|
257
342
|
console.log(` ${option.padEnd(16)} ${description}`);
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme Comparator Component
|
|
3
|
+
*
|
|
4
|
+
* React component for comparing two themes side-by-side
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useMemo } from 'react';
|
|
8
|
+
import type { Theme } from '../types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Theme comparator props
|
|
12
|
+
*/
|
|
13
|
+
export interface ThemeComparatorProps {
|
|
14
|
+
/** First theme to compare */
|
|
15
|
+
themeA: Theme;
|
|
16
|
+
/** Second theme to compare */
|
|
17
|
+
themeB: Theme;
|
|
18
|
+
/** Show only differences */
|
|
19
|
+
showOnlyDifferences?: boolean;
|
|
20
|
+
/** CSS class name */
|
|
21
|
+
className?: string;
|
|
22
|
+
/** Inline styles */
|
|
23
|
+
style?: React.CSSProperties;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface Difference {
|
|
27
|
+
path: string;
|
|
28
|
+
valueA: any;
|
|
29
|
+
valueB: any;
|
|
30
|
+
type: 'added' | 'removed' | 'changed';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Theme Comparator Component
|
|
35
|
+
*
|
|
36
|
+
* Compares two themes and highlights differences
|
|
37
|
+
*/
|
|
38
|
+
export const ThemeComparator: React.FC<ThemeComparatorProps> = ({
|
|
39
|
+
themeA,
|
|
40
|
+
themeB,
|
|
41
|
+
showOnlyDifferences = false,
|
|
42
|
+
className,
|
|
43
|
+
style,
|
|
44
|
+
}) => {
|
|
45
|
+
const differences = useMemo(() => {
|
|
46
|
+
const diffs: Difference[] = [];
|
|
47
|
+
|
|
48
|
+
const compareObjects = (objA: any, objB: any, path: string = '') => {
|
|
49
|
+
const keysA = Object.keys(objA || {});
|
|
50
|
+
const keysB = Object.keys(objB || {});
|
|
51
|
+
const allKeys = new Set([...keysA, ...keysB]);
|
|
52
|
+
|
|
53
|
+
for (const key of allKeys) {
|
|
54
|
+
if (key === '__isJSTheme') continue;
|
|
55
|
+
|
|
56
|
+
const currentPath = path ? `${path}.${key}` : key;
|
|
57
|
+
const valueA = objA?.[key];
|
|
58
|
+
const valueB = objB?.[key];
|
|
59
|
+
|
|
60
|
+
if (valueA === undefined && valueB !== undefined) {
|
|
61
|
+
diffs.push({
|
|
62
|
+
path: currentPath,
|
|
63
|
+
valueA: undefined,
|
|
64
|
+
valueB,
|
|
65
|
+
type: 'added',
|
|
66
|
+
});
|
|
67
|
+
} else if (valueA !== undefined && valueB === undefined) {
|
|
68
|
+
diffs.push({
|
|
69
|
+
path: currentPath,
|
|
70
|
+
valueA,
|
|
71
|
+
valueB: undefined,
|
|
72
|
+
type: 'removed',
|
|
73
|
+
});
|
|
74
|
+
} else if (typeof valueA === 'object' && typeof valueB === 'object' && !Array.isArray(valueA) && !Array.isArray(valueB)) {
|
|
75
|
+
compareObjects(valueA, valueB, currentPath);
|
|
76
|
+
} else if (JSON.stringify(valueA) !== JSON.stringify(valueB)) {
|
|
77
|
+
diffs.push({
|
|
78
|
+
path: currentPath,
|
|
79
|
+
valueA,
|
|
80
|
+
valueB,
|
|
81
|
+
type: 'changed',
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
compareObjects(themeA, themeB);
|
|
88
|
+
return diffs;
|
|
89
|
+
}, [themeA, themeB]);
|
|
90
|
+
|
|
91
|
+
const formatValue = (value: any): string => {
|
|
92
|
+
if (value === undefined) return 'undefined';
|
|
93
|
+
if (value === null) return 'null';
|
|
94
|
+
if (typeof value === 'string') return `"${value}"`;
|
|
95
|
+
if (typeof value === 'object') return JSON.stringify(value, null, 2);
|
|
96
|
+
return String(value);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getTypeColor = (type: Difference['type']): string => {
|
|
100
|
+
switch (type) {
|
|
101
|
+
case 'added': return '#4caf50';
|
|
102
|
+
case 'removed': return '#f44336';
|
|
103
|
+
case 'changed': return '#ff9800';
|
|
104
|
+
default: return '#666';
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div className={`atomix-theme-comparator ${className || ''}`} style={style}>
|
|
110
|
+
<div className="comparator-header">
|
|
111
|
+
<div className="theme-column">
|
|
112
|
+
<h3>{themeA.name}</h3>
|
|
113
|
+
{themeA.version && <span className="version">v{themeA.version}</span>}
|
|
114
|
+
</div>
|
|
115
|
+
<div className="vs-divider">VS</div>
|
|
116
|
+
<div className="theme-column">
|
|
117
|
+
<h3>{themeB.name}</h3>
|
|
118
|
+
{themeB.version && <span className="version">v{themeB.version}</span>}
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div className="comparator-stats">
|
|
123
|
+
<div className="stat">
|
|
124
|
+
<span className="stat-value">{differences.length}</span>
|
|
125
|
+
<span className="stat-label">Total Differences</span>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="stat">
|
|
128
|
+
<span className="stat-value" style={{ color: '#4caf50' }}>
|
|
129
|
+
{differences.filter(d => d.type === 'added').length}
|
|
130
|
+
</span>
|
|
131
|
+
<span className="stat-label">Added</span>
|
|
132
|
+
</div>
|
|
133
|
+
<div className="stat">
|
|
134
|
+
<span className="stat-value" style={{ color: '#f44336' }}>
|
|
135
|
+
{differences.filter(d => d.type === 'removed').length}
|
|
136
|
+
</span>
|
|
137
|
+
<span className="stat-label">Removed</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div className="stat">
|
|
140
|
+
<span className="stat-value" style={{ color: '#ff9800' }}>
|
|
141
|
+
{differences.filter(d => d.type === 'changed').length}
|
|
142
|
+
</span>
|
|
143
|
+
<span className="stat-label">Changed</span>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{differences.length === 0 ? (
|
|
148
|
+
<div className="no-differences">
|
|
149
|
+
✅ Themes are identical
|
|
150
|
+
</div>
|
|
151
|
+
) : (
|
|
152
|
+
<div className="differences-list">
|
|
153
|
+
<h4>Differences</h4>
|
|
154
|
+
{differences.map((diff, index) => (
|
|
155
|
+
<div key={index} className="difference-item">
|
|
156
|
+
<div className="difference-header">
|
|
157
|
+
<span
|
|
158
|
+
className="difference-type"
|
|
159
|
+
style={{ backgroundColor: getTypeColor(diff.type) }}
|
|
160
|
+
>
|
|
161
|
+
{diff.type}
|
|
162
|
+
</span>
|
|
163
|
+
<code className="difference-path">{diff.path}</code>
|
|
164
|
+
</div>
|
|
165
|
+
<div className="difference-values">
|
|
166
|
+
<div className="value-column">
|
|
167
|
+
<div className="value-label">{themeA.name}</div>
|
|
168
|
+
<pre className="value-content">
|
|
169
|
+
{formatValue(diff.valueA)}
|
|
170
|
+
</pre>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="value-divider">→</div>
|
|
173
|
+
<div className="value-column">
|
|
174
|
+
<div className="value-label">{themeB.name}</div>
|
|
175
|
+
<pre className="value-content">
|
|
176
|
+
{formatValue(diff.valueB)}
|
|
177
|
+
</pre>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
))}
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
<style>{`
|
|
186
|
+
.atomix-theme-comparator {
|
|
187
|
+
border: 1px solid #e0e0e0;
|
|
188
|
+
border-radius: 8px;
|
|
189
|
+
background: white;
|
|
190
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
191
|
+
padding: 24px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.comparator-header {
|
|
195
|
+
display: grid;
|
|
196
|
+
grid-template-columns: 1fr auto 1fr;
|
|
197
|
+
gap: 24px;
|
|
198
|
+
align-items: center;
|
|
199
|
+
margin-bottom: 24px;
|
|
200
|
+
padding-bottom: 24px;
|
|
201
|
+
border-bottom: 2px solid #e0e0e0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.theme-column {
|
|
205
|
+
text-align: center;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.theme-column h3 {
|
|
209
|
+
margin: 0;
|
|
210
|
+
font-size: 20px;
|
|
211
|
+
color: #333;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.version {
|
|
215
|
+
display: inline-block;
|
|
216
|
+
margin-top: 4px;
|
|
217
|
+
padding: 2px 8px;
|
|
218
|
+
background: #e3f2fd;
|
|
219
|
+
color: #1976d2;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
font-size: 12px;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.vs-divider {
|
|
225
|
+
font-weight: bold;
|
|
226
|
+
font-size: 24px;
|
|
227
|
+
color: #666;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.comparator-stats {
|
|
231
|
+
display: grid;
|
|
232
|
+
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
|
233
|
+
gap: 16px;
|
|
234
|
+
margin-bottom: 24px;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.stat {
|
|
238
|
+
text-align: center;
|
|
239
|
+
padding: 16px;
|
|
240
|
+
background: #f5f5f5;
|
|
241
|
+
border-radius: 8px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.stat-value {
|
|
245
|
+
display: block;
|
|
246
|
+
font-size: 32px;
|
|
247
|
+
font-weight: bold;
|
|
248
|
+
color: #2196f3;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.stat-label {
|
|
252
|
+
display: block;
|
|
253
|
+
font-size: 12px;
|
|
254
|
+
color: #666;
|
|
255
|
+
margin-top: 4px;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
.no-differences {
|
|
259
|
+
text-align: center;
|
|
260
|
+
padding: 48px;
|
|
261
|
+
font-size: 18px;
|
|
262
|
+
color: #4caf50;
|
|
263
|
+
background: #e8f5e9;
|
|
264
|
+
border-radius: 8px;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.differences-list h4 {
|
|
268
|
+
margin: 0 0 16px 0;
|
|
269
|
+
font-size: 18px;
|
|
270
|
+
color: #333;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.difference-item {
|
|
274
|
+
margin-bottom: 16px;
|
|
275
|
+
border: 1px solid #e0e0e0;
|
|
276
|
+
border-radius: 8px;
|
|
277
|
+
overflow: hidden;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.difference-header {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 12px;
|
|
284
|
+
padding: 12px;
|
|
285
|
+
background: #f5f5f5;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.difference-type {
|
|
289
|
+
padding: 4px 8px;
|
|
290
|
+
border-radius: 4px;
|
|
291
|
+
font-size: 11px;
|
|
292
|
+
font-weight: bold;
|
|
293
|
+
color: white;
|
|
294
|
+
text-transform: uppercase;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.difference-path {
|
|
298
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
299
|
+
font-size: 13px;
|
|
300
|
+
color: #1976d2;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.difference-values {
|
|
304
|
+
display: grid;
|
|
305
|
+
grid-template-columns: 1fr auto 1fr;
|
|
306
|
+
gap: 16px;
|
|
307
|
+
padding: 16px;
|
|
308
|
+
align-items: start;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.value-column {
|
|
312
|
+
min-width: 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.value-label {
|
|
316
|
+
font-size: 12px;
|
|
317
|
+
font-weight: bold;
|
|
318
|
+
color: #666;
|
|
319
|
+
margin-bottom: 8px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.value-content {
|
|
323
|
+
margin: 0;
|
|
324
|
+
padding: 12px;
|
|
325
|
+
background: #f5f5f5;
|
|
326
|
+
border-radius: 4px;
|
|
327
|
+
font-family: 'Monaco', 'Menlo', monospace;
|
|
328
|
+
font-size: 12px;
|
|
329
|
+
overflow-x: auto;
|
|
330
|
+
white-space: pre-wrap;
|
|
331
|
+
word-break: break-all;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.value-divider {
|
|
335
|
+
font-size: 20px;
|
|
336
|
+
color: #666;
|
|
337
|
+
align-self: center;
|
|
338
|
+
}
|
|
339
|
+
`}</style>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
};
|
|
343
|
+
|