shru-design-system 0.1.5 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Theme Configuration
3
+ * Registry of all available themes organized by category
4
+ *
5
+ * Base themes are defined here. Additional themes can be discovered dynamically
6
+ * by scanning the /tokens/themes/ directory structure.
7
+ */
8
+
9
+ /**
10
+ * Centralized theme category order
11
+ * Used everywhere to ensure consistency
12
+ * This is the SINGLE SOURCE OF TRUTH for category order
13
+ * Custom category is included but handled specially (optional, user-created files)
14
+ *
15
+ * ⚠️ IF YOU UPDATE THIS, ALSO UPDATE:
16
+ * 1. src/themes/themeConfig.ts - THEME_CATEGORY_ORDER (TypeScript source)
17
+ * 2. scripts/apply-theme-sync.js - THEME_CATEGORY_ORDER constant (standalone script, can't import)
18
+ */
19
+ export const THEME_CATEGORY_ORDER = ['color', 'typography', 'shape', 'density', 'animation', 'custom'];
20
+
21
+ // Base theme categories (always available)
22
+ export const baseThemeCategories = {
23
+ color: {
24
+ name: 'Color',
25
+ order: 1.0,
26
+ themes: {
27
+ white: {
28
+ name: 'White',
29
+ file: 'color/white.json',
30
+ icon: '🎨',
31
+ description: 'Light theme with white background'
32
+ },
33
+ dark: {
34
+ name: 'Dark',
35
+ file: 'color/dark.json',
36
+ icon: '🌙',
37
+ description: 'Dark theme with dark background'
38
+ }
39
+ }
40
+ },
41
+ typography: {
42
+ name: 'Typography',
43
+ order: 2.0,
44
+ themes: {
45
+ sans: {
46
+ name: 'Sans',
47
+ file: 'typography/sans.json',
48
+ icon: '📝',
49
+ description: 'Sans-serif font family'
50
+ },
51
+ serif: {
52
+ name: 'Serif',
53
+ file: 'typography/serif.json',
54
+ icon: '📖',
55
+ description: 'Serif font family'
56
+ }
57
+ }
58
+ },
59
+ shape: {
60
+ name: 'Shape',
61
+ order: 3.0,
62
+ themes: {
63
+ smooth: {
64
+ name: 'Smooth',
65
+ file: 'shape/smooth.json',
66
+ icon: '🔲',
67
+ description: 'Smooth rounded corners'
68
+ },
69
+ sharp: {
70
+ name: 'Sharp',
71
+ file: 'shape/sharp.json',
72
+ icon: '⬜',
73
+ description: 'Sharp square corners'
74
+ }
75
+ }
76
+ },
77
+ density: {
78
+ name: 'Density',
79
+ order: 4.0,
80
+ themes: {
81
+ comfortable: {
82
+ name: 'Comfortable',
83
+ file: 'density/comfortable.json',
84
+ icon: '📏',
85
+ description: 'Comfortable spacing'
86
+ },
87
+ compact: {
88
+ name: 'Compact',
89
+ file: 'density/compact.json',
90
+ icon: '📐',
91
+ description: 'Compact spacing'
92
+ }
93
+ }
94
+ },
95
+ animation: {
96
+ name: 'Animation',
97
+ order: 5.0,
98
+ themes: {
99
+ gentle: {
100
+ name: 'Gentle',
101
+ file: 'animation/gentle.json',
102
+ icon: '✨',
103
+ description: 'Gentle animations'
104
+ },
105
+ brisk: {
106
+ name: 'Brisk',
107
+ file: 'animation/brisk.json',
108
+ icon: '⚡',
109
+ description: 'Fast, brisk animations'
110
+ }
111
+ }
112
+ },
113
+ // Custom themes are not included in base config
114
+ // They should be discovered dynamically or registered by users
115
+ // Users can add custom themes by creating files in /tokens/themes/custom/
116
+ // and registering them using registerTheme()
117
+ };
118
+ // Cache for dynamically discovered themes
119
+ let discoveredThemesCache = null;
120
+ /**
121
+ * Discover themes by scanning token directory structure
122
+ * Scans /tokens/themes/ to find all available theme files
123
+ */
124
+ export async function discoverThemes() {
125
+ if (discoveredThemesCache) {
126
+ return discoveredThemesCache;
127
+ }
128
+ const discovered = JSON.parse(JSON.stringify(baseThemeCategories));
129
+ try {
130
+ // Get base path for tokens
131
+ const tokensBase = typeof window !== 'undefined' && window.__THEME_TOKENS_BASE__
132
+ ? window.__THEME_TOKENS_BASE__
133
+ : '/tokens';
134
+ // Known categories from base config
135
+ const knownCategories = Object.keys(baseThemeCategories);
136
+ // For each category, try to discover additional themes
137
+ for (const category of knownCategories) {
138
+ const categoryPath = `${tokensBase}/themes/${category}`;
139
+ // Try to fetch an index or scan common theme files
140
+ // Since we can't list directories via fetch, we'll try common patterns
141
+ // Users can add themes by following the naming convention
142
+ // For now, we'll rely on users to add themes to the config
143
+ // But we can validate that theme files exist when requested
144
+ }
145
+ discoveredThemesCache = discovered;
146
+ return discovered;
147
+ }
148
+ catch (error) {
149
+ // Only log in debug mode
150
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
151
+ console.warn('Error discovering themes:', error);
152
+ }
153
+ return baseThemeCategories;
154
+ }
155
+ }
156
+ /**
157
+ * Register a custom theme dynamically
158
+ * Allows users to add themes without modifying the base config
159
+ */
160
+ export function registerTheme(category, themeId, metadata) {
161
+ if (!discoveredThemesCache) {
162
+ discoveredThemesCache = JSON.parse(JSON.stringify(baseThemeCategories));
163
+ }
164
+ // TypeScript now knows discoveredThemesCache is not null after the check above
165
+ const cache = discoveredThemesCache;
166
+ // Create category if it doesn't exist
167
+ if (!cache[category]) {
168
+ cache[category] = {
169
+ name: category.charAt(0).toUpperCase() + category.slice(1),
170
+ order: 99, // Custom categories get high order
171
+ themes: {}
172
+ };
173
+ }
174
+ // Register the theme
175
+ cache[category].themes[themeId] = {
176
+ name: metadata.name,
177
+ file: metadata.file,
178
+ icon: metadata.icon || '🎨',
179
+ description: metadata.description || ''
180
+ };
181
+ return cache;
182
+ }
183
+ /**
184
+ * Get merged theme categories (base + discovered)
185
+ */
186
+ export async function getThemeCategories() {
187
+ return await discoverThemes();
188
+ }
189
+ // For backward compatibility, export baseThemeCategories as themeCategories
190
+ export const themeCategories = baseThemeCategories;
191
+ /**
192
+ * Get theme file path
193
+ */
194
+ export function getThemeFilePath(category, themeId) {
195
+ const categories = discoveredThemesCache || baseThemeCategories;
196
+ const theme = categories[category]?.themes[themeId];
197
+ if (!theme)
198
+ return null;
199
+ return `/tokens/themes/${theme.file}`;
200
+ }
201
+ /**
202
+ * Get all themes for a category
203
+ */
204
+ export async function getThemesForCategory(category) {
205
+ const categories = await getThemeCategories();
206
+ return categories[category]?.themes || {};
207
+ }
208
+ /**
209
+ * Get theme by ID
210
+ */
211
+ export async function getTheme(category, themeId) {
212
+ const categories = await getThemeCategories();
213
+ return categories[category]?.themes[themeId] || null;
214
+ }
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Theme Utilities
3
+ * Pure utility functions for theme management
4
+ * Note: generateAndApplyTheme has side effects (modifies DOM) but is the main theme application utility
5
+ */
6
+ import { getThemeCategories, THEME_CATEGORY_ORDER } from './themeConfig';
7
+ /**
8
+ * Generate theme combination name
9
+ */
10
+ export function getThemeName(selectedThemes) {
11
+ const parts = [];
12
+ if (selectedThemes.color)
13
+ parts.push(selectedThemes.color);
14
+ if (selectedThemes.typography)
15
+ parts.push(selectedThemes.typography);
16
+ if (selectedThemes.shape)
17
+ parts.push(selectedThemes.shape);
18
+ if (selectedThemes.density)
19
+ parts.push(selectedThemes.density);
20
+ if (selectedThemes.animation)
21
+ parts.push(selectedThemes.animation);
22
+ return parts.length > 0 ? parts.join('-') : 'default';
23
+ }
24
+ /**
25
+ * Validate theme selection
26
+ */
27
+ export function validateThemeSelection(selectedThemes, themeCategories) {
28
+ const errors = [];
29
+ for (const [category, themeId] of Object.entries(selectedThemes)) {
30
+ if (!themeId)
31
+ continue;
32
+ const categoryConfig = themeCategories[category];
33
+ if (!categoryConfig) {
34
+ errors.push(`Unknown category: ${category}`);
35
+ continue;
36
+ }
37
+ const theme = categoryConfig.themes[themeId];
38
+ if (!theme) {
39
+ errors.push(`Theme ${themeId} not found in category ${category}`);
40
+ }
41
+ }
42
+ return {
43
+ valid: errors.length === 0,
44
+ errors
45
+ };
46
+ }
47
+ /**
48
+ * Get default theme selections
49
+ *
50
+ * ⚠️ IF YOU UPDATE THIS, ALSO UPDATE:
51
+ * 1. src/themes/themeUtils.ts - getDefaultThemes() function (TypeScript source)
52
+ * 2. scripts/apply-theme-sync.js - DEFAULT_THEMES constant (standalone script, can't import)
53
+ */
54
+ export function getDefaultThemes() {
55
+ return {
56
+ color: 'white',
57
+ typography: 'sans',
58
+ shape: 'smooth',
59
+ density: 'comfortable',
60
+ animation: 'gentle'
61
+ };
62
+ }
63
+ /**
64
+ * Deep clone object
65
+ */
66
+ export function deepClone(obj) {
67
+ return JSON.parse(JSON.stringify(obj));
68
+ }
69
+ /**
70
+ * Check if value is an object
71
+ */
72
+ function isObject(item) {
73
+ return item && typeof item === 'object' && !Array.isArray(item);
74
+ }
75
+ /**
76
+ * Deep merge objects
77
+ */
78
+ export function deepMerge(target, source) {
79
+ const output = { ...target };
80
+ if (isObject(target) && isObject(source)) {
81
+ Object.keys(source).forEach(key => {
82
+ if (isObject(source[key])) {
83
+ if (!(key in target)) {
84
+ Object.assign(output, { [key]: source[key] });
85
+ }
86
+ else {
87
+ output[key] = deepMerge(target[key], source[key]);
88
+ }
89
+ }
90
+ else {
91
+ Object.assign(output, { [key]: source[key] });
92
+ }
93
+ });
94
+ }
95
+ return output;
96
+ }
97
+ // Cache for loaded JSON files
98
+ const tokenCache = new Map();
99
+ /**
100
+ * Load JSON file with caching
101
+ */
102
+ export async function loadTokenFile(path) {
103
+ if (tokenCache.has(path)) {
104
+ return deepClone(tokenCache.get(path));
105
+ }
106
+ try {
107
+ const response = await fetch(path);
108
+ if (!response.ok) {
109
+ // 404 means file doesn't exist - return null instead of throwing
110
+ if (response.status === 404) {
111
+ return null;
112
+ }
113
+ throw new Error(`Failed to load ${path}: ${response.statusText}`);
114
+ }
115
+ const contentType = response.headers.get('content-type');
116
+ // Check if response is actually JSON (not HTML error page)
117
+ if (!contentType || !contentType.includes('application/json')) {
118
+ // Likely got HTML error page instead of JSON
119
+ return null;
120
+ }
121
+ const data = await response.json();
122
+ tokenCache.set(path, data);
123
+ return deepClone(data);
124
+ }
125
+ catch (error) {
126
+ // Only log errors in debug mode
127
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
128
+ console.warn(`Error loading token file ${path}:`, error);
129
+ }
130
+ // Return null instead of throwing - allows theme to continue with other files
131
+ return null;
132
+ }
133
+ }
134
+ /**
135
+ * Resolve token references
136
+ * {palette.blue.500} → actual color value
137
+ */
138
+ function resolveReferences(tokens, palette) {
139
+ const resolved = JSON.parse(JSON.stringify(tokens));
140
+ function resolveValue(value) {
141
+ if (typeof value !== 'string')
142
+ return value;
143
+ // Match {path.to.token} pattern
144
+ const match = value.match(/^\{([^}]+)\}$/);
145
+ if (!match)
146
+ return value;
147
+ const path = match[1].split('.');
148
+ let current = { palette, ...resolved };
149
+ for (const key of path) {
150
+ if (current && typeof current === 'object' && key in current) {
151
+ current = current[key];
152
+ }
153
+ else {
154
+ // Token reference not found - return original value
155
+ // Only warn in debug mode
156
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
157
+ console.warn(`Token reference not found: {${match[1]}}`);
158
+ }
159
+ return value; // Return original if not found
160
+ }
161
+ }
162
+ return typeof current === 'string' ? current : value;
163
+ }
164
+ function traverse(obj) {
165
+ for (const key in obj) {
166
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
167
+ traverse(obj[key]);
168
+ }
169
+ else {
170
+ obj[key] = resolveValue(obj[key]);
171
+ }
172
+ }
173
+ }
174
+ traverse(resolved);
175
+ return resolved;
176
+ }
177
+ /**
178
+ * Convert hex color to HSL format (without hsl() wrapper)
179
+ * Returns format: "h s% l%" for use with hsl(var(--color))
180
+ */
181
+ function hexToHSL(hex) {
182
+ // Remove # if present
183
+ hex = hex.replace('#', '');
184
+ // Parse RGB
185
+ const r = parseInt(hex.substring(0, 2), 16) / 255;
186
+ const g = parseInt(hex.substring(2, 4), 16) / 255;
187
+ const b = parseInt(hex.substring(4, 6), 16) / 255;
188
+ const max = Math.max(r, g, b);
189
+ const min = Math.min(r, g, b);
190
+ let h = 0;
191
+ let s = 0;
192
+ const l = (max + min) / 2;
193
+ if (max !== min) {
194
+ const d = max - min;
195
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
196
+ switch (max) {
197
+ case r:
198
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
199
+ break;
200
+ case g:
201
+ h = ((b - r) / d + 2) / 6;
202
+ break;
203
+ case b:
204
+ h = ((r - g) / d + 4) / 6;
205
+ break;
206
+ }
207
+ }
208
+ h = Math.round(h * 360);
209
+ s = Math.round(s * 100);
210
+ const lPercent = Math.round(l * 100);
211
+ return `${h} ${s}% ${lPercent}%`;
212
+ }
213
+ /**
214
+ * Check if a string is a hex color
215
+ */
216
+ function isHexColor(value) {
217
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
218
+ }
219
+ /**
220
+ * Flatten nested object to CSS variables
221
+ * Maps token structure to CSS variable naming:
222
+ * - color.primary → --primary (semantic colors are flat)
223
+ * - font.body → --font-body
224
+ * - radius.button → --radius-button
225
+ * - font.body → --font-body
226
+ * - spacing.component.md → --spacing-component-md
227
+ */
228
+ function flattenToCSS(tokens, prefix = '', result = {}, isColorContext = false) {
229
+ for (const key in tokens) {
230
+ const value = tokens[key];
231
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
232
+ // Token files are already in correct structure (no nested typography/shape wrappers)
233
+ const enteringColor = key === 'color' && prefix === '';
234
+ const inColorContext = isColorContext || enteringColor;
235
+ if (enteringColor) {
236
+ // When entering color object, flatten without "color-" prefix
237
+ flattenToCSS(value, '', result, true);
238
+ }
239
+ else if (inColorContext) {
240
+ // Already in color context, continue with empty prefix
241
+ flattenToCSS(value, '', result, true);
242
+ }
243
+ else {
244
+ // For all other tokens, use simple prefix-based flattening
245
+ // font.body → --font-body, radius.button → --radius-button, spacing.component.md → --spacing-component-md
246
+ const newPrefix = prefix ? `${prefix}-${key}` : key;
247
+ flattenToCSS(value, newPrefix, result, false);
248
+ }
249
+ }
250
+ else {
251
+ // Generate CSS variable name
252
+ let cssKey;
253
+ if (isColorContext || (prefix === '' && key === 'color')) {
254
+ // Semantic colors: --primary, --background, etc. (no "color-" prefix)
255
+ cssKey = `--${key}`;
256
+ }
257
+ else if (prefix === '') {
258
+ // Top-level tokens (non-color)
259
+ cssKey = `--${key}`;
260
+ }
261
+ else {
262
+ // Nested tokens: --font-body, --radius-button, --spacing-component-md, etc.
263
+ cssKey = `--${prefix}-${key}`;
264
+ }
265
+ // Convert hex colors to HSL format for Tailwind compatibility
266
+ let finalValue = value;
267
+ if (isColorContext && typeof value === 'string' && isHexColor(value)) {
268
+ finalValue = hexToHSL(value);
269
+ }
270
+ result[cssKey] = finalValue;
271
+ }
272
+ }
273
+ return result;
274
+ }
275
+ /**
276
+ * Map theme variables to Tailwind-compatible CSS variables
277
+ * This function automatically passes through ALL CSS variables
278
+ * and only adds convenience mappings for Tailwind-specific needs
279
+ */
280
+ function mapToTailwindVars(cssVars) {
281
+ // Start with all generated variables - they're all valid CSS variables
282
+ const mapped = { ...cssVars };
283
+ // Only add convenience mappings for Tailwind's expected variable names
284
+ // These are optional - the actual variables are already in the map
285
+ // Map radius-button to --radius for Tailwind's rounded-* utilities
286
+ if (cssVars['--radius-button'] && !cssVars['--radius']) {
287
+ mapped['--radius'] = cssVars['--radius-button'];
288
+ }
289
+ // Map radius-card to --radius-lg for larger rounded corners
290
+ if (cssVars['--radius-card'] && !cssVars['--radius-lg']) {
291
+ mapped['--radius-lg'] = cssVars['--radius-card'];
292
+ }
293
+ // Map font-body to --font-sans for Tailwind's font-sans utility
294
+ if (cssVars['--font-body'] && !cssVars['--font-sans']) {
295
+ mapped['--font-sans'] = cssVars['--font-body'];
296
+ }
297
+ // Map spacing-base or spacing-component-md to --spacing for convenience
298
+ if (cssVars['--spacing-base'] && !cssVars['--spacing']) {
299
+ mapped['--spacing'] = cssVars['--spacing-base'];
300
+ }
301
+ else if (cssVars['--spacing-component-md'] && !cssVars['--spacing']) {
302
+ mapped['--spacing'] = cssVars['--spacing-component-md'];
303
+ }
304
+ return mapped;
305
+ }
306
+ /**
307
+ * Generate CSS string from CSS variables
308
+ */
309
+ function generateCSSString(cssVars) {
310
+ const mappedVars = mapToTailwindVars(cssVars);
311
+ const vars = Object.entries(mappedVars)
312
+ .sort(([a], [b]) => a.localeCompare(b)) // Sort alphabetically for easier debugging
313
+ .map(([key, value]) => ` ${key}: ${value};`)
314
+ .join('\n');
315
+ // Debug mode: log all CSS variables in development
316
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
317
+ console.group('🎨 Design System CSS Variables');
318
+ console.table(mappedVars);
319
+ console.log('Total variables:', Object.keys(mappedVars).length);
320
+ console.groupEnd();
321
+ }
322
+ return `:root {\n${vars}\n}`;
323
+ }
324
+ /**
325
+ * Apply CSS to DOM
326
+ */
327
+ function applyThemeCSS(css) {
328
+ let styleTag = document.getElementById('dynamic-theme');
329
+ if (!styleTag) {
330
+ styleTag = document.createElement('style');
331
+ styleTag.id = 'dynamic-theme';
332
+ document.head.appendChild(styleTag);
333
+ }
334
+ styleTag.textContent = css;
335
+ }
336
+ /**
337
+ * Debug helper: Enable debug mode to see all CSS variables in console
338
+ * Call this in browser console: window.__DESIGN_SYSTEM_DEBUG__ = true
339
+ */
340
+ export function enableDebugMode() {
341
+ if (typeof window !== 'undefined') {
342
+ window.__DESIGN_SYSTEM_DEBUG__ = true;
343
+ console.log('🔍 Design System debug mode enabled');
344
+ console.log('CSS variables will be logged when themes change');
345
+ }
346
+ }
347
+ /**
348
+ * Debug helper: Get all current CSS variables
349
+ */
350
+ export function getCurrentCSSVariables() {
351
+ if (typeof window === 'undefined')
352
+ return {};
353
+ const styleTag = document.getElementById('dynamic-theme');
354
+ if (!styleTag)
355
+ return {};
356
+ const cssText = styleTag.textContent || '';
357
+ const vars = {};
358
+ const matches = cssText.matchAll(/--([^:]+):\s*([^;]+);/g);
359
+ for (const match of matches) {
360
+ vars[`--${match[1].trim()}`] = match[2].trim();
361
+ }
362
+ return vars;
363
+ }
364
+ /**
365
+ * Generate and apply theme
366
+ * Main function that composes everything
367
+ */
368
+ export async function generateAndApplyTheme(selectedThemes = {}) {
369
+ try {
370
+ // Get theme categories (with dynamic discovery)
371
+ const themeCategories = await getThemeCategories();
372
+ // Validate theme selection (but be lenient with custom themes - they might not exist yet)
373
+ const validation = validateThemeSelection(selectedThemes, themeCategories);
374
+ if (!validation.valid) {
375
+ // Filter out custom theme errors - custom themes are optional
376
+ const nonCustomErrors = validation.errors.filter(err => !err.includes('custom'));
377
+ if (nonCustomErrors.length > 0) {
378
+ // Only log in debug mode
379
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
380
+ console.error('Invalid theme selection:', nonCustomErrors);
381
+ }
382
+ throw new Error(`Invalid theme selection: ${nonCustomErrors.join(', ')}`);
383
+ }
384
+ // If only custom theme errors, just warn and continue
385
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
386
+ console.warn('Custom theme files not found, continuing without them:', validation.errors);
387
+ }
388
+ }
389
+ // 1. Load base tokens
390
+ const base = await loadTokenFile('/tokens/base.json');
391
+ if (!base) {
392
+ throw new Error('Failed to load base tokens from /tokens/base.json');
393
+ }
394
+ // 2. Load palette
395
+ const palettes = await loadTokenFile('/tokens/palettes.json');
396
+ if (!palettes || !palettes.palette) {
397
+ throw new Error('Failed to load palette from /tokens/palettes.json');
398
+ }
399
+ const palette = palettes.palette;
400
+ // 3. Deep merge: base → palette
401
+ let merged = deepMerge(base, { palette });
402
+ // 4. Load and merge category themes in order (includes custom, use centralized order from themeConfig.js)
403
+ for (const category of THEME_CATEGORY_ORDER) {
404
+ const themeId = selectedThemes[category];
405
+ if (!themeId)
406
+ continue;
407
+ const themePath = `/tokens/themes/${category}/${themeId}.json`;
408
+ const themeData = await loadTokenFile(themePath);
409
+ // Only merge if theme data was successfully loaded
410
+ if (themeData) {
411
+ merged = deepMerge(merged, themeData);
412
+ }
413
+ else {
414
+ // For custom themes, silently skip if not found (user-created, optional)
415
+ // For other categories, warn in debug mode
416
+ if (category !== 'custom' && typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
417
+ console.warn(`Theme file not found: ${themePath}`);
418
+ }
419
+ else if (category === 'custom' && typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
420
+ console.warn(`Custom theme file not found: ${themePath} (this is normal if you haven't created it yet)`);
421
+ }
422
+ }
423
+ }
424
+ // 6. Resolve references
425
+ const resolved = resolveReferences(merged, palette);
426
+ // 7. Flatten to CSS variables
427
+ const cssVars = flattenToCSS(resolved);
428
+ // 8. Generate CSS string
429
+ const css = generateCSSString(cssVars);
430
+ // 9. Apply to DOM
431
+ if (typeof document !== 'undefined') {
432
+ applyThemeCSS(css);
433
+ // Debug: expose CSS variables to window for inspection
434
+ if (window.__DESIGN_SYSTEM_DEBUG__) {
435
+ window.__DESIGN_SYSTEM_VARS__ = cssVars;
436
+ console.log('💡 Access CSS variables via: window.__DESIGN_SYSTEM_VARS__');
437
+ }
438
+ }
439
+ return {
440
+ success: true,
441
+ themeName: getThemeName(selectedThemes),
442
+ cssVars
443
+ };
444
+ }
445
+ catch (error) {
446
+ // Only log in debug mode
447
+ if (typeof window !== 'undefined' && window.__DESIGN_SYSTEM_DEBUG__) {
448
+ console.error('Error generating theme:', error);
449
+ }
450
+ throw error;
451
+ }
452
+ }