diff --git a/docs/branch-review-records/5b03318c07763efecca63e3f476dd4dfa1757c1297c491599e715bfbe3739a04.record.md b/docs/branch-review-records/5b03318c07763efecca63e3f476dd4dfa1757c1297c491599e715bfbe3739a04.record.md new file mode 100644 index 0000000000..b2a5233af3 --- /dev/null +++ b/docs/branch-review-records/5b03318c07763efecca63e3f476dd4dfa1757c1297c491599e715bfbe3739a04.record.md @@ -0,0 +1 @@ +| 2026-08-18 | claude/settings-development-section (PR #2109) | 5bda0941ea2b257af23f569a25e7659bcebcdf40 | Run PR sweep: CI fix + threads + drift | Before: Static PR checks failing (design-system-contract ratchet — legacyShadowAliases at settings-dialog.tsx increased 3 -> 4 via var(--shadow-soft) in the new Development section), 0 unresolved review threads, branch 1 commit behind main (clean merge-tree). After: synced origin/main into the branch via update_pull_request_branch (no conflicts, base now 5ae2bb6e), fixed the shadow-alias regression by switching to shadow-[var(--e2),var(--shadow-inset)] (same pattern as account-setup-dialog.tsx), pushed b1f9dac6..5bda0941. No review threads existed to action. | npm run check:design-system-contract -> 'Design-system contract passed'; node scripts/run-vitest.mjs run tests/settings-dialog-actions.dom.test.tsx tests/client-secret-surface.test.ts -> 'Test Files 2 passed (2) / Tests 13 passed (13)'; npx tsc -p tsconfig.typecheck.json --noEmit -> exit 0 no diagnostics; npx eslint settings-dialog.tsx -> exit 0; npx prettier --check settings-dialog.tsx -> 'All matched files use Prettier code style!'; no provider-backed checks run | diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index bc7d5e097b..f26f0f46b7 100644 --- a/src/components/clinical-dashboard/settings-dialog.tsx +++ b/src/components/clinical-dashboard/settings-dialog.tsx @@ -1,5 +1,6 @@ "use client"; +import Link from "next/link"; import { type FormEvent, type ReactNode, type UIEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ArrowLeft, @@ -8,6 +9,7 @@ import { Check, ChevronRight, CircleHelp, + FlaskConical, CircleUserRound, Globe2, Keyboard, @@ -63,7 +65,8 @@ type SettingsSectionId = | "notifications" | "privacy" | "keyboard" - | "help"; + | "help" + | "development"; const SETTINGS_SECTIONS: ReadonlyArray<{ id: SettingsSectionId; navLabel: string; icon: LucideIcon }> = [ { id: "account", navLabel: "Account", icon: CircleUserRound }, @@ -74,6 +77,7 @@ const SETTINGS_SECTIONS: ReadonlyArray<{ id: SettingsSectionId; navLabel: string { id: "privacy", navLabel: "Privacy", icon: ShieldCheck }, { id: "keyboard", navLabel: "Shortcuts", icon: Keyboard }, { id: "help", navLabel: "Help & About", icon: CircleHelp }, + { id: "development", navLabel: "Development", icon: FlaskConical }, ]; const APPEARANCE_OPTIONS: ReadonlyArray<{ value: ThemePreference; label: string; icon: LucideIcon }> = [ @@ -140,6 +144,9 @@ export function SettingsDialog({ }) { const closeButtonRef = useRef(null); const guideButtonRef = useRef(null); + const visibleSettingsSections = SETTINGS_SECTIONS.filter( + (item) => item.id !== "development" || caringContactPrototypeVisible, + ); const scrollRef = useRef(null); // The title bar is sticky inside the scroll region on every breakpoint, so its // height is the amount of the scroll port a section would otherwise land @@ -533,7 +540,7 @@ export function SettingsDialog({
+ + {caringContactPrototypeVisible ? ( + +
+

Caring Contact

+

+ Linked prototype of the caring-contacts workflow. Synthetic data only — no patient record, message + or schedule here is real. +

+ +
+
+ ) : null} @@ -1035,6 +1068,14 @@ export function SettingsDialog({ ); } +// Temporary developer affordance: a way into the Caring Contact prototype while +// it is being built. Mirrors `mockupsEnabled()` in src/lib/env.ts without +// importing that server-only module: `NODE_ENV` and `NEXT_PUBLIC_*` are both +// inlined at build time, so this evaluates to `false` in a production deploy +// that has not opted in — exactly when `/mockups/*` would 404 anyway. +const caringContactPrototypeVisible = + process.env.NODE_ENV !== "production" || process.env.NEXT_PUBLIC_MOCKUPS_ENABLED === "true"; + function SettingsSection({ id, title, diff --git a/tests/settings-dialog-actions.dom.test.tsx b/tests/settings-dialog-actions.dom.test.tsx index 00d76fc531..acb2cd509c 100644 --- a/tests/settings-dialog-actions.dom.test.tsx +++ b/tests/settings-dialog-actions.dom.test.tsx @@ -97,6 +97,33 @@ afterEach(async () => { }); describe("SettingsDialog — destructive and account actions", () => { + // A Development section carries the in-progress surfaces while they are being + // built. It is gated to development builds, matching `mockupsEnabled()`, and the + // rail must not advertise a section the body does not render. The route is a + // mockup, so the entry navigates through and closes the sheet behind it. + it("opens the Caring Contact prototype from a gated Development section", () => { + renderDialog(); + const development = document.querySelector('[data-settings-section="development"]'); + expect(development).not.toBeNull(); + expect(development).toHaveTextContent("Development"); + expect(development).toHaveTextContent("Synthetic data only"); + + const prototypeLink = screen.getByTestId("settings-row-caring-contact-prototype"); + expect(prototypeLink).toHaveAttribute("href", "/mockups/caring-contacts"); + expect(prototypeLink).toHaveTextContent("Open Caring Contact prototype"); + expect(prototypeLink).toHaveTextContent("Temporary"); + expect(development?.contains(prototypeLink)).toBe(true); + + // The desktop rail lists exactly the sections that render. + const railLabels = [...document.querySelectorAll("[data-settings-nav-target]")].map((el) => + el.getAttribute("data-settings-nav-target"), + ); + const renderedIds = [...document.querySelectorAll("[data-settings-section]")].map((el) => + el.getAttribute("data-settings-section"), + ); + if (railLabels.length) expect(railLabels).toEqual(renderedIds); + }); + it("clears recent searches through the privacy action", () => { renderDialog(); const button = screen.getByRole("button", { name: "Clear recent searches" });