From 95abc8c5d602349073769ae5a5127d3368bbe4cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 09:36:05 +0000 Subject: [PATCH 01/11] feat(shell): one width per work page kind, desktop layout pieces, labelled Clinical rail Work pages declare a reading or wide column and the band's title and tabs follow it, so the title lines up with the cards on desktop. Wide pages open out only from 1024px (tablet rule: phone at 768, desktop at 1024). Adds WorkWidePage, WorkSplit and WorkColumns for area threads. Desktop Clinical matches Work: labels under the rail icons, no Mode eyebrow in the pill, and the bell on tablets and desktop. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01KHkbt7EPd1iS6ryTjtnzq2 --- docs/design-system/DECISIONS.md | 24 ++++ src/app/work-mode.css | 126 +++++++++++++++++- src/components/admin/admin-kit.tsx | 8 +- .../clinical-dashboard/ClinicalSidebar.tsx | 101 +++++++------- .../master-search-header.tsx | 18 +-- src/components/information-page-shell.tsx | 7 +- src/components/my-day/my-day-frame.tsx | 6 +- src/components/my-day/my-day-page.tsx | 2 +- src/components/needs-you/needs-you-button.tsx | 44 +++++- .../needs-you/notifications-page.tsx | 2 +- .../needs-you/staff-work-header-controls.tsx | 10 ++ .../open-shifts/open-shifts-alerts-page.tsx | 2 +- .../open-shifts/open-shifts-browse-page.tsx | 2 +- .../open-shifts/open-shifts-log-page.tsx | 2 +- .../open-shifts/open-shifts-mine-page.tsx | 2 +- .../open-shifts/open-shifts-post-page.tsx | 2 +- .../open-shifts/open-shifts-posted-page.tsx | 2 +- .../open-shifts-posted-shift-page.tsx | 2 +- .../work-frame/work-desktop-layout.tsx | 71 ++++++++++ .../work-profile/work-profile-page.tsx | 2 +- src/lib/needs-you/homes.ts | 12 ++ tests/mode-pages-sheet-classes.test.ts | 6 +- tests/needs-you-homes.test.ts | 11 ++ 23 files changed, 388 insertions(+), 76 deletions(-) create mode 100644 src/components/work-frame/work-desktop-layout.tsx diff --git a/docs/design-system/DECISIONS.md b/docs/design-system/DECISIONS.md index 203d43b0e2..4e4f304423 100644 --- a/docs/design-system/DECISIONS.md +++ b/docs/design-system/DECISIONS.md @@ -245,6 +245,30 @@ without retraining the reader. --- +## C9 · The desktop and tablet shell (usefulness and layout audit, 11 October 2026) + +**Chose.** Built on the owner's go-ahead for the 11 October usefulness and layout audit: + +1. **Tablet rule.** Page content behaves like a phone at 768 px (an upright tablet: one + column) and like desktop from 1024 px (a tablet on its side: two columns, a list beside a + record). The side rail still replaces the menu button from 768 px (owner pick "Burger and + rail", 7 October), so only the content follows the rule. +2. **One width per kind of page.** Reading pages (lists, forms, settings) keep the 45rem + column; wide pages (calendars, boards, tables, a list beside a record, the shared page + shell) take 64rem from 1024 px. The band's title and tabs always take the page's width, + so the title lines up with the cards. Declared with `data-page-width` (work-mode.css, + "page widths"); `src/components/work-frame/work-desktop-layout.tsx` gives area pages + `WorkWidePage`, `WorkSplit` (list beside record) and `WorkColumns` (two or three columns). +3. **Desktop Clinical matches Work.** The Clinical rail shows a name under every icon, in the + work rail's style and Clinical blue; the mode pill drops its small "Mode" label at every + width; tablets and desktop show the bell in the Clinical header, as on Work pages. Phones, + the brand lockup on mode homes and the Clinical search box are unchanged. + +**Rejected.** Moving the side rail to 1024 px: it was the owner's own 7 October pick, and an +upright tablet reads well with it. + +--- + ## Q&A record — clinical inputs (31 July 2026) | # | Question | Answer | Design consequence | diff --git a/src/app/work-mode.css b/src/app/work-mode.css index c641100884..8526f46e28 100644 --- a/src/app/work-mode.css +++ b/src/app/work-mode.css @@ -172,6 +172,95 @@ html[data-work-frame] main { background-color: var(--surface-raised); } +/* ------------------------------------------------------ page widths + + One column width per kind of work page, and the band's title and tabs + always line up with it (desktop and tablet shell, 11 Oct 2026 audit). + + - Reading pages (lists, forms, settings) keep the 45rem column. + - Wide pages (calendars, boards, tables, list beside a record) declare + `data-page-width="wide"` on their outer container, and from 1024px the + whole frame, band included, takes the 64rem column. A reading page that + sizes its own container says `data-page-width="reading"`. + + Tablet rule: below 1024px (an upright tablet) every page stays the single + phone column; from 1024px (sideways, and desktop) wide pages open out. + `:has()` keeps it in CSS, so the server render is already the right width. */ +body { + --work-col: 45rem; +} + +@media (min-width: 1024px) { + body:has([data-work-frame] [data-page-width="wide"]) { + --work-col: 64rem; + } +} + +/* The shared page shell inside a work area takes the frame's column rather + than the clinical 80rem container, so the title and the cards line up. */ +[data-work-frame] [data-page-width] { + max-width: var(--work-col); +} + +/* A column that draws its own 12px side padding (the work body's) holds the + same content width as the band. */ +[data-work-frame] [data-page-width][data-page-padded] { + max-width: calc(var(--work-col) + 1.5rem); +} + +/* ------------------------------------------------------ desktop layouts + + The pieces in work-desktop-layout.tsx. One phone column below 1024px; from + 1024px a list beside the record, or two or three columns of cards. */ +.work-wide, +.work-split, +.work-columns { + display: grid; + grid-template-columns: minmax(0, 1fr); + align-content: start; + gap: calc(var(--spacing) * 2.25); + width: 100%; + margin: 0 auto; + padding: calc(var(--spacing) * 3) calc(var(--spacing) * 3) + max(calc(var(--spacing) * 7.5), calc(var(--safe-area-bottom, 0px) + var(--spacing) * 4)); + box-sizing: border-box; +} + +.work-split__list, +.work-split__detail, +.work-columns > * { + display: grid; + min-width: 0; + align-content: start; + gap: calc(var(--spacing) * 2.25); +} + +@media (min-width: 1024px) { + .work-split { + grid-template-columns: minmax(17rem, 22rem) minmax(0, 1fr); + gap: calc(var(--spacing) * 5); + align-items: start; + } + + .work-split__list { + position: sticky; + top: calc(var(--work-chrome-h, 0px) + var(--work-tabs-h, 0px) + var(--spacing) * 3); + max-height: calc(100dvh - var(--work-chrome-h, 0px) - var(--work-tabs-h, 0px) - var(--spacing) * 6); + overflow-y: auto; + overscroll-behavior: contain; + } + + .work-columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: calc(var(--spacing) * 4); + align-items: start; + } + + .work-columns[data-columns="3"] { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + /* ------------------------------------------------------------ the band */ .work-band { @@ -309,7 +398,7 @@ html[data-motion="reduced"] .work-band__mark { align-items: flex-end; justify-content: space-between; gap: calc(var(--spacing) * 2.5); - max-width: calc(45rem + var(--work-gutter) * 2); + max-width: calc(var(--work-col, 45rem) + var(--work-gutter) * 2); margin: 0 auto; padding: 0.375rem var(--work-gutter-right) 0 var(--work-gutter-left); } @@ -341,7 +430,7 @@ html[data-motion="reduced"] .work-band__mark { } .work-band__status { - max-width: calc(45rem + var(--work-gutter) * 2); + max-width: calc(var(--work-col, 45rem) + var(--work-gutter) * 2); margin: calc(var(--spacing) * 1.5) auto 0; padding: 0 var(--work-gutter-right) 0 var(--work-gutter-left); box-sizing: border-box; @@ -397,7 +486,7 @@ html[data-motion="reduced"] .work-band__mark { --work-tab-gap: calc(var(--spacing) * 5); display: flex; gap: var(--work-tab-gap); - max-width: calc(45rem + var(--work-gutter) * 2); + max-width: calc(var(--work-col, 45rem) + var(--work-gutter) * 2); margin: calc(var(--spacing) * 1) auto -1px; /* The 1px under the row holds the current tab's underline, which sits on the band's bottom line, inside the row's own box. */ @@ -2726,6 +2815,37 @@ html[data-work-frame] .app-toast-region { color: var(--mode-identity-deep, var(--mode-identity)); } +/* The clinical rail wears the same items (ClinicalSidebar's collapsed rail), + lit in Clinical blue and marked current by `aria-current="page"`. */ +.clinical-rail__item { + --mode-identity: var(--clinical-accent); + --mode-identity-soft: var(--clinical-accent-soft); + --mode-identity-deep: var(--clinical-accent); + flex: none; + text-align: center; +} + +.clinical-rail__item:focus-visible { + outline: 2px solid var(--focus); + outline-offset: 2px; +} + +.clinical-rail__item[aria-current="page"] .work-rail__pill, +.clinical-rail__item[data-current="true"] .work-rail__pill { + background: var(--mode-identity-soft); + color: var(--mode-identity); +} + +.clinical-rail__item[aria-current="page"] .work-rail__label, +.clinical-rail__item[data-current="true"] .work-rail__label { + font-weight: 750; + color: var(--mode-identity-deep); +} + +.clinical-rail__item:hover .work-rail__pill { + background: var(--surface-subtle); +} + .work-rail__count { position: absolute; top: -0.1875rem; diff --git a/src/components/admin/admin-kit.tsx b/src/components/admin/admin-kit.tsx index 6adc0fa542..616572710d 100644 --- a/src/components/admin/admin-kit.tsx +++ b/src/components/admin/admin-kit.tsx @@ -32,7 +32,13 @@ export function AdminPage({ }) { return (
-
{children}
+
+ {children} +
); } diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index 553bee8353..c5375e794a 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -23,6 +23,7 @@ import { Sparkles, SunMedium, Wrench, + type LucideIcon, } from "lucide-react"; import { appModeIcons } from "@/lib/app-mode-icons"; import { BrandMark } from "@/components/clinical-dashboard/brand"; @@ -170,8 +171,6 @@ export function selectModeFromLinkClick( const collapsedSidebarControl = "h-tap w-tap shrink-0 place-items-center rounded-xl border border-transparent text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; const collapsedSidebarButton = `grid ${collapsedSidebarControl}`; -const collapsedSidebarActiveButton = - "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]"; /* The same state on a full-width row, and deliberately the same language: the * rail and the expanded list mark the current mode, so they may not mark it two * different ways. @@ -200,6 +199,36 @@ const sidebarItemActive = 48px controls broken into groups rather than four rules with three spacings. */ const collapsedSidebarDivider = "my-1.5 h-px w-8 shrink-0 bg-[color:var(--border)]"; +/* The rail's items wear the work rail's look: an icon in a soft pill with its + name underneath, so a reader never has to guess what an icon is and the two + sides read as one app (desktop and tablet shell, 11 Oct 2026 audit). The + classes live with the work rail in work-mode.css. */ +const railItem = "work-rail__item clinical-rail__item"; + +/* Shorter names where a mode's own label would not fit under its icon. */ +const railShortLabels: Partial> = { + dsm: "DSM-5", + medicines: "Medicines", + "therapy-compass": "Therapy", +}; + +function railLabel(id: AppModeId | "favourites", label: string) { + return (id !== "favourites" && railShortLabels[id]) || label; +} + +function RailItemBody({ icon: Icon, label }: { icon: LucideIcon; label: string }) { + return ( + <> + + + + ); +} + /* Brand lockup header — the phone drawer (ClinicalMobileSidebar) and the * expanded desktop sidebar share it, so the two entry points to the same * navigation read as one product rather than two headers that happen to carry @@ -341,20 +370,19 @@ function SidebarModesTrigger({ if (variant === "collapsed") { return ( - + + ); } @@ -1126,27 +1154,15 @@ function ClinicalCollapsedRail({