diff --git a/docs/site-map.md b/docs/site-map.md index 77f2c93e8f..9bd77fe332 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -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`. diff --git a/src/app/mockups/development/page.tsx b/src/app/mockups/development/page.tsx new file mode 100644 index 0000000000..29f4abc737 --- /dev/null +++ b/src/app/mockups/development/page.tsx @@ -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 ( +
+
+

+

+

In-progress surfaces

+

+ 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. +

+
+ +

+

+ + +
+ ); +} diff --git a/src/components/clinical-dashboard/settings-dialog.tsx b/src/components/clinical-dashboard/settings-dialog.tsx index bc7d5e097b..475f0cc183 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 ? ( + +
+

Development page

+

+ Index of the surfaces being built, including the Caring Contact prototype. Synthetic data only โ€” no + patient record, message or schedule on them 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..6e350d6773 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 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" });