From 8493f10a6489ebaa8ef1cabcd23b6c5cd0913b64 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:08:42 +0000 Subject: [PATCH 1/5] Give the Guide Centre footer search the shared edge-to-edge phone dock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Guide Centre composer was the only phone search bar that did not use the shared dock chrome. It rides the generic `Sheet` footer slot, and `Sheet` always wraps that slot in `border-t border-[color:var(--border)] p-3 sm:p-4`; the guide then added `bg-[color:var(--surface-raised)]` and `shadow-[var(--e4)]` on top. On phones that produced a full-width opaque raised band with a hard top border sitting over the content behind the pill — the "cover" the shared dock exists to avoid. Every other phone composer keeps the band itself transparent (`.answer-footer-search-dock.answer-footer-search-edge`: flush left/right/bottom, safe-area padding, `background: transparent`) and paints one localized `.answer-footer-search-backdrop` scrim that tints around the pill and tapers to zero at the physical edge. Adopt exactly that pair on the guide footer and move the band's own border, surface and elevation to `sm:` only, so the tablet/desktop dialog footer is unchanged. The dock stays on the default scrim height rather than `document-mobile-search-compact`, because the tour action row sits above the pill in the same shape as the differentials-compare and patient-details dock addons. Measured on a 393x852 phone: left 0, right 0, bottom 0, width 393, background rgba(0,0,0,0), border-top 0px, no box-shadow, 160px scrim. At 1024 wide the footer is still static, `--surface-raised`, 1px top border, 16px padding, scrim hidden. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012gATrPoaRwCEU21gex48Ve --- docs/design-system/adoption-manifest.json | 1 + docs/search-chrome-behaviour.md | 47 +++-- .../clinical-dashboard/guide-dialog.tsx | 173 ++++++++++-------- .../guide-centre-design-contract.dom.test.tsx | 29 ++- 4 files changed, 160 insertions(+), 90 deletions(-) diff --git a/docs/design-system/adoption-manifest.json b/docs/design-system/adoption-manifest.json index 2d88695c2e..02b561f45e 100644 --- a/docs/design-system/adoption-manifest.json +++ b/docs/design-system/adoption-manifest.json @@ -1611,6 +1611,7 @@ "tests/document-filter-panel.dom.test.tsx", "tests/document-organization.test.ts", "tests/factsheet-save.dom.test.tsx", + "tests/guide-centre-design-contract.dom.test.tsx", "tests/header-scroll-hide-contract.test.ts", "tests/image-lightbox-geometry.test.ts", "tests/in-page-nav-route-sections.dom.test.tsx", diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index f28f5e564a..ccd3760729 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -4,17 +4,42 @@ 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 | -| Tools directory (`/tools`, no submitted query) | Compact bottom dock on phones; in-flow hero composer from `sm` | Shell dock reserve on phones; page content from `sm` | -| 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`) | In-flow hero composer at home; shared compact dock after submission | Page content at home; shell reserve for submitted results | -| 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 | +| Tools directory (`/tools`, no submitted query) | Compact bottom dock on phones; in-flow hero composer from `sm` | Shell dock reserve on phones; page content from `sm` | +| 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`) | In-flow hero composer at home; shared compact dock after submission | Page content at home; shell reserve for submitted results | +| Info/detail pages with no composer | No fixed composer | Idle shell padding only | +| Guide Centre dialog (`GuideDialog`) | Shared edge-to-edge phone dock inside the Sheet footer; Sheet footer band from `sm` | `[data-guide-content]` bottom pad (`guide-search-dock` reserve owner) | + +### The Guide Centre footer is a dock, not a footer band + +`GuideDialog` renders its search composer through the shared `Sheet` footer slot, and +`Sheet` always wraps that slot in `border-t border-[color:var(--border)] p-3 sm:p-4` +(`src/components/ui/sheet.tsx`). On phones that band is the wrong chrome: an opaque +`--surface-raised` slab with a hard top border reads as a cover over the content behind +the composer, which is exactly what every other phone composer avoids. + +So the guide footer carries `answer-footer-search-dock answer-footer-search-edge` and +renders one `.answer-footer-search-backdrop` child, the same pair the shell dock uses. +`globals.css` then owns the phone geometry — flush `left/right/bottom: 0`, safe-area +padding, `background: transparent` — and the scrim tints only around the pill before +tapering to zero at the physical edge. The band's own border, surface and elevation are +`sm:` only, and the scrim is `sm:hidden`, so the tablet/desktop dialog footer is +unchanged. + +Two consequences worth keeping: + +- The dock stays on the **default** scrim height, not `document-mobile-search-compact`: + the tour action row sits above the pill, the same shape the differentials-compare and + patient-details dock addons take, and the compact 5rem scrim would end mid-row. +- The footer wrapper is the dock element, so its children need `relative z-10` to paint + above the scrim. ## Default in-page navigation template diff --git a/src/components/clinical-dashboard/guide-dialog.tsx b/src/components/clinical-dashboard/guide-dialog.tsx index cd9dd51e24..4339b6934d 100644 --- a/src/components/clinical-dashboard/guide-dialog.tsx +++ b/src/components/clinical-dashboard/guide-dialog.tsx @@ -789,93 +789,100 @@ function GuideDialogSession({ onClose }: { onClose: () => void }) { ? "Review guided tour" : "Resume guided tour"; const footer = ( -
-
- {view === "tour" && !tourComplete ? ( - <> - - - - - ) : view === "tour" && tourComplete ? ( - - ) : ( - <> - - {view === "topic" || view === "topics" ? ( + <> + {/* Same localized glass the shared phone dock paints: the footer band itself + stays transparent and this scrim tints only around the pill, tapering to + zero at the physical edge. Without it the Sheet's opaque footer surface + reads as a slab covering the content behind the composer. */} + + ); const hasSearch = query.trim().length > 0; @@ -909,7 +916,17 @@ function GuideDialogSession({ onClose }: { onClose: () => void }) { mobilePlacement="fullscreen" footer={footer} footerClassName={cn( - "absolute inset-x-0 bottom-0 z-30 border-t border-[color:var(--border)] bg-[color:var(--surface-raised)] pl-[max(0.75rem,env(safe-area-inset-left))] pr-[max(0.75rem,env(safe-area-inset-right))] pb-[max(0.75rem,env(safe-area-inset-bottom))] pt-3 shadow-[var(--e4)] transition-[transform,opacity] duration-[var(--duration-moderate)] motion-reduce:transition-none sm:static sm:p-4 sm:shadow-none", + // Phones use the SHARED edge-to-edge dock chrome, not a Sheet footer band: + // `.answer-footer-search-dock.answer-footer-search-edge` (globals.css) owns + // the flush left/right/bottom geometry, the safe-area padding and the + // transparent background, exactly as every other phone composer does. The + // border/surface/elevation below are therefore sm+ only — on phones they + // painted an opaque slab across the content behind the composer. The dock + // stays on the DEFAULT scrim height, not `document-mobile-search-compact`: + // the tour action row sits above the pill here, the same shape the + // differentials/patient-details dock addons take. + "answer-footer-search-dock answer-footer-search-edge", + "absolute inset-x-0 bottom-0 z-30 border-t-0 bg-transparent p-0 shadow-none transition-[transform,opacity] duration-[var(--duration-moderate)] motion-reduce:transition-none sm:static sm:border-t sm:border-[color:var(--border)] sm:bg-[color:var(--surface-raised)] sm:p-4", chromeHidden && "pointer-events-none translate-y-full opacity-0 sm:pointer-events-auto sm:translate-y-0 sm:opacity-100", )} diff --git a/tests/guide-centre-design-contract.dom.test.tsx b/tests/guide-centre-design-contract.dom.test.tsx index 09258d48c3..d5b8692960 100644 --- a/tests/guide-centre-design-contract.dom.test.tsx +++ b/tests/guide-centre-design-contract.dom.test.tsx @@ -21,7 +21,34 @@ it("keeps Guide chrome on approved elevation and non-layout transitions", () => expect(footer).not.toBeNull(); expect(header).toHaveClass("transition-[border-color,opacity]"); expect(header).not.toHaveClass("transition-[max-height,padding,border-color,opacity]"); - expect(footer).toHaveClass("shadow-[var(--e4)]"); expect(footer).not.toHaveClass("shadow-[var(--shadow-elevated)]"); expect(footer).toHaveClass("transition-[transform,opacity]"); }); + +/** + * The phone footer is the SHARED edge-to-edge composer dock, not a Sheet footer + * band. Its own surface/border/elevation are sm+ only: on phones they painted an + * opaque `--surface-raised` slab across the content behind the search pill, which + * is exactly the "big cover" every other phone composer avoids by staying + * transparent behind a localized `.answer-footer-search-backdrop` scrim. + */ +it("gives the Guide footer the shared phone composer dock chrome", () => { + render(); + + const dialog = screen.getByRole("dialog", { name: "Clinical KB guide" }); + const footer = dialog.querySelector("[data-guide-mobile-footer]")?.parentElement; + + expect(footer).not.toBeNull(); + expect(footer).toHaveClass("answer-footer-search-dock"); + expect(footer).toHaveClass("answer-footer-search-edge"); + + // Phone band chrome is off; the sm+ Sheet footer keeps it. + expect(footer).toHaveClass("border-t-0", "bg-transparent", "shadow-none", "p-0"); + expect(footer).toHaveClass("sm:border-t", "sm:bg-[color:var(--surface-raised)]", "sm:p-4"); + expect(footer).not.toHaveClass("border-t", "bg-[color:var(--surface-raised)]"); + + // The scrim the dock geometry tints with, hidden once the footer is a real band. + const backdrop = footer?.querySelector(".answer-footer-search-backdrop"); + expect(backdrop).not.toBeNull(); + expect(backdrop).toHaveClass("sm:hidden"); +}); From 1d7d7b3c8d1728dd0be7f65b6ac814718b54dff0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:17:43 +0000 Subject: [PATCH 2/5] Record Guide Centre footer dock review in the ledger Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012gATrPoaRwCEU21gex48Ve --- ...684747a62117c9d75bf90397fb061f937dc1df32ca05c31fbbc.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/5ef27bddd67e7684747a62117c9d75bf90397fb061f937dc1df32ca05c31fbbc.record.md diff --git a/docs/branch-review-records/5ef27bddd67e7684747a62117c9d75bf90397fb061f937dc1df32ca05c31fbbc.record.md b/docs/branch-review-records/5ef27bddd67e7684747a62117c9d75bf90397fb061f937dc1df32ca05c31fbbc.record.md new file mode 100644 index 0000000000..4e4ea330dc --- /dev/null +++ b/docs/branch-review-records/5ef27bddd67e7684747a62117c9d75bf90397fb061f937dc1df32ca05c31fbbc.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/clinical-guide-footer-search-4l54hp | 8493f10a6489ebaa8ef1cabcd23b6c5cd0913b64 | Guide Centre footer composer shared phone dock chrome | approved | verify:pr-local all green; verify:phone-chrome static and unit stages green; focused-browser stage blocked by Playwright revision drift | From fe643d95d67f7ea78a66a44c6cf66a5e7855ec9e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 18:00:39 +0000 Subject: [PATCH 3/5] Render the Guide tour action as a dock addon pill on phones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Converting the footer to the shared dock left one filled control inside it. The band behind the tour action is now deliberately transparent so page content reads through the scrim, and dropping an opaque `primaryControl` on top puts back a smaller version of the exact cover the conversion removed. Every other dock addon in this repo is an outlined translucent pill: `patient-details-fab__button` and Compare's quiet `differentials-mobile-compare-fab__button--empty`. Give the tour action the same framing — pill radius, 1px `--border-strong`, `color-mix(--surface 92%)`, weight 800, `--e3` — scoped `max-sm:` so the `sm+` Sheet band keeps the primary treatment for the guide's main call to action. Measured at 393x852: background `srgb 0.07 0.086 0.102 / 0.92`, border `rgb(71, 80, 90)` 1px, pill radius, 48px min-height, weight 800. At 1024: unchanged filled `rgb(245, 247, 247)`, 12px radius, no border, weight 600. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012gATrPoaRwCEU21gex48Ve --- docs/search-chrome-behaviour.md | 6 ++++ .../clinical-dashboard/guide-dialog.tsx | 30 ++++++++++++++++-- .../guide-centre-design-contract.dom.test.tsx | 31 +++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/docs/search-chrome-behaviour.md b/docs/search-chrome-behaviour.md index ccd3760729..1e8e54f3b6 100644 --- a/docs/search-chrome-behaviour.md +++ b/docs/search-chrome-behaviour.md @@ -40,6 +40,12 @@ Two consequences worth keeping: patient-details dock addons take, and the compact 5rem scrim would end mid-row. - The footer wrapper is the dock element, so its children need `relative z-10` to paint above the scrim. +- The tour action is a dock **addon**, so on phones it takes the addon-pill treatment the + other two addons use — `patient-details-fab__button` and Compare's quiet + `--empty` state: an outlined translucent pill, never a filled primary control. A filled + button there puts back a smaller version of the cover the dock conversion removed, + because the band behind it is transparent by design. Those overrides are `max-sm:` + only; from `sm` the footer is a real band and the primary treatment is correct. ## Default in-page navigation template diff --git a/src/components/clinical-dashboard/guide-dialog.tsx b/src/components/clinical-dashboard/guide-dialog.tsx index 4339b6934d..265e713e36 100644 --- a/src/components/clinical-dashboard/guide-dialog.tsx +++ b/src/components/clinical-dashboard/guide-dialog.tsx @@ -143,6 +143,30 @@ function GuideSearch({ ); } +/** + * The tour action is a dock ADDON on phones, so it takes the shared addon-pill + * treatment the other two dock addons use — `Patient details` + * (`patient-details-fab__button`) and Compare's quiet state + * (`differentials-mobile-compare-fab__button--empty`): a rounded pill on + * `--border-strong` over a translucent `--surface`, never a filled slab. + * + * A filled control there defeats the dock. The band behind it is deliberately + * transparent so page content reads through the scrim; dropping an opaque + * primary button on top puts back a smaller version of the exact cover this + * footer was converted to a dock to remove. + * + * From `sm` the footer is a real Sheet band again, the addon framing no longer + * applies, and the primary treatment is the correct one for the guide's main + * call to action — so every override here is `max-sm:`. + */ +const guideTourAction = cn( + primaryControl, + "max-sm:rounded-full max-sm:border max-sm:border-[color:var(--border-strong)]", + "max-sm:bg-[color-mix(in_srgb,var(--surface)_92%,transparent)] max-sm:text-[color:var(--text-heading)]", + "max-sm:font-extrabold max-sm:shadow-[var(--e3)]", + "max-sm:hover:bg-[color-mix(in_srgb,var(--surface-subtle)_92%,transparent)] max-sm:hover:shadow-[var(--e3)]", +); + function GuideTopNavigation({ view, onNavigate }: { view: GuideView; onNavigate: (view: GuideView) => void }) { const items: ReadonlyArray<{ view: GuideView; label: string; icon: LucideIcon }> = [ { view: "home", label: "Guide home", icon: BookOpen }, @@ -822,13 +846,13 @@ function GuideDialogSession({ onClose }: { onClose: () => void }) { > Exit tour - ) : view === "tour" && tourComplete ? ( - ) : ( @@ -857,7 +881,7 @@ function GuideDialogSession({ onClose }: { onClose: () => void }) {