diff --git a/docs/site-map.md b/docs/site-map.md index f957fc9462..1f53342d9b 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -988,6 +988,13 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/mockups/calculators-popup-sheet` - Route discovered from app directory Source: `src/app/mockups/calculators-popup-sheet/page.tsx`. - `/mockups/calculators-search` - Route discovered from app directory Source: `src/app/mockups/calculators-search/page.tsx`. - `/mockups/calculators-search-page` - Route discovered from app directory Source: `src/app/mockups/calculators-search-page/page.tsx`. +- `/mockups/document-navigation-contract` - Route discovered from app directory Source: `src/app/mockups/document-navigation-contract/page.tsx`. +- `/mockups/document-navigation-final` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final/page.tsx`. +- `/mockups/document-navigation-pane` - Route discovered from app directory Source: `src/app/mockups/document-navigation-pane/page.tsx`. +- `/mockups/document-phone-fused-directions` - Route discovered from app directory Source: `src/app/mockups/document-phone-fused-directions/page.tsx`. +- `/mockups/document-phone-title` - Route discovered from app directory Source: `src/app/mockups/document-phone-title/page.tsx`. +- `/mockups/document-phone-title-refined` - Route discovered from app directory Source: `src/app/mockups/document-phone-title-refined/page.tsx`. +- `/mockups/document-phone-zero-chrome` - Route discovered from app directory Source: `src/app/mockups/document-phone-zero-chrome/page.tsx`. - `/mockups/document-search` - Route discovered from app directory Source: `src/app/mockups/document-search/page.tsx`. - `/mockups/document-search-evidence-lens` - Route discovered from app directory Source: `src/app/mockups/document-search-evidence-lens/page.tsx`. - `/mockups/document-search-triage-board` - Route discovered from app directory Source: `src/app/mockups/document-search-triage-board/page.tsx`. diff --git a/src/app/mockups/document-navigation-contract/page.tsx b/src/app/mockups/document-navigation-contract/page.tsx new file mode 100644 index 0000000000..3739903c59 --- /dev/null +++ b/src/app/mockups/document-navigation-contract/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { DocumentNavigationContractMockups } from "@/components/document-navigation-contract-mockups"; + +export const metadata: Metadata = { + title: "Document navigation against the chrome contract - Clinical KB", + description: + "Two complete document-navigation candidates at desktop, tablet, and phone, built to the shipped header and footer behaviour.", +}; + +export default function DocumentNavigationContractMockupPage() { + return ; +} diff --git a/src/app/mockups/document-navigation-final/page.tsx b/src/app/mockups/document-navigation-final/page.tsx new file mode 100644 index 0000000000..79a9cd0470 --- /dev/null +++ b/src/app/mockups/document-navigation-final/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { DocumentNavigationFinalMockups } from "@/components/document-navigation-final-mockups"; + +export const metadata: Metadata = { + title: "Document navigation, final design - Clinical KB", + description: + "The final document navigation design: segment-track header row and a two-column section grid, at desktop, tablet, and phone.", +}; + +export default function DocumentNavigationFinalMockupPage() { + return ; +} diff --git a/src/app/mockups/document-navigation-pane/page.tsx b/src/app/mockups/document-navigation-pane/page.tsx new file mode 100644 index 0000000000..f3638f10b5 --- /dev/null +++ b/src/app/mockups/document-navigation-pane/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { DocumentNavigationPaneMockups } from "@/components/document-navigation-pane-mockups"; + +export const metadata: Metadata = { + title: "Document navigation pane mockups - Clinical KB", + description: + "Three navigation panes anchored to the universal header for the document page, shown on desktop, tablet, and phone.", +}; + +export default function DocumentNavigationPaneMockupPage() { + return ; +} diff --git a/src/app/mockups/document-phone-fused-directions/page.tsx b/src/app/mockups/document-phone-fused-directions/page.tsx new file mode 100644 index 0000000000..89fac1545a --- /dev/null +++ b/src/app/mockups/document-phone-fused-directions/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { DocumentPhoneFusedDirectionsMockups } from "@/components/document-phone-fused-directions-mockups"; + +export const metadata: Metadata = { + title: "Document phone fused row directions - Clinical KB", + description: "Three directions built on the fused phone title row for the document page.", +}; + +export default function DocumentPhoneFusedDirectionsMockupPage() { + return ; +} diff --git a/src/app/mockups/document-phone-title-refined/page.tsx b/src/app/mockups/document-phone-title-refined/page.tsx new file mode 100644 index 0000000000..bb5945ab76 --- /dev/null +++ b/src/app/mockups/document-phone-title-refined/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { DocumentPhoneTitleRefinedMockups } from "@/components/document-phone-title-refined-mockups"; + +export const metadata: Metadata = { + title: "Document phone title refined mockups - Clinical KB", + description: "Three corrected variants of the fused phone title row for the document page.", +}; + +export default function DocumentPhoneTitleRefinedMockupPage() { + return ; +} diff --git a/src/app/mockups/document-phone-title/page.tsx b/src/app/mockups/document-phone-title/page.tsx new file mode 100644 index 0000000000..32fe1c767a --- /dev/null +++ b/src/app/mockups/document-phone-title/page.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from "next"; + +import { DocumentPhoneTitleMockups } from "@/components/document-phone-title-mockups"; + +export const metadata: Metadata = { + title: "Document phone title mockups - Clinical KB", + description: "Three ways to keep the document title visible on a phone alongside the navigation pane.", +}; + +export default function DocumentPhoneTitleMockupPage() { + return ; +} diff --git a/src/app/mockups/document-phone-zero-chrome/page.tsx b/src/app/mockups/document-phone-zero-chrome/page.tsx new file mode 100644 index 0000000000..404350d273 --- /dev/null +++ b/src/app/mockups/document-phone-zero-chrome/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { DocumentPhoneZeroChromeMockups } from "@/components/document-phone-zero-chrome-mockups"; + +export const metadata: Metadata = { + title: "Document phone navigation, zero new chrome - Clinical KB", + description: + "Section navigation on the document page that adds no permanent phone chrome and no second scroll owner.", +}; + +export default function DocumentPhoneZeroChromeMockupPage() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index a30dc493b3..8b0cec5bfe 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -11,6 +11,12 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { const isFavouritesPageMockup = pathname.startsWith("/mockups/favourites-"); const isDocumentSearchMockup = pathname.startsWith("/mockups/document-search"); const isDocumentTopNavigationMockup = pathname === "/mockups/document-top-navigation"; + const isDocumentNavigationPaneMockup = pathname === "/mockups/document-navigation-pane"; + const isDocumentPhoneTitleMockup = pathname.startsWith("/mockups/document-phone-"); + // Draws its own universal top bar and document composer inside every frame, so + // the shared composer would read as a second, real search bar over the study. + const isDocumentNavigationContractMockup = + pathname === "/mockups/document-navigation-contract" || pathname === "/mockups/document-navigation-final"; const isSourceOverlayRedesignMockup = pathname === "/mockups/document-search/source-overlays"; const isStandaloneDocumentFlow = pathname === "/mockups/document-search"; const isUniversalSearchRedesignMockup = pathname === "/mockups/universal-search-redesign"; @@ -28,7 +34,11 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { ? "tools" : isFavouritesPageMockup ? "favourites" - : isDocumentSearchMockup || isDocumentTopNavigationMockup + : isDocumentSearchMockup || + isDocumentTopNavigationMockup || + isDocumentNavigationPaneMockup || + isDocumentPhoneTitleMockup || + isDocumentNavigationContractMockup ? "documents" : "answer" } @@ -37,6 +47,9 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isFavouritesPageMockup && !isStandaloneDocumentFlow && !isDocumentTopNavigationMockup && + !isDocumentNavigationPaneMockup && + !isDocumentPhoneTitleMockup && + !isDocumentNavigationContractMockup && !isUniversalSearchRedesignMockup && !isCalculatorsSearchPageMockup && !isPhoneInPageNavigationMockup diff --git a/src/components/document-navigation-contract-mockups.tsx b/src/components/document-navigation-contract-mockups.tsx new file mode 100644 index 0000000000..69847a8d83 --- /dev/null +++ b/src/components/document-navigation-contract-mockups.tsx @@ -0,0 +1,773 @@ +"use client"; + +import { + ArrowLeft, + ChevronDown, + Compass, + FileImage, + FileSearch, + FileText, + ListTree, + Menu, + MessageSquarePlus, + Plus, + Quote, + Search, + ShieldCheck, + Sparkles, + type LucideIcon, +} from "lucide-react"; +import { useId } from "react"; + +import { cn } from "@/components/ui-primitives"; + +type CandidateId = "index" | "rail"; +type Device = "desktop" | "tablet" | "phone"; + +const documentTitle = "Clinical practice guideline for schizophrenia"; + +const sections: Array<{ label: string; short: string; detail: string; icon: LucideIcon; collapsible: boolean }> = [ + { label: "Overview", short: "Overview", detail: "84 pages", icon: Compass, collapsible: false }, + { label: "High-yield summary", short: "Summary", detail: "8 points", icon: Sparkles, collapsible: true }, + { label: "PDF preview", short: "PDF", detail: "p. 12 / 84", icon: FileText, collapsible: false }, + { label: "Pinned evidence", short: "Evidence", detail: "27 passages", icon: Quote, collapsible: false }, + { label: "Indexed source text", short: "Text", detail: "312 chunks", icon: FileSearch, collapsible: true }, + { label: "Tables and diagrams", short: "Tables", detail: "6 visuals", icon: FileImage, collapsible: true }, + { label: "Indexing details", short: "Indexing", detail: "v3 · OCR", icon: ShieldCheck, collapsible: true }, +]; + +const activeIndex = 3; +const activeSection = sections[activeIndex]; + +const contractFindings: Array<{ area: string; finding: string; consequence: string }> = [ + { + area: "Phone header", + finding: + "The document header is not its own sticky bar. `PhoneHeaderCollapsePortal` moves it into the universal collapse row below `sm`, and the contract allows exactly one collapse owner per phone header.", + consequence: + "A second sticky or fixed phone header is out of bounds. Anything added lands inside the collapse row.", + }, + { + area: "Collapse budget", + finding: + "`readChromeCollapseMetrics` charges the collapse row's controls, the released top safe-area, and the dock reserve against the remaining scroll runway.", + consequence: "Every extra row of header height makes hide-on-scroll fail to fire on short documents.", + }, + { + area: "Phone footer", + finding: + "`DocumentViewer` already owns the bottom: a 56 px floating composer pill with `+` actions, portalled through `PhoneFooterLayerPortal`, reserve `9rem + safe-area + keyboard`, and `0rem` once hidden.", + consequence: + "The bottom edge is taken. Navigation cannot claim a persistent bar there without a second footer owner.", + }, + { + area: "Sheets", + finding: + "The contract states plainly that modal and sheet headers are not viewport chrome and keep their own scroll context.", + consequence: "A bottom sheet is the one overlay that adds no chrome, no reserve, and no scroll owner.", + }, + { + area: "Tablet", + finding: "The top bar hides and reveals, and tablet search stays pinned beneath it as the second sticky layer.", + consequence: + "A sticky nav pane would be a third layer. Tablet navigation should sit in page flow or share the pinned stack.", + }, + { + area: "Desktop", + finding: + "The top bar is the only sticky desktop chrome; desktop search is page-flow. `DocumentViewer` already runs `lg:grid-cols-[minmax(0,1fr)_480px]`.", + consequence: + "A third column is real crowding under 1440 px, and any sticky rail must drop its offset to 0 when the top bar hides or it leaves a dead band.", + }, +]; + +const candidates: Array<{ + id: CandidateId; + title: string; + verdict: string; + summary: string; + perDevice: Record; +}> = [ + { + id: "index", + title: "Candidate A — Section index, no new chrome", + verdict: "Contract-safest", + summary: + "Navigation borrows surfaces that already exist. Desktop puts the index at the top of the 480 px column that is already there, tablet keeps it in page flow, and phone makes the existing title a disclosure for a bottom sheet.", + perDevice: { + desktop: "Sticky index card at the top of the existing right column. No third column, no new grid.", + tablet: "In-flow index card above the document. Scrolls away with content — no third sticky layer.", + phone: + "Existing header title becomes the disclosure; sections open in a bottom sheet. Zero added collapse height.", + }, + }, + { + id: "rail", + title: "Candidate B — Anchored rail, sheet on phone", + verdict: "Most spatial", + summary: + "A persistent rail gives the document a fixed sense of place on the wide breakpoints. Its sticky offset is bound to the top-bar hide state, and below `sm` it degrades to the same sheet as Candidate A because the collapse contract leaves no other option.", + perDevice: { + desktop: "17 rem labelled rail, sticky. Offset follows the top bar: header height when shown, 0 when hidden.", + tablet: "Compact icon rail sharing the pinned stack rather than adding a layer beneath it.", + phone: "Identical to Candidate A — the rail has nowhere to live inside a single collapse row.", + }, + }, +]; + +const perfected: Array<{ title: string; body: string }> = [ + { + title: "Phone: title disclosure plus bottom sheet", + body: "No added collapse height, so the hide budget is untouched. The sheet is not chrome, so it adds no reserve and no scroll owner.", + }, + { + title: "Sheet opens with the composer blurred", + body: "The contract requires blurring the focused composer so hide-on-scroll can reclaim both edges. Opening the sheet blurs the document search input first.", + }, + { + title: "Sheet sits above the footer layer", + body: "The composer pill is `z-40` on the phone footer layer. The sheet takes a higher layer and its scrim covers the pill, so there is never a live control under an open sheet.", + }, + { + title: "Desktop index rides the existing column", + body: "No third column below 1440 px. The index is the first card in the 480 px column the viewer already renders.", + }, + { + title: "Sticky offset follows the top bar", + body: "Any sticky index or rail switches from `top: header + safe-area` to `top: 0` while the top bar is hidden, so no dead band is left behind it.", + }, + { + title: "Jump opens the accordion first", + body: 'Target sections are `
` — exclusive. The handler opens the target, waits a frame, then scrolls.', + }, + { + title: "Anchor offset is measured, not fixed", + body: "`scroll-mt-24` assumes a constant header. Offsets read the live collapse-row height so a hidden header does not leave headings floating mid-viewport.", + }, + { + title: "Reduced motion keeps the release", + body: "The sheet and any collapse animate normally, and under reduced motion they snap — but the full edge release still happens, per the contract.", + }, +]; + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +/* ------------------------------------------------------------------ */ +/* Shared chrome stand-ins */ +/* ------------------------------------------------------------------ */ + +function UniversalTopBar({ device }: { device: Device }) { + const phone = device === "phone"; + + return ( +
+ {phone ? ( + +
+ ); +} + +function DocumentHeader({ device, sheetId, expanded }: { device: Device; sheetId?: string; expanded?: boolean }) { + const phone = device === "phone"; + + return ( +
+ + + {phone ? ( + + ) : ( +

+ {documentTitle} +

+ )} + + +
+ ); +} + +/** The real phone footer: 56 px composer pill with the `+` actions button. */ +function PhoneComposerPill({ dimmed = false }: { dimmed?: boolean }) { + return ( +
+
+ + + + Search within this document... + + + +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* Navigation surfaces */ +/* ------------------------------------------------------------------ */ + +function SectionRows({ dense = false }: { dense?: boolean }) { + return ( +
    + {sections.map((section, index) => { + const selected = index === activeIndex; + const Icon = section.icon; + + return ( +
  • + +
  • + ); + })} +
+ ); +} + +function SectionCards() { + return ( +
+ {sections.map((section, index) => { + const selected = index === activeIndex; + const Icon = section.icon; + const last = index === sections.length - 1; + + return ( + + ); + })} +
+ ); +} + +function IndexCard({ note, sticky = false }: { note: string; sticky?: boolean }) { + return ( + + ); +} + +function IconRail({ note }: { note: string }) { + return ( + + ); +} + +function LabelledRail() { + return ( + + ); +} + +function SectionsSheet({ id }: { id: string }) { + return ( + <> +