dex/src/store/settings.js
chris 4b9bc5c0ab Settings: text size, reduce motion, landing screen, haptics
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
2026-09-04 12:35:26 -04:00

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