Skip to content
Closed
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
1 change: 1 addition & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/development` - Route discovered from app directory Source: `src/app/mockups/development/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`.
Expand Down
97 changes: 97 additions & 0 deletions src/app/mockups/development/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { Metadata } from "next";
import Link from "next/link";
import { ArrowRight, FlaskConical, ShieldAlert } from "lucide-react";

import { CARING_CONTACT_MOCKUP_ROUTES } from "@/components/caring-contacts/mockups/routes";

export const metadata: Metadata = {
title: "Development · Clinical KB",
description: "In-progress surfaces reachable only in development builds.",
};

// The index of what is being built. It lives under /mockups so it inherits the
// namespace's production gate (`mockupsEnabled()` in the mockups layout) rather
// than needing a second one, and so it 404s in a production deploy exactly like
// the surfaces it links to.
const DEVELOPMENT_SURFACES = [
{
id: "caring-contacts",
name: "Caring Contact",
summary:
"Linked prototype of the one-way caring-contacts coordination workflow: 13 routes, the activation flow, and the offline, permission, session-expiry, conflict and recovery states.",
href: CARING_CONTACT_MOCKUP_ROUTES.today,
status: "Synthetic prototype",
entries: [
{ label: "Today", href: CARING_CONTACT_MOCKUP_ROUTES.today },
{ label: "Patients", href: CARING_CONTACT_MOCKUP_ROUTES.patients },
{ label: "Schedule", href: CARING_CONTACT_MOCKUP_ROUTES.schedule },
{ label: "Templates", href: CARING_CONTACT_MOCKUP_ROUTES.templates },
{ label: "System states", href: CARING_CONTACT_MOCKUP_ROUTES.systemStates },
],
},
] as const;

export default function DevelopmentIndexPage() {
return (
<main className="mx-auto grid w-full max-w-[64rem] gap-6 px-4 py-8 sm:px-6" data-testid="development-index">
<header className="grid gap-2">
<p className="inline-flex items-center gap-2 text-xs font-extrabold uppercase tracking-wide text-[color:var(--clinical-accent)]">
<FlaskConical aria-hidden="true" className="size-icon-sm" />
Development
</p>
<h1 className="text-2xl font-extrabold text-[color:var(--text-heading)] sm:text-3xl">In-progress surfaces</h1>
<p className="max-w-3xl text-sm leading-6 text-[color:var(--text-muted)]">
Work being built, reachable from Settings while it is in development. These routes are not part of the
clinical product and 404 in a production deploy.
</p>
</header>

<p className="flex items-start gap-2 rounded-xl border border-[color:var(--warning)]/30 bg-[color:var(--warning-soft)] px-4 py-3 text-sm leading-6 text-[color:var(--text)]">
<ShieldAlert aria-hidden="true" className="mt-0.5 size-icon-sm shrink-0 text-[color:var(--warning)]" />
<span>
<strong className="font-extrabold text-[color:var(--text-heading)]">Synthetic data only.</strong> No patient,
message, schedule or team record on these surfaces is real, and nothing here is validated clinical decision
support.
</span>
</p>

<ul className="grid gap-4">
{DEVELOPMENT_SURFACES.map((surface) => (
<li
key={surface.id}
className="grid gap-3 rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] p-5 shadow-[var(--shadow-inset)]"
>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1">
<h2 className="text-lg font-extrabold text-[color:var(--text-heading)]">{surface.name}</h2>
<span className="rounded-full border border-[color:var(--border)] px-2 py-0.5 text-xs font-bold text-[color:var(--text-muted)]">
{surface.status}
</span>
</div>
<p className="max-w-3xl text-sm leading-6 text-[color:var(--text-muted)]">{surface.summary}</p>

<Link
href={surface.href}
className="inline-flex min-h-tap w-full items-center justify-center gap-2 rounded-lg bg-[color:var(--command)] px-4 text-sm font-extrabold text-[color:var(--command-contrast)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:w-auto"
data-testid={`development-open-${surface.id}`}
>
Open {surface.name} prototype
<ArrowRight aria-hidden="true" className="size-icon-sm" />
</Link>

<div className="flex flex-wrap gap-2 border-t border-[color:var(--border)] pt-3">
{surface.entries.map((entry) => (
<Link
key={entry.href}
href={entry.href}
className="inline-flex min-h-tap items-center rounded-lg border border-[color:var(--border)] px-3 text-xs font-bold text-[color:var(--text)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]"
>
{entry.label}
</Link>
))}
</div>
</li>
))}
</ul>
</main>
);
}
45 changes: 43 additions & 2 deletions src/components/clinical-dashboard/settings-dialog.tsx
Original file line number Diff line number Diff 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 Down Expand 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 Down Expand 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 Down Expand 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 Down Expand 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(--shadow-soft),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)]">Development page</p>
<p className="mt-1 text-sm font-medium leading-5 text-[color:var(--text-muted)]">
Index of the surfaces being built, including the Caring Contact prototype. Synthetic data only — no
patient record, message or schedule on them is real.
</p>
<Link
href="/mockups/development"
onClick={onClose}
className={cn(floatingControl, "mt-3 min-h-10 w-full gap-2 text-sm")}
data-testid="settings-row-development-page"
>
<FlaskConical aria-hidden="true" className="h-4 w-4" />
Open Development page
<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 number Diff line number Diff 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 Development page 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-development-page");
expect(prototypeLink).toHaveAttribute("href", "/mockups/development");
expect(prototypeLink).toHaveTextContent("Open Development page");
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