// shared.jsx — Fitbody design tokens + core UI primitives // Loaded first. Exports everything to window for the screen files. // Faithful iOS system colors. Light = systemBackground / grouped neutrals + iOS // system accents; Dark = true-black elevated surfaces + iOS dark accents. // PRIMARY = systemBlue. Token KEY names kept; values reskinned per theme. // (xxxDark = darker shade for 3D lips & accent text in LIGHT; in DARK it's a // brighter shade so accent text stays legible on dark tinted fills.) const LIGHT = { // labels ink: '#1C1C1E', ink2: '#6C6C70', ink3: '#8E8E93', // grouped backgrounds bg: '#FFFFFF', surface: '#F2F2F7', surface2: '#E5E5EA', line: 'rgba(60,60,67,0.12)', line2: 'rgba(60,60,67,0.22)', glass: 'rgba(249,249,249,0.94)', glassBorder: 'rgba(60,60,67,0.12)', // accents (iOS system, light) coral: '#007AFF', coralDark: '#0062CC', // PRIMARY — systemBlue amber: '#FF9500', amberDark: '#C2660A', // streak — systemOrange calm: '#30B0C7', calmDark: '#1E8299', // recovery — systemTeal good: '#34C759', // success — systemGreen violet:'#5856D6', violetDark:'#3D3CB0', // strength — systemIndigo teal: '#32ADE6', tealDark: '#1B89BC', // cardio — systemCyan pink: '#FF2D55', pinkDark: '#D11440', // social — systemPink energy:'#34C759', energyDark:'#248A43', // energy — systemGreen // tinted fills (~12%) coralSoft: '#E1EDFF', coralTint: '#F0F6FF', amberSoft: '#FFEFD9', calmSoft: '#DEF3F7', goodSoft: '#DDF5E3', violetSoft: '#E8E8FB', tealSoft: '#DEF1FC', pinkSoft: '#FFE0E6', energySoft: '#DDF5E3', }; const DARK = { ink: '#FFFFFF', ink2: '#AEAEB2', ink3: '#8E8E93', // true-black grouped: base #000, elevated cards #1C1C1E bg: '#1C1C1E', surface: '#000000', surface2: '#2C2C2E', line: 'rgba(255,255,255,0.10)', line2: 'rgba(255,255,255,0.20)', glass: 'rgba(30,30,32,0.82)', glassBorder: 'rgba(255,255,255,0.12)', // accents (iOS system, dark — brighter) coral: '#0A84FF', coralDark: '#409CFF', // PRIMARY — systemBlue (dark) amber: '#FF9F0A', amberDark: '#FFB340', // streak — systemOrange (dark) calm: '#5AC8E0', calmDark: '#7FD9EC', // recovery — systemTeal (dark) good: '#30D158', // success — systemGreen (dark) violet:'#5E5CE6', violetDark:'#8482FF', // strength — systemIndigo (dark) teal: '#64D2FF', tealDark: '#9BE0FF', // cardio — systemCyan (dark) pink: '#FF375F', pinkDark: '#FF6B86', // social — systemPink (dark) energy:'#30D158', energyDark:'#5FE07F', // energy — systemGreen (dark) // tinted fills (dark, ~18% over black) coralSoft: '#14294A', coralTint: '#102038', amberSoft: '#3A2A0E', calmSoft: '#0F2A33', goodSoft: '#0E2A18', violetSoft: '#1E1E3C', tealSoft: '#0E2733', pinkSoft: '#331620', energySoft: '#0E2A18', }; const T = { ...LIGHT, r1: 14, r2: 20, r3: 28, r4: 36 }; window.T = T; window.applyTheme = (mode) => { Object.assign(T, mode === 'dark' ? DARK : LIGHT); document.documentElement.dataset.theme = mode; try { localStorage.setItem('fb_theme', mode); } catch (e) {} }; // category accent map — resolved LIVE against T (so soft-tints follow theme) const CAT_KEY = { mobility: { k: 'coral', label: 'Mobility' }, strength: { k: 'violet', label: 'Strength' }, recovery: { k: 'calm', label: 'Recovery' }, cardio: { k: 'teal', label: 'Cardio' }, breath: { k: 'amber', label: 'Breath' }, social: { k: 'pink', label: 'Social' }, }; function catColor(name) { const e = CAT_KEY[name] || CAT_KEY.mobility; return { c: T[e.k], d: T[e.k + 'Dark'], s: T[e.k + 'Soft'], label: e.label }; } window.catColor = catColor; // ── Phone metrics ────────────────────────────────────────── const PHONE_W = 390, PHONE_H = 844; window.PHONE_W = PHONE_W; window.PHONE_H = PHONE_H; // ── iOS-style status bar (tintable) ──────────────────────── function StatusBar({ light = false }) { const c = light ? '#fff' : T.ink; return (
9:41
); } // ── Icons (clean 1.9 stroke line set) ────────────────────── function Icon({ name, size = 24, color = 'currentColor', sw = 1.9, fill = 'none', style }) { const p = { fill, stroke: fill === 'none' ? color : 'none', strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round' }; const paths = { home: , quest: , coach: , academy: , progress:, gear: , back: , close: , chevron: , flame: , bolt: , clock: , seat: , camera_off: , cameraline: , check: , drop: , moon: , wind: , pause: , play: , bell: , user: , sparkle: , arrow: , plus: , lock: , share: , leaf: , dumbbell:, target: , gem: , freeze: , chest: , crown: , heart: , star: , medal: , scan: , chat: , }; return {paths[name]}; } // ── Ava — coral character with face + bounce (game mode) ─── function AvaOrb({ size = 44, speaking = false, mood = 'happy', style }) { const eyeY = size * 0.42, eyeDx = size * 0.17, eyeR = size * 0.082; return (
{/* eyes */} {/* smile */} {/* cheeks */} {/* glossy highlight */}
{speaking && (
)}
); } // ── Buttons — flat iOS filled / tinted (system style) ────── function Btn({ children, onClick, variant = 'primary', size = 'lg', disabled, icon, style }) { const base = { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8, width: '100%', border: 'none', cursor: disabled ? 'default' : 'pointer', fontFamily: 'inherit', fontWeight: 600, letterSpacing: -0.3, borderRadius: 14, transition: 'opacity .12s ease, transform .12s ease', height: size === 'lg' ? 52 : size === 'md' ? 48 : 40, fontSize: size === 'lg' ? 17 : size === 'md' ? 16 : 15, opacity: disabled ? 0.4 : 1, }; const variants = { primary: { background: T.coral, color: '#fff' }, calm: { background: T.calm, color: '#fff' }, good: { background: T.good, color: '#fff' }, amber: { background: T.amber, color: '#fff' }, violet: { background: T.violet, color: '#fff' }, teal: { background: T.teal, color: '#fff' }, pink: { background: T.pink, color: '#fff' }, dark: { background: T.ink, color: T.bg }, soft: { background: T.surface2, color: T.ink }, coralSoft:{ background: T.coralSoft, color: T.coralDark }, ghost: { background: 'transparent', color: T.coral, weight: 600 }, outline: { background: 'transparent', color: T.coral, edge: T.line2 }, }; const v = variants[variant]; const press = (el, on) => { if (disabled) return; el.style.opacity = on ? '0.62' : (disabled ? '0.4' : '1'); el.style.transform = on ? 'scale(0.985)' : 'scale(1)'; }; return ( ); } // ── Chip (selectable) ────────────────────────────────────── function Chip({ children, active, onClick, icon, style }) { return ( ); } // ── Card ─────────────────────────────────────────────────── function Card({ children, style, pad = 20, onClick }) { return (
{children}
); } // ── Progress bar — flat solid iOS fill ───────────────────── function XPBar({ value, max, color = T.coral, height = 8, animate }) { const pct = Math.min(100, (value / max) * 100); return (
); } // ── Streak / energy pills for headers (chunky game style) ── function StatPill({ icon, label, tone = 'amber' }) { const tones = { amber: { bg: T.amberSoft, fg: T.amberDark }, coral: { bg: T.coralSoft, fg: T.coralDark }, calm: { bg: T.calmSoft, fg: T.calmDark }, teal: { bg: T.tealSoft, fg: T.tealDark }, ink: { bg: T.surface2, fg: T.ink }, }; const t = tones[tone]; return (
{icon}{label}
); } // ── Energy pill — the play resource w/ recharge countdown ── function fmtClock(s) { const m = Math.floor(s / 60), ss = s % 60; return `${String(m).padStart(2, '0')}:${String(ss).padStart(2, '0')}`; } function EnergyPill({ energy, max, pro, nextSec, onClick }) { const full = energy >= max; return ( ); } // ── Daily goal ring (Duolingo-style) ─────────────────────── function DailyGoalRing({ value, goal, size = 56 }) { const r = size / 2 - 5, c = 2 * Math.PI * r, pct = Math.min(1, value / goal); return (
); } // ── Ava speech block ─────────────────────────────────────── function AvaSays({ children, size = 44, align = 'row', style }) { return (
AVA
{children}
); } // ── Quest meta condition tags ────────────────────────────── function MetaTags({ tags }) { return (
{tags.map((t, i) => ( {t.icon && }{t.label} ))}
); } // ── Bottom sheet modal ───────────────────────────────────── function Sheet({ open, onClose, children, light }) { if (!open) return null; return (
e.stopPropagation()} style={{ width: '100%', background: T.bg, borderRadius: '28px 28px 0 0', padding: '12px 22px 30px', boxSizing: 'border-box', animation: 'sheetUp .32s cubic-bezier(.2,.9,.2,1)', }}>
{children}
); } // ── Bottom navigation ────────────────────────────────────── function BottomNav({ active, onNav }) { const items = [ { id: 'home', label: 'Home', icon: 'home' }, { id: 'quest', label: 'Quest', icon: 'quest' }, { id: 'academy', label: 'Academy', icon: 'academy' }, { id: 'progress', label: 'Progress', icon: 'progress' }, { id: 'league', label: 'League', icon: 'medal' }, ]; return (
{items.map(it => { const on = active === it.id; return ( ); })}
); } // ── Scrollable screen body with safe paddings ────────────── function Screen({ children, bg = T.surface, light, pad = true, nav, scroll = true, style }) { return (
{children}
{nav && }
); } // ── Floating Ava chat button (replaces Coach tab) ────────── function ChatFab({ onClick }) { return ( ); } // ── Top bar with optional back ───────────────────────────── function TopBar({ title, onBack, right, light, transparent }) { return (
{onBack && ( )}
{title}
{right}
); } Object.assign(window, { StatusBar, Icon, AvaOrb, Btn, Chip, Card, XPBar, StatPill, EnergyPill, fmtClock, DailyGoalRing, AvaSays, MetaTags, Sheet, BottomNav, ChatFab, Screen, TopBar, });