bulma-turbo-themes 0.7.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +319 -0
- data/LICENSE +21 -0
- data/README.md +175 -0
- data/assets/css/custom.css +32 -0
- data/assets/css/themes/bulma-dark.css +96 -0
- data/assets/css/themes/bulma-light.css +96 -0
- data/assets/css/themes/catppuccin-frappe.css +99 -0
- data/assets/css/themes/catppuccin-latte.css +99 -0
- data/assets/css/themes/catppuccin-macchiato.css +99 -0
- data/assets/css/themes/catppuccin-mocha.css +99 -0
- data/assets/css/themes/dracula.css +212 -0
- data/assets/css/themes/github-dark.css +103 -0
- data/assets/css/themes/github-light.css +103 -0
- data/assets/css/themes/global.css +460 -0
- data/assets/img/bulma-logo-dark.png +0 -0
- data/assets/img/bulma-logo.png +0 -0
- data/assets/img/catppuccin-logo-latte.png +0 -0
- data/assets/img/catppuccin-logo-macchiato.png +0 -0
- data/assets/img/dracula-logo.png +0 -0
- data/assets/img/github-logo-dark.png +0 -0
- data/assets/img/github-logo-light.png +0 -0
- data/assets/js/theme-selector.js +489 -0
- data/lib/bulma-turbo-themes/version.rb +6 -0
- data/lib/bulma-turbo-themes.rb +9 -0
- metadata +93 -0
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
const THEMES = [
|
|
2
|
+
{
|
|
3
|
+
id: 'bulma-light',
|
|
4
|
+
name: 'Bulma Light',
|
|
5
|
+
cssFile: 'assets/css/themes/bulma-light.css',
|
|
6
|
+
icon: 'assets/img/bulma-logo.png',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
id: 'bulma-dark',
|
|
10
|
+
name: 'Bulma Dark',
|
|
11
|
+
cssFile: 'assets/css/themes/bulma-dark.css',
|
|
12
|
+
icon: 'assets/img/bulma-logo-dark.png',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'catppuccin-latte',
|
|
16
|
+
name: 'Catppuccin Latte',
|
|
17
|
+
cssFile: 'assets/css/themes/catppuccin-latte.css',
|
|
18
|
+
icon: 'assets/img/catppuccin-logo-latte.png',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'catppuccin-frappe',
|
|
22
|
+
name: 'Catppuccin Frappé',
|
|
23
|
+
cssFile: 'assets/css/themes/catppuccin-frappe.css',
|
|
24
|
+
icon: 'assets/img/catppuccin-logo-latte.png',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'catppuccin-macchiato',
|
|
28
|
+
name: 'Catppuccin Macchiato',
|
|
29
|
+
cssFile: 'assets/css/themes/catppuccin-macchiato.css',
|
|
30
|
+
icon: 'assets/img/catppuccin-logo-macchiato.png',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'catppuccin-mocha',
|
|
34
|
+
name: 'Catppuccin Mocha',
|
|
35
|
+
cssFile: 'assets/css/themes/catppuccin-mocha.css',
|
|
36
|
+
icon: 'assets/img/catppuccin-logo-macchiato.png',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'dracula',
|
|
40
|
+
name: 'Dracula',
|
|
41
|
+
cssFile: 'assets/css/themes/dracula.css',
|
|
42
|
+
icon: 'assets/img/dracula-logo.png',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: 'github-light',
|
|
46
|
+
name: 'GitHub Light',
|
|
47
|
+
cssFile: 'assets/css/themes/github-light.css',
|
|
48
|
+
icon: 'assets/img/github-logo-light.png',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'github-dark',
|
|
52
|
+
name: 'GitHub Dark',
|
|
53
|
+
cssFile: 'assets/css/themes/github-dark.css',
|
|
54
|
+
icon: 'assets/img/github-logo-dark.png',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const STORAGE_KEY = 'bulma-theme-flavor';
|
|
58
|
+
const DEFAULT_THEME = 'catppuccin-mocha';
|
|
59
|
+
function getBaseUrl(doc) {
|
|
60
|
+
const baseElement = doc.documentElement;
|
|
61
|
+
const raw = baseElement?.getAttribute('data-baseurl') || '';
|
|
62
|
+
try {
|
|
63
|
+
const u = new URL(raw, 'http://localhost');
|
|
64
|
+
// Only allow same-origin relative paths; strip origin used for parsing
|
|
65
|
+
return u.origin === 'http://localhost' ? u.pathname.replace(/\/$/, '') : '';
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return '';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function applyTheme(doc, themeId) {
|
|
72
|
+
const theme = THEMES.find((t) => t.id === themeId) || THEMES.find((t) => t.id === DEFAULT_THEME);
|
|
73
|
+
const baseUrl = getBaseUrl(doc);
|
|
74
|
+
const flavorLink = doc.getElementById('theme-flavor-css');
|
|
75
|
+
if (flavorLink) {
|
|
76
|
+
// Build a safe URL relative to base by prepending baseUrl to relative path
|
|
77
|
+
try {
|
|
78
|
+
const fullPath = baseUrl ? `${baseUrl}/${theme.cssFile}` : theme.cssFile;
|
|
79
|
+
const url = new URL(fullPath, 'http://localhost');
|
|
80
|
+
flavorLink.href = url.pathname;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Ignore invalid URL
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
doc.documentElement.setAttribute('data-flavor', themeId);
|
|
87
|
+
// Update trigger button icon
|
|
88
|
+
const triggerIcon = doc.getElementById('theme-flavor-trigger-icon');
|
|
89
|
+
if (triggerIcon) {
|
|
90
|
+
// Clear existing content first
|
|
91
|
+
while (triggerIcon.firstChild) {
|
|
92
|
+
triggerIcon.removeChild(triggerIcon.firstChild);
|
|
93
|
+
}
|
|
94
|
+
if (theme.icon) {
|
|
95
|
+
// Create and append img element (CSP-friendly)
|
|
96
|
+
const img = doc.createElement('img');
|
|
97
|
+
try {
|
|
98
|
+
const fullPath = baseUrl ? `${baseUrl}/${theme.icon}` : theme.icon;
|
|
99
|
+
const url = new URL(fullPath, 'http://localhost');
|
|
100
|
+
img.src = url.pathname;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Ignore invalid URL
|
|
104
|
+
}
|
|
105
|
+
img.alt = theme.name;
|
|
106
|
+
img.title = theme.name; // Tooltip on hover
|
|
107
|
+
img.width = 28;
|
|
108
|
+
img.height = 28;
|
|
109
|
+
triggerIcon.appendChild(img);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
// Fallback: show first two letters with circular background
|
|
113
|
+
const span = doc.createElement('span');
|
|
114
|
+
span.textContent = theme.name.substring(0, 2).toUpperCase();
|
|
115
|
+
span.style.fontSize = '12px';
|
|
116
|
+
span.style.fontWeight = 'bold';
|
|
117
|
+
span.style.color = 'var(--theme-text, inherit)';
|
|
118
|
+
span.style.display = 'flex';
|
|
119
|
+
span.style.alignItems = 'center';
|
|
120
|
+
span.style.justifyContent = 'center';
|
|
121
|
+
span.style.width = '28px';
|
|
122
|
+
span.style.height = '28px';
|
|
123
|
+
span.style.borderRadius = '50%';
|
|
124
|
+
span.style.backgroundColor = 'var(--theme-surface-1, #f5f5f5)';
|
|
125
|
+
span.style.border = '1px solid var(--theme-border, #ddd)';
|
|
126
|
+
span.title = theme.name; // Tooltip on hover
|
|
127
|
+
triggerIcon.appendChild(span);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Update active state in dropdown
|
|
131
|
+
const dropdownItems = doc.querySelectorAll('#theme-flavor-items .dropdown-item');
|
|
132
|
+
dropdownItems.forEach((item) => {
|
|
133
|
+
if (item.getAttribute('data-theme-id') === themeId) {
|
|
134
|
+
item.classList.add('is-active');
|
|
135
|
+
item.setAttribute('aria-checked', 'true');
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
item.classList.remove('is-active');
|
|
139
|
+
item.setAttribute('aria-checked', 'false');
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
export function initTheme(documentObj, windowObj) {
|
|
144
|
+
const savedTheme = windowObj.localStorage.getItem(STORAGE_KEY) || DEFAULT_THEME;
|
|
145
|
+
applyTheme(documentObj, savedTheme);
|
|
146
|
+
}
|
|
147
|
+
export function initNavbar(documentObj) {
|
|
148
|
+
const currentPath = documentObj.location.pathname;
|
|
149
|
+
const navbarItems = documentObj.querySelectorAll('.navbar-item');
|
|
150
|
+
// Find the matching link first
|
|
151
|
+
let matchingItem = null;
|
|
152
|
+
const checkedItems = new Set();
|
|
153
|
+
navbarItems.forEach((item) => {
|
|
154
|
+
const link = item;
|
|
155
|
+
if (link.href) {
|
|
156
|
+
try {
|
|
157
|
+
const linkPath = new URL(link.href).pathname;
|
|
158
|
+
// Remove trailing slashes for comparison
|
|
159
|
+
const normalizedCurrentPath = currentPath.replace(/\/$/, '') || '/';
|
|
160
|
+
const normalizedLinkPath = linkPath.replace(/\/$/, '') || '/';
|
|
161
|
+
checkedItems.add(item);
|
|
162
|
+
if (normalizedCurrentPath === normalizedLinkPath) {
|
|
163
|
+
matchingItem = item;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// Ignore invalid URLs - don't add to checkedItems
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
// Clear all active states except the matching one (only for items that were checked)
|
|
172
|
+
navbarItems.forEach((item) => {
|
|
173
|
+
if (item !== matchingItem && checkedItems.has(item)) {
|
|
174
|
+
item.classList.remove('is-active');
|
|
175
|
+
const link = item;
|
|
176
|
+
// Check if removeAttribute exists (for test mocks that might not have it)
|
|
177
|
+
if (link && 'removeAttribute' in link && typeof link.removeAttribute === 'function') {
|
|
178
|
+
link.removeAttribute('aria-current');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
// Set active state for the matching link
|
|
183
|
+
if (matchingItem) {
|
|
184
|
+
matchingItem.classList.add('is-active');
|
|
185
|
+
const link = matchingItem;
|
|
186
|
+
// Check if setAttribute exists (for test mocks that might not have it)
|
|
187
|
+
if (link && 'setAttribute' in link && typeof link.setAttribute === 'function') {
|
|
188
|
+
link.setAttribute('aria-current', 'page');
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Handle Reports dropdown highlighting
|
|
192
|
+
const reportsLink = documentObj.querySelector('[data-testid="nav-reports"]');
|
|
193
|
+
if (reportsLink) {
|
|
194
|
+
const reportPaths = ['/coverage', '/playwright', '/lighthouse'];
|
|
195
|
+
const normalizedCurrentPath = currentPath.replace(/\/$/, '') || '/';
|
|
196
|
+
const isOnReportsPage = reportPaths.some((path) => normalizedCurrentPath === path || normalizedCurrentPath.startsWith(path + '/'));
|
|
197
|
+
if (isOnReportsPage) {
|
|
198
|
+
reportsLink.classList.add('is-active');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
reportsLink.classList.remove('is-active');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
window.initNavbar = initNavbar;
|
|
206
|
+
export function wireFlavorSelector(documentObj, windowObj) {
|
|
207
|
+
const abortController = new AbortController();
|
|
208
|
+
const baseUrl = getBaseUrl(documentObj);
|
|
209
|
+
const dropdownContent = documentObj.getElementById('theme-flavor-items');
|
|
210
|
+
const trigger = documentObj.querySelector('.theme-flavor-trigger');
|
|
211
|
+
const dropdown = documentObj.getElementById('theme-flavor-dd');
|
|
212
|
+
if (!dropdownContent || !trigger || !dropdown) {
|
|
213
|
+
return {
|
|
214
|
+
cleanup: () => {
|
|
215
|
+
abortController.abort();
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
let currentIndex = -1;
|
|
220
|
+
const menuItems = [];
|
|
221
|
+
// Get current theme to set initial aria-checked state
|
|
222
|
+
const currentThemeId = windowObj.localStorage.getItem(STORAGE_KEY) ||
|
|
223
|
+
documentObj.documentElement.getAttribute('data-flavor') ||
|
|
224
|
+
DEFAULT_THEME;
|
|
225
|
+
// Populate dropdown with theme options
|
|
226
|
+
THEMES.forEach((theme) => {
|
|
227
|
+
const item = documentObj.createElement('a');
|
|
228
|
+
item.href = '#';
|
|
229
|
+
item.className = 'dropdown-item';
|
|
230
|
+
item.setAttribute('data-theme-id', theme.id);
|
|
231
|
+
item.setAttribute('role', 'menuitemradio');
|
|
232
|
+
item.setAttribute('aria-label', theme.name);
|
|
233
|
+
item.setAttribute('tabindex', '-1');
|
|
234
|
+
const isActive = theme.id === currentThemeId;
|
|
235
|
+
item.setAttribute('aria-checked', String(isActive));
|
|
236
|
+
if (isActive) {
|
|
237
|
+
item.classList.add('is-active');
|
|
238
|
+
}
|
|
239
|
+
if (theme.icon) {
|
|
240
|
+
const img = documentObj.createElement('img');
|
|
241
|
+
try {
|
|
242
|
+
const fullPath = baseUrl ? `${baseUrl}/${theme.icon}` : theme.icon;
|
|
243
|
+
const url = new URL(fullPath, 'http://localhost');
|
|
244
|
+
img.src = url.pathname;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Ignore invalid URL
|
|
248
|
+
}
|
|
249
|
+
img.alt = theme.name;
|
|
250
|
+
img.title = theme.name;
|
|
251
|
+
img.width = 28;
|
|
252
|
+
img.height = 28;
|
|
253
|
+
item.appendChild(img);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
// Fallback: show first two letters with styled background
|
|
257
|
+
const span = documentObj.createElement('span');
|
|
258
|
+
span.textContent = theme.name.substring(0, 2);
|
|
259
|
+
span.style.fontSize = '14px';
|
|
260
|
+
span.style.fontWeight = 'bold';
|
|
261
|
+
span.style.color = 'var(--theme-text, inherit)';
|
|
262
|
+
item.appendChild(span);
|
|
263
|
+
}
|
|
264
|
+
// Always include a visually hidden full name for screen readers
|
|
265
|
+
const srOnly = documentObj.createElement('span');
|
|
266
|
+
srOnly.textContent = theme.name;
|
|
267
|
+
srOnly.style.position = 'absolute';
|
|
268
|
+
srOnly.style.width = '1px';
|
|
269
|
+
srOnly.style.height = '1px';
|
|
270
|
+
srOnly.style.padding = '0';
|
|
271
|
+
srOnly.style.margin = '-1px';
|
|
272
|
+
srOnly.style.overflow = 'hidden';
|
|
273
|
+
srOnly.style.clip = 'rect(0, 0, 0, 0)';
|
|
274
|
+
srOnly.style.whiteSpace = 'nowrap';
|
|
275
|
+
srOnly.style.border = '0';
|
|
276
|
+
item.appendChild(srOnly);
|
|
277
|
+
item.addEventListener('click', (e) => {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
applyTheme(documentObj, theme.id);
|
|
280
|
+
windowObj.localStorage.setItem(STORAGE_KEY, theme.id);
|
|
281
|
+
closeDropdown({ restoreFocus: true });
|
|
282
|
+
});
|
|
283
|
+
menuItems.push(item);
|
|
284
|
+
dropdownContent.appendChild(item);
|
|
285
|
+
});
|
|
286
|
+
// Update aria-expanded on trigger
|
|
287
|
+
const updateAriaExpanded = (expanded) => {
|
|
288
|
+
trigger.setAttribute('aria-expanded', String(expanded));
|
|
289
|
+
};
|
|
290
|
+
// Focus management
|
|
291
|
+
const focusMenuItem = (index) => {
|
|
292
|
+
if (index < 0 || index >= menuItems.length)
|
|
293
|
+
return;
|
|
294
|
+
const item = menuItems[index];
|
|
295
|
+
// Set tabindex to -1 for all items
|
|
296
|
+
menuItems.forEach((menuItem) => {
|
|
297
|
+
menuItem.setAttribute('tabindex', '-1');
|
|
298
|
+
});
|
|
299
|
+
// Focus and set tabindex to 0 on current item
|
|
300
|
+
item.setAttribute('tabindex', '0');
|
|
301
|
+
item.focus();
|
|
302
|
+
currentIndex = index;
|
|
303
|
+
};
|
|
304
|
+
const openDropdown = () => {
|
|
305
|
+
dropdown.classList.add('is-active');
|
|
306
|
+
updateAriaExpanded(true);
|
|
307
|
+
currentIndex = -1;
|
|
308
|
+
};
|
|
309
|
+
const closeDropdown = (options = {}) => {
|
|
310
|
+
const { restoreFocus = true } = options;
|
|
311
|
+
dropdown.classList.remove('is-active');
|
|
312
|
+
updateAriaExpanded(false);
|
|
313
|
+
menuItems.forEach((menuItem) => {
|
|
314
|
+
menuItem.setAttribute('tabindex', '-1');
|
|
315
|
+
});
|
|
316
|
+
currentIndex = -1;
|
|
317
|
+
if (restoreFocus) {
|
|
318
|
+
// Only restore focus to trigger when explicitly requested (e.g., selection or Esc)
|
|
319
|
+
trigger.focus();
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
// Open dropdown on hover
|
|
323
|
+
dropdown.addEventListener('mouseenter', () => {
|
|
324
|
+
openDropdown();
|
|
325
|
+
}, { signal: abortController.signal });
|
|
326
|
+
// Close dropdown when mouse leaves
|
|
327
|
+
dropdown.addEventListener('mouseleave', () => {
|
|
328
|
+
// Only close if not keyboard navigating
|
|
329
|
+
if (currentIndex === -1) {
|
|
330
|
+
closeDropdown();
|
|
331
|
+
}
|
|
332
|
+
}, { signal: abortController.signal });
|
|
333
|
+
// Toggle dropdown helper function
|
|
334
|
+
const toggleDropdown = (focusFirst = false) => {
|
|
335
|
+
const isActive = dropdown.classList.toggle('is-active');
|
|
336
|
+
updateAriaExpanded(isActive);
|
|
337
|
+
if (!isActive) {
|
|
338
|
+
currentIndex = -1;
|
|
339
|
+
menuItems.forEach((menuItem) => {
|
|
340
|
+
menuItem.setAttribute('tabindex', '-1');
|
|
341
|
+
menuItem.setAttribute('aria-checked', String(menuItem.classList.contains('is-active')));
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
else if (focusFirst && menuItems.length > 0) {
|
|
345
|
+
// ...rest of the existing logic
|
|
346
|
+
// When opening via keyboard, focus first item
|
|
347
|
+
focusMenuItem(0);
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
// Toggle dropdown on trigger click (for touch devices)
|
|
351
|
+
trigger.addEventListener('click', (e) => {
|
|
352
|
+
e.preventDefault();
|
|
353
|
+
toggleDropdown();
|
|
354
|
+
}, { signal: abortController.signal });
|
|
355
|
+
// Close dropdown when clicking outside
|
|
356
|
+
documentObj.addEventListener('click', (e) => {
|
|
357
|
+
if (!dropdown.contains(e.target)) {
|
|
358
|
+
// Close on any outside click; do not steal focus from the newly clicked element
|
|
359
|
+
closeDropdown({ restoreFocus: false });
|
|
360
|
+
}
|
|
361
|
+
}, { signal: abortController.signal });
|
|
362
|
+
// Handle Escape key globally to close dropdown
|
|
363
|
+
documentObj.addEventListener('keydown', (e) => {
|
|
364
|
+
if (e.key === 'Escape' && dropdown.classList.contains('is-active')) {
|
|
365
|
+
closeDropdown({ restoreFocus: true });
|
|
366
|
+
}
|
|
367
|
+
}, { signal: abortController.signal });
|
|
368
|
+
// Keyboard navigation
|
|
369
|
+
trigger.addEventListener('keydown', (e) => {
|
|
370
|
+
const key = e.key;
|
|
371
|
+
if (key === 'Enter' || key === ' ') {
|
|
372
|
+
e.preventDefault();
|
|
373
|
+
const wasActive = dropdown.classList.contains('is-active');
|
|
374
|
+
if (wasActive) {
|
|
375
|
+
// If already open, close it
|
|
376
|
+
toggleDropdown(false);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// If closed, open and focus first item
|
|
380
|
+
toggleDropdown(true);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (key === 'ArrowDown') {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
if (!dropdown.classList.contains('is-active')) {
|
|
386
|
+
dropdown.classList.add('is-active');
|
|
387
|
+
updateAriaExpanded(true);
|
|
388
|
+
focusMenuItem(0); // Focus first item when opening
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// If currentIndex is -1 (dropdown opened via mouse or not yet initialized), focus first item
|
|
392
|
+
if (currentIndex < 0) {
|
|
393
|
+
focusMenuItem(0);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
const nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
|
|
397
|
+
focusMenuItem(nextIndex);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (key === 'ArrowUp') {
|
|
402
|
+
e.preventDefault();
|
|
403
|
+
if (!dropdown.classList.contains('is-active')) {
|
|
404
|
+
dropdown.classList.add('is-active');
|
|
405
|
+
updateAriaExpanded(true);
|
|
406
|
+
// Start from last item when opening with ArrowUp
|
|
407
|
+
focusMenuItem(menuItems.length - 1);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
// If currentIndex is -1 (dropdown opened via mouse), start from last
|
|
411
|
+
const startIndex = currentIndex < 0 ? menuItems.length - 1 : currentIndex;
|
|
412
|
+
const prevIndex = startIndex > 0 ? startIndex - 1 : menuItems.length - 1;
|
|
413
|
+
focusMenuItem(prevIndex);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}, { signal: abortController.signal });
|
|
417
|
+
// Keyboard navigation on menu items
|
|
418
|
+
menuItems.forEach((item, index) => {
|
|
419
|
+
item.addEventListener('keydown', (e) => {
|
|
420
|
+
const key = e.key;
|
|
421
|
+
if (key === 'ArrowDown') {
|
|
422
|
+
e.preventDefault();
|
|
423
|
+
const nextIndex = index < menuItems.length - 1 ? index + 1 : 0;
|
|
424
|
+
focusMenuItem(nextIndex);
|
|
425
|
+
}
|
|
426
|
+
else if (key === 'ArrowUp') {
|
|
427
|
+
e.preventDefault();
|
|
428
|
+
const prevIndex = index > 0 ? index - 1 : menuItems.length - 1;
|
|
429
|
+
focusMenuItem(prevIndex);
|
|
430
|
+
}
|
|
431
|
+
else if (key === 'Escape') {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
closeDropdown();
|
|
434
|
+
}
|
|
435
|
+
else if (key === 'Enter' || key === ' ') {
|
|
436
|
+
e.preventDefault();
|
|
437
|
+
item.click();
|
|
438
|
+
}
|
|
439
|
+
else if (key === 'Home') {
|
|
440
|
+
e.preventDefault();
|
|
441
|
+
focusMenuItem(0);
|
|
442
|
+
}
|
|
443
|
+
else if (key === 'End') {
|
|
444
|
+
e.preventDefault();
|
|
445
|
+
focusMenuItem(menuItems.length - 1);
|
|
446
|
+
}
|
|
447
|
+
}, { signal: abortController.signal });
|
|
448
|
+
});
|
|
449
|
+
// Initialize aria-expanded
|
|
450
|
+
updateAriaExpanded(false);
|
|
451
|
+
return {
|
|
452
|
+
cleanup: () => {
|
|
453
|
+
abortController.abort();
|
|
454
|
+
},
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
export function enhanceAccessibility(documentObj) {
|
|
458
|
+
const pres = documentObj.querySelectorAll('.highlight > pre');
|
|
459
|
+
pres.forEach((pre) => {
|
|
460
|
+
if (!pre.hasAttribute('tabindex'))
|
|
461
|
+
pre.setAttribute('tabindex', '0');
|
|
462
|
+
if (!pre.hasAttribute('role'))
|
|
463
|
+
pre.setAttribute('role', 'region');
|
|
464
|
+
if (!pre.hasAttribute('aria-label'))
|
|
465
|
+
pre.setAttribute('aria-label', 'Code block');
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Auto-initialize on DOMContentLoaded
|
|
469
|
+
if (typeof document !== 'undefined' && typeof window !== 'undefined') {
|
|
470
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
471
|
+
console.warn('Theme switcher initializing...');
|
|
472
|
+
try {
|
|
473
|
+
initTheme(document, window);
|
|
474
|
+
const { cleanup } = wireFlavorSelector(document, window);
|
|
475
|
+
enhanceAccessibility(document);
|
|
476
|
+
// Register cleanup to run on teardown
|
|
477
|
+
const pagehideHandler = () => {
|
|
478
|
+
cleanup();
|
|
479
|
+
window.removeEventListener('pagehide', pagehideHandler);
|
|
480
|
+
};
|
|
481
|
+
window.addEventListener('pagehide', pagehideHandler);
|
|
482
|
+
console.warn('Theme switcher initialized successfully');
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
console.error('Theme switcher initialization failed:', error);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
//# sourceMappingURL=index.js.map
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: bulma-turbo-themes
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.7.4
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Turbo Coder
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-11-16 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jekyll
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.5'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '5.0'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '3.5'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '5.0'
|
|
33
|
+
description: Bulma Turbo Themes provides multiple color schemes (Catppuccin, Dracula,
|
|
34
|
+
GitHub) and an accessible theme selector component for Jekyll sites.
|
|
35
|
+
email:
|
|
36
|
+
- turbocoder13@users.noreply.github.com
|
|
37
|
+
executables: []
|
|
38
|
+
extensions: []
|
|
39
|
+
extra_rdoc_files: []
|
|
40
|
+
files:
|
|
41
|
+
- CHANGELOG.md
|
|
42
|
+
- LICENSE
|
|
43
|
+
- README.md
|
|
44
|
+
- assets/css/custom.css
|
|
45
|
+
- assets/css/themes/bulma-dark.css
|
|
46
|
+
- assets/css/themes/bulma-light.css
|
|
47
|
+
- assets/css/themes/catppuccin-frappe.css
|
|
48
|
+
- assets/css/themes/catppuccin-latte.css
|
|
49
|
+
- assets/css/themes/catppuccin-macchiato.css
|
|
50
|
+
- assets/css/themes/catppuccin-mocha.css
|
|
51
|
+
- assets/css/themes/dracula.css
|
|
52
|
+
- assets/css/themes/github-dark.css
|
|
53
|
+
- assets/css/themes/github-light.css
|
|
54
|
+
- assets/css/themes/global.css
|
|
55
|
+
- assets/img/bulma-logo-dark.png
|
|
56
|
+
- assets/img/bulma-logo.png
|
|
57
|
+
- assets/img/catppuccin-logo-latte.png
|
|
58
|
+
- assets/img/catppuccin-logo-macchiato.png
|
|
59
|
+
- assets/img/dracula-logo.png
|
|
60
|
+
- assets/img/github-logo-dark.png
|
|
61
|
+
- assets/img/github-logo-light.png
|
|
62
|
+
- assets/js/theme-selector.js
|
|
63
|
+
- lib/bulma-turbo-themes.rb
|
|
64
|
+
- lib/bulma-turbo-themes/version.rb
|
|
65
|
+
homepage: https://github.com/TurboCoder13/bulma-turbo-themes
|
|
66
|
+
licenses:
|
|
67
|
+
- MIT
|
|
68
|
+
metadata:
|
|
69
|
+
bug_tracker_uri: https://github.com/TurboCoder13/bulma-turbo-themes/issues
|
|
70
|
+
changelog_uri: https://github.com/TurboCoder13/bulma-turbo-themes/blob/main/CHANGELOG.md
|
|
71
|
+
documentation_uri: https://turbocoder13.github.io/bulma-turbo-themes/
|
|
72
|
+
homepage_uri: https://github.com/TurboCoder13/bulma-turbo-themes
|
|
73
|
+
source_code_uri: https://github.com/TurboCoder13/bulma-turbo-themes
|
|
74
|
+
post_install_message:
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 2.6.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.5.22
|
|
90
|
+
signing_key:
|
|
91
|
+
specification_version: 4
|
|
92
|
+
summary: Modern, accessible theme packs and a drop-in theme selector for Bulma 1.x
|
|
93
|
+
test_files: []
|