/** * BetterDesk Console - Accessibility Preferences */ (function() { 'use strict'; const STORAGE_KEY = 'betterdesk_accessibility_v1'; const DEFAULTS = Object.freeze({ fontScale: 100, lineHeight: 1.5, letterSpacing: 0, wordSpacing: 0, saturation: 100, contrast: 'normal', colorFilter: 'none', activeProfile: '', readableFont: false, dyslexiaFont: false, underlineLinks: false, highlightLinks: false, highlightHeadings: false, strongFocus: false, largeCursor: false, reduceMotion: false, pauseAnimations: false, readingGuide: false, readingMask: false, hideMedia: false }); const SELECT_VALUES = { contrast: ['normal', 'high-dark', 'high-light', 'yellow-black'], colorFilter: ['none', 'protanopia', 'deuteranopia', 'tritanopia', 'achromatopsia'] }; let settings = loadSettings(); let modalEl = null; let lastFocusedEl = null; let readingUiReady = false; let currentPointerY = Math.round(window.innerHeight / 2); function tr(key, fallback) { const value = typeof window._ === 'function' ? window._(`accessibility.${key}`) : `accessibility.${key}`; return value === `accessibility.${key}` ? fallback : value; } function esc(value) { return window.Utils?.escapeHtml ? Utils.escapeHtml(String(value)) : String(value); } function clampNumber(value, min, max, fallback) { const numeric = Number(value); if (!Number.isFinite(numeric)) return fallback; return Math.min(max, Math.max(min, numeric)); } function sanitize(candidate) { const source = candidate && typeof candidate === 'object' ? candidate : {}; const clean = { ...DEFAULTS }; clean.fontScale = clampNumber(source.fontScale, 90, 160, DEFAULTS.fontScale); clean.lineHeight = clampNumber(source.lineHeight, 1.2, 2.1, DEFAULTS.lineHeight); clean.letterSpacing = clampNumber(source.letterSpacing, 0, 0.14, DEFAULTS.letterSpacing); clean.wordSpacing = clampNumber(source.wordSpacing, 0, 0.22, DEFAULTS.wordSpacing); clean.saturation = clampNumber(source.saturation, 0, 180, DEFAULTS.saturation); clean.contrast = SELECT_VALUES.contrast.includes(source.contrast) ? source.contrast : DEFAULTS.contrast; clean.colorFilter = SELECT_VALUES.colorFilter.includes(source.colorFilter) ? source.colorFilter : DEFAULTS.colorFilter; clean.activeProfile = ['lowVision', 'dyslexia', 'motionSafe', 'colorAssist'].includes(source.activeProfile) ? source.activeProfile : DEFAULTS.activeProfile; Object.keys(DEFAULTS).forEach(key => { if (typeof DEFAULTS[key] === 'boolean') clean[key] = source[key] === true; }); return clean; } function loadSettings() { try { return sanitize(JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')); } catch (_) { return { ...DEFAULTS }; } } function saveSettings(nextSettings) { settings = sanitize(nextSettings); try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch (_) { /* localStorage may be disabled */ } applySettings(settings); syncButtonState(); return settings; } function resetSettings() { settings = { ...DEFAULTS }; try { localStorage.removeItem(STORAGE_KEY); } catch (_) {} applySettings(settings); syncButtonState(); if (modalEl) syncModalControls(); } function isDefault(current) { return Object.keys(DEFAULTS).every(key => current[key] === DEFAULTS[key]); } function applySettings(current) { const root = document.documentElement; root.style.setProperty('--bd-a11y-font-scale', String(current.fontScale / 100)); root.style.setProperty('--bd-a11y-line-height', String(current.lineHeight)); root.style.setProperty('--bd-a11y-letter-spacing', `${current.letterSpacing}em`); root.style.setProperty('--bd-a11y-word-spacing', `${current.wordSpacing}em`); root.dataset.a11yActive = isDefault(current) ? 'false' : 'true'; root.dataset.a11yFontScaled = current.fontScale !== DEFAULTS.fontScale ? 'true' : 'false'; root.dataset.a11yContrast = current.contrast; root.dataset.a11yReadableFont = String(current.readableFont); root.dataset.a11yDyslexia = String(current.dyslexiaFont); root.dataset.a11yUnderlineLinks = String(current.underlineLinks); root.dataset.a11yHighlightLinks = String(current.highlightLinks); root.dataset.a11yHighlightHeadings = String(current.highlightHeadings); root.dataset.a11yStrongFocus = String(current.strongFocus); root.dataset.a11yLargeCursor = String(current.largeCursor); root.dataset.a11yReduceMotion = String(current.reduceMotion); root.dataset.a11yPauseAnimations = String(current.pauseAnimations); root.dataset.a11yReadingGuide = String(current.readingGuide); root.dataset.a11yReadingMask = String(current.readingMask); root.dataset.a11yHideMedia = String(current.hideMedia); const filters = []; if (current.colorFilter !== 'none') filters.push(`url(#bd-a11y-${current.colorFilter})`); if (current.saturation !== 100) filters.push(`saturate(${current.saturation}%)`); root.dataset.a11yFiltered = filters.length > 0 ? 'true' : 'false'; root.style.setProperty('--bd-a11y-visual-filter', filters.length > 0 ? filters.join(' ') : 'none'); ensureFilterDefs(); ensureReadingUi(); updateReadingUi(currentPointerY); } function syncButtonState() { const btn = document.getElementById('accessibility-btn'); if (!btn) return; const active = !isDefault(settings); btn.classList.toggle('active', active); btn.setAttribute('aria-pressed', String(active)); } function ensureFilterDefs() { if (document.getElementById('bd-a11y-filter-defs')) return; const wrapper = document.createElement('div'); wrapper.innerHTML = ` `; document.body.appendChild(wrapper.firstElementChild); } function ensureReadingUi() { if (readingUiReady) return; const guide = document.createElement('div'); const maskTop = document.createElement('div'); const maskBottom = document.createElement('div'); guide.className = 'bd-a11y-reading-guide'; maskTop.className = 'bd-a11y-reading-mask-top'; maskBottom.className = 'bd-a11y-reading-mask-bottom'; document.body.append(guide, maskTop, maskBottom); document.addEventListener('pointermove', event => { currentPointerY = event.clientY; updateReadingUi(currentPointerY); }, { passive: true }); window.addEventListener('resize', () => updateReadingUi(currentPointerY), { passive: true }); readingUiReady = true; } function updateReadingUi(y) { const guide = document.querySelector('.bd-a11y-reading-guide'); const maskTop = document.querySelector('.bd-a11y-reading-mask-top'); const maskBottom = document.querySelector('.bd-a11y-reading-mask-bottom'); if (!guide || !maskTop || !maskBottom) return; const focusHeight = 150; const top = Math.max(0, y - focusHeight / 2); const bottom = Math.min(window.innerHeight, y + focusHeight / 2); guide.style.top = `${Math.max(0, y - 2)}px`; maskTop.style.top = '0'; maskTop.style.height = `${top}px`; maskBottom.style.top = `${bottom}px`; maskBottom.style.height = `${Math.max(0, window.innerHeight - bottom)}px`; } function optionHtml(value, label, selected) { return ``; } function rangeField(key, label, min, max, step, suffix) { const value = settings[key]; return `
${esc(tr('subtitle', 'Adjust the console to your vision, motion and reading needs.'))}