From a33ab97e5b55edcb26ace179471d97b7cf71118e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:49:24 +0000 Subject: [PATCH 1/7] Add Dictionary Browse header redesign study with three directions The Browse terms header stacks five bands before the first result on a 390 px phone: kicker, title, description, view switch, an A-Z / Z-A sort pill alone on its own row, then the letter rail. This adds a runnable design study at /mockups/dictionary-browse-header rendering the current header beside three rebuilt directions. All three drop the description line and the orphaned sort pill, moving sort into the Filters sheet where the other modes already keep it. - 01 Compact title bar: title and count share a line; view switch and letter rail fuse into one sticky control band. Two bands on phone. - 02 Fused letter rail (recommended): the A-Z / Abbreviations segment disappears; All and Abbr become pinned lead chips on the rail itself, so one row does both jobs. Two bands on phone. - 03 Index rail + jump sheet: no horizontal scroller; the title carries the active scope and opens a jump sheet, with a Contacts-style index rail on the right edge of the list. One band on phone. Each direction is shown at 1440 px and in a scrollable 390 px phone frame that draws its own top bar, mode nav and composer, so shared mockup chrome is suppressed for this route. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jr2rJuYT4sdi2yDGBTkyd --- docs/site-map.md | 1 + mockups/README.md | 16 + .../mockups/dictionary-browse-header/page.tsx | 5 + src/app/mockups/mockups-layout-client.tsx | 9 +- .../dictionary-browse-header-mockups.tsx | 855 ++++++++++++++++++ 5 files changed, 884 insertions(+), 2 deletions(-) create mode 100644 src/app/mockups/dictionary-browse-header/page.tsx create mode 100644 src/components/dictionary-browse-header-mockups.tsx diff --git a/docs/site-map.md b/docs/site-map.md index 77f2c93e8f..9dd06bba26 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1017,6 +1017,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/caring-contacts/team` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/team/page.tsx`. - `/mockups/caring-contacts/templates` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/templates/page.tsx`. - `/mockups/caring-contacts/templates/[pathwayId]` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/templates/[pathwayId]/page.tsx`. +- `/mockups/dictionary-browse-header` - Route discovered from app directory Source: `src/app/mockups/dictionary-browse-header/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-final-review` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final-review/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index 696a41a229..ebff10018e 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -43,6 +43,22 @@ Some document-search mockups include live handoff routes (for example `document- - Full three-direction study: [`/mockups/privacy-page-directions`](../src/app/mockups/privacy-page-directions/page.tsx) - Static comps: [`public/mockups/privacy-page-redesign-2026-08/`](../public/mockups/privacy-page-redesign-2026-08/README.md) +## Dictionary Browse header study (2026-08-18) + +Runnable study at [`/mockups/dictionary-browse-header`](../src/app/mockups/dictionary-browse-header/page.tsx). The +brief was to drop the description line under **Browse terms** and the orphaned A–Z / Z–A sort pill that floats on its +own row, then rebuild the header for the phone. All three directions move sort into the Filters sheet, where the other +modes already keep it; they differ in how much letter navigation stays on screen. + +| Direction | Phone chrome before a result | Trade-off | +| --------------------------- | ---------------------------- | ------------------------------------------------------------- | +| 01 Compact title bar | 2 bands | Keeps both browse views explicit; least vertical saving | +| 02 Fused letter rail (rec.) | 2 bands | Abbreviations becomes a rail chip rather than a separate view | +| 03 Index rail + jump sheet | 1 band | Edge rail is a fine-motor target; jump sheet is the a11y path | + +The current header is rendered side by side at the top of the page for comparison. Shared mockup chrome is suppressed +because each frame draws its own top bar, mode nav and composer. + ## Phone Choose mode sheet YES comps Runnable study at [`/mockups/phone-mode-sheet-yes`](../src/app/mockups/phone-mode-sheet-yes/page.tsx): design review of the shipping phone mode sheet plus **YES 01 perfected** (sectioned clinical list — shipping recommendation) and YES 02 (icon deck alternate). Shared mockup chrome is suppressed so only the in-frame sheet is judged. diff --git a/src/app/mockups/dictionary-browse-header/page.tsx b/src/app/mockups/dictionary-browse-header/page.tsx new file mode 100644 index 0000000000..62433a6413 --- /dev/null +++ b/src/app/mockups/dictionary-browse-header/page.tsx @@ -0,0 +1,5 @@ +import { DictionaryBrowseHeaderMockupsPage } from "@/components/dictionary-browse-header-mockups"; + +export default function DictionaryBrowseHeaderMockupRoute() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index 455b81b95b..aeae14b6d3 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -24,6 +24,9 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { const isStandaloneDocumentFlow = pathname === "/mockups/document-search"; const isUniversalSearchRedesignMockup = pathname === "/mockups/universal-search-redesign"; const isSearchHeadingMockup = pathname === "/mockups/search-heading"; + // Draws its own phone/desktop frames with a top bar, mode nav and composer in + // every frame, because the header under study sits directly beneath them. + const isDictionaryBrowseHeaderMockup = pathname === "/mockups/dictionary-browse-header"; // Renders the results header inside its own device frames; the shared composer // would read as a second, real search bar over the study. const isSearchRefineAdaptiveMockup = pathname === "/mockups/search-refine-adaptive"; @@ -122,7 +125,8 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isPinnedPlusMenuMockup && !isPhoneModeSheetYesMockup && !isSidebarLiveMockup && - !isCaringContactMockup + !isCaringContactMockup && + !isDictionaryBrowseHeaderMockup } chromeVisible={ !isSourceOverlayRedesignMockup && @@ -144,7 +148,8 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isPinnedPlusMenuMockup && !isPhoneModeSheetYesMockup && !isSidebarLiveMockup && - !isCaringContactMockup + !isCaringContactMockup && + !isDictionaryBrowseHeaderMockup } > {children} diff --git a/src/components/dictionary-browse-header-mockups.tsx b/src/components/dictionary-browse-header-mockups.tsx new file mode 100644 index 0000000000..1ac8cf944d --- /dev/null +++ b/src/components/dictionary-browse-header-mockups.tsx @@ -0,0 +1,855 @@ +"use client"; + +import { useState } from "react"; +import { + ArrowRight, + Check, + ChevronDown, + Filter, + GitCompareArrows, + Menu, + MessageSquarePlus, + Plus, + Search, + SendHorizontal, + SlidersHorizontal, + X, +} from "lucide-react"; + +import { cn } from "@/components/ui-primitives"; + +/* ------------------------------------------------------------------ * + * Dictionary → Browse header redesign study (2026-08-18) + * + * Brief: drop the description line under "Browse terms", drop the + * orphaned A–Z / Z–A sort pill that floats on its own row, and rebuild + * the header so the phone layout reaches a result without five stacked + * bands. Sort survives inside the Filters sheet in all three, which is + * where the other modes already keep it. + * ------------------------------------------------------------------ */ + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); + +type Direction = "titlebar" | "rail" | "index"; + +type SampleEntry = { + term: string; + kind: string; + alias?: string; + definition: string; +}; + +const sampleEntries: readonly SampleEntry[] = [ + { + term: "Acceptance and commitment therapy", + kind: "Therapy", + alias: "ACT", + definition: "A therapy that develops acceptance, present-moment awareness and action guided by personal values.", + }, + { + term: "Acute dystonia", + kind: "Clinical finding", + definition: + "A sudden medication-associated sustained muscle contraction that produces abnormal posture or movement.", + }, + { + term: "Advance statement", + kind: "Legal / ethical", + definition: + "A written record of a person's treatment preferences made while they have capacity, for use in later care.", + }, +]; + +const directions: ReadonlyArray<{ + id: Direction; + number: string; + name: string; + summary: string; + strengths: readonly string[]; + cost: string; + bands: string; + recommended?: boolean; +}> = [ + { + id: "titlebar", + number: "01", + name: "Compact title bar", + summary: + "The smallest honest edit: title and count share a line, the view switch and letter rail fuse into one sticky control band, and sort moves into Filters.", + strengths: ["Lowest migration risk", "Keeps both browse views explicit", "Rail stays pinned while scrolling"], + cost: "Still two bands of chrome before the first result on a phone.", + bands: "2 bands on phone", + }, + { + id: "rail", + number: "02", + name: "Fused letter rail", + summary: + "The A–Z / Abbreviations segment disappears. All and Abbr become pinned lead chips on the letter rail itself, so one row does both jobs.", + strengths: ["One control row total", "Abbr stays visible, never scrolls away", "Largest vertical saving"], + cost: "Abbreviations reads as a peer of a letter rather than a separate view.", + bands: "2 bands on phone", + recommended: true, + }, + { + id: "index", + number: "03", + name: "Index rail + jump sheet", + summary: + "No horizontal scroller at all. The title carries the active scope and opens a jump sheet — tap it in either frame below; a Contacts-style index rail rides the right edge of the list.", + strengths: ["Results start after one band", "Scales past 96 terms", "Sticky letter headings orient the scroll"], + cost: "The edge rail is a fine-motor target; the jump sheet is the accessible path and must stay first-class.", + bands: "1 band on phone", + }, +]; + +/* ---------------------------- shared bits ---------------------------- */ + +function ResultRow({ entry }: { entry: SampleEntry }) { + return ( +
+
+ ); +} + +function LetterChip({ + value, + active, + onSelect, + tone = "accent", +}: { + value: string; + active: boolean; + onSelect: () => void; + tone?: "accent" | "purple"; +}) { + return ( + + ); +} + +function FilterButton({ count, labelled = false, onOpen }: { count: number; labelled?: boolean; onOpen: () => void }) { + return ( + + ); +} + +function CountPill({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +/* ------------------------- direction 01: title bar ------------------------- */ + +function TitleBarHeader({ compact }: { compact: boolean }) { + const [view, setView] = useState<"az" | "abbr">("az"); + const [letter, setLetter] = useState("All"); + + return ( +
+
+ {!compact ? ( +

+ Complete catalogue +

+ ) : null} +
+

+ Browse terms +

+ 96 terms +
+
+
+
+
+ {(["az", "abbr"] as const).map((option) => ( + + ))} +
+ undefined} /> +
+
+ {["All", ...letters].map((value) => ( + setLetter(value)} /> + ))} +
+
+
+ ); +} + +/* ------------------------- direction 02: fused rail ------------------------ */ + +function FusedRailHeader({ compact }: { compact: boolean }) { + const [scope, setScope] = useState("All"); + + return ( +
+
+

+ Browse terms +

+ 96 + + undefined} /> +
+
+
+ setScope("All")} /> + setScope("Abbr")} tone="purple" /> +
+
+ {letters.map((value) => ( + setScope(value)} /> + ))} +
+
+
+ ); +} + +/* --------------------- direction 03: index rail + sheet -------------------- */ + +function IndexHeader({ compact, scope, onOpenSheet }: { compact: boolean; scope: string; onOpenSheet: () => void }) { + return ( +
+ + 96 + undefined} /> +
+ ); +} + +function JumpSheet({ + scope, + onSelect, + onClose, +}: { + scope: string; + onSelect: (value: string) => void; + onClose: () => void; +}) { + return ( +
+ +
+
+ + +
+
+ {letters.map((value) => ( + + ))} +
+ + + ); +} + +function IndexRail({ + scope, + onSelect, + compact, +}: { + scope: string; + onSelect: (value: string) => void; + compact: boolean; +}) { + return ( +
+ {letters.map((value) => ( + + ))} +
+ ); +} + +/* ------------------------------ device frames ------------------------------ */ + +function PhoneChrome() { + return ( + <> +
+ +
+
+ {[ + { label: "Search", active: false }, + { label: "Browse", active: true }, + { label: "More", active: false }, + ].map((tab) => ( + + {tab.label} + + ))} +
+ + ); +} + +function PhoneComposer() { + return ( +
+
+
+
+ ); +} + +function PhoneFrame({ + children, + label, + overlay, +}: { + children: React.ReactNode; + label: string; + overlay?: React.ReactNode; +}) { + return ( +
+
+ {label} + 390 px · scrollable +
+
+ +
{children}
+ + {overlay} +
+
+ ); +} + +function DesktopFrame({ + children, + label, + overlay, +}: { + children: React.ReactNode; + label: string; + overlay?: React.ReactNode; +}) { + return ( +
+
+ {label} + 1440 px +
+
+
+ {children} + {overlay} +
+
+
+ ); +} + +function ResultList({ + withIndexRail, + scope, + onSelect, + compact = false, +}: { + withIndexRail?: boolean; + scope?: string; + onSelect?: (value: string) => void; + compact?: boolean; +}) { + return ( +
+ {withIndexRail ? ( +
+ A +
+ ) : ( +

96 showing

+ )} +
+ {[...sampleEntries, ...sampleEntries].map((entry, index) => ( + + ))} +
+ {withIndexRail && scope && onSelect ? : null} +
+ ); +} + +function SimpleDirectionFrames({ direction }: { direction: Exclude }) { + const Header = direction === "titlebar" ? TitleBarHeader : FusedRailHeader; + return ( + <> + +
+ + + +
+ + + + ); +} + +function IndexDirectionFrames() { + const [desktopScope, setDesktopScope] = useState("All"); + const [desktopSheet, setDesktopSheet] = useState(false); + const [phoneScope, setPhoneScope] = useState("All"); + const [phoneSheet, setPhoneSheet] = useState(false); + + return ( + <> + { + setDesktopScope(value); + setDesktopSheet(false); + }} + onClose={() => setDesktopSheet(false)} + /> + ) : null + } + > + setDesktopSheet(true)} /> + + + { + setPhoneScope(value); + setPhoneSheet(false); + }} + onClose={() => setPhoneSheet(false)} + /> + ) : null + } + > + setPhoneSheet(true)} /> + + + + ); +} + +/* --------------------------------- page ---------------------------------- */ + +export function DictionaryBrowseHeaderMockupsPage() { + const [showBefore, setShowBefore] = useState(true); + + return ( +
+
+
+

+ Dictionary · Browse +

+

+ Three headers for Browse terms, rebuilt for the phone +

+

+ All three drop the description line and the orphaned A–Z / Z–A sort pill. Sort moves into the Filters sheet, + which is where every other mode already keeps it. What differs is how much of the letter navigation stays on + screen. +

+ +
+
+ + {showBefore ? ( +
+
+
+

+ Today — five stacked bands +

+

+ Kicker, title, description, view switch, a sort pill alone on its own row, then the letter rail. On a + 390 px phone the first result lands below the fold. +

+
+
+ + + + + + + + +
+
+
+ ) : null} + +
+ {directions.map((direction) => ( +
+
+
+ + {direction.number} + +
+
+

+ {direction.name} +

+ {direction.recommended ? ( + + Recommended + + ) : null} + + {direction.bands} + +
+

+ {direction.summary} +

+

+ Trade-off · {direction.cost} +

+
+
+
+ {direction.strengths.map((strength) => ( + + {strength} + + ))} +
+
+ +
+ {direction.id === "index" ? : } +
+
+ ))} +
+
+ ); +} + +/* The header exactly as it ships today, for side-by-side judgement. */ +function CurrentHeader({ compact }: { compact: boolean }) { + return ( +
+
+

+ Complete catalogue +

+

+ Browse terms +

+

+ Scan the same source-linked result system by letter or abbreviation. +

+
+
+
+
+
+ + A–Z + + + Abbreviations + +
+ + + + A–Z + +
+
+ {["All", ...letters].map((value) => ( + + {value} + + ))} +
+
+
+
+ ); +} From dcb2835c34302c20c1099e45a3a19f3e4eb6ebbf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 16:58:32 +0000 Subject: [PATCH 2/7] Record branch review for the Browse header mockup study Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jr2rJuYT4sdi2yDGBTkyd --- ...0407ee7e9c44816a1c23691e853437cb963c33d0dcd860f4099.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/599419ed3f41f0407ee7e9c44816a1c23691e853437cb963c33d0dcd860f4099.record.md diff --git a/docs/branch-review-records/599419ed3f41f0407ee7e9c44816a1c23691e853437cb963c33d0dcd860f4099.record.md b/docs/branch-review-records/599419ed3f41f0407ee7e9c44816a1c23691e853437cb963c33d0dcd860f4099.record.md new file mode 100644 index 0000000000..80aa64a969 --- /dev/null +++ b/docs/branch-review-records/599419ed3f41f0407ee7e9c44816a1c23691e853437cb963c33d0dcd860f4099.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/header-redesign-mockups-3ms5kn | a33ab97e5b55edcb26ace179471d97b7cf71118e | Dictionary Browse header redesign mockup study (design scratch) | approved | verify:pr-local (673 files/7276 tests pass), build, check:bundle-budget, check:rag:fixtures, check:medication-interactions, check:medication-lexicon-report, Chromium dark+light screenshot review | From 02baa95fdc137f14bd4f4df59020ed18e25efce0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 17:52:50 +0000 Subject: [PATCH 3/7] Add round-two Browse header study: letter dropdown, Abbreviations in Filters Follow-up to the title-bar direction. Three more versions at /mockups/dictionary-browse-header-compact, all sharing two moves: - The 27-chip horizontal letter rail becomes a dropdown on phones, opening a 48 px letter grid as a bottom sheet (anchored popover on desktop). - Abbreviations stops being a header segment and becomes a Show option inside the Filters sheet, beside sort. Demoting a view switch into a sheet hides state, so every version surfaces an active Abbreviations chip beside the letter control; without it the header would claim 96 terms while listing 24 abbreviations. - 01 Title bar + letter dropdown: full-size title, then one sticky toolbar. Desktop keeps the chip rail, which still fits. Two rows. - 02 Single fused row (recommended): title, dropdown, count and Filters on one sticky line, both breakpoints. At 390 px an active filter chip costs the row its title and count rather than overflowing. - 03 Slim toolbar, title retired: the phone mode nav already reads Browse, so the visible heading goes (kept for screen readers) and only a slim toolbar remains. Desktop keeps its title. Also fixes a latent bug shared with the round-one study: the mockup stylesheet only emits Tailwind classes some source actually uses, and no production file uses a bare grid-cols-6 (only xl:grid-cols-6), so both letter pickers silently collapsed to a single column. Both now pin gridTemplateColumns inline instead of depending on class generation. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jr2rJuYT4sdi2yDGBTkyd --- docs/site-map.md | 1 + mockups/README.md | 20 + .../dictionary-browse-header-compact/page.tsx | 5 + src/app/mockups/mockups-layout-client.tsx | 2 +- ...ctionary-browse-header-compact-mockups.tsx | 663 ++++++++++++++++++ .../dictionary-browse-header-mockups.tsx | 22 +- 6 files changed, 701 insertions(+), 12 deletions(-) create mode 100644 src/app/mockups/dictionary-browse-header-compact/page.tsx create mode 100644 src/components/dictionary-browse-header-compact-mockups.tsx diff --git a/docs/site-map.md b/docs/site-map.md index 9dd06bba26..5267090137 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1018,6 +1018,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/mockups/caring-contacts/templates` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/templates/page.tsx`. - `/mockups/caring-contacts/templates/[pathwayId]` - Route discovered from app directory Source: `src/app/mockups/caring-contacts/templates/[pathwayId]/page.tsx`. - `/mockups/dictionary-browse-header` - Route discovered from app directory Source: `src/app/mockups/dictionary-browse-header/page.tsx`. +- `/mockups/dictionary-browse-header-compact` - Route discovered from app directory Source: `src/app/mockups/dictionary-browse-header-compact/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-final-review` - Route discovered from app directory Source: `src/app/mockups/document-navigation-final-review/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index ebff10018e..1e9c3ac758 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -59,6 +59,26 @@ modes already keep it; they differ in how much letter navigation stays on screen The current header is rendered side by side at the top of the page for comparison. Shared mockup chrome is suppressed because each frame draws its own top bar, mode nav and composer. +## Dictionary Browse header, round two (2026-08-18) + +Runnable study at [`/mockups/dictionary-browse-header-compact`](../src/app/mockups/dictionary-browse-header-compact/page.tsx), +a follow-up to the round-one study above. Every version replaces the 27-chip horizontal letter rail with a **letter +dropdown on phones** and moves **Abbreviations out of the header into the Filters sheet** beside sort. + +| Version | Phone chrome | Trade-off | +| ------------------------------ | ------------ | ------------------------------------------------------------------- | +| 01 Title bar + letter dropdown | 2 rows | Title still costs a row the mode nav already implies | +| 02 Single fused row (rec.) | 1 row | An active filter chip costs the row its title and count at 390 px | +| 03 Slim toolbar, title retired | 1 slim bar | Phone loses its visual page title; depends on the mode nav above it | + +Demoting a view switch into a sheet hides state, so each version surfaces an active **Abbreviations** chip beside the +letter control. Without it the header would claim 96 terms while listing 24 abbreviations. + +Note for anyone extending these: the mockup stylesheet only emits Tailwind classes that some source actually uses, and +no production file uses a bare `grid-cols-6` (only `xl:grid-cols-6`). The 26-letter pickers therefore pin +`gridTemplateColumns` inline rather than depending on class generation — a bare `grid-cols-6` silently collapses them +to one column. + ## Phone Choose mode sheet YES comps Runnable study at [`/mockups/phone-mode-sheet-yes`](../src/app/mockups/phone-mode-sheet-yes/page.tsx): design review of the shipping phone mode sheet plus **YES 01 perfected** (sectioned clinical list — shipping recommendation) and YES 02 (icon deck alternate). Shared mockup chrome is suppressed so only the in-frame sheet is judged. diff --git a/src/app/mockups/dictionary-browse-header-compact/page.tsx b/src/app/mockups/dictionary-browse-header-compact/page.tsx new file mode 100644 index 0000000000..e8d1832631 --- /dev/null +++ b/src/app/mockups/dictionary-browse-header-compact/page.tsx @@ -0,0 +1,5 @@ +import { DictionaryBrowseHeaderCompactMockupsPage } from "@/components/dictionary-browse-header-compact-mockups"; + +export default function DictionaryBrowseHeaderCompactMockupRoute() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index aeae14b6d3..cfc472e524 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -26,7 +26,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { const isSearchHeadingMockup = pathname === "/mockups/search-heading"; // Draws its own phone/desktop frames with a top bar, mode nav and composer in // every frame, because the header under study sits directly beneath them. - const isDictionaryBrowseHeaderMockup = pathname === "/mockups/dictionary-browse-header"; + const isDictionaryBrowseHeaderMockup = pathname.startsWith("/mockups/dictionary-browse-header"); // Renders the results header inside its own device frames; the shared composer // would read as a second, real search bar over the study. const isSearchRefineAdaptiveMockup = pathname === "/mockups/search-refine-adaptive"; diff --git a/src/components/dictionary-browse-header-compact-mockups.tsx b/src/components/dictionary-browse-header-compact-mockups.tsx new file mode 100644 index 0000000000..62eb7145b6 --- /dev/null +++ b/src/components/dictionary-browse-header-compact-mockups.tsx @@ -0,0 +1,663 @@ +"use client"; + +import { useState } from "react"; +import { ArrowUpDown, Check, ChevronDown, SlidersHorizontal, X } from "lucide-react"; + +import { + CountPill, + DesktopFrame, + PhoneFrame, + ResultRow, + focusRing, + letters, + sampleEntries, +} from "@/components/dictionary-browse-header-mockups"; +import { cn } from "@/components/ui-primitives"; + +/* ------------------------------------------------------------------ * + * Dictionary → Browse header, round two (2026-08-18) + * + * Follow-up to /mockups/dictionary-browse-header. Same two removals as + * before (description line, orphaned A–Z / Z–A sort pill), plus two new + * moves the whole round shares: + * + * 1. The 27-chip horizontal letter rail becomes a dropdown on phones. + * 2. Abbreviations stops being a header segment and becomes a Show + * option inside the Filters sheet, next to sort. + * + * Demoting a view switch into a sheet hides state, so every version + * surfaces an active "Abbreviations" chip beside the letter trigger + * when that option is on. Without it the header lies about what the + * list contains. + * ------------------------------------------------------------------ */ + +type Version = "titlebar" | "single" | "toolbar"; + +const versions: ReadonlyArray<{ + id: Version; + number: string; + name: string; + summary: string; + strengths: readonly string[]; + cost: string; + chrome: string; + recommended?: boolean; +}> = [ + { + id: "titlebar", + number: "01", + name: "Title bar + letter dropdown", + summary: + "Keeps a full-size page title, then one sticky toolbar: the letter dropdown takes the width, Filters sits at the end. Desktop keeps the chip rail, which still fits there.", + strengths: ["Strongest page identity", "Full-width dropdown target", "Desktop keeps one-tap letters"], + cost: "Two bands on phone — the title still costs a row that the mode nav already implies.", + chrome: "2 rows on phone", + }, + { + id: "single", + number: "02", + name: "Single fused row", + summary: + "Title, letter dropdown, count and Filters share one sticky line. The dropdown shrinks to a chip beside the title, and the same row serves desktop.", + strengths: ["One row, both breakpoints", "Adapts rather than overflows", "Nothing scrolls horizontally"], + cost: "The letter chip is a smaller target than a full-width control, and at 390 px an active filter chip costs the row its title and count — both stand down to the mode nav until the filter clears.", + chrome: "1 row on phone", + recommended: true, + }, + { + id: "toolbar", + number: "03", + name: "Slim toolbar, title retired", + summary: + "The phone mode nav already reads Browse, so the visible heading goes (kept for screen readers) and only a slim toolbar remains: letters, count, Filters. Desktop keeps its title, having no tab bar to inherit it from.", + strengths: ["Smallest possible chrome", "Results start immediately", "Toolbar reads as controls, not decoration"], + cost: "Phone loses its visual page title; it only works while the mode nav above it stays.", + chrome: "1 slim bar on phone", + }, +]; + +/* ------------------------------- state ------------------------------- */ + +function useBrowseState() { + const [letter, setLetter] = useState("All"); + const [abbrOnly, setAbbrOnly] = useState(false); + const [sort, setSort] = useState<"az" | "za">("az"); + const [letterOpen, setLetterOpen] = useState(false); + const [filtersOpen, setFiltersOpen] = useState(false); + const activeFilters = (abbrOnly ? 1 : 0) + (sort === "za" ? 1 : 0); + return { + letter, + setLetter, + abbrOnly, + setAbbrOnly, + sort, + setSort, + letterOpen, + setLetterOpen, + filtersOpen, + setFiltersOpen, + activeFilters, + }; +} + +type BrowseState = ReturnType; + +/* ------------------------------ controls ----------------------------- */ + +function LetterTrigger({ + value, + onOpen, + grow = false, + size = "regular", +}: { + value: string; + onOpen: () => void; + grow?: boolean; + size?: "regular" | "chip"; +}) { + return ( + + ); +} + +function AbbrChip({ onClear, size = "regular" }: { onClear: () => void; size?: "regular" | "chip" }) { + return ( + + ); +} + +function FiltersControl({ + count, + labelled = false, + onOpen, + size = "regular", +}: { + count: number; + labelled?: boolean; + onOpen: () => void; + size?: "regular" | "chip"; +}) { + return ( + + ); +} + +/* ------------------------------ overlays ----------------------------- */ + +function SheetShell({ + title, + onClose, + children, + anchored = false, +}: { + title: string; + onClose: () => void; + children: React.ReactNode; + anchored?: boolean; +}) { + if (anchored) { + return ( +
+
+ ); + } + + return ( +
+
+ ); +} + +function SheetHeading({ title, onClose }: { title: string; onClose: () => void }) { + return ( +
+

{title}

+ +
+ ); +} + +function LetterPicker({ + value, + onSelect, + onClose, + anchored, +}: { + value: string; + onSelect: (next: string) => void; + onClose: () => void; + anchored?: boolean; +}) { + return ( + + +
+ {letters.map((option) => ( + + ))} +
+
+ ); +} + +function OptionRow({ + label, + hint, + selected, + onSelect, +}: { + label: string; + hint?: string; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function BrowseFilterSheet({ state, anchored }: { state: BrowseState; anchored?: boolean }) { + const close = () => state.setFiltersOpen(false); + return ( + +
+
+

+ Show · moved here from the header +

+
+ state.setAbbrOnly(false)} + /> + state.setAbbrOnly(true)} + /> +
+
+
+

+ Sort +

+
+ {(["az", "za"] as const).map((option) => ( + + ))} +
+
+
+

+ Topics +

+
+ {["Mood", "Psychosis", "Legal / ethical", "Therapies"].map((topic) => ( + + {topic} + + ))} +
+
+ +
+
+ ); +} + +/* ------------------------------- headers ----------------------------- */ + +function LetterRailDesktop({ value, onSelect }: { value: string; onSelect: (next: string) => void }) { + return ( +
+ {["All", ...letters].map((option) => ( + + ))} +
+ ); +} + +function VersionHeader({ version, compact, state }: { version: Version; compact: boolean; state: BrowseState }) { + const count = state.abbrOnly ? "24 abbreviations" : "96 terms"; + + if (version === "titlebar") { + return ( +
+
+

+ Browse terms +

+ {count} + {compact ? null : ( + <> + {state.abbrOnly ? state.setAbbrOnly(false)} /> : null} + state.setFiltersOpen(true)} /> + + )} +
+
+ {compact ? ( +
+ state.setLetterOpen(true)} grow /> + {state.abbrOnly ? state.setAbbrOnly(false)} /> : null} + state.setFiltersOpen(true)} /> +
+ ) : ( +
+ +
+ )} +
+
+ ); + } + + if (version === "single") { + return ( +
+

+ Browse terms +

+ state.setLetterOpen(true)} + size={compact ? "chip" : "regular"} + /> + {state.abbrOnly ? ( + state.setAbbrOnly(false)} size={compact ? "chip" : "regular"} /> + ) : null} + + {state.abbrOnly ? 24 : 96} + + state.setFiltersOpen(true)} + size={compact ? "chip" : "regular"} + /> +
+ ); + } + + return ( + <> + {compact ? ( +

Browse terms

+ ) : ( +
+

Browse terms

+
+ )} +
+ state.setLetterOpen(true)} size="chip" /> + {state.abbrOnly ? state.setAbbrOnly(false)} size="chip" /> : null} + {count} + state.setFiltersOpen(true)} size="chip" /> +
+ + ); +} + +/* -------------------------------- frames ------------------------------ */ + +function VersionResults({ state }: { state: BrowseState }) { + const rows = state.abbrOnly ? sampleEntries.slice(0, 2) : sampleEntries; + return ( +
+ {[...rows, ...rows].map((entry, index) => ( + + ))} +
+ ); +} + +function VersionFrames({ version }: { version: Version }) { + const desktop = useBrowseState(); + const phone = useBrowseState(); + + const overlayFor = (state: BrowseState, anchored: boolean) => { + if (state.letterOpen) { + return ( + { + state.setLetter(next); + state.setLetterOpen(false); + }} + onClose={() => state.setLetterOpen(false)} + /> + ); + } + if (state.filtersOpen) return ; + return null; + }; + + return ( + <> + + + + + + + + + + ); +} + +/* --------------------------------- page -------------------------------- */ + +export function DictionaryBrowseHeaderCompactMockupsPage() { + return ( +
+
+
+

+ Dictionary · Browse · round two +

+

+ Letters in a dropdown, Abbreviations in Filters +

+

+ Three compact takes on the title-bar direction. All three replace the 27-chip horizontal rail with a letter + dropdown on phones and move Abbreviations out of the header into the Filters sheet beside sort. They differ + only in how much header survives around those two controls. +

+

+ Every frame is live. Open the letter dropdown, then open Filters and switch to{" "} + Abbreviations only — a purple chip appears beside the letter + control. That chip is load-bearing: a view switch demoted into a sheet is otherwise invisible, and the + header would claim to list all 96 terms while showing 24 abbreviations. +

+
+
+ +
+ {versions.map((version) => ( +
+
+
+ + {version.number} + +
+
+

+ {version.name} +

+ {version.recommended ? ( + + Recommended + + ) : null} + + {version.chrome} + +
+

{version.summary}

+

+ Trade-off · {version.cost} +

+
+
+
+ {version.strengths.map((strength) => ( + + {strength} + + ))} +
+
+ +
+ +
+
+ ))} +
+
+ ); +} diff --git a/src/components/dictionary-browse-header-mockups.tsx b/src/components/dictionary-browse-header-mockups.tsx index 1ac8cf944d..d6e095264b 100644 --- a/src/components/dictionary-browse-header-mockups.tsx +++ b/src/components/dictionary-browse-header-mockups.tsx @@ -28,21 +28,21 @@ import { cn } from "@/components/ui-primitives"; * where the other modes already keep it. * ------------------------------------------------------------------ */ -const focusRing = +export const focusRing = "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); +export const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split(""); type Direction = "titlebar" | "rail" | "index"; -type SampleEntry = { +export type SampleEntry = { term: string; kind: string; alias?: string; definition: string; }; -const sampleEntries: readonly SampleEntry[] = [ +export const sampleEntries: readonly SampleEntry[] = [ { term: "Acceptance and commitment therapy", kind: "Therapy", @@ -108,7 +108,7 @@ const directions: ReadonlyArray<{ /* ---------------------------- shared bits ---------------------------- */ -function ResultRow({ entry }: { entry: SampleEntry }) { +export function ResultRow({ entry }: { entry: SampleEntry }) { return (