From 3606707b65a82f5ace23f8a162f9b229f83b7019 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:44:18 +0800 Subject: [PATCH 1/3] feat(auth): refine responsive account setup --- .../account-setup-dialog.tsx | 252 +++++++++++------- tests/favourites-auth-gate.dom.test.tsx | 28 +- tests/ui-smoke.spec.ts | 42 ++- 3 files changed, 198 insertions(+), 124 deletions(-) diff --git a/src/components/clinical-dashboard/account-setup-dialog.tsx b/src/components/clinical-dashboard/account-setup-dialog.tsx index 4f004e3533..0a176c50e9 100644 --- a/src/components/clinical-dashboard/account-setup-dialog.tsx +++ b/src/components/clinical-dashboard/account-setup-dialog.tsx @@ -1,7 +1,18 @@ "use client"; +import Link from "next/link"; import { type FormEvent, useState } from "react"; -import { Clock3, Heart, Loader2, Mail, ShieldCheck, SlidersHorizontal, type LucideIcon } from "lucide-react"; +import { + ArrowRight, + Clock3, + Heart, + Loader2, + LockKeyhole, + Mail, + ShieldCheck, + SlidersHorizontal, + type LucideIcon, +} from "lucide-react"; import { BrandMark } from "@/components/clinical-dashboard/brand"; import { ProviderBrandIcon, type SsoProvider } from "@/components/clinical-dashboard/provider-brand-icons"; @@ -10,31 +21,31 @@ import { TextField } from "@/components/ui/text-field"; import { AsyncButton, cn, floatingControl, InlineNotice, primaryControl } from "@/components/ui-primitives"; import { useAuthSession, type OAuthProvider } from "@/lib/supabase/client"; -const storageBenefits = [ +const workspaceBenefits = [ { - label: "Saved favourites", - detail: "Available whenever you sign in on another device.", - scope: "Account", + label: "Save favourites", + mobileLabel: "Favourites sync", + detail: "Reopen trusted resources on any device.", icon: Heart, }, { - label: "Clinical defaults", - detail: "Your jurisdiction and answer style follow your account.", - scope: "Account", + label: "Keep your clinical defaults", + mobileLabel: "Preferences sync", + detail: "Your jurisdiction and answer style follow you.", icon: SlidersHorizontal, }, { - label: "Recent searches", - detail: "Stay in this browser session and do not sync.", - scope: "This device", + label: "Recent searches stay here", + mobileLabel: "Searches stay here", + detail: "Browser activity does not sync to your account.", icon: Clock3, }, ] as const; -type StorageBenefit = { +type WorkspaceBenefit = { label: string; + mobileLabel: string; detail: string; - scope: "Account" | "This device"; icon: LucideIcon; }; @@ -60,10 +71,10 @@ export function AccountSetupDialog({ const busy = auth.status === "loading"; const actionBusy = busy || pendingProvider !== null; const isFavouritesIntent = intent === "favourites"; - const title = isFavouritesIntent ? "Sign up to save favourites" : "Set up your workspace"; + const title = isFavouritesIntent ? "Sign up to save favourites" : "Continue to your workspace"; const description = isFavouritesIntent ? "Sign in or create an account to save favourites and reopen them on any device." - : "Sign in or create an account to keep favourites and clinical defaults with you."; + : "Sign in or create an account in one step."; const error = actionAttempted ? auth.error : null; const notice = actionAttempted ? auth.notice : null; @@ -91,59 +102,61 @@ export function AccountSetupDialog({ - - - } - headerClassName="bg-[color:var(--surface-lux)]" - titleClassName="text-base sm:text-lg" - bodyClassName="bg-[color:var(--surface)] p-4 sm:p-6" - contentClassName="account-setup-dialog max-h-[calc(100dvh-0.5rem)] sm:max-h-[calc(100dvh-2rem)] sm:max-w-[44rem]" + headerClassName="absolute right-3 top-3 z-30 w-auto border-0 bg-transparent p-0 sm:right-4 sm:top-4 sm:p-0" + titleClassName="sr-only" + bodyClassName="bg-[color:var(--surface)] p-0 sm:p-0" + contentClassName="account-setup-dialog relative max-h-[calc(100dvh-0.5rem)] sm:max-h-[calc(100dvh-2rem)] sm:max-w-[68rem]" portal > -
-
-
-

- Choose how to continue -

-

- Use an existing account for the quickest setup. -

-
+
+ -
- {(["Apple", "Google", "Microsoft"] as const).map((provider) => ( - void chooseProvider(provider)} - /> - ))} -
-
+ +
+
+

+ {title} +

+

+ {description} +

+
-
- - or continue with email - -
+
+ {(["Apple", "Google", "Microsoft"] as const).map((provider) => ( + void chooseProvider(provider)} + /> + ))} +
+
+ +
+ + or continue with email + +
-
setEmail(event.target.value)} - placeholder="you@clinic.example" + placeholder="name@clinic.com" autoComplete="email" inputMode="email" enterKeyHint="go" @@ -158,68 +171,109 @@ export function AccountSetupDialog({ busy={busy && pendingProvider === null} busyLabel="Sending link…" disabled={actionBusy || !email.trim()} - idleIcon={
- {notice ? {notice} : null} - {error ? {error} : null} +

+

+ + {notice ? {notice} : null} + {error ? {error} : null} + + + + +
+ ); +} + +function AccountOrientationPanel() { + return ( +
+
); } -function StorageBenefitRow({ benefit }: { benefit: StorageBenefit }) { +function WorkspaceBenefitRow({ benefit }: { benefit: WorkspaceBenefit }) { const Icon = benefit.icon; return ( -
  • - -
  • + + - {benefit.label} - {benefit.detail} - - - {benefit.scope} + + {benefit.mobileLabel} + + + {benefit.label} + + + {benefit.detail} +
  • ); } +function PrivacyFooter({ className }: { className?: string }) { + return ( +
    +
    + ); +} + function ProviderButton({ provider, busy, @@ -240,7 +294,7 @@ function ProviderButton({ data-provider={provider.toLowerCase()} className={cn( floatingControl, - "w-full min-w-0 justify-center gap-2.5 bg-[color:var(--surface-lux)] px-3 shadow-[var(--shadow-inset)]", + "min-h-tap w-full min-w-0 justify-center gap-2.5 bg-[color:var(--surface-lux)] px-3 shadow-[var(--shadow-inset)]", )} > {pending ? ( diff --git a/tests/favourites-auth-gate.dom.test.tsx b/tests/favourites-auth-gate.dom.test.tsx index 681453a3cb..0534422e19 100644 --- a/tests/favourites-auth-gate.dom.test.tsx +++ b/tests/favourites-auth-gate.dom.test.tsx @@ -154,18 +154,24 @@ describe("favourites auth gate DOM", () => { expect(screen.getByRole("heading", { name: "Sign up to save favourites" })).toBeVisible(); expect(screen.getByText(/Sign in or create an account to save favourites/i)).toBeVisible(); - expect(screen.getByText("Saved favourites")).toBeVisible(); + expect(screen.getByText("Save favourites")).toBeVisible(); }); it("separates account-synced data from device-only recents", () => { render( undefined} />); - expect(screen.getByRole("heading", { name: "What’s saved where" })).toBeVisible(); - expect(screen.getAllByText("Account")).toHaveLength(2); - expect(screen.getByText("This device")).toBeVisible(); - expect(screen.getByText(/Recent searches/i)).toBeVisible(); - expect(screen.getByText(/Stay in this browser session and do not sync/i)).toBeVisible(); - expect(screen.getByText(/No PHI required\./i)).toBeVisible(); + expect(screen.getByRole("heading", { name: "Your workspace, wherever you work." })).toBeVisible(); + expect(screen.getByText("Save favourites")).toBeVisible(); + expect(screen.getByText(/Reopen trusted resources on any device/i)).toBeVisible(); + expect(screen.getByText("Keep your clinical defaults")).toBeVisible(); + expect(screen.getByText(/Your jurisdiction and answer style follow you/i)).toBeVisible(); + expect(screen.getByText("Recent searches stay here")).toBeVisible(); + expect(screen.getByText(/Browser activity does not sync to your account/i)).toBeVisible(); + expect(screen.getAllByText("Do not enter patient-identifiable information.")).toHaveLength(2); + expect(screen.getAllByRole("link", { name: "Privacy and data processing" })).toHaveLength(2); + for (const privacyLink of screen.getAllByRole("link", { name: "Privacy and data processing" })) { + expect(privacyLink).toHaveAttribute("href", "/privacy"); + } expect(screen.queryByText(/Everything syncs across your devices/i)).toBeNull(); expect(screen.queryByText(/never shared/i)).toBeNull(); expect(screen.queryByText("Account-scoped saves")).toBeNull(); @@ -178,7 +184,7 @@ describe("favourites auth gate DOM", () => { const apple = screen.getByRole("button", { name: "Continue with Apple" }); const google = screen.getByRole("button", { name: "Continue with Google" }); const microsoft = screen.getByRole("button", { name: "Continue with Microsoft" }); - const email = screen.getByLabelText(/Email address/); + const email = screen.getByLabelText(/Work email/); expect(apple.compareDocumentPosition(google) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); expect(google.compareDocumentPosition(microsoft) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); @@ -206,16 +212,16 @@ describe("favourites auth gate DOM", () => { for (const provider of ["Apple", "Google", "Microsoft"]) { expect(screen.getByRole("button", { name: `Continue with ${provider}` })).toBeDisabled(); } - expect(screen.getByRole("button", { name: "Continue with email" })).toBeDisabled(); + expect(screen.getByRole("button", { name: "Continue securely" })).toBeDisabled(); }); it("submits email and announces success and failure feedback", async () => { const user = userEvent.setup(); const { rerender } = render( undefined} />); - const submit = screen.getByRole("button", { name: "Continue with email" }); + const submit = screen.getByRole("button", { name: "Continue securely" }); expect(submit).toBeDisabled(); - const email = screen.getByLabelText(/Email address/); + const email = screen.getByLabelText(/Work email/); expect(email).toHaveAttribute("data-sheet-autofocus", "true"); await user.type(email, "clinician@clinic.example"); await user.click(submit); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index f574c5100f..e675520546 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -904,7 +904,7 @@ function accountSettingsDialog(page: Page) { } function accountSetupDialog(page: Page) { - return page.getByRole("dialog", { name: "Set up your workspace" }); + return page.getByRole("dialog", { name: "Account setup" }); } async function expectAccountSettingsSurface(settings: Locator) { @@ -981,19 +981,31 @@ async function expectMobileSettingsLayout(settings: Locator) { } async function expectAccountSetupSurface(setup: Locator) { - await expect(setup.getByRole("heading", { name: "Set up your workspace" })).toBeVisible(); - await expect(setup.getByLabel("Email address")).toBeVisible(); - await expect(setup.getByRole("button", { name: "Continue with email" })).toBeVisible(); + await expect(setup.getByRole("heading", { name: "Continue to your workspace" })).toBeVisible(); + await expect(setup.getByRole("heading", { name: "Your workspace, wherever you work." })).toBeVisible(); + await expect(setup.getByLabel("Work email")).toBeVisible(); + await expect(setup.getByRole("button", { name: "Continue securely" })).toBeVisible(); await expect(setup.getByRole("button", { name: "Continue with Apple" })).toBeEnabled(); await expect(setup.getByRole("button", { name: "Continue with Google" })).toBeEnabled(); await expect(setup.getByRole("button", { name: "Continue with Microsoft" })).toBeEnabled(); await expect(setup.getByText(/Apple sign-in is not available/i)).toHaveCount(0); - await expect(setup.getByRole("heading", { name: "What’s saved where" })).toBeVisible(); - await expect(setup.getByText("Account", { exact: true })).toHaveCount(2); - await expect(setup.getByText("This device", { exact: true })).toBeVisible(); - await expect(setup.getByText(/Stay in this browser session and do not sync/i)).toBeVisible(); - await expect(setup.getByText(/No PHI required\./i)).toBeVisible(); - await expect(setup).toContainText("Do not enter patient-identifying information during sign-in."); + const accountSetupViewportWidth = await setup.evaluate(() => window.innerWidth); + if (accountSetupViewportWidth >= 1024) { + await expect(setup.getByText("Save favourites", { exact: true })).toBeVisible(); + await expect(setup.getByText(/Reopen trusted resources on any device/i)).toBeVisible(); + await expect(setup.getByText("Keep your clinical defaults", { exact: true })).toBeVisible(); + await expect(setup.getByText(/Your jurisdiction and answer style follow you/i)).toBeVisible(); + await expect(setup.getByText("Recent searches stay here", { exact: true })).toBeVisible(); + await expect(setup.getByText(/Browser activity does not sync to your account/i)).toBeVisible(); + } else { + await expect(setup.getByText("Favourites sync", { exact: true })).toBeVisible(); + await expect(setup.getByText("Preferences sync", { exact: true })).toBeVisible(); + await expect(setup.getByText("Searches stay here", { exact: true })).toBeVisible(); + } + const privacyLink = setup.getByRole("link", { name: "Privacy and data processing" }); + await expect(privacyLink).toBeVisible(); + await expect(privacyLink).toHaveAttribute("href", "/privacy"); + await expect(privacyLink.locator("xpath=..")).toContainText("Do not enter patient-identifiable information."); } async function expectAccountProviderLayout(setup: Locator, layout: "row" | "stack") { @@ -1588,7 +1600,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(setup).toBeVisible(); await expectAccountSetupSurface(setup); await expectAccountProviderLayout(setup, "stack"); - await expect(setup.getByLabel("Email address")).toBeFocused(); + await expect(setup.getByLabel("Work email")).toBeFocused(); const setupBox = await setup.boundingBox(); expect(setupBox).not.toBeNull(); expect(setupBox!.x).toBeGreaterThanOrEqual(-1); @@ -1597,8 +1609,10 @@ test.describe("Clinical KB UI smoke coverage", () => { await page.setViewportSize({ width: 320, height: 700 }); const setupClose = setup.getByRole("button", { name: "Close account setup" }); - await expect(setup.getByLabel("Email address")).toBeInViewport(); - await expect(setup.getByRole("button", { name: "Continue with email" })).toBeInViewport(); + const setupEmail = setup.getByLabel("Work email"); + await setupEmail.scrollIntoViewIfNeeded(); + await expect(setupEmail).toBeInViewport(); + await expect(setup.getByRole("button", { name: "Continue securely" })).toBeInViewport(); await expect(setupClose).toBeInViewport(); await expectNoPageHorizontalOverflow(page); @@ -1606,7 +1620,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await setupScrollPort.evaluate((element) => { element.scrollTop = element.scrollHeight; }); - await expect(setup.getByText("No PHI required")).toBeInViewport(); + await expect(setup.getByRole("link", { name: "Privacy and data processing" })).toBeInViewport(); await expect(setupClose).toBeInViewport(); await page.emulateMedia({ reducedMotion: "reduce", forcedColors: "active" }); From e79e5d5900e031bd0e8ad6a47c6a23343411daea Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:47:40 +0800 Subject: [PATCH 2/3] chore(review): record account setup review --- ...82663cf239be31aa3a5eb4df2c4e9b49b249ce251af596c2038.record.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/branch-review-records/9ef3a3c00e3f782663cf239be31aa3a5eb4df2c4e9b49b249ce251af596c2038.record.md diff --git a/docs/branch-review-records/9ef3a3c00e3f782663cf239be31aa3a5eb4df2c4e9b49b249ce251af596c2038.record.md b/docs/branch-review-records/9ef3a3c00e3f782663cf239be31aa3a5eb4df2c4e9b49b249ce251af596c2038.record.md new file mode 100644 index 0000000000..5c925c86a1 --- /dev/null +++ b/docs/branch-review-records/9ef3a3c00e3f782663cf239be31aa3a5eb4df2c4e9b49b249ce251af596c2038.record.md @@ -0,0 +1 @@ +| 2026-08-14 | codex/account-setup-polish-20260814 | 3606707b65a82f5ace23f8a162f9b229f83b7019 | account setup responsive auth privacy UI | No reproducible P0-P3 findings; local dev-server stale chunk was cleared by repository-safe restart and did not reproduce | live desktop and 390px phone review; axe WCAG A/AA 0 violations; focused DOM 17 passed; focused Chromium desktop and phone passed; typecheck, formatting, production-readiness passed; verify:pr-local timed out after 15 minutes without decisive output | From 9c3d1b9dbf4484205c0d636337f72b4e9d1bbc4c Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 16 Aug 2026 02:27:11 +0800 Subject: [PATCH 3/3] fix(services): remove quick-search suggestions from service filters --- docs/filter-contract.md | 9 +++--- .../services/services-navigator-page.tsx | 29 ------------------- tests/ui-tools.spec.ts | 15 +++------- 3 files changed, 9 insertions(+), 44 deletions(-) diff --git a/docs/filter-contract.md b/docs/filter-contract.md index 0469e77946..342de4b41a 100644 --- a/docs/filter-contract.md +++ b/docs/filter-contract.md @@ -28,10 +28,11 @@ does today — tells the reader they cannot hold two domains at once, which is f ### There is no `navigate` kind, and that is the point -Services' quick filters do not filter. They call `router.push` and **replace the query**, so -choosing one discards the search and its results with no warning and no undo. A control labelled -"Filter" must not do that. Query-replacing presets belong beside the composer as suggested -searches (`AnswerSuggestionChips`), not inside the filter sheet. +Services' former quick filters did not filter. They called `router.push` and **replaced the query**, +so choosing one discarded the search and its results with no warning and no undo. A control labelled +"Filter" must not do that. The presets were removed because they also competed visually with the +service-group navigation; query-replacing suggestions should only return if they have a distinct, +clearly labelled home beside the composer. Factsheets' category dimension is not this pattern, despite an earlier draft of this section grouping it with services' quick filters: `filterFactsheets(query, category)` ANDs the two, so diff --git a/src/components/services/services-navigator-page.tsx b/src/components/services/services-navigator-page.tsx index b02e528d70..e6443dfbcf 100644 --- a/src/components/services/services-navigator-page.tsx +++ b/src/components/services/services-navigator-page.tsx @@ -13,7 +13,6 @@ import { SearchResultsSkeleton, } from "@/components/clinical-dashboard/search-results-header-band"; import { UniversalSearchAlsoMatches } from "@/components/clinical-dashboard/universal-search-also-matches"; -import { AnswerSuggestionChips } from "@/components/clinical-dashboard/answer-suggestion-chips"; import { ResultFilterSheet, ResultFilterTrigger, @@ -55,16 +54,6 @@ import { useRegistryRecords } from "@/lib/use-registry-records"; type ServiceResultScope = "results" | "all"; -const bestFitQuery = "13YARN crisis Aboriginal Torres Strait Islander phone"; -const serviceQuickFilters = [ - { label: "Best fit", query: bestFitQuery }, - { label: "Crisis", query: "crisis" }, - { label: "Culturally safe", query: "Aboriginal Torres Strait Islander" }, - { label: "Phone referral", query: "phone referral" }, - { label: "Free", query: "free" }, - { label: "WA", query: "WA" }, -] as const; - function displayText(value: string | null | undefined, fallback = "Confirm locally") { return value?.trim() ? value.trim() : fallback; } @@ -661,24 +650,6 @@ export function ServicesNavigatorPage() { ) : undefined } /> - - {/* Evicted from the filter sheet: every quick filter called - `router.push` and replaced the query outright, discarding the - search and its results with no warning and no undo — exactly - what docs/filter-contract.md section 1 forbids inside a control - labelled "Filter". Framed as a new search, which is what - picking one does. */} - filter.label)} - onPick={(label) => { - const filter = serviceQuickFilters.find((item) => item.label === label); - if (filter) applyServiceQuery(filter.query); - }} - /> } > diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index ee790159a6..8e1cbf323e 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1132,8 +1132,7 @@ test.describe("Clinical KB tools launcher", () => { const quickFilter = page.getByTestId("service-filter-trigger-phone"); await expect(quickFilter).toBeVisible(); await expect(quickFilter).toHaveAccessibleName(/No filters active/); - await page.getByTestId("service-quick-search-suggestions").getByRole("button", { name: "Crisis" }).click(); - await expect(page).toHaveURL(/\/services\?.*q=crisis/); + await expect(page.getByText("Try a focused search", { exact: true })).toHaveCount(0); // Phones keep the full search results in the page instead of opening a // command sheet over the small viewport. @@ -1330,6 +1329,7 @@ test.describe("Clinical KB tools launcher", () => { await gotoLauncher(page, "/services?focus=1"); await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeVisible(); await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeFocused(); + await expect(page.getByText("Try a focused search", { exact: true })).toHaveCount(0); await gotoLauncher(page, "/forms?focus=1"); await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toBeVisible(); @@ -1370,16 +1370,9 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByRole("navigation", { name: "Service groups" })).toBeVisible(); await expect(page.getByTestId("services-shortlist-bar")).toHaveCount(0); - const culturallySafe = page - .getByTestId("service-quick-search-suggestions") - .getByRole("button", { name: "Culturally safe" }); - await expect(culturallySafe).toBeVisible(); - await waitForReactEventHandler(culturallySafe); - await culturallySafe.click(); - await expect(page).toHaveURL(/q=Aboriginal\+Torres\+Strait\+Islander/); + await expect(page.getByText("Try a focused search", { exact: true })).toHaveCount(0); await expect(page.getByTestId("service-search-result-13yarn")).toBeVisible(); - // Quick search suggestions and facet clearing are separate contracts. // Exercise a real facet, then clear only that facet while preserving q. await page.getByTestId("service-filter-trigger-desktop").click(); const filterPanel = page.getByTestId("service-filter-panel"); @@ -1389,7 +1382,7 @@ test.describe("Clinical KB tools launcher", () => { await crisisFacet.click(); await expect(page).toHaveURL(/acuity_flags=crisis_high/); await filterPanel.getByTestId("service-filter-panel-clear").click(); - await expect(page).toHaveURL(/q=Aboriginal\+Torres\+Strait\+Islander/); + await expect(page).toHaveURL(/q=13YARN/); await expect(page.getByTestId("service-search-result-13yarn")).toBeVisible(); await filterPanel.getByRole("button", { name: "Close", exact: true }).click();