import type { A11yStore, A11yStoreConfig, ContrastLevel, ColorblindMode } from './types'; import { DEFAULT_A11Y_SETTINGS, A11Y_STORAGE_KEY_SUFFIX } from './a11y-constants'; import { getSystemReducedMotion, createReducedMotionListener, applyA11yAttributes, loadA11yFromStorage, saveA11yToStorage, } from './a11y-utils'; import { isBrowser } from './utils'; /** * Create an A11y store for your app * * @example * ```typescript * import { createA11yStore } from '@manacore/shared-theme'; * * export const a11y = createA11yStore({ appId: 'myapp' }); * * // In +layout.svelte * onMount(() => { * const cleanup = a11y.initialize(); * return cleanup; * }); * ``` */ export function createA11yStore(config: A11yStoreConfig): A11yStore { const { appId, defaults = {} } = config; const storageKey = `${appId}${A11Y_STORAGE_KEY_SUFFIX}`; // Merge defaults const defaultSettings = { ...DEFAULT_A11Y_SETTINGS, ...defaults }; // Svelte 5 runes state let contrast = $state(defaultSettings.contrast); let colorblind = $state(defaultSettings.colorblind); let userReduceMotion = $state(null); // null = use system let systemReduceMotion = $state(false); // Derived: effective reduce motion const reduceMotion = $derived(userReduceMotion !== null ? userReduceMotion : systemReduceMotion); // Derived: whether user has explicitly set reduce motion const reduceMotionExplicit = $derived(userReduceMotion !== null); /** * Apply current A11y settings to document */ function applySettings(): void { applyA11yAttributes({ contrast, colorblind, reduceMotion, }); saveSettings(); } /** * Save settings to localStorage */ function saveSettings(): void { saveA11yToStorage(storageKey, { contrast, colorblind, reduceMotion: userReduceMotion !== null ? userReduceMotion : false, }); } /** * Set contrast level */ function setContrast(level: ContrastLevel): void { if (level === contrast) return; if (level !== 'normal' && level !== 'high') { console.warn(`Invalid contrast level: ${level}`); return; } contrast = level; applySettings(); } /** * Set colorblind mode */ function setColorblind(mode: ColorblindMode): void { if (mode === colorblind) return; const validModes: ColorblindMode[] = ['none', 'deuteranopia', 'protanopia', 'monochrome']; if (!validModes.includes(mode)) { console.warn(`Invalid colorblind mode: ${mode}`); return; } colorblind = mode; applySettings(); } /** * Set reduce motion preference */ function setReduceMotion(reduce: boolean): void { userReduceMotion = reduce; applySettings(); } /** * Reset reduce motion to system default */ function resetReduceMotion(): void { userReduceMotion = null; applySettings(); } /** * Reset all A11y settings to defaults */ function resetAll(): void { contrast = defaultSettings.contrast; colorblind = defaultSettings.colorblind; userReduceMotion = null; applySettings(); } /** * Initialize A11y store * - Loads saved preferences from localStorage * - Sets up reduced motion listener * - Applies initial settings * * @returns Cleanup function to remove listeners */ function initialize(): () => void { if (!isBrowser()) { return () => {}; } // Get system reduced motion preference systemReduceMotion = getSystemReducedMotion(); // Load saved preferences const saved = loadA11yFromStorage(storageKey); if (saved) { if (saved.contrast && (saved.contrast === 'normal' || saved.contrast === 'high')) { contrast = saved.contrast; } if (saved.colorblind) { const validModes: ColorblindMode[] = ['none', 'deuteranopia', 'protanopia', 'monochrome']; if (validModes.includes(saved.colorblind as ColorblindMode)) { colorblind = saved.colorblind as ColorblindMode; } } if (typeof saved.reduceMotion === 'boolean') { userReduceMotion = saved.reduceMotion; } } // Apply initial settings applySettings(); // Listen for system reduced motion changes const cleanup = createReducedMotionListener((reduces) => { systemReduceMotion = reduces; // Only re-apply if user hasn't explicitly set a preference if (userReduceMotion === null) { applySettings(); } }); return cleanup; } return { get contrast() { return contrast; }, get colorblind() { return colorblind; }, get reduceMotion() { return reduceMotion; }, get reduceMotionExplicit() { return reduceMotionExplicit; }, setContrast, setColorblind, setReduceMotion, resetReduceMotion, resetAll, initialize, }; }