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.
- package/README.md +92 -10
- package/bin/omgkit.js +178 -0
- package/lib/cli.js +1 -1
- package/lib/generators/css.generator.js +151 -0
- package/lib/generators/figma.generator.js +311 -0
- package/lib/generators/index.js +135 -0
- package/lib/generators/scss.generator.js +249 -0
- package/lib/generators/style-dictionary.generator.js +456 -0
- package/lib/generators/tailwind.generator.js +251 -0
- package/lib/theme-v2.js +755 -0
- package/lib/theme.js +746 -4
- package/package.json +2 -2
- package/plugin/agents/fullstack-developer.md +1 -0
- package/plugin/agents/ui-ux-designer.md +163 -54
- package/plugin/commands/design/export.md +232 -0
- package/plugin/commands/design/rebuild.md +199 -0
- package/plugin/commands/design/rollback.md +179 -0
- package/plugin/commands/design/scan.md +155 -0
- package/plugin/commands/design/validate.md +223 -0
- package/plugin/registry.yaml +7 -3
- package/plugin/skills/frontend/design-system-context/SKILL.md +252 -0
- package/templates/design/schema/theme-v2.schema.json +384 -0
- package/templates/design/themes/tech-ai/electric-cyan-v2.json +362 -0
package/lib/theme-v2.js
ADDED
|
@@ -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
|
+
}
|