From 8effa5abe77e9008fb12f6ff996a51aa6d406ab5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 02:40:09 +0000 Subject: [PATCH 1/8] mockups(record-pages): three breadcrumb header directions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record pages that use InformationPageBreadcrumbs have no in-page section index, so the repository's default in-page navigation header is the wrong shape for them: its title disclosure opens a sheet listing one item and its weighted track renders one full-width segment. What those pages do have is a breadcrumb row followed by a wrapping toolbar — on a 390px phone the factsheet detail page spends three bands of chrome before the first line of content, and none of it is sticky. Adds /mockups/breadcrumb-header, a design-scratch study of three headers that keep the document header's row grammar (back, title, ellipsis, one scroll owner) and drop the section machinery: 01 Crumb rail identity and return only; actions in the sheet 02 Action rail + one promoted primary action pill 03 Crumb rail + mode + a segmented view mode in the track slot All three portal into the phone collapse slot below sm and stick from sm, keep the page's h1 in the hero, and share one actions sheet. No component is extracted yet — this is scratch for design review. Shared mockup chrome is suppressed for the route because each frame draws its own universal phone header. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBAt8pJVz2TxMEUJeWEdUy --- docs/site-map.md | 1 + mockups/README.md | 14 + src/app/mockups/breadcrumb-header/page.tsx | 12 + src/app/mockups/mockups-layout-client.tsx | 5 + src/components/breadcrumb-header-mockups.tsx | 625 +++++++++++++++++++ 5 files changed, 657 insertions(+) create mode 100644 src/app/mockups/breadcrumb-header/page.tsx create mode 100644 src/components/breadcrumb-header-mockups.tsx diff --git a/docs/site-map.md b/docs/site-map.md index 94bec969a8..b14a7ece19 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -986,6 +986,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/answer-evidence-popups` - Route discovered from app directory Source: `src/app/mockups/answer-evidence-popups/page.tsx`. - `/mockups/answer-home-proposal` - Route discovered from app directory Source: `src/app/mockups/answer-home-proposal/page.tsx`. +- `/mockups/breadcrumb-header` - Route discovered from app directory Source: `src/app/mockups/breadcrumb-header/page.tsx`. - `/mockups/calculators-bedside-sheet` - Route discovered from app directory Source: `src/app/mockups/calculators-bedside-sheet/page.tsx`. - `/mockups/calculators-clinical-console` - Route discovered from app directory Source: `src/app/mockups/calculators-clinical-console/page.tsx`. - `/mockups/calculators-directory-grid` - Route discovered from app directory Source: `src/app/mockups/calculators-directory-grid/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index 690fd4e85e..081b8f648d 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -60,3 +60,17 @@ Static desktop/phone comps for the pages that need redesign (not ModeHome mockup These are PNGs for design review only. Runnable `/mockups/*` routes are a separate implementation step. **Perfected combined comps** (desktop + phone in one image, recommended directions only) live in [`public/mockups/mode-page-redesign-2026-07/perfected-combined/`](../public/mockups/mode-page-redesign-2026-07/perfected-combined/README.md). + +## Breadcrumb header study (2026-08-09) + +Runnable study at [`/mockups/breadcrumb-header`](../src/app/mockups/breadcrumb-header/page.tsx): three sticky header directions for record pages that use `InformationPageBreadcrumbs` and have **no in-page section index** — factsheets, services, forms, DSM, specifiers, formulation, medications. + +`InPageNavHeader` stays the default for in-page navigation per [`docs/search-chrome-behaviour.md`](../docs/search-chrome-behaviour.md); it is the wrong shape here because a page with no sections gets a disclosure that opens a one-item sheet and a weighted track that renders one full-width segment. The directions keep that header's row grammar (back, title, ellipsis, one scroll owner) and drop the section machinery: + +| Direction | Adds | Fits | +| -------------------- | ------------------------------------- | --------------------------------------- | +| 01 Crumb rail | Nothing — identity and return only | Forms, DSM, specifiers, formulation | +| 02 Action rail | One promoted primary action pill | Factsheets, services, medications | +| 03 Crumb rail + mode | Segmented view mode in the track slot | Factsheet reading level, medication age | + +Shared mockup chrome is suppressed because each frame draws its own universal phone header. These are design scratch — no component was extracted from them yet. diff --git a/src/app/mockups/breadcrumb-header/page.tsx b/src/app/mockups/breadcrumb-header/page.tsx new file mode 100644 index 0000000000..1573ae7758 --- /dev/null +++ b/src/app/mockups/breadcrumb-header/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { BreadcrumbHeaderMockups } from "@/components/breadcrumb-header-mockups"; + +export const metadata: Metadata = { + title: "Breadcrumb header · mockups", + description: "Three sticky header directions for record pages that have no in-page section index.", +}; + +export default function BreadcrumbHeaderMockupsPage() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 4a2c9d166f..6fd0211ab2 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -30,6 +30,9 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { // them would read as a third, real band competing with the two on offer. const isSearchBandDirectionsMockup = pathname === "/mockups/search-band-directions"; const isPhoneInPageNavigationMockup = pathname === "/mockups/phone-inpage-navigation"; + // Draws its own universal phone header above every header under study; the + // shared chrome would read as a second, real header over the comparison. + const isBreadcrumbHeaderMockup = pathname === "/mockups/breadcrumb-header"; // Draws its own composer in every frame, and the notice under study is the one // the shared composer renders — showing both would put two different privacy // lines on screen at once. @@ -83,6 +86,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isUniversalSearchRedesignMockup && !isCalculatorsSearchPageMockup && !isPhoneInPageNavigationMockup && + !isBreadcrumbHeaderMockup && !isSearchBandDirectionsMockup && !isTherapyNavigationMockup && !isWarningConsolidationMockup && @@ -100,6 +104,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isSearchRefineAdaptiveMockup && !isSearchBandDirectionsMockup && !isPhoneInPageNavigationMockup && + !isBreadcrumbHeaderMockup && !isTherapyNavigationMockup && !isWarningConsolidationMockup && !isWarningLineMockup && diff --git a/src/components/breadcrumb-header-mockups.tsx b/src/components/breadcrumb-header-mockups.tsx new file mode 100644 index 0000000000..1991d4b47b --- /dev/null +++ b/src/components/breadcrumb-header-mockups.tsx @@ -0,0 +1,625 @@ +"use client"; + +import { + ArrowLeft, + Bookmark, + BookOpen, + ChevronDown, + Clock, + Download, + Ellipsis, + Link2, + Menu, + MessageSquarePlus, + Pill, + Printer, + type LucideIcon, +} from "lucide-react"; + +import { cn } from "@/components/ui-primitives"; + +/** + * Design scratch: a breadcrumb header for record pages that have no in-page + * sections. + * + * `docs/search-chrome-behaviour.md` makes the DocumentViewer header the default + * for **in-page navigation** — back control, title + active-section disclosure, + * ellipsis actions, weighted segment track. Eight pages use + * `InformationPageBreadcrumbs` instead (factsheets, services, forms, DSM, + * specifiers, formulation, medications), and they have nothing for that track to + * measure: there is no section index, so the disclosure opens a sheet listing + * one thing and the track renders a single full-width segment. + * + * What those pages do have is a breadcrumb row followed by a wrapping toolbar. + * On a 390 px phone the factsheet detail page spends three wrapped rows — + * roughly 250 px — on crumb, reading-level segment, Save and Download PDF + * before the first word of content, and none of it is sticky, so the return + * path scrolls away with it. + * + * These three directions keep the document header's row grammar and drop the + * parts that only make sense when a page has sections. + */ + +const parent = { label: "All factsheets", href: "/factsheets/search" }; +const pageTitle = "Sertraline"; +const pageBrand = "(Zoloft)"; + +type VariantId = "crumb" | "action" | "mode"; +type FrameState = "rest" | "scrolled"; +type ReadingLevel = "easy" | "standard"; + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +const sheetActions: Array<{ label: string; detail: string; icon: LucideIcon }> = [ + { label: "Download PDF", detail: "Print-styled patient copy", icon: Download }, + { label: "Save factsheet", detail: "Adds to Favourites", icon: Bookmark }, + { label: "Copy link", detail: "Share with a colleague", icon: Link2 }, + { label: "Print", detail: "Uses the same print stylesheet", icon: Printer }, +]; + +const variants: Array<{ + id: VariantId; + title: string; + verdict: string; + description: string; + notes: string[]; + fits: string; +}> = [ + { + id: "crumb", + title: "Crumb rail", + verdict: "Leanest · widest reuse", + description: + "Identity and return, nothing else. The row is back control, page title, ellipsis — the document header with the section disclosure and the segment track removed rather than left empty. Every page action moves into the actions sheet, so the body carries no toolbar at all.", + notes: [ + "One 56 px row replaces the crumb row plus the wrapped toolbar: about 190 px back on a 390 px phone.", + "Title is a ``; the page keeps its `

` in the hero, so there is still exactly one heading.", + 'Back control is icon-only below `sm` and always carries `aria-label="Back to all factsheets"`.', + "Costs one tap to reach Download PDF — the honest price of the smallest header.", + ], + fits: "Forms, DSM criteria, specifiers, formulation — pages whose actions are secondary to reading.", + }, + { + id: "action", + title: "Action rail", + verdict: "Keeps the primary action in reach", + description: + "The same row, with the page's one primary action promoted to a pinned pill: icon-only on a phone, icon and label from `sm`. Everything else stays in the ellipsis sheet. This is what makes the header worth pinning — the action you came for is reachable from any scroll position, not only from the top of the page.", + notes: [ + "Exactly one promoted action. A second pill is what turned the original toolbar into three rows.", + "The pill is the same 48 px target as the ellipsis and sits inside the row, not below it.", + "Sheet keeps the full action list, including the promoted one, so the sheet is never a partial menu.", + "Promoted action is a page-level prop, not a fixed slot: services promote Refer, factsheets promote Download PDF.", + ], + fits: "Factsheets, services, medications — pages with one obvious verb.", + }, + { + id: "mode", + title: "Crumb rail + mode track", + verdict: "Handles a page-level view mode", + description: + "The action rail plus a segmented control on the header's bottom edge, in the exact slot the document header gives `DocumentSectionTrack`. It reads as the same family, but the segments switch how the page renders rather than where you are in it — which is what the factsheet reading level actually is.", + notes: [ + "Occupies the track slot, so the header stays two conceptual bands, not three stacked rows.", + "Segments are `aria-pressed` toggles in a labelled group, not tabs — no panel semantics, no roving focus.", + "Renders only when the page has a mode; otherwise the header is direction 02 exactly.", + "Active segment uses `--clinical-accent` on the underline, matching the track's active styling.", + ], + fits: "Factsheets (Easy read / Standard) and medications (Adult / Paediatric).", + }, +]; + +/* ---------------- shared chrome ---------------- */ + +/** Stand-in for the universal phone search header the real header portals under. */ +function UniversalPhoneHeader({ hidden = false }: { hidden?: boolean }) { + return ( + + ); +} + +function BackControl({ showLabel }: { showLabel: boolean }) { + return ( + + + ); +} + +function HeaderTitle() { + return ( + + {pageTitle} {pageBrand} + + ); +} + +function PrimaryActionPill({ showLabel }: { showLabel: boolean }) { + return ( + + ); +} + +function ActionsTrigger() { + return ( + + ); +} + +function ModeTrack({ level }: { level: ReadingLevel }) { + return ( +
+ {( + [ + ["easy", "Easy read"], + ["standard", "Standard"], + ] as const + ).map(([value, label]) => { + const active = value === level; + return ( + + ); + })} +
+ ); +} + +/* ---------------- the three headers ---------------- */ + +function BreadcrumbHeader({ + variant, + wide = false, + level = "easy", +}: { + variant: VariantId; + /** `true` renders the `sm+` shape: visible ancestor label and action labels. */ + wide?: boolean; + level?: ReadingLevel; +}) { + return ( +
+
+ + + {variant === "crumb" ? null : } + +
+ {variant === "mode" ? : null} +
+ ); +} + +/* ---------------- page body under the header ---------------- */ + +function HeroCard() { + return ( +
+
+ + +
+

+ Medications +

+

+

+
+
+

+ {pageTitle} {pageBrand} +

+

+ A commonly used SSRI for depression and anxiety — how it works, how to take it, and what to expect. +

+
+ ); +} + +function GlanceCard() { + return ( +
+

At a glance

+
+ {[ + ["Drug class", "SSRI antidepressant"], + ["Common brand", "Zoloft"], + ["Usual dose", "50–200 mg daily"], + ["Takes effect", "2–6 weeks"], + ].map(([term, detail]) => ( +
+
{term}
+
{detail}
+
+ ))} +
+
+ ); +} + +function ProseBlock({ heading }: { heading: string }) { + return ( +
+

{heading}

+
+ {[1, 0.92, 0.97, 0.7].map((width, index) => ( +
+ ))} +
+
+ ); +} + +function PageBody({ scrolled }: { scrolled: boolean }) { + return ( +
+ {scrolled ? ( + <> + + + + + ) : ( + <> + + + + + )} +
+ ); +} + +/* ---------------- the toolbar being replaced ---------------- */ + +function TodayHeader() { + return ( +
+
+
+
+ + + Easy read + + + Standard + + + + + + +
+
+ ); +} + +/* ---------------- frames ---------------- */ + +const stateCopy: Record = { + rest: "At rest · page top", + scrolled: "Scrolled · chrome revealed", +}; + +function PhoneFrame({ + children, + caption, + tone = "quiet", +}: { + children: React.ReactNode; + caption: string; + tone?: "quiet" | "warn"; +}) { + return ( +
+

+ {caption} +

+
+ {children} +
+
+ ); +} + +function VariantFrame({ variant, state }: { variant: VariantId; state: FrameState }) { + return ( + + + + + + ); +} + +function ActionsSheetPreview() { + return ( +
+
+

This factsheet

+ Shared `Sheet` +
+

Sertraline · updated Jul 2026

+
    + {sheetActions.map(({ label, detail, icon: Icon }) => ( +
  • + + +
  • + ))} +
+
+ ); +} + +function VariantShowcase({ variant, number }: { variant: (typeof variants)[number]; number: number }) { + return ( +
+
+
+

+ Direction {String(number).padStart(2, "0")} · {variant.verdict} +

+

+ {variant.title} +

+
+

+ {variant.description} +

+
+ +
    + {variant.notes.map((note) => ( +
  • + {note} +
  • + ))} +
+ +
+ + +
+

+ `sm+` · sticky, labels visible +

+
+ +
+ +
+
+

{variant.fits}

+
+
+
+ ); +} + +/* ---------------- page ---------------- */ + +export function BreadcrumbHeaderMockups() { + return ( +
+
+
+

+ Record pages · Breadcrumb header +

+

+ A sticky header for pages that have no sections +

+

+ `InPageNavHeader` is the repository default for in-page navigation, and it is the wrong shape for the eight + pages that use `InformationPageBreadcrumbs`: they have no section index, so its disclosure opens a sheet + listing one item and its weighted track renders one full-width segment. These three directions keep that + header’s row grammar — back, title, ellipsis, one owner for scroll — and replace the section machinery + with what breadcrumb pages actually carry: a return path and a small set of page actions. +

+
+ {[ + [ + "Scroll ownership", + "Below `sm` the header portals into `#phone-header-collapse-addon-slot` and hides with the universal chrome. No second scroll-hide hook, ever.", + ], + [ + "Sticky at `sm+`", + "`sticky top-0`, `z-30`, opaque `--surface`, `border-b` — the same pinning the document header uses.", + ], + [ + "One heading", + "The header title is a ``; the page keeps its `

` in the hero, so nothing gains a second `

`.", + ], + ].map(([term, detail]) => ( +
+
+ {term} +
+
{detail}
+
+ ))} +

+
+ +
+

+ Today · what is being replaced +

+

+ Crumb row, then a toolbar that wraps +

+
+ + + + + +
    + {[ + "Three rows of chrome before the first line of content, and the toolbar wraps differently per page depending on how many actions it has.", + "Nothing is sticky: scroll to the side-effects section and the way back to the factsheet list is gone.", + "The reading-level segment is a view mode, but it sits in the row reserved for verbs, so it reads as a third action.", + "Save and Download PDF are full-width-ish pills competing with each other; neither is clearly primary.", + ].map((problem) => ( +
  • + {problem} +
  • + ))} +
+
+
+ +
+ {variants.map((variant, index) => ( + + ))} +
+ +
+

+ Shared by all three +

+

+ The actions sheet is the whole toolbar +

+

+ The ellipsis opens the same `Sheet` the document header uses, listing every page action including any + promoted one. A page with no actions renders no ellipsis at all rather than a control that opens an empty + sheet — the behaviour `InPageNavHeader` already has. +

+
+ +
+
+
+
+ ); +} From 003d3cdc4c2354642107d64895ff28fe537478df Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 02:43:17 +0000 Subject: [PATCH 2/8] docs(ledger): record the breadcrumb header mockup study review Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBAt8pJVz2TxMEUJeWEdUy --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 17f12774aa..5010fe536c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -826,3 +826,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-08-08 | cursor/more-modes-popup-2f4b | 04e6a80653c3ccf103577f6c3886b162498621ed | sidebar more-modes sheet popup | pass | focused-pw tablet rail; test:focused ClinicalSidebar; favourites+therapy wiring; verify:pr-local stages+build+rag-fixtures | | 2026-08-08 | cursor/more-modes-popup-2f4b | bea4b0c09b74368cf6d63e944bac9c1eec6b0c93 | sidebar more-modes sheet popup | pass | focused-pw tablet rail; test:focused ClinicalSidebar; favourites+therapy wiring; verify:pr-local stages+build+rag-fixtures | | 2026-08-09 | claude/document-viewer-optimization-tu8tnj | 5a0d6be02bc92fa2615d2141b338ec8f7c1143b1 | docs: document-viewer Phase 3 handover brief (PR #1765) | Supersedes the earlier row, whose 'all ten gates completed' wording could read as all executable checks having run. Correct scope: verify:pr-local ran the ten gates APPLICABLE to docs-only changes (check:runtime, check:installed-lock-parity, format:changed, sitemap:check, docs:check-index, docs:check-inventory, docs:check-scripts, docs:check-links, check:branch-review-ledger, check:outstanding-issues); the risk router SKIPPED lint, typecheck, the full unit suite, RAG fixture validation, and build as recognised low-risk documentation scope. Also records the merge resolution: duplicate #286 (main's in-page-nav series vs this branch's authorizationHeader row) resolved by renumbering the branch row to #289, next-id 290, after the auto-merge silently dropped that detail row rather than conflicting. Review findings addressed: governance preflight now required by behaviour per AGENTS.md:257 rather than inferred from pr-policy path classification; API-route scope contradiction resolved; signed-URL warning corrected to state both identity bugs are already fixed on main with regression coverage. | verify:pr-local ten docs-scope gates passed, none failed; check:outstanding-issues 287 rows unique ids next-id=290 no ids deleted; ledger:dedupe 771 unique rows; git merge-tree vs origin/main exit 0; viewer line refs re-verified against 50ef12e | +| 2026-08-09 | claude/breadcrumb-header-mockups-cei6lw | 8effa5abe77e9008fb12f6ff996a51aa6d406ab5 | mockups: breadcrumb header study (3 directions) + sitemap/README | self-reviewed; design-scratch only, no production surface changed | typecheck, eslint(changed), prettier --check, sitemap:check, vitest(site-map/mockup-boundary/env-mockups/docs-inventory/route-reachability) 23 passed | From ca5e4e7ae9b53af49b362ad11ca988fdd1c3a9d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 03:22:34 +0000 Subject: [PATCH 3/8] feat(record-pages): ship the breadcrumb header on factsheets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The factsheet detail page carried a breadcrumb row plus a toolbar that wrapped to three bands on a phone — roughly 250px before the first line of content — and none of it stuck, so the way back scrolled away with it. This replaces that with the header direction chosen from the /mockups/breadcrumb-header study. Extends InPageNavHeader rather than adding a third header copy: it already branched to a plain title when a page had no sections, and usePageSectionWeights observes nothing for an empty list, so the breadcrumb shape costs those pages no measurement. Four optional props, every default preserving today's behaviour so the differentials adopter is unchanged: sections? omit to drop the disclosure, section sheet and track primaryAction one promoted action, Button variant="secondary" mode a page-level view mode via the shared SegmentedControl showBackLabel false keeps the arrow alone so the title owns the row The ellipsis drops to a flat face only when a primary action is present: `secondary` is stylistically identical to its bordered face, so beside one the row would read as two equal buttons. Reading level rides the `mode` slot — it is how the page renders, not where you are in it — and only `medRich` sheets (1 of 8) get it. Below `sm` it wraps to its own full-width band; from `sm` it is inline and costs no extra height. Measured on /factsheets/sertraline: 131px phone, 75px from sm, 65px with no mode. `layout="fit"` there is load-bearing. `equal` gives each segment an 8rem floor sized for a full-width group, but `sm:w-auto` makes the group shrink-to-fit and its intrinsic width comes from the labels — 171px against 268px of segments, which overflowed under the primary action at 700-834px. Also: registers the factsheet detail routes as header-addon-slot claimants, moves the save-failure live region with Save, re-offsets the lg sidebar to clear both headers, and adds the route to the phone-scroll matrix — no browser test visited a factsheet detail route before. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NBAt8pJVz2TxMEUJeWEdUy --- docs/design-system/COMPONENTS.md | 4 +- docs/design-system/adoption-manifest.json | 10 +- docs/search-chrome-behaviour.md | 46 ++++- mockups/README.md | 4 +- .../factsheets/factsheet-detail-page.tsx | 127 +++++++------ .../in-page-nav/in-page-nav-header.tsx | 172 ++++++++++++++---- src/components/mode-nav/header-addon-slot.ts | 3 + tests/factsheet-detail-header.dom.test.tsx | 64 +++++++ tests/factsheet-save.dom.test.tsx | 27 ++- tests/helpers/phone-scroll.ts | 11 ++ tests/in-page-nav-header.dom.test.tsx | 62 +++++++ tests/mode-nav-addon-slot.dom.test.tsx | 1 + 12 files changed, 414 insertions(+), 117 deletions(-) create mode 100644 tests/factsheet-detail-header.dom.test.tsx diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index f056dc154d..601576f07b 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -958,7 +958,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `AnswerFooter` | answer | yes | yes | no | yes | no | 0 | | `AsyncButton` | controls | yes | yes | inherited-global-root | yes | no | 4 | | `Breadcrumb` | layout | yes | yes | inherited-global-root | yes | no | 1 | -| `Button` | controls | yes | yes | inherited-global-root | yes | no | 2 | +| `Button` | controls | yes | yes | inherited-global-root | yes | no | 3 | | `Checkbox` | controls | yes | yes | no | yes | no | 0 | | `Chip` | controls | yes | yes | inherited-global-root | yes | no | 3 | | `Citation` | source | yes | yes | no | yes | no | 0 | @@ -990,7 +990,7 @@ This generated snapshot is a local source-derived inventory. It does not assert | `RetrievalStateBanner` | answer | yes | yes | inherited-global-root | yes | no | 1 | | `SafeBoldText` | layout | yes | yes | inherited-global-root | yes | no | 8 | | `SearchField` | controls | yes | yes | no | yes | no | 0 | -| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 2 | +| `SegmentedControl` | controls | yes | yes | inherited-global-root | yes | no | 3 | | `Select` | controls | yes | yes | inherited-global-root | yes | no | 2 | | `Sheet` | layout | yes | yes | inherited-global-root | yes | no | 23 | | `Skeleton` | feedback | yes | yes | inherited-global-root | yes | no | 6 | diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index dbfd5d14a5..ebfd7b6b2c 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -272,11 +272,13 @@ "directImportFiles": [ "src/components/AccessibleTable.tsx", "src/components/clinical-dashboard/signed-image.tsx", + "src/components/in-page-nav/in-page-nav-header.tsx", "src/components/ui/confirm-dialog.tsx" ], "productImportFiles": [ "src/components/AccessibleTable.tsx", - "src/components/clinical-dashboard/signed-image.tsx" + "src/components/clinical-dashboard/signed-image.tsx", + "src/components/in-page-nav/in-page-nav-header.tsx" ], "designSync": { "listedInSourceMap": true, @@ -1338,11 +1340,13 @@ "entryExported": true, "directImportFiles": [ "src/components/clinical-dashboard/patient-profile-panel.tsx", - "src/components/clinical-dashboard/settings-dialog.tsx" + "src/components/clinical-dashboard/settings-dialog.tsx", + "src/components/in-page-nav/in-page-nav-header.tsx" ], "productImportFiles": [ "src/components/clinical-dashboard/patient-profile-panel.tsx", - "src/components/clinical-dashboard/settings-dialog.tsx" + "src/components/clinical-dashboard/settings-dialog.tsx", + "src/components/in-page-nav/in-page-nav-header.tsx" ], "designSync": { "listedInSourceMap": true, diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index c61433e33f..31e0866571 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -4,15 +4,16 @@ This repo uses one shared search experience across the global shell, dashboard r ## Page ownership model -| Page state | Composer placement | Reserve owner | -| ----------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -| Answer home / standalone mode homes | In-flow hero composer on phones and larger breakpoints | Page content; no fixed phone dock reserve | -| Submitted/search-result views | Compact bottom dock on phones; in normal page flow on tablets and desktops | Shell/dashboard `--mobile-composer-reserve` on phones; page content on desktop | -| Answer result view | Overlaid glass header plus answer composer dock | Dashboard `#main-content` top/bottom reserves | -| Document detail/source routes | `DocumentViewer` floating composer | `DocumentViewer` content padding | -| Document section navigation | Header row disclosure (phone sheet) + rail index card at `lg` | None — adds no chrome and no reserve | -| Calculators (`/calculators`) | Page-owned composer (desktop top + phone bottom dock) | Calculators page pad; shell reserve stays `0` | -| Info/detail pages with no composer | No fixed composer | Idle shell padding only | +| Page state | Composer placement | Reserve owner | +| ----------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | +| Answer home / standalone mode homes | In-flow hero composer on phones and larger breakpoints | Page content; no fixed phone dock reserve | +| Submitted/search-result views | Compact bottom dock on phones; in normal page flow on tablets and desktops | Shell/dashboard `--mobile-composer-reserve` on phones; page content on desktop | +| Answer result view | Overlaid glass header plus answer composer dock | Dashboard `#main-content` top/bottom reserves | +| Document detail/source routes | `DocumentViewer` floating composer | `DocumentViewer` content padding | +| Document section navigation | Header row disclosure (phone sheet) + rail index card at `lg` | None — adds no chrome and no reserve | +| Record page breadcrumb header | Same header row without the disclosure or track; view mode inline from `sm` | None — portals into the phone collapse row, sticky at `sm+` | +| Calculators (`/calculators`) | Page-owned composer (desktop top + phone bottom dock) | Calculators page pad; shell reserve stays `0` | +| Info/detail pages with no composer | No fixed composer | Idle shell padding only | ## Default in-page navigation template @@ -57,6 +58,33 @@ detailed DocumentViewer rules remain invariant 22 — but new work mounts `InPag - Do not give the in-page header its own scroll-hide hook; share the universal collapse signal described under “Scroll hide/reveal”. +**The breadcrumb shape (pages with no section index).** The eight record pages behind +`InformationPageBreadcrumbs` have no sections, so the disclosure would open a sheet listing +one item and the track would render one full-width segment. Omit `sections` and +`InPageNavHeader` drops both and renders the breadcrumb shape instead — same row grammar, +same single collapse owner, none of the section machinery. `usePageSectionWeights` observes +nothing for an empty list, so those pages pay no measurement cost. Three optional props +shape that row: + +- `showBackLabel={false}` keeps the arrow alone at every width when the row also carries an + action or a mode, so the title owns the space. `back.label` is still the accessible name + and becomes the desktop tooltip. +- `primaryAction` promotes exactly **one** page action, as `Button variant="secondary"` — + not the filled `--command` slab, because a control pinned to every scroll position should + not be the page's heaviest. Its label is `sr-only` below `sm` so the accessible name does + not change with the breakpoint. A second promoted control is what turns the row back into + the wrapping toolbar this shape replaced; everything else belongs in `actions`. +- `mode` is a page-level **view** mode — how the page renders, not where you are in it — and + uses the shared `SegmentedControl`. Below `sm` it wraps to its own full-width band under + the row; from `sm` it sits inline and costs no extra height (measured on + `/factsheets/sertraline`: 131px phone, 75px from `sm`, 65px with no mode). + +Adopted by `src/components/factsheets/factsheet-detail-page.tsx`. When a page adopts this, +register its routes in `isHeaderAddonSlotOwnedRoute` +(`src/components/mode-nav/header-addon-slot.ts`) and add the component to the expected +claimants in `tests/mode-nav-addon-slot.dom.test.tsx`, or that guard fails: the slot holds +exactly one page-owned header. + **Not this template:** Therapy-style `ModeNav` (multi-route mode tabs via `ModeNavHeaderPortal`) is a different pattern for mode-level page switching. Info-page `PageHeader` / breadcrumb chrome is also not in-page section navigation. Existing diff --git a/mockups/README.md b/mockups/README.md index 081b8f648d..7e5381fe75 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -73,4 +73,6 @@ Runnable study at [`/mockups/breadcrumb-header`](../src/app/mockups/breadcrumb-h | 02 Action rail | One promoted primary action pill | Factsheets, services, medications | | 03 Crumb rail + mode | Segmented view mode in the track slot | Factsheet reading level, medication age | -Shared mockup chrome is suppressed because each frame draws its own universal phone header. These are design scratch — no component was extracted from them yet. +Shared mockup chrome is suppressed because each frame draws its own universal phone header. + +**Outcome: direction 02 shipped**, as the breadcrumb shape of the existing `InPageNavHeader` rather than a new component — omitting `sections` drops the disclosure and the track, and `primaryAction` / `mode` / `showBackLabel` shape the row. Adopted first on `/factsheets/`, where the reading level rides the `mode` slot. Contract: `docs/search-chrome-behaviour.md` ("The breadcrumb shape"). The study stays as the record of the alternatives. diff --git a/src/components/factsheets/factsheet-detail-page.tsx b/src/components/factsheets/factsheet-detail-page.tsx index 2e5f2e6a10..b1ddb2b74f 100644 --- a/src/components/factsheets/factsheet-detail-page.tsx +++ b/src/components/factsheets/factsheet-detail-page.tsx @@ -27,7 +27,8 @@ import { type Factsheet, } from "@/components/factsheets/factsheets-data"; import { factsheetGlyph } from "@/components/factsheets/factsheets-icons"; -import { InformationPageBreadcrumbs, InformationPageShell } from "@/components/information-page-shell"; +import { InPageNavHeader } from "@/components/in-page-nav/in-page-nav-header"; +import { InformationPageShell } from "@/components/information-page-shell"; import { cn, toneDanger, toneWarning } from "@/components/ui-primitives"; import { readSavedRegistrySlugs, @@ -40,6 +41,11 @@ function accentBorder(accent: string) { return `color-mix(in srgb, ${accent} 35%, var(--surface))`; } +const readingLevelOptions = [ + { value: "easy", label: "Easy read" }, + { value: "standard", label: "Standard" }, +]; + function Heading({ children }: { children: ReactNode }) { return

{children}

; } @@ -106,69 +112,55 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) { return ( <> - {/* action bar */} -
-
- -
- {factsheet.kind === "medRich" ? ( -
- {(["easy", "standard"] as const).map((level) => { - const isActive = readingLevel === level; - return ( - - ); - })} -
- ) : null} - - - {saveNotice} - - -
-
-
+ {/* The breadcrumb shape of the shared in-page header: no section index, + so no disclosure and no segment track. Reading level is a page-level + view mode rather than an action, so it rides the `mode` slot and + stays out of the row reserved for verbs. */} + setReadingLevel(value === "standard" ? "standard" : "easy"), + } + : undefined + } + actionsTitle="This factsheet" + actionsDescription={`${factsheet.title} · updated ${factsheet.reviewedOn}`} + actionsNoun="factsheet" + testIdPrefix="factsheet" + actions={() => ( + // Deliberately does not close the sheet: saving is a state change + // you want to see reflected on the control you just pressed. + + )} + /> + {/* The only surfacing of the save-failure path, so it moves with Save + rather than being dropped with the toolbar that used to host it. */} + + {saveNotice} +
@@ -349,7 +341,12 @@ export function FactsheetDetailPage({ factsheet }: { factsheet: Factsheet }) {
{/* sidebar */} -