@howssatoshi/quantumcss 1.4.2 → 1.5.0
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/README.md +8 -7
- package/dist/quantum.min.css +1 -4678
- package/examples/blog-template.html +268 -0
- package/examples/gaming-template.html +355 -0
- package/examples/images/starlight.jpg +0 -0
- package/examples/index.html +138 -0
- package/examples/kitchen-sink.html +1012 -0
- package/examples/news-template.html +320 -0
- package/examples/shopping/images/headset.jpg +0 -0
- package/examples/shopping/images/sneakers.jpg +0 -0
- package/examples/shopping/images/windbreaker.jpg +0 -0
- package/examples/shopping/index.html +330 -0
- package/examples/starlight.html +161 -0
- package/examples/theme-test.html +174 -0
- package/examples/travel/index.html +254 -0
- package/examples/verify_fixes.html +52 -0
- package/examples/verify_presets.html +32 -0
- package/package.json +8 -1
- package/src/generator.js +19 -8
- package/src/starlight.js +406 -15
- package/src/styles/quantum-animations.css +3 -2
- package/src/styles/quantum-base.css +295 -91
- package/src/styles/quantum-components.css +47 -47
- package/src/styles/quantum-responsive.css +5 -4
- package/src/styles/starlight.css +184 -184
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Quantum CSS - Verify Presets</title>
|
|
7
|
+
<link rel="stylesheet" href="../dist/quantum.min.css">
|
|
8
|
+
<script src="../src/starlight.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body class="bg-gray-50 p-12">
|
|
11
|
+
<div class="max-w-4xl mx-auto space-y-12">
|
|
12
|
+
<h1 class="text-4xl font-bold">Verification: Component Presets</h1>
|
|
13
|
+
|
|
14
|
+
<div class="space-y-4">
|
|
15
|
+
<h2 class="text-xl font-semibold">Buttons</h2>
|
|
16
|
+
<div class="flex gap-4">
|
|
17
|
+
<button class="btn-primary">Primary Preset</button>
|
|
18
|
+
<button class="md:btn-primary">Responsive Primary</button>
|
|
19
|
+
<button class="btn-secondary">Secondary Preset</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div class="space-y-4">
|
|
24
|
+
<h2 class="text-xl font-semibold">Cards</h2>
|
|
25
|
+
<div class="card-premium max-w-sm">
|
|
26
|
+
<h3 class="text-2xl font-bold mb-4">Premium Card</h3>
|
|
27
|
+
<p class="text-gray-600">This card uses the 'card-premium' preset defined in quantum.config.json.</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@howssatoshi/quantumcss",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Advanced utility-first CSS framework with JIT generation and modern components",
|
|
5
5
|
"main": "dist/quantum.min.css",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"src",
|
|
12
|
+
"examples",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -45,5 +46,11 @@
|
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"chokidar": "^5.0.0",
|
|
47
48
|
"glob": "^13.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^9.39.2",
|
|
52
|
+
"eslint": "^9.39.2",
|
|
53
|
+
"stylelint": "^17.1.1",
|
|
54
|
+
"stylelint-config-standard": "^40.0.0"
|
|
48
55
|
}
|
|
49
56
|
}
|
package/src/generator.js
CHANGED
|
@@ -58,7 +58,9 @@ function generateCSS(configPath) {
|
|
|
58
58
|
while ((match = classAttrRegex.exec(content)) !== null) {
|
|
59
59
|
match[1].split(/\s+/).forEach(cls => { if (cls) rawClasses.add(cls); });
|
|
60
60
|
}
|
|
61
|
-
} catch
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore errors reading files
|
|
63
|
+
}
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
const utilities = new Set();
|
|
@@ -67,6 +69,15 @@ function generateCSS(configPath) {
|
|
|
67
69
|
dark: new Set()
|
|
68
70
|
};
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Escapes a class name for use in a CSS selector
|
|
74
|
+
* @param {string} cls - The raw class name
|
|
75
|
+
* @returns {string} The escaped selector
|
|
76
|
+
*/
|
|
77
|
+
const escapeSelector = (cls) => {
|
|
78
|
+
return cls.replace(/([:[\]/.\\])/g, '\\$1');
|
|
79
|
+
};
|
|
80
|
+
|
|
70
81
|
const sideMap = {
|
|
71
82
|
p: 'padding', pt: 'padding-top', pr: 'padding-right', pb: 'padding-bottom', pl: 'padding-left',
|
|
72
83
|
px: ['padding-left', 'padding-right'], py: ['padding-top', 'padding-bottom'],
|
|
@@ -159,8 +170,8 @@ function generateCSS(configPath) {
|
|
|
159
170
|
} else if (prefix === 'col' && cParts[1] === 'span') {
|
|
160
171
|
property = 'grid-column'; value = `span ${cParts[2]} / span ${cParts[2]}`;
|
|
161
172
|
} else if (prefix === 'space') {
|
|
162
|
-
const amount = theme.spacing[cParts[2]] || `${
|
|
163
|
-
const escaped = fullCls
|
|
173
|
+
const amount = theme.spacing[cParts[2]] || `${parseFloat(cParts[2]) * 0.25}rem`;
|
|
174
|
+
const escaped = escapeSelector(fullCls);
|
|
164
175
|
customSelector = `.${escaped} > * + *`;
|
|
165
176
|
property = cParts[1] === 'y' ? 'margin-top' : 'margin-left';
|
|
166
177
|
value = isNeg ? `-${amount}` : amount;
|
|
@@ -169,12 +180,12 @@ function generateCSS(configPath) {
|
|
|
169
180
|
if (valKey.startsWith('[') && valKey.endsWith(']')) value = valKey.slice(1, -1);
|
|
170
181
|
else if (theme.borderRadius[valKey]) value = theme.borderRadius[valKey];
|
|
171
182
|
else {
|
|
172
|
-
const num =
|
|
183
|
+
const num = parseFloat(valKey);
|
|
173
184
|
value = isNaN(num) ? '0.375rem' : `${num * 0.125}rem`;
|
|
174
185
|
}
|
|
175
186
|
} else if (prefix === 'scale') {
|
|
176
187
|
property = 'transform';
|
|
177
|
-
value = `scale(${
|
|
188
|
+
value = `scale(${parseFloat(valKey) / 100})`;
|
|
178
189
|
} else if (prefix === 'transition') {
|
|
179
190
|
property = 'transition-property';
|
|
180
191
|
if (valKey === 'all') value = 'all';
|
|
@@ -193,8 +204,8 @@ function generateCSS(configPath) {
|
|
|
193
204
|
property = sideMap[prefix];
|
|
194
205
|
let v = valKey;
|
|
195
206
|
if (v.startsWith('[') && v.endsWith(']')) v = v.slice(1, -1);
|
|
196
|
-
else if (v.includes('/')) v = `${(
|
|
197
|
-
else v = theme.spacing[v] || v;
|
|
207
|
+
else if (v.includes('/')) v = `${(parseFloat(v.split('/')[0])/parseFloat(v.split('/')[1])*100).toFixed(2)}%`;
|
|
208
|
+
else v = theme.spacing[v] || (isNaN(parseFloat(v)) ? v : `${parseFloat(v) * 0.25}rem`);
|
|
198
209
|
value = isNeg ? (Array.isArray(v) ? v.map(x => `-${x}`) : `-${v}`) : v;
|
|
199
210
|
} else if (prefix === 'shadow') {
|
|
200
211
|
if (theme.shadows[valKey]) { property = 'box-shadow'; value = theme.shadows[valKey]; }
|
|
@@ -235,7 +246,7 @@ function generateCSS(configPath) {
|
|
|
235
246
|
|
|
236
247
|
merged.forEach(group => {
|
|
237
248
|
const { breakpoint, variant, customSelector, rules } = group;
|
|
238
|
-
const escapedFull = fullCls
|
|
249
|
+
const escapedFull = escapeSelector(fullCls);
|
|
239
250
|
let selector = customSelector || `.${escapedFull}`;
|
|
240
251
|
if (variant) { if (variant === 'group-hover') selector = `.group:hover ${selector}`; else selector += `:${variant}`}
|
|
241
252
|
|
package/src/starlight.js
CHANGED
|
@@ -39,17 +39,83 @@ const Starlight = {
|
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* Initializes navigation menu toggles for mobile view.
|
|
42
|
-
*
|
|
42
|
+
* Uses data attributes for flexible targeting and reduced DOM dependencies.
|
|
43
43
|
*/
|
|
44
44
|
initNavigation() {
|
|
45
|
-
const toggles = document.querySelectorAll('
|
|
45
|
+
const toggles = document.querySelectorAll('[data-nav-toggle]');
|
|
46
46
|
|
|
47
47
|
toggles.forEach(toggle => {
|
|
48
|
-
|
|
48
|
+
const config = {
|
|
49
|
+
targetSelector: toggle.getAttribute('data-nav-target'),
|
|
50
|
+
targetId: toggle.getAttribute('data-nav-target-id'),
|
|
51
|
+
activeClass: toggle.getAttribute('data-nav-active-class') || 'active',
|
|
52
|
+
closeOnOutside: toggle.getAttribute('data-nav-close-outside') !== 'false'
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Find target menu with multiple fallback methods
|
|
56
|
+
let menu = null;
|
|
57
|
+
|
|
58
|
+
if (config.targetId) {
|
|
59
|
+
menu = document.getElementById(config.targetId);
|
|
60
|
+
} else if (config.targetSelector) {
|
|
61
|
+
menu = document.querySelector(config.targetSelector);
|
|
62
|
+
} else {
|
|
63
|
+
// Fallback: look for common patterns
|
|
64
|
+
const nav = toggle.closest('nav') || toggle.closest('[data-nav]') || toggle.parentElement;
|
|
65
|
+
if (nav) {
|
|
66
|
+
menu = nav.querySelector('.nav-menu-mobile, [data-nav-menu]');
|
|
67
|
+
if (!menu && nav.nextElementSibling) {
|
|
68
|
+
menu = nav.nextElementSibling.querySelector('.nav-menu-mobile, [data-nav-menu]') ||
|
|
69
|
+
nav.nextElementSibling;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (menu) {
|
|
75
|
+
toggle.addEventListener('click', (e) => {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
|
|
79
|
+
const isActive = toggle.classList.toggle(config.activeClass);
|
|
80
|
+
menu.classList.toggle(config.activeClass, isActive);
|
|
81
|
+
|
|
82
|
+
// Update aria attributes for accessibility
|
|
83
|
+
toggle.setAttribute('aria-expanded', isActive);
|
|
84
|
+
menu.setAttribute('aria-hidden', !isActive);
|
|
85
|
+
|
|
86
|
+
// Emit custom event
|
|
87
|
+
toggle.dispatchEvent(new CustomEvent('navToggle', {
|
|
88
|
+
detail: { menu, isActive }
|
|
89
|
+
}));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Close menu when clicking outside (optional)
|
|
93
|
+
if (config.closeOnOutside) {
|
|
94
|
+
const handleOutsideClick = (e) => {
|
|
95
|
+
if (!menu.contains(e.target) && !toggle.contains(e.target)) {
|
|
96
|
+
menu.classList.remove(config.activeClass);
|
|
97
|
+
toggle.classList.remove(config.activeClass);
|
|
98
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
99
|
+
menu.setAttribute('aria-hidden', 'true');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
document.addEventListener('click', handleOutsideClick);
|
|
104
|
+
|
|
105
|
+
// Clean up on component destruction
|
|
106
|
+
toggle.addEventListener('navDestroy', () => {
|
|
107
|
+
document.removeEventListener('click', handleOutsideClick);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Fallback for legacy hamburger (backward compatibility)
|
|
114
|
+
const legacyToggles = document.querySelectorAll('.hamburger:not([data-nav-toggle])');
|
|
115
|
+
legacyToggles.forEach(toggle => {
|
|
49
116
|
const nav = toggle.closest('nav') || toggle.closest('.starlight-nav') || toggle.parentElement;
|
|
50
117
|
if (!nav) return;
|
|
51
118
|
|
|
52
|
-
// Find the menu - it might be inside the nav or a sibling
|
|
53
119
|
let menu = nav.querySelector('.nav-menu-mobile');
|
|
54
120
|
if (!menu && nav.nextElementSibling && nav.nextElementSibling.classList.contains('nav-menu-mobile')) {
|
|
55
121
|
menu = nav.nextElementSibling;
|
|
@@ -62,7 +128,6 @@ const Starlight = {
|
|
|
62
128
|
menu.classList.toggle('active', isActive);
|
|
63
129
|
});
|
|
64
130
|
|
|
65
|
-
// Close menu when clicking outside
|
|
66
131
|
document.addEventListener('click', (e) => {
|
|
67
132
|
if (!menu.contains(e.target) && !toggle.contains(e.target)) {
|
|
68
133
|
menu.classList.remove('active');
|
|
@@ -117,15 +182,72 @@ const Starlight = {
|
|
|
117
182
|
|
|
118
183
|
/**
|
|
119
184
|
* Initializes accordion components.
|
|
120
|
-
*
|
|
185
|
+
* Uses data attributes for flexible configuration and reduced DOM dependencies.
|
|
121
186
|
*/
|
|
122
187
|
initAccordions() {
|
|
123
|
-
const
|
|
188
|
+
const accordions = document.querySelectorAll('[data-accordion]');
|
|
124
189
|
|
|
125
|
-
|
|
190
|
+
accordions.forEach(accordion => {
|
|
191
|
+
const config = {
|
|
192
|
+
allowMultiple: accordion.getAttribute('data-accordion-allow-multiple') === 'true',
|
|
193
|
+
closeOthers: accordion.getAttribute('data-accordion-close-others') !== 'false',
|
|
194
|
+
headerSelector: accordion.getAttribute('data-accordion-header') || '.accordion-header',
|
|
195
|
+
contentSelector: accordion.getAttribute('data-accordion-content') || '.accordion-content',
|
|
196
|
+
itemSelector: accordion.getAttribute('data-accordion-item') || '.accordion-item'
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Find headers within this accordion
|
|
200
|
+
const headers = accordion.querySelectorAll(config.headerSelector);
|
|
201
|
+
|
|
202
|
+
headers.forEach(header => {
|
|
203
|
+
header.addEventListener('click', (e) => {
|
|
204
|
+
e.preventDefault();
|
|
205
|
+
|
|
206
|
+
// Find the associated item (could be parent, sibling, or specified via data attribute)
|
|
207
|
+
let item = header.closest(config.itemSelector);
|
|
208
|
+
if (!item) {
|
|
209
|
+
const itemId = header.getAttribute('data-accordion-item-id');
|
|
210
|
+
if (itemId) {
|
|
211
|
+
item = document.getElementById(itemId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!item) return;
|
|
216
|
+
|
|
217
|
+
const isActive = item.classList.contains('active');
|
|
218
|
+
const groupId = accordion.getAttribute('data-accordion-group');
|
|
219
|
+
|
|
220
|
+
// Handle accordion grouping logic
|
|
221
|
+
if (config.closeOthers && !config.allowMultiple && !isActive) {
|
|
222
|
+
if (groupId) {
|
|
223
|
+
// Close items in same group across different accordions
|
|
224
|
+
document.querySelectorAll(`[data-accordion-group="${groupId}"] ${config.itemSelector}.active`)
|
|
225
|
+
.forEach(i => i.classList.remove('active'));
|
|
226
|
+
} else {
|
|
227
|
+
// Close items in this accordion only
|
|
228
|
+
accordion.querySelectorAll(`${config.itemSelector}.active`)
|
|
229
|
+
.forEach(i => i.classList.remove('active'));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Toggle current item
|
|
234
|
+
item.classList.toggle('active', !isActive);
|
|
235
|
+
|
|
236
|
+
// Emit custom event for external handling
|
|
237
|
+
accordion.dispatchEvent(new CustomEvent('accordionToggle', {
|
|
238
|
+
detail: { item, isActive: !isActive }
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Fallback for legacy accordion-header (backward compatibility)
|
|
245
|
+
const legacyHeaders = document.querySelectorAll('.accordion-header:not([data-accordion])');
|
|
246
|
+
legacyHeaders.forEach(header => {
|
|
247
|
+
const item = header.parentElement;
|
|
248
|
+
const group = item.closest('.accordion-group');
|
|
249
|
+
|
|
126
250
|
header.addEventListener('click', () => {
|
|
127
|
-
const item = header.parentElement;
|
|
128
|
-
const group = item.closest('.accordion-group');
|
|
129
251
|
const isActive = item.classList.contains('active');
|
|
130
252
|
|
|
131
253
|
// If in a group, close others
|
|
@@ -140,12 +262,128 @@ const Starlight = {
|
|
|
140
262
|
|
|
141
263
|
/**
|
|
142
264
|
* Initializes tab components.
|
|
143
|
-
*
|
|
265
|
+
* Uses data attributes for flexible configuration and reduced DOM dependencies.
|
|
144
266
|
*/
|
|
145
267
|
initTabs() {
|
|
146
|
-
const
|
|
268
|
+
const tabContainers = document.querySelectorAll('[data-tabs]');
|
|
269
|
+
|
|
270
|
+
tabContainers.forEach(container => {
|
|
271
|
+
const config = {
|
|
272
|
+
buttonSelector: container.getAttribute('data-tabs-buttons') || '.tab-button',
|
|
273
|
+
panelSelector: container.getAttribute('data-tabs-panels') || '.tab-panel',
|
|
274
|
+
activeClass: container.getAttribute('data-tabs-active-class') || 'active',
|
|
275
|
+
orientation: container.getAttribute('data-tabs-orientation') || 'horizontal',
|
|
276
|
+
initialTab: container.getAttribute('data-tabs-initial')
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Find buttons and panels with flexible targeting
|
|
280
|
+
let buttons, panels;
|
|
281
|
+
|
|
282
|
+
if (container.hasAttribute('data-tabs-buttons')) {
|
|
283
|
+
buttons = document.querySelectorAll(config.buttonSelector);
|
|
284
|
+
} else {
|
|
285
|
+
buttons = container.querySelectorAll(config.buttonSelector);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (container.hasAttribute('data-tabs-panels')) {
|
|
289
|
+
panels = document.querySelectorAll(config.panelSelector);
|
|
290
|
+
} else {
|
|
291
|
+
panels = container.querySelectorAll(config.panelSelector);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Initialize active tab
|
|
295
|
+
let activeTabId = config.initialTab;
|
|
296
|
+
if (!activeTabId) {
|
|
297
|
+
const firstButton = buttons[0];
|
|
298
|
+
if (firstButton) {
|
|
299
|
+
activeTabId = firstButton.getAttribute('data-tab') || firstButton.getAttribute('data-tabs-target');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Set initial state
|
|
304
|
+
if (activeTabId) {
|
|
305
|
+
buttons.forEach(btn => {
|
|
306
|
+
const btnTargetId = btn.getAttribute('data-tab') || btn.getAttribute('data-tabs-target');
|
|
307
|
+
btn.classList.toggle(config.activeClass, btnTargetId === activeTabId);
|
|
308
|
+
btn.setAttribute('aria-selected', btnTargetId === activeTabId);
|
|
309
|
+
btn.setAttribute('tabindex', btnTargetId === activeTabId ? '0' : '-1');
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
panels.forEach(panel => {
|
|
313
|
+
panel.classList.toggle(config.activeClass, panel.id === activeTabId);
|
|
314
|
+
panel.setAttribute('aria-hidden', panel.id !== activeTabId);
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add click handlers
|
|
319
|
+
buttons.forEach(button => {
|
|
320
|
+
button.addEventListener('click', (e) => {
|
|
321
|
+
e.preventDefault();
|
|
322
|
+
|
|
323
|
+
const targetId = button.getAttribute('data-tab') || button.getAttribute('data-tabs-target');
|
|
324
|
+
if (!targetId) return;
|
|
325
|
+
|
|
326
|
+
// Update buttons
|
|
327
|
+
buttons.forEach(btn => {
|
|
328
|
+
const btnTargetId = btn.getAttribute('data-tab') || btn.getAttribute('data-tabs-target');
|
|
329
|
+
const isActive = btnTargetId === targetId;
|
|
330
|
+
btn.classList.toggle(config.activeClass, isActive);
|
|
331
|
+
btn.setAttribute('aria-selected', isActive);
|
|
332
|
+
btn.setAttribute('tabindex', isActive ? '0' : '-1');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Update panels
|
|
336
|
+
panels.forEach(panel => {
|
|
337
|
+
const isActive = panel.id === targetId;
|
|
338
|
+
panel.classList.toggle(config.activeClass, isActive);
|
|
339
|
+
panel.setAttribute('aria-hidden', !isActive);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Emit custom event
|
|
343
|
+
container.dispatchEvent(new CustomEvent('tabChange', {
|
|
344
|
+
detail: { targetId, activeButton: button, activePanel: document.getElementById(targetId) }
|
|
345
|
+
}));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Keyboard navigation
|
|
349
|
+
button.addEventListener('keydown', (e) => {
|
|
350
|
+
const currentIndex = Array.from(buttons).indexOf(button);
|
|
351
|
+
let nextIndex;
|
|
352
|
+
|
|
353
|
+
switch (e.key) {
|
|
354
|
+
case 'ArrowLeft':
|
|
355
|
+
case 'ArrowUp':
|
|
356
|
+
e.preventDefault();
|
|
357
|
+
nextIndex = currentIndex > 0 ? currentIndex - 1 : buttons.length - 1;
|
|
358
|
+
break;
|
|
359
|
+
case 'ArrowRight':
|
|
360
|
+
case 'ArrowDown':
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
nextIndex = currentIndex < buttons.length - 1 ? currentIndex + 1 : 0;
|
|
363
|
+
break;
|
|
364
|
+
case 'Home':
|
|
365
|
+
e.preventDefault();
|
|
366
|
+
nextIndex = 0;
|
|
367
|
+
break;
|
|
368
|
+
case 'End':
|
|
369
|
+
e.preventDefault();
|
|
370
|
+
nextIndex = buttons.length - 1;
|
|
371
|
+
break;
|
|
372
|
+
default:
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (nextIndex !== undefined) {
|
|
377
|
+
buttons[nextIndex].focus();
|
|
378
|
+
buttons[nextIndex].click();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
});
|
|
147
383
|
|
|
148
|
-
|
|
384
|
+
// Fallback for legacy tab-list (backward compatibility)
|
|
385
|
+
const legacyTabLists = document.querySelectorAll('.tab-list:not([data-tabs])');
|
|
386
|
+
legacyTabLists.forEach(list => {
|
|
149
387
|
const buttons = list.querySelectorAll('.tab-button');
|
|
150
388
|
const container = list.parentElement;
|
|
151
389
|
|
|
@@ -154,11 +392,9 @@ const Starlight = {
|
|
|
154
392
|
const targetId = button.getAttribute('data-tab');
|
|
155
393
|
if (!targetId) return;
|
|
156
394
|
|
|
157
|
-
// Update buttons
|
|
158
395
|
buttons.forEach(btn => btn.classList.remove('active'));
|
|
159
396
|
button.classList.add('active');
|
|
160
397
|
|
|
161
|
-
// Update panels
|
|
162
398
|
const panels = container.querySelectorAll('.tab-panel');
|
|
163
399
|
panels.forEach(panel => {
|
|
164
400
|
panel.classList.toggle('active', panel.id === targetId);
|
|
@@ -166,6 +402,160 @@ const Starlight = {
|
|
|
166
402
|
});
|
|
167
403
|
});
|
|
168
404
|
});
|
|
405
|
+
},
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Initializes theme management using data attributes for AI predictability.
|
|
409
|
+
* Manages theme state via localStorage and html[data-theme] attribute.
|
|
410
|
+
* Configurable via data attributes on theme toggle elements.
|
|
411
|
+
*/
|
|
412
|
+
initTheme() {
|
|
413
|
+
const config = {
|
|
414
|
+
defaultTheme: 'dark',
|
|
415
|
+
storageKey: 'theme',
|
|
416
|
+
themes: ['light', 'dark', 'auto'],
|
|
417
|
+
iconSelector: {
|
|
418
|
+
light: '.sun-icon, [data-theme-icon="light"]',
|
|
419
|
+
dark: '.moon-icon, [data-theme-icon="dark"]',
|
|
420
|
+
auto: '.system-icon, [data-theme-icon="auto"]'
|
|
421
|
+
},
|
|
422
|
+
autoDetect: true
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// Override config with global settings if available
|
|
426
|
+
if (window.StarlightConfig && window.StarlightConfig.theme) {
|
|
427
|
+
Object.assign(config, window.StarlightConfig.theme);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const html = document.documentElement;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Updates the UI icons to match the target theme
|
|
434
|
+
* @param {string} theme - The theme that is active (light, dark, or auto)
|
|
435
|
+
* @param {string} effectiveTheme - The actual theme being displayed
|
|
436
|
+
*/
|
|
437
|
+
const updateIcons = (theme, effectiveTheme) => {
|
|
438
|
+
const hasAutoIcon = document.querySelector(config.iconSelector.auto) !== null;
|
|
439
|
+
|
|
440
|
+
config.themes.forEach(t => {
|
|
441
|
+
const selector = config.iconSelector[t];
|
|
442
|
+
if (selector) {
|
|
443
|
+
document.querySelectorAll(selector).forEach(icon => {
|
|
444
|
+
if (theme === 'auto') {
|
|
445
|
+
// In auto mode, show system icon if it exists, otherwise show the effective theme icon
|
|
446
|
+
const isAutoIcon = t === 'auto';
|
|
447
|
+
const isEffectiveIcon = t === effectiveTheme;
|
|
448
|
+
|
|
449
|
+
if (hasAutoIcon) {
|
|
450
|
+
icon.classList.toggle('hidden', !isAutoIcon);
|
|
451
|
+
} else {
|
|
452
|
+
icon.classList.toggle('hidden', !isEffectiveIcon);
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
icon.classList.toggle('hidden', t !== theme);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Sets the theme and updates storage/UI
|
|
464
|
+
* @param {string} theme - The theme to set (light, dark, or auto)
|
|
465
|
+
* @param {string} source - Source of the change for event detail
|
|
466
|
+
* @param {boolean} save - Whether to save the preference to localStorage
|
|
467
|
+
*/
|
|
468
|
+
window.setTheme = (theme, source = 'api', save = true) => {
|
|
469
|
+
if (!config.themes.includes(theme)) {
|
|
470
|
+
console.warn(`Starlight: Theme "${theme}" is not supported.`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const previousTheme = localStorage.getItem(config.storageKey) || (config.autoDetect ? 'auto' : config.defaultTheme);
|
|
475
|
+
let effectiveTheme = theme;
|
|
476
|
+
|
|
477
|
+
if (theme === 'auto' && config.autoDetect) {
|
|
478
|
+
effectiveTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
479
|
+
localStorage.setItem(`${config.storageKey}-effective`, effectiveTheme);
|
|
480
|
+
} else {
|
|
481
|
+
localStorage.removeItem(`${config.storageKey}-effective`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Apply to DOM
|
|
485
|
+
html.setAttribute('data-theme', effectiveTheme);
|
|
486
|
+
document.body.classList.toggle('light-mode', effectiveTheme === 'light');
|
|
487
|
+
document.body.classList.toggle('dark-mode', effectiveTheme === 'dark');
|
|
488
|
+
|
|
489
|
+
// Save preference
|
|
490
|
+
if (save) {
|
|
491
|
+
localStorage.setItem(config.storageKey, theme);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Update icons
|
|
495
|
+
updateIcons(theme, effectiveTheme);
|
|
496
|
+
|
|
497
|
+
// Dispatch event
|
|
498
|
+
window.dispatchEvent(new CustomEvent('themechange', {
|
|
499
|
+
detail: { theme, effectiveTheme, previousTheme, source }
|
|
500
|
+
}));
|
|
501
|
+
|
|
502
|
+
return theme;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Cycles to the next available theme
|
|
507
|
+
*/
|
|
508
|
+
window.toggleTheme = () => {
|
|
509
|
+
const currentTheme = localStorage.getItem(config.storageKey) || (config.autoDetect ? 'auto' : config.defaultTheme);
|
|
510
|
+
const currentIndex = config.themes.indexOf(currentTheme);
|
|
511
|
+
const nextIndex = (currentIndex + 1) % config.themes.length;
|
|
512
|
+
const nextTheme = config.themes[nextIndex];
|
|
513
|
+
|
|
514
|
+
return window.setTheme(nextTheme, 'toggle');
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Initialize individual toggles
|
|
518
|
+
const createThemeToggle = (element) => {
|
|
519
|
+
element.addEventListener('click', (e) => {
|
|
520
|
+
e.preventDefault();
|
|
521
|
+
window.toggleTheme();
|
|
522
|
+
});
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
// Auto-detect system theme changes
|
|
526
|
+
if (config.autoDetect) {
|
|
527
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', (e) => {
|
|
528
|
+
const savedTheme = localStorage.getItem(config.storageKey);
|
|
529
|
+
const currentActiveTheme = savedTheme || 'auto';
|
|
530
|
+
|
|
531
|
+
if (currentActiveTheme === 'auto') {
|
|
532
|
+
const newEffective = e.matches ? 'light' : 'dark';
|
|
533
|
+
|
|
534
|
+
// Apply to DOM
|
|
535
|
+
html.setAttribute('data-theme', newEffective);
|
|
536
|
+
document.body.classList.toggle('light-mode', newEffective === 'light');
|
|
537
|
+
document.body.classList.toggle('dark-mode', newEffective === 'dark');
|
|
538
|
+
|
|
539
|
+
localStorage.setItem(`${config.storageKey}-effective`, newEffective);
|
|
540
|
+
updateIcons('auto', newEffective);
|
|
541
|
+
|
|
542
|
+
window.dispatchEvent(new CustomEvent('themechange', {
|
|
543
|
+
detail: { theme: 'auto', effectiveTheme: newEffective, source: 'system' }
|
|
544
|
+
}));
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Initialize UI
|
|
550
|
+
document.querySelectorAll('.theme-toggle, [data-theme-toggle]').forEach(createThemeToggle);
|
|
551
|
+
|
|
552
|
+
// Set initial theme
|
|
553
|
+
const savedTheme = localStorage.getItem(config.storageKey);
|
|
554
|
+
if (savedTheme) {
|
|
555
|
+
window.setTheme(savedTheme, 'init');
|
|
556
|
+
} else {
|
|
557
|
+
window.setTheme(config.autoDetect ? 'auto' : config.defaultTheme, 'init', false);
|
|
558
|
+
}
|
|
169
559
|
}
|
|
170
560
|
};
|
|
171
561
|
|
|
@@ -180,5 +570,6 @@ if (typeof window !== 'undefined') {
|
|
|
180
570
|
Starlight.initDropdowns();
|
|
181
571
|
Starlight.initAccordions();
|
|
182
572
|
Starlight.initTabs();
|
|
573
|
+
Starlight.initTheme();
|
|
183
574
|
});
|
|
184
575
|
}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
@keyframes cosmic-pulse {
|
|
10
|
-
0%, 100% { box-shadow: 0 0 20px
|
|
11
|
-
50% { box-shadow: 0 0 40px
|
|
10
|
+
0%, 100% { box-shadow: 0 0 20px rgb(0 212 255 / 20%), 0 0 40px rgb(0 212 255 / 10%); }
|
|
11
|
+
50% { box-shadow: 0 0 40px rgb(0 212 255 / 50%), 0 0 80px rgb(0 212 255 / 20%); }
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
@keyframes star-twinkle {
|
|
@@ -135,6 +135,7 @@
|
|
|
135
135
|
transform: translateY(-25%);
|
|
136
136
|
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
|
|
137
137
|
}
|
|
138
|
+
|
|
138
139
|
50% {
|
|
139
140
|
transform: none;
|
|
140
141
|
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
|