rizzo-css 0.0.7 → 0.0.9

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.
@@ -4,73 +4,461 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>{{TITLE}}</title>
7
+ <!-- Theme flash prevention: apply saved theme/settings before first paint (same as main site) -->
8
+ <script>
9
+ (function() {
10
+ try {
11
+ var savedTheme = localStorage.getItem('theme');
12
+ var defaultDark = 'github-dark-classic';
13
+ var defaultLight = 'github-light';
14
+ var resolvedTheme;
15
+ if (!savedTheme) {
16
+ var initialTheme = document.documentElement.getAttribute('data-theme');
17
+ resolvedTheme = initialTheme || (window.matchMedia('(prefers-color-scheme: dark)').matches ? defaultDark : defaultLight);
18
+ } else if (savedTheme === 'system') {
19
+ resolvedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? defaultDark : defaultLight;
20
+ } else {
21
+ resolvedTheme = savedTheme;
22
+ }
23
+ document.documentElement.setAttribute('data-theme', resolvedTheme);
24
+ var savedFontScale = localStorage.getItem('fontSizeScale');
25
+ if (savedFontScale) {
26
+ document.documentElement.style.setProperty('--font-size-scale', savedFontScale);
27
+ }
28
+ if (localStorage.getItem('reducedMotion') === 'true') {
29
+ document.documentElement.classList.add('reduced-motion');
30
+ }
31
+ if (localStorage.getItem('highContrast') === 'true') {
32
+ document.documentElement.classList.add('high-contrast');
33
+ }
34
+ var savedScrollbarStyle = localStorage.getItem('scrollbarStyle') || 'thin';
35
+ if (savedScrollbarStyle === 'thick') {
36
+ document.documentElement.classList.add('scrollbar-thick');
37
+ } else if (savedScrollbarStyle === 'hidden') {
38
+ document.documentElement.classList.add('scrollbar-hidden', 'hide-scrollbars');
39
+ }
40
+ if (!savedScrollbarStyle && localStorage.getItem('hideScrollbars') === 'true') {
41
+ document.documentElement.classList.add('scrollbar-hidden', 'hide-scrollbars');
42
+ localStorage.setItem('scrollbarStyle', 'hidden');
43
+ }
44
+ } catch (e) {}
45
+ })();
46
+ </script>
47
+ <!-- Toast: showToast, removeToast, removeAllToasts (same as main site) -->
48
+ <script>
49
+ (function() {
50
+ if (typeof window === 'undefined' || window.showToast) return;
51
+ function showToast(message, options) {
52
+ if (!message) return null;
53
+ options = options || {};
54
+ var variant = options.variant || 'info';
55
+ var position = options.position || 'top-right';
56
+ var autoDismiss = options.autoDismiss !== undefined ? options.autoDismiss : 5000;
57
+ var dismissible = options.dismissible !== undefined ? options.dismissible : true;
58
+ var toastId = 'toast-' + Math.random().toString(36).substr(2, 9);
59
+ function createToast() {
60
+ if (!document.body) return;
61
+ var containerId = 'toast-container-' + position;
62
+ var container = document.getElementById(containerId);
63
+ if (!container) {
64
+ container = document.createElement('div');
65
+ container.id = containerId;
66
+ container.className = 'toast-container toast-container--' + position;
67
+ container.style.cssText = 'display:flex;visibility:visible;z-index:1100;';
68
+ document.body.appendChild(container);
69
+ }
70
+ var toast = document.createElement('div');
71
+ toast.id = toastId;
72
+ toast.className = 'alert alert--' + variant;
73
+ toast.setAttribute('role', 'alert');
74
+ toast.setAttribute('aria-live', 'polite');
75
+ toast.style.cssText = 'display:flex;visibility:visible;opacity:0;transition:opacity 0.3s ease-out, transform 0.3s ease-out;';
76
+ var isRight = position.indexOf('right') !== -1;
77
+ var isLeft = position.indexOf('left') !== -1;
78
+ toast.style.transform = isRight ? 'translateX(100%)' : isLeft ? 'translateX(-100%)' : 'translateY(-100%)';
79
+ var content = document.createElement('div');
80
+ content.className = 'alert__content';
81
+ content.textContent = message;
82
+ toast.appendChild(content);
83
+ var closeBtn;
84
+ if (dismissible) {
85
+ closeBtn = document.createElement('button');
86
+ closeBtn.type = 'button';
87
+ closeBtn.className = 'alert__close';
88
+ closeBtn.setAttribute('aria-label', 'Dismiss toast');
89
+ closeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="4" x2="12" y2="12"></line><line x1="12" y1="4" x2="4" y2="12"></line></svg>';
90
+ toast.appendChild(closeBtn);
91
+ closeBtn.addEventListener('click', function() {
92
+ toast.style.opacity = '0';
93
+ toast.style.transform = isRight ? 'translateX(100%)' : isLeft ? 'translateX(-100%)' : 'translateY(-100%)';
94
+ setTimeout(function() {
95
+ if (toast.parentNode) toast.remove();
96
+ if (container.children.length === 0) container.remove();
97
+ }, 300);
98
+ });
99
+ }
100
+ container.appendChild(toast);
101
+ requestAnimationFrame(function() {
102
+ toast.offsetHeight;
103
+ if (variant === 'warning') {
104
+ toast.style.color = 'var(--warning-text)';
105
+ content.style.color = 'var(--warning-text)';
106
+ if (closeBtn) closeBtn.style.color = 'var(--warning-text)';
107
+ }
108
+ setTimeout(function() {
109
+ requestAnimationFrame(function() {
110
+ toast.style.opacity = '1';
111
+ toast.style.transform = isRight || isLeft ? 'translateX(0)' : 'translateY(0)';
112
+ });
113
+ }, 10);
114
+ });
115
+ if (autoDismiss > 0) {
116
+ setTimeout(function() {
117
+ if (toast.parentNode) {
118
+ toast.style.opacity = '0';
119
+ toast.style.transform = isRight ? 'translateX(100%)' : isLeft ? 'translateX(-100%)' : 'translateY(-100%)';
120
+ setTimeout(function() {
121
+ if (toast.parentNode) toast.remove();
122
+ if (container.children.length === 0) container.remove();
123
+ }, 300);
124
+ }
125
+ }, autoDismiss);
126
+ }
127
+ }
128
+ if (document.body) createToast();
129
+ else document.addEventListener('DOMContentLoaded', createToast);
130
+ return toastId;
131
+ }
132
+ function removeToast(toastId) {
133
+ var toast = document.getElementById(toastId);
134
+ if (toast) {
135
+ var container = toast.parentElement;
136
+ var position = container ? container.id.replace('toast-container-', '') : 'top-right';
137
+ toast.style.opacity = '0';
138
+ toast.style.transform = position.indexOf('right') !== -1 ? 'translateX(100%)' : position.indexOf('left') !== -1 ? 'translateX(-100%)' : 'translateY(-100%)';
139
+ setTimeout(function() {
140
+ if (toast.parentNode) toast.remove();
141
+ if (container && container.classList.contains('toast-container') && container.children.length === 0) container.remove();
142
+ }, 300);
143
+ }
144
+ }
145
+ function removeAllToasts() {
146
+ document.querySelectorAll('.toast-container').forEach(function(c) {
147
+ c.querySelectorAll('.alert').forEach(function(t) {
148
+ t.style.opacity = '0';
149
+ t.style.transform = 'translateY(-10px)';
150
+ });
151
+ setTimeout(function() { c.remove(); }, 300);
152
+ });
153
+ }
154
+ window.showToast = showToast;
155
+ window.removeToast = removeToast;
156
+ window.removeAllToasts = removeAllToasts;
157
+ })();
158
+ </script>
7
159
  <link rel="stylesheet" href="{{LINK_HREF}}" />
8
160
  </head>
9
161
  <body>
10
- <header style="margin-bottom: var(--spacing-6);">
11
- <h1>Hello, Rizzo CSS</h1>
12
- <p>Vanilla JS same styles and components as Astro/Svelte. Use the same BEM classes; see <a href="https://rizzo-css.vercel.app/docs/components">docs</a>.</p>
13
- <div style="display: flex; align-items: center; gap: var(--spacing-3); flex-wrap: wrap; margin-top: var(--spacing-3);">
14
- <label for="theme-select">Theme:</label>
15
- <select id="theme-select" style="width: auto; min-width: 12rem;">
16
- <option value="github-dark-classic">github-dark-classic</option>
17
- <option value="github-light">github-light</option>
18
- <option value="shades-of-purple">shades-of-purple</option>
19
- <option value="sandstorm-classic">sandstorm-classic</option>
20
- <option value="rocky-blood-orange">rocky-blood-orange</option>
21
- <option value="minimal-dark-neon-yellow">minimal-dark-neon-yellow</option>
22
- <option value="hack-the-box">hack-the-box</option>
23
- <option value="pink-cat-boo">pink-cat-boo</option>
24
- <option value="red-velvet-cupcake">red-velvet-cupcake</option>
25
- <option value="orangy-one-light">orangy-one-light</option>
26
- <option value="sunflower">sunflower</option>
27
- <option value="green-breeze-light">green-breeze-light</option>
28
- <option value="cute-pink">cute-pink</option>
29
- <option value="semi-light-purple">semi-light-purple</option>
162
+ <a href="#main-content" class="skip-link">Skip to main content</a>
163
+ <header class="container flex flex-wrap items-center justify-between gap-4" style="padding-top: var(--spacing-4); padding-bottom: var(--spacing-4); border-bottom: 1px solid var(--border);">
164
+ <a href="#" class="font-semibold" style="font-size: var(--font-size-lg); color: var(--text); text-decoration: none;">{{TITLE}}</a>
165
+ <div class="flex items-center gap-3 flex-wrap">
166
+ <label for="theme-select" class="sr-only">Theme</label>
167
+ <select id="theme-select" class="form-control" style="width: auto; min-width: 12rem;" aria-label="Theme">
168
+ <option value="system">System</option>
169
+ <optgroup label="Dark themes">
170
+ <option value="github-dark-classic">GitHub Dark Classic</option>
171
+ <option value="shades-of-purple">Shades of Purple</option>
172
+ <option value="sandstorm-classic">Sandstorm Classic</option>
173
+ <option value="rocky-blood-orange">Rocky Blood Orange</option>
174
+ <option value="minimal-dark-neon-yellow">Minimal Dark Neon Yellow</option>
175
+ <option value="hack-the-box">Hack The Box</option>
176
+ <option value="pink-cat-boo">Pink Cat Boo</option>
177
+ </optgroup>
178
+ <optgroup label="Light themes">
179
+ <option value="github-light">GitHub Light</option>
180
+ <option value="red-velvet-cupcake">Red Velvet Cupcake</option>
181
+ <option value="orangy-one-light">Orangy One Light</option>
182
+ <option value="sunflower">Sunflower</option>
183
+ <option value="green-breeze-light">Green Breeze Light</option>
184
+ <option value="cute-pink">Cute Pink</option>
185
+ <option value="semi-light-purple">Semi Light Purple</option>
186
+ </optgroup>
30
187
  </select>
188
+ <button type="button" class="btn btn-outline" aria-label="Open settings" onclick="window.openSettings && window.openSettings()">Settings</button>
31
189
  </div>
32
190
  </header>
33
191
 
34
- <section style="margin-bottom: var(--spacing-6);">
35
- <h2>Buttons</h2>
36
- <div style="display: flex; flex-wrap: wrap; gap: var(--spacing-3);">
37
- <button type="button" class="btn">Default</button>
38
- <button type="button" class="btn btn-primary">Primary</button>
39
- <button type="button" class="btn btn-success">Success</button>
40
- <button type="button" class="btn btn-warning">Warning</button>
41
- <button type="button" class="btn btn-error">Error</button>
42
- <button type="button" class="btn btn-outline">Outline</button>
43
- </div>
44
- </section>
45
-
46
- <section style="margin-bottom: var(--spacing-6);">
47
- <h2>Card &amp; Badge</h2>
48
- <div class="card" style="max-width: 20rem;">
49
- <div class="card__body">
50
- <h3 class="card__title">Card title</h3>
51
- <p>Same component styles as Astro and Svelte. Use class names from the <a href="https://rizzo-css.vercel.app/docs/components">component docs</a>.</p>
52
- <span class="badge badge--primary">Badge</span>
192
+ <!-- Settings panel (same structure as main site; CSS from Rizzo) -->
193
+ <div class="settings" data-settings aria-hidden="true">
194
+ <div class="settings__overlay" data-settings-overlay aria-hidden="true"></div>
195
+ <div class="settings__panel" role="dialog" aria-modal="true" aria-labelledby="settings-title" aria-hidden="true">
196
+ <div class="settings__header">
197
+ <h2 id="settings-title" class="settings__title">Settings</h2>
198
+ <button type="button" class="settings__close" aria-label="Close settings" data-settings-close>
199
+ <svg width="20" height="20" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2"><line x1="4" y1="4" x2="12" y2="12"></line><line x1="12" y1="4" x2="4" y2="12"></line></svg>
200
+ </button>
201
+ </div>
202
+ <div class="settings__content">
203
+ <section class="settings__section">
204
+ <h3 class="settings__section-title">Theme</h3>
205
+ <div class="settings__control">
206
+ <label for="settings-theme" class="settings__label">Theme</label>
207
+ <select id="settings-theme" class="form-control" aria-label="Theme" data-settings-theme style="width: 100%;">
208
+ <option value="system">System</option>
209
+ <optgroup label="Dark themes">
210
+ <option value="github-dark-classic">GitHub Dark Classic</option>
211
+ <option value="shades-of-purple">Shades of Purple</option>
212
+ <option value="sandstorm-classic">Sandstorm Classic</option>
213
+ <option value="rocky-blood-orange">Rocky Blood Orange</option>
214
+ <option value="minimal-dark-neon-yellow">Minimal Dark Neon Yellow</option>
215
+ <option value="hack-the-box">Hack The Box</option>
216
+ <option value="pink-cat-boo">Pink Cat Boo</option>
217
+ </optgroup>
218
+ <optgroup label="Light themes">
219
+ <option value="github-light">GitHub Light</option>
220
+ <option value="red-velvet-cupcake">Red Velvet Cupcake</option>
221
+ <option value="orangy-one-light">Orangy One Light</option>
222
+ <option value="sunflower">Sunflower</option>
223
+ <option value="green-breeze-light">Green Breeze Light</option>
224
+ <option value="cute-pink">Cute Pink</option>
225
+ <option value="semi-light-purple">Semi Light Purple</option>
226
+ </optgroup>
227
+ </select>
228
+ </div>
229
+ </section>
230
+ <section class="settings__section">
231
+ <h3 class="settings__section-title">Font Size</h3>
232
+ <div class="settings__control">
233
+ <label for="font-size-slider" class="settings__label">
234
+ <span class="settings__label-text">Adjust text size</span>
235
+ <span class="settings__label-value" data-font-size-value>100%</span>
236
+ </label>
237
+ <input type="range" id="font-size-slider" class="settings__slider" min="0.75" max="1.5" step="0.05" value="1" aria-label="Font size" data-font-size-slider style="--slider-progress: 50%;" />
238
+ <div class="settings__slider-labels"><span>Small</span><span>Default</span><span>Large</span></div>
239
+ </div>
240
+ </section>
241
+ <section class="settings__section">
242
+ <h3 class="settings__section-title">Accessibility</h3>
243
+ <div class="settings__control">
244
+ <label class="settings__checkbox-label">
245
+ <input type="checkbox" class="settings__checkbox" data-reduced-motion aria-label="Reduce motion" />
246
+ <span>Reduce motion</span>
247
+ </label>
248
+ <p class="settings__help-text">Minimize animations and transitions</p>
249
+ </div>
250
+ <div class="settings__control">
251
+ <label class="settings__checkbox-label">
252
+ <input type="checkbox" class="settings__checkbox" data-high-contrast aria-label="High contrast" />
253
+ <span>High contrast</span>
254
+ </label>
255
+ <p class="settings__help-text">Increase contrast for better visibility</p>
256
+ </div>
257
+ <div class="settings__control">
258
+ <div class="settings__label"><span class="settings__label-text">Scrollbar style</span></div>
259
+ <div class="settings__radio-group" role="radiogroup" aria-label="Scrollbar style">
260
+ <label class="settings__radio-label">
261
+ <input type="radio" name="scrollbar-style" value="thin" class="settings__radio" data-scrollbar-style aria-label="Thin scrollbar" checked />
262
+ <span>Thin</span>
263
+ </label>
264
+ <label class="settings__radio-label">
265
+ <input type="radio" name="scrollbar-style" value="thick" class="settings__radio" data-scrollbar-style aria-label="Thick scrollbar" />
266
+ <span>Thick</span>
267
+ </label>
268
+ <label class="settings__radio-label">
269
+ <input type="radio" name="scrollbar-style" value="hidden" class="settings__radio" data-scrollbar-style aria-label="Hidden scrollbars" />
270
+ <span>Hidden</span>
271
+ </label>
272
+ </div>
273
+ <p class="settings__help-text">Choose your preferred scrollbar appearance</p>
274
+ </div>
275
+ </section>
53
276
  </div>
54
277
  </div>
55
- </section>
278
+ </div>
56
279
 
57
- <p><small>All themes and component styles are included. Run <code>npx rizzo-css theme</code> to list themes.</small></p>
280
+ <main id="main-content" class="flex flex-col items-center justify-center text-center min-h-screen" style="padding: var(--spacing-12) var(--spacing-4); min-height: calc(100vh - 4rem);">
281
+ <span class="badge badge--primary badge--sm mb-4">Vanilla JS + Rizzo CSS</span>
282
+ <h1 style="font-size: clamp(2.5rem, 8vw, 4rem); font-weight: 800; line-height: 1.1; margin: 0 0 var(--spacing-4) 0; color: var(--text);">Build something great</h1>
283
+ <p style="font-size: var(--font-size-xl); color: var(--text-dim); max-width: 42ch; margin: 0 0 var(--spacing-8) 0; line-height: var(--line-height-relaxed);">Same design system as Astro and Svelte — 14 themes, 24 components, full keyboard and screen reader support.</p>
284
+ <div class="flex flex-wrap justify-center gap-4 mb-12">
285
+ <a href="https://rizzo-css.vercel.app/docs/getting-started" class="btn btn-primary" target="_blank" rel="noopener noreferrer">Get started</a>
286
+ <a href="https://rizzo-css.vercel.app/docs/components" class="btn btn-outline" target="_blank" rel="noopener noreferrer">Components</a>
287
+ <button type="button" class="btn btn-outline" onclick="window.showToast && window.showToast('Hello from Rizzo!', { variant: 'success' });">Show toast</button>
288
+ </div>
289
+ <div class="flex flex-wrap justify-center gap-3 mb-16">
290
+ <span class="badge badge--info">14 themes</span>
291
+ <span class="badge badge--info">24 components</span>
292
+ <span class="badge badge--info">WCAG AA</span>
293
+ </div>
294
+ <footer style="margin-top: auto; padding-top: var(--spacing-8); color: var(--text-dim); font-size: var(--font-size-sm);">
295
+ <a href="https://rizzo-css.vercel.app" class="text-accent" style="color: var(--accent);">Rizzo CSS</a> — design system for the web
296
+ </footer>
297
+ </main>
58
298
 
59
299
  <script>
60
- (function () {
61
- var select = document.getElementById('theme-select');
62
- var html = document.documentElement;
63
- var key = 'rizzo-theme';
300
+ (function() {
301
+ var KEY = 'theme';
302
+ var defaultDark = 'github-dark-classic';
303
+ var defaultLight = 'github-light';
304
+ function resolveSystem() {
305
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? defaultDark : defaultLight;
306
+ }
64
307
  function applyTheme(value) {
65
- html.setAttribute('data-theme', value);
66
- try { localStorage.setItem(key, value); } catch (e) {}
67
- select.value = value;
68
- }
69
- var stored = null;
70
- try { stored = localStorage.getItem(key); } catch (e) {}
71
- var initial = stored || html.getAttribute('data-theme') || 'github-dark-classic';
72
- applyTheme(initial);
73
- select.addEventListener('change', function () { applyTheme(select.value); });
308
+ var effective = value === 'system' ? resolveSystem() : value;
309
+ document.documentElement.setAttribute('data-theme', effective);
310
+ try { localStorage.setItem(KEY, value); } catch (e) {}
311
+ var headerSelect = document.getElementById('theme-select');
312
+ var settingsSelect = document.getElementById('settings-theme');
313
+ if (headerSelect && headerSelect.value !== value) headerSelect.value = value;
314
+ if (settingsSelect && settingsSelect.value !== value) settingsSelect.value = value;
315
+ try { window.dispatchEvent(new CustomEvent('rizzo-theme-change', { detail: { themeValue: value, effective: effective } })); } catch (e) {}
316
+ }
317
+ function syncSelects() {
318
+ var stored = null;
319
+ try { stored = localStorage.getItem(KEY); } catch (e) {}
320
+ var value = stored || 'system';
321
+ var headerSelect = document.getElementById('theme-select');
322
+ var settingsSelect = document.getElementById('settings-theme');
323
+ if (headerSelect) headerSelect.value = value;
324
+ if (settingsSelect) settingsSelect.value = value;
325
+ }
326
+ var headerSelect = document.getElementById('theme-select');
327
+ var settingsSelect = document.getElementById('settings-theme');
328
+ if (headerSelect) headerSelect.addEventListener('change', function() { applyTheme(headerSelect.value); });
329
+ if (settingsSelect) settingsSelect.addEventListener('change', function() { applyTheme(settingsSelect.value); });
330
+ syncSelects();
331
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() {
332
+ if ((localStorage.getItem(KEY) || 'system') === 'system') applyTheme('system');
333
+ });
334
+ })();
335
+ </script>
336
+ <script>
337
+ (function initSettings() {
338
+ var settings = document.querySelector('[data-settings]');
339
+ if (!settings) return;
340
+ var overlay = settings.querySelector('[data-settings-overlay]');
341
+ var panel = settings.querySelector('.settings__panel');
342
+ var closeBtn = settings.querySelector('[data-settings-close]');
343
+ var fontSizeSlider = settings.querySelector('[data-font-size-slider]');
344
+ var fontSizeValue = settings.querySelector('[data-font-size-value]');
345
+ var reducedMotion = settings.querySelector('[data-reduced-motion]');
346
+ var highContrast = settings.querySelector('[data-high-contrast]');
347
+ var scrollbarStyleRadios = settings.querySelectorAll('[data-scrollbar-style]');
348
+ var html = document.documentElement;
349
+ if (!panel || !overlay || !closeBtn) return;
350
+ function updateSliderProgress(slider) {
351
+ var min = parseFloat(slider.min), max = parseFloat(slider.max), value = parseFloat(slider.value);
352
+ slider.style.setProperty('--slider-progress', ((value - min) / (max - min)) * 100 + '%');
353
+ }
354
+ function applyFontSize(scale) {
355
+ html.style.setProperty('--font-size-scale', scale);
356
+ if (fontSizeValue) fontSizeValue.textContent = Math.round(scale * 100) + '%';
357
+ }
358
+ function applyScrollbarStyle(style) {
359
+ html.classList.remove('scrollbar-thin', 'scrollbar-thick', 'scrollbar-hidden', 'hide-scrollbars');
360
+ if (style === 'thick') html.classList.add('scrollbar-thick');
361
+ else if (style === 'hidden') html.classList.add('scrollbar-hidden', 'hide-scrollbars');
362
+ }
363
+ function loadSettings() {
364
+ var saved = localStorage.getItem('fontSizeScale');
365
+ if (saved && fontSizeSlider) {
366
+ fontSizeSlider.value = saved;
367
+ applyFontSize(parseFloat(saved));
368
+ }
369
+ if (fontSizeSlider) updateSliderProgress(fontSizeSlider);
370
+ if (reducedMotion) {
371
+ reducedMotion.checked = localStorage.getItem('reducedMotion') === 'true';
372
+ html.classList.toggle('reduced-motion', reducedMotion.checked);
373
+ }
374
+ if (highContrast) {
375
+ highContrast.checked = localStorage.getItem('highContrast') === 'true';
376
+ html.classList.toggle('high-contrast', highContrast.checked);
377
+ }
378
+ var scrollbar = localStorage.getItem('scrollbarStyle') || 'thin';
379
+ for (var i = 0; i < scrollbarStyleRadios.length; i++) {
380
+ if (scrollbarStyleRadios[i].value === scrollbar) scrollbarStyleRadios[i].checked = true;
381
+ }
382
+ applyScrollbarStyle(scrollbar);
383
+ }
384
+ function getFocusable(container) {
385
+ var sel = 'button:not([disabled]),a[href],input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])';
386
+ return Array.prototype.slice.call(container.querySelectorAll(sel));
387
+ }
388
+ var previousActive = null;
389
+ function openSettings() {
390
+ previousActive = document.activeElement;
391
+ settings.setAttribute('aria-hidden', 'false');
392
+ overlay.setAttribute('aria-hidden', 'false');
393
+ panel.setAttribute('aria-hidden', 'false');
394
+ panel.removeAttribute('data-open');
395
+ void panel.offsetHeight;
396
+ requestAnimationFrame(function() {
397
+ requestAnimationFrame(function() {
398
+ panel.setAttribute('data-open', 'true');
399
+ if (closeBtn) closeBtn.focus();
400
+ });
401
+ });
402
+ }
403
+ function closeSettings() {
404
+ panel.removeAttribute('data-open');
405
+ var duration = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 0 : 300;
406
+ setTimeout(function() {
407
+ settings.setAttribute('aria-hidden', 'true');
408
+ overlay.setAttribute('aria-hidden', 'true');
409
+ panel.setAttribute('aria-hidden', 'true');
410
+ if (previousActive) {
411
+ previousActive.focus();
412
+ previousActive = null;
413
+ }
414
+ }, duration);
415
+ }
416
+ closeBtn.addEventListener('click', closeSettings);
417
+ overlay.addEventListener('click', closeSettings);
418
+ document.addEventListener('keydown', function(e) {
419
+ if (panel.getAttribute('data-open') !== 'true') return;
420
+ if (e.key === 'Escape') { e.preventDefault(); closeSettings(); return; }
421
+ if (e.key === 'Tab') {
422
+ var els = getFocusable(panel);
423
+ if (els.length === 0) return;
424
+ var first = els[0], last = els[els.length - 1], active = document.activeElement;
425
+ if (e.shiftKey) {
426
+ if (active === first || !panel.contains(active)) { e.preventDefault(); last.focus(); }
427
+ } else {
428
+ if (active === last || !panel.contains(active)) { e.preventDefault(); first.focus(); }
429
+ }
430
+ }
431
+ });
432
+ if (fontSizeSlider) {
433
+ fontSizeSlider.addEventListener('input', function() {
434
+ var scale = parseFloat(this.value);
435
+ applyFontSize(scale);
436
+ updateSliderProgress(this);
437
+ localStorage.setItem('fontSizeScale', scale);
438
+ });
439
+ }
440
+ if (reducedMotion) {
441
+ reducedMotion.addEventListener('change', function() {
442
+ html.classList.toggle('reduced-motion', this.checked);
443
+ localStorage.setItem('reducedMotion', this.checked);
444
+ });
445
+ }
446
+ if (highContrast) {
447
+ highContrast.addEventListener('change', function() {
448
+ html.classList.toggle('high-contrast', this.checked);
449
+ localStorage.setItem('highContrast', this.checked);
450
+ });
451
+ }
452
+ for (var j = 0; j < scrollbarStyleRadios.length; j++) {
453
+ scrollbarStyleRadios[j].addEventListener('change', function() {
454
+ if (this.checked) {
455
+ applyScrollbarStyle(this.value);
456
+ localStorage.setItem('scrollbarStyle', this.value);
457
+ }
458
+ });
459
+ }
460
+ loadSettings();
461
+ window.openSettings = openSettings;
74
462
  })();
75
463
  </script>
76
464
  </body>