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 1767e0c50d..7859496a6e 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. */ @@ -2727,6 +2816,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; @@ -2747,7 +2867,9 @@ html[data-work-frame] .app-toast-region { @media (forced-colors: active) { .work-side-menu__row[aria-current="true"], - .work-rail__item[aria-current="true"] .work-rail__pill { + .work-rail__item[aria-current="true"] .work-rail__pill, + .clinical-rail__item[aria-current="page"] .work-rail__pill, + .clinical-rail__item[data-current="true"] .work-rail__pill { outline: 2px solid Highlight; } 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({