Four new preferences, all in settings.js / applied via applyTheme() so boot and live changes go through one path: - fontScale (small/default/large): scales root font-size (92/100/115%); the whole stylesheet is already rem-based so this alone rescales everything. - reduceMotion: an explicit in-app override alongside the existing `prefers-reduced-motion` OS check, via a new :root[data-reduce-motion="true"] rule in tokens.css mirroring the existing media-query kill-switch. New prefersReducedMotion() helper (checks both) replaces the three hand-rolled matchMedia checks in router.js/Card.js/PokemonDetail.js. - defaultRoute (dex/team/search): redirects on a genuinely hash-less launch only (bookmark, home-screen icon, bare URL) — explicit nav (e.g. tapping "Dex") is never touched, since it's applied once in main.js before initRouter runs. - haptics: a short navigator.vibrate() pulse on the same catch/seen/ favorite toggles that already play the pop animation, via a new lib/haptics.js buzz() (no-ops without Vibration API support, e.g. iOS Safari). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017Ve7HLspzeG2xDPtJQ8vmu
72 lines
2.5 KiB
JavaScript
72 lines
2.5 KiB
JavaScript
import { createStore } from './createStore.js';
|
|
|
|
/**
|
|
* User preferences. Small, synchronous, read on boot.
|
|
*
|
|
* - versionGroup: the selected "game" (PokéAPI version-group key)
|
|
* - pokedex: the active regional dex within that game (null = first one)
|
|
* - theme: 'system' | 'light' | 'dark'
|
|
* - spriteStyle: 'default' (pixel) | 'official' | 'home'
|
|
* - showShiny: default the detail view to shiny sprites
|
|
* - locale: reserved for localized names (PokéAPI supports many)
|
|
* - fontScale: 'small' | 'default' | 'large' text size
|
|
* - reduceMotion: in-app override — off even if the OS doesn't ask for it
|
|
* - defaultRoute: 'dex' | 'team' | 'search' — landing screen on cold launch
|
|
* - haptics: short vibration on catch (Vibration API, Android only)
|
|
*/
|
|
export const settings = createStore('pdx.settings', {
|
|
versionGroup: 'all', // 'all' = National Dex, newest data, no generation limits
|
|
pokedex: null,
|
|
theme: 'system', // system | light | dark | black | sepia
|
|
accent: 'red', // red | blue | green | amber | violet | rose
|
|
spriteStyle: 'official',
|
|
showShiny: false,
|
|
showTeamNav: true,
|
|
locale: 'en',
|
|
fontScale: 'default',
|
|
reduceMotion: false,
|
|
defaultRoute: 'dex',
|
|
haptics: true,
|
|
});
|
|
|
|
const DARKISH = new Set(['dark', 'black']);
|
|
const FONT_SCALE = { small: 0.9, default: 1, large: 1.15 };
|
|
|
|
export function applyTheme(state = settings.get()) {
|
|
const root = document.documentElement;
|
|
const { theme, accent, fontScale, reduceMotion } = state;
|
|
|
|
if (!theme || theme === 'system') root.removeAttribute('data-theme');
|
|
else root.setAttribute('data-theme', theme);
|
|
|
|
if (!accent || accent === 'red') root.removeAttribute('data-accent');
|
|
else root.setAttribute('data-accent', accent);
|
|
|
|
root.style.fontSize = `${(FONT_SCALE[fontScale] ?? 1) * 100}%`;
|
|
|
|
if (reduceMotion) root.setAttribute('data-reduce-motion', 'true');
|
|
else root.removeAttribute('data-reduce-motion');
|
|
|
|
const meta = document.querySelector('meta[name="theme-color"]');
|
|
if (meta) {
|
|
const dark =
|
|
DARKISH.has(theme) ||
|
|
((!theme || theme === 'system') &&
|
|
window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
meta.setAttribute(
|
|
'content',
|
|
dark
|
|
? '#0b0b0c'
|
|
: getComputedStyle(root).getPropertyValue('--accent').trim() || '#b3161a',
|
|
);
|
|
}
|
|
}
|
|
|
|
/** True if animations should be skipped — OS preference or the in-app override. */
|
|
export function prefersReducedMotion() {
|
|
return (
|
|
!!settings.get().reduceMotion ||
|
|
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
);
|
|
}
|