From 106f8e07d695a44f48503b0ed7766be884a66f76 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 10:30:13 +0000 Subject: [PATCH 1/6] Add Calculators Show all chip mockup directions. Rebuild the Tools launcher chip for a Calculators home with four interiors: the shipping transplant, a matched wordmark, a recommended icon-well capsule, and a symmetric count pair. Co-authored-by: BigSimmo --- docs/site-map.md | 1 + mockups/README.md | 17 + src/app/mockups/calculators-show-all/page.tsx | 13 + src/app/mockups/mockups-layout-client.tsx | 5 + src/components/calculator-mockups/index.ts | 1 + .../show-all-directions-mockup.tsx | 690 ++++++++++++++++++ .../calculators-show-all-mockups.dom.test.tsx | 49 ++ 7 files changed, 776 insertions(+) create mode 100644 src/app/mockups/calculators-show-all/page.tsx create mode 100644 src/components/calculator-mockups/show-all-directions-mockup.tsx create mode 100644 tests/calculators-show-all-mockups.dom.test.tsx diff --git a/docs/site-map.md b/docs/site-map.md index 33f300dc51..f090270fb3 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -1156,6 +1156,7 @@ This file is generated by `npm run docs:update` (or `npm run sitemap:update` dir - `/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/calculators-show-all` - Route discovered from app directory Source: `src/app/mockups/calculators-show-all/page.tsx`. - `/mockups/care-plan` - Route discovered from app directory Source: `src/app/mockups/care-plan/page.tsx`. - `/mockups/care-plan/governance` - Route discovered from app directory Source: `src/app/mockups/care-plan/governance/page.tsx`. - `/mockups/care-plan/patients` - Route discovered from app directory Source: `src/app/mockups/care-plan/patients/page.tsx`. diff --git a/mockups/README.md b/mockups/README.md index f8316a5f6c..9af9f8df40 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -25,6 +25,23 @@ Runnable mockups under `src/app/mockups/*` inherit the shared Clinical KB header - The bottom composer routes live searches to the dashboard with `mode`, `q`, and `run=1`; New chat routes to `/?mode=answer&focus=1`. - If a future mockup must be standalone, move it outside the `/mockups` route shell or add an explicit opt-out route group before implementing it. +## Calculators Show all chip (2026-08-24) + +Runnable study at [`/mockups/calculators-show-all`](../src/app/mockups/calculators-show-all/page.tsx). Tools already +proved the placement — hero, then a compact launcher, then search — but the shipping chip is optically left-heavy: a +14px accent icon, heading-coloured label, equal 12px padding, and an inset shadow. These frames keep that stack for +Calculators and rebuild the chip. + +| Direction | Interior | Trade-off | +| --------------------------- | ------------------------------------------------------ | --------------------------------------------- | +| 01 Tools transplant | Shipping Tools classes, calculator glyph | Documents the defect; not a candidate | +| 02 Matched wordmark | One accent colour, optical trailing padding | Still a loose glyph, no designed left mass | +| 03 Icon-well capsule (rec.) | 32px circular well, soft fill, 48px tap height | Closest to Tools; well is the designed object | +| 04 Symmetric pair | Matching wells left (icon) and right (directory count) | Strongest symmetry; a new grammar vs Tools | + +Shared mockup chrome is suppressed because every phone frame draws its own top bar and composer. Tapping **Show all** +opens the unfiltered directory in that frame. + ## Production behavior - Ordinary `/mockups/*` prototype routes return 404 in production. Explicit developer-gated subtrees and the isolated diff --git a/src/app/mockups/calculators-show-all/page.tsx b/src/app/mockups/calculators-show-all/page.tsx new file mode 100644 index 0000000000..d74d1b6163 --- /dev/null +++ b/src/app/mockups/calculators-show-all/page.tsx @@ -0,0 +1,13 @@ +import type { Metadata } from "next"; + +import { CalculatorsShowAllDirectionsMockup } from "@/components/calculator-mockups"; + +export const metadata: Metadata = { + title: "Calculators Show all directions - Clinical KB", + description: + "Four directions for a Calculators home Show all chip, rebuilt from the Tools launcher grammar with tighter colour, padding, and symmetry.", +}; + +export default function CalculatorsShowAllMockupRoute() { + return ; +} diff --git a/src/app/mockups/mockups-layout-client.tsx b/src/app/mockups/mockups-layout-client.tsx index baeddb5b94..fea4ee04fa 100644 --- a/src/app/mockups/mockups-layout-client.tsx +++ b/src/app/mockups/mockups-layout-client.tsx @@ -78,6 +78,9 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { // at the bottom on phones), so the shared universal composer is suppressed here // to avoid a second, floating search bar. const isCalculatorsSearchPageMockup = pathname === "/mockups/calculators-search-page"; + // Draws its own phone chrome, hero, Show all chip, and composer inside every + // frame. Shared header + composer would read as a second live home over the study. + const isCalculatorsShowAllMockup = pathname === "/mockups/calculators-show-all"; // Draws its own top bar, composer and results band inside every device frame, because two of // the three directions restructure that band. Shared chrome above them would read as a second, // real header and a second real composer over the study. @@ -118,6 +121,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { !isDocumentNavigationContractMockup && !isUniversalSearchRedesignMockup && !isCalculatorsSearchPageMockup && + !isCalculatorsShowAllMockup && !isPhoneInPageNavigationMockup && !isSearchBandDirectionsMockup && !isServicesFilterRefinedMockup && @@ -141,6 +145,7 @@ export function MockupsLayoutClient({ children }: { children: ReactNode }) { chromeVisible={ !isSourceOverlayRedesignMockup && !isToolsSearchDirectionsMockup && + !isCalculatorsShowAllMockup && !isSearchHeadingMockup && !isSearchRefineAdaptiveMockup && !isSearchBandDirectionsMockup && diff --git a/src/components/calculator-mockups/index.ts b/src/components/calculator-mockups/index.ts index 74d5c5023f..52db1427dc 100644 --- a/src/components/calculator-mockups/index.ts +++ b/src/components/calculator-mockups/index.ts @@ -5,3 +5,4 @@ export { CalculatorsGuidedFlowMockup } from "./guided-flow-mockup"; export { CalculatorsPopupSheetMockup } from "./popup-sheet-mockup"; export { CalculatorsSearchPageMockup } from "./search-page-mockup"; export { CalculatorsSearchDetailMockup } from "./search-detail-mockup"; +export { CalculatorsShowAllDirectionsMockup } from "./show-all-directions-mockup"; diff --git a/src/components/calculator-mockups/show-all-directions-mockup.tsx b/src/components/calculator-mockups/show-all-directions-mockup.tsx new file mode 100644 index 0000000000..9644d13096 --- /dev/null +++ b/src/components/calculator-mockups/show-all-directions-mockup.tsx @@ -0,0 +1,690 @@ +"use client"; + +/** + * Design-scratch: Calculators home “Show all” control. + * + * Tools ships a centered outline pill under the hero (`applications-launcher-page.tsx`). + * The grammar is right — a compact launcher into the unfiltered directory — but the + * chip itself is optically left-heavy: a 14px accent icon, heading-coloured label, + * equal 12px padding, and an inset shadow. These directions keep that launcher + * placement on a Calculators home and rebuild the chip so icon, label, and padding + * share one colour, one tap height, and a designed interior. + * + * Shared mockup chrome is suppressed (`mockups-layout-client.tsx`) because every + * phone frame draws its own top bar and composer. Per `mockups/README.md`, shared + * chrome is inherited or suppressed, never forked into the page. + */ + +import { + ArrowLeft, + Calculator, + ChevronDown, + Clock3, + Menu, + MessageSquarePlus, + Plus, + Search, + type LucideIcon, +} from "lucide-react"; +import { type FormEvent, useId, useMemo, useState } from "react"; + +import { cn } from "@/components/ui-primitives"; + +import { calculators, plannedCalculators, type CalculatorFixture } from "./calculator-fixtures"; + +const focusRing = + "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; + +type DirectionId = "baseline" | "wordmark" | "well" | "pair"; +type FrameView = "home" | "directory"; + +const READY_COUNT = calculators.length; +const DIRECTORY_COUNT = READY_COUNT + plannedCalculators.length; + +const shortcuts = calculators.slice(0, 8).map((calc) => ({ + id: calc.id, + label: calc.abbrev === "AUDIT-C" ? "AUDIT" : calc.abbrev === "SAD PERSONS" ? "SAD" : calc.abbrev, + icon: calc.icon, +})); + +const directions: Array<{ + id: DirectionId; + number: string; + name: string; + summary: string; + strengths: string[]; + recommended?: boolean; +}> = [ + { + id: "baseline", + number: "01", + name: "Tools transplant", + summary: + "The shipping Tools chip, with a calculator glyph. Documents the imbalance: mixed colours, a 14px icon, equal padding, and an inset shadow.", + strengths: ["Known grammar", "Shows the defect"], + }, + { + id: "wordmark", + number: "02", + name: "Matched wordmark", + summary: + "Icon and label become one mark: shared accent colour, button-scale glyph, tight gap, and extra trailing padding so the pair sits optically centred.", + strengths: ["Same recipe", "One colour", "Optical padding"], + }, + { + id: "well", + number: "03", + name: "Icon-well capsule", + summary: + "The calculator sits in a circular well that matches the remaining pill height. Left inset equals the well’s own padding; the label gets a designed trailing cap.", + strengths: ["Designed interior", "Clear hit area", "Reads as a chip"], + recommended: true, + }, + { + id: "pair", + number: "04", + name: "Symmetric pair", + summary: + "Matching wells on both ends — calculator left, count right — so the label is truly centred. The count answers “show all of what?”", + strengths: ["Bilateral symmetry", "Honest count", "Strongest object"], + }, +]; + +function ShowAllButton({ direction, onOpen }: { direction: DirectionId; onOpen: () => void }) { + const describedBy = useId(); + + if (direction === "baseline") { + return ( + + ); + } + + if (direction === "wordmark") { + return ( + + ); + } + + if (direction === "well") { + return ( + + ); + } + + return ( + + ); +} + +function PhoneChrome() { + return ( +
+ +
+ ); +} + +function Hero() { + return ( +
+ + +
+

+ Calculators +

+

+ Scores, screening, risk. +

+
+
+ ); +} + +function HomeComposer({ + value, + onChange, + onSubmit, +}: { + value: string; + onChange: (next: string) => void; + onSubmit: () => void; +}) { + return ( +
) => { + event.preventDefault(); + onSubmit(); + }} + className="grid min-h-13 w-full grid-cols-[var(--spacing-tap)_minmax(0,1fr)_var(--spacing-tap)] items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-card)]" + > + + + + +
+ ); +} + +function ShortcutTile({ + label, + icon: Icon, + selected, + onSelect, +}: { + label: string; + icon: LucideIcon; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function DirectoryRow({ + abbrev, + name, + icon: Icon, + meta, + comingSoon, + selected, + onSelect, +}: { + abbrev: string; + name: string; + icon: LucideIcon; + meta?: string; + comingSoon?: boolean; + selected: boolean; + onSelect: () => void; +}) { + return ( + + ); +} + +function matchesQuery(query: string, ...values: string[]) { + const needle = query.trim().toLowerCase(); + if (!needle) return true; + return values.some((value) => value.toLowerCase().includes(needle)); +} + +function PhoneHome({ + direction, + query, + selectedId, + onQueryChange, + onOpenDirectory, + onSelect, +}: { + direction: DirectionId; + query: string; + selectedId: string | null; + onQueryChange: (next: string) => void; + onOpenDirectory: () => void; + onSelect: (id: string) => void; +}) { + const visibleShortcuts = shortcuts.filter((tile) => matchesQuery(query, tile.label, tile.id)); + + return ( +
+ + + { + if (visibleShortcuts.length === 1) onSelect(visibleShortcuts[0].id); + else onOpenDirectory(); + }} + /> +
+ {visibleShortcuts.map((tile) => ( + onSelect(tile.id)} + /> + ))} +
+
+ ); +} + +function PhoneDirectory({ + query, + selectedId, + onQueryChange, + onBack, + onSelect, +}: { + query: string; + selectedId: string | null; + onQueryChange: (next: string) => void; + onBack: () => void; + onSelect: (id: string) => void; +}) { + const ready = calculators.filter((calc) => matchesQuery(query, calc.abbrev, calc.name, calc.indication)); + const planned = plannedCalculators.filter((calc) => matchesQuery(query, calc.abbrev, calc.name, calc.indication)); + + return ( +
+
+ +
+

All calculators

+

+ {ready.length} ready · {planned.length} planned +

+
+
+ undefined} /> +
+ {ready.map((calc: CalculatorFixture) => ( + onSelect(calc.id)} + /> + ))} + {planned.map((calc) => ( + onSelect(calc.abbrev)} + /> + ))} +
+
+ ); +} + +function PhoneFrame({ direction }: { direction: DirectionId }) { + const [view, setView] = useState("home"); + const [query, setQuery] = useState(""); + const [selectedId, setSelectedId] = useState(null); + const caption = directions.find((item) => item.id === direction)?.name ?? direction; + + return ( +
+
+ + {caption} · phone + + 390 px +
+
+ +
+ {view === "home" ? ( + setView("directory")} + onSelect={setSelectedId} + /> + ) : ( + setView("home")} + onSelect={setSelectedId} + /> + )} +
+
+
+ ); +} + +function ButtonStrip() { + const [opened, setOpened] = useState(null); + + return ( +
+
+

+ Close-up +

+

+ Four chips, one placement +

+

+ Each control opens the unfiltered directory. The recommended well keeps the Tools grammar and rebuilds the + interior so the icon is a designed mass, not a leftover glyph. +

+
+
+ {directions.map((direction) => ( +
+

+ {direction.number} {direction.name} +

+ setOpened(direction.id)} /> + {opened === direction.id ? ( +

+ Opens {DIRECTORY_COUNT} calculators +

+ ) : ( +

Tap to preview the action

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

+ Well geometry +

+

+ 48px tap height. 4px padding around a 32px well. 8px gap to the label. 16px trailing cap. Icon and label share{" "} + --clinical-accent. No inset shadow on + the pill itself — the well carries the depth. +

+
+ ); +} + +export function CalculatorsShowAllDirectionsMockup() { + const readyIds = useMemo(() => calculators.map((calc) => calc.id), []); + + return ( +
+
+
+

+ Calculators home +

+

+ A Show all chip that sits still +

+

+ Tools already proved the placement: hero, then a compact launcher, then search. These frames keep that stack + for Calculators and rebuild the chip so the icon and label share a colour, a tap height, and a designed + interior. {READY_COUNT} ready scores, {plannedCalculators.length} planned. +

+

{readyIds.join(", ")}

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

+ {direction.name} +

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

+ {direction.summary} +

+
+
+
+ {direction.strengths.map((strength) => ( + + {strength} + + ))} +
+
+ +
+ +
+ {direction.id === "well" ? : null} +
+

+ What changed vs Tools +

+
    + {direction.id === "baseline" ? ( + <> +
  • Icon is accent; label is heading colour — two competing weights.
  • +
  • Equal 12px padding cannot balance a left glyph.
  • +
  • Inset shadow on the whole pill makes the top-left read lighter.
  • + + ) : null} + {direction.id === "wordmark" ? ( + <> +
  • Icon steps up to the button glyph token (`size-icon-md`).
  • +
  • Label joins the accent, so the pair reads as one mark.
  • +
  • Trailing padding is 4px wider than the leading padding.
  • + + ) : null} + {direction.id === "well" ? ( + <> +
  • The glyph lives in a 32px well, not as a loose 14px orphan.
  • +
  • Soft fill replaces white-plus-inset-shadow.
  • +
  • Tap height stays 48px at every breakpoint — Tools shrinks on desktop.
  • + + ) : null} + {direction.id === "pair" ? ( + <> +
  • Left and right wells are the same 32px object.
  • +
  • The label is centred between them, not optically guessed.
  • +
  • The count is the directory size, including planned scores.
  • + + ) : null} +
+
+
+
+
+ ))} +
+
+ ); +} diff --git a/tests/calculators-show-all-mockups.dom.test.tsx b/tests/calculators-show-all-mockups.dom.test.tsx new file mode 100644 index 0000000000..9965c7da98 --- /dev/null +++ b/tests/calculators-show-all-mockups.dom.test.tsx @@ -0,0 +1,49 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { render, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; + +import { CalculatorsShowAllDirectionsMockup } from "@/components/calculator-mockups"; + +const DIRECTIONS = ["baseline", "wordmark", "well", "pair"] as const; + +describe("calculators Show all directions mockup", () => { + it("renders the four phone homes with a recommended well", () => { + render(); + + expect(screen.getByTestId("calculators-show-all-study")).toBeInTheDocument(); + expect(screen.getByRole("heading", { level: 1, name: "A Show all chip that sits still" })).toBeInTheDocument(); + expect(screen.getByRole("heading", { level: 2, name: "Icon-well capsule" })).toBeInTheDocument(); + expect(screen.getByText("Recommended")).toBeInTheDocument(); + + for (const direction of DIRECTIONS) { + const frame = screen.getByTestId(`calculators-show-all-frame-${direction}`); + expect(frame).toHaveAttribute("data-view", "home"); + expect(within(frame).getAllByRole("heading", { name: "Calculators" }).length).toBeGreaterThan(0); + } + }); + + it("opens the unfiltered directory from the recommended Show all chip and returns home", async () => { + const user = userEvent.setup(); + render(); + + const frame = screen.getByTestId("calculators-show-all-frame-well"); + await user.click(within(frame).getByRole("button", { name: "Show all calculators" })); + + expect(frame).toHaveAttribute("data-view", "directory"); + expect(within(frame).getByRole("heading", { name: "All calculators" })).toBeInTheDocument(); + expect(within(frame).getByLabelText("All calculators")).toBeInTheDocument(); + + await user.click(within(frame).getByRole("button", { name: "Back to calculators home" })); + expect(frame).toHaveAttribute("data-view", "home"); + expect(within(frame).getByRole("heading", { name: "Calculators" })).toBeInTheDocument(); + }); + + it("suppresses shared mockup chrome so frames are the only home on the page", () => { + const source = readFileSync(resolve(process.cwd(), "src/app/mockups/mockups-layout-client.tsx"), "utf8"); + expect(source).toContain('pathname === "/mockups/calculators-show-all"'); + expect(source).toContain("!isCalculatorsShowAllMockup"); + }); +}); From ddc61ef52a26174151498ad858c92eb0bcc9ecdf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 11:03:13 +0000 Subject: [PATCH 2/6] Simplify Calculators mockups to three Show all button styles. Keep the Tools-like phone home unchanged and vary only the Show all chip: outline, soft fill, and icon well. Co-authored-by: BigSimmo --- mockups/README.md | 24 +- src/app/mockups/calculators-show-all/page.tsx | 5 +- .../show-all-directions-mockup.tsx | 697 +++--------------- .../calculators-show-all-mockups.dom.test.tsx | 36 +- 4 files changed, 144 insertions(+), 618 deletions(-) diff --git a/mockups/README.md b/mockups/README.md index 9af9f8df40..f958bef0a5 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -27,20 +27,16 @@ Runnable mockups under `src/app/mockups/*` inherit the shared Clinical KB header ## Calculators Show all chip (2026-08-24) -Runnable study at [`/mockups/calculators-show-all`](../src/app/mockups/calculators-show-all/page.tsx). Tools already -proved the placement — hero, then a compact launcher, then search — but the shipping chip is optically left-heavy: a -14px accent icon, heading-coloured label, equal 12px padding, and an inset shadow. These frames keep that stack for -Calculators and rebuild the chip. - -| Direction | Interior | Trade-off | -| --------------------------- | ------------------------------------------------------ | --------------------------------------------- | -| 01 Tools transplant | Shipping Tools classes, calculator glyph | Documents the defect; not a candidate | -| 02 Matched wordmark | One accent colour, optical trailing padding | Still a loose glyph, no designed left mass | -| 03 Icon-well capsule (rec.) | 32px circular well, soft fill, 48px tap height | Closest to Tools; well is the designed object | -| 04 Symmetric pair | Matching wells left (icon) and right (directory count) | Strongest symmetry; a new grammar vs Tools | - -Shared mockup chrome is suppressed because every phone frame draws its own top bar and composer. Tapping **Show all** -opens the unfiltered directory in that frame. +Three phone homes at [`/mockups/calculators-show-all`](../src/app/mockups/calculators-show-all/page.tsx). The page is +the Tools launcher with Calculators copy. Only the **Show all** chip changes. + +| Style | Chip | +| ---------- | -------------------------------------------------------------- | +| 01 Outline | Hairline pill, icon + label one accent colour, optical padding | +| 02 Soft | Soft accent fill, no border, same centred wordmark | +| 03 Well | 32px circular icon well + label | + +Shared mockup chrome is suppressed because each frame draws its own top bar and composer. ## Production behavior diff --git a/src/app/mockups/calculators-show-all/page.tsx b/src/app/mockups/calculators-show-all/page.tsx index d74d1b6163..31c82acb82 100644 --- a/src/app/mockups/calculators-show-all/page.tsx +++ b/src/app/mockups/calculators-show-all/page.tsx @@ -3,9 +3,8 @@ import type { Metadata } from "next"; import { CalculatorsShowAllDirectionsMockup } from "@/components/calculator-mockups"; export const metadata: Metadata = { - title: "Calculators Show all directions - Clinical KB", - description: - "Four directions for a Calculators home Show all chip, rebuilt from the Tools launcher grammar with tighter colour, padding, and symmetry.", + title: "Calculators Show all buttons - Clinical KB", + description: "Three phone mockups of the Calculators home that differ only in the Show all button style.", }; export default function CalculatorsShowAllMockupRoute() { diff --git a/src/components/calculator-mockups/show-all-directions-mockup.tsx b/src/components/calculator-mockups/show-all-directions-mockup.tsx index 9644d13096..cc0e4c23bd 100644 --- a/src/components/calculator-mockups/show-all-directions-mockup.tsx +++ b/src/components/calculator-mockups/show-all-directions-mockup.tsx @@ -1,45 +1,21 @@ "use client"; /** - * Design-scratch: Calculators home “Show all” control. - * - * Tools ships a centered outline pill under the hero (`applications-launcher-page.tsx`). - * The grammar is right — a compact launcher into the unfiltered directory — but the - * chip itself is optically left-heavy: a 14px accent icon, heading-coloured label, - * equal 12px padding, and an inset shadow. These directions keep that launcher - * placement on a Calculators home and rebuild the chip so icon, label, and padding - * share one colour, one tap height, and a designed interior. - * - * Shared mockup chrome is suppressed (`mockups-layout-client.tsx`) because every - * phone frame draws its own top bar and composer. Per `mockups/README.md`, shared - * chrome is inherited or suppressed, never forked into the page. + * Design-scratch: three phone homes that are identical except the Show all chip. + * Shared mockup chrome is suppressed so each frame’s own top bar is the only header. */ -import { - ArrowLeft, - Calculator, - ChevronDown, - Clock3, - Menu, - MessageSquarePlus, - Plus, - Search, - type LucideIcon, -} from "lucide-react"; -import { type FormEvent, useId, useMemo, useState } from "react"; +import { Calculator, ChevronDown, Menu, MessageSquarePlus, Plus, Search, type LucideIcon } from "lucide-react"; +import { type FormEvent, useState } from "react"; import { cn } from "@/components/ui-primitives"; -import { calculators, plannedCalculators, type CalculatorFixture } from "./calculator-fixtures"; +import { calculators } from "./calculator-fixtures"; const focusRing = "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"; -type DirectionId = "baseline" | "wordmark" | "well" | "pair"; -type FrameView = "home" | "directory"; - -const READY_COUNT = calculators.length; -const DIRECTORY_COUNT = READY_COUNT + plannedCalculators.length; +type ButtonStyle = "outline" | "soft" | "well"; const shortcuts = calculators.slice(0, 8).map((calc) => ({ id: calc.id, @@ -47,109 +23,49 @@ const shortcuts = calculators.slice(0, 8).map((calc) => ({ icon: calc.icon, })); -const directions: Array<{ - id: DirectionId; - number: string; - name: string; - summary: string; - strengths: string[]; - recommended?: boolean; -}> = [ - { - id: "baseline", - number: "01", - name: "Tools transplant", - summary: - "The shipping Tools chip, with a calculator glyph. Documents the imbalance: mixed colours, a 14px icon, equal padding, and an inset shadow.", - strengths: ["Known grammar", "Shows the defect"], - }, - { - id: "wordmark", - number: "02", - name: "Matched wordmark", - summary: - "Icon and label become one mark: shared accent colour, button-scale glyph, tight gap, and extra trailing padding so the pair sits optically centred.", - strengths: ["Same recipe", "One colour", "Optical padding"], - }, - { - id: "well", - number: "03", - name: "Icon-well capsule", - summary: - "The calculator sits in a circular well that matches the remaining pill height. Left inset equals the well’s own padding; the label gets a designed trailing cap.", - strengths: ["Designed interior", "Clear hit area", "Reads as a chip"], - recommended: true, - }, - { - id: "pair", - number: "04", - name: "Symmetric pair", - summary: - "Matching wells on both ends — calculator left, count right — so the label is truly centred. The count answers “show all of what?”", - strengths: ["Bilateral symmetry", "Honest count", "Strongest object"], - }, +const styles: Array<{ id: ButtonStyle; number: string; name: string }> = [ + { id: "outline", number: "01", name: "Outline" }, + { id: "soft", number: "02", name: "Soft fill" }, + { id: "well", number: "03", name: "Icon well" }, ]; -function ShowAllButton({ direction, onOpen }: { direction: DirectionId; onOpen: () => void }) { - const describedBy = useId(); +function ShowAllButton({ style, pressed, onPress }: { style: ButtonStyle; pressed: boolean; onPress: () => void }) { + const label = "Show all"; - if (direction === "baseline") { + if (style === "outline") { return ( - ); - } - - if (direction === "wordmark") { - return ( - ); } - if (direction === "well") { + if (style === "soft") { return ( ); } @@ -157,20 +73,19 @@ function ShowAllButton({ direction, onOpen }: { direction: DirectionId; onOpen: return ( ); } @@ -197,492 +112,112 @@ function PhoneChrome() { ); } -function Hero() { - return ( -
- - -
-

- Calculators -

-

- Scores, screening, risk. -

-
-
- ); -} - -function HomeComposer({ - value, - onChange, - onSubmit, -}: { - value: string; - onChange: (next: string) => void; - onSubmit: () => void; -}) { - return ( -
) => { - event.preventDefault(); - onSubmit(); - }} - className="grid min-h-13 w-full grid-cols-[var(--spacing-tap)_minmax(0,1fr)_var(--spacing-tap)] items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-card)]" - > - - - - -
- ); -} - -function ShortcutTile({ - label, - icon: Icon, - selected, - onSelect, -}: { - label: string; - icon: LucideIcon; - selected: boolean; - onSelect: () => void; -}) { - return ( - - ); -} - -function DirectoryRow({ - abbrev, - name, - icon: Icon, - meta, - comingSoon, - selected, - onSelect, -}: { - abbrev: string; - name: string; - icon: LucideIcon; - meta?: string; - comingSoon?: boolean; - selected: boolean; - onSelect: () => void; -}) { - return ( - - ); -} - -function matchesQuery(query: string, ...values: string[]) { - const needle = query.trim().toLowerCase(); - if (!needle) return true; - return values.some((value) => value.toLowerCase().includes(needle)); -} - -function PhoneHome({ - direction, - query, - selectedId, - onQueryChange, - onOpenDirectory, - onSelect, -}: { - direction: DirectionId; - query: string; - selectedId: string | null; - onQueryChange: (next: string) => void; - onOpenDirectory: () => void; - onSelect: (id: string) => void; -}) { - const visibleShortcuts = shortcuts.filter((tile) => matchesQuery(query, tile.label, tile.id)); - - return ( -
- - - { - if (visibleShortcuts.length === 1) onSelect(visibleShortcuts[0].id); - else onOpenDirectory(); - }} - /> -
- {visibleShortcuts.map((tile) => ( - onSelect(tile.id)} - /> - ))} -
-
- ); -} - -function PhoneDirectory({ - query, - selectedId, - onQueryChange, - onBack, - onSelect, -}: { - query: string; - selectedId: string | null; - onQueryChange: (next: string) => void; - onBack: () => void; - onSelect: (id: string) => void; -}) { - const ready = calculators.filter((calc) => matchesQuery(query, calc.abbrev, calc.name, calc.indication)); - const planned = plannedCalculators.filter((calc) => matchesQuery(query, calc.abbrev, calc.name, calc.indication)); - - return ( -
-
- -
-

All calculators

-

- {ready.length} ready · {planned.length} planned -

-
-
- undefined} /> -
- {ready.map((calc: CalculatorFixture) => ( - onSelect(calc.id)} - /> - ))} - {planned.map((calc) => ( - onSelect(calc.abbrev)} - /> - ))} -
-
- ); -} - -function PhoneFrame({ direction }: { direction: DirectionId }) { - const [view, setView] = useState("home"); +function PhoneHome({ style }: { style: ButtonStyle }) { const [query, setQuery] = useState(""); + const [pressed, setPressed] = useState(false); const [selectedId, setSelectedId] = useState(null); - const caption = directions.find((item) => item.id === direction)?.name ?? direction; return (
-
- - {caption} · phone - - 390 px +
+ {styles.find((item) => item.id === style)?.number} {styles.find((item) => item.id === style)?.name}
-
- {view === "home" ? ( - setView("directory")} - onSelect={setSelectedId} - /> - ) : ( - setView("home")} - onSelect={setSelectedId} - /> - )} -
-
-
- ); -} +
+
+ + +
+

+ Calculators +

+

+ Scores, screening, risk. +

+
+
-function ButtonStrip() { - const [opened, setOpened] = useState(null); + setPressed((value) => !value)} /> - return ( -
-
-

- Close-up -

-

- Four chips, one placement -

-

- Each control opens the unfiltered directory. The recommended well keeps the Tools grammar and rebuilds the - interior so the icon is a designed mass, not a leftover glyph. -

-
-
- {directions.map((direction) => ( -
) => event.preventDefault()} + className="grid min-h-13 w-full grid-cols-[var(--spacing-tap)_minmax(0,1fr)_var(--spacing-tap)] items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface-lux)] shadow-[var(--shadow-card)]" > -

- {direction.number} {direction.name} -

- setOpened(direction.id)} /> - {opened === direction.id ? ( -

- Opens {DIRECTORY_COUNT} calculators -

- ) : ( -

Tap to preview the action

- )} + + + + + + +
+ {shortcuts.map((tile) => { + const Icon: LucideIcon = tile.icon; + const selected = selectedId === tile.id; + return ( + + ); + })}
- ))} +
-
- ); -} - -function Anatomy() { - return ( -
-

- Well geometry -

-

- 48px tap height. 4px padding around a 32px well. 8px gap to the label. 16px trailing cap. Icon and label share{" "} - --clinical-accent. No inset shadow on - the pill itself — the well carries the depth. -

-
+ ); } export function CalculatorsShowAllDirectionsMockup() { - const readyIds = useMemo(() => calculators.map((calc) => calc.id), []); - return (
-
-
-

- Calculators home -

-

- A Show all chip that sits still -

-

- Tools already proved the placement: hero, then a compact launcher, then search. These frames keep that stack - for Calculators and rebuild the chip so the icon and label share a colour, a tap height, and a designed - interior. {READY_COUNT} ready scores, {plannedCalculators.length} planned. -

-

{readyIds.join(", ")}

-
-
- -
- - - {directions.map((direction) => ( -
-
-
- - {direction.number} - -
-
-

- {direction.name} -

- {direction.recommended ? ( - - Recommended - - ) : null} -
-

- {direction.summary} -

-
-
-
- {direction.strengths.map((strength) => ( - - {strength} - - ))} -
-
- -
- -
- {direction.id === "well" ? : null} -
-

- What changed vs Tools -

-
    - {direction.id === "baseline" ? ( - <> -
  • Icon is accent; label is heading colour — two competing weights.
  • -
  • Equal 12px padding cannot balance a left glyph.
  • -
  • Inset shadow on the whole pill makes the top-left read lighter.
  • - - ) : null} - {direction.id === "wordmark" ? ( - <> -
  • Icon steps up to the button glyph token (`size-icon-md`).
  • -
  • Label joins the accent, so the pair reads as one mark.
  • -
  • Trailing padding is 4px wider than the leading padding.
  • - - ) : null} - {direction.id === "well" ? ( - <> -
  • The glyph lives in a 32px well, not as a loose 14px orphan.
  • -
  • Soft fill replaces white-plus-inset-shadow.
  • -
  • Tap height stays 48px at every breakpoint — Tools shrinks on desktop.
  • - - ) : null} - {direction.id === "pair" ? ( - <> -
  • Left and right wells are the same 32px object.
  • -
  • The label is centred between them, not optically guessed.
  • -
  • The count is the directory size, including planned scores.
  • - - ) : null} -
-
-
-
-
+

Calculators Show all button styles

+
+ {styles.map((style) => ( + ))}
diff --git a/tests/calculators-show-all-mockups.dom.test.tsx b/tests/calculators-show-all-mockups.dom.test.tsx index 9965c7da98..8d62540f5a 100644 --- a/tests/calculators-show-all-mockups.dom.test.tsx +++ b/tests/calculators-show-all-mockups.dom.test.tsx @@ -7,41 +7,37 @@ import { describe, expect, it } from "vitest"; import { CalculatorsShowAllDirectionsMockup } from "@/components/calculator-mockups"; -const DIRECTIONS = ["baseline", "wordmark", "well", "pair"] as const; +const STYLES = ["outline", "soft", "well"] as const; -describe("calculators Show all directions mockup", () => { - it("renders the four phone homes with a recommended well", () => { +describe("calculators Show all button mockups", () => { + it("renders three phone homes that differ only by the Show all chip", () => { render(); expect(screen.getByTestId("calculators-show-all-study")).toBeInTheDocument(); - expect(screen.getByRole("heading", { level: 1, name: "A Show all chip that sits still" })).toBeInTheDocument(); - expect(screen.getByRole("heading", { level: 2, name: "Icon-well capsule" })).toBeInTheDocument(); - expect(screen.getByText("Recommended")).toBeInTheDocument(); - - for (const direction of DIRECTIONS) { - const frame = screen.getByTestId(`calculators-show-all-frame-${direction}`); - expect(frame).toHaveAttribute("data-view", "home"); - expect(within(frame).getAllByRole("heading", { name: "Calculators" }).length).toBeGreaterThan(0); + expect(screen.getByRole("heading", { level: 1, name: "Calculators Show all button styles" })).toBeInTheDocument(); + + for (const style of STYLES) { + const frame = screen.getByTestId(`calculators-show-all-frame-${style}`); + expect(within(frame).getByRole("heading", { name: "Calculators" })).toBeInTheDocument(); + expect(within(frame).getByRole("button", { name: "Show all calculators" })).toHaveTextContent("Show all"); } }); - it("opens the unfiltered directory from the recommended Show all chip and returns home", async () => { + it("presses the Show all chip without changing the rest of the home", async () => { const user = userEvent.setup(); render(); const frame = screen.getByTestId("calculators-show-all-frame-well"); - await user.click(within(frame).getByRole("button", { name: "Show all calculators" })); - - expect(frame).toHaveAttribute("data-view", "directory"); - expect(within(frame).getByRole("heading", { name: "All calculators" })).toBeInTheDocument(); - expect(within(frame).getByLabelText("All calculators")).toBeInTheDocument(); + const showAll = within(frame).getByRole("button", { name: "Show all calculators" }); + expect(showAll).toHaveAttribute("aria-pressed", "false"); - await user.click(within(frame).getByRole("button", { name: "Back to calculators home" })); - expect(frame).toHaveAttribute("data-view", "home"); + await user.click(showAll); + expect(showAll).toHaveAttribute("aria-pressed", "true"); expect(within(frame).getByRole("heading", { name: "Calculators" })).toBeInTheDocument(); + expect(within(frame).getByLabelText("Calculator shortcuts")).toBeInTheDocument(); }); - it("suppresses shared mockup chrome so frames are the only home on the page", () => { + it("suppresses shared mockup chrome so the three phones are the page", () => { const source = readFileSync(resolve(process.cwd(), "src/app/mockups/mockups-layout-client.tsx"), "utf8"); expect(source).toContain('pathname === "/mockups/calculators-show-all"'); expect(source).toContain("!isCalculatorsShowAllMockup"); From 68316136e286af412440e0dec371d0c649c1275c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 24 Aug 2026 14:11:45 +0000 Subject: [PATCH 3/6] Polish Show all as a soft-fill well chip. Keep the 48px tap target and implement a 36px soft capsule with a 28px icon well on Tools home. Add two tighter phone mockups beside the recommended style. Co-authored-by: BigSimmo --- mockups/README.md | 10 +- src/app/mockups/calculators-show-all/page.tsx | 3 +- src/components/applications-launcher-page.tsx | 13 ++- .../show-all-directions-mockup.tsx | 95 ++++++++++++------- .../calculators-show-all-mockups.dom.test.tsx | 8 +- tests/favourites-auth-gate.dom.test.tsx | 11 +++ 6 files changed, 93 insertions(+), 47 deletions(-) diff --git a/mockups/README.md b/mockups/README.md index f958bef0a5..da87be589e 100644 --- a/mockups/README.md +++ b/mockups/README.md @@ -30,11 +30,11 @@ Runnable mockups under `src/app/mockups/*` inherit the shared Clinical KB header Three phone homes at [`/mockups/calculators-show-all`](../src/app/mockups/calculators-show-all/page.tsx). The page is the Tools launcher with Calculators copy. Only the **Show all** chip changes. -| Style | Chip | -| ---------- | -------------------------------------------------------------- | -| 01 Outline | Hairline pill, icon + label one accent colour, optical padding | -| 02 Soft | Soft accent fill, no border, same centred wordmark | -| 03 Well | 32px circular icon well + label | +| Style | Chip | +| --------------- | ------------------------------------------------------------------- | +| 01 Recommended | Soft fill + 28px surface well, 36px capsule, 48px tap (implemented) | +| 02 Soft capsule | Option 2 polished — whisper fill, no well, optical `pl-3.5 pr-4` | +| 03 Quiet well | Option 3 polished — well only, no pill fill | Shared mockup chrome is suppressed because each frame draws its own top bar and composer. diff --git a/src/app/mockups/calculators-show-all/page.tsx b/src/app/mockups/calculators-show-all/page.tsx index 31c82acb82..2fb4a04a25 100644 --- a/src/app/mockups/calculators-show-all/page.tsx +++ b/src/app/mockups/calculators-show-all/page.tsx @@ -4,7 +4,8 @@ import { CalculatorsShowAllDirectionsMockup } from "@/components/calculator-mock export const metadata: Metadata = { title: "Calculators Show all buttons - Clinical KB", - description: "Three phone mockups of the Calculators home that differ only in the Show all button style.", + description: + "Recommended Show all chip plus two polished phone alternatives. The homes are identical except the chip.", }; export default function CalculatorsShowAllMockupRoute() { diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index ecaa60af37..0a288664dd 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -836,12 +836,19 @@ export function ApplicationsLauncherWorkspace({ aria-label="Show all tools" data-testid="tools-show-all" className={cn( - "inline-flex min-h-tap items-center justify-center gap-2 rounded-full border border-[color:var(--clinical-accent-border)] bg-[color:var(--surface)] px-3 text-xs font-semibold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--clinical-accent-soft)] sm:text-sm lg:min-h-9", + "group inline-flex min-h-tap items-center justify-center text-[color:var(--clinical-accent)] lg:min-h-9", focusRing, )} > -