Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line numberDiff line numberDiff line change
@@ -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 |
45 changes: 43 additions & 2 deletions src/components/clinical-dashboard/settings-dialog.tsx
Original file line numberDiff line numberDiff line change
@@ -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,
Expand All@@ -8,6 +9,7 @@ import {
Check,
ChevronRight,
CircleHelp,
FlaskConical,
CircleUserRound,
Globe2,
Keyboard,
Expand DownExpand Up@@ -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 },
Expand All@@ -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 }> = [
Expand DownExpand Up@@ -140,6 +144,9 @@ export function SettingsDialog({
}) {
const closeButtonRef = useRef<HTMLButtonElement | null>(null);
const guideButtonRef = useRef<HTMLButtonElement | null>(null);
const visibleSettingsSections = SETTINGS_SECTIONS.filter(
(item) => item.id !== "development" || caringContactPrototypeVisible,
);
const scrollRef = useRef<HTMLDivElement | null>(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
Expand DownExpand Up@@ -533,7 +540,7 @@ export function SettingsDialog({
<div className="relative grid h-full max-h-full min-h-0 overflow-hidden lg:h-[min(88dvh,840px)] lg:grid-cols-[248px_minmax(0,1fr)]">
<aside className="hidden border-r border-[color:var(--border-lux)] bg-[color:var(--surface)]/72 px-4 pb-5 pt-6 lg:flex lg:flex-col">
<nav aria-label="Settings sections" className="grid gap-1">
{SETTINGS_SECTIONS.map((item) => {
{visibleSettingsSections.map((item) => {
const Icon = item.icon;
const active = item.id === activeSection;
return (
Expand DownExpand Up@@ -1028,13 +1035,47 @@ export function SettingsDialog({
</button>
</div>
</SettingsSection>

{caringContactPrototypeVisible ? (
<SettingsSection
id="development"
title="Development"
note="In-progress surfaces, reachable only in development builds. Not clinical content."
>
<div className="rounded-xl border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] p-4 shadow-[var(--e2),var(--shadow-inset)] lg:rounded-xl lg:bg-[color:var(--surface)] lg:shadow-[var(--shadow-inset)]">
<p className="text-sm font-semibold leading-5 text-[color:var(--text-heading)]">Caring Contact</p>
<p className="mt-1 text-sm font-medium leading-5 text-[color:var(--text-muted)]">
Linked prototype of the caring-contacts workflow. Synthetic data only — no patient record, message
or schedule here is real.
</p>
<Link
href="/mockups/caring-contacts"
onClick={onClose}
className={cn(floatingControl, "mt-3 min-h-10 w-full gap-2 text-sm")}
data-testid="settings-row-caring-contact-prototype"
>
<FlaskConical aria-hidden="true" className="h-4 w-4" />
Open Caring Contact prototype
<span className="ml-auto text-xs font-semibold text-[color:var(--text-muted)]">Temporary</span>
</Link>
</div>
</SettingsSection>
) : null}
</div>
</div>
</div>
</Sheet>
);
}

// 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,
Expand Down
27 changes: 27 additions & 0 deletions tests/settings-dialog-actions.dom.test.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 <Link> 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" });
Expand Down
Loading