// 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' }) => (
);
// ─────────────────────────────────────────────────────────────
// Custom big-title nav (replaces ios-frame's default so we can
// place a clean avatar + ellipsis trailing pair).
// ─────────────────────────────────────────────────────────────
const NavTopRow = ({ leading, trailing }) => (
);
const Avatar = ({ initial = 'L' }) => (
{initial}
);
const PillBtn = ({ children, onClick }) => (
{children}
);
const BigTitle = ({ title, leading, trailing, sub }) => (
<>
>
);
// Inline nav (after push) — back chevron + centered title
const InlineNav = ({ back = 'Health', title }) => (
);
// ─────────────────────────────────────────────────────────────
// Section header inside a screen scroll content
// ─────────────────────────────────────────────────────────────
const SectionHead = ({ icon, color, label, 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) => (
))}
);
// ─────────────────────────────────────────────────────────────
// Insight card
// ─────────────────────────────────────────────────────────────
const InsightCard = ({ emoji, icon, color, 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,
});