From 9984122989a70daba4f32e920aa2177698ba194d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 10:27:08 +0000 Subject: [PATCH 01/16] Design: 12 px text floor, mono figures, one shape per status, paler fades, Bright light From the Ward Flow design review (owner approved all, 12 Oct 2026). - The two legacy small type steps (3xs 10 px, 2xs 11 px) and the work kit's sub-12 px sizes now sit at the 12 px floor. - Counts, day numbers, ring and row-end figures use Geist Mono. - Work tags are grey pills whose shape carries the status colour: triangle overdue, filled dot due soon, tick done, dashed ring not recorded. Admin's shared status mark uses the same set. - Faded rows (roster days outside the month, stale on-call rows, locked assessments) take a paler colour through data-dim instead of opacity. - New Bright light preference in Settings, App preferences: solid surfaces, darker lines and secondary text, heavier body text, light mode only. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0113Rr2NnmLuLKLWdKTwHHqd --- src/app/globals.css | 60 ++++++++-- src/app/layout.tsx | 2 +- src/app/work-mode.css | 113 +++++++++++++++--- src/components/admin/admin-status-tag.tsx | 21 ++-- .../clinical-dashboard/settings-dialog.tsx | 10 ++ .../clinical-dashboard/settings-sections.ts | 6 + .../clinical-dashboard/use-app-preferences.ts | 3 +- .../on-call-roster-whos-on-section.tsx | 2 +- .../roster/team/calendar/month-view.tsx | 2 +- .../assessments/assessments-parts.tsx | 2 +- src/lib/account-preferences.ts | 10 ++ tests/admin-renewals-page.dom.test.tsx | 6 +- tests/app-preferences.test.ts | 1 + 13 files changed, 198 insertions(+), 40 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index 3c3908c356..0ff2f97c69 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -236,11 +236,11 @@ `eyebrowText` in ui-primitives.tsx for the uppercase eyebrow treatment (`--text-2xs` used to bake that tracking in; it is now the neutral 11px step and the tracking lives at the recipe/call site instead). - 3xs 10px micro floor: dense metadata, legend labels, count - bubbles. An 8px `4xs` step existed here and is retired - — unreadable at any density in a clinical product, so - nothing below 10px is a legal step. ≤9px snaps here. - 2xs 11px Chip `compact` / secondary labels (most common small size) + 3xs 12px legacy name, now an alias of xs. 12px is the floor for + every word on screen (owner decision, 12 Oct 2026, from + the Ward Flow review): the old 10px and 8px steps were + too small to read on a phone in a clinical product. + 2xs 12px legacy name, now an alias of xs (was 11px) (xs 12 · sm 14 · base 16 come from Tailwind's default scale — Chip `standard` uses xs / 12px; see docs/design-system/TOKENS.md) sm-minus 13px one notch under sm — dense body/labels bigger than xs @@ -248,8 +248,8 @@ (lg 18 · xl 20 · 2xl 24 come from Tailwind's default scale) lg-minus 17px one notch under lg — display titles a shade under 18 2xl-minus 22px one notch under 2xl — wide-screen nowrap headings */ - --text-3xs: 0.625rem; - --text-2xs: 0.6875rem; + --text-3xs: 0.75rem; /* alias of xs, see above */ + --text-2xs: 0.75rem; /* alias of xs, see above */ --text-sm-minus: 0.8125rem; --text-base-minus: 0.9375rem; --text-lg-minus: 1.0625rem; @@ -6671,3 +6671,49 @@ html.theme-transitioning *:after { .universal-header-trailing:has(> #universal-header-trailing-slot > *) .universal-header-new-chat { display: none; } + +/* Dimmed content takes a paler colour, never opacity, so the text keeps its + contrast (opacity multiplies against whatever sits behind it). Put + `data-dim` on the row or cell; status colours inside it go quiet too. */ +[data-dim] { + --text-heading: var(--text-muted); + --text: var(--text-muted); + --work-ink: var(--text-muted); + --clinical-accent: var(--text-muted); + --danger-text: var(--text-muted); + --danger-bg: var(--surface-subtle); + --warning-text: var(--text-muted); + --warning-bg: var(--surface-subtle); + color: var(--text-muted); +} + +/* Bright light (Settings, App preferences): for glare on a ward or in sun. + Owner decision 12 Oct 2026, from the Ward Flow review. Solid surfaces with + no glass or blur, darker lines and secondary text, body text one step + heavier. It is a light-only look: turning it on pins light mode, and it + stands down if the reader later picks dark. The selector out-ranks the + `.ckb-v2.ckb-v2` token layer so the overrides win. */ +html.ckb-v2.ckb-v2[data-bright]:not(.dark) { + --border: var(--neutral-400); + --border-strong: var(--neutral-500); + --neutral-300: var(--neutral-400); + --text-muted: var(--text); + --decoration-soft: var(--neutral-500); +} +html[data-bright]:not(.dark) body { + font-weight: 500; +} +html[data-bright]:not(.dark) *, +html[data-bright]:not(.dark) *::before, +html[data-bright]:not(.dark) *::after { + -webkit-backdrop-filter: none !important; + backdrop-filter: none !important; +} +html[data-bright]:not(.dark) .work-band, +html[data-bright]:not(.dark) .universal-header { + background: var(--surface-raised) !important; + background-image: none !important; +} +html[data-bright]:not(.dark) .work-card { + box-shadow: none; +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ed3dfb1c84..195a163ff0 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -158,7 +158,7 @@ export default async function RootLayout({ // read it), which reads as a hydration mismatch on this attribute. suppressHydrationWarning dangerouslySetInnerHTML={{ - __html: `${THEME_BOOTSTRAP_SCRIPT}${GLASS_FALLBACK_SCRIPT}(function(){try{var p=JSON.parse(localStorage.getItem("clinical-kb-preferences")||"{}");if(p&&typeof p==="object"){if(p.density==="compact"||p.density==="spacious"){document.documentElement.setAttribute("data-density",p.density);}if(p.motion==="reduced"||p.motion==="full"){document.documentElement.setAttribute("data-motion",p.motion);}}}catch(e){/* storage blocked or stored preferences JSON corrupt - the default density/motion apply */}})();`, + __html: `${THEME_BOOTSTRAP_SCRIPT}${GLASS_FALLBACK_SCRIPT}(function(){try{var p=JSON.parse(localStorage.getItem("clinical-kb-preferences")||"{}");if(p&&typeof p==="object"){if(p.density==="compact"||p.density==="spacious"){document.documentElement.setAttribute("data-density",p.density);}if(p.motion==="reduced"||p.motion==="full"){document.documentElement.setAttribute("data-motion",p.motion);}if(p.brightLight===true){document.documentElement.setAttribute("data-bright","");}}}catch(e){/* storage blocked or stored preferences JSON corrupt - the default density/motion apply */}})();`, }} /> b, +[class*="countFigure"], +[data-testid="roster-month-totals"] dd, +[data-figure] { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + letter-spacing: -0.02em; +} + +/* One shape per status. The word is plain grey ink; only the glyph carries + colour: triangle overdue, filled dot due soon, tick done, dashed ring not + recorded. Colour is never the only signal. */ +.work-tag { + display: inline-flex; + align-items: center; + gap: 0.375em; + background: var(--work-wash); + color: var(--work-ink-muted); + box-shadow: inset 0 0 0 1px var(--work-line); +} +.work-tag::before { + content: ""; + flex: none; + width: 0.75em; + height: 0.75em; + border-radius: 50%; + background: var(--mode-identity); +} +.work-tag[data-tone="red"]::before { + border-radius: 0; + background: var(--danger-text); + clip-path: polygon(50% 4%, 98% 94%, 2% 94%); +} +.work-tag[data-tone="amber"]::before { + background: var(--warning, var(--warning-text)); +} +.work-tag[data-tone="green"]::before { + border-radius: 0; + background: var(--success-text); + clip-path: polygon(8% 52%, 22% 38%, 40% 56%, 80% 14%, 94% 28%, 40% 84%); +} +.work-tag[data-tone="neutral"]::before { + background: transparent; + box-shadow: inset 0 0 0 1.5px var(--decoration-soft); +} +.work-tag[data-tone="red"], +.work-tag[data-tone="amber"], +.work-tag[data-tone="green"], +.work-tag[data-tone="neutral"] { + background: var(--work-wash); + color: var(--work-ink-muted); +} +.work-row__end .work-tag { + font-family: inherit; + letter-spacing: 0.06em; +} diff --git a/src/components/admin/admin-status-tag.tsx b/src/components/admin/admin-status-tag.tsx index afdac07079..e4c7cde164 100644 --- a/src/components/admin/admin-status-tag.tsx +++ b/src/components/admin/admin-status-tag.tsx @@ -25,19 +25,19 @@ const STATUS_WORDS: Record = { plain: "", }; -/** The mockup's shapes, drawn on a 10px grid: a diamond, a triangle, a dashed ring, a dot. */ +/** One shape per status (Ward Flow review, 12 Oct 2026), on a 10px grid: triangle for date passed, filled dot for start renewing, dashed ring for not recorded, tick for recorded. */ export function AdminStatusMark({ status }: { readonly status: AdminStatus }) { if (status === "date-passed") { return ( - {gap ? ( diff --git a/src/components/roster/team/calendar/month-view.tsx b/src/components/roster/team/calendar/month-view.tsx index 00f984cb4f..55c17b9002 100644 --- a/src/components/roster/team/calendar/month-view.tsx +++ b/src/components/roster/team/calendar/month-view.tsx @@ -78,8 +78,8 @@ export function MonthView({ cell.mine.length > 0 ? "min-w-0 rounded-lg bg-[color:var(--surface-wash)] ring-2 ring-inset ring-[color:var(--mode-identity)]" : "min-w-0 rounded-lg ring-1 ring-inset ring-[color:var(--border)]", - !cell.inMonth && "opacity-60", )} + data-dim={cell.inMonth ? undefined : ""} >