diff --git a/mockups/README.md b/mockups/README.md index 690fd4e85..dcdeebf6e 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -45,7 +45,7 @@ Some document-search mockups include live handoff routes (for example `document- ## Phone Choose mode sheet YES comps -Runnable study at [`/mockups/phone-mode-sheet-yes`](../src/app/mockups/phone-mode-sheet-yes/page.tsx): design review of the shipping phone mode sheet plus **YES 01 perfected** (sectioned clinical list — shipping recommendation) and YES 02 (icon deck alternate). Shared mockup chrome is suppressed so only the in-frame sheet is judged. +Runnable study at [`/mockups/phone-mode-sheet-yes`](../src/app/mockups/phone-mode-sheet-yes/page.tsx): **new YES comps** after feedback rejected Find / Diagnose / Care organisation. **YES 01 dense title list** (shipping recommendation) and **YES 02 flat icon tiles** — both flat, glance-first. Shared mockup chrome is suppressed so only the in-frame sheet is judged. ## Mode-page redesign comps (2026-07-31) diff --git a/src/app/mockups/phone-mode-sheet-yes/page.tsx b/src/app/mockups/phone-mode-sheet-yes/page.tsx index d8c475c25..29cd300f8 100644 --- a/src/app/mockups/phone-mode-sheet-yes/page.tsx +++ b/src/app/mockups/phone-mode-sheet-yes/page.tsx @@ -3,9 +3,9 @@ import type { Metadata } from "next"; import { PhoneModeSheetYesMockups } from "@/components/phone-mode-sheet-yes-mockups"; export const metadata: Metadata = { - title: "Phone mode sheet YES 01 perfected - Clinical KB", + title: "Phone mode sheet glance YES comps - Clinical KB", description: - "Perfected phone Choose mode sheet: sectioned clinical list with all review findings closed, plus the icon-deck alternate.", + "New phone Choose mode sheet YES comps: dense title list and flat icon tiles, without Find/Diagnose/Care organisation.", }; export default function PhoneModeSheetYesMockupPage() { diff --git a/src/components/phone-mode-sheet-yes-mockups.tsx b/src/components/phone-mode-sheet-yes-mockups.tsx index 914e0f311..e91c512b9 100644 --- a/src/components/phone-mode-sheet-yes-mockups.tsx +++ b/src/components/phone-mode-sheet-yes-mockups.tsx @@ -16,42 +16,24 @@ import { appModeIcons } from "@/lib/app-mode-icons"; import { appModeDefinitions, visibleAppModeDefinitions, type AppModeId } from "@/lib/app-modes"; /** - * Design scratch: phone "Choose mode" sheet — review + YES comps. + * Design scratch: phone "Choose mode" sheet — new YES comps (2026-08). * - * YES 01 (sectioned list) is the perfected master: every review finding closed, - * sticky offset debt removed, current mode folded into the sheet header, and - * selection is interactive so the study can be judged as a real switcher. + * Feedback rejected Find / Diagnose / Care organisation and roomy spacing — + * they made mode choice harder at a glance. Both YES directions stay flat. + * + * YES 01 — Dense title list (shipping recommendation) + * YES 02 — Flat icon tiles (alternate glance layout, still no lanes) */ -type GroupId = "find" | "diagnose" | "care"; -type VariantId = "current" | "sections" | "deck"; -type SectionsPreview = "rest" | "scrolled" | "switched"; +type VariantId = "current" | "dense" | "tiles"; +type DensePreview = "rest" | "scrolled" | "switched"; const focusRing = "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -const GROUPS: Array<{ id: GroupId; label: string; hint: string; modes: AppModeId[] }> = [ - { - id: "find", - label: "Find", - hint: "Answers, sources, services", - modes: ["answer", "documents", "services", "forms", "favourites"], - }, - { - id: "diagnose", - label: "Diagnose", - hint: "Criteria, clues, mechanisms", - modes: ["differentials", "dsm", "specifiers", "formulation"], - }, - { - id: "care", - label: "Care", - hint: "Meds, tools, therapy", - modes: ["prescribing", "tools", "therapy-compass", "factsheets"], - }, -]; - const ACTIVE_MODE: AppModeId = "answer"; +const FLAT_MODE_IDS = visibleAppModeDefinitions("production").map((mode) => mode.id); +const MID_SCROLL_MODE: AppModeId = "differentials"; function modeOf(id: AppModeId) { const definition = appModeDefinitions.find((mode) => mode.id === id); @@ -64,60 +46,24 @@ function modeOf(id: AppModeId) { const reviewFindings = [ { severity: "P1", - title: "Flat list, no clinical intent", + title: "Organisation slowed the pick", detail: - "Thirteen equal-weight rows force linear scanning. Answer, DSM, and Medication compete visually even though they serve different jobs.", + "Find / Diagnose / Care headers, hints, and counts spent height on chrome. Mode choice on phone is a glance task — more visible rows beat clinical grouping.", }, { severity: "P1", - title: "Descriptions truncate on phone", - detail: - "Live rows use `truncate` on a single line, so the subtitle that differentiates modes disappears on a 390 px sheet.", + title: "Subtitles cost catalogue height", + detail: "Two-line rows hide modes below the fold. Titles carry the choice; descriptions can live in aria-label.", }, { severity: "P2", - title: "Selected state is easy to miss mid-scroll", - detail: - "Soft fill + check is correct, but there is no sticky ‘current mode’ context above the list, so returning from a long scroll loses place.", - }, - { - severity: "P2", - title: "Thumb targets are dense for a primary switcher", - detail: - "32 px icon wells inside 52 px rows pack the sheet, but the mode switcher is a high-frequency control — it should feel larger than a desktop menu ported down.", + title: "Selected state must survive density", + detail: "Soft fill alone is weak mid-list. A left rail + check keeps place without a second sticky card.", }, { severity: "P3", - title: "Header copy is generic", - detail: - "‘Choose mode / Switch the clinical workspace mode’ names the UI, not the clinician’s current place. A current-mode line orients faster.", - }, -] as const; - -const perfectedResolutions = [ - { - issue: "Flat catalogue", - fix: "Find / Diagnose / Care sections with sticky labels and counts — scan by clinical job.", - }, - { - issue: "Truncated subtitles", - fix: "Two-line `line-clamp-2` descriptions; inactive titles stay full `--text-heading` weight.", - }, - { - issue: "Lost place mid-scroll", - fix: "Current mode lives in the non-scrolling sheet header; section labels stick at `top-0` — no fragile offset.", - }, - { - issue: "Dense thumb targets", - fix: "`min-h-12` rows, 44 px icon wells, 10 px row gap rhythm — primary switcher, not a desktop menu port.", - }, - { - issue: "Generic header", - fix: "Header reads “Currently · Answer” with the live icon; updates when you pick another mode.", - }, - { - issue: "Duplicate current chrome", - fix: "Removed the bulky sticky “Now in” card that stole first-viewport height and repeated the selected row.", + title: "Header should orient, not instruct", + detail: "Currently · {mode} orients faster than ‘Switch the clinical workspace mode’.", }, ] as const; @@ -129,29 +75,29 @@ const variants: Array<{ changes: string[]; }> = [ { - id: "sections", - title: "Sectioned clinical list — perfected", - verdict: "YES · Master for intent", + id: "dense", + title: "Dense title list", + verdict: "YES · Fastest pick", description: - "Every review finding closed. Current mode sits in the sheet header, section labels are the only sticky layer, rows are phone-native, and selection is interactive. This is the direction to land.", + "Flat catalogue, title-only rows. Nearly every mode visible on open — pick by name without scrolling through section chrome. Shipping recommendation.", changes: [ - "Header carries “Currently · {mode}” — orients without a second card.", - "Sticky section labels only (`top-0`); no magic `4.6rem` offset under a strip.", - "`min-h-12` rows, 44 px wells, two-line descriptions, readable inactive titles.", - "Tap a mode: header and selection update together — proven in the Switched frame.", + "No Find / Diagnose / Care headers, hints, or counts.", + "Title-only min-h-12 rows; description stays in aria-label.", + "Header carries Currently · {mode}; selection lifts into the top-bar pill.", + "Arrow / Home / End roving focus; preview scroll stays inside the sheet.", ], }, { - id: "deck", - title: "Icon deck with lane chips", - verdict: "YES · Best overview", + id: "tiles", + title: "Flat icon tiles", + verdict: "YES · Icon-forward", description: - "Two-column mode cards plus Find / Diagnose / Care filter chips. Prefer this if browsing all modes matters more than a vertical reading order. YES 01 remains the shipping recommendation.", + "Same flat catalogue, presented as a compact two-column tile grid. Still no clinical lanes — icon + label for glance recognition.", changes: [ - "Lane chips filter the deck without opening a second surface.", - "Current mode sits in a full-width identity card above the grid.", - "Cards are icon-forward with two-line labels; selected card uses accent ring + check.", - "Same Sheet chrome (grip, title, close, safe-area) so the concept can drop into the live Sheet.", + "One continuous grid — no section labels or lane chips.", + "Compact tiles (icon + title) keep all modes in view.", + "Selected tile uses accent wash, ring, and corner check.", + "Use when icon recognition matters more than a single text column.", ], }, ]; @@ -268,11 +214,43 @@ function scrollContainerToTarget(container: HTMLElement, target: HTMLElement, bl container.scrollTop += targetCenter - containerCenter; } +/** Shared roving-focus state for flat mode catalogues; sheets keep their own key maps. */ +function useRovingModeFocus(selected: AppModeId, onSelectedChange: (id: AppModeId) => void) { + const optionRefs = useRef>([]); + const [focusIndex, setFocusIndex] = useState(Math.max(0, FLAT_MODE_IDS.indexOf(selected))); + + function focusModeOption(index: number) { + const next = Math.max(0, Math.min(FLAT_MODE_IDS.length - 1, index)); + setFocusIndex(next); + optionRefs.current[next]?.focus(); + } + + function selectMode(id: AppModeId) { + onSelectedChange(id); + const nextIndex = FLAT_MODE_IDS.indexOf(id); + if (nextIndex >= 0) setFocusIndex(nextIndex); + } + + return { optionRefs, focusIndex, focusModeOption, selectMode }; +} + +function CurrentlyLine({ modeId }: { modeId: AppModeId }) { + const mode = modeOf(modeId); + const Icon = appModeIcons[modeId]; + + return ( + + + + + Currently {mode.label} + + + ); +} + function CurrentShippingSheet() { - // `visibleAppModeDefinitions("production")`, not a local `!mode.devOnly`: - // `devOnly` is optional on the union, so reading it off every member does not - // typecheck (TS2339). app-modes.ts already exports this guard — passing - // "production" reproduces the intent here, which is the shipping mode list. const modes = visibleAppModeDefinitions("production"); return ( @@ -326,23 +304,7 @@ function CurrentShippingSheet() { ); } -function CurrentlyLine({ modeId }: { modeId: AppModeId }) { - const mode = modeOf(modeId); - const Icon = appModeIcons[modeId]; - - return ( - - - - - Currently {mode.label} - - - ); -} - -function ModeRow({ +function DenseModeRow({ modeId, active, index, @@ -368,108 +330,81 @@ function ModeRow({ type="button" role="menuitemradio" aria-checked={active} + aria-label={`${mode.label}. ${mode.description}`} tabIndex={tabIndex} onClick={() => onSelect(modeId)} onKeyDown={(event) => onKeyDown(event, index)} style={{ animationDelay: `${Math.min(index, 8) * 28}ms` }} className={cn( - "relative grid min-h-12 w-full grid-cols-[2.75rem_minmax(0,1fr)_1.75rem] items-center gap-2.5 rounded-xl px-2.5 py-2.5 text-left transition-[background-color,color,box-shadow] duration-[var(--duration-fast)] ease-[var(--ease-out-soft)] motion-safe:animate-[cascade-fade-up_var(--duration-moderate)_var(--ease-out-soft)_both] motion-reduce:animate-none motion-reduce:transition-none", + "relative grid min-h-12 w-full grid-cols-[2rem_minmax(0,1fr)_1.25rem] items-center gap-2 rounded-lg px-2 py-1 text-left transition-[background-color,color] duration-[var(--duration-fast)] ease-[var(--ease-out-soft)] motion-reduce:transition-none", focusRing, active - ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--text)] shadow-[var(--shadow-inset)]" + ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--text)]" : "text-[color:var(--text)] hover:bg-[color:var(--surface-subtle)]", )} > {active ? (