From 0d8c36a77cf3683856ac366c728ff285a83b2b5d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 19:51:33 +0000 Subject: [PATCH 1/6] Design follow-ups: On Call hospital change in the hero, Admin date line, square Clinical chips, 12 px legends, Work search and safety plan polish Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw --- src/components/admin/admin-today-page.tsx | 3 +- src/components/admin/admin-work.module.css | 3 +- .../answer-suggestion-chips.tsx | 9 +- .../universal-search-command-surface.tsx | 14 +- src/components/cme/cme-work.css | 7 +- .../favourites/favourites-shelf.tsx | 8 +- src/components/medicines/medicines-home.tsx | 4 +- src/components/my-day/my-day-today-cards.tsx | 2 +- src/components/on-call/kit/hospital-line.tsx | 73 ++++++---- src/components/on-call/now/on-call-hero.tsx | 27 +++- src/components/on-call/on-call-home.tsx | 18 ++- src/components/patient-safety-plan.tsx | 23 ++- src/components/roster/roster-month-grid.tsx | 2 +- src/components/show-all-chip.tsx | 9 +- .../work-search/work-search-button.tsx | 22 ++- tests/admin-today-page.dom.test.tsx | 20 +++ tests/clinical-chip-corners.dom.test.tsx | 131 ++++++++++++++++++ tests/my-day-favourites-shelf.dom.test.tsx | 11 ++ tests/on-call-now-hero.dom.test.tsx | 25 ++++ tests/on-call-now.dom.test.tsx | 33 +++++ tests/patient-safety-plan.dom.test.tsx | 17 +++ tests/work-search-coach.dom.test.tsx | 59 ++++++++ 22 files changed, 469 insertions(+), 51 deletions(-) create mode 100644 tests/clinical-chip-corners.dom.test.tsx create mode 100644 tests/work-search-coach.dom.test.tsx diff --git a/src/components/admin/admin-today-page.tsx b/src/components/admin/admin-today-page.tsx index eb7d5c02ba..a7fce5eb10 100644 --- a/src/components/admin/admin-today-page.tsx +++ b/src/components/admin/admin-today-page.tsx @@ -110,7 +110,8 @@ export function AdminTodayPage({ now: nowProp }: { now?: Date } = {}) { const load = adminLoadState(state); const bandShown = useModeBandShown(); const dateEcho = formatZonedLongDay(today); - useModeBandHeading({ title: "Admin" }); + // The date line over the title, as every other Work page has it (My Day, CPD, Roster, Teaching). + useModeBandHeading({ eyebrow: dateEcho, title: "Admin" }); const own = useMemo(() => selectAdminOwnEntries(state), [state]); const shared = useMemo(() => selectAdminSharedEntries(state), [state]); diff --git a/src/components/admin/admin-work.module.css b/src/components/admin/admin-work.module.css index c7bd9aac84..c2e45aacfb 100644 --- a/src/components/admin/admin-work.module.css +++ b/src/components/admin/admin-work.module.css @@ -512,9 +512,10 @@ fill: currentColor; } +/* Full hero ink, not faded: the labels are small, so they keep their contrast + on the slate (design follow-up 15 Oct 2026). */ .onHero .windowLabels { color: inherit; - opacity: 0.86; } .onHero .windowLabels b { diff --git a/src/components/clinical-dashboard/answer-suggestion-chips.tsx b/src/components/clinical-dashboard/answer-suggestion-chips.tsx index f57d06e4b8..7737f82019 100644 --- a/src/components/clinical-dashboard/answer-suggestion-chips.tsx +++ b/src/components/clinical-dashboard/answer-suggestion-chips.tsx @@ -115,6 +115,7 @@ export function AnswerSuggestionChips({ layout = "wrap", className, icon: Icon, + chipShape = "pill", }: { suggestions: string[]; onPick: (suggestion: string) => void; @@ -129,6 +130,12 @@ export function AnswerSuggestionChips({ // Optional leading glyph rendered inside every chip — used to signal a chip's // kind (e.g. a history icon on recent-search chips) without changing the label. icon?: LucideIcon; + // "control" squares the chip to the Clinical control rung (`rounded-lg`, as + // `controlBase`): rounded pills are a Work-mode shape only (owner decision, + // 15 Oct 2026). The chip's base rule in globals.css is unlayered, so the + // utility needs the important modifier to win. Default stays "pill" so the + // answer-thread follow-ups and any Work caller keep their shape. + chipShape?: "pill" | "control"; }) { const scrolls = layout === "scroll"; const { ref: railRef, node: railNode, edges } = useRailScrollEdges(); @@ -166,7 +173,7 @@ export function AnswerSuggestionChips({ type="button" disabled={disabled} onClick={() => onPick(suggestion)} - className={cn("answer-suggestion-chip", focusRing)} + className={cn("answer-suggestion-chip", chipShape === "control" && "!rounded-lg", focusRing)} > {Icon ? ( <> diff --git a/src/components/clinical-dashboard/universal-search-command-surface.tsx b/src/components/clinical-dashboard/universal-search-command-surface.tsx index fc180d110c..e8e48f8213 100644 --- a/src/components/clinical-dashboard/universal-search-command-surface.tsx +++ b/src/components/clinical-dashboard/universal-search-command-surface.tsx @@ -33,6 +33,7 @@ import { useSavedRegistryFavourites } from "@/components/clinical-dashboard/use- import { cn } from "@/components/ui-primitives"; import { appModeDefinition, filterCrossModesForSession, type AppModeId } from "@/lib/app-modes"; import { appModeIcons } from "@/lib/app-mode-icons"; +import { modeMenuSideForMode } from "@/lib/phone-mode-groups"; import { commandDropdownCanDisplay, commandDropdownMinimumWidthMediaQuery, @@ -305,7 +306,15 @@ function SmartIntentCue({ active, modeLabel }: { active: boolean; modeLabel: str ); } -function SmartPromptRow({ examples, onPickExample }: { examples: string[]; onPickExample: (example: string) => void }) { +function SmartPromptRow({ + modeId, + examples, + onPickExample, +}: { + modeId: AppModeId; + examples: string[]; + onPickExample: (example: string) => void; +}) { return ( ); } @@ -1264,6 +1275,7 @@ export function UniversalSearchCommandSurface({ {showHomeSuggestions ? ( { onQueryChange(example); diff --git a/src/components/cme/cme-work.css b/src/components/cme/cme-work.css index 178aa12ae3..71b9248d41 100644 --- a/src/components/cme/cme-work.css +++ b/src/components/cme/cme-work.css @@ -29,7 +29,10 @@ html[data-mode-identity="cme"] { --cme-hero-track: color-mix(in srgb, white 14%, transparent); --cme-hero-line: color-mix(in srgb, white 22%, transparent); --cme-hero-panel: color-mix(in srgb, white 10%, transparent); - --cme-hero-soft-text: color-mix(in srgb, white 82%, transparent); + /* The hero's quieter words (the kicker, "of 50 h", the legend, the pace line): + the hero's own ink, barely lifted, so they stay readable on the copper + (design follow-up 15 Oct 2026: at 82% white they read as faint). */ + --cme-hero-soft-text: color-mix(in srgb, var(--mode-identity-contrast) 94%, transparent); } html.dark [data-mode-identity="cme"], @@ -39,6 +42,8 @@ html.dark[data-mode-identity="cme"] { --cme-cat-4: color-mix(in oklab, var(--mode-identity) 24%, var(--surface-raised)); --cme-hero-cat-2: color-mix(in oklab, var(--mode-identity) 70%, white); --cme-hero-cat-3: var(--mode-identity); + /* The dark hero is inked with the heading colour, not white. */ + --cme-hero-soft-text: color-mix(in srgb, var(--text-heading) 92%, transparent); } /* --------------------------------------------------------------- page body */ diff --git a/src/components/favourites/favourites-shelf.tsx b/src/components/favourites/favourites-shelf.tsx index 4675e35010..63758c7d53 100644 --- a/src/components/favourites/favourites-shelf.tsx +++ b/src/components/favourites/favourites-shelf.tsx @@ -49,7 +49,13 @@ function TileFace({ item }: { item: FavouriteItem }) { ) : null} - + {/* Words wrap only at spaces (no break-words, no hyphens: Chromium splits unknown drug + names mid-word, "Acamprosa te"). Two lines, then an ellipsis; a single word too long + for the tile ends in an ellipsis rather than being cut in two (15 Oct 2026). */} + {label} - + {suggestion} diff --git a/src/components/my-day/my-day-today-cards.tsx b/src/components/my-day/my-day-today-cards.tsx index 71427f923a..0436063caf 100644 --- a/src/components/my-day/my-day-today-cards.tsx +++ b/src/components/my-day/my-day-today-cards.tsx @@ -866,7 +866,7 @@ export function ShiftLegend({
    {shown.map(([kind, label]) => (
  • diff --git a/src/components/on-call/kit/hospital-line.tsx b/src/components/on-call/kit/hospital-line.tsx index a8bc61b6b6..2e42cf4ae8 100644 --- a/src/components/on-call/kit/hospital-line.tsx +++ b/src/components/on-call/kit/hospital-line.tsx @@ -22,35 +22,60 @@ export function OnCallHospitalLine({ readonly handbook: HospitalHandbookState; readonly testId?: string; }) { - const [open, setOpen] = useState(false); const name = handbook.siteName ?? handbook.serviceName; if (!name) return null; - const canChange = handbook.hospitals.length > 1; return (

    {name}

    - {canChange ? ( - <> - - setOpen(false)} title="Choose hospital" testId={`${testId}-sheet`}> -
    - setOpen(false)} /> -
    -
    - - ) : null} +
    ); } + +/** + * The "Change" control on its own, with its chooser sheet, for a surface that + * already names the hospital (Now's summary hero). Renders nothing unless the + * reader belongs to more than one hospital. `variant="hero"` draws it as the + * hero's quiet pill; the default is the hospital line's text button. + */ +export function OnCallHospitalChange({ + handbook, + testId, + variant = "line", +}: { + readonly handbook: HospitalHandbookState; + /** Stem for the control's testids: `${testId}-change` and `${testId}-sheet`. */ + readonly testId: string; + readonly variant?: "line" | "hero"; +}) { + const [open, setOpen] = useState(false); + const name = handbook.siteName ?? handbook.serviceName; + if (!name || handbook.hospitals.length <= 1) return null; + return ( + <> + + setOpen(false)} title="Choose hospital" testId={`${testId}-sheet`}> +
    + setOpen(false)} /> +
    +
    + + ); +} diff --git a/src/components/on-call/now/on-call-hero.tsx b/src/components/on-call/now/on-call-hero.tsx index b4d5b78304..b8373ce276 100644 --- a/src/components/on-call/now/on-call-hero.tsx +++ b/src/components/on-call/now/on-call-hero.tsx @@ -1,6 +1,7 @@ "use client"; import { Phone } from "lucide-react"; +import type { ReactNode } from "react"; import { WorkHero } from "@/components/mode-kit/work"; import { onCallShiftClock } from "@/components/on-call/now/shift-clock"; @@ -25,6 +26,11 @@ import type { OnCallShiftContext } from "@/lib/on-call/shift-context"; * window (as "Right now" reads it), and the number is the hospital's recorded * switchboard. With no switchboard recorded there is no pill, never an invented * number. While the handbook loads the pill's place is held, so nothing moves. + * + * The hero is also where the page names the hospital (its eyebrow), so the + * "Change hospital" control sits here as a quiet second pill, not on a separate + * hospital line that would repeat the name. It is passed in as + * `hospitalChange`, and is absent for a reader with only one hospital. */ export function NowOnCallHero({ context, @@ -37,6 +43,7 @@ export function NowOnCallHero({ rosterError = false, rosterSignedOut = false, now, + hospitalChange = null, }: { readonly context: OnCallShiftContext; /** The hospital's switchboard row, or null when none is recorded. */ @@ -51,6 +58,8 @@ export function NowOnCallHero({ /** Signed out, the roster read returns nothing, which is not the same as no shift. */ readonly rosterSignedOut?: boolean; readonly now: Date; + /** The quiet "Change hospital" pill, or null when there is nothing to change. */ + readonly hospitalChange?: ReactNode; }) { const hospitalPhone = useOnCallHospitalPhone(); const clock = onCallShiftClock(context, now); @@ -68,7 +77,14 @@ export function NowOnCallHero({ ? `Switchboard ${dial.display}` : null; - const eyebrow = [clock ? "On call now" : "Right now", hospitalName].filter(Boolean).join(" · "); + const lead = clock ? "On call now" : "Right now"; + const eyebrow = hospitalName ? ( + <> + {lead} · {hospitalName} + + ) : ( + lead + ); const title = clock ? `${onCallDurationWords(clock.minutesLeft)} left` : cover && periodLabel @@ -120,7 +136,14 @@ export function NowOnCallHero({ eyebrow={eyebrow} title={{title}} sub={sub ? {sub} : null} - footer={action ?
    {action}
    : null} + footer={ + action || hospitalChange ? ( +
    + {action} + {hospitalChange} +
    + ) : null + } /> ); } diff --git a/src/components/on-call/on-call-home.tsx b/src/components/on-call/on-call-home.tsx index 04112c43fb..5c010219a8 100644 --- a/src/components/on-call/on-call-home.tsx +++ b/src/components/on-call/on-call-home.tsx @@ -13,7 +13,7 @@ import { focusRing } from "@/components/card-recipes"; import { InformationPageShell } from "@/components/information-page-shell"; import { toHandbookDial } from "@/components/on-call/kit/dial-row"; import { OnCallHandbookState } from "@/components/on-call/kit/handbook-state"; -import { OnCallHospitalLine } from "@/components/on-call/kit/hospital-line"; +import { OnCallHospitalChange } from "@/components/on-call/kit/hospital-line"; import { NowCrisisLines } from "@/components/on-call/now/crisis-lines"; import { NowEmergencyPin } from "@/components/on-call/now/emergency-pin"; import { NowOnCallHero } from "@/components/on-call/now/on-call-hero"; @@ -62,7 +62,8 @@ import { useWorkTimeZone } from "@/components/work-time/use-work-time-zone"; * * 0. the offline banner and the load-failed notice, as before, then the * summary hero (time left on your shift, switchboard as the white pill); - * 1. the hospital line (review F3), then the handbook's state when its numbers + * 1. the hospital, named once in the hero's eyebrow with a quiet "Change + * hospital" pill beside the call pill, then the handbook's state when its numbers * are not on screen (signed out, expired, no service, unavailable); * 2. the hospital's pinned emergency route, above everything else; * 3. the public crisis lines whenever the hospital's own numbers are not on @@ -85,7 +86,7 @@ import { useWorkTimeZone } from "@/components/work-time/use-work-time-zone"; * * Nothing moves under the thumb: while the hospital loads, each hospital module * holds its space with static outlines, and nothing is ever inserted above the - * hospital line. + * summary hero. * * The hospital's own after-hours times are a Stage B field. Until a hospital * sets them, Now does not adapt to after hours: `handbook.hours` is null, the @@ -296,7 +297,7 @@ function OnCallHomeLive({ now: pinnedNow }: { now?: Date } = {}) { const hasEntries = entries.length > 0; // The handbook's own sign-in states (signed out, or a session that ended) - // already ask the reader to sign in, under the hospital line; the first-run + // already ask the reader to sign in, under the summary hero; the first-run // block then does not ask a second time. Spelled as what it is not, because // this file is a compliance surface and one status name is a banned word. const handbookAsksSignIn = @@ -333,7 +334,8 @@ function OnCallHomeLive({ now: pinnedNow }: { now?: Date } = {}) { <> {/* The summary hero leads every Work page (Work standard, owner 15 Oct 2026). It is drawn from the first paint and holds its shape while the handbook loads, so - nothing is inserted above the hospital line later. */} + nothing is inserted above it later. It names the hospital and carries the + "Change hospital" pill, so there is no separate hospital line repeating the name. */} 1 ? ( + + ) : null + } /> - {!ready && !handbookLoading ? : null} } diff --git a/src/components/patient-safety-plan.tsx b/src/components/patient-safety-plan.tsx index eacae45b5d..7858f9eef5 100644 --- a/src/components/patient-safety-plan.tsx +++ b/src/components/patient-safety-plan.tsx @@ -36,7 +36,6 @@ import { fieldLabel, fieldControlPlain, ignoreUnavailableActivation, - metadataPill, panelSubtle, primaryControl, toneNeutral, @@ -406,6 +405,24 @@ function AddRow({ ); } +/** + * How many entries a step holds. Drawn as a quiet tinted number, not a bordered + * box, so it reads as a count rather than a field to type into (design + * follow-up 15 Oct 2026). The number alone is hidden from screen readers; they + * hear it with its noun ("0 entries"). + */ +function StepCount({ count, noun }: { count: number; noun: readonly [string, string] }) { + return ( + + + {`${count} ${count === 1 ? noun[0] : noun[1]}`} + + ); +} + function EntryChip({ entry, kind, onRemove }: { entry: Entry; kind: StepKind; onRemove: () => void }) { return (
  • {def.builderTitle}

    {def.helper}

    - {entries.length} + {entries.length ? ( @@ -1078,7 +1095,7 @@ export function PatientSafetyPlan() { The people, plans and things that matter — worth coming back to.

    - {reasons.length} + {reasons.length ? (
      diff --git a/src/components/roster/roster-month-grid.tsx b/src/components/roster/roster-month-grid.tsx index 0c29830502..fec8aad80a 100644 --- a/src/components/roster/roster-month-grid.tsx +++ b/src/components/roster/roster-month-grid.tsx @@ -137,7 +137,7 @@ export function RosterMonthLegend({ }) { return (
        {LEGEND_KINDS.map((kind) => ( diff --git a/src/components/show-all-chip.tsx b/src/components/show-all-chip.tsx index e5cdccb570..a312d0a5a1 100644 --- a/src/components/show-all-chip.tsx +++ b/src/components/show-all-chip.tsx @@ -7,7 +7,10 @@ import { cn } from "@/components/ui-primitives"; /** * Soft-fill well chip used under a mode-home hero (Tools, Calculators). * - * Visual capsule is 36px (`h-9` row face). The hit target stays `min-h-tap` + * Visual face is 36px (`h-9` row face). Its corners are the Clinical control + * rung (`rounded-lg`, the same as `controlBase`), not a pill: every caller is a + * Clinical page, and rounded pills are a Work-mode shape only (owner decision, + * 15 Oct 2026). The icon well inside steps down one rung to `rounded-md`. The hit target stays `min-h-tap` * (48px) on phones and may tighten to `lg:min-h-compact-meta` (40px) on * desktop — catalogue disclosure chrome, not a primary CTA. Never `min-h-11` * and never `--row-compact` (36px) as the tap. @@ -36,10 +39,10 @@ export function ShowAllChip({ focusRing, )} > - + diff --git a/src/components/work-search/work-search-button.tsx b/src/components/work-search/work-search-button.tsx index 2023d06e26..ba9970c917 100644 --- a/src/components/work-search/work-search-button.tsx +++ b/src/components/work-search/work-search-button.tsx @@ -14,6 +14,8 @@ import { isWorkSearchArea } from "@/lib/work-search/model"; /** Remembered per device so the one-time "New" note never comes back once read. A convenience only. */ const COACH_KEY = "psychsift:work-search-coach-seen"; +/** How long the one-time note stays before it goes by itself. */ +const COACH_SHOW_MS = 8000; function coachSeen(): boolean { try { @@ -78,7 +80,7 @@ function prefetchWhenIdle(): () => void { * the header names `header-trailing`) has room for it beside the bell: 20.75rem is the bell, * the gap and the 18rem pill, less the grid gap it may lean into. Measured in rem, so at large * text sizes the control stays a round icon rather than running under the mode switcher. It opens a full-screen search across Roster, Teaching, CPD, Admin and On Call. - * The first time it appears, a small "New" note points at it until dismissed. + * The first time it appears, a small "New" note points at it for a few seconds, once per device. * * Keys (work-mode redesign, ideas list #3): "/" or Ctrl K (Command K on a Mac) opens * it from anywhere on a work page that is not a text field, and "?" shows the list. @@ -97,11 +99,25 @@ export function WorkSearchButton({ modeId, className }: { modeId: AppModeId; cla useEffect(() => { if (!coach) return; + // On a wide screen the note hangs below the header over the top of the page (it covered + // CPD's Customise button and the My Day heading, 15 Oct 2026). So it is shown once only: + // counted as read as soon as it appears, and it goes by itself after a few seconds, or + // at the reader's first tap, key or scroll, whichever comes first. + markCoachSeen(); + const setAsideNow = () => setSetAside(true); const onPointerDown = (event: PointerEvent) => { - if (!coachRef.current?.contains(event.target as Node)) setSetAside(true); + if (!coachRef.current?.contains(event.target as Node)) setAsideNow(); }; + const timer = window.setTimeout(setAsideNow, COACH_SHOW_MS); document.addEventListener("pointerdown", onPointerDown, true); - return () => document.removeEventListener("pointerdown", onPointerDown, true); + document.addEventListener("keydown", setAsideNow, true); + window.addEventListener("scroll", setAsideNow, { capture: true, passive: true }); + return () => { + window.clearTimeout(timer); + document.removeEventListener("pointerdown", onPointerDown, true); + document.removeEventListener("keydown", setAsideNow, true); + window.removeEventListener("scroll", setAsideNow, { capture: true }); + }; }, [coach]); const buttonRef = useRef(null); diff --git a/tests/admin-today-page.dom.test.tsx b/tests/admin-today-page.dom.test.tsx index 6411cb8bfe..5462cfeaa8 100644 --- a/tests/admin-today-page.dom.test.tsx +++ b/tests/admin-today-page.dom.test.tsx @@ -45,6 +45,20 @@ vi.mock("@/components/clinical-dashboard/account-setup-dialog", () => ({ AccountSetupDialog: () => null, })); +// Records what the page hands the band, so the date line over the title can be checked +// without a band on screen. The real hook still runs. +const bandHeadings = vi.hoisted(() => [] as unknown[]); +vi.mock("@/components/mode-band/mode-band", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useModeBandHeading: (heading: Parameters[0]) => { + bandHeadings.push(heading); + actual.useModeBandHeading(heading); + }, + }; +}); + import { AdminTodayPage } from "@/components/admin/admin-today-page"; // 09:00 on Sat 26 Sep 2026 in Perth, the same fixed instant the selector tests use. @@ -124,6 +138,12 @@ describe("AdminTodayPage", () => { expect(screen.queryByRole("navigation", { name: "Sections of this page" })).toBeNull(); }); + it("puts the date line over the title in the band, like every other Work page", () => { + bandHeadings.length = 0; + render(); + expect(bandHeadings.at(-1)).toEqual({ eyebrow: expect.stringContaining("Saturday 26 September"), title: "Admin" }); + }); + it("shows static skeletons while loading, nothing ready-shaped", () => { state.loading = true; render(); diff --git a/tests/clinical-chip-corners.dom.test.tsx b/tests/clinical-chip-corners.dom.test.tsx new file mode 100644 index 0000000000..104c724463 --- /dev/null +++ b/tests/clinical-chip-corners.dom.test.tsx @@ -0,0 +1,131 @@ +/** @vitest-environment jsdom */ + +// Owner decision (15 Oct 2026): Clinical keeps its square-ish control corners on +// buttons and chips; rounded pills are a Work-mode shape only. Three Clinical +// chips had drifted to pills — the mode-home "Prompts" rail (DSM, Differentials +// and the other Clinical homes), the "Show all" chip (Calculators), and the +// Medicines "Try" suggestions. Each now wears the Clinical control rung +// (`rounded-lg`, the same as `controlBase`), and the Prompts rail on a Work +// mode keeps its pill. + +import { cleanup, render, screen, within } from "@testing-library/react"; +import { Calculator } from "lucide-react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const universal = vi.hoisted(() => ({ + state: { groups: [], loading: false, query: "" } as Record, +})); + +vi.mock("@/components/clinical-dashboard/use-universal-search", async (importOriginal) => { + const actual = await importOriginal(); + return { ...actual, useUniversalSearch: () => universal.state }; +}); + +vi.mock("@/lib/supabase/client", () => ({ + useAuthSession: () => ({ status: "signed_out", session: null, isConfigured: true, authorizationHeader: {} }), +})); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ push: vi.fn(), replace: vi.fn(), prefetch: vi.fn() }), + usePathname: () => "/", +})); + +vi.mock("@/components/clinical-dashboard/use-saved-registry-favourites", () => ({ + useSavedRegistryFavourites: () => ({ items: [], status: "ready", registryStatus: "ready", refetch: () => undefined }), +})); + +vi.mock("@/components/clinical-dashboard/search-command-context", () => ({ + useSearchCommand: () => null, +})); + +import { AnswerSuggestionChips } from "@/components/clinical-dashboard/answer-suggestion-chips"; +import { UniversalSearchCommandSurface } from "@/components/clinical-dashboard/universal-search-command-surface"; +import { MedicinesHome } from "@/components/medicines/medicines-home"; +import { ShowAllChip } from "@/components/show-all-chip"; +import type { AppModeId } from "@/lib/app-modes"; + +function HomeSurface({ modeId }: { modeId: AppModeId }) { + return ( + undefined} + onQueryChange={() => undefined} + onSearch={() => undefined} + onPickRecent={() => undefined} + onCrossMode={() => undefined} + showHomeSuggestions + > + + + ); +} + +function promptChips() { + return within(screen.getByTestId("smart-search-prompt-row")).getAllByRole("button"); +} + +beforeEach(() => { + vi.stubGlobal("matchMedia", () => ({ matches: false, addEventListener: vi.fn(), removeEventListener: vi.fn() })); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); +}); + +describe("Clinical chips keep the control corner, not a pill", () => { + it("leaves the shared suggestion chip a pill unless a caller asks for the control shape", () => { + const { unmount } = render( undefined} />); + expect(screen.getByRole("button", { name: "One" })).not.toHaveClass("!rounded-lg"); + unmount(); + + render( undefined} chipShape="control" />); + expect(screen.getByRole("button", { name: "Two" })).toHaveClass("answer-suggestion-chip", "!rounded-lg"); + }); + + it.each(["dsm", "differentials"])("squares the Prompts chips on the %s home", (modeId) => { + render(); + const chips = promptChips(); + expect(chips.length).toBeGreaterThan(0); + for (const chip of chips) expect(chip).toHaveClass("!rounded-lg"); + }); + + it("keeps the Prompts chips a pill on a Work mode home", () => { + render(); + const chips = promptChips(); + expect(chips.length).toBeGreaterThan(0); + for (const chip of chips) expect(chip).not.toHaveClass("!rounded-lg"); + }); + + it("draws the Show all chip on the control rung, with its icon well one rung smaller", () => { + render( + , + ); + const link = screen.getByTestId("calc-all"); + expect(link).toHaveClass("min-h-tap"); + const face = link.firstElementChild as HTMLElement; + expect(face).toHaveClass("rounded-lg"); + expect(face).not.toHaveClass("rounded-full"); + const well = screen.getByTestId("calc-all-well"); + expect(well).toHaveClass("rounded-md"); + expect(well).not.toHaveClass("rounded-full"); + }); + + it("draws the Medicines Try suggestions on the control rung at the 48px tap floor", () => { + render(); + const links = within(screen.getByTestId("medicines-find-try")).getAllByRole("link"); + expect(links.length).toBeGreaterThan(0); + for (const link of links) { + expect(link).toHaveClass("min-h-12", "rounded-lg"); + expect(link).not.toHaveClass("rounded-xl"); + const face = link.firstElementChild as HTMLElement; + expect(face).toHaveClass("rounded-lg"); + expect(face).not.toHaveClass("rounded-xl"); + } + }); +}); diff --git a/tests/my-day-favourites-shelf.dom.test.tsx b/tests/my-day-favourites-shelf.dom.test.tsx index e14affae52..f088baf3a2 100644 --- a/tests/my-day-favourites-shelf.dom.test.tsx +++ b/tests/my-day-favourites-shelf.dom.test.tsx @@ -149,6 +149,17 @@ describe("My Day Favourites shelf", () => { expect(screen.getByTestId("my-day-favourites-all")).toHaveTextContent("All 1"); }); + it("wraps a long name only at spaces, so a long drug name ellipsises rather than splitting mid-word", () => { + shelfState.current = state({ items: [clinical("acamprosate", "Acamprosate renal dosing")] }); + render(); + const name = screen.getByTestId("favourites-shelf-tile-name"); + expect(name).toHaveTextContent("Acamprosate renal dosing"); + expect(name).toHaveClass("line-clamp-2", "text-ellipsis", "[overflow-wrap:normal]", "[word-break:normal]"); + for (const splitting of ["break-words", "break-all", "hyphens-auto", "wrap-anywhere", "wrap-break-word"]) { + expect(name).not.toHaveClass(splitting); + } + }); + it("signed out with no work pages, draws nothing", () => { shelfState.current = state({ signedIn: false, items: [clinical("delirium", "Delirium")] }); const { container } = render(); diff --git a/tests/on-call-now-hero.dom.test.tsx b/tests/on-call-now-hero.dom.test.tsx index cb9512fadb..5247d65b78 100644 --- a/tests/on-call-now-hero.dom.test.tsx +++ b/tests/on-call-now-hero.dom.test.tsx @@ -50,6 +50,31 @@ describe("NowOnCallHero", () => { expect(call).toHaveClass("work-hero__action"); }); + it("names the hospital in its eyebrow and carries the Change hospital pill beside the call pill", () => { + render( + + Change hospital + + } + />, + ); + const hero = screen.getByTestId("on-call-now-hero"); + expect(within(hero).getByTestId("on-call-now-hospital")).toHaveTextContent("Synthetic Hospital"); + const actions = hero.querySelector(".work-hero__actions"); + expect(actions).not.toBeNull(); + expect(within(actions as HTMLElement).getByRole("link", { name: /^Call switchboard, / })).toBeInTheDocument(); + expect(within(actions as HTMLElement).getByRole("button", { name: "Change hospital" })).toBeInTheDocument(); + }); + it("says no rostered shift is on, and offers no number the hospital has not recorded", () => { render( { expect(within(hero).getByRole("link", { name: /^Call switchboard, / })).toHaveAttribute("href", "tel:0890000000"); }); + it("names the hospital once, in the hero, with Change hospital as the hero's quiet pill", async () => { + handbook.state = { + ...readyHandbook(handbookItems([{ id: "sw", title: "Switchboard", phone: "9000 0000" }])), + hospitals: [ + { serviceId: "svc", serviceName: "Synthetic Health Service", siteId: "a", siteName: "Synthetic Hospital" }, + { + serviceId: "svc", + serviceName: "Synthetic Health Service", + siteId: "b", + siteName: "Synthetic North Hospital", + }, + ], + }; + render(); + const hero = screen.getByTestId("on-call-now-hero"); + // The hospital's name appears in the hero's eyebrow and nowhere else at the top of the page. + expect(hero.contains(screen.getByTestId("on-call-now-hospital"))).toBe(true); + expect(screen.getAllByText("Synthetic Hospital")).toHaveLength(1); + const change = within(hero).getByRole("button", { name: "Change hospital, currently Synthetic Hospital" }); + expect(change).toHaveTextContent("Change hospital"); + expect(change).toHaveClass("work-hero__action"); + expect(change).toHaveAttribute("data-variant", "quiet"); + await userEvent.click(change); + expect(await screen.findByRole("option", { name: /Synthetic North Hospital/ })).toBeInTheDocument(); + }); + + it("offers no Change hospital pill when the reader has only one hospital", () => { + handbook.state = readyHandbook(handbookItems([{ id: "sw", title: "Switchboard", phone: "9000 0000" }])); + render(); + expect(screen.queryByRole("button", { name: /Change hospital/ })).toBeNull(); + expect(screen.getByTestId("on-call-now-hospital")).toHaveTextContent("Synthetic Hospital"); + }); + it("pins only this site's clinical Emergency rows, three at most, with the quiet red dot", async () => { handbook.state = readyHandbook( handbookItems([ diff --git a/tests/patient-safety-plan.dom.test.tsx b/tests/patient-safety-plan.dom.test.tsx index 0bc695aa86..f483eaebec 100644 --- a/tests/patient-safety-plan.dom.test.tsx +++ b/tests/patient-safety-plan.dom.test.tsx @@ -45,6 +45,23 @@ describe("PatientSafetyPlan — incomplete-plan draft guard", () => { expect(screen.getByRole("heading", { name: /Safety plan generator/i })).toBeTruthy(); }); + it("draws each step's count as a plain tinted number, not an input-like box, and names it for screen readers", () => { + render(); + const counts = screen.getAllByTestId("safety-plan-step-count"); + // Six steps plus Reasons for living, all empty on a blank plan. + expect(counts).toHaveLength(7); + for (const count of counts) { + const className = count.getAttribute("class") ?? ""; + expect(className).not.toMatch(/(?:^|\s)border(?:\s|$|-)/); + expect(className).not.toContain("shadow"); + expect(className).toContain("rounded-full"); + expect(className).toContain("text-xs"); + } + expect(counts[0].textContent).toBe("00 entries"); + expect(within(counts[0]).getByText("0 entries")).toHaveClass("sr-only"); + expect(counts.at(-1)?.textContent).toBe("00 reasons"); + }); + it("flags the patient copy as a draft until every step is complete", async () => { render(); diff --git a/tests/work-search-coach.dom.test.tsx b/tests/work-search-coach.dom.test.tsx new file mode 100644 index 0000000000..496ee41edf --- /dev/null +++ b/tests/work-search-coach.dom.test.tsx @@ -0,0 +1,59 @@ +import { act, fireEvent, render, screen } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("@/components/work-search/lazy-work-search-sheet", () => ({ + LazyWorkSearchSheet: () => null, + LazyWorkSearchKeys: () => null, + prefetchWorkSearchSheet: () => {}, +})); + +import { WorkSearchButton } from "@/components/work-search/work-search-button"; + +const KEY = "psychsift:work-search-coach-seen"; + +describe("AI Search's one-time New note", () => { + beforeEach(() => { + window.localStorage.removeItem(KEY); + vi.useFakeTimers(); + }); + afterEach(() => { + vi.useRealTimers(); + window.localStorage.removeItem(KEY); + }); + + it("is counted as read as soon as it shows, then goes by itself after a few seconds", () => { + render(); + expect(screen.getByRole("note")).toHaveTextContent("New"); + expect(window.localStorage.getItem(KEY)).toBe("1"); + act(() => { + vi.advanceTimersByTime(7999); + }); + expect(screen.queryByRole("note")).not.toBeNull(); + act(() => { + vi.advanceTimersByTime(1); + }); + expect(screen.queryByRole("note")).toBeNull(); + }); + + it("goes at the reader's first scroll", () => { + render(); + act(() => { + fireEvent.scroll(window); + }); + expect(screen.queryByRole("note")).toBeNull(); + }); + + it("goes at the reader's first tap elsewhere on the page", () => { + render(); + act(() => { + fireEvent.pointerDown(document.body); + }); + expect(screen.queryByRole("note")).toBeNull(); + }); + + it("never shows again once read", () => { + window.localStorage.setItem(KEY, "1"); + render(); + expect(screen.queryByRole("note")).toBeNull(); + }); +}); From 9d4da4a9205912ab2e081d6cdc5ef34e057efde5 Mon Sep 17 00:00:00 2001 From: "qodo-code-review[bot]" <151058649+qodo-code-review[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:11:18 +0000 Subject: [PATCH 2/6] =?UTF-8?q?fix:=202=20findings=20=E2=80=94=20Preserve?= =?UTF-8?q?=20the=20search=20coach=20note=20for=20the=20current=20visit;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Preserve the search coach note for the current visit - Only mark visible desktop search guidance as seen --- .../work-search/work-search-button.tsx | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/components/work-search/work-search-button.tsx b/src/components/work-search/work-search-button.tsx index ba9970c917..4da02133cf 100644 --- a/src/components/work-search/work-search-button.tsx +++ b/src/components/work-search/work-search-button.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useEffectEvent, useRef, useState, useSyncExternalStore } from "react"; +import { useEffect, useEffectEvent, useRef, useState } from "react"; import { LazyWorkSearchKeys, @@ -33,10 +33,6 @@ function markCoachSeen() { } } -function subscribeNever() { - return () => {}; -} - /** Typing in a field (or a rich-text box): the shortcut keys are the reader's letters there, not ours. */ function isTyping(target: EventTarget | null): boolean { if (!(target instanceof HTMLElement)) return false; @@ -88,8 +84,9 @@ function prefetchWhenIdle(): () => void { export function WorkSearchButton({ modeId, className }: { modeId: AppModeId; className?: string }) { const [open, setOpen] = useState(false); const [keysOpen, setKeysOpen] = useState(false); - // Read from storage after hydration (the server snapshot says "seen"), then hidden for good once dismissed. - const seen = useSyncExternalStore(subscribeNever, coachSeen, () => true); + // Read from storage once after hydration; marking it seen should not hide this visit's note. + const [seen, setSeen] = useState(true); + useEffect(() => setSeen(coachSeen()), []); const [dismissed, setDismissed] = useState(false); // Any tap elsewhere hides the note for this visit, without counting it as read, // so it never sits over the page the reader is trying to use. @@ -100,10 +97,15 @@ export function WorkSearchButton({ modeId, className }: { modeId: AppModeId; cla useEffect(() => { if (!coach) return; // On a wide screen the note hangs below the header over the top of the page (it covered - // CPD's Customise button and the My Day heading, 15 Oct 2026). So it is shown once only: - // counted as read as soon as it appears, and it goes by itself after a few seconds, or - // at the reader's first tap, key or scroll, whichever comes first. - markCoachSeen(); + // CPD's Customise button and the My Day heading, 15 Oct 2026). Count it as read only when + // that explanatory note is actually visible; the phone-only ring must not consume it. + const desktop = window.matchMedia("(min-width: 1024px)"); + const markIfVisible = () => { + if (!desktop.matches) return; + markCoachSeen(); + }; + markIfVisible(); + desktop.addEventListener("change", markIfVisible); const setAsideNow = () => setSetAside(true); const onPointerDown = (event: PointerEvent) => { if (!coachRef.current?.contains(event.target as Node)) setAsideNow(); @@ -114,6 +116,7 @@ export function WorkSearchButton({ modeId, className }: { modeId: AppModeId; cla window.addEventListener("scroll", setAsideNow, { capture: true, passive: true }); return () => { window.clearTimeout(timer); + desktop.removeEventListener("change", markIfVisible); document.removeEventListener("pointerdown", onPointerDown, true); document.removeEventListener("keydown", setAsideNow, true); window.removeEventListener("scroll", setAsideNow, { capture: true }); From d08426b555345d6adc2aeaf62a00f042961ad3f4 Mon Sep 17 00:00:00 2001 From: "qodo-code-review[bot]" <151058649+qodo-code-review[bot]@users.noreply.github.com> Date: Fri, 16 Oct 2026 05:11:21 +0000 Subject: [PATCH 3/6] fix: Use square Clinical shapes for formulation presets --- src/components/formulation/formulation-home-page.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/formulation/formulation-home-page.tsx b/src/components/formulation/formulation-home-page.tsx index 891d66fbfe..11e732efae 100644 --- a/src/components/formulation/formulation-home-page.tsx +++ b/src/components/formulation/formulation-home-page.tsx @@ -277,6 +277,7 @@ function FormulationResults({ query }: { query: string }) { label="Try another pattern" labelPlacement="above" layout="scroll" + chipShape="control" testId="formulation-pattern-suggestions" suggestions={formulationSearchPresets.map((preset) => preset.label)} onPick={(label) => { From 55a3d87526c8770ff056683ffadd96514656ce10 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 05:16:52 +0000 Subject: [PATCH 4/6] Work search note: count it as read when it goes, and only when it showed; square Formulation prompt chips Storing the note as read the moment it appeared hid it at the next re-render, because the stored value is read on every render. It is now stored when it goes (timer, tap, key or scroll), and only at desktop width, where the note is on screen, so a phone visit that shows only the ring does not use it up. Formulation's "Try another pattern" chips take the Clinical control corners like the other Clinical prompts. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw --- .../formulation/formulation-home-page.tsx | 1 + .../work-search/work-search-button.tsx | 19 +++++++--- tests/work-search-coach.dom.test.tsx | 37 ++++++++++++++++++- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/components/formulation/formulation-home-page.tsx b/src/components/formulation/formulation-home-page.tsx index 891d66fbfe..11e732efae 100644 --- a/src/components/formulation/formulation-home-page.tsx +++ b/src/components/formulation/formulation-home-page.tsx @@ -277,6 +277,7 @@ function FormulationResults({ query }: { query: string }) { label="Try another pattern" labelPlacement="above" layout="scroll" + chipShape="control" testId="formulation-pattern-suggestions" suggestions={formulationSearchPresets.map((preset) => preset.label)} onPick={(label) => { diff --git a/src/components/work-search/work-search-button.tsx b/src/components/work-search/work-search-button.tsx index ba9970c917..41eb2cabef 100644 --- a/src/components/work-search/work-search-button.tsx +++ b/src/components/work-search/work-search-button.tsx @@ -33,6 +33,11 @@ function markCoachSeen() { } } +/** The note itself only shows from 1024px (the `lg` breakpoint); below that only the ring does. */ +function noteVisible(): boolean { + return typeof window.matchMedia === "function" && window.matchMedia("(min-width: 64rem)").matches; +} + function subscribeNever() { return () => {}; } @@ -100,11 +105,15 @@ export function WorkSearchButton({ modeId, className }: { modeId: AppModeId; cla useEffect(() => { if (!coach) return; // On a wide screen the note hangs below the header over the top of the page (it covered - // CPD's Customise button and the My Day heading, 15 Oct 2026). So it is shown once only: - // counted as read as soon as it appears, and it goes by itself after a few seconds, or - // at the reader's first tap, key or scroll, whichever comes first. - markCoachSeen(); - const setAsideNow = () => setSetAside(true); + // CPD's Customise button and the My Day heading, 15 Oct 2026). So it goes by itself after a + // few seconds, or at the reader's first tap, key or scroll, whichever comes first. It is + // counted as read only when it goes and only if the note was on screen: storing it as it + // appears would hide it at the next re-render (the stored value is read on every render), + // and a phone shows only the ring, so a phone visit must not use up the note. + const setAsideNow = () => { + if (noteVisible()) markCoachSeen(); + setSetAside(true); + }; const onPointerDown = (event: PointerEvent) => { if (!coachRef.current?.contains(event.target as Node)) setAsideNow(); }; diff --git a/tests/work-search-coach.dom.test.tsx b/tests/work-search-coach.dom.test.tsx index 496ee41edf..dbbdb4c9b3 100644 --- a/tests/work-search-coach.dom.test.tsx +++ b/tests/work-search-coach.dom.test.tsx @@ -11,20 +11,37 @@ import { WorkSearchButton } from "@/components/work-search/work-search-button"; const KEY = "psychsift:work-search-coach-seen"; +/** The note shows from 1024px; below that only the ring does. */ +function setViewport(desktop: boolean) { + window.matchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: desktop, + media: query, + onchange: null, + addEventListener: () => {}, + removeEventListener: () => {}, + addListener: () => {}, + removeListener: () => {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; +} + describe("AI Search's one-time New note", () => { + const originalMatchMedia = window.matchMedia; beforeEach(() => { window.localStorage.removeItem(KEY); + setViewport(true); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); window.localStorage.removeItem(KEY); + window.matchMedia = originalMatchMedia; }); - it("is counted as read as soon as it shows, then goes by itself after a few seconds", () => { + it("goes by itself after a few seconds, and is counted as read when it goes", () => { render(); expect(screen.getByRole("note")).toHaveTextContent("New"); - expect(window.localStorage.getItem(KEY)).toBe("1"); + expect(window.localStorage.getItem(KEY)).toBeNull(); act(() => { vi.advanceTimersByTime(7999); }); @@ -33,6 +50,22 @@ describe("AI Search's one-time New note", () => { vi.advanceTimersByTime(1); }); expect(screen.queryByRole("note")).toBeNull(); + expect(window.localStorage.getItem(KEY)).toBe("1"); + }); + + it("stays through a re-render while it is showing", () => { + const { rerender } = render(); + rerender(); + expect(screen.getByRole("note")).toHaveTextContent("New"); + }); + + it("is not used up on a phone, where only the ring shows", () => { + setViewport(false); + render(); + act(() => { + vi.advanceTimersByTime(8000); + }); + expect(window.localStorage.getItem(KEY)).toBeNull(); }); it("goes at the reader's first scroll", () => { From 09d624445d29b5698b8c7edb906a953661a955ea Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 05:20:32 +0000 Subject: [PATCH 5/6] Review fixes: favourites wrap at large text, Admin date line recorded as the owner's decision, real one-hospital test - Favourites: at large text the two-line clamp lifts, so a long single word may break there rather than spill past its tile. - Admin: the page comment still said the owner had dropped the date line. The owner brought it back on 16 Oct 2026 after seeing the picture; comment updated and the decision indexed in docs/decisions. - On Call: the "only one hospital" test now gives the reader exactly one hospital, not none. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw --- docs/decisions/2026-10-16-admin-shows-the-date-line.md | 10 ++++++++++ src/components/admin/admin-today-page.tsx | 5 +++-- src/components/favourites/favourites-shelf.tsx | 5 +++-- tests/on-call-now.dom.test.tsx | 7 ++++++- 4 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 docs/decisions/2026-10-16-admin-shows-the-date-line.md diff --git a/docs/decisions/2026-10-16-admin-shows-the-date-line.md b/docs/decisions/2026-10-16-admin-shows-the-date-line.md new file mode 100644 index 0000000000..b5388d17e7 --- /dev/null +++ b/docs/decisions/2026-10-16-admin-shows-the-date-line.md @@ -0,0 +1,10 @@ +# Decision: Admin shows the date line over its title, like the other Work pages + +- **Status:** decided 2026-10-16 +- **Source:** `src/components/admin/admin-today-page.tsx`, find "The owner brought the date line back on 16 Oct 2026" + +The owner approved a before and after picture of Admin with the date over its title, matching My Day, +CPD, Roster and Teaching. This replaces the earlier wish, from the 13 Oct 2026 design audit, to leave +the date line off Admin. + +If this summary and the source ever differ, the source wins. diff --git a/src/components/admin/admin-today-page.tsx b/src/components/admin/admin-today-page.tsx index a7fce5eb10..24818c0223 100644 --- a/src/components/admin/admin-today-page.tsx +++ b/src/components/admin/admin-today-page.tsx @@ -46,8 +46,9 @@ import { useWorkTimeZone } from "@/components/work-time/use-work-time-zone"; * * Phone is one column in that order. From a laptop width it splits into what * to act on (left) and what is ahead (right); left then right IS the phone - * order. The band names the page "Admin" (audit D15, 13 Oct 2026): no greeting, - * and no date line, which the owner asked to drop from Admin. + * order. The band names the page "Admin" (audit D15, 13 Oct 2026) with no + * greeting. The owner brought the date line back on 16 Oct 2026, after seeing it + * beside the other Work pages, so Admin shows the date over its title like them. * * Fail-closed: while loading, signed out or after a failed load nothing from * the reader's records is drawn, never a saved copy (the device cache holds diff --git a/src/components/favourites/favourites-shelf.tsx b/src/components/favourites/favourites-shelf.tsx index 63758c7d53..e2f8516fc5 100644 --- a/src/components/favourites/favourites-shelf.tsx +++ b/src/components/favourites/favourites-shelf.tsx @@ -51,10 +51,11 @@ function TileFace({ item }: { item: FavouriteItem }) { {/* Words wrap only at spaces (no break-words, no hyphens: Chromium splits unknown drug names mid-word, "Acamprosa te"). Two lines, then an ellipsis; a single word too long - for the tile ends in an ellipsis rather than being cut in two (15 Oct 2026). */} + for the tile ends in an ellipsis rather than being cut in two (15 Oct 2026). At large + text the clamp lifts, so there a long word may break rather than spill past the tile. */} {label} diff --git a/tests/on-call-now.dom.test.tsx b/tests/on-call-now.dom.test.tsx index aca03b68bc..6dfb243956 100644 --- a/tests/on-call-now.dom.test.tsx +++ b/tests/on-call-now.dom.test.tsx @@ -143,7 +143,12 @@ describe("Now: the safety order", () => { }); it("offers no Change hospital pill when the reader has only one hospital", () => { - handbook.state = readyHandbook(handbookItems([{ id: "sw", title: "Switchboard", phone: "9000 0000" }])); + handbook.state = { + ...readyHandbook(handbookItems([{ id: "sw", title: "Switchboard", phone: "9000 0000" }])), + hospitals: [ + { serviceId: "svc", serviceName: "Synthetic Health Service", siteId: "a", siteName: "Synthetic Hospital" }, + ], + }; render(); expect(screen.queryByRole("button", { name: /Change hospital/ })).toBeNull(); expect(screen.getByTestId("on-call-now-hospital")).toHaveTextContent("Synthetic Hospital"); From f5be6796c294a3cced1ac4dd4509c0b8c5e7d004 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 05:26:55 +0000 Subject: [PATCH 6/6] Refresh the repo awareness snapshot for the new decision record Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw --- data/repo-awareness-snapshot.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/data/repo-awareness-snapshot.json b/data/repo-awareness-snapshot.json index 79151e4f6e..06694b5ce6 100644 --- a/data/repo-awareness-snapshot.json +++ b/data/repo-awareness-snapshot.json @@ -1,7 +1,7 @@ { "version": "repo-awareness-snapshot-v3", "captured_revision": { - "committed_at": "2026-10-15" + "committed_at": "2026-10-16" }, "routes": { "modes": [ @@ -2500,6 +2500,11 @@ "section": "agents", "catalogued": true }, + { + "path": "docs/decisions/2026-10-16-admin-shows-the-date-line.md", + "section": "decisions", + "catalogued": true + }, { "path": "docs/product/clinical-trust-direction.md", "section": "product",