@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.
@@ -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.4.2",
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 (e) {}
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]] || `${parseInt(cParts[2]) * 0.25}rem`;
163
- const escaped = fullCls.replace(/([:[\/])/g, '\\$1');
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 = parseInt(valKey);
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(${parseInt(valKey) / 100})`;
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 = `${(parseInt(v.split('/')[0])/parseInt(v.split('/')[1])*100).toFixed(2)}%`;
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.replace(/([:[\/])/g, '\\$1');
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
- * Expects a toggle element with class '.hamburger' and a menu with class '.nav-menu-mobile'.
42
+ * Uses data attributes for flexible targeting and reduced DOM dependencies.
43
43
  */
44
44
  initNavigation() {
45
- const toggles = document.querySelectorAll('.hamburger');
45
+ const toggles = document.querySelectorAll('[data-nav-toggle]');
46
46
 
47
47
  toggles.forEach(toggle => {
48
- // Find the closest navigation container or parent
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
- * Toggles '.active' class on '.accordion-item' when header is clicked.
185
+ * Uses data attributes for flexible configuration and reduced DOM dependencies.
121
186
  */
122
187
  initAccordions() {
123
- const headers = document.querySelectorAll('.accordion-header');
188
+ const accordions = document.querySelectorAll('[data-accordion]');
124
189
 
125
- headers.forEach(header => {
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
- * Switches '.active' class on buttons and panels.
265
+ * Uses data attributes for flexible configuration and reduced DOM dependencies.
144
266
  */
145
267
  initTabs() {
146
- const tabLists = document.querySelectorAll('.tab-list');
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
- tabLists.forEach(list => {
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 rgba(0, 212, 255, 0.2), 0 0 40px rgba(0, 212, 255, 0.1); }
11
- 50% { box-shadow: 0 0 40px rgba(0, 212, 255, 0.5), 0 0 80px rgba(0, 212, 255, 0.2); }
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);