import { useState, useCallback } from "react";
const COLORS = {
teal: "#0D6E6E", tealLight: "#E6F4F4", tealMid: "#1A8F8F",
navy: "#0B2545", slate: "#4A5568", muted: "#718096",
border: "#CBD5E0", bg: "#F7FAFA", white: "#FFFFFF",
green: "#276749", greenBg: "#F0FFF4",
yellow: "#744210", yellowBg: "#FFFBEB",
red: "#9B2335", redBg: "#FFF5F5",
blue: "#1A4E8A", blueBg: "#EBF4FF",
purple: "#553C9A", purpleBg: "#FAF5FF",
};
const TABS = ["Dosing", "Peds", "Scoring", "General"];
// ─── UNIT HELPERS ─────────────────────────────────────────────────────────────
const lbsToKg = (lbs) => parseFloat(lbs) / 2.205;
const kgToLbs = (kg) => parseFloat(kg) * 2.205;
const feetInchesToCm = (ft, inch) => (parseInt(ft || 0) * 12 + parseFloat(inch || 0)) * 2.54;
// ─── SHARED STYLES ────────────────────────────────────────────────────────────
const labelStyle = { fontFamily:"'DM Sans',sans-serif", fontSize:13, fontWeight:600, color:COLORS.slate, display:"block" };
const inputStyle = { width:"100%", padding:"10px 12px", borderRadius:8, border:`1.5px solid ${COLORS.border}`, fontFamily:"'DM Sans',sans-serif", fontSize:15, color:COLORS.navy, outline:"none", boxSizing:"border-box", background:COLORS.bg };
const selectStyle = { padding:"10px 12px", borderRadius:8, border:`1.5px solid ${COLORS.border}`, fontFamily:"'DM Sans',sans-serif", fontSize:15, color:COLORS.navy, background:COLORS.white, cursor:"pointer" };
// ─── SMALL REUSABLES ─────────────────────────────────────────────────────────
function Row({ label, value }) {
return (
{label}
{value}
);
}
function Stat({ label, value, sub }) {
return (
{label}
{value}
{sub &&
{sub}
}
);
}
function RiskBadge({ level, risk, action }) {
const map = {
green:{ bg:COLORS.greenBg, color:COLORS.green, border:"#9AE6B4" },
yellow:{ bg:COLORS.yellowBg, color:COLORS.yellow, border:"#F6E05E" },
red:{ bg:COLORS.redBg, color:COLORS.red, border:"#FC8181" },
blue:{ bg:COLORS.blueBg, color:COLORS.blue, border:"#90CDF4" },
purple:{ bg:COLORS.purpleBg, color:COLORS.purple, border:"#D6BCFA" },
};
const s = map[level] || map.green;
return (
{risk}
{action &&
{action}
}
);
}
function CalcShell({ title, subtitle, children }) {
return (
{title}
{subtitle &&
{subtitle}
}
{children}
);
}
function Field({ label, id, vals, set, placeholder }) {
return (
{label}
set(id,e.target.value)} style={{...inputStyle, marginTop:6}} placeholder={placeholder||"0"} />
);
}
function HeightField({ ftId, inId, vals, set }) {
return (
);
}
function SexToggle({ sexKey, vals, set }) {
return (
Sex
{["male","female"].map(s=>(
set(sexKey,s)}
style={{ flex:1, padding:"10px 0", borderRadius:8, border:`1.5px solid ${vals[sexKey]===s||(!vals[sexKey]&&s==="male")?COLORS.teal:COLORS.border}`, background:vals[sexKey]===s||(!vals[sexKey]&&s==="male")?COLORS.teal:COLORS.white, color:vals[sexKey]===s||(!vals[sexKey]&&s==="male")?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontWeight:600, fontSize:14, cursor:"pointer" }}>
{s.charAt(0).toUpperCase()+s.slice(1)}
))}
);
}
function ResultBox({ label, value, unit, badge, note }) {
return (
{label}
{value} {unit}
{badge &&
}
{note &&
{note}
}
);
}
function SubTabBar({ options, active, onSelect }) {
return (
{options.map(o=>(
onSelect(o.key)}
style={{ padding:"7px 13px", borderRadius:8, border:`1.5px solid ${active===o.key?COLORS.teal:COLORS.border}`, background:active===o.key?COLORS.teal:COLORS.white, color:active===o.key?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontWeight:600, fontSize:12, cursor:"pointer" }}>
{o.label}
))}
);
}
// ══════════════════════════════════════════════════════════════════════════════
// DOSING DATA
// ══════════════════════════════════════════════════════════════════════════════
const MEDICATIONS = [
{ name:"Acetaminophen", indications:["Fever / Pain"], dosePerKg:15, maxDoseMg:1000, maxDailyMg:4000, frequency:"Every 4–6 hrs", form:"Liquid 160mg/5mL · Chewable 80mg · Tab 325mg", note:"Max 5 doses/day. Avoid in liver disease." },
{ name:"Ibuprofen", indications:["Fever / Pain / Inflammation"], dosePerKg:10, maxDoseMg:600, maxDailyMg:2400, frequency:"Every 6–8 hrs", form:"Liquid 100mg/5mL · Tab 200mg · Tab 400mg", note:"Give with food. Avoid < 6 months or if dehydrated/renal concerns." },
{ name:"Amoxicillin (standard)", indications:["Otitis Media / Pharyngitis / Skin"], dosePerKg:40, maxDoseMg:500, maxDailyMg:1500, frequency:"Every 8 hrs", form:"Susp 250mg/5mL · Susp 400mg/5mL · Cap 500mg", note:"High-dose: 80–90 mg/kg/day for resistant AOM." },
{ name:"Amoxicillin (high-dose)", indications:["Resistant AOM / Sinusitis"], dosePerKg:80, maxDoseMg:875, maxDailyMg:1750, frequency:"Every 12 hrs", form:"Susp 400mg/5mL · Tab 875mg", note:"Use when resistant Strep pneumo suspected." },
{ name:"Azithromycin", indications:["Atypical Pneumonia / Strep (PCN allergy)"], dosePerKg:10, maxDoseMg:500, maxDailyMg:500, frequency:"Once daily × 5 days", form:"Susp 200mg/5mL · Tab 250mg", note:"Day 1: 10mg/kg. Days 2–5: 5mg/kg. Max 500mg day 1." },
{ name:"Cetirizine", indications:["Allergic Rhinitis / Urticaria"], dosePerKg:0.25, maxDoseMg:10, maxDailyMg:10, frequency:"Once daily", form:"Syrup 5mg/5mL · Tab 5mg / 10mg", note:"Ages 6m–2yr: 2.5mg. Ages 2–5: up to 5mg. Ages ≥6: up to 10mg." },
{ name:"Diphenhydramine", indications:["Allergic Rxn / Urticaria / Insomnia"], dosePerKg:1.25, maxDoseMg:50, maxDailyMg:300, frequency:"Every 6 hrs", form:"Liquid 12.5mg/5mL · Cap 25mg · Tab 50mg", note:"Not for use < 2 years. Sedating — use caution." },
];
// ══════════════════════════════════════════════════════════════════════════════
// DOSING TAB
// ══════════════════════════════════════════════════════════════════════════════
function DosingTab() {
const [weight, setWeight] = useState("");
const [unit, setUnit] = useState("lbs");
const [selected, setSelected] = useState(null);
const weightKg = unit==="lbs" ? lbsToKg(weight) : parseFloat(weight);
const weightLbs = unit==="lbs" ? parseFloat(weight) : kgToLbs(weight);
const isValid = !isNaN(weightKg) && weightKg>0 && weightKg<=200;
const calc = useCallback((med)=>{
if(!isValid) return null;
const doseMg = Math.min(med.dosePerKg*weightKg, med.maxDoseMg);
const liquidMl = (doseMg/160)*5;
const liquidTsp = (liquidMl/4.929).toFixed(2);
return { doseMg:doseMg.toFixed(1), liquidMl:liquidMl.toFixed(2), liquidTsp };
},[weightKg, isValid]);
return (
Patient Weight
setWeight(e.target.value)} style={inputStyle} />
setUnit(e.target.value)} style={selectStyle}>
lbs
kg
{weight && isValid &&
≈ {weightLbs.toFixed(1)} lbs · {weightKg.toFixed(1)} kg
}
{MEDICATIONS.map(med=>{
const result=calc(med); const open=selected===med.name;
return (
setSelected(open?null:med.name)} style={{ width:"100%", background:"none", border:"none", padding:"14px 18px", textAlign:"left", cursor:"pointer", display:"flex", justifyContent:"space-between", alignItems:"center" }}>
{med.name}
{med.indications.join(" · ")}
{result ? (
{result.doseMg} mg
{result.liquidTsp} tsp*
) : {open?"▲":"▼"} }
{open && (
{result && (
)}
⚠ {med.note}
*Volume based on 160mg/5mL. Verify concentration in hand.
)}
);
})}
);
}
// ══════════════════════════════════════════════════════════════════════════════
// PEDS TAB — all pediatric-specific modules
// ══════════════════════════════════════════════════════════════════════════════
const PEDS_MODULES = [
{ key:"bili", label:"BiliTool" },
{ key:"vitals", label:"Vitals" },
{ key:"bp", label:"BP%" },
{ key:"apgar", label:"Apgar" },
{ key:"ivfluid",label:"IV Fluids" },
{ key:"altmed", label:"Antipyretic" },
{ key:"growth", label:"Growth Z" },
{ key:"dehy", label:"Dehydration" },
];
function PedsTab() {
const [mod, setMod] = useState("bili");
const [vals, setVals] = useState({});
const set = (k,v) => setVals(p=>({...p,[k]:v}));
return (
{setMod(k);setVals({});}} />
{mod==="bili" && }
{mod==="vitals" && }
{mod==="bp" && }
{mod==="apgar" && }
{mod==="ivfluid" && }
{mod==="altmed" && }
{mod==="growth" && }
{mod==="dehy" && }
);
}
// ─── BILITOOL ─────────────────────────────────────────────────────────────────
// AAP 2022 phototherapy thresholds (mg/dL) by gestational age zone at given hours
function getBiliThreshold(ga, hours, risk) {
// Simplified AAP 2022 nomogram thresholds (mg/dL) — phototherapy line
// risk: "low" = no risk factors, "high" = isoimmune / G6PD / albumin <3
const base = {
24: { low:9.0, high:7.0 },
36: { low:11.0, high:9.5 },
48: { low:13.0, high:11.0 },
60: { low:14.5, high:12.5 },
72: { low:15.5, high:13.5 },
84: { low:16.5, high:14.5 },
96: { low:17.5, high:15.5 },
120:{ low:18.5, high:16.5 },
144:{ low:19.0, high:17.0 },
168:{ low:19.5, high:17.5 },
};
// GA adjustment: reduce threshold by 0.5 for each week below 40
const gaAdj = Math.max(0, (40 - Math.min(parseInt(ga)||40, 40)) * 0.5);
const keys = Object.keys(base).map(Number).sort((a,b)=>a-b);
let closest = keys[keys.length-1];
for(let k of keys){ if(hours<=k){ closest=k; break; } }
const val = base[closest][risk||"low"] - gaAdj;
return Math.max(val, 5).toFixed(1);
}
function BiliTool({ vals, set }) {
const ga = parseInt(vals.ga)||0;
const hours = parseFloat(vals.hours)||0;
const bili = parseFloat(vals.bili)||0;
const ready = ga>=35 && hours>=12 && bili>0;
let result = null;
if(ready){
const lowThresh = parseFloat(getBiliThreshold(ga, hours, "low"));
const highThresh = parseFloat(getBiliThreshold(ga, hours, "high"));
const exchThresh = lowThresh + 5;
if(bili >= exchThresh){
result = { level:"red", risk:"Exchange Transfusion Zone", action:`Bilirubin ${bili} mg/dL meets exchange transfusion threshold (~${exchThresh.toFixed(1)} mg/dL). Urgent neonatology consult.`, thresh:`Photo threshold: ${lowThresh} / ${highThresh} mg/dL` };
} else if(bili >= lowThresh){
result = { level:"yellow", risk:"Phototherapy Recommended", action:`Bilirubin (${bili}) ≥ threshold (${lowThresh} mg/dL at ${hours}h for ${ga}w GA). Start phototherapy. Recheck in 4–6 hrs.`, thresh:`High-risk threshold: ${highThresh} mg/dL` };
} else {
result = { level:"green", risk:"Below Phototherapy Threshold", action:`Bilirubin (${bili}) < ${lowThresh} mg/dL (threshold at ${hours}h, ${ga}w GA). Supportive care. Recheck per clinical judgment.`, thresh:`High-risk threshold: ${highThresh} mg/dL` };
}
}
return (
Neurotoxicity Risk Factors
{[["low","None / Low"],["high","High (isoimmune, G6PD, albumin <3)"]].map(([v,l])=>(
set("risk",v)}
style={{ flex:1, padding:"8px 6px", borderRadius:8, border:`1.5px solid ${vals.risk===v||(!vals.risk&&v==="low")?COLORS.teal:COLORS.border}`, background:vals.risk===v||(!vals.risk&&v==="low")?COLORS.teal:COLORS.white, color:vals.risk===v||(!vals.risk&&v==="low")?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontWeight:600, fontSize:12, cursor:"pointer", lineHeight:1.3 }}>
{l}
))}
{result && (
<>
{result.thresh}
>
)}
Based on AAP 2022 Clinical Practice Guideline. Confirm with institution protocol.
);
}
// ─── VITALS NORMS ─────────────────────────────────────────────────────────────
const VITALS_DATA = [
{ age:"Newborn (0–1 mo)", rr:"40–60", hr:"100–160", spo2:"≥95%", sbp:"60–90", concern:"RR>60 or <30, SpO₂<94%" },
{ age:"Infant (1–12 mo)", rr:"30–50", hr:"100–160", spo2:"≥96%", sbp:"70–100", concern:"RR>50, SpO₂<95%" },
{ age:"Toddler (1–3 yr)", rr:"24–40", hr:"90–150", spo2:"≥97%", sbp:"80–110", concern:"RR>40, SpO₂<95%" },
{ age:"Preschool (3–6 yr)",rr:"22–34", hr:"80–140", spo2:"≥97%", sbp:"80–110", concern:"RR>34, SpO₂<95%" },
{ age:"School (6–12 yr)", rr:"18–30", hr:"70–120", spo2:"≥97%", sbp:"85–120", concern:"RR>30, SpO₂<95%" },
{ age:"Adolescent (>12)", rr:"12–20", hr:"60–100", spo2:"≥97%", sbp:"90–130", concern:"RR>20, SpO₂<95%" },
];
function VitalsNorms() {
return (
Respiratory Rate & SpO₂ Age Norms
Breaths/min · HR (bpm) · SpO₂ · SBP (mmHg)
{VITALS_DATA.map(v=>(
))}
);
}
// ─── BP PERCENTILE ────────────────────────────────────────────────────────────
// Simplified AAP 2017 BP classification by age/sex/height percentile
// Returns 50th/90th/95th percentile SBP approximations
function getBPNorms(age, sex, htPct) {
// Base 50th percentile SBP from AAP 2017 table approximations
const maleSBP50 = { 1:85,2:87,3:88,4:90,5:91,6:92,7:92,8:93,9:94,10:95,11:96,12:97,13:98 };
const femaleSBP50 = { 1:83,2:85,3:86,4:88,5:89,6:90,7:91,8:91,9:92,10:93,11:94,12:95,13:96 };
const base = (sex==="male"?maleSBP50:femaleSBP50)[Math.min(Math.max(parseInt(age)||1,1),13)] || 90;
const htAdj = htPct==="25"?-1:htPct==="75"?1:htPct==="90"?2:0;
const p50 = base + htAdj;
const p90 = p50 + 11;
const p95 = p50 + 13;
const p99 = p50 + 18;
return { p50, p90, p95, p99 };
}
function BPPercentile({ vals, set }) {
const age = parseInt(vals.bpage)||0;
const sbp = parseInt(vals.sbp)||0;
const dbp = parseInt(vals.dbp)||0;
const ready = age>=1 && age<=17 && sbp>0;
let result = null;
if(ready && age<=13){
const norms = getBPNorms(age, vals.bpsex||"male", vals.htpct||"50");
if(sbp >= norms.p99 || dbp >= 80){
result = { level:"red", risk:"Stage 2 Hypertension", action:`SBP ≥ 99th percentile (${norms.p99} mmHg) or DBP ≥ 80. Evaluate for secondary causes. Prompt referral.` };
} else if(sbp >= norms.p95){
result = { level:"red", risk:"Stage 1 Hypertension", action:`SBP ≥ 95th percentile (${norms.p95} mmHg). Confirm on 3 occasions. Lifestyle + follow-up.` };
} else if(sbp >= norms.p90){
result = { level:"yellow", risk:"Elevated BP", action:`SBP ≥ 90th percentile (${norms.p90} mmHg). Recheck in 6 months. Lifestyle counseling.` };
} else {
result = { level:"green", risk:"Normal BP", action:`SBP below 90th percentile (${norms.p90} mmHg) for age/sex/height.` };
}
result.norms = norms;
} else if(ready && age>=13){
// Adolescent ≥ 13: use adult thresholds
if(sbp>=130||dbp>=80) result={level:"red",risk:"Stage 1 HTN (adolescent)",action:"SBP ≥ 130 or DBP ≥ 80. Lifestyle modification. Evaluate secondary causes."};
else if(sbp>=120||dbp>=80) result={level:"yellow",risk:"Elevated BP (adolescent)",action:"SBP 120–129 and DBP <80. Lifestyle counseling. Recheck."};
else result={level:"green",risk:"Normal BP (adolescent)",action:"Below elevated threshold for adolescent."};
}
return (
{parseInt(vals.bpage||0)<13 && (
Height Percentile
set("htpct",e.target.value)} style={{...selectStyle, width:"100%", marginTop:6}}>
{["5","10","25","50","75","90","95"].map(p=>{p}th )}
)}
{result && (
<>
{result.norms && (
)}
>
)}
);
}
// ─── APGAR SCORE ──────────────────────────────────────────────────────────────
const APGAR_ITEMS = [
{ id:"appear", label:"Appearance (Skin Color)", options:[{v:0,l:"Blue/pale all over"},{v:1,l:"Blue hands/feet, pink body"},{v:2,l:"Pink all over"}] },
{ id:"pulse", label:"Pulse (Heart Rate)", options:[{v:0,l:"Absent"},{v:1,l:"< 100 bpm"},{v:2,l:"≥ 100 bpm"}] },
{ id:"grimace",label:"Grimace (Reflex)", options:[{v:0,l:"No response"},{v:1,l:"Grimace only"},{v:2,l:"Cry, cough, sneeze"}] },
{ id:"active", label:"Activity (Muscle Tone)", options:[{v:0,l:"Limp"},{v:1,l:"Some flexion"},{v:2,l:"Active motion"}] },
{ id:"resp", label:"Respiration", options:[{v:0,l:"Absent"},{v:1,l:"Weak / irregular"},{v:2,l:"Strong cry"}] },
];
function ApgarScore({ vals, set }) {
const [minute, setMinute] = useState("1");
const key = m => `apgar_${m}_`;
const score = APGAR_ITEMS.reduce((s,item)=>{
const v = vals[key(minute)+item.id];
return s + (v!==undefined ? parseInt(v) : 0);
}, 0);
const allSet = APGAR_ITEMS.every(item=>vals[key(minute)+item.id]!==undefined);
let result = null;
if(allSet){
if(score>=7) result={level:"green",risk:`Score ${score} — Normal`,action:"Continue routine newborn care. Reassess at 5 minutes."};
else if(score>=4) result={level:"yellow",risk:`Score ${score} — Moderate concern`,action:"Stimulate, suction, supplemental O₂. Reassess at 5 min. Consider NICU consult."};
else result={level:"red",risk:`Score ${score} — Severe / Requires resuscitation`,action:"Immediate resuscitation. Warm, dry, stimulate, PPV if needed. NICU."};
}
return (
{["1","5","10"].map(m=>(
setMinute(m)}
style={{ flex:1, padding:"8px 0", borderRadius:8, border:`1.5px solid ${minute===m?COLORS.teal:COLORS.border}`, background:minute===m?COLORS.teal:COLORS.white, color:minute===m?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontWeight:700, fontSize:13, cursor:"pointer" }}>
{m}-min
))}
{APGAR_ITEMS.map(item=>(
{item.label}
{item.options.map(opt=>{
const k = key(minute)+item.id;
const sel = vals[k]===String(opt.v)||vals[k]===opt.v;
return (
set(k, opt.v)}
style={{ padding:"8px 12px", borderRadius:8, border:`1.5px solid ${sel?COLORS.teal:COLORS.border}`, background:sel?COLORS.teal:COLORS.white, color:sel?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontSize:13, cursor:"pointer", textAlign:"left", display:"flex", justifyContent:"space-between" }}>
{opt.l}
{opt.v}
);
})}
))}
{minute}-min Score
{allSet?score:"—"}/10
{result && }
);
}
// ─── IV FLUID (HOLLIDAY-SEGAR 4-2-1) ─────────────────────────────────────────
function calcIVFluid(weightLbs) {
const wt = lbsToKg(weightLbs);
let mlPerHr = 0;
if(wt<=10) mlPerHr = wt*4;
else if(wt<=20) mlPerHr = 40+(wt-10)*2;
else mlPerHr = 60+(wt-20)*1;
const daily = mlPerHr*24;
const dailyOz = (daily/29.574).toFixed(1);
return { mlPerHr:mlPerHr.toFixed(1), daily:Math.round(daily), dailyOz, wt:wt.toFixed(1) };
}
function IVFluid({ vals, set }) {
const ready = vals.ivwt && parseFloat(vals.ivwt)>0;
const result = ready ? calcIVFluid(parseFloat(vals.ivwt)) : null;
return (
{result && (
)}
Rule: 4 mL/kg/hr for first 10kg · 2 mL/kg/hr for next 10kg · 1 mL/kg/hr for each kg after 20kg. Typical fluid: D5 ½NS + 20mEq/L KCl.
);
}
// ─── ANTIPYRETIC ALTERNATING SCHEDULE ─────────────────────────────────────────
function calcAltMed(weightLbs) {
const wt = lbsToKg(weightLbs);
const apapMg = Math.min(15*wt, 1000);
const ibupMg = Math.min(10*wt, 600);
const apapTsp = ((apapMg/160)*5/4.929).toFixed(2);
const ibupTsp = ((ibupMg/100)*5/4.929).toFixed(2);
return { apapMg:apapMg.toFixed(1), ibupMg:ibupMg.toFixed(1), apapTsp, ibupTsp };
}
function AltMed({ vals, set }) {
const ready = vals.altwt && parseFloat(vals.altwt)>0 && lbsToKg(parseFloat(vals.altwt))>=6;
const result = ready ? calcAltMed(parseFloat(vals.altwt)) : null;
const schedule = result ? [
{ time:"0 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue },
{ time:"3 hrs", drug:"Acetaminophen",dose:`${result.apapMg} mg (${result.apapTsp} tsp)`, color:COLORS.teal },
{ time:"6 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue },
{ time:"9 hrs", drug:"Acetaminophen",dose:`${result.apapMg} mg (${result.apapTsp} tsp)`, color:COLORS.teal },
{ time:"12 hrs", drug:"Ibuprofen", dose:`${result.ibupMg} mg (${result.ibupTsp} tsp)`, color:COLORS.blue },
] : [];
return (
{ready && result && (
<>
Sample 12-hour Schedule
{schedule.map(s=>(
))}
⚠ Ibuprofen not for <6 months or <12 lbs. Do not exceed labeled max daily doses. Give with food.
>
)}
);
}
// ─── GROWTH Z-SCORE / WEIGHT-FOR-AGE ─────────────────────────────────────────
// WHO/CDC simplified weight-for-age median and SD values (approximate, 0–18 yrs)
const WEIGHT_NORMS = {
male: [
{age:0, p3:2.5, p5:2.8, p10:3.0, p25:3.3, p50:3.5, p75:3.8, p85:4.0, p95:4.3, p97:4.5},
{age:6, p3:5.7, p5:6.1, p10:6.5, p25:7.1, p50:7.9, p75:8.7, p85:9.1, p95:9.9, p97:10.2},
{age:12, p3:7.7, p5:8.1, p10:8.7, p25:9.5, p50:10.2,p75:11.3,p85:11.8,p95:12.9,p97:13.5},
{age:24, p3:10.5, p5:11.0, p10:11.5,p25:12.4,p50:13.5,p75:14.7,p85:15.4,p95:16.8,p97:17.4},
{age:36, p3:12.5, p5:13.0, p10:13.7,p25:14.7,p50:16.0,p75:17.5,p85:18.4,p95:20.1,p97:21.0},
{age:60, p3:15.3, p5:16.0, p10:16.9,p25:18.3,p50:20.0,p75:22.3,p85:23.6,p95:26.3,p97:27.8},
{age:120,p3:24.7, p5:26.0, p10:27.7,p25:30.8,p50:34.7,p75:39.9,p85:43.5,p95:50.7,p97:54.5},
{age:180,p3:46.0, p5:49.0, p10:52.5,p25:58.5,p50:65.0,p75:74.0,p85:80.0,p95:92.0,p97:97.0},
],
female: [
{age:0, p3:2.4, p5:2.7, p10:2.9, p25:3.2, p50:3.4, p75:3.7, p85:3.9, p95:4.2, p97:4.4},
{age:6, p3:5.4, p5:5.7, p10:6.1, p25:6.7, p50:7.3, p75:8.0, p85:8.5, p95:9.3, p97:9.6},
{age:12, p3:7.1, p5:7.5, p10:8.0, p25:8.8, p50:9.5, p75:10.5,p85:11.0,p95:12.0,p97:12.5},
{age:24, p3:10.0, p5:10.5, p10:11.1,p25:12.0,p50:13.1,p75:14.4,p85:15.2,p95:16.7,p97:17.5},
{age:36, p3:12.0, p5:12.5, p10:13.2,p25:14.3,p50:15.6,p75:17.3,p85:18.3,p95:20.3,p97:21.4},
{age:60, p3:14.7, p5:15.4, p10:16.3,p25:17.8,p50:19.5,p75:22.0,p85:23.6,p95:26.8,p97:28.6},
{age:120,p3:23.5, p5:24.8, p10:26.5,p25:29.7,p50:33.7,p75:39.5,p85:43.4,p95:52.0,p97:56.6},
{age:180,p3:42.0, p5:45.0, p10:49.0,p25:55.0,p50:62.0,p75:72.0,p85:79.0,p95:93.0,p97:100.0},
]
};
function getWeightPercentile(ageMo, weightKg, sex) {
const table = WEIGHT_NORMS[sex]||WEIGHT_NORMS.male;
let row = table[0];
for(let i=1;i=table[i].age) row=table[i]; }
const p50=row.p50;
if(weightKg<=row.p3) return {pct:"< 3rd", level:"red", ftt:true};
if(weightKg<=row.p5) return {pct:"3rd–5th", level:"red", ftt:true};
if(weightKg<=row.p10) return {pct:"5th–10th", level:"yellow", ftt:true};
if(weightKg<=row.p25) return {pct:"10th–25th", level:"yellow", ftt:false};
if(weightKg<=row.p75) return {pct:"25th–75th", level:"green", ftt:false};
if(weightKg<=row.p85) return {pct:"75th–85th", level:"green", ftt:false};
if(weightKg<=row.p95) return {pct:"85th–95th", level:"yellow", ftt:false};
return {pct:"> 95th", level:"red", ftt:false, obese:true};
}
function GrowthZ({ vals, set }) {
const ageMo = parseFloat(vals.gage)||0;
const weightLbs = parseFloat(vals.gwt)||0;
const weightKg = lbsToKg(weightLbs);
const ready = ageMo>0 && weightLbs>0 && ageMo<=216;
let result=null;
if(ready){
const r = getWeightPercentile(ageMo, weightKg, vals.gsex||"male");
const fttText = r.ftt ? "⚠ Weight-for-age below 10th percentile. Evaluate for Failure to Thrive (FTT). Consider dietary history, growth velocity, and referral." : "";
const obText = r.obese ? "⚠ Weight-for-age > 95th percentile. Obesity risk. Evaluate diet, activity, family history." : "";
result = { ...r, action: fttText||obText||"Weight within expected range for age. Continue routine monitoring.", risk:`${r.pct} percentile` };
}
return (
{ready && result && (
<>
>
)}
);
}
// ─── DEHYDRATION SEVERITY (GORELICK) ─────────────────────────────────────────
const GORELICK_ITEMS = [
{ id:"caprefill", label:"Capillary refill time", options:[{v:0,l:"< 2 seconds (normal)"},{v:1,l:"2–3 seconds"},{v:2,l:"> 3 seconds"}] },
{ id:"turgor", label:"Skin turgor", options:[{v:0,l:"Normal (immediate recoil)"},{v:1,l:"Reduced (slow recoil)"}] },
{ id:"mucous", label:"Mucous membranes", options:[{v:0,l:"Moist"},{v:1,l:"Dry/sticky"}] },
{ id:"eyes", label:"Eyes", options:[{v:0,l:"Normal"},{v:1,l:"Sunken"}] },
{ id:"tears", label:"Tears", options:[{v:0,l:"Present"},{v:1,l:"Decreased/absent"}] },
{ id:"general", label:"General appearance", options:[{v:0,l:"Normal"},{v:1,l:"Ill-appearing"}] },
{ id:"hr", label:"Heart rate", options:[{v:0,l:"Normal for age"},{v:1,l:"Mildly elevated"},{v:2,l:"Markedly elevated"}] },
{ id:"breathing", label:"Breathing pattern", options:[{v:0,l:"Normal"},{v:1,l:"Deep/rapid"}] },
{ id:"bp", label:"Blood pressure", options:[{v:0,l:"Normal"},{v:1,l:"Hypotensive/orthostatic"}] },
{ id:"urine", label:"Urine output", options:[{v:0,l:"Normal"},{v:1,l:"Decreased"}] },
];
function Dehydration({ vals, set }) {
const score = GORELICK_ITEMS.reduce((s,item)=>{
const v=vals["deh_"+item.id]; return s+(v!==undefined?parseInt(v):0);
},0);
const allSet = GORELICK_ITEMS.every(item=>vals["deh_"+item.id]!==undefined);
let result=null;
if(allSet){
if(score<=1) result={level:"green",risk:"Minimal / No Dehydration",action:"< 3% fluid deficit. Encourage oral fluids. Continue breastfeeding/formula. No ORS required."};
else if(score<=5) result={level:"yellow",risk:"Mild–Moderate Dehydration (3–9%)",action:"ORS 50–100 mL/kg over 2–4 hrs. Small frequent sips. Replace ongoing losses (10 mL/kg per vomit/stool). Reassess frequently."};
else result={level:"red",risk:"Severe Dehydration (> 9%)",action:"IV fluid resuscitation. NS 20 mL/kg bolus; repeat as needed. Hospital admission. Monitor electrolytes."};
}
return (
{GORELICK_ITEMS.map(item=>(
{item.label}
{item.options.map(opt=>{
const k="deh_"+item.id; const sel=vals[k]===String(opt.v)||vals[k]===opt.v;
return (
set(k,opt.v)}
style={{ padding:"8px 12px", borderRadius:8, border:`1.5px solid ${sel?COLORS.teal:COLORS.border}`, background:sel?COLORS.teal:COLORS.white, color:sel?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontSize:13, cursor:"pointer", textAlign:"left", display:"flex", justifyContent:"space-between" }}>
{opt.l}
{opt.v}
);
})}
))}
Severity Score
{allSet?score:"—"}
{result && }
);
}
// ══════════════════════════════════════════════════════════════════════════════
// SCORING TAB (unchanged from before)
// ══════════════════════════════════════════════════════════════════════════════
const SCORES = {
centor: {
name:"Centor / McIsaac Score", subtitle:"Strep Pharyngitis Risk",
description:"Estimates probability of Group A Strep in pharyngitis.",
criteria:[
{id:"exudate",label:"Tonsillar exudate or swelling",points:1},
{id:"nodes",label:"Tender anterior cervical lymphadenopathy",points:1},
{id:"fever",label:"Fever history (> 100.4°F)",points:1},
{id:"nocough",label:"Absence of cough",points:1},
{id:"age3_14",label:"Age 3–14 years",points:1},
{id:"age15_44",label:"Age 15–44 years",points:0},
{id:"age45plus",label:"Age ≥ 45 years",points:-1},
],
ageOptions:["age3_14","age15_44","age45plus"],
interpret:(score)=>{
if(score<=0) return {risk:"Low",action:"No testing or antibiotics needed.",level:"green"};
if(score===1) return {risk:"Low (5–10%)",action:"No testing or antibiotics recommended.",level:"green"};
if(score===2) return {risk:"Moderate (11–17%)",action:"Consider rapid strep test. Treat if positive.",level:"yellow"};
if(score===3) return {risk:"Moderate-High (28–35%)",action:"Rapid strep test recommended. Treat if positive.",level:"yellow"};
return {risk:"High (51–53%)",action:"Consider empiric antibiotics or confirm with rapid test.",level:"red"};
},
},
westley: {
name:"Westley Croup Score", subtitle:"Croup Severity",
description:"Assesses severity of croup (laryngotracheobronchitis).",
criteria:[
{id:"stridor0",label:"Stridor: None",points:0},{id:"stridor1",label:"Stridor: With agitation",points:1},{id:"stridor2",label:"Stridor: At rest",points:2},
{id:"retractions0",label:"Retractions: None",points:0},{id:"retractions1",label:"Retractions: Mild",points:1},{id:"retractions2",label:"Retractions: Moderate",points:2},{id:"retractions3",label:"Retractions: Severe",points:3},
{id:"airentry0",label:"Air Entry: Normal",points:0},{id:"airentry1",label:"Air Entry: Decreased",points:1},{id:"airentry2",label:"Air Entry: Markedly decreased",points:2},
{id:"cyan0",label:"Cyanosis: None",points:0},{id:"cyan4",label:"Cyanosis: With agitation",points:4},{id:"cyan5",label:"Cyanosis: At rest",points:5},
{id:"consc0",label:"Consciousness: Normal",points:0},{id:"consc5",label:"Consciousness: Altered",points:5},
],
groups:[
{label:"Stridor",ids:["stridor0","stridor1","stridor2"]},
{label:"Retractions",ids:["retractions0","retractions1","retractions2","retractions3"]},
{label:"Air Entry",ids:["airentry0","airentry1","airentry2"]},
{label:"Cyanosis",ids:["cyan0","cyan4","cyan5"]},
{label:"Level of Consciousness",ids:["consc0","consc5"]},
],
interpret:(score)=>{
if(score<=2) return {risk:"Mild",action:"Outpatient care. Dexamethasone 0.15mg/kg PO. Supportive care.",level:"green"};
if(score<=5) return {risk:"Moderate",action:"Dexamethasone 0.6mg/kg IM/PO. Consider nebulized epinephrine. Monitor 2–4 hrs.",level:"yellow"};
if(score<=11) return {risk:"Severe",action:"Nebulized epinephrine + Dexamethasone. Hospital admission likely.",level:"red"};
return {risk:"Impending respiratory failure",action:"Immediate airway management. ICU admission.",level:"red"};
},
},
pecarn: {
name:"PECARN Head Injury", subtitle:"CT Head Decision Rule (< 2 years)",
description:"Identifies low-risk children < 2 years old after head trauma.",
criteria:[
{id:"gcs14",label:"GCS < 15",points:2},
{id:"ams",label:"Altered mental status (agitation, somnolence, repetitive questions, slow response)",points:2},
{id:"palp_skull",label:"Palpable skull fracture",points:2},
{id:"occipital",label:"Occipital / parietal / temporal scalp hematoma",points:1},
{id:"loc",label:"Loss of consciousness ≥ 5 seconds",points:1},
{id:"severe_mech",label:"Severe injury mechanism (MVA, fall > 3 ft, struck by high-impact object)",points:1},
{id:"not_acting",label:"Not acting normally per parent",points:1},
],
interpret:(score)=>{
if(score>=4) return {risk:"High",action:"CT head recommended.",level:"red"};
if(score>=1) return {risk:"Intermediate",action:"CT vs. observation based on clinical judgment. Discuss with caregiver.",level:"yellow"};
return {risk:"Low (< 0.02% ciTBI)",action:"CT not required. Observation acceptable. Discharge with return precautions.",level:"green"};
},
},
};
function ScoringTab() {
const [activeScore, setActiveScore] = useState("centor");
const [checked, setChecked] = useState({});
const score = SCORES[activeScore];
const ageOptions = score.ageOptions||[];
const toggle=(id)=>{
if(ageOptions.includes(id)){
setChecked(prev=>{ const next={...prev}; ageOptions.forEach(a=>delete next[a]); if(!prev[id]) next[id]=true; return next; });
} else { setChecked(prev=>({...prev,[id]:!prev[id]})); }
};
const total = score.criteria.reduce((s,c)=>checked[c.id]?s+c.points:s,0);
const result = score.interpret(total);
const renderCriteria=()=>{
if(score.groups) return score.groups.map(g=>(
{g.label}
{g.ids.map(id=>{ const c=score.criteria.find(x=>x.id===id); return
toggle(id)} radio={true} />; })}
));
return score.criteria.map(c=>toggle(c.id)} radio={ageOptions.includes(c.id)} />);
};
return (
{Object.entries(SCORES).map(([key,s])=>(
{setActiveScore(key);setChecked({});}}
style={{ padding:"8px 14px", borderRadius:8, border:`1.5px solid ${activeScore===key?COLORS.teal:COLORS.border}`, background:activeScore===key?COLORS.teal:COLORS.white, color:activeScore===key?COLORS.white:COLORS.slate, fontFamily:"'DM Sans',sans-serif", fontWeight:600, fontSize:13, cursor:"pointer" }}>
{s.name.split(" ")[0]}
))}
{score.name}
{score.description}
{renderCriteria()}
);
}
function CheckRow({ criterion, checked, onChange, radio }) {
return (
{criterion.label}
0?COLORS.teal:criterion.points<0?COLORS.red:COLORS.muted, fontSize:14, minWidth:28, textAlign:"right" }}>
{criterion.points>0?`+${criterion.points}`:criterion.points}
);
}
// ══════════════════════════════════════════════════════════════════════════════
// GENERAL TAB
// ══════════════════════════════════════════════════════════════════════════════
const GENERAL_MODULES = [
{key:"bmi", label:"BMI"},
{key:"bsa", label:"BSA"},
{key:"ibw", label:"IBW"},
{key:"crcl", label:"CrCl"},
{key:"fluid", label:"Fluids"},
];
function calcBMI(weightLbs, htFt, htIn) {
const totalInches = parseFloat(htFt)*12+parseFloat(htIn||0);
return ((weightLbs/(totalInches*totalInches))*703).toFixed(1);
}
function bmiCategory(bmi) {
if(bmi<18.5) return {label:"Underweight",level:"yellow"};
if(bmi<25) return {label:"Normal",level:"green"};
if(bmi<30) return {label:"Overweight",level:"yellow"};
return {label:"Obese",level:"red"};
}
function calcBSA(weightLbs, htFt, htIn) {
const wt=lbsToKg(weightLbs); const ht=feetInchesToCm(htFt,htIn);
return Math.sqrt((wt*ht)/3600).toFixed(2);
}
function calcIBW(htFt, htIn, sex) {
const totalIn=parseFloat(htFt||0)*12+parseFloat(htIn||0);
const base=sex==="male"?50:45.5;
const ibwKg=Math.max(base+2.3*(totalIn-60),0);
return {kg:ibwKg.toFixed(1), lbs:kgToLbs(ibwKg).toFixed(1)};
}
function calcCrCl(age, weightLbs, creatinine, sex) {
const wt=lbsToKg(weightLbs);
const base=((140-age)*wt)/(72*creatinine);
return sex==="female"?(base*0.85).toFixed(1):base.toFixed(1);
}
function calcMaintFluid(weightLbs) {
const wt=lbsToKg(weightLbs);
let ml=0;
if(wt<=10) ml=wt*100;
else if(wt<=20) ml=1000+(wt-10)*50;
else ml=1500+(wt-20)*20;
return {ml:Math.round(ml), oz:(ml/29.574).toFixed(1), rate:(ml/24).toFixed(1)};
}
function GeneralTab() {
const [active, setActive] = useState("bmi");
const [vals, setVals] = useState({});
const set=(k,v)=>setVals(p=>({...p,[k]:v}));
const renderCalc=()=>{
switch(active){
case "bmi": {
const bmi=vals.wt&&vals.htft?calcBMI(parseFloat(vals.wt),vals.htft,vals.htin||0):null;
const cat=bmi?bmiCategory(parseFloat(bmi)):null;
return {bmi&& } ;
}
case "bsa": {
const bsa=vals.wt2&&vals.htft2?calcBSA(parseFloat(vals.wt2),vals.htft2,vals.htin2||0):null;
return {bsa&& } ;
}
case "ibw": {
const ibw=vals.htft3?calcIBW(vals.htft3,vals.htin3||0,vals.sex||"male"):null;
return {ibw&&} ;
}
case "crcl": {
const crcl=vals.age4&&vals.wt4&&vals.cr?calcCrCl(parseFloat(vals.age4),parseFloat(vals.wt4),parseFloat(vals.cr),vals.sex4||"male"):null;
return {crcl&& } ;
}
case "fluid": {
const result=vals.wt5?calcMaintFluid(parseFloat(vals.wt5)):null;
return {result&&} ;
}
default: return null;
}
};
return (
{setActive(k);setVals({});}} />
{renderCalc()}
);
}
// ══════════════════════════════════════════════════════════════════════════════
// APP ROOT
// ══════════════════════════════════════════════════════════════════════════════
export default function App() {
const [tab, setTab] = useState("Dosing");
return (
⚕
ClinCalc
Point-of-Care Medical Calculator
{TABS.map(t=>(
setTab(t)}
style={{ flex:1, padding:"10px 0", background:"none", border:"none", borderBottom:`3px solid ${tab===t?COLORS.tealMid:"transparent"}`, color:tab===t?COLORS.white:"#94A3B8", fontFamily:"'DM Sans',sans-serif", fontWeight:700, fontSize:14, cursor:"pointer", transition:"all 0.15s" }}>
{t}
))}
{tab==="Dosing" &&
}
{tab==="Peds" &&
}
{tab==="Scoring" &&
}
{tab==="General" &&
}
For clinical decision support only. Always verify dosing against current references and use clinical judgment. Not a substitute for professional medical advice.
);
}