shru-design-system 0.1.2 → 0.1.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.
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Synchronous theme application script (optimized)
3
+ * This script runs before React to prevent theme flash
4
+ * Injected automatically by init script
5
+ */
6
+
7
+ (function() {
8
+ 'use strict';
9
+
10
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
11
+ return;
12
+ }
13
+
14
+ const STORAGE_KEY = 'design-system-theme';
15
+ const DEFAULT_THEMES = {
16
+ color: 'white',
17
+ typography: 'sans',
18
+ shape: 'smooth',
19
+ density: 'comfortable',
20
+ animation: 'gentle'
21
+ };
22
+
23
+ // Get theme from localStorage (optimized - single read)
24
+ var selectedThemes = DEFAULT_THEMES;
25
+ try {
26
+ var stored = localStorage.getItem(STORAGE_KEY);
27
+ if (stored) selectedThemes = JSON.parse(stored);
28
+ } catch (e) {}
29
+
30
+ // Optimized helper functions
31
+ function isObject(item) {
32
+ return item && typeof item === 'object' && !Array.isArray(item);
33
+ }
34
+
35
+ function deepMerge(target, source) {
36
+ var output = {};
37
+ for (var key in target) output[key] = target[key];
38
+ if (isObject(target) && isObject(source)) {
39
+ for (var key in source) {
40
+ if (isObject(source[key])) {
41
+ output[key] = (key in target && isObject(target[key]))
42
+ ? deepMerge(target[key], source[key])
43
+ : source[key];
44
+ } else {
45
+ output[key] = source[key];
46
+ }
47
+ }
48
+ }
49
+ return output;
50
+ }
51
+
52
+ function loadJSONSync(path) {
53
+ try {
54
+ var xhr = new XMLHttpRequest();
55
+ xhr.open('GET', path, false);
56
+ xhr.send(null);
57
+ if (xhr.status === 200 || xhr.status === 0) {
58
+ return JSON.parse(xhr.responseText);
59
+ }
60
+ } catch (e) {}
61
+ return null;
62
+ }
63
+
64
+ function resolveValue(value, palette, resolved) {
65
+ if (typeof value !== 'string') return value;
66
+ var match = value.match(/^\{([^}]+)\}$/);
67
+ if (!match) return value;
68
+ var path = match[1].split('.');
69
+ var current = { palette: palette };
70
+ for (var key in resolved) current[key] = resolved[key];
71
+ for (var i = 0; i < path.length; i++) {
72
+ var key = path[i];
73
+ if (current && typeof current === 'object' && key in current) {
74
+ current = current[key];
75
+ } else {
76
+ return value;
77
+ }
78
+ }
79
+ return typeof current === 'string' ? current : value;
80
+ }
81
+
82
+ function resolveReferences(tokens, palette) {
83
+ var resolved = JSON.parse(JSON.stringify(tokens));
84
+ function traverse(obj) {
85
+ for (var key in obj) {
86
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
87
+ traverse(obj[key]);
88
+ } else {
89
+ obj[key] = resolveValue(obj[key], palette, resolved);
90
+ }
91
+ }
92
+ }
93
+ traverse(resolved);
94
+ return resolved;
95
+ }
96
+
97
+ function hexToHSL(hex) {
98
+ hex = hex.replace('#', '');
99
+ var r = parseInt(hex.substring(0, 2), 16) / 255;
100
+ var g = parseInt(hex.substring(2, 4), 16) / 255;
101
+ var b = parseInt(hex.substring(4, 6), 16) / 255;
102
+ var max = Math.max(r, g, b);
103
+ var min = Math.min(r, g, b);
104
+ var h = 0;
105
+ var s = 0;
106
+ var l = (max + min) / 2;
107
+ if (max !== min) {
108
+ var d = max - min;
109
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
110
+ switch (max) {
111
+ case r:
112
+ h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
113
+ break;
114
+ case g:
115
+ h = ((b - r) / d + 2) / 6;
116
+ break;
117
+ case b:
118
+ h = ((r - g) / d + 4) / 6;
119
+ break;
120
+ }
121
+ }
122
+ h = Math.round(h * 360);
123
+ s = Math.round(s * 100);
124
+ var lPercent = Math.round(l * 100);
125
+ return h + ' ' + s + '% ' + lPercent + '%';
126
+ }
127
+
128
+ function isHexColor(value) {
129
+ return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value);
130
+ }
131
+
132
+ function flattenToCSS(tokens, prefix, result, isColorContext) {
133
+ prefix = prefix || '';
134
+ result = result || {};
135
+ isColorContext = isColorContext || false;
136
+
137
+ for (var key in tokens) {
138
+ var value = tokens[key];
139
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
140
+ var enteringColor = key === 'color' && prefix === '';
141
+ var inColorContext = isColorContext || enteringColor;
142
+
143
+ if (enteringColor) {
144
+ flattenToCSS(value, '', result, true);
145
+ } else if (inColorContext) {
146
+ flattenToCSS(value, '', result, true);
147
+ } else {
148
+ var newPrefix = prefix ? prefix + '-' + key : key;
149
+ flattenToCSS(value, newPrefix, result, false);
150
+ }
151
+ } else {
152
+ var cssKey;
153
+ if (isColorContext || (prefix === '' && key === 'color')) {
154
+ cssKey = '--' + key;
155
+ } else if (prefix === '') {
156
+ cssKey = '--' + key;
157
+ } else {
158
+ cssKey = '--' + prefix + '-' + key;
159
+ }
160
+
161
+ var finalValue = value;
162
+ if (isColorContext && typeof value === 'string' && isHexColor(value)) {
163
+ finalValue = hexToHSL(value);
164
+ }
165
+
166
+ result[cssKey] = finalValue;
167
+ }
168
+ }
169
+ return result;
170
+ }
171
+
172
+ function mapToTailwindVars(cssVars) {
173
+ var mapped = {};
174
+ for (var key in cssVars) mapped[key] = cssVars[key];
175
+ if (cssVars['--radius-button']) mapped['--radius'] = cssVars['--radius-button'];
176
+ if (cssVars['--radius-card']) mapped['--radius-lg'] = cssVars['--radius-card'];
177
+ if (cssVars['--font-body']) mapped['--font-sans'] = cssVars['--font-body'];
178
+ if (cssVars['--spacing-base']) {
179
+ mapped['--spacing'] = cssVars['--spacing-base'];
180
+ } else if (cssVars['--spacing-component-md']) {
181
+ mapped['--spacing'] = cssVars['--spacing-component-md'];
182
+ }
183
+ if (cssVars['--spacing-component-xs']) mapped['--spacing-component-xs'] = cssVars['--spacing-component-xs'];
184
+ if (cssVars['--spacing-component-sm']) mapped['--spacing-component-sm'] = cssVars['--spacing-component-sm'];
185
+ if (cssVars['--spacing-component-md']) mapped['--spacing-component-md'] = cssVars['--spacing-component-md'];
186
+ if (cssVars['--spacing-component-lg']) mapped['--spacing-component-lg'] = cssVars['--spacing-component-lg'];
187
+ if (cssVars['--spacing-component-xl']) mapped['--spacing-component-xl'] = cssVars['--spacing-component-xl'];
188
+ if (cssVars['--duration-fast']) mapped['--duration-fast'] = cssVars['--duration-fast'];
189
+ if (cssVars['--duration-normal']) mapped['--duration-normal'] = cssVars['--duration-normal'];
190
+ if (cssVars['--duration-slow']) mapped['--duration-slow'] = cssVars['--duration-slow'];
191
+ return mapped;
192
+ }
193
+
194
+ // Apply theme synchronously (optimized)
195
+ try {
196
+ var base = loadJSONSync('/tokens/base.json');
197
+ if (!base) return;
198
+
199
+ var palettes = loadJSONSync('/tokens/palettes.json');
200
+ var palette = (palettes && palettes.palette) || {};
201
+ var merged = deepMerge(base, { palette: palette });
202
+
203
+ // Only load theme files that are actually selected (optimized)
204
+ var categoryOrder = ['color', 'typography', 'shape', 'density', 'animation'];
205
+ for (var i = 0; i < categoryOrder.length; i++) {
206
+ var category = categoryOrder[i];
207
+ var themeId = selectedThemes[category];
208
+ if (!themeId) continue;
209
+
210
+ var themeData = loadJSONSync('/tokens/themes/' + category + '/' + themeId + '.json');
211
+ if (themeData) merged = deepMerge(merged, themeData);
212
+ }
213
+
214
+ if (selectedThemes.custom) {
215
+ var customData = loadJSONSync('/tokens/themes/custom/' + selectedThemes.custom + '.json');
216
+ if (customData) merged = deepMerge(merged, customData);
217
+ }
218
+
219
+ var resolved = resolveReferences(merged, palette);
220
+ var cssVars = flattenToCSS(resolved);
221
+ var mappedVars = mapToTailwindVars(cssVars);
222
+
223
+ // Generate CSS efficiently
224
+ var cssLines = [':root {'];
225
+ for (var key in mappedVars) {
226
+ cssLines.push(' ' + key + ': ' + mappedVars[key] + ';');
227
+ }
228
+ cssLines.push('}');
229
+ var css = cssLines.join('\n');
230
+
231
+ // Apply to DOM
232
+ var styleTag = document.getElementById('dynamic-theme');
233
+ if (!styleTag) {
234
+ styleTag = document.createElement('style');
235
+ styleTag.id = 'dynamic-theme';
236
+ document.head.insertBefore(styleTag, document.head.firstChild);
237
+ }
238
+ styleTag.textContent = css;
239
+ } catch (error) {
240
+ // Silently fail - theme will apply via React hook
241
+ }
242
+ })();
package/scripts/init.js CHANGED
@@ -124,6 +124,22 @@ export default {
124
124
  md: "calc(var(--radius) - 2px)",
125
125
  sm: "calc(var(--radius) - 4px)",
126
126
  },
127
+ fontFamily: {
128
+ sans: ["var(--font-sans)", "system-ui", "sans-serif"],
129
+ body: ["var(--font-body)", "var(--font-sans)", "system-ui", "sans-serif"],
130
+ },
131
+ spacing: {
132
+ 'component-xs': "var(--spacing-component-xs, 0.25rem)",
133
+ 'component-sm': "var(--spacing-component-sm, 0.5rem)",
134
+ 'component-md': "var(--spacing-component-md, 1rem)",
135
+ 'component-lg': "var(--spacing-component-lg, 1.5rem)",
136
+ 'component-xl': "var(--spacing-component-xl, 2rem)",
137
+ },
138
+ transitionDuration: {
139
+ 'fast': "var(--duration-fast, 150ms)",
140
+ 'normal': "var(--duration-normal, 300ms)",
141
+ 'slow': "var(--duration-slow, 500ms)",
142
+ },
127
143
  },
128
144
  },
129
145
  plugins: [],
@@ -310,19 +326,15 @@ function createTokenFiles() {
310
326
  },
311
327
  "base": "0.25rem"
312
328
  },
313
- "typography": {
314
- "font": {
315
- "body": "var(--font-sans)",
316
- "sans": "var(--font-sans)",
317
- "mono": "var(--font-mono)"
318
- }
329
+ "font": {
330
+ "body": "var(--font-sans)",
331
+ "sans": "var(--font-sans)",
332
+ "mono": "var(--font-mono)"
319
333
  },
320
- "shape": {
321
- "radius": {
322
- "button": "0.375rem",
323
- "card": "0.5rem",
324
- "input": "0.375rem"
325
- }
334
+ "radius": {
335
+ "button": "0.375rem",
336
+ "card": "0.5rem",
337
+ "input": "0.375rem"
326
338
  }
327
339
  };
328
340
  fs.writeFileSync(basePath, JSON.stringify(baseJson, null, 2));
@@ -466,11 +478,9 @@ function createTokenFiles() {
466
478
  if (!fs.existsSync(sansThemePath)) {
467
479
  const sansTheme = {
468
480
  "_createdBy": LIBRARY_NAME,
469
- "typography": {
470
- "font": {
471
- "body": "system-ui, -apple-system, sans-serif",
472
- "sans": "system-ui, -apple-system, sans-serif"
473
- }
481
+ "font": {
482
+ "body": "system-ui, -apple-system, sans-serif",
483
+ "sans": "system-ui, -apple-system, sans-serif"
474
484
  }
475
485
  };
476
486
  fs.writeFileSync(sansThemePath, JSON.stringify(sansTheme, null, 2));
@@ -482,11 +492,9 @@ function createTokenFiles() {
482
492
  if (!fs.existsSync(serifThemePath)) {
483
493
  const serifTheme = {
484
494
  "_createdBy": LIBRARY_NAME,
485
- "typography": {
486
- "font": {
487
- "body": "Georgia, serif",
488
- "sans": "Georgia, serif"
489
- }
495
+ "font": {
496
+ "body": "Georgia, serif",
497
+ "sans": "Georgia, serif"
490
498
  }
491
499
  };
492
500
  fs.writeFileSync(serifThemePath, JSON.stringify(serifTheme, null, 2));
@@ -498,12 +506,10 @@ function createTokenFiles() {
498
506
  if (!fs.existsSync(smoothThemePath)) {
499
507
  const smoothTheme = {
500
508
  "_createdBy": LIBRARY_NAME,
501
- "shape": {
502
- "radius": {
503
- "button": "0.5rem",
504
- "card": "0.75rem",
505
- "input": "0.5rem"
506
- }
509
+ "radius": {
510
+ "button": "0.5rem",
511
+ "card": "0.75rem",
512
+ "input": "0.5rem"
507
513
  }
508
514
  };
509
515
  fs.writeFileSync(smoothThemePath, JSON.stringify(smoothTheme, null, 2));
@@ -515,12 +521,10 @@ function createTokenFiles() {
515
521
  if (!fs.existsSync(sharpThemePath)) {
516
522
  const sharpTheme = {
517
523
  "_createdBy": LIBRARY_NAME,
518
- "shape": {
519
- "radius": {
520
- "button": "0",
521
- "card": "0",
522
- "input": "0"
523
- }
524
+ "radius": {
525
+ "button": "0",
526
+ "card": "0",
527
+ "input": "0"
524
528
  }
525
529
  };
526
530
  fs.writeFileSync(sharpThemePath, JSON.stringify(sharpTheme, null, 2));
@@ -600,6 +604,66 @@ function createTokenFiles() {
600
604
  }
601
605
  }
602
606
 
607
+ function injectThemeScript() {
608
+ const scriptPath = path.join(__dirname, 'apply-theme-sync.js');
609
+ const scriptContent = fs.readFileSync(scriptPath, 'utf8');
610
+
611
+ // Try to find and update index.html
612
+ const htmlPaths = [
613
+ path.join(process.cwd(), 'index.html'),
614
+ path.join(process.cwd(), 'public', 'index.html'),
615
+ ];
616
+
617
+ for (const htmlPath of htmlPaths) {
618
+ if (fs.existsSync(htmlPath)) {
619
+ let htmlContent = fs.readFileSync(htmlPath, 'utf8');
620
+
621
+ // Check if script is already injected
622
+ if (htmlContent.includes('dynamic-theme') || htmlContent.includes('apply-theme-sync')) {
623
+ log('Theme sync script already in index.html', 'green');
624
+ return;
625
+ }
626
+
627
+ // Add resource hints for token files (helps browser preload)
628
+ // Preload critical token files that are always needed
629
+ const preloadLinks = ` <!-- Preload token files for faster theme application - Created by ${LIBRARY_NAME} -->
630
+ <link rel="preload" href="/tokens/base.json" as="fetch" crossorigin>
631
+ <link rel="preload" href="/tokens/palettes.json" as="fetch" crossorigin>
632
+ <link rel="preload" href="/tokens/themes/color/white.json" as="fetch" crossorigin>
633
+ <link rel="preload" href="/tokens/themes/color/dark.json" as="fetch" crossorigin>`;
634
+
635
+ // Inject blocking script in <head> (runs before React)
636
+ const scriptTag = ` <!-- Blocking theme script to prevent flash - Created by ${LIBRARY_NAME} -->
637
+ <script>${scriptContent}</script>`;
638
+
639
+ if (htmlContent.includes('</head>')) {
640
+ // Add preload links and script before </head>
641
+ htmlContent = htmlContent.replace('</head>', `${preloadLinks}\n${scriptTag}\n</head>`);
642
+ fs.writeFileSync(htmlPath, htmlContent);
643
+ log('Injected theme sync script and preload links into index.html', 'green');
644
+ return;
645
+ } else if (htmlContent.includes('<head>')) {
646
+ // If no closing </head>, try to add after <head>
647
+ htmlContent = htmlContent.replace('<head>', `<head>\n${preloadLinks}\n${scriptTag}`);
648
+ fs.writeFileSync(htmlPath, htmlContent);
649
+ log('Injected theme sync script and preload links into index.html', 'green');
650
+ return;
651
+ } else if (htmlContent.includes('</body>')) {
652
+ // Last resort: add before </body>
653
+ htmlContent = htmlContent.replace('</body>', `${preloadLinks}\n${scriptTag}\n</body>`);
654
+ fs.writeFileSync(htmlPath, htmlContent);
655
+ log('Injected theme sync script and preload links into index.html (before </body>)', 'yellow');
656
+ return;
657
+ }
658
+ }
659
+ }
660
+
661
+ log('⚠️ Could not find index.html. Add these to <head> manually:', 'yellow');
662
+ log(' <link rel="preload" href="/tokens/base.json" as="fetch" crossorigin>', 'blue');
663
+ log(' <link rel="preload" href="/tokens/palettes.json" as="fetch" crossorigin>', 'blue');
664
+ log(' <script>/* apply-theme-sync.js content */</script>', 'blue');
665
+ }
666
+
603
667
  function checkMainFile() {
604
668
  const possiblePaths = [
605
669
  path.join(process.cwd(), 'src', 'main.tsx'),
@@ -657,6 +721,7 @@ function main() {
657
721
  createPostCSSConfig();
658
722
  createCSSFile();
659
723
  createTokenFiles();
724
+ injectThemeScript();
660
725
  checkMainFile();
661
726
 
662
727
  log('\n✅ Setup complete!', 'green');