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.
- package/dist/index.d.mts +11 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +289 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +289 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/apply-theme-sync.js +242 -0
- package/scripts/init.js +99 -34
|
@@ -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
|
-
"
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
"
|
|
321
|
-
"
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
"
|
|
470
|
-
"
|
|
471
|
-
|
|
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
|
-
"
|
|
486
|
-
"
|
|
487
|
-
|
|
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
|
-
"
|
|
502
|
-
"
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
"
|
|
519
|
-
"
|
|
520
|
-
|
|
521
|
-
|
|
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');
|