// health/lib.jsx // Shared Ordentus iOS components — icons, tab bar, status bar wrapper, // metric primitives, charts. All theming via the `dark` boolean. // // Globals exported (window.*): Icon, TabBar, BigTitle, NavBar, IslandStatus, // MetricTile, RingChart, BarChart, ConsistencyDots, StageStack, HBar, // SectionHead, InsightCard, Pill, TrendChip, SegControl, Scrim, Banner, // FrameScreen. // ───────────────────────────────────────────────────────────── // SF Symbol-style icons, stroke 2.2, currentColor. // We draw chrome icons (Ordentus has its own pack) — line weight matches // the assets/icons/* SVGs in the design system. Names mirror Ordentus's // icon library but stay neutral/HIG-aligned. // ───────────────────────────────────────────────────────────── const Icon = ({ name, size = 22, stroke = 2, style }) => { const s = { width: size, height: size, ...style }; const sw = stroke; const common = { fill: 'none', stroke: 'currentColor', strokeWidth: sw, strokeLinecap: 'round', strokeLinejoin: 'round' }; switch (name) { case 'calendar': return (); case 'calendar-filled': return (); case 'heart-filled': return (); case 'today': return (); case 'heart': return (); case 'share': return (); case 'settings': return (); case 'moon': return (); case 'cognition': case 'brain': return (); case 'activity': case 'bolt': return (); case 'mindfulness': case 'sun': return (); case 'trend-up': return (); case 'chevron-right': return (); case 'chevron-left': return (); case 'chevron-down': return (); case 'ellipsis': return (); case 'plus': return (); case 'close': return (); case 'check': return (); case 'refresh': return (); case 'search': return (); case 'arrow-up': return (); case 'arrow-down': return (); case 'arrow-right': return (); case 'alert': return (); case 'wifi-off': return (); case 'bed': return (); case 'pencil': return (); case 'shuffle': return (); case 'clock': return (); case 'footsteps': return (); case 'flame': return (); case 'lock': return (); case 'sparkles': return (); case 'home': return (); case 'home-filled': return (); case 'user': case 'person': return (); case 'person-filled': return (); case 'cloud-sun': return (); case 'thermometer': return (); default: return null; } }; // ───────────────────────────────────────────────────────────── // Tab bar — 5 tabs, frosted glass on iOS // ───────────────────────────────────────────────────────────── const TAB_DEFS = [ { id: 'calendar', label: 'Calendar', icon: 'calendar' }, { id: 'today', label: 'Today', icon: 'today' }, { id: 'health', label: 'Health', icon: 'heart' }, { id: 'share', label: 'Share', icon: 'share' }, { id: 'settings', label: 'Settings', icon: 'settings' }, ]; const TabBar = ({ active = 'health' }) => (
{TAB_DEFS.map(t => (
{t.label}
))}
); // ───────────────────────────────────────────────────────────── // Custom big-title nav (replaces ios-frame's default so we can // place a clean avatar + ellipsis trailing pair). // ───────────────────────────────────────────────────────────── const NavTopRow = ({ leading, trailing }) => (
{leading}
{trailing}
); const Avatar = ({ initial = 'L' }) => (
{initial}
); const PillBtn = ({ children, onClick }) => ( ); const BigTitle = ({ title, leading, trailing, sub }) => ( <>

{title}

{sub &&
{sub}
}
); // Inline nav (after push) — back chevron + centered title const InlineNav = ({ back = 'Health', title }) => (
{back}
{title}
); // ───────────────────────────────────────────────────────────── // Section header inside a screen scroll content // ───────────────────────────────────────────────────────────── const SectionHead = ({ icon, color, label, see }) => (
{icon && }
{label}
{see && {see}}
); // ───────────────────────────────────────────────────────────── // Pill / trend chip // ───────────────────────────────────────────────────────────── const Pill = ({ tone = 'info', children }) => ( {children} ); const TrendChip = ({ dir = 'flat', value }) => ( {dir === 'up' && } {dir === 'down' && } {dir === 'flat' && } {value} ); // ───────────────────────────────────────────────────────────── // Metric tile (Apple Health hierarchy: lbl → value → trend → ctx) // ───────────────────────────────────────────────────────────── const MetricTile = ({ dotColor, label, value, unit, trendDir, trendValue, ctx, accent }) => (
{dotColor && } {label}
{value} {unit && {unit}}
{(trendDir || ctx) && (
{trendDir && } {ctx &&
{ctx}
}
)}
); // ───────────────────────────────────────────────────────────── // Ring chart (animated stroke-end on appear) // Centered text uses baseline-aligned number+unit, with optional // sub-labels below. We let the inner flex column dictate visual center // so the number sits dead-centre in the ring regardless of how many // caption lines follow. // ───────────────────────────────────────────────────────────── const RingChart = ({ percent = 78, size = 130, stroke = 12, color = 'var(--io-purple)', label, sublabel, valueSize, valueUnit = '%', dark }) => { const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const dash = c * Math.min(Math.max(percent, 0), 100) / 100; const trackColor = dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)'; // Auto-scale the number to the ring. Heuristic: ~35% of diameter looks // right at every size we use (86 / 108 / 110 / 130). const numSize = valueSize ?? Math.round(size * 0.34); const unitSize = Math.round(numSize * 0.42); return (
30 ? -1.2 : -0.5, color: 'var(--io-label)', }}>{percent} {valueUnit}
{label &&
{label}
} {sublabel &&
{sublabel}
}
); }; // ───────────────────────────────────────────────────────────── // 7-day bar chart with dashed goal line // ───────────────────────────────────────────────────────────── const BarChart = ({ data, goal = 8, max = 10, height = 130, todayIndex = 6, showLabels = true, accent = 'var(--io-purple)' }) => { // data: [{ day:'Mon', hours: 6.2 }, ...] const goalPct = (1 - goal / max) * 100; return (
Goal {goal}h
{data.map((d, i) => { const h = Math.max(2, (d.hours / max) * height); const under = d.hours < goal - 1.5; return (
); })}
{showLabels && (
{data.map((d, i) => ( {d.day} ))}
)}
); }; // ───────────────────────────────────────────────────────────── // Sleep stage stack (REM / Deep / Core / Awake) // ───────────────────────────────────────────────────────────── const StageStack = ({ stages }) => { // stages: [{ kind: 'deep', pct: 21, label: '1h 18m' }, ...] const colors = { deep: 'var(--io-stage-deep)', rem: 'var(--io-stage-rem)', core: 'var(--io-stage-core)', awake: 'var(--io-stage-awake)', }; return (
{stages.map((s, i) => ())}
{stages.map((s, i) => (
{s.kind === 'rem' ? 'REM' : s.kind[0].toUpperCase() + s.kind.slice(1)}
{s.label}
{s.pct}%
))}
); }; // ───────────────────────────────────────────────────────────── // Consistency dots (consistent wake/sleep rhythm) // ───────────────────────────────────────────────────────────── const ConsistencyDots = ({ filled, total = 10 }) => ( {Array.from({ length: total }).map((_, i) => ( ))} ); // ───────────────────────────────────────────────────────────── // Horizontal bars (cognitive overview) // ───────────────────────────────────────────────────────────── const HBar = ({ rows }) => (
{rows.map((r, i) => (
{r.label}
{r.value}
))}
); // ───────────────────────────────────────────────────────────── // Insight card // ───────────────────────────────────────────────────────────── const InsightCard = ({ emoji, icon, color, title, body }) => (
{emoji ? emoji : }
{title}
{body}
); // ───────────────────────────────────────────────────────────── // Segmented control // ───────────────────────────────────────────────────────────── const SegControl = ({ items, active, onChange }) => (
{items.map(it => (
onChange && onChange(it)}>{it}
))}
); // ───────────────────────────────────────────────────────────── // Banner (inline error / offline / warn) // ───────────────────────────────────────────────────────────── const Banner = ({ kind = 'warn', icon = 'alert', children, action }) => (
{children}
{action && {action}}
); // ───────────────────────────────────────────────────────────── // Wrapper that hosts IOSDevice + theme attribute on inner root. // We forgo IOSNavBar — every screen draws its own chrome. // ───────────────────────────────────────────────────────────── const FrameScreen = ({ dark = false, children, width = 402, height = 874 }) => (
{children}
); Object.assign(window, { Icon, TabBar, NavTopRow, Avatar, PillBtn, BigTitle, InlineNav, SectionHead, Pill, TrendChip, MetricTile, RingChart, BarChart, StageStack, ConsistencyDots, HBar, InsightCard, SegControl, Banner, FrameScreen, });