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:
Kev
2026-06-16 10:37:31 -04:00
parent f88961885c
commit 956a7455eb
13 changed files with 949 additions and 8 deletions
+5 -2
View File
@@ -7,6 +7,7 @@ import Nav from '@/components/Nav';
import Footer from '@/components/Footer';
import AuthGate from '@/components/AuthGate';
import HashRedirect from '@/components/vyndr/HashRedirect';
import GlobalHosts from '@/components/vyndr/GlobalHosts';
import ParlayTray from '@/components/ParlayTray';
import BottomTabBar from '@/components/BottomTabBar';
import InstallPrompt from '@/components/InstallPrompt';
@@ -128,9 +129,9 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<ParlayProvider>
<HashRedirect />
<Nav />
{/* Header = 60px nav + 32px ticker; offset main so content clears it. */}
{/* Header = 60px nav + 32px ticker + 30px heartbeat (§8); offset main. */}
<AuthGate>
<main style={{ paddingTop: 96, minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
<main style={{ paddingTop: 124, minHeight: '100vh', paddingBottom: 80 }}>{children}</main>
</AuthGate>
<Footer />
<ParlayTray />
@@ -141,6 +142,8 @@ export default async function RootLayout({ children }: { children: React.ReactNo
<MFAChallenge />
<CookieConsent />
<SentryInit />
{/* Session 38 — prefs apply + paywall/checkout/prefs globals (§9/§10/§12) */}
<GlobalHosts />
</ParlayProvider>
</ExplainModeProvider>
</AuthProvider>