shru-design-system 0.1.2 → 0.1.3

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,230 @@
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
+ if (enteringColor) {
143
+ flattenToCSS(value, '', result, true);
144
+ } else if (inColorContext) {
145
+ flattenToCSS(value, '', result, true);
146
+ } else {
147
+ flattenToCSS(value, prefix ? prefix + '-' + key : key, result, false);
148
+ }
149
+ } else {
150
+ var cssKey = isColorContext || (prefix === '' && key === 'color')
151
+ ? '--' + key
152
+ : prefix === ''
153
+ ? '--' + key
154
+ : '--' + prefix + '-' + key;
155
+
156
+ // Convert hex colors to HSL format for Tailwind compatibility
157
+ var finalValue = value;
158
+ if (isColorContext && typeof value === 'string' && isHexColor(value)) {
159
+ finalValue = hexToHSL(value);
160
+ }
161
+
162
+ result[cssKey] = finalValue;
163
+ }
164
+ }
165
+ return result;
166
+ }
167
+
168
+ function mapToTailwindVars(cssVars) {
169
+ var mapped = {};
170
+ for (var key in cssVars) mapped[key] = cssVars[key];
171
+ if (cssVars['--radius-button']) mapped['--radius'] = cssVars['--radius-button'];
172
+ if (cssVars['--radius-card']) mapped['--radius-lg'] = cssVars['--radius-card'];
173
+ if (cssVars['--font-body']) mapped['--font-sans'] = cssVars['--font-body'];
174
+ if (cssVars['--spacing-base']) {
175
+ mapped['--spacing'] = cssVars['--spacing-base'];
176
+ } else if (cssVars['--spacing-component-md']) {
177
+ mapped['--spacing'] = cssVars['--spacing-component-md'];
178
+ }
179
+ return mapped;
180
+ }
181
+
182
+ // Apply theme synchronously (optimized)
183
+ try {
184
+ var base = loadJSONSync('/tokens/base.json');
185
+ if (!base) return;
186
+
187
+ var palettes = loadJSONSync('/tokens/palettes.json');
188
+ var palette = (palettes && palettes.palette) || {};
189
+ var merged = deepMerge(base, { palette: palette });
190
+
191
+ // Only load theme files that are actually selected (optimized)
192
+ var categoryOrder = ['color', 'typography', 'shape', 'density', 'animation'];
193
+ for (var i = 0; i < categoryOrder.length; i++) {
194
+ var category = categoryOrder[i];
195
+ var themeId = selectedThemes[category];
196
+ if (!themeId) continue;
197
+
198
+ var themeData = loadJSONSync('/tokens/themes/' + category + '/' + themeId + '.json');
199
+ if (themeData) merged = deepMerge(merged, themeData);
200
+ }
201
+
202
+ if (selectedThemes.custom) {
203
+ var customData = loadJSONSync('/tokens/themes/custom/' + selectedThemes.custom + '.json');
204
+ if (customData) merged = deepMerge(merged, customData);
205
+ }
206
+
207
+ var resolved = resolveReferences(merged, palette);
208
+ var cssVars = flattenToCSS(resolved);
209
+ var mappedVars = mapToTailwindVars(cssVars);
210
+
211
+ // Generate CSS efficiently
212
+ var cssLines = [':root {'];
213
+ for (var key in mappedVars) {
214
+ cssLines.push(' ' + key + ': ' + mappedVars[key] + ';');
215
+ }
216
+ cssLines.push('}');
217
+ var css = cssLines.join('\n');
218
+
219
+ // Apply to DOM
220
+ var styleTag = document.getElementById('dynamic-theme');
221
+ if (!styleTag) {
222
+ styleTag = document.createElement('style');
223
+ styleTag.id = 'dynamic-theme';
224
+ document.head.insertBefore(styleTag, document.head.firstChild);
225
+ }
226
+ styleTag.textContent = css;
227
+ } catch (error) {
228
+ // Silently fail - theme will apply via React hook
229
+ }
230
+ })();
package/scripts/init.js CHANGED
@@ -124,6 +124,10 @@ 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
+ },
127
131
  },
128
132
  },
129
133
  plugins: [],
@@ -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');