Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,10 @@ export function MedicationNavHeader({
if (isMedicationTabId(id)) onSelectTab(id);
}}
// Every slot carries a section count, so the four-slot band clips.
rail={{ label: "Medication sections", countedLabels: true }}
// `extended-counted`, named rather than inherited. Its four slots each
// carry an icon, a label and a count badge, so they need the wider bands
// this profile was calibrated for — not Therapy's wordmark bands.
rail={{ label: "Medication sections", density: "extended-counted", countedLabels: true }}
/>
);
}
8 changes: 6 additions & 2 deletions src/components/in-page-nav/in-page-nav-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ export type InPageNavHeaderProps =
*/
rail?: {
label: string;
/** Calibrated label family — moves only the band widths. */
density?: ModeNavDensityProfile;
/**
* Calibrated label family, moving only the band widths. Required: see
* the note on `InPageSectionRail`'s own `density` for why inheriting a
* default here shipped a regression.
*/
density: ModeNavDensityProfile;
/** `true` when every slot carries a count badge beside its label. */
countedLabels?: boolean;
};
Expand Down
16 changes: 14 additions & 2 deletions src/components/in-page-nav/in-page-section-rail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function InPageSectionRail({
sectionSheetOpen,
label,
testIdPrefix,
density = "extended-counted",
density,
countedLabels = false,
}: {
sections: readonly PageSection[];
Expand All @@ -50,8 +50,20 @@ export function InPageSectionRail({
* Which calibrated label family this rail's labels belong to. It only moves
* the container width at which each band becomes active — never the order,
* and never which item folds first.
*
* Required, with no default, and that is the point. It used to default to
* `extended`, so medication's rail inherited Therapy's profile without ever
* naming it — invisible to `registryModeNavDensityProfiles`, which lists only
* the modes that render the top bar. Retuning `extended` for Therapy in
* PR #2686 therefore silently unfolded medication's counted labels early, and
* `ui-smoke`'s prescribing journey was the only thing that caught it.
*
* A default cannot be safe here: the value is a calibration against one
* label family's measured widths, so inheriting someone else's is always a
* guess. Making it required costs each call site one line and makes the
* consumer set answerable by `git grep "density:"`.
*/
density?: ModeNavDensityProfile;
density: ModeNavDensityProfile;
/**
* `true` when every slot carries a count badge beside its label, which needs
* roughly a third more width per slot.
Expand Down
11 changes: 10 additions & 1 deletion src/components/therapy-compass/pathway-step-stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { cn } from "@/components/ui-primitives";

import type { Pathway, PathwayStep, Therapy } from "./data/types";
import { pathwayLinkedStepCount } from "./pathway-review-label";
import { StatusBadge } from "./ui";

type PathwayStepStackProps = {
steps: PathwayStep[];
Expand Down Expand Up @@ -61,6 +62,11 @@ function StepCard({
<span className="inline-flex items-center rounded-md border border-[color:var(--border)] bg-[color:var(--surface-inset)] px-2 py-0.5 text-2xs font-bold tracking-eyebrow text-[color:var(--text-muted)]">
{roleLabel}
</span>
{/* Only when the step resolves to a record. `description` falls
back to that record's `bestUsedFor`, so a linked step quotes
its clinical prose and owes its review state; an unlinked step
names no record and has none to state. */}
{therapy ? <StatusBadge status={therapy.reviewStatus} size="compact" /> : null}
</div>
<p className="mt-1.5 mb-0 text-xs leading-normal text-[color:var(--text-muted)]">{description}</p>
</div>
Expand Down Expand Up @@ -106,7 +112,10 @@ function StepCard({
<Scale aria-hidden="true" size={17} strokeWidth={1.6} />
</span>
<div className="min-w-0 flex-1">
<div className="text-sm-minus font-semibold text-[color:var(--text-heading)]">{title}</div>
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm-minus font-semibold text-[color:var(--text-heading)]">{title}</span>
{therapy ? <StatusBadge status={therapy.reviewStatus} size="compact" /> : null}
</div>
<div className="mt-0.5 line-clamp-2 text-xs text-[color:var(--text-muted)]">{description}</div>
</div>
<span className="whitespace-nowrap text-3xs font-bold tracking-eyebrow text-[color:var(--text-muted)]">
Expand Down
9 changes: 9 additions & 0 deletions src/components/therapy-compass/record/related-therapies.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { InteractiveRow } from "@/components/ui/interactive-row";

import { cardPreviewText } from "../data/select";
import type { RelatedTherapy } from "../data/related";
import { StatusBadge } from "../ui";

/**
* Nearest neighbours, each carrying the reason it is here.
Expand Down Expand Up @@ -44,6 +45,14 @@ export function RelatedTherapies({ related, onOpen }: { related: RelatedTherapy[
<span className="rounded-full border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2 py-px text-3xs font-semibold text-[color:var(--clinical-accent)]">
{reason}
</span>
{/* The row prints a sentence of another record's clinical
prose, so it owes that record's review state — the same
obligation the card, detail, brief and sheet already meet.
It matters more since the catalogue-wide notice went
(PR #2686): the per-record badge is now the whole
disclosure, and this listing was one of two surfaces
quoting a record without it. */}
<StatusBadge status={therapy.reviewStatus} size="compact" />
</span>
<span className="mt-1 line-clamp-2 block text-xs leading-5 text-[color:var(--text-muted)]">
{cardPreviewText(therapy.bestUsedFor ?? therapy.clinicalSummary, {
Expand Down
6 changes: 3 additions & 3 deletions src/components/therapy-compass/ui.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { ShieldCheck, TriangleAlert, type LucideIcon } from "lucide-react";

import { Chip, type ChipAppearance } from "@/components/ui/chip";
import { Chip, type ChipAppearance, type ChipSize } from "@/components/ui/chip";
import { missingValuePhrase } from "@/components/ui/missing-value";
import { cn, EmptyState as SharedEmptyState, LoadingPanel } from "@/components/ui-primitives";

Expand Down Expand Up @@ -86,12 +86,12 @@ export function TagRow({

// ---- review status badge ------------------------------------------------

export function StatusBadge({ status }: { status: string }) {
export function StatusBadge({ status, size = "standard" }: { status: string; size?: ChipSize }) {
const meta = reviewStatusMeta(status);
const tone = meta.tone === "success" ? "success" : meta.tone === "warning" ? "warning" : "neutral";
const Icon = meta.tone === "success" ? ShieldCheck : TriangleAlert;
return (
<Chip size="standard" appearance={{ kind: "status", tone }} icon={Icon}>
<Chip size={size} appearance={{ kind: "status", tone }} icon={Icon}>
{meta.label}
</Chip>
);
Expand Down
4 changes: 2 additions & 2 deletions tests/in-page-nav-header.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe("InPageNavHeader", () => {
});

it("keeps a phone gap between the action row and the section rail", () => {
renderHeader({ rail: { label: "Service sections" } });
renderHeader({ rail: { label: "Service sections", density: "balanced-four" } });
expect(screen.getByTestId("service-section-rail")).toHaveClass("mt-2");
});

Expand Down Expand Up @@ -201,7 +201,7 @@ describe("InPageNavHeader", () => {
// closes after the viewport crosses `sm` must not restore to a display:none
// button (focus then falls to the page body).
const user = userEvent.setup();
renderHeader({ rail: { label: "Service sections" } });
renderHeader({ rail: { label: "Service sections", density: "balanced-four" } });
const trigger = screen.getByTestId("service-section-trigger");

await user.click(trigger);
Expand Down
26 changes: 26 additions & 0 deletions tests/mode-nav-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,32 @@ describe("ModeNav item contract", () => {
});
});

describe("ModeNav density is chosen, never inherited", () => {
const railSource = read("src/components/in-page-nav/in-page-section-rail.tsx");
const headerSource = read("src/components/in-page-nav/in-page-nav-header.tsx");

it("requires every rail to name its own density profile", () => {
// `density` defaulted to a profile, so medication's rail inherited
// Therapy's without naming it. `registryModeNavDensityProfiles` lists only
// the modes that render the top bar, so the coupling was invisible there,
// and retuning `extended` for Therapy in PR #2686 unfolded medication's
// counted labels early. A default cannot be safe: the value is calibrated
// against one label family's measured widths.
expect(railSource).not.toMatch(/density\s*=\s*"/);
expect(railSource).toMatch(/^\s*density: ModeNavDensityProfile;/m);
expect(headerSource).toMatch(/^\s*density: ModeNavDensityProfile;/m);
});

it("keeps every rail call site naming a profile, so the consumer set is greppable", () => {
for (const path of [
"src/components/clinical-dashboard/medication-nav-header.tsx",
"src/components/therapy-compass/therapy-record-nav-header.tsx",
]) {
expect(read(path), `${path} must name its density profile`).toMatch(/density: "/);
}
});
});

describe("ModeNav overflow slot", () => {
const moreSlot = modeNavSource.slice(
modeNavSource.indexOf("plan.moreUntil !== null ? ("),
Expand Down
8 changes: 8 additions & 0 deletions tests/therapy-review-regressions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ describe("Therapy review regression contracts", () => {
"src/components/therapy-compass/screens/sheets-screen.tsx",
"src/components/therapy-compass/screens/compare-screen.tsx",
"src/components/therapy-compass/screens/pathways-screen.tsx",
// The two secondary listings. Both quote another record's clinical prose
// (`bestUsedFor` / `clinicalSummary`) beside its name, and both shipped
// without its review state — the gap the 2026-09-02 audit recorded under
// L03c. Harmless while a catalogue-wide notice also stated the caveat;
// load-bearing once PR #2686 removed it and left the per-record badge as
// the whole disclosure.
"src/components/therapy-compass/record/related-therapies.tsx",
"src/components/therapy-compass/pathway-step-stack.tsx",
]) {
expect(source(path), `${path} must still surface reviewStatus`).toContain("reviewStatus");
}
Expand Down
Loading