Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7060b06
Design: fix layout bugs across Clinical and Work, add On Call and Ros…
claude Oct 15, 2026
88f2dc4
CPD: keep Log an activity when a year's entries total 0 h
claude Oct 15, 2026
99b13ca
Work hero pills: use the pill radius token and a border, per the desi…
claude Oct 15, 2026
76a381c
fix: Use the shell-owned dock reserve
qodo-code-review[bot] Oct 15, 2026
ff8a7aa
fix: 2 findings — Show the selected calling route; Show a neutral title
qodo-code-review[bot] Oct 15, 2026
b83296d
fix: Select a future shift for Next
qodo-code-review[bot] Oct 15, 2026
6c7a4c9
fix: Draw a visible CPD focus ring
qodo-code-review[bot] Oct 15, 2026
0e96c88
fix: Allow emergency routes to wrap safely
qodo-code-review[bot] Oct 15, 2026
5ed2132
fix: Only show None listed for empty sections
qodo-code-review[bot] Oct 15, 2026
1b492d8
test: pass rosterLoading to NowOnCallHero in its DOM test
cubic-dev-ai[bot] Oct 15, 2026
539a538
Review bot fixes: keep DSM results padding, accurate hero and section…
claude Oct 15, 2026
60f68db
Merge remote-tracking branch 'origin/claude/unified-design-system-6zl…
claude Oct 15, 2026
5082141
Roster hero: find the next shift among all loaded shifts, and load th…
claude Oct 15, 2026
f266cfe
DSM search: drop the page-local dock pad, the shell reserve already c…
claude Oct 15, 2026
ea09761
On Call hero: say the roster couldn't load instead of claiming no shi…
claude Oct 15, 2026
5f86d76
Phone focus: re-reveal a Tab-focused control while layout settles aft…
claude Oct 15, 2026
176d3c3
On Call hero asks a signed-out reader to sign in, and phone focus sto…
claude Oct 15, 2026
af15fbe
Phone focus: end the settle window on a key press, so Enter on an in-…
claude Oct 15, 2026
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
55 changes: 55 additions & 0 deletions src/app/work-mode.css
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,61 @@ a.work-hero:focus-visible {
margin-top: calc(var(--spacing) * 2.5);
}

/* The hero's actions (Work standard, owner 15 Oct 2026): the main action is a
white rounded pill in the mode colour, any second one a quiet tint of the
hero's own ink. 48px tall, the production tap target. In dark themes the pill
takes the heading ink as its fill (see --work-hero-action-fill). */
.work-hero__actions {
display: flex;
flex-wrap: wrap;
gap: calc(var(--spacing) * 2);
}

.work-hero__action {
display: inline-flex;
min-height: 3rem;
min-width: 0;
align-items: center;
justify-content: center;
gap: calc(var(--spacing) * 1.5);
padding: 0 calc(var(--spacing) * 4.5);
border: 0;
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;
font-size: var(--text-sm, 0.875rem);
font-weight: 700;
text-decoration: none;
white-space: nowrap;
cursor: pointer;
}

.work-hero__action[data-variant="quiet"] {
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 14%, transparent);
color: inherit;
}

.work-hero__action:focus-visible {
outline: 2px solid var(--work-hero-action-fill, var(--surface-raised));
outline-offset: 2px;
}

/* Holds the pill's place while its number loads, so nothing moves under the thumb. */
.work-hero__action[data-placeholder] {
width: 10rem;
background: color-mix(in srgb, currentColor 14%, transparent);
}

@media (forced-colors: active) {
.work-hero__action {
border: 1px solid ButtonText;
background: ButtonFace;
color: ButtonText;
}
}

@media (forced-colors: active) {
.work-hero {
border: 1px solid CanvasText;
Expand Down
29 changes: 21 additions & 8 deletions src/components/cme/cme-dashboard-catch-up.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,19 @@ export function CmeCategoryLegend({
);
}

/**
* "· 1 h today" after the year label. It wraps as one unit (nowrap), so a narrow
* phone never leaves "1 H" on one line and "TODAY" on the next.
*/
function TodayNote({ hours }: { readonly hours: number }) {
return (
<>
{" "}
<span data-testid="cme-audited-today" className="whitespace-nowrap">{`· ${formatHours(hours)} h today`}</span>
</>
);
}

export type CmeYearSummaryProps = {
readonly year: number;
/** Perth calendar date, `YYYY-MM-DD`. */
Expand All @@ -278,6 +291,8 @@ export type CmeYearSummaryProps = {
readonly onOpenGap: () => void;
/** The empty variant's one action ("Log your first activity"). */
readonly firstLogAction?: ReactNode;
/** "Log an activity", drawn as a full-width white pill at the foot of the card once something is logged. */
readonly logAction?: ReactNode;
/** Hours logged today, added to the kicker when above zero ("· 1 h today"): it replaced "About this year" (12 Oct 2026). */
readonly todayHours?: number;
/** Customise's "Year dates": off leaves the kicker as the bare year (a closed year still says so). */
Expand All @@ -299,7 +314,7 @@ export type CmeYearSummaryProps = {
*/
export function CmeYearSummary(props: CmeYearSummaryProps) {
const { year, today, loggedHours, targetHours, categoryHours, plan, weeklyHours, entries, closed, onOpenGap } = props;
const { firstLogAction, todayHours = 0, showTimeLeft = true } = props;
const { firstLogAction, logAction, todayHours = 0, showTimeLeft = true } = props;

const label = showTimeLeft || closed ? cmeYearLabel({ year, today, closed }) : String(year);

Expand All @@ -308,9 +323,7 @@ export function CmeYearSummary(props: CmeYearSummaryProps) {
<section data-testid="cme-year-summary" aria-label={`CPD hours for ${year}`} className={SUMMARY_CARD}>
<div className={`${LABEL} nums`}>
<span data-testid="cme-year-label">{label}</span>
{todayHours > 0 ? (
<span data-testid="cme-audited-today">{` · ${formatHours(todayHours)} h today`}</span>
) : null}
{todayHours > 0 ? <TodayNote hours={todayHours} /> : null}
</div>
<p data-testid="cme-total-hours" className="m-0 flex items-baseline gap-1.5">
{loggedHours > 0 ? (
Expand All @@ -332,6 +345,7 @@ export function CmeYearSummary(props: CmeYearSummaryProps) {
>
Set up your year
</Link>
{logAction}
</section>
);
}
Expand All @@ -356,7 +370,7 @@ export function CmeYearSummary(props: CmeYearSummaryProps) {
<p data-testid="cme-empty-target-line" className="work-empty__body">
{`Your target is ${formatHours(targetHours)} h for ${year}. Log your first activity and it shows here.`}
</p>
{firstLogAction}
{firstLogAction ?? logAction}
</div>
</section>
);
Expand All @@ -373,9 +387,7 @@ export function CmeYearSummary(props: CmeYearSummaryProps) {
<div className="min-w-0">
<div className="cpd-hero-kicker nums">
<span data-testid="cme-year-label">{label}</span>
{todayHours > 0 ? (
<span data-testid="cme-audited-today">{` · ${formatHours(todayHours)} h today`}</span>
) : null}
{todayHours > 0 ? <TodayNote hours={todayHours} /> : null}
</div>
{/* Read as one sentence: "32.5 of 50 hours logged, 65 percent". */}
<p data-testid="cme-total-hours" className="cpd-hero-big m-0" role="img" aria-label={spoken}>
Expand All @@ -388,6 +400,7 @@ export function CmeYearSummary(props: CmeYearSummaryProps) {
<SummaryBar categoryHours={categoryHours} routineHours={routineHours} scale={scale} />
<HeroLegend categoryHours={categoryHours} plan={plan} onOpenGap={onOpenGap} />
<CmePacePanel plan={plan} weeklyHours={weeklyHours} entries={entries} year={year} onOpenGap={onOpenGap} />
{logAction ? <div className="cpd-hero-log">{logAction}</div> : null}
</section>
);
}
40 changes: 22 additions & 18 deletions src/components/cme/cme-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -344,15 +346,24 @@ export function CmeDashboard({
: { kind: "loaded", at: now }
}
/>
{underBand ? (
<CmePageAction icon={SlidersHorizontal} label="Customise" href="/cme/customise" testId="cme-customise-action" />
) : (
<div className="flex justify-end">
<Button variant="toolbar" size="sm" icon={Settings2} onClick={onOpenCustomise}>
Customise
</Button>
</div>
)}
{/* 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. */}
<div data-page-width="wide" data-page-padded="" className="mx-auto box-border w-full px-3">
{underBand ? (
<CmePageAction
icon={SlidersHorizontal}
label="Customise"
href="/cme/customise"
testId="cme-customise-action"
/>
) : (
<div className="flex justify-end">
<Button variant="toolbar" size="sm" icon={Settings2} onClick={onOpenCustomise}>
Customise
</Button>
</div>
)}
</div>
</>
);

Expand Down Expand Up @@ -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")}
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
todayHours={shows("audited-today") ? loggedTodayHours : 0}
showTimeLeft={shows("year-dates")}
/>
Expand Down Expand Up @@ -622,13 +634,6 @@ export function CmeDashboard({
</CmeGroup>
) : null;

// The dock: the one filled button. Before anything is logged it lives in the empty card instead.
const dock = nothingLogged ? null : (
<div className="work-dock" role="group" aria-label="Actions">
<div className="work-dock__capsule">{logLink("Log an activity", "work-button min-h-tap")}</div>
</div>
);

return (
<main className="w-full" data-testid="cme-year-page">
<div className="contents">{status}</div>
Expand All @@ -653,7 +658,6 @@ export function CmeDashboard({
{trainingLink}
{provenance}
</div>
{dock}
</WorkColumns>
</div>

Expand Down
5 changes: 4 additions & 1 deletion src/components/cme/cme-log-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,10 @@ export function CmeLogPage({
</p>
) : 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. */}
<div className="cpd-dock-reserve grid min-w-0 content-start gap-[calc(var(--spacing)*2.25)]">
{showFinish ? finish : activities}
</div>

<div
className="work-dock"
Expand Down
35 changes: 35 additions & 0 deletions src/components/cme/cme-work.css
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,31 @@ html.dark[data-mode-identity="cme"] {
gap: calc(var(--spacing) * 2.5);
}

/* "Log an activity" at the foot of the hero: a full-width white pill in the mode
colour (Teaching's hero action), replacing the Year page's floating dock. */
.cpd-hero-log {
margin-top: calc(var(--spacing) * 3);
}

.work-hero .cpd-hero-log > .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 var(--work-hero-action-fill, var(--surface-raised));
outline-offset: 2px;
}
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved

.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;
Expand Down Expand Up @@ -954,6 +979,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 {
Expand Down
9 changes: 8 additions & 1 deletion src/components/differentials/differential-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,15 @@ function SectionRow({
<div className="min-w-0">
<h3 className="text-base font-bold text-[color:var(--text-heading)]">{section.title}</h3>
{scopeNote ? <ScopeNote label={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. */}
<p className="mt-1 text-xs leading-5 text-[color:var(--text-muted)] sm:text-sm sm:leading-6">
{section.summary}
{section.summary.trim() ||
// 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")}
Comment thread
gitar-bot[bot] marked this conversation as resolved.
Resolved
</p>
</div>
<span className="hidden justify-self-end text-xs font-semibold text-[color:var(--text-muted)] sm:block">
Expand Down
4 changes: 3 additions & 1 deletion src/components/dsm/dsm-compare-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 2 additions & 0 deletions src/components/dsm/dsm-search-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,8 @@ export function DsmSearchPage({

return (
<div data-testid="dsm-search-page" className="min-h-full bg-[color:var(--background)]">
{/* Phone dock clearance comes from the shell's own reserve, which also
collapses when the pill hides, so this page adds no bottom pad. */}
<div className={cn(pageContainer, "space-y-4 px-4 py-4 sm:px-6 sm:py-6 lg:px-8")}>
<SearchResultsHeaderBand
modeId="dsm"
Expand Down
2 changes: 1 addition & 1 deletion src/components/favourites/favourites-shelf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ function TileFace({ item }: { item: FavouriteItem }) {
) : null}
</span>
<span className="block w-full min-w-0">
<span className="line-clamp-2 break-words text-xs font-bold leading-tight text-[color:var(--work-ink)]">
<span className="line-clamp-2 break-words hyphens-auto text-xs font-bold leading-tight text-[color:var(--work-ink)]">
{label}
</span>
<span
Expand Down
1 change: 0 additions & 1 deletion src/components/formulation/formulation-builder-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,6 @@ export function FormulationBuilderPage({
<FormulationPageShell>
<InformationPageHeader
className="border-b border-[color:var(--border)] pb-5"
eyebrow="Formulation builder"
title="Build a formulation that can be tested"
subtitle="Select candidate mechanisms, structure them with a clinical framework, challenge the hypothesis, then edit a de-identified draft."
/>
Expand Down
5 changes: 4 additions & 1 deletion src/components/mode-home-start-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,10 @@ function ModeHomeStartListBody({ modeId, config }: { modeId: StartListMode; conf
{config.favouriteType ? (
<FavouritesGroupWhenAccount modeId={modeId} favouriteType={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 ? (
<ModeHomeStartGroup id={`${modeId}-home-browse`} title="Browse" testId="mode-home-browse-list">
{config.browse.slice(0, EMPTY_HOME_BROWSE_SHOWN).map((link) => (
<ModeHomeStartRow key={link.href} href={link.href} icon={LayoutList} title={link.label} />
Expand Down
9 changes: 3 additions & 6 deletions src/components/my-day/my-day-today-cards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
<HeroTop
ring={
<HeroRing
figure={left.figure}
word={left.rest ? `${left.rest} left` : "left"}
fraction={(at - start) / Math.max(1, end - start)}
/>
// The title already says the time left, so the ring shows how much of the shift is done.
<HeroRing figure={`${Math.round(done * 100)}%`} word="done" fraction={done} />
}
eyebrow={`${words.eyebrow}${asOf}`}
title={words.big}
Expand Down
16 changes: 13 additions & 3 deletions src/components/on-call/now/emergency-pin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,21 @@ 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")}
>
<Phone aria-hidden="true" strokeWidth={1.75} className="size-icon-md shrink-0" />
<span className={cn(modeNumberText, "font-semibold")}>{primary.display}</span>
{primaryWhere ? <span className="text-sm font-normal">{primaryWhere}</span> : null}
{/* Keep each dialable segment intact while allowing a long route to wrap. */}
<span className="grid min-w-0 text-left">
<span className={cn(modeNumberText, "font-semibold")}>
{primary.display.split(", then ext ").map((segment, index) => (
<span key={segment} className="whitespace-nowrap">
{index > 0 ? ", then ext " : null}
{segment}
</span>
))}
</span>
{primaryWhere ? <span className="text-sm font-normal">{primaryWhere}</span> : null}
</span>
</a>
) : null}
{dial && (deskOnly || !primary?.tel) && !extension ? (
Expand Down
Loading
Loading