From a154913da7ff5ef932655eb358a3b10eb251a2c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 04:40:21 +0000 Subject: [PATCH 01/10] Give both modes one look: Clinical band, grouped lists, calmer controls Owner-approved design pass from the 11 Oct 2026 review (decision C8). - Clinical top bars and tab bars share the mode's soft tint as one band; the tabs stay in the collapse track and the bottom search box is untouched. - Work band and top bar lose the dot texture; the band drawing is fainter. - Clinical page headings share one eyebrow, title and description style. - Dictionary, Sources, Therapy pathways, Formulation, Specifiers, Forms and Differentials lists group items as divided rows in one hairline card, with no side bars and no cards inside cards. - Priority labels are neutral tags; red stays for urgent. Forms shows its count once. Black Clinical buttons take the area colour. - Work chip rows stay on one line and scroll with an edge fade; Bookings' Book looks like a button; Documents' empty state gets a line drawing. - Work polish: one way back on five pages, aligned profile statuses, On Call type sizes and grouped People, First week and Leave wallet tidy-ups, example wording in CPD, and the Leave and requests gap. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01T6yVRM8TyEHCooxU8ke9nA --- docs/design-system/DECISIONS.md | 32 ++++ .../(search-app)/documents/search/page.tsx | 69 +++++++- src/app/mode-band.css | 36 ++++ src/app/work-mode.css | 82 ++++++--- .../admin/contract/contract-end-page.tsx | 3 +- .../admin/leave/leave-wallet-page.tsx | 23 +-- src/components/admin/renewals/kind-chips.tsx | 8 +- .../master-search-header.tsx | 9 + .../cme/applications/applications-page.tsx | 12 +- src/components/cme/cme-entry-form.tsx | 2 +- src/components/cme/cme-new-entry-route.tsx | 3 +- src/components/cme/cme-quick-log.tsx | 2 +- .../dictionary/dictionary-catalogue-pages.tsx | 85 +++++++-- .../dictionary/dictionary-result-row.tsx | 107 +++++------ .../differential-stream-workspace.tsx | 42 +++-- .../factsheets/factsheets-search-page.tsx | 7 +- src/components/first-nations/fn-button.tsx | 29 ++- .../forms/forms-search-results-page.tsx | 77 ++++---- .../formulation/formulation-builder-page.tsx | 44 ++--- src/components/mode-band/mode-band.tsx | 7 +- src/components/mode-kit/work.tsx | 7 +- .../mode-picker/mode-picker-row.tsx | 3 +- .../my-day/my-day-calendar-page.tsx | 4 +- src/components/on-call/call/call-page.tsx | 56 +++--- .../on-call/first-week/first-week-page.tsx | 68 +++---- src/components/on-call/kit/grouped-list.tsx | 23 ++- src/components/on-call/now/right-now.tsx | 7 +- .../on-call/now/signed-out-example.tsx | 10 +- .../open-shifts/open-shifts-browse-page.tsx | 38 +--- .../roster/requests/roster-requests-page.tsx | 10 +- .../roster/staffing/roster-staffing-entry.tsx | 19 +- .../sources/sources-browse-client.tsx | 16 +- .../sources/sources-catalogue-client.tsx | 19 +- .../specifiers/specifier-builder-page.tsx | 24 ++- src/components/specifiers/specifier-ui.tsx | 4 +- src/components/therapy-compass/controls.ts | 14 +- .../therapy-compass/pathway-picker-sheet.tsx | 8 +- .../therapy-compass/pathway-step-stack.tsx | 40 +++-- .../screens/pathways-screen.tsx | 2 +- .../tools/tools-search-results-page.tsx | 9 +- src/components/ui/catalogue-toolbar.tsx | 166 +++++++++--------- src/components/ui/page-header.tsx | 33 ++-- .../work-frame/work-frame-header.tsx | 2 +- .../work-profile/work-profile-page.tsx | 17 +- .../work-screens/admin/bookings-page.tsx | 10 +- tests/admin-contract-page.dom.test.tsx | 5 +- tests/admin-leave-wallet-page.dom.test.tsx | 3 +- tests/admin-renewals-page.dom.test.tsx | 9 +- tests/cme-applications-page.dom.test.tsx | 4 +- tests/first-nations-design.dom.test.tsx | 11 +- ...forms-search-register-listing.dom.test.tsx | 9 +- tests/on-call-first-week-page.dom.test.tsx | 3 +- tests/open-shifts-browse-sample.dom.test.tsx | 5 +- 53 files changed, 863 insertions(+), 474 deletions(-) diff --git a/docs/design-system/DECISIONS.md b/docs/design-system/DECISIONS.md index 6ba277fcc4..1ac35419fa 100644 --- a/docs/design-system/DECISIONS.md +++ b/docs/design-system/DECISIONS.md @@ -213,6 +213,38 @@ convergence. --- +## C8 · One look for both modes (owner design review, 11 October 2026) + +**Chose.** After a review of every Work and Clinical screen at 320 px, 390 px, desktop and +dark mode, the owner approved these seven rules for both modes: + +1. **One look for both modes.** Clinical keeps its own components and behaviour, including + the search box at the bottom of the screen and how it works, but shares Work's look: round + header buttons on a tinted top bar, the tab bar in the same tint, hairline cards, the same + controls and illustrations, in Clinical blue. No search bar is added at the top. +2. **Calmer bands.** The band and top bar carry no dot texture, and the band's line drawing + is fainter (0.08 light, 0.11 dark, from 0.11 and 0.14). +3. **Primary buttons take the area colour.** Never black or ink-filled buttons, in either mode. +4. **Filter chips stay on one line.** They scroll sideways with an edge fade and never wrap to + leave one chip alone. +5. **Red means overdue, lapsed or urgent only,** in both modes. Priority and category labels + ("High", "Medium") use neutral tags. +6. **One way back.** If the header shows Back, the page adds no extra "‹ My Day" style link + under the band. +7. **One card per group,** in both modes: items sit as divided rows in one hairline card, with + no cards inside cards and no coloured side bars. + +**Rejected.** A top search bar or a title band in Clinical mode. The owner asked that the +Clinical search box keep its bottom position and behaviour, and that Clinical keep its own +components; a title band would also have moved the tab bar out of the top bar's collapse +track and changed how it hides on scroll. + +**Why.** The two modes read as two different apps (different header, list, card and button +styles). Sharing one visual language while keeping each mode's own controls balances them +without retraining the reader. + +--- + ## Q&A record — clinical inputs (31 July 2026) | # | Question | Answer | Design consequence | diff --git a/src/app/(search-app)/documents/search/page.tsx b/src/app/(search-app)/documents/search/page.tsx index 62d3dc8559..efb6432d06 100644 --- a/src/app/(search-app)/documents/search/page.tsx +++ b/src/app/(search-app)/documents/search/page.tsx @@ -1,16 +1,78 @@ import type { Metadata } from "next"; import Link from "next/link"; import { buttonFaceClass } from "@/components/ui/button-face"; +import { cn } from "@/components/ui-primitives"; export const metadata: Metadata = { title: "Document Search - PsychSift", description: "Search indexed clinical documents and review matching evidence.", }; +// Primary buttons wear the area colour, never the shared near-black command +// fill (design pass, October 2026). Re-pointing the command tokens on the +// button itself keeps Button's hover and pressed states and leaves every other +// control on the page alone. +const clinicalPrimaryAccent = + "[--command:var(--clinical-accent)] [--command-hover:var(--clinical-accent-hover)] [--command-active:var(--clinical-accent-active)] [--command-contrast:var(--clinical-accent-contrast)]"; + +// The empty screen's line drawing: two stacked documents and a magnifier, in +// the area colour. It repeats what the heading says, so it is hidden from +// screen readers. Fills use tokens through classes so dark mode follows. +function DocumentsSearchIllustration() { + return ( + + ); +} + export default function DocumentsSearchRoute() { return (
-

+ +

Indexed sources

Search clinical documents

@@ -19,7 +81,10 @@ export default function DocumentsSearchRoute() { matching page and passage.

- + Open Documents search
diff --git a/src/app/mode-band.css b/src/app/mode-band.css index c1bf9b9757..fb93d6c548 100644 --- a/src/app/mode-band.css +++ b/src/app/mode-band.css @@ -433,3 +433,39 @@ html[data-mode-band] [data-testid="chrome-safe-area-top"] { :is(div, header):has(> div:only-child > [data-under-band]:only-child) { display: contents; } + +/* The clinical band (owner design review, 11 Oct 2026). Every Clinical page + but the shared home tints its top bar with the mode's soft colour, and a + mode with its own pages (Dictionary, Differentials, Sources and the rest) + tints its tab bar to match, with the band's hairline under it, so the two + read as one band the way a work page's top bar and band do. The tab bar + stays in the top bar's collapse track, so it still hides and reveals with + the top bar exactly as before. The round header buttons are already white + circles, which the tint now shows. The header marks the page in its server + HTML (`data-clinical-band`), so the tint is there from the first paint. A + page under a mode band (Medicines, Psychiatry) keeps the band's own colour. */ +html:not([data-mode-band]):has(.universal-header[data-clinical-band]) { + --clinical-band-bg: var(--mode-identity-soft); + --clinical-band-line: var(--mode-identity-border); +} + +html.dark:not([data-mode-band]):has(.universal-header[data-clinical-band]) { + --clinical-band-bg: color-mix(in srgb, var(--mode-identity) 11%, var(--background)); + --clinical-band-line: color-mix(in srgb, var(--mode-identity) 26%, var(--background)); +} + +html:not([data-mode-band]) .universal-header[data-clinical-band], +html:not([data-mode-band]) [data-testid="chrome-safe-area-top"][data-clinical-band], +html:not([data-mode-band]):has(.universal-header[data-clinical-band]) #phone-header-collapse-addon-slot .mode-nav-rail { + background: var(--clinical-band-bg); +} + +html:not([data-mode-band]):has(.universal-header[data-clinical-band]) #phone-header-collapse-addon-slot .mode-nav-rail { + border-bottom-color: var(--clinical-band-line); +} + +/* A tinted top bar with no tab bar under it ends on the band's hairline. */ +html:not([data-mode-band]) + .universal-header[data-clinical-band]:not(:has(~ #phone-header-collapse-addon-slot .mode-nav-rail)) { + box-shadow: inset 0 -1px 0 var(--clinical-band-line); +} diff --git a/src/app/work-mode.css b/src/app/work-mode.css index 6fbad8c776..4b41df967c 100644 --- a/src/app/work-mode.css +++ b/src/app/work-mode.css @@ -193,21 +193,6 @@ html[data-work-frame] main { } } -/* The dot texture, lined up with the top bar's so the two read as one. */ -.work-band::before, -html[data-work-frame] .universal-header[data-work-frame]::after, -.universal-header[data-work-band]::after { - content: ""; - position: absolute; - inset: 0; - z-index: var(--work-z-under); - pointer-events: none; - background-image: radial-gradient(var(--mode-identity-border) 0.7px, transparent 0.8px); - background-size: 12px 12px; - background-position: 0 var(--work-band-offset, 0); - opacity: 0.55; -} - /* The faint line drawing, cropped by its own box at the band's corner. */ .work-band__mark { position: absolute; @@ -219,12 +204,13 @@ html[data-work-frame] .universal-header[data-work-frame]::after, background: var(--mode-identity); mask: var(--work-band-mark, none) left top / 12.25rem 12.25rem no-repeat; -webkit-mask: var(--work-band-mark, none) left top / 12.25rem 12.25rem no-repeat; - opacity: 0.11; + /* Fainter since the 11 Oct design review: colour says where you are, the drawing only adds a hint. */ + opacity: 0.08; pointer-events: none; } .dark .work-band__mark { - opacity: 0.14; + opacity: 0.11; } /* Pinned tabs (owner request 8 Oct 2026). Once the title has scrolled under @@ -1670,18 +1656,70 @@ a.work-hero:focus-visible { } /* Chips: filters and choices. A 30px shape inside a 48px tap. */ +/* Chip rows stay on one line and scroll sideways (design pass, October 2026): + wrapping left one chip alone on a second line. Where scroll-driven + animations exist, a soft fade appears only at an edge with more chips past + it, and only while the row actually overflows. Elsewhere (older iPhone + Safari) there is no fade at all, so no chip is ever dimmed. The tab swipe + already ignores a row that scrolls sideways. */ .work-chips { display: flex; - flex-wrap: wrap; - gap: 0 calc(var(--spacing) * 1.5); -} - -.work-chips[data-scroll] { flex-wrap: nowrap; + gap: 0 calc(var(--spacing) * 1.5); overflow-x: auto; + overscroll-behavior-x: contain; scrollbar-width: none; } +.work-chips::-webkit-scrollbar { + display: none; +} + +@property --work-chips-fade-start { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@property --work-chips-fade-end { + syntax: ""; + inherits: false; + initial-value: 0px; +} + +@keyframes work-chips-fade { + from { + --work-chips-fade-start: 0px; + --work-chips-fade-end: 1.75rem; + } + 1% { + --work-chips-fade-start: 1.75rem; + } + 99% { + --work-chips-fade-end: 1.75rem; + } + to { + --work-chips-fade-start: 1.75rem; + --work-chips-fade-end: 0px; + } +} + +@supports (animation-timeline: scroll()) { + .work-chips { + /* A row that does not overflow has an inactive timeline, so both fades + stay at 0px and nothing is masked. */ + mask-image: linear-gradient( + to right, + transparent, + black var(--work-chips-fade-start), + black calc(100% - var(--work-chips-fade-end)), + transparent + ); + animation: work-chips-fade linear both; + animation-timeline: scroll(self inline); + } +} + .work-chip { position: relative; display: inline-flex; diff --git a/src/components/admin/contract/contract-end-page.tsx b/src/components/admin/contract/contract-end-page.tsx index 3d6c33157d..a1e9108ef4 100644 --- a/src/components/admin/contract/contract-end-page.tsx +++ b/src/components/admin/contract/contract-end-page.tsx @@ -19,7 +19,6 @@ import { createEntry, deleteEntry, errorWords, - JuniorBackLink, JuniorFootNote, JuniorNotice, JuniorSectionLabel, @@ -392,7 +391,7 @@ export function ContractEndPage({ now: nowProp }: { now?: Date } = {}) { return ( <> - + {/* One way back: the band's Today tab is Admin Today, so the page adds no second link to it. */}
Contract end diff --git a/src/components/admin/leave/leave-wallet-page.tsx b/src/components/admin/leave/leave-wallet-page.tsx index 7e3349833b..53d5be3870 100644 --- a/src/components/admin/leave/leave-wallet-page.tsx +++ b/src/components/admin/leave/leave-wallet-page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { BookOpen, + ChevronRight, Copy, EyeOff, ExternalLink, @@ -25,7 +26,6 @@ import { ContractEndEntryLink } from "@/components/admin/contract/contract-entry import { LeaveEntitlementList } from "@/components/admin/leave/leave-entitlement-list"; import { copyLabel, - JuniorBackLink, JuniorFootNote, JuniorSectionLabel, JuniorUndoBar, @@ -588,7 +588,7 @@ export function LeaveWalletPage({ now: nowProp }: { now?: Date } = {}) { return ( - + {/* One way back: the band's Today tab is Admin Today, so the page adds no second link to it. */}
Leave wallet @@ -664,14 +664,17 @@ export function LeaveWalletPage({ now: nowProp }: { now?: Date } = {}) { Your wallet {/* - The mockup's wallet: each card's top edge tucks 8 px under the card before it, a deliberate stacked - deck. Each card is at least 64 px tall, so at least 56 px of every card stays uncovered and tappable, - and the overlap only ever covers the bottom padding, never a word. A focused card lifts above the - next so its focus ring shows in full. + One grouped card with a hairline between leave types, the same list shape as the rest of Work mode, + rather than a stacked deck of separate cards. Each row is a full-width button at least 64 px tall. */} -
    +
      {visible.map((type, index) => ( -
    • 0 && "-mt-2")}> +
    • 0 && "border-t border-[color:var(--border)]")}>
    • ))} diff --git a/src/components/admin/renewals/kind-chips.tsx b/src/components/admin/renewals/kind-chips.tsx index bcdb216d8e..0e899a2257 100644 --- a/src/components/admin/renewals/kind-chips.tsx +++ b/src/components/admin/renewals/kind-chips.tsx @@ -20,9 +20,9 @@ export function checklistKindLabel(kind: ChecklistKindFilter): string { /** * The kind filter row (Josh's locked mockup): "All, Registration, Checks, * Health, Training, Job" as the work kit's chips. `aria-pressed`, not a radio - * group: a chip narrows the list rather than replacing it. The chips wrap onto - * a second line on a phone rather than scrolling sideways, so none is ever - * hidden off the edge (and a sideways drag here never fights the tab swipe). + * group: a chip narrows the list rather than replacing it. Like every chip + * row, they stay on one line and scroll sideways, with an edge fade showing + * there is more; the tab swipe ignores a row that scrolls sideways. */ export function ChecklistKindChips({ active, @@ -34,7 +34,7 @@ export function ChecklistKindChips({ readonly testId?: string; }) { return ( -
      +
      {CHECKLIST_KIND_FILTERS.map((kind) => ( (null); @@ -2540,6 +2547,7 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { // before the band has published it on (work-mode.css). data-work-band={routeWorkFramed ? "" : undefined} data-mode-identity={routeWorkFrame?.area.identity} + data-clinical-band={routeClinicalBand ? "" : undefined} className={cn( // No backdrop-filter on the header itself: it would form a backdrop // root and starve the .edge-glass-header-backdrop scrim (the single @@ -3126,6 +3134,7 @@ export function MasterSearchHeader(props: MasterSearchHeaderProps) { // Tinted with the top bar from the first paint on a framed work page. data-work-band={routeWorkFramed ? "" : undefined} data-mode-identity={routeWorkFrame?.area.identity} + data-clinical-band={routeClinicalBand ? "" : undefined} className={cn( // Visible phone chrome owns the OS inset. Hidden phone chrome must // release it so the scroll surface reaches the physical viewport diff --git a/src/components/cme/applications/applications-page.tsx b/src/components/cme/applications/applications-page.tsx index 7e1c78831f..783833cb87 100644 --- a/src/components/cme/applications/applications-page.tsx +++ b/src/components/cme/applications/applications-page.tsx @@ -8,7 +8,6 @@ import { focusRing } from "@/components/card-recipes"; import { ActionDock, CpdFeaturePage, - type CpdFeatureBack, flatCard, flatRow, IconCircle, @@ -49,8 +48,7 @@ import { import { perthCalendarDate } from "@/lib/cme/cpd-year"; import { useApplicationsStore } from "@/lib/cme/device-record"; -/** The page's parent, where its link lives. */ -const APPLICATIONS_BACK: CpdFeatureBack = { href: "/cme/summary", label: "CPD summary" }; +// No page-level back link: the band's Summary tab is this page's parent, so a second way back would repeat it. type DateSheetState = { open: false } | { open: true; stage: ApplicationStageId | null }; type RefereeSheetState = { open: false } | { open: true; id: string | null }; @@ -76,12 +74,7 @@ export function ApplicationsPage({ demoMode, now }: { readonly demoMode: boolean if (!state) { return ( - +