// 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 (
);
}
// ── 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 ;
}
// ── 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 */}
{/* 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 (
);
}
// ── 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 (
);
}
// ── 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,
});