bulma-turbo-themes 0.10.7 → 0.11.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.
@@ -1,687 +0,0 @@
1
- const THEME_FAMILIES = {
2
- bulma: { name: 'Bulma', description: 'Classic Bulma themes' },
3
- catppuccin: { name: 'Catppuccin', description: 'Soothing pastel themes' },
4
- github: { name: 'GitHub', description: 'GitHub-inspired themes' },
5
- dracula: { name: 'Dracula', description: 'Dark vampire aesthetic' },
6
- };
7
- const THEMES = [
8
- // Bulma themes
9
- {
10
- id: 'bulma-light',
11
- name: 'Light',
12
- description: 'Classic Bulma look with a bright, neutral palette.',
13
- cssFile: 'assets/css/themes/bulma-light.css',
14
- icon: 'assets/img/bulma-logo.png',
15
- family: 'bulma',
16
- appearance: 'light',
17
- colors: { bg: '#ffffff', surface: '#f5f5f5', accent: '#00d1b2', text: '#363636' },
18
- },
19
- {
20
- id: 'bulma-dark',
21
- name: 'Dark',
22
- description: 'Dark Bulma theme tuned for low-light reading.',
23
- cssFile: 'assets/css/themes/bulma-dark.css',
24
- icon: 'assets/img/bulma-logo.png',
25
- family: 'bulma',
26
- appearance: 'dark',
27
- colors: { bg: '#1a1a2e', surface: '#252540', accent: '#00d1b2', text: '#f5f5f5' },
28
- },
29
- // Catppuccin themes
30
- {
31
- id: 'catppuccin-latte',
32
- name: 'Latte',
33
- description: 'Light, soft Catppuccin palette for daytime use.',
34
- cssFile: 'assets/css/themes/catppuccin-latte.css',
35
- icon: 'assets/img/catppuccin-logo-latte.png',
36
- family: 'catppuccin',
37
- appearance: 'light',
38
- colors: { bg: '#eff1f5', surface: '#e6e9ef', accent: '#8839ef', text: '#4c4f69' },
39
- },
40
- {
41
- id: 'catppuccin-frappe',
42
- name: 'Frappé',
43
- description: 'Balanced dark Catppuccin theme for focused work.',
44
- cssFile: 'assets/css/themes/catppuccin-frappe.css',
45
- icon: 'assets/img/catppuccin-logo-latte.png',
46
- family: 'catppuccin',
47
- appearance: 'dark',
48
- colors: { bg: '#303446', surface: '#414559', accent: '#ca9ee6', text: '#c6d0f5' },
49
- },
50
- {
51
- id: 'catppuccin-macchiato',
52
- name: 'Macchiato',
53
- description: 'Deep, atmospheric Catppuccin variant with rich contrast.',
54
- cssFile: 'assets/css/themes/catppuccin-macchiato.css',
55
- icon: 'assets/img/catppuccin-logo-macchiato.png',
56
- family: 'catppuccin',
57
- appearance: 'dark',
58
- colors: { bg: '#24273a', surface: '#363a4f', accent: '#c6a0f6', text: '#cad3f5' },
59
- },
60
- {
61
- id: 'catppuccin-mocha',
62
- name: 'Mocha',
63
- description: 'Cozy, high-contrast Catppuccin theme for late-night sessions.',
64
- cssFile: 'assets/css/themes/catppuccin-mocha.css',
65
- icon: 'assets/img/catppuccin-logo-macchiato.png',
66
- family: 'catppuccin',
67
- appearance: 'dark',
68
- colors: { bg: '#1e1e2e', surface: '#313244', accent: '#cba6f7', text: '#cdd6f4' },
69
- },
70
- // Dracula theme
71
- {
72
- id: 'dracula',
73
- name: 'Classic',
74
- description: 'Iconic Dracula dark theme with vibrant accents.',
75
- cssFile: 'assets/css/themes/dracula.css',
76
- icon: 'assets/img/dracula-logo.png',
77
- family: 'dracula',
78
- appearance: 'dark',
79
- colors: { bg: '#282a36', surface: '#44475a', accent: '#bd93f9', text: '#f8f8f2' },
80
- },
81
- // GitHub themes
82
- {
83
- id: 'github-light',
84
- name: 'Light',
85
- description: 'GitHub-inspired light theme suited for documentation and UI heavy pages.',
86
- cssFile: 'assets/css/themes/github-light.css',
87
- icon: 'assets/img/github-logo-light.png',
88
- family: 'github',
89
- appearance: 'light',
90
- colors: { bg: '#ffffff', surface: '#f6f8fa', accent: '#0969da', text: '#1f2328' },
91
- },
92
- {
93
- id: 'github-dark',
94
- name: 'Dark',
95
- description: 'GitHub dark theme optimized for code-heavy views.',
96
- cssFile: 'assets/css/themes/github-dark.css',
97
- icon: 'assets/img/github-logo-dark.png',
98
- family: 'github',
99
- appearance: 'dark',
100
- colors: { bg: '#0d1117', surface: '#161b22', accent: '#58a6ff', text: '#c9d1d9' },
101
- },
102
- ];
103
- const STORAGE_KEY = 'bulma-theme-flavor';
104
- const DEFAULT_THEME = 'catppuccin-mocha';
105
- function getCurrentThemeFromClasses(element) {
106
- const classList = Array.from(element.classList);
107
- for (const className of classList) {
108
- if (className.startsWith('theme-')) {
109
- return className.substring(6); // Remove 'theme-' prefix
110
- }
111
- }
112
- return null;
113
- }
114
- function getBaseUrl(doc) {
115
- const baseElement = doc.documentElement;
116
- const raw = baseElement?.getAttribute('data-baseurl') || '';
117
- try {
118
- const u = new URL(raw, 'http://localhost');
119
- // Only allow same-origin relative paths; strip origin used for parsing
120
- return u.origin === 'http://localhost' ? u.pathname.replace(/\/$/, '') : '';
121
- }
122
- catch {
123
- return '';
124
- }
125
- }
126
- async function applyTheme(doc, themeId) {
127
- const theme = THEMES.find((t) => t.id === themeId) || THEMES.find((t) => t.id === DEFAULT_THEME);
128
- const baseUrl = getBaseUrl(doc);
129
- // Add loading state to trigger button
130
- const trigger = doc.getElementById('theme-flavor-trigger');
131
- if (trigger) {
132
- trigger.classList.add('is-loading');
133
- }
134
- try {
135
- // Apply theme class immediately (before CSS loading)
136
- // This ensures the theme is applied even if CSS loading fails
137
- const classList = Array.from(doc.documentElement.classList);
138
- classList.forEach((className) => {
139
- if (className.startsWith('theme-')) {
140
- doc.documentElement.classList.remove(className);
141
- }
142
- });
143
- // Add the new theme class (use resolved theme.id, not the input themeId)
144
- doc.documentElement.classList.add(`theme-${theme.id}`);
145
- // Lazy load theme CSS if not already loaded
146
- // Use resolved theme.id consistently (not the input themeId which may have been invalid)
147
- const themeLinkId = `theme-${theme.id}-css`;
148
- let themeLink = doc.getElementById(themeLinkId);
149
- if (!themeLink) {
150
- themeLink = doc.createElement('link');
151
- themeLink.id = themeLinkId;
152
- themeLink.rel = 'stylesheet';
153
- themeLink.type = 'text/css';
154
- themeLink.setAttribute('data-theme-id', theme.id);
155
- try {
156
- // Resolve path relative to site root
157
- // Use baseUrl if set, otherwise resolve from root
158
- const base = baseUrl
159
- ? `${window.location.origin}${baseUrl}/`
160
- : `${window.location.origin}/`;
161
- const resolvedPath = new URL(theme.cssFile, base).pathname;
162
- themeLink.href = resolvedPath;
163
- }
164
- catch {
165
- console.warn(`Invalid theme CSS path for ${theme.id}`);
166
- // Theme class already applied, so we can return successfully
167
- return;
168
- }
169
- // Add to document head
170
- doc.head.appendChild(themeLink);
171
- // Wait for CSS to load (but don't fail if it doesn't load)
172
- try {
173
- await new Promise((resolve, reject) => {
174
- // Store timeout ID to clear on success/error (prevents memory leak)
175
- const timeoutId = setTimeout(() => {
176
- themeLink.onload = null;
177
- themeLink.onerror = null;
178
- reject(new Error(`Theme ${theme.id} load timeout`));
179
- }, 10000);
180
- themeLink.onload = () => {
181
- clearTimeout(timeoutId);
182
- themeLink.onload = null;
183
- themeLink.onerror = null;
184
- resolve();
185
- };
186
- themeLink.onerror = () => {
187
- clearTimeout(timeoutId);
188
- themeLink.onload = null;
189
- themeLink.onerror = null;
190
- reject(new Error(`Failed to load theme ${theme.id}`));
191
- };
192
- });
193
- }
194
- catch (error) {
195
- // CSS loading failed, but theme class is already applied
196
- // Log the error but don't throw - theme switching should still work
197
- console.warn(`Theme CSS failed to load for ${theme.id}:`, error);
198
- }
199
- }
200
- // Clean up old theme CSS links (keep current and base themes)
201
- const themeLinks = doc.querySelectorAll('link[id^="theme-"][id$="-css"]');
202
- themeLinks.forEach((link) => {
203
- const linkThemeId = link.id.replace('theme-', '').replace('-css', '');
204
- if (linkThemeId !== theme.id && linkThemeId !== 'base') {
205
- link.remove();
206
- }
207
- });
208
- // Update trigger button icon with theme's icon image
209
- const triggerIcon = doc.getElementById('theme-flavor-trigger-icon');
210
- if (triggerIcon && theme.icon) {
211
- try {
212
- // Resolve path relative to site root
213
- // Use baseUrl if set, otherwise resolve from root
214
- const base = baseUrl
215
- ? `${window.location.origin}${baseUrl}/`
216
- : `${window.location.origin}/`;
217
- const resolvedPath = new URL(theme.icon, base).pathname;
218
- triggerIcon.src = resolvedPath;
219
- triggerIcon.alt = `${THEME_FAMILIES[theme.family].name} ${theme.name}`;
220
- triggerIcon.title = `${THEME_FAMILIES[theme.family].name} ${theme.name}`;
221
- }
222
- catch {
223
- console.warn(`Invalid theme icon path for ${theme.id}`);
224
- }
225
- }
226
- // Update active state in dropdown
227
- const dropdownItems = doc.querySelectorAll('#theme-flavor-menu .dropdown-item.theme-item');
228
- dropdownItems.forEach((item) => {
229
- if (item.getAttribute('data-theme-id') === theme.id) {
230
- item.classList.add('is-active');
231
- item.setAttribute('aria-checked', 'true');
232
- }
233
- else {
234
- item.classList.remove('is-active');
235
- item.setAttribute('aria-checked', 'false');
236
- }
237
- });
238
- }
239
- finally {
240
- // Remove loading state
241
- if (trigger) {
242
- trigger.classList.remove('is-loading');
243
- }
244
- }
245
- }
246
- export async function initTheme(documentObj, windowObj) {
247
- // Check if theme was already applied by blocking script
248
- const initialTheme = windowObj.__INITIAL_THEME__;
249
- const savedTheme = windowObj.localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME;
250
- // If blocking script already applied theme and it matches saved, just load CSS if needed
251
- if (initialTheme && initialTheme === savedTheme) {
252
- const currentTheme = getCurrentThemeFromClasses(documentObj.documentElement);
253
- if (currentTheme === savedTheme) {
254
- // Theme class already applied by blocking script, just ensure CSS is loaded
255
- const themeLinkId = `theme-${savedTheme}-css`;
256
- const themeLink = documentObj.getElementById(themeLinkId);
257
- if (!themeLink) {
258
- // CSS not loaded yet, load it now
259
- await applyTheme(documentObj, savedTheme);
260
- }
261
- return;
262
- }
263
- }
264
- // Otherwise, apply theme normally
265
- await applyTheme(documentObj, savedTheme);
266
- }
267
- export function initNavbar(documentObj) {
268
- const currentPath = documentObj.location.pathname;
269
- const navbarItems = documentObj.querySelectorAll('.navbar-item');
270
- // Find the matching link first
271
- let matchingItem = null;
272
- const checkedItems = new Set();
273
- navbarItems.forEach((item) => {
274
- const link = item;
275
- if (link.href) {
276
- try {
277
- const linkPath = new URL(link.href).pathname;
278
- // Remove trailing slashes for comparison
279
- const normalizedCurrentPath = currentPath.replace(/\/$/, '') || '/';
280
- const normalizedLinkPath = linkPath.replace(/\/$/, '') || '/';
281
- checkedItems.add(item);
282
- if (normalizedCurrentPath === normalizedLinkPath) {
283
- matchingItem = item;
284
- }
285
- }
286
- catch {
287
- // Ignore invalid URLs - don't add to checkedItems
288
- }
289
- }
290
- });
291
- // Clear all active states except the matching one (only for items that were checked)
292
- navbarItems.forEach((item) => {
293
- if (item !== matchingItem && checkedItems.has(item)) {
294
- item.classList.remove('is-active');
295
- const link = item;
296
- // Check if removeAttribute exists (for test mocks that might not have it)
297
- if (link && 'removeAttribute' in link && typeof link.removeAttribute === 'function') {
298
- link.removeAttribute('aria-current');
299
- }
300
- }
301
- });
302
- // Set active state for the matching link
303
- if (matchingItem) {
304
- matchingItem.classList.add('is-active');
305
- const link = matchingItem;
306
- // Check if setAttribute exists (for test mocks that might not have it)
307
- if (link && 'setAttribute' in link && typeof link.setAttribute === 'function') {
308
- link.setAttribute('aria-current', 'page');
309
- }
310
- }
311
- // Handle Reports dropdown highlighting
312
- const reportsLink = documentObj.querySelector('[data-testid="nav-reports"]');
313
- if (reportsLink) {
314
- const reportPaths = ['/coverage', '/playwright', '/lighthouse'];
315
- const normalizedCurrentPath = currentPath.replace(/\/$/, '') || '/';
316
- const isOnReportsPage = reportPaths.some((path) => normalizedCurrentPath === path || normalizedCurrentPath.startsWith(path + '/'));
317
- if (isOnReportsPage) {
318
- reportsLink.classList.add('is-active');
319
- }
320
- else {
321
- reportsLink.classList.remove('is-active');
322
- }
323
- }
324
- }
325
- // Only assign to window in browser context
326
- if (typeof window !== 'undefined') {
327
- window.initNavbar = initNavbar;
328
- }
329
- export function wireFlavorSelector(documentObj, windowObj) {
330
- const abortController = new AbortController();
331
- const dropdownMenu = documentObj.getElementById('theme-flavor-menu');
332
- const trigger = documentObj.getElementById('theme-flavor-trigger');
333
- // Get the outer dropdown container (navbar-item has-dropdown) for active state toggling
334
- const dropdown = trigger?.closest('.navbar-item.has-dropdown');
335
- const selectEl = documentObj.getElementById('theme-flavor-select');
336
- const baseUrl = getBaseUrl(documentObj);
337
- if (!dropdownMenu || !trigger || !dropdown) {
338
- return {
339
- cleanup: () => {
340
- abortController.abort();
341
- },
342
- };
343
- }
344
- let currentIndex = -1;
345
- const menuItems = [];
346
- // Get current theme to set initial aria-checked state
347
- const currentThemeId = windowObj.localStorage.getItem(STORAGE_KEY) ||
348
- getCurrentThemeFromClasses(documentObj.documentElement) ||
349
- DEFAULT_THEME;
350
- // Keep optional native select (DDL) in sync with current theme
351
- if (selectEl) {
352
- // Clear any existing options
353
- while (selectEl.firstChild) {
354
- selectEl.removeChild(selectEl.firstChild);
355
- }
356
- THEMES.forEach((theme) => {
357
- const option = documentObj.createElement('option');
358
- option.value = theme.id;
359
- option.textContent = theme.name;
360
- if (theme.id === currentThemeId) {
361
- option.selected = true;
362
- }
363
- selectEl.appendChild(option);
364
- });
365
- // Enable select when JS is active
366
- selectEl.disabled = false;
367
- // Allow changing theme via native select
368
- selectEl.addEventListener('change', (event) => {
369
- const target = event.target;
370
- const selectedThemeId = target?.value || DEFAULT_THEME;
371
- windowObj.localStorage.setItem(STORAGE_KEY, selectedThemeId);
372
- applyTheme(documentObj, selectedThemeId).catch((error) => {
373
- console.error(`Failed to apply theme ${selectedThemeId}:`, error);
374
- });
375
- });
376
- }
377
- // Populate dropdown with grouped theme options
378
- const families = Object.keys(THEME_FAMILIES);
379
- let animationDelay = 0;
380
- families.forEach((familyKey) => {
381
- const familyThemes = THEMES.filter((t) => t.family === familyKey);
382
- if (familyThemes.length === 0)
383
- return;
384
- const familyMeta = THEME_FAMILIES[familyKey];
385
- // Create family group container
386
- const group = documentObj.createElement('div');
387
- group.className = 'theme-family-group';
388
- group.setAttribute('role', 'group');
389
- group.setAttribute('aria-labelledby', `theme-family-${familyKey}`);
390
- if (group.style && typeof group.style.setProperty === 'function') {
391
- group.style.setProperty('--animation-delay', `${animationDelay}ms`);
392
- }
393
- animationDelay += 30;
394
- // Create family header
395
- const header = documentObj.createElement('div');
396
- header.className = 'theme-family-header';
397
- header.id = `theme-family-${familyKey}`;
398
- const headerTitle = documentObj.createElement('span');
399
- headerTitle.className = 'theme-family-name';
400
- headerTitle.textContent = familyMeta.name;
401
- header.appendChild(headerTitle);
402
- group.appendChild(header);
403
- // Create themes container
404
- const themesContainer = documentObj.createElement('div');
405
- themesContainer.className = 'theme-family-items';
406
- familyThemes.forEach((theme) => {
407
- const item = documentObj.createElement('button');
408
- item.type = 'button';
409
- item.className = 'dropdown-item theme-item';
410
- item.setAttribute('data-theme-id', theme.id);
411
- item.setAttribute('data-appearance', theme.appearance);
412
- item.setAttribute('role', 'menuitemradio');
413
- item.setAttribute('aria-label', `${familyMeta.name} ${theme.name} (${theme.appearance}). ${theme.description}`);
414
- item.setAttribute('tabindex', '-1');
415
- const isActive = theme.id === currentThemeId;
416
- item.setAttribute('aria-checked', String(isActive));
417
- if (isActive) {
418
- item.classList.add('is-active');
419
- }
420
- // Icon
421
- const icon = documentObj.createElement('img');
422
- icon.className = 'theme-icon';
423
- if (theme.icon) {
424
- const iconPath = baseUrl ? `${baseUrl}/${theme.icon}` : theme.icon;
425
- icon.src = iconPath;
426
- icon.alt = `${familyMeta.name} ${theme.name}`;
427
- }
428
- icon.width = 24;
429
- icon.height = 24;
430
- item.appendChild(icon);
431
- // Text content
432
- const copy = documentObj.createElement('div');
433
- copy.className = 'theme-copy';
434
- const titleEl = documentObj.createElement('span');
435
- titleEl.className = 'theme-title';
436
- titleEl.textContent = `${familyMeta.name} · ${theme.name}`;
437
- copy.appendChild(titleEl);
438
- const descriptionEl = documentObj.createElement('span');
439
- descriptionEl.className = 'theme-description';
440
- descriptionEl.textContent = theme.description;
441
- copy.appendChild(descriptionEl);
442
- item.appendChild(copy);
443
- // Checkmark for active state
444
- const check = documentObj.createElement('span');
445
- check.className = 'theme-check';
446
- const svg = documentObj.createElementNS('http://www.w3.org/2000/svg', 'svg');
447
- svg.setAttribute('width', '16');
448
- svg.setAttribute('height', '16');
449
- svg.setAttribute('viewBox', '0 0 24 24');
450
- svg.setAttribute('fill', 'none');
451
- svg.setAttribute('stroke', 'currentColor');
452
- svg.setAttribute('stroke-width', '3');
453
- svg.setAttribute('stroke-linecap', 'round');
454
- svg.setAttribute('stroke-linejoin', 'round');
455
- const polyline = documentObj.createElementNS('http://www.w3.org/2000/svg', 'polyline');
456
- polyline.setAttribute('points', '20 6 9 17 4 12');
457
- svg.appendChild(polyline);
458
- check.appendChild(svg);
459
- item.appendChild(check);
460
- item.addEventListener('click', (e) => {
461
- e.preventDefault();
462
- // Always update localStorage and close dropdown, even if CSS loading fails
463
- windowObj.localStorage.setItem(STORAGE_KEY, theme.id);
464
- if (selectEl) {
465
- selectEl.value = theme.id;
466
- // Notify any listeners watching the native select
467
- const changeEvent = new Event('change', { bubbles: true });
468
- selectEl.dispatchEvent(changeEvent);
469
- }
470
- closeDropdown({ restoreFocus: true });
471
- // Apply theme asynchronously (doesn't block dropdown closing)
472
- applyTheme(documentObj, theme.id).catch((error) => {
473
- console.error(`Failed to apply theme ${theme.id}:`, error);
474
- });
475
- });
476
- menuItems.push(item);
477
- themesContainer.appendChild(item);
478
- });
479
- group.appendChild(themesContainer);
480
- dropdownMenu.appendChild(group);
481
- });
482
- // Update aria-expanded on trigger
483
- const updateAriaExpanded = (expanded) => {
484
- if (trigger) {
485
- trigger.setAttribute('aria-expanded', String(expanded));
486
- }
487
- };
488
- // Focus management
489
- const focusMenuItem = (index) => {
490
- if (index < 0 || index >= menuItems.length)
491
- return;
492
- const item = menuItems[index];
493
- // Set tabindex to -1 for all items
494
- menuItems.forEach((menuItem) => {
495
- menuItem.setAttribute('tabindex', '-1');
496
- });
497
- // Focus and set tabindex to 0 on current item
498
- item.setAttribute('tabindex', '0');
499
- item.focus();
500
- currentIndex = index;
501
- };
502
- const closeDropdown = (options = {}) => {
503
- const { restoreFocus = true } = options;
504
- if (dropdown) {
505
- dropdown.classList.remove('is-active');
506
- }
507
- updateAriaExpanded(false);
508
- menuItems.forEach((menuItem) => {
509
- menuItem.setAttribute('tabindex', '-1');
510
- });
511
- currentIndex = -1;
512
- if (restoreFocus && trigger) {
513
- // Only restore focus to trigger when explicitly requested (e.g., selection or Esc)
514
- trigger.focus();
515
- }
516
- };
517
- // Toggle dropdown helper function
518
- const toggleDropdown = (focusFirst = false) => {
519
- if (!dropdown)
520
- return;
521
- const isActive = dropdown.classList.toggle('is-active');
522
- updateAriaExpanded(isActive);
523
- if (!isActive) {
524
- currentIndex = -1;
525
- menuItems.forEach((menuItem) => {
526
- if (menuItem && typeof menuItem.setAttribute === 'function') {
527
- menuItem.setAttribute('tabindex', '-1');
528
- const isActiveItem = menuItem.classList && typeof menuItem.classList.contains === 'function'
529
- ? menuItem.classList.contains('is-active')
530
- : false;
531
- menuItem.setAttribute('aria-checked', String(isActiveItem));
532
- }
533
- });
534
- }
535
- else if (focusFirst && menuItems.length > 0) {
536
- // When opening via keyboard, focus first item
537
- focusMenuItem(0);
538
- }
539
- };
540
- // Toggle dropdown on trigger click (for touch devices)
541
- if (trigger) {
542
- trigger.addEventListener('click', (e) => {
543
- e.preventDefault();
544
- toggleDropdown();
545
- }, { signal: abortController.signal });
546
- }
547
- // Close dropdown when clicking outside
548
- documentObj.addEventListener('click', (e) => {
549
- if (dropdown && !dropdown.contains(e.target)) {
550
- // Close on any outside click; do not steal focus from the newly clicked element
551
- closeDropdown({ restoreFocus: false });
552
- }
553
- }, { signal: abortController.signal });
554
- // Handle Escape key globally to close dropdown
555
- documentObj.addEventListener('keydown', (e) => {
556
- if (e.key === 'Escape' && dropdown && dropdown.classList.contains('is-active')) {
557
- closeDropdown({ restoreFocus: true });
558
- }
559
- }, { signal: abortController.signal });
560
- // Keyboard navigation
561
- if (trigger) {
562
- trigger.addEventListener('keydown', (e) => {
563
- if (!dropdown)
564
- return;
565
- const key = e.key;
566
- if (key === 'Enter' || key === ' ') {
567
- e.preventDefault();
568
- const wasActive = dropdown.classList.contains('is-active');
569
- if (wasActive) {
570
- // If already open, close it
571
- toggleDropdown(false);
572
- }
573
- else {
574
- // If closed, open and focus first item
575
- toggleDropdown(true);
576
- }
577
- }
578
- else if (key === 'ArrowDown') {
579
- e.preventDefault();
580
- if (!dropdown.classList.contains('is-active')) {
581
- dropdown.classList.add('is-active');
582
- updateAriaExpanded(true);
583
- focusMenuItem(0); // Focus first item when opening
584
- }
585
- else {
586
- // If currentIndex is -1 (dropdown opened via mouse or not yet initialized), focus first item
587
- if (currentIndex < 0) {
588
- focusMenuItem(0);
589
- }
590
- else {
591
- const nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
592
- focusMenuItem(nextIndex);
593
- }
594
- }
595
- }
596
- else if (key === 'ArrowUp') {
597
- e.preventDefault();
598
- if (!dropdown.classList.contains('is-active')) {
599
- dropdown.classList.add('is-active');
600
- updateAriaExpanded(true);
601
- // Start from last item when opening with ArrowUp
602
- focusMenuItem(menuItems.length - 1);
603
- }
604
- else {
605
- // If currentIndex is -1 (dropdown opened via mouse), start from last
606
- const startIndex = currentIndex < 0 ? menuItems.length - 1 : currentIndex;
607
- const prevIndex = startIndex > 0 ? startIndex - 1 : menuItems.length - 1;
608
- focusMenuItem(prevIndex);
609
- }
610
- }
611
- }, { signal: abortController.signal });
612
- }
613
- // Keyboard navigation on menu items
614
- menuItems.forEach((item, index) => {
615
- item.addEventListener('keydown', (e) => {
616
- const key = e.key;
617
- if (key === 'ArrowDown') {
618
- e.preventDefault();
619
- const nextIndex = index < menuItems.length - 1 ? index + 1 : 0;
620
- focusMenuItem(nextIndex);
621
- }
622
- else if (key === 'ArrowUp') {
623
- e.preventDefault();
624
- const prevIndex = index > 0 ? index - 1 : menuItems.length - 1;
625
- focusMenuItem(prevIndex);
626
- }
627
- else if (key === 'Escape') {
628
- e.preventDefault();
629
- closeDropdown();
630
- }
631
- else if (key === 'Enter' || key === ' ') {
632
- e.preventDefault();
633
- item.click();
634
- }
635
- else if (key === 'Home') {
636
- e.preventDefault();
637
- focusMenuItem(0);
638
- }
639
- else if (key === 'End') {
640
- e.preventDefault();
641
- focusMenuItem(menuItems.length - 1);
642
- }
643
- }, { signal: abortController.signal });
644
- });
645
- // Initialize aria-expanded
646
- updateAriaExpanded(false);
647
- // For navbar dropdown, ensure proper initial state
648
- if (dropdown) {
649
- dropdown.classList.remove('is-active');
650
- }
651
- return {
652
- cleanup: () => {
653
- abortController.abort();
654
- },
655
- };
656
- }
657
- export function enhanceAccessibility(documentObj) {
658
- const pres = documentObj.querySelectorAll('.highlight > pre');
659
- pres.forEach((pre) => {
660
- if (!pre.hasAttribute('tabindex'))
661
- pre.setAttribute('tabindex', '0');
662
- if (!pre.hasAttribute('role'))
663
- pre.setAttribute('role', 'region');
664
- if (!pre.hasAttribute('aria-label'))
665
- pre.setAttribute('aria-label', 'Code block');
666
- });
667
- }
668
- // Auto-initialize on DOMContentLoaded
669
- if (typeof document !== 'undefined' && typeof window !== 'undefined') {
670
- document.addEventListener('DOMContentLoaded', () => {
671
- initTheme(document, window)
672
- .then(() => {
673
- const { cleanup } = wireFlavorSelector(document, window);
674
- enhanceAccessibility(document);
675
- // Register cleanup to run on teardown
676
- const pagehideHandler = () => {
677
- cleanup();
678
- window.removeEventListener('pagehide', pagehideHandler);
679
- };
680
- window.addEventListener('pagehide', pagehideHandler);
681
- })
682
- .catch((error) => {
683
- console.error('Theme switcher initialization failed:', error);
684
- });
685
- });
686
- }
687
- //# sourceMappingURL=index.js.map