Skip to content
Merged
31 changes: 31 additions & 0 deletions src/app/work-mode.css
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,11 @@ body {
gap: calc(var(--spacing) * 2.25);
}

/* A column with nothing in it takes no gap; a dock spans every column. */
.work-columns > :empty {
display: none;
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Resolved
}

@media (min-width: 1024px) {
.work-split {
grid-template-columns: minmax(17rem, 22rem) minmax(0, 1fr);
Expand All @@ -259,6 +264,32 @@ body {
.work-columns[data-columns="3"] {
grid-template-columns: repeat(3, minmax(0, 1fr));
}

/* Keep a remaining dock in the first column when the right column is empty. */
.work-columns:has(> :nth-child(2):empty) {
grid-template-columns: minmax(0, 1fr);
}
}

/* The CPD log's filter column (from 1024px, desktop layout audit 11 Oct 2026):
it holds every filter, so the chip row over the list steps aside, the
column's chips wrap rather than cut off mid-word, and the Log button sits
over the list instead of across the column. */
@media (min-width: 1024px) {
[data-filter-column] .work-chips {
flex-wrap: wrap;
row-gap: calc(var(--spacing) * 1.5);
overflow-x: visible;
mask-image: none;
}

.work-chips[data-beside-filters] {
display: none;
}

.work-dock[data-beside-filters] {
margin-inline-start: calc(var(--spacing) * 72);
}
}

/* ------------------------------------------------------------ the band */
Expand Down
14 changes: 13 additions & 1 deletion src/components/clinical-dashboard/ClinicalSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import dynamic from "next/dynamic";
import {
useEffect,
useRef,
Expand Down Expand Up @@ -60,14 +61,25 @@ import {
ClinicalMenuWorkAreas,
clinicalMenuTabId,
} from "@/components/clinical-dashboard/clinical-menu-drawer";
import { LazyWorkSideCounts } from "@/components/work-frame/lazy-work-side-nav";
import { useWorkSideCounts } from "@/components/work-frame/work-frame-store";
import { useNewWorkMode } from "@/components/work-mode-launch/work-mode-launch-provider";
import { needsYouBellVisibleForAuth } from "@/lib/needs-you/homes";
import type { ModeMenuSideId } from "@/lib/phone-mode-groups";
import { useAuthSession } from "@/lib/supabase/client";
import type { WorkAreaId } from "@/lib/work-frame/areas";

// The counts reader is loaded straight from its own file. Going through
// work-frame/lazy-work-side-nav would pull in the two-pane menu, which imports
// this file back and makes an import cycle.
const LazyCountsReader = dynamic(
() => import("@/components/work-frame/work-side-counts-reader").then((module) => module.WorkSideCountsReader),
{ ssr: false },
);

function LazyWorkSideCounts({ active }: { readonly active: boolean }) {
return active ? <LazyCountsReader /> : null;
}

export type SidebarIdentity = {
displayName: string;
initials: string;
Expand Down
2 changes: 1 addition & 1 deletion src/components/cme/cme-dashboard-whats-left.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ export function CmeWhatsLeft({
end={
// A short word: held to the 48 px tap width, its words kept at the right edge.
<CmeTextLink href={`/cme/check?year=${set.year}`} testId="cme-year-check-link" className="min-w-12 justify-end">
Report
End of year
</CmeTextLink>
}
>
Expand Down
42 changes: 24 additions & 18 deletions src/components/cme/cme-dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { CME_LOG_TRIGGER_ATTRIBUTE, openCmeQuickLog } from "@/components/cme/cme
import { useCmeTeachingUnloggedCount } from "@/components/cme/cme-teaching-prompt";
import { CmeBandAction, CmeKvCard, cmeFreshnessEyebrow } from "@/components/cme/cme-work-kit";
import { CmeYearInWeeks } from "@/components/cme/cme-year-in-weeks";
import { WorkBody } from "@/components/mode-kit/work";
import { WorkColumns } from "@/components/work-frame/work-desktop-layout";
import { Button } from "@/components/ui/button";
import { NewWorkModeOnly } from "@/components/work-mode-launch/work-mode-launch-provider";
import { addDays, expandEvents } from "@/lib/calendar/calendar-event";
Expand Down Expand Up @@ -607,15 +607,15 @@ export function CmeDashboard({
/>,
);
}
// "What's left" carries the only other link to the Report; when it is hidden this row keeps the
// Report (exports, closing the year, Customise) one tap away.
// "What's left" carries the only other link to End of year; when it is hidden this row keeps
// End of year (what is missing, evidence, handing it in) one tap away.
if (!showWhatsLeft) {
aboutRows.push(
<CmeFlatRow
key="check"
testId="cme-about-year-check"
title="Report"
subtitle="The year check, exports and closing the year"
title="End of year"
subtitle="What is missing, evidence to tidy, and handing it in"
href={`/cme/check?year=${set.year}`}
/>,
);
Expand Down Expand Up @@ -660,20 +660,26 @@ export function CmeDashboard({
<main className="w-full" data-testid="cme-year-page">
<div className="contents">{status}</div>
<div data-mode-identity="cme" data-testid="cme-year-body" className="min-w-0">
<WorkBody>
{summary}
{nextStepNode}
{toLog}
{whatsLeft}
{chipsNode}
<NewWorkModeOnly>
<ApplicationsTodayCard today={today} />
</NewWorkModeOnly>
{byCategory}
{alsoForYou}
{about}
{/* One column on a phone, in this order. From 1024px the year and what is left sit on the left, and
the dates and the year's details on the right (11 Oct 2026 layout audit). */}
<WorkColumns>
<div>
{summary}
{nextStepNode}
{toLog}
{whatsLeft}
{chipsNode}
</div>
<div>
<NewWorkModeOnly>
<ApplicationsTodayCard today={today} />
</NewWorkModeOnly>
{byCategory}
{alsoForYou}
{about}
</div>
{dock}
Comment thread
BigSimmo marked this conversation as resolved.
Resolved
</WorkBody>
</WorkColumns>
</div>

<CmeTodayDetailSheet
Expand Down
24 changes: 20 additions & 4 deletions src/components/cme/cme-log-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { useCmeTeachingUnloggedCount } from "@/components/cme/cme-teaching-promp
import { CmeBandAction, CmeBanner, CmeHint, CmeNoteLine } from "@/components/cme/cme-work-kit";
import { useModeBandHeading } from "@/components/mode-band/mode-band";
import { useWorkUndoToast, WorkBody } from "@/components/mode-kit/work";
import { WorkWidePage } from "@/components/work-frame/work-desktop-layout";
import { Sheet } from "@/components/ui/sheet";
import { SearchField } from "@/components/ui/text-field";
import { cn, InlineNotice } from "@/components/ui-primitives";
Expand Down Expand Up @@ -367,6 +368,7 @@ export function CmeLogPage({
<aside
aria-label="Filter your log"
data-testid="cme-log-filter-column"
data-filter-column=""
className="hidden lg:sticky lg:top-4 lg:block"
>
<CmeLogFilterPanel
Expand Down Expand Up @@ -402,7 +404,13 @@ export function CmeLogPage({
</button>
</div>

<div role="group" aria-label="Narrow the log" className="work-chips flex-wrap">
{/* From 1024px the side column holds the year, category and audit filters, so the chip row would repeat them. */}
<div
role="group"
aria-label="Narrow the log"
className="work-chips flex-wrap"
data-beside-filters={wide ? "" : undefined}
>
{yearChip}
<button
ref={categoryButtonRef}
Expand Down Expand Up @@ -626,6 +634,9 @@ export function CmeLogPage({
</>
);

// The log with its filter column is a wide page from 1024px, so the list keeps a readable width; To finish stays a reading page.
const LogBody = showFinish ? WorkBody : WorkWidePage;

return (
<main data-testid="cme-log-page" data-mode-identity="cme" className="w-full">
{showFinish ? null : (
Expand All @@ -636,7 +647,7 @@ export function CmeLogPage({
testId="cme-log-filter-action"
/>
)}
<WorkBody>
<LogBody>
<PageTitleUnderBand className={cmePageTitle}>{showFinish ? "To finish" : "Log"}</PageTitleUnderBand>
<div role="status" data-testid="cme-log-saved" className="contents">
{justSaved ? <CmeBanner icon={Check}>Saved to your log.</CmeBanner> : null}
Expand All @@ -657,7 +668,12 @@ export function CmeLogPage({

{showFinish ? finish : activities}

<div className="work-dock" role="group" aria-label="Log actions">
<div
className="work-dock"
role="group"
aria-label="Log actions"
data-beside-filters={wide && !showFinish ? "" : undefined}
>
<div className="work-dock__capsule">
<Link
href={`/cme/new?year=${set.year}`}
Expand All @@ -683,7 +699,7 @@ export function CmeLogPage({
) : null}
</div>
</div>
</WorkBody>
</LogBody>

{!showFinish ? (
<>
Expand Down
17 changes: 8 additions & 9 deletions tests/challenger-m4-2-empirical.dom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,15 @@ describe("Empirical Challenger M4-2 Verification Suite", () => {
expect(diffContent).toContain("pb-[calc(6.25rem+var(--safe-area-bottom))]");
});

// The CPD Year page now sits in the shared WorkBody, which owns the home-bar
// clearance (work-mode redesign, owner request 6 Oct 2026).
it("verifies var(--safe-area-bottom) clears the CPD Year page through WorkBody", () => {
expect(cmeContent).toContain("<WorkBody>");
// The CPD Year page sits in the desktop frame's WorkColumns (desktop layout audit,
// 11 Oct 2026), whose shared rule owns the home-bar clearance like WorkBody's.
it("verifies var(--safe-area-bottom) clears the CPD Year page through WorkColumns", () => {
expect(cmeContent).toContain("<WorkColumns>");
const workCss = readFileSync(resolve(process.cwd(), "src/app/work-mode.css"), "utf8");
const workBodyRule = workCss.slice(
workCss.indexOf(".work-body {"),
workCss.indexOf("}", workCss.indexOf(".work-body {")),
);
expect(workBodyRule).toContain("var(--safe-area-bottom, 0px)");
const start = workCss.indexOf(".work-columns {");
const columnsRule = workCss.slice(start, workCss.indexOf("}", start));
expect(start).toBeGreaterThan(-1);
expect(columnsRule).toContain("var(--safe-area-bottom, 0px)");
});

it("confirms zero raw env(safe-area-inset-bottom) occurrences in differential presentation and cme dashboard", () => {
Expand Down
14 changes: 10 additions & 4 deletions tests/ui-smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,9 @@ test.describe("PsychSift UI smoke coverage", () => {
await waitForDemoDashboardReady(page);

const menu = await openMobileClinicalGuideMenu(page);
await menu.getByRole("link", { name: "Medication" }).click();
// The menu lists Medication twice by design: once as a pinned shortcut and once
// under its group. The shortcut is the one this test is about.
await menu.getByRole("navigation", { name: "Pinned shortcuts" }).getByRole("link", { name: "Medication" }).click();

await expect.poll(() => new URL(page.url()).pathname, { timeout: 30_000 }).toBe("/");
await expect.poll(() => new URL(page.url()).searchParams.get("mode"), { timeout: 30_000 }).toBe("prescribing");
Expand Down Expand Up @@ -1568,7 +1570,7 @@ test.describe("PsychSift UI smoke coverage", () => {
await moreModesSheet.getByRole("button", { name: "Close more modes" }).click();
expect(
await navigation.getByRole("link").evaluateAll((links) => links.map((link) => link.getAttribute("aria-label"))),
).toEqual(["My Day", "Answer", "Documents", "Services", "Medication", "Factsheets", "Forms", "Tools"]);
).toEqual(["My Day", "Answer", "Documents", "Services", "Psychiatry", "Medication", "Forms", "Factsheets"]);
await expect(rail.getByTestId("sidebar-more-modes")).toBeVisible();
await expect(rail.getByTestId("sidebar-more-modes")).toBeFocused();

Expand All @@ -1584,14 +1586,18 @@ test.describe("PsychSift UI smoke coverage", () => {
{ path: "/documents", label: "Documents" },
{ path: "/favourites", label: "Favourites" },
{ path: "/medications", label: "Medication" },
{ path: "/tools", label: "Tools" },
// Tools left the menu (usefulness and layout audit, 11 Oct 2026); Psychiatry,
// which now holds Calculators and Dictionary, is pinned in its place.
{ path: "/psychiatry", label: "Psychiatry" },
] as const) {
await gotoApp(page, route.path);
if (route.path.includes("mode=answer")) {
await waitForDemoDashboardReady(page);
}

const activeLink = page.getByRole("link", { name: route.label, exact: true });
const activeLink = page
.getByLabel("PsychSift collapsed sidebar")
.getByRole("link", { name: route.label, exact: true });
await expect(activeLink).toBeVisible();
await expect(activeLink).toHaveAttribute("aria-current", "page");
}
Expand Down
3 changes: 2 additions & 1 deletion tests/ui-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,8 @@ test.describe("PsychSift tools directory and legacy launcher", () => {
await expect(servicesMenu.getByRole("menuitemradio", { name: /^Forms\b/ })).toBeVisible();
await expect(servicesMenu.getByRole("menuitemradio", { name: /^Differentials\b/ })).toBeVisible();
await expect(servicesMenu.getByRole("menuitemradio", { name: /^Medication\b/ })).toBeVisible();
await expect(servicesMenu.getByRole("menuitemradio", { name: /^Tools\b/ })).toBeVisible();
// Tools left the mode menu (usefulness and layout audit, 11 Oct 2026).
await expect(servicesMenu.getByRole("menuitemradio", { name: /^Tools\b/ })).toHaveCount(0);
const formsMode = servicesMenu.getByRole("menuitemradio", { name: /^Forms\b/ });
await waitForReactEventHandler(formsMode);
await formsMode.click();
Expand Down
Loading