From 9d74b367583a6d78dbddd8e27b683263a5c5e7fc Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 04:53:21 +0000 Subject: [PATCH 1/8] Make the written record, not a reachability scan, the gate on retiring a mockup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mockup surface reached ~430 files and ~98,000 lines, and the filenames advertise superseded generations of one screen — answer-chat-perfected, -perfected-v2, answer-chat-redesign, five document-navigation rounds, four document-phone rounds. A survey of all 20 design-scratch families found that reading is wrong in three ways that matter, and that nothing was keeping the one record we have honest. A mockup here is usually a provenance artifact, not a stale draft. In this repository a mockup and the production change it justifies are normally added in the SAME commit: search-heading with the search-band rewrite, universal-search-command with universal-search-command-surface.tsx, sidebar-live with ClinicalSidebar and use-sidebar-pins.ts, all nine document mockups with document-viewer/section-nav.tsx. Retirement is only ever right for a losing generation inside a multi-generation study. Filename supersession is systematically backwards. answer-chat-perfected-v2 imports answer-chat-perfected; so does answer-loading-redesign. Dictionary rounds 2 and 3 both import round 1. Services-filter round 2 imports round 1. The privacy study imports the privacy winner. In at least five families, deleting "the older generation" breaks the newer one. A third of the surface is not design scratch. /mockups/development, /mockups/caring-contacts, /mockups/care-plan and /mockups/ward-flow are live in production behind DeveloperAreaGate and linked from Settings; with their implementation modules that is ~199 files, 82 of which have no "mockup" in their name at all. So this adds a policy and a gate that make the record load-bearing: - docs/mockup-retirement-policy.md — three tiers decided by evidence of a consumer rather than by path, a six-point evidence bar drawn from the 2026-08-27 Favourites retirement (the only one this repo has performed), who decides, and where a retired mockup goes. The negative rule is the point: an unevidenced generation is not retired, it is asked about. - scripts/check-mockup-retirement.mjs + check:mockups, wired into verify:cheap and CI. It enforces index completeness and accuracy, and in --diff mode refuses a deletion that is unrecorded, under a developer-gated prefix, or still referenced by any survivor — including the base-module inversion above. It fails closed and never clears a candidate; reachability is exactly what proved insufficient in PR #2204. - mockups/README.md gains a "Retired mockups" table, seeded with the Favourites six and breadcrumb-header, and is corrected in five places the survey refuted: the answer family (three shipped, cited in four commit bodies and three production comments), document navigation (6230c4db added all five drafts and the implementation together; document-navigation-perfected is the winner), document phone chrome (none shipped), calculators (promoted to production by PR #1227, the stalest line in the file), and Tools (#162 closed 2026-08-15). Two smaller claims corrected: source-overlays does not hand off into the real viewer, and the accessible-table fixture has no automated test. AGENTS.md and CLAUDE.md said mockups were exempt from two gates "and from nothing else". Reading the actual rule and config sources, they are also exempt from no-hardcoded-hex, require-z-index-ladder, require-lucide-icon-aria, check:icon-scale, check:design-system-contract, the required Playwright lane, CodeRabbit, and knip entirely — so the repo's own unused-code detector is blind to this surface. Corrected to match the code. No exemption is widened. docs/answer-page-redesign-handover.md still said "design settled, not built" after shipping across five PRs, and was edited twice on 2026-08-26 without the status changing. That is very likely what caused the answer family to be mis-triaged as parallel drafts. Two findings are filed rather than fixed: the calculators mockup still serves prescribing, ECT and admission directives that PR #2491 removed from production on clinical-safety grounds the day before (those routes 404 in production, so this is not a patient-facing exposure), and source-preview-popover.tsx plus a proxy redirect for a route that no longer exists are dead wiring beside this surface. No mockup is deleted in this commit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015UPvNZYvyuDxc1o7xfTGCS --- .claude/skills/gates/SKILL.md | 2 +- .github/workflows/ci.yml | 4 + AGENTS.md | 26 +- CLAUDE.md | 14 +- docs/README.md | 1 + docs/answer-page-redesign-handover.md | 12 +- docs/mockup-retirement-policy.md | 141 ++++++ .../60d75939-a2e4-44a7-af96-203ed2f2ca9c.json | 12 + .../ee0de317-ae07-433a-9456-5997de14bc7b.json | 12 + docs/scripts-index.md | 2 +- mockups/README.md | 112 ++++- package.json | 3 +- scripts/check-mockup-retirement.mjs | 460 ++++++++++++++++++ tests/mockup-retirement.test.ts | 316 ++++++++++++ 14 files changed, 1077 insertions(+), 40 deletions(-) create mode 100644 docs/mockup-retirement-policy.md create mode 100644 docs/outstanding-issues-inbox/60d75939-a2e4-44a7-af96-203ed2f2ca9c.json create mode 100644 docs/outstanding-issues-inbox/ee0de317-ae07-433a-9456-5997de14bc7b.json create mode 100644 scripts/check-mockup-retirement.mjs create mode 100644 tests/mockup-retirement.test.ts diff --git a/.claude/skills/gates/SKILL.md b/.claude/skills/gates/SKILL.md index f6ecc8e528..ca6477eb01 100644 --- a/.claude/skills/gates/SKILL.md +++ b/.claude/skills/gates/SKILL.md @@ -26,7 +26,7 @@ Check these before believing any result. for exactly this reason — if installed packages do not match `package-lock.json`, treat any test, lint, or typecheck result as void until `npm ci` has run. Its own failure message says as much. - **`verify:cheap` stops at the first failing check.** Everything after that point never ran. Do not - describe the change as broadly verified when the gate died at check 2 of 37. + describe the change as broadly verified when the gate died at check 2 of 38. - **Changed-file formatting is required in CI but is not part of `verify:cheap`.** A locally green `verify:cheap` can still fail CI on formatting. During iteration, format only task-owned files. Before a push, follow `AGENTS.md`: from an isolated or otherwise fully owned worktree run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09554b1f1f..f4685f2af0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -367,6 +367,10 @@ jobs: if: needs.changes.outputs.docs_changed == 'true' || needs.changes.outputs.static_heavy_changed == 'true' run: npm run sitemap:check + - name: Mockup retirement policy + if: needs.changes.outputs.docs_changed == 'true' || needs.changes.outputs.static_heavy_changed == 'true' + run: npm run check:mockups + - name: Repo awareness snapshot if: needs.changes.outputs.docs_changed == 'true' || needs.changes.outputs.static_heavy_changed == 'true' run: npm run check:repo-awareness-snapshot diff --git a/AGENTS.md b/AGENTS.md index a1c519a7ef..2b318f372d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -279,11 +279,27 @@ action must perform one; a page that ships must be reachable. ` - ); -} - -function CloseButton({ label = "Close popup" }: { label?: string }) { - return ( - - ); -} - -function SourceCapsule() { - return ( - - ); -} - -function SourceRows() { - return ( -
- {sources.map(([title, meta, support, score], index) => ( -
- -
- ))} -
- ); -} - -function ActionRow({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -} - -function ButtonText({ children }: { children: ReactNode }) { - return {children}; -} - -function DesktopSourcePreviewDemo() { - return ( -
-

- Clozapine monitoring should include FBC/ANC, myocarditis symptoms, metabolic checks, constipation prevention, - and shared-care communication. -

-
- -
- -
-
-

- The answer body stays in place. The preview floats above nearby content instead of pushing the support card - down. -

-
- ); -} - -function SourcePreviewPopover() { - return ( -
-
-
-

Source preview

-

- Check the best passage, status, and source action before opening the PDF. -

-
- 3 sources -
- -
- “Monitor FBC/ANC, myocarditis symptoms, metabolic risk, constipation, and shared-care communication during - clozapine initiation.” -
- - - - Open source - - - - Copy quote - - - - View cited section - - -
- ); -} - -function MobileSheetFrame({ - title, - description, - children, -}: { - title: string; - description: string; - children: ReactNode; -}) { - return ( -
-
-
-
-

{title}

-

{description}

-
- -
-
{children}
-
- ); -} - -function EvidenceTabs({ selected }: { selected: string }) { - return ( -
-
- {tabs.map(([label, count, Icon]) => { - const active = label === selected; - return ( - - ); - })} -
-
- ); -} - -function FeedbackPanel() { - return ( -
-

Clinical verification

-

- Mark whether the linked evidence is safe to use, needs correction, or is source-insufficient. -

-
- - - Verified - - - - Needs correction - - - - Source insufficient - -
-
- ); -} - -function EvidenceSummaryMini({ selected }: { selected: string }) { - return ( -
-
-
-

{selected} evidence

-

- Showing only the items used to support this answer. -

-
- Source-backed -
-
- ); -} - -function TablePreview({ expanded = false }: { expanded?: boolean }) { - return ( -
-
- Clozapine monitoring schedule - {expanded ? ( - Verify against source - ) : ( - - )} -
-
- - - - {["Domain", "Monitoring", "Action"].map((header) => ( - - ))} - - - - {tableRows.map((row) => ( - - {row.map((cell) => ( - - ))} - - ))} - -
- {header} -
- {cell} -
-
-
- ); -} - -function SourceCards() { - return ( -
- {sources.map(([title, meta, support, score]) => ( -
-
- -
-

{title}

-

{meta}

-
- - {support} - {score} - -
-

- Passage supports monitoring and escalation wording. Open source to inspect the highlighted PDF section. -

- - - - Open source - - - - Scope to this - - -
- ))} -
- ); -} - -function QuoteCards() { - return ( -
- {[1, 2].map((item) => ( -
-
-

Exact quote

- {item === 1 ? "p.12" : "p.14"} -
-
- “FBC/ANC monitoring, myocarditis symptoms, metabolic review and constipation prevention should be - checked during initiation and ongoing care.” -
- - - - Copy - - - - Ask about quote - - -
- ))} -
- ); -} - -function ImageEvidence() { - const items = [ - { label: "Table crop", icon: Table2, body: "Monitoring domains extracted from a table image." }, - { label: "PDF page region", icon: FileImage, body: "Page crop used to check source layout and nearby wording." }, - ] as const; - - return ( -
- {items.map(({ label, icon: Icon, body }) => ( -
-
-
-
-
-
-

{label}

-

{body}

-
-
- ))} -
- ); -} - -function PdfLinks() { - return ( -
- {["Clozapine physical health protocol", "Shared-care communication checklist"].map((title, index) => ( - - ))} -
- ); -} - -function EvidenceMap() { - const rows = [ - ["Monitoring", "Moderate", "2", "Current / locally reviewed"], - ["Escalation", "Strong", "3", "Current / locally reviewed"], - ["Metabolic review", "Partial", "1", "Review due"], - ] as const; - return ( -
- - - - {["Section", "Support", "Citations", "Source status"].map((header) => ( - - ))} - - - - {rows.map((row) => ( - - {row.map((cell) => ( - - ))} - - ))} - -
- {header} -
- {cell} -
-
- ); -} - -function MobileEvidencePanel({ selected }: { selected: string }) { - return ( - -
- {selected === "Tables" ? : } - - {selected === "Tables" ? : null} - {selected === "Sources" ? : null} - {selected === "Images" ? : null} - {selected === "Quotes" ? : null} - {selected === "PDFs" ? : null} - {selected === "Map" ? : null} -
-
- ); -} - -function DesktopEvidenceModal() { - return ( -
- - ); -} - -function TableDialog() { - return ( -
-
-
-

Clozapine monitoring table

-

- Expanded from visual evidence. Use the source PDF for final verification. -

-
- -
-
- -
-
- ); -} - -function WeakEvidencePopup() { - return ( -
-
- - - -
-
-

Evidence support is limited

- Do not copy into notes -
-

- Treat this as source finding only. The closest indexed passages are nearby, but they do not directly support - a clinical answer yet. -

- - - - Open closest passage - - - - Restrict to current local sources - - -
-
-
- ); -} - -export default function AnswerEvidencePopupsMockupPage() { - return ( - -
-
- -
- -
- - - -
- -
-
- {["Tables", "Sources", "Images", "Quotes", "PDFs", "Map"].map((tab) => ( -
-

- {tab} tab -

- -
- ))} -
-
- -
- -
- -
- -
- -
- -
-
-
- ); -} diff --git a/src/app/mockups/answer-home-proposal/page.tsx b/src/app/mockups/answer-home-proposal/page.tsx deleted file mode 100644 index 9103f0de9c..0000000000 --- a/src/app/mockups/answer-home-proposal/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AnswerHomeProposalMockupsPage } from "@/components/answer-home-proposal-mockups"; - -export default function AnswerHomeProposalMockupRoute() { - return ; -} diff --git a/src/app/mockups/document-navigation-final-review/page.tsx b/src/app/mockups/document-navigation-final-review/page.tsx deleted file mode 100644 index 038106df68..0000000000 --- a/src/app/mockups/document-navigation-final-review/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentNavigationFinalReviewMockups } from "@/components/document-navigation-final-review-mockups"; - -export const metadata: Metadata = { - title: "Document navigation, final review - PsychSift", - description: - "Two final document-navigation versions at desktop, tablet, and phone, with every issue from the study resolved.", -}; - -export default function DocumentNavigationFinalReviewMockupPage() { - return ; -} diff --git a/src/app/mockups/document-navigation-final/page.tsx b/src/app/mockups/document-navigation-final/page.tsx deleted file mode 100644 index 83c0468ec1..0000000000 --- a/src/app/mockups/document-navigation-final/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentNavigationFinalMockups } from "@/components/document-navigation-final-mockups"; - -export const metadata: Metadata = { - title: "Document navigation, final design - PsychSift", - 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 deleted file mode 100644 index 77242caf4e..0000000000 --- a/src/app/mockups/document-navigation-pane/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentNavigationPaneMockups } from "@/components/document-navigation-pane-mockups"; - -export const metadata: Metadata = { - title: "Document navigation pane mockups - PsychSift", - 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 deleted file mode 100644 index 9ec47701b1..0000000000 --- a/src/app/mockups/document-phone-fused-directions/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentPhoneFusedDirectionsMockups } from "@/components/document-phone-fused-directions-mockups"; - -export const metadata: Metadata = { - title: "Document phone fused row directions - PsychSift", - 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 deleted file mode 100644 index 6d54c263fe..0000000000 --- a/src/app/mockups/document-phone-title-refined/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentPhoneTitleRefinedMockups } from "@/components/document-phone-title-refined-mockups"; - -export const metadata: Metadata = { - title: "Document phone title refined mockups - PsychSift", - 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 deleted file mode 100644 index cb68512498..0000000000 --- a/src/app/mockups/document-phone-title/page.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from "next"; - -import { DocumentPhoneTitleMockups } from "@/components/document-phone-title-mockups"; - -export const metadata: Metadata = { - title: "Document phone title mockups - PsychSift", - 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 deleted file mode 100644 index c037bddbbb..0000000000 --- a/src/app/mockups/document-phone-zero-chrome/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -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 - PsychSift", - 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/components/answer-home-proposal-mockups.tsx b/src/components/answer-home-proposal-mockups.tsx deleted file mode 100644 index eda7ead82b..0000000000 --- a/src/components/answer-home-proposal-mockups.tsx +++ /dev/null @@ -1,270 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { MessageSquareText, ShieldAlert, ShieldCheck } from "lucide-react"; - -import { cn } from "@/components/ui-primitives"; - -// Design-scratch: the concrete proposal, drawn as before/after of the whole -// answer-mode hero rather than of the notice alone — because the change moves -// information between two places. The scope badge is removed and the corpus -// name it carried moves into the subtitle that already exists. -// -// Pinned by tests/privacy-ui.test.ts and reproduced verbatim on both sides: -// "Do not enter patient-identifiable information." and the "Privacy and data -// processing" link to /privacy. The subtitle is ordinary product copy -// (src/lib/ui-copy.ts, sharedHomePresentation.answer.subtitle) and is not pinned. - -const OBLIGATION = "Do not enter patient-identifiable information."; -const PRIVACY_LINK = "Privacy and data processing"; - -const focusRing = - "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; - -function Hero({ subtitle, phone }: { subtitle: React.ReactNode; phone?: boolean }) { - return ( -
- - - -

- How can I help? -

-

{subtitle}

-
- ); -} - -function Composer({ phone }: { phone?: boolean }) { - return ( -
- - + - - Ask a clinical question… - -
- ); -} - -/** Today: three notices, three type sizes, two opposing shields. */ -function BeforeFooter() { - return ( -
-

- - {OBLIGATION} -

-

- - {PRIVACY_LINK} - -

-

- - - Searches indexed clinical sources - - - Clinical Guide library -

-
- ); -} - -/** Proposed: one quiet line, words only. */ -function AfterFooter() { - return ( -

- {OBLIGATION} Verify answers against the - cited source.{" "} - - {PRIVACY_LINK} - -

- ); -} - -function Panel({ side, phone, children }: { side: "before" | "after"; phone?: boolean; children: React.ReactNode }) { - const isAfter = side === "after"; - return ( -
-
- - {isAfter ? "Proposed" : "Today"} - - {phone ? "390 px" : "1440 px"} -
-
-
{children}
-
-
- ); -} - -const changes: Array<{ what: string; why: string; pinned?: boolean }> = [ - { - what: "Remove the “Searches indexed clinical sources · Clinical Guide library” badge", - why: "Accent-blue, semibold, 14px — the loudest element on the screen, and a capability claim rather than a safety statement. Pre-query it can only assert; post-query the real sources appear as citations.", - }, - { - what: "Remove the ShieldCheck", - why: "A shield-and-tick reads as “verified”. The comment above this component in answer-status.tsx warns that pre-query copy must not assert every indexed source is verified/current (PT-06) — the wording obeys that, the icon undoes it. It also sat ~40px from a ShieldAlert meaning the opposite.", - }, - { - what: "Move “Clinical Guide library” into the subtitle", - why: "The one genuinely useful fact in the badge — that this searches a curated clinical corpus, not the open web. The subtitle already exists, so the fact keeps a home without a second voice. Ordinary product copy, not pinned by any test.", - }, - { - what: "Remove the ShieldAlert from the privacy line", - why: "With the competing shield gone, the amber icon is the only ornament left on a line that now carries weight-based hierarchy instead.", - }, - { - what: "Join the privacy link to the same line", - why: "It previously wrapped alone with no lead-in, reading as navigation rather than as the footnote to the warning above it.", - }, - { - what: "Add “Verify answers against the cited source.”", - why: "The statement the screen never made. Every other clinical mode carries an equivalent; answer mode — the only one that synthesises prose — carried none. This is new clinical-safety copy and needs governance sign-off.", - pinned: true, - }, -]; - -export function AnswerHomeProposalMockupsPage() { - return ( -
-
-
-

- Answer home — proposed change -

-

- Drop the scope badge. Keep the corpus name. One quiet line. -

-

- Three stacked notices in three type sizes with two opposing shields become one line of text, and the only - fact worth keeping from the badge moves into the subtitle that was already there. -

-
-
- -
-
-

- Phone — the surface in the screenshot -

-
- - - - - - - - Ask a clinical question or search your{" "} - Clinical Guide library. - - } - /> - - - -
-
- -
-

- Desktop -

-
- - - - - - - - Ask a clinical question or search your{" "} - Clinical Guide library. - - } - /> - - - -
-
- -
-

- What changed, and why -

-
    - {changes.map((c, i) => ( -
  1. - {i + 1} -
    -

    - {c.what} - {c.pinned ? ( - - Needs governance sign-off - - ) : null} -

    -

    {c.why}

    -
    -
  2. - ))} -
-

- Unchanged and reproduced verbatim on both sides: “{OBLIGATION}” and the “{PRIVACY_LINK}” link to{" "} - /privacy — APP-5 / PIA-5 copy pinned by tests/privacy-ui.test.ts. Adoption edits{" "} - PrivacyInputNotice, the single site-wide notice rendered on the answer, documents and - calculators composers, so all three move together and the change needs a full clinical governance preflight. -

-
-
-
- ); -} diff --git a/src/components/document-navigation-final-mockups.tsx b/src/components/document-navigation-final-mockups.tsx deleted file mode 100644 index cb7551947d..0000000000 --- a/src/components/document-navigation-final-mockups.tsx +++ /dev/null @@ -1,576 +0,0 @@ -"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 Device = "desktop" | "tablet" | "phone"; -type PhoneState = "rest" | "sheet" | "scrolled"; - -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 lineage: Array<{ from: string; kept: string; changed: string }> = [ - { - from: "Direction 02 — segment track", - kept: "The seven-segment hairline on the row's bottom edge is the position indicator at every breakpoint.", - changed: - "It replaces the `· 4 of 7` numerals, not the section label. The row keeps both lines; only the text that squeezed the title is gone.", - }, - { - from: "Direction 03 — two-column pane", - kept: "Seven compact cards in two columns, whole structure visible at once, never scrolls internally.", - changed: "The same grid is now the shared surface: desktop index card, tablet index card, and phone sheet body.", - }, - { - from: "Chrome contract review", - kept: "One collapse owner on phone, the composer keeps the footer, sheets stay outside chrome.", - changed: - "The phone pane drops from under the row and becomes a bottom sheet, so nothing is attached to a row that hides.", - }, -]; - -const spec: Array<{ title: string; body: string }> = [ - { - title: "Two lines, and the numerals are gone", - body: "Line one is the title at full width, line two names the section. Only `· 4 of 7` was dropped — the track already carries position, and the numerals were what squeezed the title.", - }, - { - title: "The track places, the label names", - body: "A hairline can show where you are but cannot say what you are in. Keeping both means a first-time reader is never asked to decode the segments.", - }, - { - title: "One pane shape at every size", - body: "The two-column grid is the index card on desktop and tablet and the sheet body on phone. One component, three placements.", - }, - { - title: "Phone pane is a sheet", - body: "Anchored to the viewport, not to a row that scroll-hides. Opening it blurs the document composer and the scrim covers the pill.", - }, - { - title: "Sticky offset follows the top bar", - body: "The desktop index card switches from `top: header + safe-area` to `top: 0` while the top bar is hidden, so no dead band appears.", - }, - { - title: "Track hides with its row", - body: "It is part of the header, so it leaves with the header and leaves zero reserve. Position is still readable from the content itself.", - }, - { - title: "Jump opens the accordion first", - body: 'Targets are `
`. The handler opens the target, waits a frame, then scrolls by the measured row height.', - }, - { - title: "Non-colour cue for the active segment", - body: "Under forced-colors the filled segments lose their hue, so the active one is also taller — 3 px against 2 px.", - }, -]; - -const focusRing = - "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; - -/* ------------------------------------------------------------------ */ -/* Shared pieces */ -/* ------------------------------------------------------------------ */ - -function UniversalTopBar({ device }: { device: Device }) { - const phone = device === "phone"; - - return ( -
- {phone ? ( - -
- ); -} - -/** Position as 2 px of height. The active segment is also taller for forced-colors. */ -function SegmentTrack() { - return ( -