Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/components/clinical-dashboard/differentials-home.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -824,10 +824,10 @@ function SearchResultsView({
/>
</div>

<div className="fixed inset-x-3 bottom-[calc(8.5rem+env(safe-area-inset-bottom))] z-30 lg:hidden">
<div className="fixed inset-x-0 bottom-[calc(8.5rem+env(safe-area-inset-bottom))] z-30 border-t border-[color:var(--border)] bg-[color:var(--background)] px-3 py-2.5 lg:hidden">
<Link
href={routeWithQuery("/differentials/presentations", query)}
className="mx-auto flex min-h-14 max-w-[26rem] items-center justify-center gap-3 rounded-xl bg-[color:var(--clinical-accent)] px-4 text-sm font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-elevated)]"
className="mx-auto flex min-h-12 max-w-[26rem] items-center justify-center gap-3 rounded-xl bg-[color:var(--clinical-accent)] px-4 text-sm font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-elevated)]"
>
<GitCompareArrows className="h-5 w-5" aria-hidden />
Compare selected ({selectedCount})
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -321,11 +321,6 @@ function GlobalMockupSearchShellClient({
(desktopSearchPlacement === "hero" || isFormsOnlyShell) && isStandaloneModeHome ? "hero" : "default"
}
searchComposerVisible={shouldShowSearchComposer}
workflowCopyText={
isDifferentialPresentationWorkflow
? "Acute confusion / encephalopathy differential comparison. Stabilise ABCs, check BGL, sats, attention test, collateral, and review medications/substances before handoff."
: undefined
}
desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
heroComposerFromTablet={isStandaloneModeHome}
/>
Expand Down
29 changes: 0 additions & 29 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,8 +19,6 @@ import {
Check,
CheckCircle2,
ChevronDown,
Cloud,
Copy,
FileText,
Filter,
Globe2,
Expand All@@ -34,7 +32,6 @@ import {
Send,
ShieldCheck,
Sparkles,
AlertCircle,
ArrowLeft,
X,
Lock,
Expand DownExpand Up@@ -199,7 +196,6 @@ export function MasterSearchHeader({
mobileSearchPlacement = "default",
desktopSearchPlacement = "default",
searchComposerVisible = true,
workflowCopyText,
desktopHomeComposerSlotId,
heroComposerFromTablet = false,
mobileLeadingAction = "menu",
Expand DownExpand Up@@ -237,7 +233,6 @@ export function MasterSearchHeader({
mobileSearchPlacement?: "default" | "bottom";
desktopSearchPlacement?: "default" | "hero";
searchComposerVisible?: boolean;
workflowCopyText?: string;
desktopHomeComposerSlotId?: string;
/** Portal the composer into the hero slot from the tablet breakpoint (sm) up,
* rather than the default desktop (lg) breakpoint. */
Expand DownExpand Up@@ -1229,20 +1224,6 @@ export function MasterSearchHeader({
<div className="relative flex min-w-0 shrink-0 items-center justify-end gap-1.5 justify-self-end sm:gap-2">
{isWorkflowHeader ? (
<>
<div className="hidden min-w-0 items-center gap-2 xl:flex">
<span className="inline-flex min-h-10 items-center gap-2 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--surface)] px-3 text-xs font-extrabold text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]">
<CheckCircle2 className="h-4 w-4" aria-hidden />
Local only
</span>
<span className="inline-flex min-h-10 items-center gap-2 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-xs font-extrabold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)]">
<Cloud className="h-4 w-4 text-[color:var(--text-muted)]" aria-hidden />
Offline ready
</span>
<span className="inline-flex min-h-10 items-center gap-2 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)]/45 px-3 text-xs font-extrabold text-[color:var(--warning)] shadow-[var(--shadow-inset)]">
<AlertCircle className="h-4 w-4" aria-hidden />
Source pending review
</span>
</div>
<button
type="button"
className="universal-header-icon-control grid h-11 w-11 shrink-0 place-items-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
Expand All@@ -1261,16 +1242,6 @@ export function MasterSearchHeader({
>
<Plus className="h-5 w-5" aria-hidden />
</button>
<button
type="button"
onClick={() => {
if (workflowCopyText) void navigator.clipboard?.writeText(workflowCopyText);
}}
className="hidden min-h-11 items-center gap-2 rounded-lg bg-[color:var(--command)] px-4 text-sm font-extrabold text-[color:var(--command-contrast)] shadow-[var(--shadow-tight)] transition hover:bg-[color:var(--command-hover)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] xl:inline-flex"
>
<Copy className="h-4 w-4" aria-hidden />
Copy after review
</button>
</>
) : null}
{!isWorkflowHeader ? (
Expand Down
6 changes: 0 additions & 6 deletions src/components/differentials/differential-detail-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -402,12 +402,6 @@ function HeaderChrome() {
</Link>
</div>
<div className="flex items-center gap-2">
<span className="hidden rounded-lg border border-[color:var(--success)]/20 bg-[color:var(--success-soft)] px-3 py-2 text-xs font-bold text-[color:var(--success)] sm:inline-flex">
Local only
</span>
<span className="hidden rounded-lg border border-[color:var(--warning)]/20 bg-[color:var(--warning-soft)] px-3 py-2 text-xs font-bold text-[color:var(--warning)] sm:inline-flex">
Source pending review
</span>
<button
className="grid h-10 w-10 place-items-center rounded-lg text-[color:var(--text-heading)] hover:bg-[color:var(--surface-subtle)]"
aria-label="New item"
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -180,7 +180,7 @@ function CandidateHeader({ candidate }: { candidate: CandidateView }) {
function DesktopComparisonTable({ candidates }: { candidates: CandidateView[] }) {
const workflow = acuteConfusionPresentationWorkflow;
return (
<section className="hidden xl:block" aria-label="Desktop differential comparison table">
<section className="hidden md:block" aria-label="Differential comparison table">
<div className="mb-2 flex flex-wrap items-center justify-between gap-3">
<div className="flex flex-wrap items-center gap-3">
<button
Expand DownExpand Up@@ -381,6 +381,34 @@ function ReviewPanel() {
);
}

function CopyAfterReviewPanel({ text }: { text: string }) {
return (
<section className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-[var(--shadow-inset)]">
<h2 className="text-sm font-extrabold uppercase text-[color:var(--text-muted)]">Copy after review</h2>
<p className="mt-2 text-sm font-medium leading-6 text-[color:var(--text-muted)]">
Create a concise comparison summary for documentation or handoff.
</p>
<CopyAfterReviewButton text={text} className="mt-3 w-full" />
</section>
);
}

/** The review/handoff panels shared by the desktop side rail (stacked) and the
* tablet reflow grid below the comparison table. Safety snapshot is rendered
* separately so it can lead each layout. */
function ReviewPanels({ candidates }: { candidates: CandidateView[] }) {
const selectedCandidates = candidates.filter((candidate) => candidate.selected);
return (
<>
<SelectedDifferentialsPanel candidates={candidates} />
<HighestUrgencyPanel candidates={candidates} />
<ReviewPanel />
<CopyAfterReviewPanel text={comparisonCopy(selectedCandidates)} />
<SourceStatusPanel />
</>
);
}

function SourceStatusPanel() {
const status = acuteConfusionPresentationWorkflow.sourceStatus;
return (
Expand DownExpand Up@@ -454,7 +482,7 @@ function MobileCandidateCard({ candidate, index }: { candidate: CandidateView; i
function MobileComparison({ candidates }: { candidates: CandidateView[] }) {
const selected = candidates.filter((candidate) => candidate.selected);
return (
<section className="grid gap-3 xl:hidden" aria-label="Mobile differential comparison">
<section className="grid gap-3 md:hidden" aria-label="Mobile differential comparison">
<div className="grid grid-cols-[minmax(0,1fr)_3rem] gap-2">
<button
type="button"
Expand All@@ -481,17 +509,10 @@ function MobileComparison({ candidates }: { candidates: CandidateView[] }) {
<MobileCandidateCard key={candidate.record.slug} candidate={candidate} index={index} />
))}
</div>
<div className="fixed inset-x-0 bottom-0 z-30 grid grid-cols-2 gap-2 rounded-t-xl border-t border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent)] p-1.5 pb-[calc(0.4rem+env(safe-area-inset-bottom))] shadow-[var(--shadow-elevated)]">
<Link
href="/differentials/presentations"
className="inline-flex min-h-11 items-center justify-center gap-2 rounded-lg border border-[color:var(--clinical-accent-contrast)]/40 bg-[color:var(--clinical-accent-contrast)]/5 px-2 text-xs font-extrabold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-inset)] sm:text-sm"
>
<GitCompareArrows className="h-4 w-4" aria-hidden />
Compare ({acuteConfusionPresentationWorkflow.selectedCount} selected)
</Link>
<div className="fixed inset-x-0 bottom-0 z-30 rounded-t-xl border-t border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent)] p-1.5 pb-[calc(0.4rem+env(safe-area-inset-bottom))] shadow-[var(--shadow-elevated)]">
<CopyAfterReviewButton
text={comparisonCopy(candidates)}
className="min-h-11 bg-[color:var(--surface)] px-2 !text-xs !text-[color:var(--clinical-accent)] hover:bg-[color:var(--surface-raised)] sm:!text-sm"
className="w-full min-h-11 bg-[color:var(--surface)] px-2 !text-sm !text-[color:var(--clinical-accent)] hover:bg-[color:var(--surface-raised)]"
/>
</div>
</section>
Expand DownExpand Up@@ -529,12 +550,11 @@ function MobileTabs() {
export function DifferentialPresentationWorkflowPage({ query = "" }: { query?: string }) {
const workflow = acuteConfusionPresentationWorkflow;
const candidates = getCandidates();
const selectedCandidates = candidates.filter((candidate) => candidate.selected);

return (
<main
data-testid="differential-presentation-page"
className="min-h-[calc(100dvh-4rem)] overflow-x-hidden bg-[color:var(--background)] px-3 pb-[calc(6.25rem+env(safe-area-inset-bottom))] pt-4 text-[color:var(--text)] sm:px-5 xl:px-7 xl:pb-8 xl:pt-6"
className="min-h-[calc(100dvh-4rem)] overflow-x-hidden bg-[color:var(--background)] px-3 pb-[calc(6.25rem+env(safe-area-inset-bottom))] pt-4 text-[color:var(--text)] sm:px-5 md:pb-8 xl:px-7 xl:pt-6"
>
<div className="mx-auto grid w-full max-w-[94rem] gap-5 xl:grid-cols-[minmax(0,1fr)_23.5rem]">
<div className="min-w-0">
Expand DownExpand Up@@ -605,23 +625,21 @@ export function DifferentialPresentationWorkflowPage({ query = "" }: { query?: s
</section>

<MobileTabs />
{/* Tablet / mid (md–lg): safety leads, then the scrollable table, then
the review panels reflow into a grid below — no fixed side rail. */}
<div className="mb-4 hidden md:block xl:hidden">
<SafetySnapshot />
</div>
<DesktopComparisonTable candidates={candidates} />
<MobileComparison candidates={candidates} />
<div className="mt-4 hidden items-start gap-4 md:grid md:grid-cols-2 lg:grid-cols-3 xl:hidden">
<ReviewPanels candidates={candidates} />
</div>
</div>

<aside className="hidden min-w-0 gap-4 xl:grid" aria-label="Differential review sidebar">
<SafetySnapshot />
<SelectedDifferentialsPanel candidates={candidates} />
<HighestUrgencyPanel candidates={candidates} />
<ReviewPanel />
<section className="rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] p-4 shadow-[var(--shadow-inset)]">
<h2 className="text-sm font-extrabold uppercase text-[color:var(--text-muted)]">Copy after review</h2>
<p className="mt-2 text-sm font-medium leading-6 text-[color:var(--text-muted)]">
Create a concise comparison summary for documentation or handoff.
</p>
<CopyAfterReviewButton text={comparisonCopy(selectedCandidates)} className="mt-3 w-full" />
</section>
<SourceStatusPanel />
<ReviewPanels candidates={candidates} />
</aside>
</div>

Expand Down
22 changes: 17 additions & 5 deletions tests/ui-tools.spec.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -550,15 +550,19 @@ test.describe("Clinical KB applications launcher", () => {
await expect(page.getByRole("button", { name: "Mode Differentials" })).toBeVisible();
await expect(page.getByTestId("differential-presentation-page")).toBeVisible();
await expect(page.getByRole("heading", { level: 1, name: "Acute confusion / encephalopathy" })).toBeVisible();
await expect(page.getByText("Selected differentials (6 of 8)")).toBeVisible();
await expect(
page.getByLabel("Differential review sidebar").getByText("Selected differentials (6 of 8)"),
).toBeVisible();
await expect(page.getByRole("link", { name: "Back" })).toHaveAttribute("href", "/differentials");
await expect(page.getByRole("heading", { name: "Safety snapshot" }).first()).toBeVisible();
await expect(page.getByText("Service details")).toHaveCount(0);
await expect(page.getByText("Transport order")).toHaveCount(0);
await expect(page.getByText("Local only")).toBeVisible();
await expect(page.getByText("Offline ready")).toBeVisible();
await expect(page.getByText("Source pending review").first()).toBeVisible();
await expect(page.locator("header").getByRole("button", { name: "Copy after review" })).toBeVisible();
// Page-specific status now lives in the page (Source status panel), not the
// shared global header — the header carries no differential trust chips.
await expect(page.locator("header").getByText("Local only")).toHaveCount(0);
await expect(page.locator("header").getByText("Offline ready")).toHaveCount(0);
await expect(page.locator("header").getByText("Source pending review")).toHaveCount(0);
await expect(page.locator("header").getByRole("button", { name: "Copy after review" })).toHaveCount(0);
await expect(page.getByTestId("global-search-input")).toHaveCount(0);

const tableScrolls = await page.getByTestId("differential-comparison-scroll").evaluate((element) => {
Expand All@@ -569,6 +573,14 @@ test.describe("Clinical KB applications launcher", () => {
expect(desktopTableBox?.width ?? 0).toBeGreaterThan(900);
await expectNoPageHorizontalOverflow(page);

// Mid-size (tablet): the scrollable comparison table replaces the phone
// accordions, and the review panels reflow below it instead of a side rail.
await page.setViewportSize({ width: 900, height: 1000 });
await gotoLauncher(page, "/differentials/presentations");
await expect(page.getByTestId("differential-comparison-scroll")).toBeVisible();
await expect(page.getByLabel("Mobile differential comparison")).toBeHidden();
await expectNoPageHorizontalOverflow(page);

await page.setViewportSize({ width: 390, height: 844 });
await gotoLauncher(page, "/differentials/presentations");

Expand Down
Loading