From e836f3680f3399e7595561227747c26d394593c7 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:16:08 +0800 Subject: [PATCH 1/3] feat(ux): labelled sidebar default, Forms early-access signalling, mockup tile a11y (PT-07, PT-10, PT-15) New users now get the labelled (expanded) desktop sidebar: eight icon-only destinations demand recall and hover; collapsing remains a remembered per-browser choice, and specs that exercise the collapsed rail seed that preference explicitly. Tablet keeps the icon rail. Forms is a four-record starter catalogue, so signal maturity honestly: an "Early access" pill on the sidebar entry (spoken in the collapsed rail label too) and an early-access line in the Forms home subtitle. Give the split-pane mockup's selectable tool cards a concise accessible name ("Open ") matching their link-rendered siblings, and tighten the spec to assert it. Co-Authored-By: Claude Fable 5 --- .../clinical-dashboard/ClinicalSidebar.tsx | 16 +++++++++++++--- .../use-sidebar-collapsed.ts | 8 +++++--- src/components/forms/forms-home-page.tsx | 2 +- tests/ui-smoke.spec.ts | 19 +++++++++++++++++++ tests/ui-tools.spec.ts | 3 +++ 5 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index ca207e0c1..995ac23c4 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -71,13 +71,18 @@ const sidebarToolItems = [ { id: "answer", label: "Answer", icon: Sparkles, href: "/?mode=answer" }, { id: "documents", label: "Documents", icon: FileText, href: "/?mode=documents" }, { id: "services", label: "Services", icon: appModeIcons.services, href: "/services" }, - { id: "forms", label: "Forms", icon: ClipboardPen, href: "/forms" }, + // badge = catalogue-maturity pill: the Forms registry is a small starter set. + { id: "forms", label: "Forms", icon: ClipboardPen, href: "/forms", badge: "Early access" }, { id: "favourites", label: "Favourites", icon: Heart, href: "/favourites" }, { id: "differentials", label: "Differentials", icon: BrainCircuit, href: "/differentials" }, { id: "prescribing", label: "Medications", icon: Pill, href: "/?mode=prescribing" }, { id: "tools", label: "Tools", icon: Wrench, href: "/?mode=tools" }, ] as const; +function sidebarItemBadge(item: (typeof sidebarToolItems)[number]): string | undefined { + return "badge" in item ? item.badge : undefined; +} + // Display-free base so callers can compose `grid` / `hidden lg:grid` without // conflicting display utilities (cn does not de-duplicate classes). const collapsedSidebarControl = @@ -251,6 +256,11 @@ export function ClinicalSidebarContent({ )} /> {item.label} + {sidebarItemBadge(item) ? ( + + {sidebarItemBadge(item)} + + ) : null} ); })} @@ -420,8 +430,8 @@ function ClinicalCollapsedRail({ onFocus={item.id === "tools" ? onPrefetchApplications : undefined} onPointerEnter={item.id === "tools" ? onPrefetchApplications : undefined} className={cn(collapsedSidebarButton, active && collapsedSidebarActiveButton)} - aria-label={item.label} - title={item.label} + aria-label={sidebarItemBadge(item) ? `${item.label} (${sidebarItemBadge(item)})` : item.label} + title={sidebarItemBadge(item) ? `${item.label} (${sidebarItemBadge(item)})` : item.label} aria-current={active ? "page" : undefined} > diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts index daf22d938..eb6d6597a 100644 --- a/src/components/clinical-dashboard/use-sidebar-collapsed.ts +++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts @@ -8,14 +8,16 @@ const changeEvent = "clinical-kb-sidebar-collapsed-change"; function getSnapshot() { try { const storedValue = window.localStorage.getItem(storageKey); - return storedValue === null ? true : storedValue === "1"; + // New users get the labelled (expanded) sidebar: eight icon-only + // destinations demand recall/hover; collapsing stays a remembered choice. + return storedValue === null ? false : storedValue === "1"; } catch { - return true; + return false; } } function getServerSnapshot() { - return true; + return false; } function subscribe(onChange: () => void) { diff --git a/src/components/forms/forms-home-page.tsx b/src/components/forms/forms-home-page.tsx index a5a6be5e7..7a20634d1 100644 --- a/src/components/forms/forms-home-page.tsx +++ b/src/components/forms/forms-home-page.tsx @@ -114,7 +114,7 @@ export function FormsHomePage() { { await expect(page.getByTestId("global-search-input")).toBeEnabled(); }); + test("desktop sidebar defaults to the labelled state for new users", async ({ page }) => { + await page.setViewportSize({ width: 1280, height: 900 }); + await mockDemoApi(page); + await gotoApp(page, "/?mode=answer"); + await waitForDemoDashboardReady(page); + + // No stored preference (PT-10): eight icon-only destinations demand recall, + // so first-run desktop shows the labelled sidebar; collapse is remembered. + await expect(page.locator("#clinical-tools-sidebar")).toBeVisible(); + await expect(page.getByRole("button", { name: "Collapse sidebar" })).toBeVisible(); + await expect(page.getByRole("button", { name: "Expand sidebar" })).toHaveCount(0); + }); + test("desktop sidebar mode sync and accessibility affordances stay coherent", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); + // This journey exercises the remembered-collapsed rail; new users now + // default to the labelled sidebar, so seed the stored preference. + await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoApp(page, "/?mode=tools"); const sidebar = page.locator("#clinical-tools-sidebar"); @@ -1018,6 +1034,9 @@ test.describe("Clinical KB UI smoke coverage", () => { }) => { await page.setViewportSize({ width: 1280, height: 900 }); await mockDemoApi(page); + // Exercises both collapsed and expanded account affordances; seed the + // remembered-collapsed preference now that new users default to labelled. + await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoApp(page, "/"); await waitForDemoDashboardReady(page); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 55801e9e3..d7e54176c 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -376,6 +376,9 @@ test.describe("Clinical KB tools launcher", () => { test("mode toggle stays global on the services home route", async ({ page }) => { await page.setViewportSize({ width: 1280, height: 900 }); + // Asserts the collapsed rail affordance below; seed the remembered + // preference now that new users default to the labelled sidebar. + await page.addInitScript(() => window.localStorage.setItem("clinical-kb-sidebar-collapsed", "1")); await gotoLauncher(page, "/?mode=answer"); // Re-open + re-click on each retry: a single click can be swallowed while the From fba18eb02acbcf5545e7aa3dd6baed3a8a3bc594 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:41:25 +0800 Subject: [PATCH 2/3] test(ui): align specs with labelled sidebar default and Forms badge The expanded sidebar's "Search recent chats" box becomes the first searchbox on shell-wrapped pages, so mockup collapse specs scope their search to #main-content. The tablet rail assertion moves from DOM-absence to hidden (the expanded panel now exists but stays display:none below lg), and the rail's Forms link is asserted with its spoken "Forms (Early access)" name. Co-Authored-By: Claude Fable 5 --- tests/ui-smoke.spec.ts | 7 +++++-- tests/ui-tools-collapse.spec.ts | 13 ++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index b962ca7cc..6337332f7 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -952,14 +952,17 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("button", { name: "Open Clinical Guide menu" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Expand sidebar" })).toHaveCount(0); - await expect(page.locator("#clinical-tools-sidebar")).toHaveCount(0); + // With the labelled default the expanded panel exists in the DOM but stays + // display:none below lg; tablet must still only present the icon rail. + await expect(page.locator("#clinical-tools-sidebar")).toBeHidden(); await expect(page.getByLabel("Clinical Guide collapsed sidebar")).toBeVisible(); for (const tool of [ { name: "Answer", href: "/?mode=answer" }, { name: "Documents", href: "/?mode=documents" }, { name: "Services", href: "/services" }, - { name: "Forms", href: "/forms" }, + // The rail speaks the catalogue-maturity badge as part of the Forms name. + { name: "Forms (Early access)", href: "/forms" }, { name: "Favourites", href: "/favourites" }, { name: "Differentials", href: "/differentials" }, { name: "Medications", href: "/?mode=prescribing" }, diff --git a/tests/ui-tools-collapse.spec.ts b/tests/ui-tools-collapse.spec.ts index b174aea09..10bb33693 100644 --- a/tests/ui-tools-collapse.spec.ts +++ b/tests/ui-tools-collapse.spec.ts @@ -9,6 +9,13 @@ async function goto(page: Page, path: string) { await page.waitForLoadState("networkidle", { timeout: 15_000 }).catch(() => undefined); } +// The shell's expanded sidebar (now the desktop default) contributes its own +// "Search recent chats" searchbox, so mockup searches must be scoped to the +// page content instead of grabbing the first searchbox on the page. +function mockupSearch(page: Page) { + return page.locator("#main-content").getByRole("searchbox").first(); +} + test.describe("Tools mockups collapse the primary region when filtering", () => { test.describe.configure({ timeout: 60_000 }); @@ -18,7 +25,7 @@ test.describe("Tools mockups collapse the primary region when filtering", () => await expect(page.getByRole("heading", { name: "Start here" })).toBeVisible(); - const search = page.getByRole("searchbox").first(); + const search = mockupSearch(page); await search.fill("medication"); await expect(page.getByRole("heading", { name: "Start here" })).toHaveCount(0); await expect(page.getByLabel("Open Medication Prescribing")).toBeVisible(); @@ -36,7 +43,7 @@ test.describe("Tools mockups collapse the primary region when filtering", () => await expect(page.getByRole("heading", { name: "Resume" })).toBeVisible(); await expect(page.getByRole("heading", { name: "Assess" })).toBeVisible(); - await page.getByRole("searchbox").first().fill("medication"); + await mockupSearch(page).fill("medication"); await expect(page.getByRole("heading", { name: "Assess" })).toHaveCount(0); await expect(page.getByLabel("Open Medication Prescribing")).toBeVisible(); }); @@ -47,7 +54,7 @@ test.describe("Tools mockups collapse the primary region when filtering", () => await expect(page.getByRole("heading", { name: "Launcher overview" })).toBeVisible(); - await page.getByRole("searchbox").first().fill("medication"); + await mockupSearch(page).fill("medication"); await expect(page.getByRole("heading", { name: "Launcher overview" })).toHaveCount(0); await expect(page.getByRole("heading", { name: "Results" })).toBeVisible(); // The split-pane card is a button; it must expose an explicit accessible From e0a745abe5c17ab7caa059dfd6d069c41fbd4e80 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 15:08:23 +0000 Subject: [PATCH 3/3] fix: apply CodeRabbit auto-fixes Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit --- .../clinical-dashboard/use-sidebar-collapsed.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts index eb6d6597a..574a82c31 100644 --- a/src/components/clinical-dashboard/use-sidebar-collapsed.ts +++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts @@ -5,7 +5,16 @@ import { useCallback, useSyncExternalStore } from "react"; const storageKey = "clinical-kb-sidebar-collapsed"; const changeEvent = "clinical-kb-sidebar-collapsed-change"; +// In-memory fallback when localStorage writes fail (e.g. private browsing mode). +// Null means no fallback needed; storage is the source of truth. +let inMemoryFallback: boolean | null = null; + function getSnapshot() { + // When storage writes have failed, the in-memory fallback takes precedence + // so the UI reflects the user's toggle even when persistence is unavailable. + if (inMemoryFallback !== null) { + return inMemoryFallback; + } try { const storedValue = window.localStorage.getItem(storageKey); // New users get the labelled (expanded) sidebar: eight icon-only @@ -39,8 +48,13 @@ export function useSidebarCollapsed() { const setCollapsed = useCallback((next: boolean) => { try { window.localStorage.setItem(storageKey, next ? "1" : "0"); + // Storage write succeeded; clear the in-memory fallback so persisted + // storage remains the source of truth. + inMemoryFallback = null; } catch { - // Ignore storage failures (private mode); state simply won't persist. + // Storage write failed (private mode, quota exceeded, etc.); remember + // the requested state in memory so the UI can reflect the toggle. + inMemoryFallback = next; } window.dispatchEvent(new Event(changeEvent)); }, []);