agent: store — add preferences store with DOM sync and SQLite persistence

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-21 10:35:09 +00:00
parent 420d0138a2
commit 49aa943d8e

View File

@@ -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);
});
}