Session 38: Design system Phase G — living layer, i18n/odds, a11y, paywall, parlay math (1890 tests)
VYNDR 2.0 conversion, Phase G (the systems that make the design alive). All 5 wired. Frontend-only; zero backend changes. - lib/parlayMath.js: correlation model (0.62/0.34/0.06/0) + parlayGrade penalty + grade->odds + combined odds (frontend; backend parlayService unchanged). - lib/oddsFormat.js: fmtOdds across american/decimal/fractional/implied with the totals-pass-through rule (safer than the prototype's parseAm, which would mis-convert 228.5) + region presets. - lib/prefs.js: applyPrefs sets <html data-*> (the S33 a11y CSS layer) + load/save. - lib/liveTick.js: single tick engine (SSR/test-safe, no auto-start, fresh state). - lib/checkout.js: checkoutUrl(plan). - LiveLayer (useLive/LiveNumber/HeartbeatBar) under the Nav ticker; GlobalHosts in layout applies prefs + registers __prefs/__goPaywall/__checkout + hosts the Preferences and Paywall modals. Nav read-meter is now a paywall trigger. Gotchas: useEffect can't return a Set.delete unsub directly (boolean != cleanup); header grew to 124px so layout paddingTop + Slate sticky-top updated to match. 18 new tests. Backend 1872 -> 1890, 146 suites, zero regressions. Web build clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
/* ============================================================
|
||||
VYNDR 2.0 — preferences store (§9 i18n + §10 a11y).
|
||||
Plain CommonJS so the modal/applier import it (allowJs) AND Jest tests
|
||||
the apply/load/save logic with a fake element + fake storage.
|
||||
|
||||
The CSS layer already exists (Session 33 added the <html data-*>
|
||||
overrides to globals.css). This module SETS those attributes.
|
||||
============================================================ */
|
||||
|
||||
const PREFS_KEY = 'vyndr_prefs';
|
||||
|
||||
const DEFAULTS = {
|
||||
lang: 'en',
|
||||
region: 'US',
|
||||
odds: 'american',
|
||||
currency: 'USD',
|
||||
motion: 'on', // 'on' | 'reduced'
|
||||
contrast: 'off', // 'off' | 'high'
|
||||
text: 'base', // 'sm' | 'base' | 'lg' | 'xl'
|
||||
cb: 'off', // 'off' | 'on' (colorblind-safe)
|
||||
font: 'default', // 'default' | 'readable'
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply preferences to a document element by setting/removing the data-*
|
||||
* attributes the CSS layer keys off. `el` defaults to document.documentElement;
|
||||
* tests pass a fake element exposing setAttribute/removeAttribute.
|
||||
*/
|
||||
function applyPrefs(prefs, el) {
|
||||
const target = el || (typeof document !== 'undefined' ? document.documentElement : null);
|
||||
if (!target) return;
|
||||
const p = { ...DEFAULTS, ...(prefs || {}) };
|
||||
|
||||
const setOrRemove = (attr, value) => {
|
||||
if (value == null) target.removeAttribute(attr);
|
||||
else target.setAttribute(attr, value);
|
||||
};
|
||||
|
||||
setOrRemove('data-motion', p.motion === 'reduced' ? 'reduced' : null);
|
||||
setOrRemove('data-contrast', p.contrast === 'high' ? 'high' : null);
|
||||
setOrRemove('data-text', p.text && p.text !== 'base' ? p.text : null);
|
||||
setOrRemove('data-cb', p.cb === 'on' || p.cb === '1' ? '1' : null);
|
||||
setOrRemove('data-font', p.font === 'readable' ? 'readable' : null);
|
||||
}
|
||||
|
||||
/** Read persisted prefs merged over defaults. `storage` defaults to localStorage. */
|
||||
function loadPrefs(storage) {
|
||||
const store = storage || (typeof localStorage !== 'undefined' ? localStorage : null);
|
||||
if (!store) return { ...DEFAULTS };
|
||||
try {
|
||||
const raw = store.getItem(PREFS_KEY);
|
||||
if (!raw) return { ...DEFAULTS };
|
||||
return { ...DEFAULTS, ...JSON.parse(raw) };
|
||||
} catch {
|
||||
return { ...DEFAULTS };
|
||||
}
|
||||
}
|
||||
|
||||
/** Persist prefs (merged over defaults). Returns the merged object. */
|
||||
function savePrefs(prefs, storage) {
|
||||
const store = storage || (typeof localStorage !== 'undefined' ? localStorage : null);
|
||||
const merged = { ...DEFAULTS, ...(prefs || {}) };
|
||||
if (store) {
|
||||
try {
|
||||
store.setItem(PREFS_KEY, JSON.stringify(merged));
|
||||
} catch {
|
||||
/* ignore quota / privacy-mode failures */
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
module.exports = { PREFS_KEY, DEFAULTS, applyPrefs, loadPrefs, savePrefs };
|
||||
Reference in New Issue
Block a user