: null}
);
}
diff --git a/src/components/cme/cme-dashboard.tsx b/src/components/cme/cme-dashboard.tsx
index d99ba64051..41697a9478 100644
--- a/src/components/cme/cme-dashboard.tsx
+++ b/src/components/cme/cme-dashboard.tsx
@@ -83,7 +83,9 @@ import {
* Simplify plan (12 Oct 2026): "What's left" shows its top three, then "See all in End of year"; the
* copy and reflection chips and "About this year" went (they repeated What's left and the hero), and
* today's hours moved into the hero's kicker.
- * 6. the floating dock: "Log an activity" (it opens the quick-log sheet).
+ * 6. "Log an activity" (it opens the quick-log sheet), a white pill at the foot of the summary card. It
+ * was a floating dock until 15 Oct 2026, when the owner moved it into the card: at 390px the dock
+ * covered list rows ("See all in End of year").
*
* What follows is the earlier (5 Oct) description, which still holds for the
* data and the honest variants:
@@ -344,15 +346,24 @@ export function CmeDashboard({
: { kind: "loaded", at: now }
}
/>
- {underBand ? (
-
- ) : (
-
-
-
- )}
+ {/* The status sits outside the page columns, so this row takes their width and side padding itself:
+ Customise stays inside the page gutter at 390px rather than running off the right edge. */}
+
+ {underBand ? (
+
+ ) : (
+
+
+
+ )}
+
>
);
@@ -383,6 +394,7 @@ export function CmeDashboard({
closed={Boolean(set.closedAt)}
onOpenGap={() => setDetail("gap")}
firstLogAction={nothingLogged ? logLink("Log your first activity", "work-button min-h-tap mt-2") : undefined}
+ logAction={nothingLogged ? undefined : logLink("Log an activity", "work-button min-h-tap w-full")}
todayHours={shows("audited-today") ? loggedTodayHours : 0}
showTimeLeft={shows("year-dates")}
/>
@@ -622,13 +634,6 @@ export function CmeDashboard({
) : null;
- // The dock: the one filled button. Before anything is logged it lives in the empty card instead.
- const dock = nothingLogged ? null : (
-
-
{logLink("Log an activity", "work-button min-h-tap")}
-
- );
-
return (
{status}
@@ -653,7 +658,6 @@ export function CmeDashboard({
{trainingLink}
{provenance}
- {dock}
diff --git a/src/components/cme/cme-log-page.tsx b/src/components/cme/cme-log-page.tsx
index d3888611e8..78d0db6e1c 100644
--- a/src/components/cme/cme-log-page.tsx
+++ b/src/components/cme/cme-log-page.tsx
@@ -670,7 +670,10 @@ export function CmeLogPage({
) : null}
- {showFinish ? finish : activities}
+ {/* The rows end with a reserve the floating dock's height plus 16px, so the last ones never sit under it. */}
+
+ {showFinish ? finish : activities}
+
.work-button[data-variant="primary"] {
+ display: flex;
+ width: 100%;
+ color: var(--work-hero-action-ink, var(--mode-identity));
+}
+
+.work-hero .cpd-hero-log > .work-button[data-variant="primary"]::before {
+ background: var(--work-hero-action-fill, var(--surface-raised));
+}
+
+.work-hero .cpd-hero-log > .work-button:focus-visible {
+ outline: 2px solid currentColor;
+ outline-offset: 2px;
+}
+
.cpd-hero-kicker {
font-size: var(--text-3xs);
font-weight: 700;
@@ -954,6 +975,16 @@ a.cpd-months__col:focus-visible {
content: attr(data-short) / "";
}
+/* The Log keeps its floating dock, so the rows above it end with a reserve the
+ dock's height plus 16px: the last rows are never left under it. The height is
+ the dock's own box (work-mode.css .work-dock): its fade, the 48px button in
+ the capsule, and the home-bar padding. */
+.cpd-dock-reserve {
+ padding-bottom: calc(
+ 1.5rem + var(--spacing-tap, 3rem) + var(--spacing) + max(0.875rem, var(--safe-area-bottom, 0px)) + 1rem
+ );
+}
+
/* The dock's filled button keeps the 48px floor CPD holds to (min-h-12). */
html [data-mode-identity="cme"] .work-dock__capsule > .work-button,
html[data-mode-identity="cme"] .work-dock__capsule > .work-button {
diff --git a/src/components/differentials/differential-detail-page.tsx b/src/components/differentials/differential-detail-page.tsx
index a9ae3b6117..5b10fccd9c 100644
--- a/src/components/differentials/differential-detail-page.tsx
+++ b/src/components/differentials/differential-detail-page.tsx
@@ -257,8 +257,10 @@ function SectionRow({
{section.title}
{scopeNote ? : null}
+ {/* A section with neither items nor a summary would render as a bare
+ heading with no chevron and read as broken; say so plainly. */}
diff --git a/src/components/dsm/dsm-compare-chrome.tsx b/src/components/dsm/dsm-compare-chrome.tsx
index 41111cbe04..bf9db6ed12 100644
--- a/src/components/dsm/dsm-compare-chrome.tsx
+++ b/src/components/dsm/dsm-compare-chrome.tsx
@@ -42,7 +42,9 @@ export function DsmCompareChrome({
slotPlaceholder="Choose diagnosis"
icon={BookOpenCheck}
showEmptyState={false}
- slotLayout="compact"
+ // Default slot layout, not the `compact` snap rail: at 390px the rail
+ // cut slot B off and pushed C off screen. Default stacks the slots full
+ // width on a phone and goes side by side once the strip is wide enough.
onCommit={(ids) =>
router.push(
idsCompareHref(
diff --git a/src/components/dsm/dsm-search-page.tsx b/src/components/dsm/dsm-search-page.tsx
index 747d0bc441..d6e7f6b179 100644
--- a/src/components/dsm/dsm-search-page.tsx
+++ b/src/components/dsm/dsm-search-page.tsx
@@ -386,7 +386,10 @@ export function DsmSearchPage({
return (
-
+ {/* Phone: the bottom pad is the floating search pill's height (3.25rem,
+ 52px) plus 16px, so the last result card can scroll clear of the pill
+ even when the shell's own dock reserve has collapsed on scroll. */}
+
-
+
{label}
diff --git a/src/components/mode-home-start-list.tsx b/src/components/mode-home-start-list.tsx
index 82e0a89973..78935506fc 100644
--- a/src/components/mode-home-start-list.tsx
+++ b/src/components/mode-home-start-list.tsx
@@ -212,7 +212,10 @@ function ModeHomeStartListBody({ modeId, config }: { modeId: StartListMode; conf
{config.favouriteType ? (
) : null}
- {emptyHome && config.browse.length > 1 ? (
+ {/* An empty home always gets the Browse card list, even with one link:
+ Services and Documents have a single catalogue link, and as a bare
+ text link it read as unlike every other Clinical home. */}
+ {emptyHome && config.browse.length > 0 ? (
{config.browse.slice(0, EMPTY_HOME_BROWSE_SHOWN).map((link) => (
diff --git a/src/components/my-day/my-day-today-cards.tsx b/src/components/my-day/my-day-today-cards.tsx
index 3c565caae2..71427f923a 100644
--- a/src/components/my-day/my-day-today-cards.tsx
+++ b/src/components/my-day/my-day-today-cards.tsx
@@ -488,16 +488,13 @@ export function HeroCard({
const end = Date.parse(shift.endsAt);
const at = now.getTime();
if (running) {
- const left = ringParts(end - at);
const until = words.sub.split(" · ")[0] ?? words.sub;
+ const done = Math.min(1, Math.max(0, (at - start) / Math.max(1, end - start)));
top = (
+ // The title already says the time left, so the ring shows how much of the shift is done.
+
}
eyebrow={`${words.eyebrow}${asOf}`}
title={words.big}
diff --git a/src/components/on-call/now/emergency-pin.tsx b/src/components/on-call/now/emergency-pin.tsx
index e020127c1b..6c16d7bcf3 100644
--- a/src/components/on-call/now/emergency-pin.tsx
+++ b/src/components/on-call/now/emergency-pin.tsx
@@ -88,11 +88,14 @@ function EmergencyRoute({
onClick={recordCall}
aria-label={`Call ${title}${primaryWhere ? ` ${primaryWhere}` : ""}, emergency, ${spokenOnCallNumber(primary.display)}`}
data-testid={`${testId}-call`}
- className={cn(onCallFilledButton, focusRing, "justify-start")}
+ className={cn(onCallFilledButton, focusRing, "justify-start py-1.5")}
>
- {primary.display}
- {primaryWhere ? {primaryWhere} : null}
+ {/* The number and its "then ext 55" stay on one line; "from your mobile" takes its own line beneath. */}
+
+ {primary.display}
+ {primaryWhere ? {primaryWhere} : null}
+
) : null}
{dial && (deskOnly || !primary?.tel) && !extension ? (
diff --git a/src/components/on-call/now/on-call-hero.tsx b/src/components/on-call/now/on-call-hero.tsx
new file mode 100644
index 0000000000..a340527339
--- /dev/null
+++ b/src/components/on-call/now/on-call-hero.tsx
@@ -0,0 +1,104 @@
+"use client";
+
+import { Phone } from "lucide-react";
+
+import { WorkHero } from "@/components/mode-kit/work";
+import { onCallShiftClock } from "@/components/on-call/now/shift-clock";
+import { onCallCallRoute } from "@/components/on-call/kit/dial-sheet";
+import { useOnCallHospitalPhone } from "@/components/on-call/call/call-device-stores";
+import { rememberOnCallYouCalled } from "@/lib/on-call/call-marks";
+import { ON_CALL_YOU_CALLED_ENABLED } from "@/lib/on-call/feature-flags";
+import type { HandbookItem } from "@/lib/on-call/handbook-items";
+import { spokenOnCallNumber, type OnCallPeriod } from "@/lib/on-call/number-resolver";
+import type { OnCallHospitalHours } from "@/lib/on-call/now-rows";
+import { onCallCoverWindow, onCallDurationWords } from "@/lib/on-call/period-window";
+import { recordOnCallRecent } from "@/lib/on-call/recent-storage";
+import type { OnCallShiftContext } from "@/lib/on-call/shift-context";
+
+/**
+ * Now's summary card (Work standard, owner 15 Oct 2026): every Work page opens
+ * with one filled mode-colour hero, its main action a white pill. On Call's
+ * answers "how long have I got, and how do I reach switchboard?".
+ *
+ * Nothing here is new data: the time left is the reader's rostered shift (the
+ * same clock as "Your shift"), the fallback is the hospital's own after-hours
+ * 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.
+ */
+export function NowOnCallHero({
+ context,
+ answer,
+ hours,
+ hospitalPeriod,
+ hospitalName,
+ handbookLoading,
+ now,
+}: {
+ readonly context: OnCallShiftContext;
+ /** The hospital's switchboard row, or null when none is recorded. */
+ readonly answer: HandbookItem | null;
+ readonly hours: OnCallHospitalHours | null;
+ readonly hospitalPeriod: OnCallPeriod | null;
+ readonly hospitalName: string | null;
+ readonly handbookLoading: boolean;
+ readonly now: Date;
+}) {
+ const hospitalPhone = useOnCallHospitalPhone();
+ const clock = onCallShiftClock(context, now);
+ const cover = clock ? null : onCallCoverWindow(hours, now);
+ const periodLabel = hospitalPeriod ? (hospitalPeriod === "after-hours" ? "After hours" : "In hours") : null;
+ const dial = answer && answer.dial.kind !== "none" ? answer.dial : null;
+ const route = dial ? onCallCallRoute(dial, answer?.mobileDial, hospitalPhone) : null;
+ const switchboardLine = dial ? `Switchboard ${dial.display}` : null;
+
+ const eyebrow = [clock ? "On call now" : "Right now", hospitalName].filter(Boolean).join(" · ");
+ const title = clock
+ ? `${onCallDurationWords(clock.minutesLeft)} left`
+ : cover && periodLabel
+ ? `${periodLabel} until ${cover.until}`
+ : "No rostered shift on now";
+ const sub = [
+ clock
+ ? `${clock.periodLabel} shift ends ${clock.until}`
+ : cover
+ ? `${onCallDurationWords(cover.minutesLeft)} to go`
+ : null,
+ switchboardLine,
+ ]
+ .filter(Boolean)
+ .join(" · ");
+
+ const recordCall = () => {
+ if (!answer) return;
+ recordOnCallRecent({ id: answer.id, source: "handbook" });
+ if (ON_CALL_YOU_CALLED_ENABLED) rememberOnCallYouCalled(answer.id);
+ };
+
+ const action =
+ answer && route?.tel ? (
+
+
+ Call switchboard
+
+ ) : handbookLoading ? (
+
+ ) : null;
+
+ return (
+ {title}}
+ sub={sub ? {sub} : null}
+ footer={action ?
{cover.from}
- {/* "Now" sits under the dot, kept clear of both ends. */}
+ {/* "Now" sits under the dot, kept clear of both ends. The clamp is in ch so it scales with the
+ text: an end label is at most 5ch ("17:00"), "Now 19:12" is about 9ch wide (4.5ch each side
+ of its centre), so an 11ch centre leaves at least 1.5ch between them. */}
{`Now ${cover.nowTime}`}
diff --git a/src/components/on-call/now/shift-clock.tsx b/src/components/on-call/now/shift-clock.tsx
index 5cb96fd481..efba2a3351 100644
--- a/src/components/on-call/now/shift-clock.tsx
+++ b/src/components/on-call/now/shift-clock.tsx
@@ -80,7 +80,7 @@ export function NowShiftClock({ context, now }: { readonly context: OnCallShiftC
return (
-
+ From your roster}>
{`Your shift · ${clock.periodLabel}`}
diff --git a/src/components/on-call/on-call-home.tsx b/src/components/on-call/on-call-home.tsx
index d58d16943e..5fa69c8a2e 100644
--- a/src/components/on-call/on-call-home.tsx
+++ b/src/components/on-call/on-call-home.tsx
@@ -16,6 +16,7 @@ import { OnCallHandbookState } from "@/components/on-call/kit/handbook-state";
import { OnCallHospitalLine } 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";
import { NowNeedsYou, onCallNeedsYouAnswered, useOnCallCallMarks } from "@/components/on-call/now/needs-you";
import { NowRightNow } from "@/components/on-call/now/right-now";
import { NowShiftClock } from "@/components/on-call/now/shift-clock";
@@ -59,7 +60,8 @@ import { useWorkTimeZone } from "@/components/work-time/use-work-time-zone";
* Now, the On Call mode home (v6 figure "Now"): who to ring, at this hospital,
* this hour. Top to bottom, in the safety order:
*
- * 0. the offline banner and the load-failed notice, as before;
+ * 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
* are not on screen (signed out, expired, no service, unavailable);
* 2. the hospital's pinned emergency route, above everything else;
@@ -329,6 +331,18 @@ function OnCallHomeLive({ now: pinnedNow }: { now?: Date } = {}) {
columns="two"
status={
<>
+ {/* 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. */}
+
{!ready && !handbookLoading ? : null}
>
diff --git a/src/components/roster/roster-month-page.tsx b/src/components/roster/roster-month-page.tsx
index 0dc121b997..2b68836e66 100644
--- a/src/components/roster/roster-month-page.tsx
+++ b/src/components/roster/roster-month-page.tsx
@@ -1,7 +1,7 @@
"use client";
import dynamic from "next/dynamic";
-import { ChevronRight, Clock, FileUp, Plus, TriangleAlert, Users } from "lucide-react";
+import { CalendarDays, ChevronRight, Clock, FileUp, Plus, TriangleAlert, Users } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useMemo, useState, type ReactNode } from "react";
@@ -16,6 +16,7 @@ import {
WorkCard,
WorkDateRow,
WorkEmpty,
+ WorkHero,
WorkIconRow,
WorkSectionLabel,
} from "@/components/mode-kit/work";
@@ -376,43 +377,47 @@ export function RosterMonthPage({ now: pinnedNow }: { readonly now?: Date } = {}
: date === addDaysToDate(today, 1)
? `${SHIFT_KIND_LABEL[kind]} tomorrow`
: `${SHIFT_KIND_LABEL[kind]} ${dayWords(date)}`;
- const tag = onNow
- ? `${formatSpanUntil(Date.parse(leadDisplay.endsAt) - now.getTime())} left`
- : `in ${formatSpanUntil(Date.parse(leadDisplay.startsAt) - now.getTime())}`;
- // One slim line: My Day and On Call already show this shift in full (Work mode simplify plan,
- // 12 Oct 2026). Tapping it opens the day; Who is on sits beside it, one tap from Team.
+ const left = `${formatSpanUntil(Date.parse(leadDisplay.endsAt) - now.getTime())} left`;
+ const startsIn = `in ${formatSpanUntil(Date.parse(leadDisplay.startsAt) - now.getTime())}`;
+ const times = `${perthTimeOf(leadDisplay.startsAt)} to ${perthTimeOf(leadDisplay.endsAt)}`;
+ const next = upcoming[0];
+ const nextWords = next
+ ? `Next: ${SHIFT_KIND_LABEL[next.kind]} ${dayWords(perthDateOf(next.startsAt))} ${perthTimeOf(next.startsAt)}`
+ : null;
+ // The summary hero that leads every Work page (Work standard, owner 15 Oct 2026), in place
+ // of the slim white line: on now, the time left is the headline; otherwise the shift's own
+ // words ("On call tonight") are, with how soon it starts. Who's on is the white pill, and
+ // the day's sheet stays one tap away beside it.
return (
-
-
-
-
- {/* Icon only on the narrowest phones, so the shift line keeps to one line. */}
- Who is on
-
-
);
From 99b13ca8d82652903b47010b16186120e8a44eb5 Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 15 Oct 2026 16:47:45 +0000
Subject: [PATCH 03/17] Work hero pills: use the pill radius token and a
border, per the design-system contract
Co-Authored-By: Claude Opus 5.5 (1M context)
Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw
---
src/app/work-mode.css | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/app/work-mode.css b/src/app/work-mode.css
index 520cffcb27..8af6af16d3 100644
--- a/src/app/work-mode.css
+++ b/src/app/work-mode.css
@@ -1737,7 +1737,7 @@ a.work-hero:focus-visible {
gap: calc(var(--spacing) * 1.5);
padding: 0 calc(var(--spacing) * 4.5);
border: 0;
- border-radius: 999px;
+ border-radius: var(--radius-pill);
background: var(--work-hero-action-fill, var(--surface-raised));
color: var(--work-hero-action-ink, var(--mode-identity));
font: inherit;
@@ -1749,8 +1749,8 @@ a.work-hero:focus-visible {
}
.work-hero__action[data-variant="quiet"] {
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 14%, transparent);
- box-shadow: inset 0 0 0 1px color-mix(in srgb, currentColor 22%, transparent);
color: inherit;
}
From 76a381cff8db00a4a55805db6426fac45f5682ef Mon Sep 17 00:00:00 2001
From: "qodo-code-review[bot]"
<151058649+qodo-code-review[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:24:11 +0000
Subject: [PATCH 04/17] fix: Use the shell-owned dock reserve
---
src/components/dsm/dsm-search-page.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/components/dsm/dsm-search-page.tsx b/src/components/dsm/dsm-search-page.tsx
index d6e7f6b179..7af5509411 100644
--- a/src/components/dsm/dsm-search-page.tsx
+++ b/src/components/dsm/dsm-search-page.tsx
@@ -386,10 +386,7 @@ export function DsmSearchPage({
return (
- {/* Phone: the bottom pad is the floating search pill's height (3.25rem,
- 52px) plus 16px, so the last result card can scroll clear of the pill
- even when the shell's own dock reserve has collapsed on scroll. */}
-
+
Date: Thu, 15 Oct 2026 17:24:15 +0000
Subject: [PATCH 05/17] =?UTF-8?q?fix:=202=20findings=20=E2=80=94=20Show=20?=
=?UTF-8?q?the=20selected=20calling=20route;=20Show=20a=20neutral=20title?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Show the selected calling route
- Show a neutral title while data loads
---
src/components/on-call/now/on-call-hero.tsx | 15 +++++++++++++--
src/components/on-call/on-call-home.tsx | 1 +
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/src/components/on-call/now/on-call-hero.tsx b/src/components/on-call/now/on-call-hero.tsx
index a340527339..3cbcf3b431 100644
--- a/src/components/on-call/now/on-call-hero.tsx
+++ b/src/components/on-call/now/on-call-hero.tsx
@@ -33,6 +33,7 @@ export function NowOnCallHero({
hospitalPeriod,
hospitalName,
handbookLoading,
+ rosterLoading,
now,
}: {
readonly context: OnCallShiftContext;
@@ -42,6 +43,7 @@ export function NowOnCallHero({
readonly hospitalPeriod: OnCallPeriod | null;
readonly hospitalName: string | null;
readonly handbookLoading: boolean;
+ readonly rosterLoading: boolean;
readonly now: Date;
}) {
const hospitalPhone = useOnCallHospitalPhone();
@@ -50,14 +52,22 @@ export function NowOnCallHero({
const periodLabel = hospitalPeriod ? (hospitalPeriod === "after-hours" ? "After hours" : "In hours") : null;
const dial = answer && answer.dial.kind !== "none" ? answer.dial : null;
const route = dial ? onCallCallRoute(dial, answer?.mobileDial, hospitalPhone) : null;
- const switchboardLine = dial ? `Switchboard ${dial.display}` : null;
+ const switchboardLine = route ? `Switchboard ${route.display}` : null;
+ const extensionLine =
+ dial?.kind === "extension"
+ ? `Hospital extension ${dial.display}`
+ : dial?.kind === "switchboard-extension" && route !== dial
+ ? `Handbook extension ${dial.display}`
+ : null;
const eyebrow = [clock ? "On call now" : "Right now", hospitalName].filter(Boolean).join(" · ");
const title = clock
? `${onCallDurationWords(clock.minutesLeft)} left`
: cover && periodLabel
? `${periodLabel} until ${cover.until}`
- : "No rostered shift on now";
+ : rosterLoading || handbookLoading
+ ? "Checking your shift…"
+ : "No rostered shift on now";
const sub = [
clock
? `${clock.periodLabel} shift ends ${clock.until}`
@@ -65,6 +75,7 @@ export function NowOnCallHero({
? `${onCallDurationWords(cover.minutesLeft)} to go`
: null,
switchboardLine,
+ extensionLine,
]
.filter(Boolean)
.join(" · ");
diff --git a/src/components/on-call/on-call-home.tsx b/src/components/on-call/on-call-home.tsx
index 5fa69c8a2e..c39bee01a6 100644
--- a/src/components/on-call/on-call-home.tsx
+++ b/src/components/on-call/on-call-home.tsx
@@ -341,6 +341,7 @@ function OnCallHomeLive({ now: pinnedNow }: { now?: Date } = {}) {
hospitalPeriod={hospitalPeriod}
hospitalName={hospitalName}
handbookLoading={handbookLoading}
+ rosterLoading={shifts.status === "loading"}
now={now}
/>
From b83296d6f24a9695fdda2dcd830acdded9f911d3 Mon Sep 17 00:00:00 2001
From: "qodo-code-review[bot]"
<151058649+qodo-code-review[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:24:19 +0000
Subject: [PATCH 06/17] fix: Select a future shift for Next
---
src/components/roster/roster-month-page.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/components/roster/roster-month-page.tsx b/src/components/roster/roster-month-page.tsx
index 2b68836e66..4009b37cc4 100644
--- a/src/components/roster/roster-month-page.tsx
+++ b/src/components/roster/roster-month-page.tsx
@@ -380,7 +380,7 @@ export function RosterMonthPage({ now: pinnedNow }: { readonly now?: Date } = {}
const left = `${formatSpanUntil(Date.parse(leadDisplay.endsAt) - now.getTime())} left`;
const startsIn = `in ${formatSpanUntil(Date.parse(leadDisplay.startsAt) - now.getTime())}`;
const times = `${perthTimeOf(leadDisplay.startsAt)} to ${perthTimeOf(leadDisplay.endsAt)}`;
- const next = upcoming[0];
+ const next = upcoming.find((shift) => Date.parse(shift.startsAt) > now.getTime());
const nextWords = next
? `Next: ${SHIFT_KIND_LABEL[next.kind]} ${dayWords(perthDateOf(next.startsAt))} ${perthTimeOf(next.startsAt)}`
: null;
From 6c7a4c946f31c9b5e0a07828da6b0a86a2db0aa3 Mon Sep 17 00:00:00 2001
From: "qodo-code-review[bot]"
<151058649+qodo-code-review[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:24:23 +0000
Subject: [PATCH 07/17] fix: Draw a visible CPD focus ring
---
src/components/cme/cme-work.css | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/src/components/cme/cme-work.css b/src/components/cme/cme-work.css
index 9160199ed0..178aa12ae3 100644
--- a/src/components/cme/cme-work.css
+++ b/src/components/cme/cme-work.css
@@ -550,10 +550,14 @@ html.dark[data-mode-identity="cme"] {
}
.work-hero .cpd-hero-log > .work-button:focus-visible {
- outline: 2px solid currentColor;
+ outline: 2px solid var(--work-hero-action-fill, var(--surface-raised));
outline-offset: 2px;
}
+.work-hero .cpd-hero-log > .work-button[data-variant="primary"]:focus-visible::before {
+ box-shadow: none;
+}
+
.cpd-hero-kicker {
font-size: var(--text-3xs);
font-weight: 700;
From 0e96c88da163f91d52b15c7d51734101ccf2e7fb Mon Sep 17 00:00:00 2001
From: "qodo-code-review[bot]"
<151058649+qodo-code-review[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:24:26 +0000
Subject: [PATCH 08/17] fix: Allow emergency routes to wrap safely
---
src/components/on-call/now/emergency-pin.tsx | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/components/on-call/now/emergency-pin.tsx b/src/components/on-call/now/emergency-pin.tsx
index 6c16d7bcf3..4f8f676477 100644
--- a/src/components/on-call/now/emergency-pin.tsx
+++ b/src/components/on-call/now/emergency-pin.tsx
@@ -91,9 +91,16 @@ function EmergencyRoute({
className={cn(onCallFilledButton, focusRing, "justify-start py-1.5")}
>
- {/* The number and its "then ext 55" stay on one line; "from your mobile" takes its own line beneath. */}
+ {/* Keep each dialable segment intact while allowing a long route to wrap. */}
- {primary.display}
+
+ {primary.display.split(", then ext ").map((segment, index) => (
+
+ {index > 0 ? ", then ext " : null}
+ {segment}
+
+ ))}
+
{primaryWhere ? {primaryWhere} : null}
From 5ed213213d1ec93ec5bf31dc47b335de966e0a80 Mon Sep 17 00:00:00 2001
From: "qodo-code-review[bot]"
<151058649+qodo-code-review[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:24:30 +0000
Subject: [PATCH 09/17] fix: Only show None listed for empty sections
---
src/components/differentials/differential-detail-page.tsx | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/components/differentials/differential-detail-page.tsx b/src/components/differentials/differential-detail-page.tsx
index 5b10fccd9c..1a95cba70b 100644
--- a/src/components/differentials/differential-detail-page.tsx
+++ b/src/components/differentials/differential-detail-page.tsx
@@ -260,7 +260,10 @@ function SectionRow({
{/* A section with neither items nor a summary would render as a bare
heading with no chevron and read as broken; say so plainly. */}
From 1b492d8942bde1994fbbc5c3f988ebff6839ef0f Mon Sep 17 00:00:00 2001
From: "cubic-dev-ai[bot]"
<191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Date: Thu, 15 Oct 2026 17:47:07 +0000
Subject: [PATCH 10/17] test: pass rosterLoading to NowOnCallHero in its DOM
test
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The hero gained a required rosterLoading prop in this PR (on-call-home.tsx
already passes it), but tests/on-call-now-hero.dom.test.tsx still rendered it
without the prop, so `tsc` failed in Static PR checks and the Next.js build.
Pass rosterLoading={false} in the three existing renders and add a case for the
"Checking your shift…" title while the roster loads.
---
tests/on-call-now-hero.dom.test.tsx | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/tests/on-call-now-hero.dom.test.tsx b/tests/on-call-now-hero.dom.test.tsx
index 62c85ec89a..e4d083aed3 100644
--- a/tests/on-call-now-hero.dom.test.tsx
+++ b/tests/on-call-now-hero.dom.test.tsx
@@ -35,6 +35,7 @@ describe("NowOnCallHero", () => {
hospitalPeriod={null}
hospitalName="Synthetic Hospital"
handbookLoading={false}
+ rosterLoading={false}
now={AT_0350}
/>,
);
@@ -58,6 +59,7 @@ describe("NowOnCallHero", () => {
hospitalPeriod={null}
hospitalName={null}
handbookLoading={false}
+ rosterLoading={false}
now={AT_0350}
/>,
);
@@ -77,6 +79,7 @@ describe("NowOnCallHero", () => {
hospitalPeriod={null}
hospitalName={null}
handbookLoading
+ rosterLoading={false}
now={AT_0350}
/>,
);
@@ -84,4 +87,22 @@ describe("NowOnCallHero", () => {
expect(hero.querySelector("[data-placeholder]")).toHaveAttribute("aria-hidden", "true");
expect(within(hero).queryByRole("link")).toBeNull();
});
+
+ it("says it is checking the shift, not that none is on, while the roster loads", () => {
+ render(
+ ,
+ );
+ const hero = screen.getByTestId("on-call-now-hero");
+ expect(hero).toHaveTextContent("Checking your shift…");
+ expect(hero).not.toHaveTextContent("No rostered shift on now");
+ });
});
From 539a5389076d5e2384b2fe6521433236f91da43d Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 15 Oct 2026 17:48:33 +0000
Subject: [PATCH 11/17] Review bot fixes: keep DSM results padding, accurate
hero and section wording
- DSM results: restore the phone bottom padding, so the last card clears the floating search pill.
- On Call hero: the switchboard line names the number the pill rings; a bare extension says it needs a hospital phone. Adds a test for the mobile fallback.
- Differential detail: point to the clinical (or presentation) hinge by its real heading, not a "key distinction" label the page doesn't have.
Co-Authored-By: Claude Opus 5.5 (1M context)
Claude-Session: https://claude.ai/code/session_01TexgR3MMbRJjXTT2C5PoPw
---
.../differential-detail-page.tsx | 6 ++--
src/components/dsm/dsm-search-page.tsx | 5 ++-
src/components/on-call/now/on-call-hero.tsx | 19 +++++-----
tests/on-call-now-hero.dom.test.tsx | 36 +++++++++++++++++++
4 files changed, 54 insertions(+), 12 deletions(-)
diff --git a/src/components/differentials/differential-detail-page.tsx b/src/components/differentials/differential-detail-page.tsx
index 1a95cba70b..eb7543f550 100644
--- a/src/components/differentials/differential-detail-page.tsx
+++ b/src/components/differentials/differential-detail-page.tsx
@@ -261,8 +261,10 @@ function SectionRow({
heading with no chevron and read as broken; say so plainly. */}
{section.summary.trim() ||
- (section.items.length > 0 || (section.tone === "test" && record.investigations.length > 0)
- ? "See the key distinction above"
+ // Items that repeat the hinge are hidden from the list, so point to where they are shown.
+ ((section.items.length > 0 || (section.tone === "test" && record.investigations.length > 0)) &&
+ cleanDifferentialItem(record.clinicalHinge)
+ ? `See the ${record.clinicalHingeScope === "presentation" ? "presentation" : "clinical"} hinge above`
: "None listed")}
diff --git a/src/components/dsm/dsm-search-page.tsx b/src/components/dsm/dsm-search-page.tsx
index 7af5509411..d6e7f6b179 100644
--- a/src/components/dsm/dsm-search-page.tsx
+++ b/src/components/dsm/dsm-search-page.tsx
@@ -386,7 +386,10 @@ export function DsmSearchPage({
return (
-
+ {/* Phone: the bottom pad is the floating search pill's height (3.25rem,
+ 52px) plus 16px, so the last result card can scroll clear of the pill
+ even when the shell's own dock reserve has collapsed on scroll. */}
+
{
expect(hero.querySelector("[data-placeholder]")).toHaveAttribute("aria-hidden", "true");
expect(within(hero).queryByRole("link")).toBeNull();
});
+
+ it("names the number the pill rings when an extension falls back to a mobile route", () => {
+ const answer = {
+ ...SWITCHBOARD!,
+ dial: {
+ kind: "extension",
+ display: "ext 55",
+ extension: "55",
+ tel: null,
+ copy: "55",
+ route: "hospital-phone",
+ },
+ mobileDial: {
+ kind: "direct",
+ display: "0400 000 000",
+ tel: "tel:0400000000",
+ copy: "0400000000",
+ route: "any-phone",
+ },
+ } as unknown as NonNullable;
+ render(
+ ,
+ );
+ const hero = screen.getByTestId("on-call-now-hero");
+ expect(hero).toHaveTextContent("Switchboard 0400 000 000");
+ expect(hero).not.toHaveTextContent("ext 55");
+ expect(within(hero).getByRole("link", { name: /^Call switchboard, / })).toHaveAttribute("href", "tel:0400000000");
+ });
});
From 5082141f4953c1e1ee95e829f0902cb41157744a Mon Sep 17 00:00:00 2001
From: Claude
Date: Thu, 15 Oct 2026 18:15:16 +0000
Subject: [PATCH 12/17] Roster hero: find the next shift among all loaded
shifts, and load the lead shift's month before opening its day
The hero's Next line searched the three-row Coming up list, so shifts overlapping the lead could fill it and hide a later one. It now searches every loaded shift. Open day on the hero also switches to the lead shift's month first, as the Coming up rows do, so the sheet reads the right date range.
Co-Authored-By: Claude Sonnet 5.5
Claude-Session: https://claude.ai/code/session_01MuxbMS35mz82HhVZyLLtEH
---
src/components/roster/roster-month-page.tsx | 12 ++++++++++--
tests/roster-month-page.dom.test.tsx | 12 ++++++++++++
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/components/roster/roster-month-page.tsx b/src/components/roster/roster-month-page.tsx
index 4009b37cc4..db25462905 100644
--- a/src/components/roster/roster-month-page.tsx
+++ b/src/components/roster/roster-month-page.tsx
@@ -380,7 +380,11 @@ export function RosterMonthPage({ now: pinnedNow }: { readonly now?: Date } = {}
const left = `${formatSpanUntil(Date.parse(leadDisplay.endsAt) - now.getTime())} left`;
const startsIn = `in ${formatSpanUntil(Date.parse(leadDisplay.startsAt) - now.getTime())}`;
const times = `${perthTimeOf(leadDisplay.startsAt)} to ${perthTimeOf(leadDisplay.endsAt)}`;
- const next = upcoming.find((shift) => Date.parse(shift.startsAt) > now.getTime());
+ // Search every loaded shift, not the three-row Coming up list, so shifts that overlap the
+ // lead cannot fill the list and hide a later one.
+ const next = nextShifts(monthShifts, now, monthShifts.length, leadShown).find(
+ (shift) => Date.parse(shift.startsAt) > now.getTime(),
+ );
const nextWords = next
? `Next: ${SHIFT_KIND_LABEL[next.kind]} ${dayWords(perthDateOf(next.startsAt))} ${perthTimeOf(next.startsAt)}`
: null;
@@ -407,7 +411,11 @@ export function RosterMonthPage({ now: pinnedNow }: { readonly now?: Date } = {}