/* ============================================================
METABOLE — App-surface widgets (floating cards + live demos)
============================================================ */
/* ---- Small shared bits ---- */
function CardLabel({ icon, children, right }) {
return (
{icon && }{children}
{right}
);
}
/* ---- Next dose card ---- */
function DoseCard({ active = true, style }) {
return (
Due today}>Next dose
Tirzepatide
7.5 mg
·
37.5 units
·
10 mg/mL
Left abdomen · rotated
);
}
/* ---- Weight card ---- */
function WeightCard({ active = true, style }) {
return (
▼ 20.4 lb}>Weight trend
193.6
lb
14 WK
);
}
/* ---- Check-in card ---- */
function CheckinCard({ active = true, style }) {
const rows = [
{ k: "Energy", v: 4, icon: "bolt", c: "var(--green)" },
{ k: "Sleep", v: 3, icon: "moon", c: "var(--sage)" },
{ k: "Mood", v: 5, icon: "spark", c: "var(--clay)" },
];
return (
Daily check-in
{rows.map((r, i) => (
{r.k}
{[1, 2, 3, 4, 5].map(n => (
))}
))}
);
}
/* ---- Side effect card ---- */
function SideEffectCard({ active = true, style }) {
const items = [
{ k: "Nausea", lvl: "Mild", w: 28, c: "var(--amber)" },
{ k: "Fatigue", lvl: "Mild", w: 22, c: "var(--sage)" },
{ k: "Constipation", lvl: "None", w: 6, c: "var(--mint-2)" },
];
return (
THIS WK}>Side effects
{items.map((it, i) => (
))}
);
}
/* ---- Hydration / protein rings ---- */
function VitalsCard({ active = true, style }) {
return (
);
}
/* ---- Insight card ("what changed this week") ---- */
function InsightCard({ active = true, style }) {
return (
What changed this week
Your protein averaged 112g/day — up 18% — and nausea dropped on days you hydrated before noon.
+18% PROTEIN
−1.4 LB
);
}
/* ---- Injection-site rotation map (abstract) ---- */
function InjectionSiteMap({ active = true }) {
const zones = [
{ id: "LA", label: "L abdomen", x: 0, y: 0, days: 0, next: true },
{ id: "RA", label: "R abdomen", x: 1, y: 0, days: 14 },
{ id: "LT", label: "L thigh", x: 0, y: 1, days: 7 },
{ id: "RT", label: "R thigh", x: 1, y: 1, days: 11 },
{ id: "LM", label: "L arm", x: 0, y: 2, days: 4 },
{ id: "RM", label: "R arm", x: 1, y: 2, days: 18 },
];
const fresh = (d) => d === 0 ? "var(--clay)" : `color-mix(in oklab, var(--green) ${Math.min(100, d / 18 * 100)}%, var(--mint-2))`;
return (
{zones.map((z, i) => (
{z.label}
{z.next ? "USE NEXT →" : z.days + " DAYS AGO"}
))}
);
}
/* ---- Interactive compound level estimator ---- */
const COMPOUNDS = [
{ id: "tirz", name: "Tirzepatide", t12: 120, tmax: 24, interval: 168, dose: "7.5 mg", color: "var(--green)" },
{ id: "sema", name: "Semaglutide", t12: 168, tmax: 30, interval: 168, dose: "1.0 mg", color: "var(--sage)" },
{ id: "lira", name: "Liraglutide", t12: 13, tmax: 11, interval: 24, dose: "1.8 mg", color: "var(--clay)" },
{ id: "bpc", name: "BPC-157", t12: 4, tmax: 1.5, interval: 24, dose: "250 mcg", color: "var(--amber)" },
];
function bateman(t, ke, ka) {
if (t <= 0) return 0;
return (ke / (ka - ke)) * (Math.exp(-ke * t) - Math.exp(-ka * t));
}
function CompoundEstimator({ active = true }) {
const [cid, setCid] = useState("tirz");
const c = COMPOUNDS.find(x => x.id === cid);
const [t, setT] = useState(c.tmax);
useEffect(() => { setT(COMPOUNDS.find(x => x.id === cid).tmax); }, [cid]);
const ke = Math.log(2) / c.t12;
const ka = ke * 5.2;
const interval = c.interval;
const W = 520, H = 190, padL = 10, padR = 10, padT = 14, padB = 26;
const innerW = W - padL - padR, innerH = H - padT - padB;
const N = 120;
const samples = useMemo(() => Array.from({ length: N + 1 }, (_, i) => bateman((i / N) * interval, ke, ka)), [cid]);
const peak = Math.max(...samples);
const pts = samples.map((v, i) => [padL + (i / N) * innerW, padT + (1 - v / peak) * innerH]);
const line = buildPath(pts);
const area = line + ` L${pts[pts.length - 1][0]} ${padT + innerH} L${pts[0][0]} ${padT + innerH} Z`;
const lvl = bateman(t, ke, ka) / peak;
const markX = padL + (t / interval) * innerW;
const markY = padT + (1 - lvl) * innerH;
const pct = Math.round(lvl * 100);
const phase = t < c.tmax * 0.85 ? "Absorbing" : t < c.tmax * 1.8 ? "Near peak" : lvl > 0.35 ? "Declining" : "Trough";
const phaseColor = phase === "Near peak" ? "var(--green)" : phase === "Trough" ? "var(--clay)" : "var(--sage)";
const hoursToNext = Math.max(0, interval - t);
const fmtT = (h) => h < 48 ? h.toFixed(0) + "h" : (h / 24).toFixed(1) + "d";
return (
{COMPOUNDS.map(x => (
))}
Estimated level
{pct}
%
of peak
{phase}
);
}
function Row({ k, v, accent }) {
return (
{k}
{v}
);
}
Object.assign(window, {
DoseCard, WeightCard, CheckinCard, SideEffectCard, VitalsCard, InsightCard,
InjectionSiteMap, CompoundEstimator, CardLabel,
});