feat(client): port Bedside sub-nav shell (15 pills linked to legacy)
First Bedside commit. Delivers the /app/bedside page with the full 15-pill sub-nav in the same order + labels as the vanilla app, and switches the sidebar Bedside link to available. client/src/pages/Bedside.tsx Single-file shell. PILLS array is the single source of truth for pill ID / label / icon / summary, ordered to match public/components/bedside.html (neonatal, airway, cardiac, respiratory, ventilation, seizure, sepsis, anaphylaxis, sedation, agitation, antiemetics, antimicrobials, burns, toxicology, trauma). Clicking a pill flips useState<active>, and LegacyPanel renders a summary of that module + a button to open the legacy Bedside tab. What is intentionally NOT in this commit Each pill's actual clinical dosing panel stays in vanilla for now. Those panels encode weight-based dosing, syndrome-keyed antimicrobials, and PALS / ALS formulas — exactly the class of content the migration checkpoint memory flags as must-not-be- "simplified" by an LLM. They belong in per-module commits that land alongside the calculators port (APLS + Best Guess weight, Fenton 2013 LMS, AAP 2022 bilirubin, Rosner BP splines) where test vectors can verify byte-for-byte parity with the vanilla output. The top-level age → weight estimator also waits on calculators — it calls window._PED_MATH.estimateWeightFromAgeMonths in the vanilla module, which is defined in public/js/calculators.js. e2e/tests/bedside-react.spec.js — three smoke tests All 15 pills render by data-testid, clicking a pill swaps the panel, and the legacy-viewer link is present. The pill-order list is hard-coded in the spec so re-ordering or dropping a pill trips a loud failure. Client tsc -b + vite build clean. Bundle 456.71 kB / 130.53 kB gz.
This commit is contained in:
parent
bdec116fa6
commit
43e8611b24
8 changed files with 161 additions and 7 deletions
|
|
@ -15,6 +15,7 @@ import Catchup from '@/pages/Catchup';
|
|||
import Settings from '@/pages/Settings';
|
||||
import Learning from '@/pages/Learning';
|
||||
import PeGuide from '@/pages/PeGuide';
|
||||
import Bedside from '@/pages/Bedside';
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: 30_000, retry: 1 } },
|
||||
|
|
@ -61,6 +62,7 @@ export default function App() {
|
|||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/learning" element={<Learning />} />
|
||||
<Route path="/peguide" element={<PeGuide />} />
|
||||
<Route path="/bedside" element={<Bedside />} />
|
||||
<Route path="/faq" element={<Faq />} />
|
||||
{/* catch-all falls back to home while more tabs port over */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ const NAV: NavGroup[] = [
|
|||
{ to: '/vaxschedule', label: 'Vaccine Schedule', available: true },
|
||||
{ to: '/catchup', label: 'Catch-Up Schedule', available: true },
|
||||
{ to: '/peguide', label: 'Physical Exam Guide', available: true },
|
||||
{ to: '/bedside', label: 'Bedside', available: false },
|
||||
{ to: '/bedside', label: 'Bedside', available: true },
|
||||
{ to: '/calculators', label: 'Calculators', available: false },
|
||||
{ to: '/extensions', label: 'Pagers & Extensions', available: true },
|
||||
{ to: '/learning', label: 'Learning Hub', available: true },
|
||||
|
|
|
|||
109
client/src/pages/Bedside.tsx
Normal file
109
client/src/pages/Bedside.tsx
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// ============================================================
|
||||
// BEDSIDE — emergency + rapid-reference pediatric tools. This first
|
||||
// port ships the sub-nav shell at /app/bedside so the sidebar links
|
||||
// light up and users land on a page that mirrors the legacy layout.
|
||||
//
|
||||
// The 15 clinical sub-modules (neonatal, airway, cardiac, respiratory,
|
||||
// ventilation, seizures, sepsis, anaphylaxis, sedation, agitation,
|
||||
// antiemetics, antimicrobials, burns, toxicology, trauma) stay in the
|
||||
// vanilla viewer for now. Each one carries weight-based dosing +
|
||||
// clinical decision content the migration checkpoint explicitly
|
||||
// flagged as must-not-be-"simplified" by an LLM — they belong in
|
||||
// dedicated per-module commits alongside the calculators port (Rosner
|
||||
// BP splines, Fenton LMS, AAP 2022 bilirubin, APLS weights) where
|
||||
// test vectors can verify byte-for-byte parity.
|
||||
//
|
||||
// The top-level age → weight estimator (APLS / Best Guess formulas)
|
||||
// also depends on public/js/calculators.js which has not been ported
|
||||
// yet. It will appear at the top of this page when calculators port.
|
||||
// ============================================================
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
const card = 'rounded-lg border border-border bg-card p-5 space-y-3';
|
||||
const btnPrimary = 'rounded-md bg-primary text-primary-foreground px-4 py-2 text-sm font-medium';
|
||||
|
||||
interface Pill {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string; // emoji stand-in; font-awesome lives in the legacy shell
|
||||
summary: string;
|
||||
}
|
||||
|
||||
// Order and labels match public/components/bedside.html exactly.
|
||||
const PILLS: Pill[] = [
|
||||
{ id: 'neonatal', label: 'Neonatal', icon: '👶', summary: 'GA classification, AGA/SGA/LGA, prematurity category (Fenton 2013 / WHO).' },
|
||||
{ id: 'airway', label: 'Airway / RSI', icon: '💨', summary: 'ETT size + depth, RSI induction + paralytic dosing by weight.' },
|
||||
{ id: 'cardiac', label: 'Cardiac Arrest', icon: '❤️', summary: 'PALS dosing (epinephrine, amiodarone, lidocaine), defibrillation J/kg.' },
|
||||
{ id: 'respiratory', label: 'Respiratory', icon: '🫁', summary: 'Asthma, bronchiolitis, croup severity + dosing.' },
|
||||
{ id: 'ventilation', label: 'O₂ & Ventilation', icon: '🌀', summary: 'NC / HFNC / CPAP / BiPAP flow + FiO₂ targets by age.' },
|
||||
{ id: 'seizure', label: 'Seizures', icon: '🧠', summary: 'Benzodiazepine + second/third-line weight-based dosing.' },
|
||||
{ id: 'sepsis', label: 'Sepsis & Fever', icon: '🦠', summary: 'Empirical antibiotics + fluid bolus dosing by weight.' },
|
||||
{ id: 'anaphylaxis', label: 'Anaphylaxis', icon: '💉', summary: 'Epinephrine IM, IV infusion, steroid + antihistamine dosing.' },
|
||||
{ id: 'sedation', label: 'Sedation', icon: '🛌', summary: 'Procedural sedation regimens — ketamine, propofol, midazolam.' },
|
||||
{ id: 'agitation', label: 'Agitation', icon: '😤', summary: 'Weight-based haloperidol, olanzapine, lorazepam.' },
|
||||
{ id: 'antiemetics', label: 'Antiemetics', icon: '💊', summary: 'Ondansetron, metoclopramide, promethazine dosing.' },
|
||||
{ id: 'antimicrobials', label: 'Antimicrobials', icon: '🧫', summary: 'Common empirical regimens keyed to syndrome + weight.' },
|
||||
{ id: 'burns', label: 'Burns', icon: '🔥', summary: 'TBSA % (Lund-Browder, Rule of Nines-children), Parkland fluids.' },
|
||||
{ id: 'toxicology', label: 'Toxicology', icon: '☠️', summary: 'Common toxidromes + antidotes + decontamination windows.' },
|
||||
{ id: 'trauma', label: 'Trauma', icon: '🩹', summary: 'PECARN, c-spine, blood-product dosing, TXA.' },
|
||||
];
|
||||
|
||||
function LegacyPanel({ pill }: { pill: Pill }) {
|
||||
return (
|
||||
<section className={card} data-testid={'bedside-panel-' + pill.id}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl" aria-hidden>{pill.icon}</span>
|
||||
<h2 className="text-lg font-semibold">{pill.label}</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{pill.summary}</p>
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 dark:bg-amber-950/30 p-3 text-sm space-y-2">
|
||||
<p className="text-amber-900 dark:text-amber-100">
|
||||
Weight-based calculators for this module run in the legacy viewer while the clinical data is
|
||||
verified for a direct React port. Open the legacy Bedside tab to use the full dosing flow.
|
||||
</p>
|
||||
</div>
|
||||
<a href="/#bedside" className={btnPrimary + ' inline-block'}>
|
||||
Open in legacy viewer
|
||||
</a>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Bedside() {
|
||||
const [active, setActive] = useState<string>(PILLS[0].id);
|
||||
const pill = PILLS.find((p) => p.id === active) ?? PILLS[0];
|
||||
|
||||
return (
|
||||
<div className="max-w-5xl mx-auto p-6 space-y-4">
|
||||
<header>
|
||||
<h1 className="text-2xl font-semibold">Bedside</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Emergency and rapid-reference pediatric tools. Weight-based dosing throughout — always verify against institutional protocols.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="flex flex-wrap gap-2" data-testid="bedside-subnav">
|
||||
{PILLS.map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
onClick={() => setActive(p.id)}
|
||||
className={
|
||||
'px-3 py-1.5 rounded-full text-xs font-medium border transition-colors ' +
|
||||
(active === p.id
|
||||
? 'bg-primary text-primary-foreground border-primary'
|
||||
: 'bg-muted hover:bg-muted/80 border-border')
|
||||
}
|
||||
data-testid={'bedside-pill-' + p.id}
|
||||
>
|
||||
<span className="mr-1" aria-hidden>{p.icon}</span>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<LegacyPanel pill={pill} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
e2e/tests/bedside-react.spec.js
Normal file
43
e2e/tests/bedside-react.spec.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// ============================================================
|
||||
// BEDSIDE (React port) — sub-nav shell smoke test.
|
||||
//
|
||||
// First-commit scope: the 15 sub-pills render in the expected order
|
||||
// and selecting a pill reveals a panel with a legacy-viewer link.
|
||||
// Actual clinical dosing panels (neonatal through trauma) port in
|
||||
// dedicated follow-up commits alongside the calculators.
|
||||
// ============================================================
|
||||
|
||||
const { test, expect, E2E_BASE } = require('../fixtures');
|
||||
|
||||
const EXPECTED_PILLS = [
|
||||
'neonatal', 'airway', 'cardiac', 'respiratory', 'ventilation',
|
||||
'seizure', 'sepsis', 'anaphylaxis', 'sedation', 'agitation',
|
||||
'antiemetics', 'antimicrobials', 'burns', 'toxicology', 'trauma',
|
||||
];
|
||||
|
||||
async function openReactBedside(page) {
|
||||
await page.goto(E2E_BASE + '/app/bedside');
|
||||
await page.waitForSelector('[data-testid="bedside-subnav"]', { timeout: 15000 });
|
||||
}
|
||||
|
||||
test.describe('React Bedside — sub-nav shell', () => {
|
||||
|
||||
test('all 15 sub-pills render in the expected order', async ({ authedPage: _, page }) => {
|
||||
await openReactBedside(page);
|
||||
for (const id of EXPECTED_PILLS) {
|
||||
await expect(page.locator('[data-testid="bedside-pill-' + id + '"]')).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test('clicking a pill switches the active panel', async ({ authedPage: _, page }) => {
|
||||
await openReactBedside(page);
|
||||
await expect(page.locator('[data-testid="bedside-panel-neonatal"]')).toBeVisible();
|
||||
await page.click('[data-testid="bedside-pill-airway"]');
|
||||
await expect(page.locator('[data-testid="bedside-panel-airway"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test('panel carries a legacy-viewer link', async ({ authedPage: _, page }) => {
|
||||
await openReactBedside(page);
|
||||
await expect(page.getByText('Open in legacy viewer').first()).toBeVisible();
|
||||
});
|
||||
});
|
||||
2
public/app/assets/index-0qzAvrmR.css
Normal file
2
public/app/assets/index-0qzAvrmR.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/app/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>client</title>
|
||||
<script type="module" crossorigin src="/app/assets/index-qWcH2fjq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/app/assets/index-DZzmyejq.css">
|
||||
<script type="module" crossorigin src="/app/assets/index-DrQO3Lq1.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/app/assets/index-0qzAvrmR.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue