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.
- package/dist/index.d.mts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +241 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/apply-theme-sync.js +230 -0
- package/scripts/init.js +65 -0
|
@@ -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');
|