diff --git a/src/lib/stores/preferences.svelte.js b/src/lib/stores/preferences.svelte.js new file mode 100644 index 0000000..ae58804 --- /dev/null +++ b/src/lib/stores/preferences.svelte.js @@ -0,0 +1,117 @@ +// src/lib/stores/preferences.svelte.js +import { invoke } from '@tauri-apps/api/core'; + +const DEFAULTS = { + theme: 'dark', + zone: 'default', + accessibility: { + fontFamily: 'lexend', + fontSize: 16, + letterSpacing: 0, + lineHeight: 1.5, + transcriptSize: 16, + bionicReading: false, + reduceMotion: 'system' + } +}; + +const FONT_FAMILIES = { + lexend: "'Lexend', system-ui, sans-serif", + atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", + opendyslexic: "'OpenDyslexic', system-ui, sans-serif" +}; + +function readFromDOM() { + const el = document.documentElement; + return { + theme: el.dataset.theme || DEFAULTS.theme, + zone: el.dataset.zone || DEFAULTS.zone, + accessibility: { + fontFamily: el.dataset.fontFamily || DEFAULTS.accessibility.fontFamily, + fontSize: parseFloat(el.style.getPropertyValue('--font-size-body')) || DEFAULTS.accessibility.fontSize, + letterSpacing: parseFloat(el.style.getPropertyValue('--letter-spacing-body')) || DEFAULTS.accessibility.letterSpacing, + lineHeight: parseFloat(el.style.getPropertyValue('--line-height-body')) || DEFAULTS.accessibility.lineHeight, + transcriptSize: DEFAULTS.accessibility.transcriptSize, + bionicReading: el.dataset.bionicReading === 'true', + reduceMotion: el.dataset.reduceMotion || DEFAULTS.accessibility.reduceMotion + } + }; +} + +function applyToDOM(prefs) { + const el = document.documentElement; + + // Theme — resolve 'system' to actual value + el.dataset.theme = prefs.theme === 'system' + ? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark') + : prefs.theme; + + // Zone + if (prefs.zone === 'default') { + delete el.dataset.zone; + } else { + el.dataset.zone = prefs.zone; + } + + // Accessibility — inline styles for highest specificity + const a = prefs.accessibility; + el.style.setProperty('--font-family-body', FONT_FAMILIES[a.fontFamily] || FONT_FAMILIES.lexend); + el.style.setProperty('--font-size-body', `${a.fontSize}px`); + el.style.setProperty('--letter-spacing-body', `${a.letterSpacing}em`); + el.style.setProperty('--line-height-body', String(a.lineHeight)); + + el.dataset.bionicReading = String(a.bionicReading); + el.dataset.fontFamily = a.fontFamily; + + // Reduce motion — three-value resolution + const motionReduced = a.reduceMotion === 'on' + || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches); + if (motionReduced) { + el.dataset.reduceMotion = 'true'; + } else { + delete el.dataset.reduceMotion; + } +} + +let saveTimeout = null; +function persistToSQLite(prefs) { + clearTimeout(saveTimeout); + saveTimeout = setTimeout(async () => { + try { + await invoke('save_preferences', { preferences: JSON.stringify(prefs) }); + } catch (e) { + console.error('Failed to save preferences:', e); + } + }, 500); +} + +let preferences = $state(readFromDOM()); + +export function getPreferences() { + return preferences; +} + +export function updatePreferences(updates) { + preferences = { ...preferences, ...updates }; + applyToDOM(preferences); + persistToSQLite(preferences); +} + +export function updateAccessibility(updates) { + preferences = { + ...preferences, + accessibility: { ...preferences.accessibility, ...updates } + }; + applyToDOM(preferences); + persistToSQLite(preferences); +} + +// Re-resolve when OS preferences change +if (typeof window !== 'undefined') { + window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => { + if (preferences.theme === 'system') applyToDOM(preferences); + }); + window.matchMedia('(prefers-reduced-motion: reduce)').addEventListener('change', () => { + if (preferences.accessibility.reduceMotion === 'system') applyToDOM(preferences); + }); +}