omgkit 2.29.0 → 2.31.0

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.
@@ -0,0 +1,755 @@
1
+ /**
2
+ * OMGKIT Theme V2 Processing Library
3
+ * Handles v2 schema processing: color scales, $ref resolution, effects, animations
4
+ *
5
+ * @module lib/theme-v2
6
+ */
7
+
8
+ import { REQUIRED_COLORS, OPTIONAL_COLORS, V2_EXTENDED_TOKENS, V2_STATUS_COLORS } from './theme.js';
9
+
10
+ /**
11
+ * Maximum depth for $ref resolution to prevent infinite loops
12
+ */
13
+ const MAX_REF_DEPTH = 10;
14
+
15
+ /**
16
+ * Resolve a $ref value in theme
17
+ * @param {any} value - Value to resolve (may contain $ref)
18
+ * @param {Object} theme - Full theme object for lookups
19
+ * @param {Set} visited - Set of visited paths (for circular detection)
20
+ * @param {number} depth - Current recursion depth
21
+ * @returns {string} Resolved value
22
+ * @throws {Error} If circular reference or invalid path detected
23
+ */
24
+ export function resolveReference(value, theme, visited = new Set(), depth = 0) {
25
+ // Not a reference, return as-is
26
+ if (typeof value !== 'object' || value === null) {
27
+ return value;
28
+ }
29
+
30
+ // No $ref property, return as-is
31
+ if (!value.$ref) {
32
+ return value;
33
+ }
34
+
35
+ const refPath = value.$ref;
36
+
37
+ // Depth check
38
+ if (depth >= MAX_REF_DEPTH) {
39
+ throw new Error(`Maximum reference depth exceeded at: ${refPath}`);
40
+ }
41
+
42
+ // Circular reference detection
43
+ if (visited.has(refPath)) {
44
+ throw new Error(`Circular reference detected: ${refPath}`);
45
+ }
46
+ visited.add(refPath);
47
+
48
+ // Security: Prevent prototype pollution
49
+ const dangerousPaths = ['__proto__', 'constructor', 'prototype'];
50
+ const parts = refPath.split('.');
51
+ for (const part of parts) {
52
+ if (dangerousPaths.includes(part)) {
53
+ throw new Error(`Invalid reference path (security): ${refPath}`);
54
+ }
55
+ }
56
+
57
+ // Navigate to referenced value
58
+ let current = theme;
59
+ for (const part of parts) {
60
+ if (current === undefined || current === null) {
61
+ throw new Error(`Invalid reference path: ${refPath} (${part} not found)`);
62
+ }
63
+ current = current[part];
64
+ }
65
+
66
+ if (current === undefined) {
67
+ throw new Error(`Reference not found: ${refPath}`);
68
+ }
69
+
70
+ // Recursively resolve if result is also a $ref
71
+ return resolveReference(current, theme, visited, depth + 1);
72
+ }
73
+
74
+ /**
75
+ * Resolve all $refs in a token set
76
+ * @param {Object} tokenSet - Token set with potential $refs
77
+ * @param {Object} theme - Full theme for reference resolution
78
+ * @returns {Object} Resolved token set
79
+ */
80
+ export function resolveTokenSet(tokenSet, theme) {
81
+ if (!tokenSet || typeof tokenSet !== 'object') {
82
+ return tokenSet;
83
+ }
84
+
85
+ const resolved = {};
86
+ for (const [key, value] of Object.entries(tokenSet)) {
87
+ try {
88
+ resolved[key] = resolveReference(value, theme);
89
+ } catch (error) {
90
+ console.warn(`Warning: Could not resolve ${key}: ${error.message}`);
91
+ resolved[key] = value; // Keep original if resolution fails
92
+ }
93
+ }
94
+ return resolved;
95
+ }
96
+
97
+ /**
98
+ * Expand color scales to flat CSS variables
99
+ * @param {Object} scales - Color scales object from theme
100
+ * @param {string} mode - 'light' or 'dark'
101
+ * @returns {Object} Flat object of color variables
102
+ */
103
+ export function expandColorScales(scales, mode = 'light') {
104
+ const result = {};
105
+
106
+ if (!scales) return result;
107
+
108
+ for (const [scaleName, scale] of Object.entries(scales)) {
109
+ const colorName = scale.name || scaleName;
110
+
111
+ // Expand steps (1-12)
112
+ if (scale.steps && scale.steps[mode]) {
113
+ for (let i = 1; i <= 12; i++) {
114
+ const stepValue = scale.steps[mode][i] || scale.steps[mode][String(i)];
115
+ if (stepValue) {
116
+ result[`${colorName}-${i}`] = stepValue;
117
+ }
118
+ }
119
+ }
120
+
121
+ // Expand alpha variants (a1-a12)
122
+ if (scale.alpha && scale.alpha[mode]) {
123
+ for (let i = 1; i <= 12; i++) {
124
+ const alphaValue = scale.alpha[mode][i] || scale.alpha[mode][String(i)];
125
+ if (alphaValue) {
126
+ result[`${colorName}-a${i}`] = alphaValue;
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ return result;
133
+ }
134
+
135
+ /**
136
+ * Process effects into CSS variables
137
+ * @param {Object} effects - Effects object from theme
138
+ * @param {Object} theme - Full theme for $ref resolution
139
+ * @returns {Object} CSS variables for effects
140
+ */
141
+ export function processEffects(effects, theme) {
142
+ const result = {};
143
+
144
+ if (!effects) return result;
145
+
146
+ // Glassmorphism
147
+ if (effects.glassMorphism) {
148
+ if (effects.glassMorphism.background) {
149
+ const bg = resolveReference(effects.glassMorphism.background, theme);
150
+ result['glass-background'] = bg;
151
+ }
152
+ if (effects.glassMorphism.backdropBlur) {
153
+ result['glass-blur'] = effects.glassMorphism.backdropBlur;
154
+ }
155
+ }
156
+
157
+ // Glow
158
+ if (effects.glow) {
159
+ if (effects.glow.default) {
160
+ result['glow'] = effects.glow.default;
161
+ }
162
+ if (effects.glow.lg) {
163
+ result['glow-lg'] = effects.glow.lg;
164
+ }
165
+ if (effects.glow.color) {
166
+ const color = resolveReference(effects.glow.color, theme);
167
+ result['glow-color'] = color;
168
+ }
169
+ }
170
+
171
+ // Gradients
172
+ if (effects.gradient) {
173
+ for (const [name, gradient] of Object.entries(effects.gradient)) {
174
+ if (gradient.from) {
175
+ const from = resolveReference(gradient.from, theme);
176
+ result[`gradient-${name}-from`] = from;
177
+ }
178
+ if (gradient.to) {
179
+ const to = resolveReference(gradient.to, theme);
180
+ result[`gradient-${name}-to`] = to;
181
+ }
182
+ if (gradient.direction) {
183
+ result[`gradient-${name}-direction`] = gradient.direction;
184
+ }
185
+ }
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * Process animations into CSS keyframes and animations
193
+ * @param {Object} animations - Animations object from theme
194
+ * @returns {Object} { keyframes: {...}, animations: {...} }
195
+ */
196
+ export function processAnimations(animations) {
197
+ const keyframes = {};
198
+ const animationDefs = {};
199
+
200
+ if (!animations) return { keyframes, animations: animationDefs };
201
+
202
+ for (const [name, animation] of Object.entries(animations)) {
203
+ // Process keyframes
204
+ if (animation.keyframes) {
205
+ keyframes[name] = animation.keyframes;
206
+ }
207
+
208
+ // Build animation shorthand
209
+ const parts = [];
210
+ parts.push(name); // animation-name
211
+ parts.push(animation.duration || '0.2s');
212
+ parts.push(animation.easing || 'ease-out');
213
+ if (animation.iteration) {
214
+ parts.push(animation.iteration);
215
+ }
216
+
217
+ animationDefs[name] = parts.join(' ');
218
+ }
219
+
220
+ return { keyframes, animations: animationDefs };
221
+ }
222
+
223
+ /**
224
+ * Migrate V1 theme to V2 format
225
+ * @param {Object} v1Theme - V1 theme object
226
+ * @returns {Object} V2 theme object
227
+ */
228
+ export function migrateV1ToV2(v1Theme) {
229
+ if (!v1Theme) {
230
+ throw new Error('Cannot migrate null/undefined theme');
231
+ }
232
+
233
+ // Already v2?
234
+ if (v1Theme.version === '2.0' || v1Theme.scales) {
235
+ return v1Theme;
236
+ }
237
+
238
+ return {
239
+ version: '2.0',
240
+ name: v1Theme.name,
241
+ id: v1Theme.id,
242
+ category: v1Theme.category,
243
+ description: v1Theme.description || '',
244
+
245
+ colorSystem: {
246
+ type: 'semantic',
247
+ version: '1.0'
248
+ },
249
+
250
+ // V1 has no scales
251
+ scales: {},
252
+
253
+ // Map v1 colors to semanticTokens
254
+ semanticTokens: {
255
+ light: {
256
+ ...v1Theme.colors?.light,
257
+ // Add v2 tokens with sensible defaults from v1
258
+ 'surface': v1Theme.colors?.light?.muted || v1Theme.colors?.light?.background,
259
+ 'surface-hover': v1Theme.colors?.light?.accent || v1Theme.colors?.light?.muted,
260
+ 'surface-active': v1Theme.colors?.light?.secondary || v1Theme.colors?.light?.muted,
261
+ 'primary-hover': v1Theme.colors?.light?.primary,
262
+ 'secondary-hover': v1Theme.colors?.light?.secondary,
263
+ 'accent-hover': v1Theme.colors?.light?.accent,
264
+ 'border-hover': v1Theme.colors?.light?.input || v1Theme.colors?.light?.border,
265
+ 'input-hover': v1Theme.colors?.light?.input,
266
+ 'ring-offset': '0 0% 100%',
267
+ 'panel': v1Theme.colors?.light?.muted || v1Theme.colors?.light?.background,
268
+ 'panel-translucent': v1Theme.colors?.light?.muted + ' / 0.8',
269
+ 'overlay': '0 0% 0% / 0.4'
270
+ },
271
+ dark: {
272
+ ...v1Theme.colors?.dark,
273
+ 'surface': v1Theme.colors?.dark?.muted || v1Theme.colors?.dark?.background,
274
+ 'surface-hover': v1Theme.colors?.dark?.accent || v1Theme.colors?.dark?.muted,
275
+ 'surface-active': v1Theme.colors?.dark?.secondary || v1Theme.colors?.dark?.muted,
276
+ 'primary-hover': v1Theme.colors?.dark?.primary,
277
+ 'secondary-hover': v1Theme.colors?.dark?.secondary,
278
+ 'accent-hover': v1Theme.colors?.dark?.accent,
279
+ 'border-hover': v1Theme.colors?.dark?.input || v1Theme.colors?.dark?.border,
280
+ 'input-hover': v1Theme.colors?.dark?.input,
281
+ 'ring-offset': '0 0% 0%',
282
+ 'panel': v1Theme.colors?.dark?.muted || v1Theme.colors?.dark?.background,
283
+ 'panel-translucent': v1Theme.colors?.dark?.muted + ' / 0.8',
284
+ 'overlay': '0 0% 0% / 0.6'
285
+ }
286
+ },
287
+
288
+ // Status colors (use destructive from v1, add defaults for others)
289
+ statusColors: {
290
+ light: {
291
+ destructive: v1Theme.colors?.light?.destructive || '0 84.2% 60.2%',
292
+ 'destructive-foreground': v1Theme.colors?.light?.['destructive-foreground'] || '0 0% 98%',
293
+ success: '151 55% 42%',
294
+ 'success-foreground': '0 0% 100%',
295
+ warning: '39 100% 62%',
296
+ 'warning-foreground': '39 40% 20%',
297
+ info: '206 100% 50%',
298
+ 'info-foreground': '0 0% 100%'
299
+ },
300
+ dark: {
301
+ destructive: v1Theme.colors?.dark?.destructive || '0 62.8% 30.6%',
302
+ 'destructive-foreground': v1Theme.colors?.dark?.['destructive-foreground'] || '0 0% 98%',
303
+ success: '151 50% 45%',
304
+ 'success-foreground': '0 0% 100%',
305
+ warning: '39 90% 55%',
306
+ 'warning-foreground': '39 80% 10%',
307
+ info: '206 90% 55%',
308
+ 'info-foreground': '0 0% 100%'
309
+ }
310
+ },
311
+
312
+ // Chart colors from v1 if available
313
+ chartColors: {
314
+ '1': v1Theme.colors?.light?.['chart-1'] || v1Theme.colors?.light?.primary,
315
+ '2': v1Theme.colors?.light?.['chart-2'] || '206 100% 50%',
316
+ '3': v1Theme.colors?.light?.['chart-3'] || '151 55% 42%',
317
+ '4': v1Theme.colors?.light?.['chart-4'] || '39 100% 62%',
318
+ '5': v1Theme.colors?.light?.['chart-5'] || '0 84.2% 60.2%'
319
+ },
320
+
321
+ // Sidebar tokens from v1 if available
322
+ sidebarTokens: {
323
+ light: {
324
+ background: v1Theme.colors?.light?.['sidebar-background'] || '0 0% 98%',
325
+ foreground: v1Theme.colors?.light?.['sidebar-foreground'] || v1Theme.colors?.light?.['muted-foreground'],
326
+ primary: v1Theme.colors?.light?.['sidebar-primary'] || v1Theme.colors?.light?.primary,
327
+ 'primary-foreground': v1Theme.colors?.light?.['sidebar-primary-foreground'] || '0 0% 98%',
328
+ accent: v1Theme.colors?.light?.['sidebar-accent'] || v1Theme.colors?.light?.muted,
329
+ 'accent-foreground': v1Theme.colors?.light?.['sidebar-accent-foreground'] || v1Theme.colors?.light?.foreground,
330
+ border: v1Theme.colors?.light?.['sidebar-border'] || v1Theme.colors?.light?.border,
331
+ ring: v1Theme.colors?.light?.['sidebar-ring'] || v1Theme.colors?.light?.ring
332
+ },
333
+ dark: {
334
+ background: v1Theme.colors?.dark?.['sidebar-background'] || '0 0% 5%',
335
+ foreground: v1Theme.colors?.dark?.['sidebar-foreground'] || v1Theme.colors?.dark?.['muted-foreground'],
336
+ primary: v1Theme.colors?.dark?.['sidebar-primary'] || v1Theme.colors?.dark?.primary,
337
+ 'primary-foreground': v1Theme.colors?.dark?.['sidebar-primary-foreground'] || '0 0% 98%',
338
+ accent: v1Theme.colors?.dark?.['sidebar-accent'] || v1Theme.colors?.dark?.muted,
339
+ 'accent-foreground': v1Theme.colors?.dark?.['sidebar-accent-foreground'] || v1Theme.colors?.dark?.foreground,
340
+ border: v1Theme.colors?.dark?.['sidebar-border'] || v1Theme.colors?.dark?.border,
341
+ ring: v1Theme.colors?.dark?.['sidebar-ring'] || v1Theme.colors?.dark?.ring
342
+ }
343
+ },
344
+
345
+ // Typography
346
+ typography: {
347
+ fontFamily: {
348
+ sans: v1Theme.fontFamily?.sans || 'Inter, system-ui, sans-serif',
349
+ mono: v1Theme.fontFamily?.mono || 'JetBrains Mono, monospace'
350
+ },
351
+ fontFeatureSettings: '"rlig" 1, "calt" 1'
352
+ },
353
+
354
+ // Spacing
355
+ spacing: {
356
+ radius: v1Theme.radius || '0.5rem',
357
+ radiusLg: 'var(--radius)',
358
+ radiusMd: 'calc(var(--radius) - 2px)',
359
+ radiusSm: 'calc(var(--radius) - 4px)'
360
+ },
361
+
362
+ // Empty effects/animations for migrated themes
363
+ effects: {},
364
+ animations: {},
365
+
366
+ // Keep v1 colors for backward compatibility
367
+ colors: v1Theme.colors,
368
+ radius: v1Theme.radius,
369
+ fontFamily: v1Theme.fontFamily
370
+ };
371
+ }
372
+
373
+ /**
374
+ * Process a V2 theme into flat CSS variables
375
+ * @param {Object} theme - V2 theme object
376
+ * @param {string} mode - 'light' or 'dark'
377
+ * @returns {Object} Flat object of CSS variables
378
+ */
379
+ export function processV2Theme(theme, mode = 'light') {
380
+ const variables = {};
381
+
382
+ // 1. Expand color scales
383
+ if (theme.scales) {
384
+ const scaleVars = expandColorScales(theme.scales, mode);
385
+ Object.assign(variables, scaleVars);
386
+ }
387
+
388
+ // 2. Resolve semantic tokens
389
+ if (theme.semanticTokens && theme.semanticTokens[mode]) {
390
+ const resolvedTokens = resolveTokenSet(theme.semanticTokens[mode], theme);
391
+ Object.assign(variables, resolvedTokens);
392
+ }
393
+
394
+ // 3. Add status colors
395
+ if (theme.statusColors && theme.statusColors[mode]) {
396
+ Object.assign(variables, theme.statusColors[mode]);
397
+ }
398
+
399
+ // 4. Add chart colors (mode-independent)
400
+ if (theme.chartColors) {
401
+ for (const [num, value] of Object.entries(theme.chartColors)) {
402
+ const resolved = resolveReference(value, theme);
403
+ variables[`chart-${num}`] = resolved;
404
+ }
405
+ }
406
+
407
+ // 5. Add sidebar tokens
408
+ if (theme.sidebarTokens && theme.sidebarTokens[mode]) {
409
+ const sidebarTokens = resolveTokenSet(theme.sidebarTokens[mode], theme);
410
+ for (const [key, value] of Object.entries(sidebarTokens)) {
411
+ variables[`sidebar-${key}`] = value;
412
+ }
413
+ }
414
+
415
+ // 6. Process effects
416
+ if (theme.effects) {
417
+ const effectVars = processEffects(theme.effects, theme);
418
+ Object.assign(variables, effectVars);
419
+ }
420
+
421
+ return variables;
422
+ }
423
+
424
+ /**
425
+ * Get all CSS variables needed for a V2 theme
426
+ * @param {Object} theme - V2 theme object
427
+ * @returns {{ light: Object, dark: Object }} Variables for both modes
428
+ */
429
+ export function getV2CSSVariables(theme) {
430
+ return {
431
+ light: processV2Theme(theme, 'light'),
432
+ dark: processV2Theme(theme, 'dark')
433
+ };
434
+ }
435
+
436
+ /**
437
+ * Validate V2 theme structure
438
+ * @param {Object} theme - Theme to validate
439
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }}
440
+ */
441
+ export function validateV2Theme(theme) {
442
+ const errors = [];
443
+ const warnings = [];
444
+
445
+ if (!theme) {
446
+ return { valid: false, errors: ['Theme is null or undefined'], warnings };
447
+ }
448
+
449
+ // Required fields
450
+ if (!theme.name) errors.push('Missing required field: name');
451
+ if (!theme.id) errors.push('Missing required field: id');
452
+ if (!theme.category) errors.push('Missing required field: category');
453
+
454
+ // ID format
455
+ if (theme.id && !/^[a-z0-9-]+$/.test(theme.id)) {
456
+ errors.push('Invalid id format: must be kebab-case');
457
+ }
458
+
459
+ // Version
460
+ if (theme.version && theme.version !== '2.0') {
461
+ warnings.push(`Unexpected version: ${theme.version} (expected 2.0)`);
462
+ }
463
+
464
+ // Scales validation
465
+ if (theme.scales) {
466
+ for (const [scaleName, scale] of Object.entries(theme.scales)) {
467
+ if (!scale.name) {
468
+ warnings.push(`Scale ${scaleName} missing name property`);
469
+ }
470
+ if (!scale.steps?.light || !scale.steps?.dark) {
471
+ errors.push(`Scale ${scaleName} missing light or dark steps`);
472
+ }
473
+ }
474
+ }
475
+
476
+ // Semantic tokens validation
477
+ if (theme.semanticTokens) {
478
+ if (!theme.semanticTokens.light) {
479
+ errors.push('Missing semanticTokens.light');
480
+ }
481
+ if (!theme.semanticTokens.dark) {
482
+ errors.push('Missing semanticTokens.dark');
483
+ }
484
+ }
485
+
486
+ // Test $ref resolution
487
+ if (theme.semanticTokens?.light) {
488
+ for (const [key, value] of Object.entries(theme.semanticTokens.light)) {
489
+ if (typeof value === 'object' && value.$ref) {
490
+ try {
491
+ resolveReference(value, theme);
492
+ } catch (error) {
493
+ errors.push(`Invalid $ref in semanticTokens.light.${key}: ${error.message}`);
494
+ }
495
+ }
496
+ }
497
+ }
498
+
499
+ return {
500
+ valid: errors.length === 0,
501
+ errors,
502
+ warnings
503
+ };
504
+ }
505
+
506
+ /**
507
+ * Get required V2 tokens that must be present
508
+ * @returns {string[]} List of required token names
509
+ */
510
+ export function getRequiredV2Tokens() {
511
+ return [
512
+ ...REQUIRED_COLORS,
513
+ ...V2_EXTENDED_TOKENS,
514
+ ...V2_STATUS_COLORS
515
+ ];
516
+ }
517
+
518
+ /**
519
+ * Generate CSS keyframes string from animation definitions
520
+ * @param {Object} keyframes - Keyframes object from processAnimations
521
+ * @returns {string} CSS keyframes rules
522
+ */
523
+ export function generateKeyframesCSS(keyframes) {
524
+ let css = '';
525
+
526
+ for (const [name, frames] of Object.entries(keyframes)) {
527
+ css += `@keyframes ${name} {\n`;
528
+ for (const [key, properties] of Object.entries(frames)) {
529
+ css += ` ${key} {\n`;
530
+ for (const [prop, value] of Object.entries(properties)) {
531
+ // Convert camelCase to kebab-case
532
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
533
+ css += ` ${kebabProp}: ${value};\n`;
534
+ }
535
+ css += ` }\n`;
536
+ }
537
+ css += `}\n\n`;
538
+ }
539
+
540
+ return css;
541
+ }
542
+
543
+ /**
544
+ * Generate V2 theme CSS with all features
545
+ * @param {Object} theme - V2 theme object
546
+ * @returns {string} Complete CSS content
547
+ */
548
+ export function generateV2ThemeCSS(theme) {
549
+ const { light: lightVars, dark: darkVars } = getV2CSSVariables(theme);
550
+
551
+ // Generate CSS variable declarations
552
+ const generateVarDeclarations = (vars) => {
553
+ let css = '';
554
+ for (const [key, value] of Object.entries(vars)) {
555
+ css += ` --${key}: ${value};\n`;
556
+ }
557
+ return css;
558
+ };
559
+
560
+ // Process animations
561
+ const { keyframes, animations } = processAnimations(theme.animations || {});
562
+
563
+ // Generate animation CSS variables
564
+ const animationVars = Object.entries(animations)
565
+ .map(([name, value]) => ` --animation-${name}: ${value};`)
566
+ .join('\n');
567
+
568
+ // Generate keyframes CSS
569
+ const keyframesCSS = generateKeyframesCSS(keyframes);
570
+
571
+ // Generate typography CSS
572
+ const fontSans = theme.typography?.fontFamily?.sans || 'Inter, system-ui, sans-serif';
573
+ const fontMono = theme.typography?.fontFamily?.mono || 'JetBrains Mono, monospace';
574
+ const fontFeatureSettings = theme.typography?.fontFeatureSettings || '"rlig" 1, "calt" 1';
575
+
576
+ // Generate spacing CSS
577
+ const radius = theme.spacing?.radius || theme.radius || '0.5rem';
578
+
579
+ const lightVarsStr = generateVarDeclarations(lightVars);
580
+ const darkVarsStr = generateVarDeclarations(darkVars);
581
+
582
+ return `/* OMGKIT Theme: ${theme.name} */
583
+ /* Theme ID: ${theme.id} */
584
+ /* Category: ${theme.category} */
585
+ /* Version: 2.0 */
586
+ /* Generated by OMGKIT Design System v2 */
587
+
588
+ ${keyframesCSS}
589
+ @layer base {
590
+ :root {
591
+ ${lightVarsStr} --radius: ${radius};
592
+ --font-sans: ${fontSans};
593
+ --font-mono: ${fontMono};
594
+ --font-feature-settings: ${fontFeatureSettings};
595
+ ${animationVars ? animationVars + '\n' : ''}}
596
+
597
+ .dark {
598
+ ${darkVarsStr} }
599
+ }
600
+
601
+ @layer base {
602
+ * {
603
+ @apply border-border;
604
+ }
605
+ body {
606
+ @apply bg-background text-foreground;
607
+ font-family: var(--font-sans);
608
+ font-feature-settings: var(--font-feature-settings);
609
+ }
610
+ }
611
+ `;
612
+ }
613
+
614
+ /**
615
+ * Unified theme processor - handles both v1 and v2 themes
616
+ * @param {Object} theme - Theme object (v1 or v2)
617
+ * @param {Object} options - Processing options
618
+ * @param {boolean} options.forceV2 - Force v2 processing (migrate v1 if needed)
619
+ * @param {string} options.mode - 'light' or 'dark' (for partial processing)
620
+ * @returns {{ version: string, css: string, variables: Object, theme: Object }}
621
+ */
622
+ export function processTheme(theme, options = {}) {
623
+ const { forceV2 = false, mode = null } = options;
624
+
625
+ if (!theme) {
626
+ throw new Error('Theme is required');
627
+ }
628
+
629
+ // Import dynamically to avoid circular dependency
630
+ const isV2 = theme.version === '2.0' ||
631
+ theme.scales ||
632
+ theme.semanticTokens ||
633
+ theme.effects ||
634
+ theme.animations ||
635
+ theme.colorSystem ||
636
+ theme.statusColors;
637
+
638
+ if (isV2 || forceV2) {
639
+ // Process as v2
640
+ const v2Theme = isV2 ? theme : migrateV1ToV2(theme);
641
+ const variables = mode
642
+ ? { [mode]: processV2Theme(v2Theme, mode) }
643
+ : getV2CSSVariables(v2Theme);
644
+ const css = generateV2ThemeCSS(v2Theme);
645
+
646
+ return {
647
+ version: '2.0',
648
+ css,
649
+ variables,
650
+ theme: v2Theme
651
+ };
652
+ } else {
653
+ // Process as v1 (use existing generateThemeCSS pattern)
654
+ const variables = {
655
+ light: theme.colors?.light || {},
656
+ dark: theme.colors?.dark || {}
657
+ };
658
+
659
+ // Generate v1 CSS (simple format)
660
+ const generateColorVars = (colors) => {
661
+ let css = '';
662
+ for (const [key, value] of Object.entries(colors)) {
663
+ css += ` --${key}: ${value};\n`;
664
+ }
665
+ return css;
666
+ };
667
+
668
+ const lightVars = generateColorVars(theme.colors?.light || {});
669
+ const darkVars = generateColorVars(theme.colors?.dark || {});
670
+
671
+ const css = `/* OMGKIT Theme: ${theme.name} */
672
+ /* Theme ID: ${theme.id} */
673
+ /* Category: ${theme.category} */
674
+ /* Version: 1.0 */
675
+ /* Generated by OMGKIT Design System */
676
+
677
+ @layer base {
678
+ :root {
679
+ ${lightVars} --radius: ${theme.radius || '0.5rem'};
680
+ }
681
+
682
+ .dark {
683
+ ${darkVars} }
684
+ }
685
+
686
+ @layer base {
687
+ * {
688
+ @apply border-border;
689
+ }
690
+ body {
691
+ @apply bg-background text-foreground;
692
+ }
693
+ }
694
+ `;
695
+
696
+ return {
697
+ version: '1.0',
698
+ css,
699
+ variables,
700
+ theme
701
+ };
702
+ }
703
+ }
704
+
705
+ /**
706
+ * Get a flat list of all CSS variable names for a theme
707
+ * @param {Object} theme - Theme object (v1 or v2)
708
+ * @returns {string[]} Array of variable names (without -- prefix)
709
+ */
710
+ export function getThemeVariableNames(theme) {
711
+ const result = processTheme(theme);
712
+ const names = new Set();
713
+
714
+ for (const modeVars of Object.values(result.variables)) {
715
+ for (const key of Object.keys(modeVars)) {
716
+ names.add(key);
717
+ }
718
+ }
719
+
720
+ return Array.from(names);
721
+ }
722
+
723
+ /**
724
+ * Compare two themes and return differences
725
+ * @param {Object} themeA - First theme
726
+ * @param {Object} themeB - Second theme
727
+ * @returns {{ added: string[], removed: string[], changed: string[] }}
728
+ */
729
+ export function compareThemes(themeA, themeB) {
730
+ const varsA = getThemeVariableNames(themeA);
731
+ const varsB = getThemeVariableNames(themeB);
732
+
733
+ const setA = new Set(varsA);
734
+ const setB = new Set(varsB);
735
+
736
+ const added = varsB.filter(v => !setA.has(v));
737
+ const removed = varsA.filter(v => !setB.has(v));
738
+
739
+ // Check for changed values (in light mode)
740
+ const resultA = processTheme(themeA);
741
+ const resultB = processTheme(themeB);
742
+ const changed = [];
743
+
744
+ for (const key of varsA) {
745
+ if (setB.has(key)) {
746
+ const valA = resultA.variables.light?.[key];
747
+ const valB = resultB.variables.light?.[key];
748
+ if (valA !== valB) {
749
+ changed.push(key);
750
+ }
751
+ }
752
+ }
753
+
754
+ return { added, removed, changed };
755
+ }