-
-
-
-
-
- {copy.heading}
-
-
{copy.description}
-
+
+
{desktopComposerSlotId ? (
-
+
) : null}
@@ -1218,19 +935,7 @@ export function ApplicationsLauncherWorkspace({
}
return (
-
-
-
-
-
- {workspace}
-
-
+
{workspace}
);
}
diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx
index 1c9d388497..4de23418d2 100644
--- a/src/components/clinical-dashboard/ClinicalSidebar.tsx
+++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx
@@ -4,6 +4,7 @@ import { useState } from "react";
import Link from "next/link";
import {
BookOpen,
+ BrainCircuit,
ChevronDown,
ClipboardList,
FileText,
@@ -16,11 +17,11 @@ import {
Pill,
Search,
Settings as SettingsIcon,
- ShieldAlert,
Sparkles,
Sun,
Wrench,
} from "lucide-react";
+import { BrandMark } from "@/components/clinical-dashboard/brand";
import { cn, sidebarItem, sidebarToolTile, statusDotReady, textMuted } from "@/components/ui-primitives";
import { Sheet } from "@/components/ui/sheet";
import { type AppModeId } from "@/lib/app-modes";
@@ -52,6 +53,7 @@ const sidebarToolItems = [
{ id: "services", label: "Services", icon: ClipboardList, href: "/services" },
{ id: "forms", label: "Forms", icon: FileText, href: "/forms" },
{ id: "favourites", label: "Faves", icon: Heart, href: "/favourites" },
+ { id: "differentials", label: "Diffs", icon: BrainCircuit, href: "/differentials" },
{ id: "prescribing", label: "Meds", icon: Pill, href: "/?mode=prescribing" },
{ id: "tools", label: "Tools", icon: Wrench, href: "/?mode=tools" },
] as const;
@@ -60,8 +62,6 @@ const collapsedSidebarButton =
"grid h-11 w-11 shrink-0 place-items-center rounded-xl border border-transparent text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
const collapsedSidebarActiveButton =
"border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]";
-const collapsedSidebarPrimaryButton =
- "border-transparent bg-[color:var(--command)] text-[color:var(--command-contrast)] shadow-[var(--shadow-tight)] hover:bg-[color:var(--command-hover)] hover:text-[color:var(--command-contrast)]";
export function ClinicalSidebarContent({
recentQueries,
@@ -106,18 +106,15 @@ export function ClinicalSidebarContent({
{showHeader ? (
-
-
-
-
-
Clinical Guide
-
Source-backed workspace
-
+
+
+ Clinical Guide
+
-
+
+
+
+
}
>
+
+
+
+
+
+ );
+}
diff --git a/src/components/clinical-dashboard/document-search-results.tsx b/src/components/clinical-dashboard/document-search-results.tsx
index 2ff525b17e..fb7c6eb68c 100644
--- a/src/components/clinical-dashboard/document-search-results.tsx
+++ b/src/components/clinical-dashboard/document-search-results.tsx
@@ -770,7 +770,7 @@ export function DocumentSearchResultsPanel({
return `${documentCount} document${documentCount === 1 ? "" : "s"}`;
})();
return (
-
+
{recordMatchCount > 0 ||
matches.length > 0 ||
(trimmedQuery && !shouldShowHome) ||
diff --git a/src/components/clinical-dashboard/favourites-home-page.tsx b/src/components/clinical-dashboard/favourites-home-page.tsx
index 555feea45c..1d23df3483 100644
--- a/src/components/clinical-dashboard/favourites-home-page.tsx
+++ b/src/components/clinical-dashboard/favourites-home-page.tsx
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
import { FavouritesHub } from "@/components/clinical-dashboard/favourites-hub";
import { appModeHomeHref } from "@/lib/app-modes";
+import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
type FavouritesHomePageProps = {
query?: string;
@@ -24,6 +25,8 @@ export function FavouritesHomePage({ query = "" }: FavouritesHomePageProps) {
router.push(appModeHomeHref("favourites", { focus: true }));
}}
onAddFavourite={() => undefined}
+ desktopComposerSlotId={modeHomeDesktopComposerSlotId}
+ headingLevel={1}
/>
);
diff --git a/src/components/clinical-dashboard/favourites-hub.tsx b/src/components/clinical-dashboard/favourites-hub.tsx
index 9b0f09766b..aacfdd19b0 100644
--- a/src/components/clinical-dashboard/favourites-hub.tsx
+++ b/src/components/clinical-dashboard/favourites-hub.tsx
@@ -3,6 +3,7 @@
import { ArrowUpDown, ChevronDown, Filter, Folder, FolderInput, Heart, Plus, Search, X } from "lucide-react";
import { useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from "react";
import { useDismissableLayer } from "@/components/use-dismissable-layer";
+import { ModeHomeHero } from "@/components/mode-home-template";
import { cn, floatingControl, iconTilePremium, panelSubtle, primaryControl } from "@/components/ui-primitives";
import {
favouriteItems,
@@ -28,10 +29,14 @@ export function FavouritesHub({
query,
onClearQuery,
onAddFavourite,
+ desktopComposerSlotId,
+ headingLevel = 2,
}: {
query: string;
onClearQuery: () => void;
onAddFavourite: () => void;
+ desktopComposerSlotId?: string;
+ headingLevel?: 1 | 2;
}) {
const [selectedTab, setSelectedTab] = useState
("all");
const [tabMenuOpen, setTabMenuOpen] = useState(false);
@@ -113,22 +118,20 @@ export function FavouritesHub({
return (
-
-
-
-
-
-
-
- Favourites
-
-
- Keep trusted notes, sources, medication pages, and clinical sets ready for reuse.
-
-
-
+
+
+
+ {desktopComposerSlotId ? (
+
+ ) : null}
-
+
{[
{ label: "Items", value: itemCount, icon: Heart },
{ label: "Sets", value: setCount, icon: Folder },
diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx
index 2f1dcfa723..a05e1387b5 100644
--- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx
+++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx
@@ -1,34 +1,25 @@
"use client";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
-import {
- BrainCircuit,
- ClipboardList,
- FileText,
- Heart,
- Pill,
- Search,
- ShieldCheck,
- Sparkles,
- Wrench,
-} from "lucide-react";
import { Suspense, type CSSProperties, type ReactNode, useEffect, useMemo, useRef, useState } from "react";
import { ClinicalDashboard } from "@/components/clinical-dashboard";
+import { recentQueryStorageKey, SettingsDialog } from "@/components/ClinicalDashboard";
+import {
+ ClinicalDesktopSidebar,
+ ClinicalMobileSidebar,
+ deriveSidebarIdentity,
+} from "@/components/clinical-dashboard/ClinicalSidebar";
+import { GuideDialog } from "@/components/clinical-dashboard/dashboard-shell";
import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header";
+import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed";
+import { useTheme } from "@/components/clinical-dashboard/use-theme";
import { FormsSearchResultsPage } from "@/components/forms/forms-search-results-page";
-import { Sheet } from "@/components/ui/sheet";
-import { cn, sidebarItem } from "@/components/ui-primitives";
-import {
- appModeDefinition,
- appModeHomeHref,
- isAppModeId,
- isAppModeVisible,
- visibleAppModeDefinitions,
- type AppModeId,
-} from "@/lib/app-modes";
+import { cn } from "@/components/ui-primitives";
+import { appModeHomeHref, isAppModeId, isAppModeVisible, visibleAppModeDefinitions, type AppModeId } from "@/lib/app-modes";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import type { SearchScopeFilters } from "@/lib/search-scope";
+import { useAuthSession } from "@/lib/supabase/client";
import type { ClinicalQueryMode } from "@/lib/types";
const mockupQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }> = [
@@ -41,22 +32,13 @@ const mockupQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }>
{ value: "compare_guidance", label: "Compare" },
];
-const appModeIcons: Record
= {
- answer: Sparkles,
- documents: FileText,
- services: ShieldCheck,
- forms: ClipboardList,
- favourites: Heart,
- differentials: BrainCircuit,
- prescribing: Pill,
- tools: Wrench,
-};
-
type GlobalMockupSearchShellProps = {
children: ReactNode;
initialMode?: AppModeId;
availableModeIds?: readonly AppModeId[];
desktopSearchPlacement?: "default" | "hero";
+ /** Hide the shared search composer on routes that provide their own search surface. */
+ searchComposerVisible?: boolean;
};
export function GlobalMockupSearchShell(props: GlobalMockupSearchShellProps) {
@@ -80,6 +62,7 @@ function GlobalMockupSearchShellClient({
initialMode = "answer",
availableModeIds,
desktopSearchPlacement = "default",
+ searchComposerVisible = true,
}: GlobalMockupSearchShellProps) {
const router = useRouter();
const pathname = usePathname();
@@ -103,6 +86,13 @@ function GlobalMockupSearchShellClient({
const [queryMode, setQueryMode] = useState("auto");
const [scopeFilters, setScopeFilters] = useState({});
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
+ const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed();
+ const [guideOpen, setGuideOpen] = useState(false);
+ const [settingsOpen, setSettingsOpen] = useState(false);
+ const [recentQueries, setRecentQueries] = useState([]);
+ const { theme, toggleTheme } = useTheme();
+ const auth = useAuthSession();
+ const sidebarIdentity = useMemo(() => deriveSidebarIdentity(auth.session?.user.email), [auth.session?.user.email]);
const dashboardSearchMode =
isAppModeId(requestedMode) &&
isAppModeVisible(requestedMode) &&
@@ -139,6 +129,44 @@ function GlobalMockupSearchShellClient({
return () => window.cancelAnimationFrame(frame);
}, [availableModeIds, initialSearchMode, pathname, searchParamString]);
+ useEffect(() => {
+ let cancelled = false;
+ const frame = window.requestAnimationFrame(() => {
+ try {
+ const stored = JSON.parse(window.localStorage.getItem(recentQueryStorageKey) ?? "[]");
+ if (Array.isArray(stored) && !cancelled) {
+ setRecentQueries(
+ stored.filter((item): item is string => typeof item === "string" && Boolean(item.trim())).slice(0, 5),
+ );
+ }
+ } catch {
+ if (!cancelled) setRecentQueries([]);
+ }
+ });
+ return () => {
+ cancelled = true;
+ window.cancelAnimationFrame(frame);
+ };
+ }, []);
+
+ function prefetchApplications() {
+ router.prefetch("/?mode=tools");
+ router.prefetch("/favourites");
+ router.prefetch("/differentials");
+ }
+
+ function openGuide() {
+ setSettingsOpen(false);
+ setMobileMenuOpen(false);
+ setGuideOpen(true);
+ }
+
+ function openSettings() {
+ setGuideOpen(false);
+ setMobileMenuOpen(false);
+ setSettingsOpen(true);
+ }
+
function navigateToMode(mode: AppModeId, options: { query?: string; run?: boolean; focus?: boolean } = {}) {
router.push(appModeHomeHref(mode, options));
}
@@ -166,6 +194,17 @@ function GlobalMockupSearchShellClient({
navigateToMode(fallbackMode, { focus: true });
}
+ function startNewAnswerChat() {
+ setQuery("");
+ setMobileMenuOpen(false);
+ navigateToMode("answer", { focus: true });
+ }
+
+ function pickRecentQuery(recentQuery: string) {
+ setMobileMenuOpen(false);
+ navigateToMode("answer", { query: recentQuery, focus: true });
+ }
+
if (shouldRenderDashboardSearch && dashboardSearchMode === "forms" && isFormsOnlyShell) {
return ;
}
@@ -183,107 +222,120 @@ function GlobalMockupSearchShellClient({
return (
-
setQuery("")}
- onClearScope={() => undefined}
- onQueryModeChange={setQueryMode}
- onScopeFiltersChange={setScopeFilters}
- onToggleScope={() => undefined}
- onOpenUpload={() => router.push(`${appModeHomeHref("documents", { focus: true })}#sources`)}
- onOpenEvidence={() => navigateToMode("answer", { focus: true })}
- onNewChat={startNewChat}
- onOpenMobileSidebar={() => setMobileMenuOpen(true)}
- mobileLeadingAction={
- pathname === "/differentials" && searchMode === "differentials" && requestedQuery ? "back" : "menu"
- }
- onMobileBack={() => {
- setQuery("");
- navigateToMode(searchMode, { focus: true });
- }}
- queryModeOptions={mockupQueryModeOptions}
- queryInputRef={inputRef}
- headerVariant={isDifferentialPresentationWorkflow ? "workflow" : "default"}
- modeAlignment={isDifferentialPresentationWorkflow ? "default" : "center"}
- mobileSearchPlacement="bottom"
- desktopSearchPlacement={
- (desktopSearchPlacement === "hero" || isFormsOnlyShell) && isStandaloneModeHome ? "hero" : "default"
- }
- searchComposerVisible={!isDifferentialPresentationWorkflow}
- workflowCopyText={
- isDifferentialPresentationWorkflow
- ? "Acute confusion / encephalopathy differential comparison. Stabilise ABCs, check BGL, sats, attention test, collateral, and review medications/substances before handoff."
- : undefined
- }
- desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
- />
+
-
- {children}
+
+
setQuery("")}
+ onClearScope={() => undefined}
+ onQueryModeChange={setQueryMode}
+ onScopeFiltersChange={setScopeFilters}
+ onToggleScope={() => undefined}
+ onOpenUpload={() => router.push(`${appModeHomeHref("documents", { focus: true })}#sources`)}
+ onOpenEvidence={() => navigateToMode("answer", { focus: true })}
+ onNewChat={startNewChat}
+ onOpenMobileSidebar={() => setMobileMenuOpen(true)}
+ mobileLeadingAction={
+ pathname === "/differentials" && searchMode === "differentials" && requestedQuery ? "back" : "menu"
+ }
+ onMobileBack={() => {
+ setQuery("");
+ navigateToMode(searchMode, { focus: true });
+ }}
+ queryModeOptions={mockupQueryModeOptions}
+ queryInputRef={inputRef}
+ headerVariant={isDifferentialPresentationWorkflow ? "workflow" : "default"}
+ mobileSearchPlacement="bottom"
+ desktopSearchPlacement={
+ (desktopSearchPlacement === "hero" || isFormsOnlyShell) && isStandaloneModeHome ? "hero" : "default"
+ }
+ searchComposerVisible={searchComposerVisible && !isDifferentialPresentationWorkflow}
+ workflowCopyText={
+ isDifferentialPresentationWorkflow
+ ? "Acute confusion / encephalopathy differential comparison. Stabilise ABCs, check BGL, sats, attention test, collateral, and review medications/substances before handoff."
+ : undefined
+ }
+ desktopHomeComposerSlotId={isStandaloneModeHome ? modeHomeDesktopComposerSlotId : undefined}
+ />
+
+
+ {children}
+
-
setGuideOpen(false)} />
+ setSettingsOpen(false)}
+ identity={sidebarIdentity}
+ theme={theme}
+ onToggleTheme={toggleTheme}
+ onSignOut={auth.signOut}
+ onOpenGuide={openGuide}
+ />
+ setMobileMenuOpen(false)}
- title="Clinical Guide"
- description="Choose the search workspace."
- closeLabel="Close Clinical Guide menu"
- placement="left"
- contentClassName="max-w-[min(20rem,calc(100vw-1rem))]"
- >
-
-
+ recentQueries={recentQueries}
+ identity={sidebarIdentity}
+ activeMode={searchMode}
+ onOpenChange={setMobileMenuOpen}
+ onNewChat={startNewAnswerChat}
+ onPickRecent={pickRecentQuery}
+ onOpenGuide={openGuide}
+ onOpenSettings={openSettings}
+ theme={theme}
+ onToggleTheme={toggleTheme}
+ onPrefetchApplications={prefetchApplications}
+ />
);
}
diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx
index fa3658afbd..257a185db6 100644
--- a/src/components/clinical-dashboard/master-search-header.tsx
+++ b/src/components/clinical-dashboard/master-search-header.tsx
@@ -28,16 +28,12 @@ import {
Loader2,
Menu,
MessageSquarePlus,
- Moon,
- MoreHorizontal,
Pill,
Plus,
Search,
Send,
- Settings,
ShieldCheck,
Sparkles,
- Sun,
AlertCircle,
ArrowLeft,
X,
@@ -56,7 +52,6 @@ import {
} from "@/components/clinical-dashboard/mode-action-popup";
import {
cn,
- chatComposerIconButton,
chatComposerInput,
chatComposerShell,
chatSendButton,
@@ -74,7 +69,6 @@ import {
visibleAppModeDefinitions,
type AppModeId,
} from "@/lib/app-modes";
-import { type ResolvedTheme } from "@/lib/theme";
import type { ClinicalDocument, ClinicalQueryMode } from "@/lib/types";
import { type SearchScopeFilters } from "@/lib/search-scope";
import { tagSearchText } from "@/lib/document-tags";
@@ -161,14 +155,10 @@ export function MasterSearchHeader({
onOpenSourcePdf,
onNewChat,
onOpenMobileSidebar,
- onOpenSettings,
- theme,
- onToggleTheme,
queryModeOptions,
queryInputRef,
queryInputAutoFocus = false,
headerVariant = "default",
- modeAlignment = "default",
mobileSearchPlacement = "default",
desktopSearchPlacement = "default",
searchComposerVisible = true,
@@ -202,14 +192,10 @@ export function MasterSearchHeader({
onOpenSourcePdf?: () => void;
onNewChat?: () => void;
onOpenMobileSidebar?: () => void;
- onOpenSettings?: () => void;
- theme?: ResolvedTheme;
- onToggleTheme?: () => void;
queryModeOptions: Array<{ value: ClinicalQueryMode; label: string }>;
queryInputRef?: Ref;
queryInputAutoFocus?: boolean;
headerVariant?: "default" | "workflow";
- modeAlignment?: "default" | "center";
mobileSearchPlacement?: "default" | "bottom";
desktopSearchPlacement?: "default" | "hero";
searchComposerVisible?: boolean;
@@ -239,21 +225,15 @@ export function MasterSearchHeader({
const [scopeSheetOpen, setScopeSheetOpen] = useState(false);
const [actionMenuOpen, setActionMenuOpen] = useState(false);
const [modeMenuOpen, setModeMenuOpen] = useState(false);
- const [utilityMenuOpen, setUtilityMenuOpen] = useState(false);
const [usesScopeSheet, setUsesScopeSheet] = useState(false);
const [usesPhoneSearchLayout, setUsesPhoneSearchLayout] = useState(false);
const [desktopHomeComposerTarget, setDesktopHomeComposerTarget] = useState(null);
const modeMenuRef = useRef(null);
const modeButtonRef = useRef(null);
const modeOptionRefs = useRef>([]);
- const utilityMenuRef = useRef(null);
- const utilityButtonRef = useRef(null);
const scopePopoverRef = useRef(null);
const scopeSummaryRef = useRef(null);
const scopeFilterInputRef = useRef(null);
- const hasUtilityActions = Boolean(onOpenSettings || (theme && onToggleTheme));
- const UtilityThemeIcon = theme === "dark" ? Sun : Moon;
- const utilityThemeLabel = theme === "dark" ? "Light mode" : "Dark mode";
const selectedDocuments = selectedDocumentIds
.map((id) => documents.find((document) => document.id === id))
.filter((document): document is ClinicalDocument => Boolean(document));
@@ -307,26 +287,6 @@ export function MasterSearchHeader({
const actionMenuButtonLabel = `Open ${selectedAppMode.label.toLowerCase()} options`;
const isStandaloneModeHomeHeader = Boolean(desktopHomeComposerSlotId);
const useMobileBackControl = mobileLeadingAction === "back";
- const homeHeaderTitle =
- searchMode === "services"
- ? "Services Navigator"
- : searchMode === "forms"
- ? "Forms Navigator"
- : searchMode === "favourites"
- ? "Favourites"
- : searchMode === "differentials"
- ? "Differentials Navigator"
- : "Clinical Guide";
- const homeHeaderDescription =
- searchMode === "services"
- ? "Psychiatry referral directory"
- : searchMode === "forms"
- ? "Mental Health Act forms"
- : searchMode === "favourites"
- ? "Saved clinical items and sets"
- : searchMode === "differentials"
- ? "Differential diagnosis workspace"
- : selectedAppMode.description;
function currentUsesScopeSheet() {
return window.matchMedia(mobileSheetMediaQuery).matches;
@@ -591,7 +551,6 @@ export function MasterSearchHeader({
}, [desktopHomeComposerSlotId]);
const dismissModeMenu = useCallback(() => setModeMenuOpen(false), []);
- const dismissUtilityMenu = useCallback(() => setUtilityMenuOpen(false), []);
function dismissScope(reason: "outside" | "escape") {
closeScope(reason === "escape");
}
@@ -603,13 +562,6 @@ export function MasterSearchHeader({
onDismiss: dismissModeMenu,
});
- useDismissableLayer({
- enabled: utilityMenuOpen,
- refs: [utilityMenuRef, utilityButtonRef],
- restoreFocusRef: utilityButtonRef,
- onDismiss: dismissUtilityMenu,
- });
-
useDismissableLayer({
enabled: scopeOpen,
refs: [scopePopoverRef, scopeSummaryRef],
@@ -620,7 +572,6 @@ export function MasterSearchHeader({
function submit(event: FormEvent) {
event.preventDefault();
setActionMenuOpen(false);
- setUtilityMenuOpen(false);
onAsk();
}
@@ -946,7 +897,7 @@ export function MasterSearchHeader({
{submitLabel}
- {usesAnswerFooterStyle ? (
+ {usesUniversalFooterStyle ? (
@@ -1045,46 +994,23 @@ export function MasterSearchHeader({
>
{useMobileBackControl ?
:
}
- {isStandaloneModeHomeHeader ? (
-
-
-
-
-
-
- {homeHeaderTitle}
-
-
- {homeHeaderDescription}
-
-
-
- ) : null}
{
setActionMenuOpen(false);
- setUtilityMenuOpen(false);
closeScope(false);
setModeMenuOpen((open) => !open);
}}
onKeyDown={handleModeTriggerKeyDown}
className={cn(
- "universal-header-mode-button inline-grid h-12 w-[min(13rem,calc(100vw-11.5rem))] min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:w-[17.5rem] sm:min-w-[15rem]",
+ "universal-header-mode-button inline-grid h-12 w-[min(13rem,calc(100vw-11.5rem))] min-w-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-2 rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-left shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:w-auto sm:min-w-[13rem] sm:pr-3",
isWorkflowHeader && "h-11 w-[min(11rem,calc(100vw-11rem))] sm:w-[12rem] sm:min-w-0 lg:w-[12.5rem]",
)}
aria-haspopup="menu"
@@ -1096,27 +1022,12 @@ export function MasterSearchHeader({
-
+
Mode
{selectedAppMode.label}
-
- {selectedAppMode.description}
-
>
- ) : isStandaloneModeHomeHeader ? (
- /* min-[1400px]: below that the equal-thirds grid leaves the right
- column too narrow for these chips and they slide under the
- centered mode pill. */
-
-
-
- Local only
-
-
-
- Saved
-
-
- AK
-
-
) : null}
{!isWorkflowHeader ? (
- {isStandaloneModeHomeHeader ? (
-
- ) : (
-
- )}
- {isStandaloneModeHomeHeader ? null : (
- New chat
- )}
+
+ New chat
) : null}
- {!isWorkflowHeader && hasUtilityActions ? (
- <>
- {
- setActionMenuOpen(false);
- setModeMenuOpen(false);
- closeScope(false);
- setUtilityMenuOpen((open) => !open);
- }}
- className="universal-header-icon-control hidden h-11 w-11 shrink-0 place-items-center rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:grid"
- aria-haspopup="menu"
- aria-expanded={utilityMenuOpen}
- aria-controls={utilityMenuOpen ? "header-utility-menu" : undefined}
- aria-label="Open display and account actions"
- title="More actions"
- >
-
-
- {utilityMenuOpen ? (
-
- ) : null}
- >
- ) : null}
diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
index fb2fca7280..94cb8c28d5 100644
--- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
+++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx
@@ -28,7 +28,6 @@ import { useState } from "react";
import { ModeHomeTemplate } from "@/components/mode-home-template";
import {
- appBackdrop,
cn,
toneDanger,
toneInfo,
@@ -1325,32 +1324,17 @@ function MedicationDetail() {
export function AcamprosateMedicationPage() {
return (
-
-
-
-
-
-
Medication search
-
Search
-
-
- Clinical KB
-
-
-
-
+
+
+
+
+
Medication search
+
Search
+
+
diff --git a/src/components/clinical-dashboard/use-sidebar-collapsed.ts b/src/components/clinical-dashboard/use-sidebar-collapsed.ts
new file mode 100644
index 0000000000..86d3d38948
--- /dev/null
+++ b/src/components/clinical-dashboard/use-sidebar-collapsed.ts
@@ -0,0 +1,45 @@
+"use client";
+
+import { useCallback, useSyncExternalStore } from "react";
+
+const storageKey = "clinical-kb-sidebar-collapsed";
+const changeEvent = "clinical-kb-sidebar-collapsed-change";
+
+function getSnapshot() {
+ try {
+ return window.localStorage.getItem(storageKey) === "1";
+ } catch {
+ return false;
+ }
+}
+
+function getServerSnapshot() {
+ return false;
+}
+
+function subscribe(onChange: () => void) {
+ window.addEventListener("storage", onChange);
+ window.addEventListener(changeEvent, onChange);
+ return () => {
+ window.removeEventListener("storage", onChange);
+ window.removeEventListener(changeEvent, onChange);
+ };
+}
+
+/**
+ * Desktop sidebar collapse state shared across shells and persisted per
+ * browser, mirroring the use-theme.ts external-store pattern so the choice
+ * survives route changes between the dashboard and standalone shells.
+ */
+export function useSidebarCollapsed() {
+ const collapsed = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
+ const setCollapsed = useCallback((next: boolean) => {
+ try {
+ window.localStorage.setItem(storageKey, next ? "1" : "0");
+ } catch {
+ // Ignore storage failures (private mode); state simply won't persist.
+ }
+ window.dispatchEvent(new Event(changeEvent));
+ }, []);
+ return [collapsed, setCollapsed] as const;
+}
diff --git a/src/components/compact-record-home-page.tsx b/src/components/compact-record-home-page.tsx
deleted file mode 100644
index 44038aa7bd..0000000000
--- a/src/components/compact-record-home-page.tsx
+++ /dev/null
@@ -1,167 +0,0 @@
-import Link from "next/link";
-import { ArrowRight, ShieldCheck, type LucideIcon } from "lucide-react";
-
-import { cn } from "@/components/ui-primitives";
-
-export type CompactHomeAction = {
- title: string;
- description: string;
- icon: LucideIcon;
- href: string;
-};
-
-export type CompactHomePillTone = "danger" | "info" | "primary" | "success" | "neutral" | "purple";
-
-export type CompactHomePill = {
- label: string;
- href: string;
- icon?: LucideIcon;
- tone?: CompactHomePillTone;
-};
-
-type CompactRecordHomePageProps = {
- testId: string;
- title: string;
- subtitle: string;
- icon: LucideIcon;
- tasksLabel: string;
- taskCards: CompactHomeAction[];
- quickLinksTitle: string;
- quickLinks: CompactHomePill[];
- verificationLabel: string;
- verificationBody: string;
- verifiedCount: number;
- totalCount: number;
- desktopComposerSlotId?: string;
-};
-
-const pillToneClass: Record = {
- danger: "bg-[color:var(--danger)]",
- info: "bg-[color:var(--info)]",
- neutral: "bg-[color:var(--text-soft)]",
- primary: "bg-[color:var(--clinical-accent)]",
- purple: "bg-violet-600",
- success: "bg-[color:var(--success)]",
- teal: "bg-[color:var(--clinical-accent)]",
-};
-
-export function CompactRecordHomePage({
- testId,
- title,
- subtitle,
- icon: Icon,
- tasksLabel,
- taskCards,
- quickLinksTitle,
- quickLinks,
- verificationLabel,
- verificationBody,
- verifiedCount,
- totalCount,
- desktopComposerSlotId,
-}: CompactRecordHomePageProps) {
- return (
-
-
-
-
-
-
-
-
- {title}
-
-
- {subtitle}
-
-
-
-
- {desktopComposerSlotId ? (
-
- ) : null}
-
-
- {taskCards.map((card, index) => {
- const CardIcon = card.icon;
- return (
- 0 && "border-t border-[color:var(--border)]",
- )}
- >
-
-
-
-
-
- {card.title}
-
-
- {card.description}
-
-
-
-
- );
- })}
-
-
-
- {quickLinksTitle}
-
- {quickLinks.map((link) => {
- const LinkIcon = link.icon;
- return (
-
- {LinkIcon ? (
-
-
-
- ) : (
-
- )}
- {link.label}
-
- );
- })}
-
-
-
-
-
-
- {verificationLabel}
-
- •
- {verificationBody}
-
- {verifiedCount} of {totalCount} records are locally verified.
-
-
-
-
- );
-}
diff --git a/src/components/differentials/differentials-home-page.tsx b/src/components/differentials/differentials-home-page.tsx
index bc4f18c031..2fa2791685 100644
--- a/src/components/differentials/differentials-home-page.tsx
+++ b/src/components/differentials/differentials-home-page.tsx
@@ -1,4 +1,5 @@
import { DifferentialsHome } from "@/components/clinical-dashboard/differentials-home";
+import { ModeHomeMain } from "@/components/mode-home-template";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
type DifferentialsHomePageProps = {
@@ -7,8 +8,8 @@ type DifferentialsHomePageProps = {
export function DifferentialsHomePage({ query = "" }: DifferentialsHomePageProps) {
return (
-
+
-
+
);
}
diff --git a/src/components/forms/forms-home-page.tsx b/src/components/forms/forms-home-page.tsx
index 753c85672c..6a3251cbeb 100644
--- a/src/components/forms/forms-home-page.tsx
+++ b/src/components/forms/forms-home-page.tsx
@@ -1,21 +1,17 @@
-import Link from "next/link";
-import {
- ArrowLeftRight,
- ArrowRight,
- ClipboardCheck,
- FileText,
- Route,
- Search,
- ShieldCheck,
- Truck,
- UserRound,
-} from "lucide-react";
+import { ArrowLeftRight, ClipboardCheck, FileText, Route, Search, ShieldCheck, Truck, UserRound } from "lucide-react";
+import {
+ ModeHomeMain,
+ ModeHomeTemplate,
+ ModeHomeVerificationFooter,
+ type ModeHomeAction,
+ type ModeHomePill,
+} from "@/components/mode-home-template";
import { appModeHomeHref } from "@/lib/app-modes";
import { defaultFormSlug, formRecords } from "@/lib/forms";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
-const taskCards = [
+const taskCards: ModeHomeAction[] = [
{
title: "Find a form",
description: "Search by number, pathway, clock, or keyword.",
@@ -40,7 +36,7 @@ const taskCards = [
},
];
-const commonTasks = [
+const commonTasks: ModeHomePill[] = [
{
label: "Transport",
icon: Truck,
@@ -68,102 +64,27 @@ function verifiedCount() {
}
export function FormsHomePage() {
- const locallyVerifiedCount = verifiedCount();
-
return (
-
-
-
-
-
-
-
-
-
- Search, check, or follow a pathway.
-
-
-
-
-
-
-
- {taskCards.map((card, index) => {
- const CardIcon = card.icon;
- return (
- 0 ? "border-t border-[color:var(--border)] md:border-t-0" : "",
- ].join(" ")}
- >
-
-
-
-
-
- {card.title}
-
-
- {card.description}
-
-
-
-
- );
- })}
-
-
-
- Common tasks
-
- {commonTasks.map((task) => {
- const TaskIcon = task.icon;
- return (
-
-
- {task.label}
-
- );
- })}
-
-
-
-
-
-
- Source verified
-
- •
- MHA 2014 forms
-
- {locallyVerifiedCount} of {formRecords.length} records are locally verified.
-
-
-
-
+
+
+ }
+ />
+
);
}
diff --git a/src/components/mode-home-template.tsx b/src/components/mode-home-template.tsx
index beb05de4ae..7816addaec 100644
--- a/src/components/mode-home-template.tsx
+++ b/src/components/mode-home-template.tsx
@@ -1,6 +1,6 @@
import Link from "next/link";
import { type ReactNode } from "react";
-import { ArrowRight, type LucideIcon } from "lucide-react";
+import { ArrowRight, ShieldCheck, type LucideIcon } from "lucide-react";
import { cn } from "@/components/ui-primitives";
@@ -19,7 +19,7 @@ export type ModeHomePill = {
href?: string;
onClick?: () => void;
icon?: LucideIcon;
- tone?: "teal" | "danger" | "info" | "success" | "neutral" | "purple";
+ tone?: "teal" | "primary" | "danger" | "info" | "success" | "neutral" | "purple";
};
type ModeHomeTemplateProps = {
@@ -42,16 +42,105 @@ const pillToneClass: Record, string> = {
danger: "bg-[color:var(--danger)]",
info: "bg-[color:var(--info)]",
neutral: "bg-[color:var(--text-soft)]",
+ primary: "bg-[color:var(--clinical-accent)]",
purple: "bg-violet-600",
success: "bg-[color:var(--success)]",
teal: "bg-[color:var(--clinical-accent)]",
};
-export function ModeHomeTemplate({
+export function ModeHomeHero({
testId,
title,
subtitle,
icon: Icon,
+ headingLevel = 1,
+}: {
+ testId?: string;
+ title: string;
+ subtitle: string;
+ icon: LucideIcon;
+ headingLevel?: 1 | 2;
+}) {
+ const Heading = headingLevel === 1 ? "h1" : "h2";
+
+ return (
+
+
+
+
+
+
+ {title}
+
+
+ {subtitle}
+
+
+
+ );
+}
+
+/**
+ * Standalone-route wrapper that mirrors the dashboard's vertically centred
+ * Answer home: full-height, centred content, bottom padding reserved for the
+ * fixed mobile composer.
+ */
+export function ModeHomeMain({
+ testId,
+ children,
+ className,
+}: {
+ testId?: string;
+ children: ReactNode;
+ className?: string;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+export function ModeHomeVerificationFooter({
+ label,
+ body,
+ verifiedCount,
+ totalCount,
+}: {
+ label: string;
+ body: string;
+ verifiedCount: number;
+ totalCount: number;
+}) {
+ return (
+
+
+
+ {label}
+
+ •
+ {body}
+
+ {verifiedCount} of {totalCount} records are locally verified.
+
+
+ );
+}
+
+export function ModeHomeTemplate({
+ testId,
+ title,
+ subtitle,
+ icon,
actions,
actionsLabel,
desktopComposerSlotId,
@@ -62,8 +151,6 @@ export function ModeHomeTemplate({
className,
headingLevel = 1,
}: ModeHomeTemplateProps) {
- const Heading = headingLevel === 1 ? "h1" : "h2";
-
return (
-
-
-
-
-
-
- {title}
-
-
- {subtitle}
-
-
-
+
{desktopComposerSlotId ? (
) : null}
-
- {actions.map((action, index) => {
- const ActionIcon = action.icon;
- const content = (
- <>
-
-
-
-
-
- {action.title}
+ {actions.length ? (
+
+ {actions.map((action, index) => {
+ const ActionIcon = action.icon;
+ const content = (
+ <>
+
+
-
- {action.description}
+
+
+ {action.title}
+
+
+ {action.description}
+
-
-
- >
- );
- const actionClassName = cn(
- "mode-home-action group grid min-h-[4.8rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[8rem] sm:grid-cols-[3.5rem_minmax(0,1fr)_1.5rem] sm:gap-4 sm:rounded-lg sm:border sm:border-[color:var(--border)] sm:px-5 sm:py-5 sm:shadow-[var(--shadow-card)] lg:min-h-[8.4rem] lg:px-6",
- index > 0 && "border-t border-[color:var(--border)] sm:border-t-[color:var(--border)]",
- );
-
- if (action.href) {
+
+ >
+ );
+ const actionClassName = cn(
+ "mode-home-action group grid min-h-[4.8rem] w-full grid-cols-[2.5rem_minmax(0,1fr)_1.25rem] items-center gap-3 bg-[color:var(--surface)] px-4 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:relative focus-visible:z-10 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)] disabled:cursor-wait disabled:opacity-60 sm:min-h-[8rem] sm:grid-cols-[3.5rem_minmax(0,1fr)_1.5rem] sm:gap-4 sm:rounded-lg sm:border sm:border-[color:var(--border)] sm:px-5 sm:py-5 sm:shadow-[var(--shadow-card)] lg:min-h-[8.4rem] lg:px-6",
+ index > 0 && "border-t border-[color:var(--border)] sm:border-t-[color:var(--border)]",
+ );
+
+ if (action.href) {
+ return (
+
+ {content}
+
+ );
+ }
+
return (
-
+
{content}
-
+
);
- }
-
- return (
-
- {content}
-
- );
- })}
-
+ })}
+
+ ) : null}
{pills?.length ? (
{pillsTitle || pillsAction ? (
-
+
{pillsTitle ? (
{pillsTitle}
) : (
@@ -158,7 +237,7 @@ export function ModeHomeTemplate({
{pillsAction}
) : null}
-
+
{pills.map((pill) => {
const PillIcon = pill.icon;
const content = (
@@ -174,7 +253,7 @@ export function ModeHomeTemplate({
>
);
const pillClassName =
- "inline-flex min-h-11 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm font-semibold text-[color:var(--text)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] sm:min-w-0 sm:px-3";
+ "inline-flex min-h-11 shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-full border border-[color:var(--border)] bg-[color:var(--surface)] px-4 text-sm font-semibold text-[color:var(--text)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)] hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";
return pill.href ? (
{content}
diff --git a/src/components/services/services-home-page.tsx b/src/components/services/services-home-page.tsx
index b43fb0054a..7965126840 100644
--- a/src/components/services/services-home-page.tsx
+++ b/src/components/services/services-home-page.tsx
@@ -1,15 +1,17 @@
import { FileSearch, MapPinned, Route, Users } from "lucide-react";
import {
- CompactRecordHomePage,
- type CompactHomeAction,
- type CompactHomePill,
-} from "@/components/compact-record-home-page";
+ ModeHomeMain,
+ ModeHomeTemplate,
+ ModeHomeVerificationFooter,
+ type ModeHomeAction,
+ type ModeHomePill,
+} from "@/components/mode-home-template";
import { appModeHomeHref } from "@/lib/app-modes";
import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer";
import { defaultServiceSlug, serviceRecords } from "@/lib/services";
-const taskCards: CompactHomeAction[] = [
+const taskCards: ModeHomeAction[] = [
{
title: "Find a service",
description: "Search by need, catchment, provider, or keyword.",
@@ -34,7 +36,7 @@ const taskCards: CompactHomeAction[] = [
},
];
-const commonPathways: CompactHomePill[] = [
+const commonPathways: ModeHomePill[] = [
{
label: "Crisis",
tone: "danger",
@@ -77,20 +79,26 @@ function verifiedCount() {
export function ServicesHomePage() {
return (
-
+
+
+ }
+ />
+
);
}
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts
index 9201e18403..46b726841a 100644
--- a/tests/ui-tools.spec.ts
+++ b/tests/ui-tools.spec.ts
@@ -64,7 +64,7 @@ test.describe("Clinical KB applications launcher", () => {
await expect(desktopLaunchLink).toBeVisible();
await expect(desktopLaunchLink).toHaveAttribute("href", "/?mode=answer");
}
- await expect(page.getByLabel("Current app mode: Applications")).toBeVisible();
+ await expect(page.getByLabel("Current app mode: Tools")).toBeVisible();
await expect(page.getByPlaceholder("Search applications...")).toBeVisible();
await expect(page.getByLabel("Open selected application")).toBeVisible();
await expectNoPageHorizontalOverflow(page);
@@ -339,14 +339,20 @@ test.describe("Clinical KB applications launcher", () => {
await expect(page.getByTestId("global-search-input")).toHaveCount(1);
const differentialsHomeSearch = page.getByTestId("differentials-home").getByTestId("global-search-input");
await expect(differentialsHomeSearch).toBeVisible();
- const differentialsSearchBox = await differentialsHomeSearch.boundingBox();
- const differentialsHeadingBox = await page.getByRole("heading", { level: 1, name: "Differentials" }).boundingBox();
- expect(differentialsSearchBox).not.toBeNull();
- expect(differentialsHeadingBox).not.toBeNull();
- expect((differentialsHeadingBox?.y ?? 0) + (differentialsHeadingBox?.height ?? 0)).toBeLessThan(
- differentialsSearchBox?.y ?? 0,
- );
- expect((differentialsSearchBox?.y ?? 0) + (differentialsSearchBox?.height ?? 0) / 2).toBeLessThan(900 * 0.62);
+ // The shell's Suspense swap after client navigation can briefly detach the
+ // hero heading, so retry the geometry probe instead of measuring once.
+ await expect(async () => {
+ const differentialsSearchBox = await differentialsHomeSearch.boundingBox();
+ const differentialsHeadingBox = await page
+ .getByRole("heading", { level: 1, name: "Differentials" })
+ .boundingBox();
+ expect(differentialsSearchBox).not.toBeNull();
+ expect(differentialsHeadingBox).not.toBeNull();
+ expect((differentialsHeadingBox?.y ?? 0) + (differentialsHeadingBox?.height ?? 0)).toBeLessThan(
+ differentialsSearchBox?.y ?? 0,
+ );
+ expect((differentialsSearchBox?.y ?? 0) + (differentialsSearchBox?.height ?? 0) / 2).toBeLessThan(900 * 0.62);
+ }).toPass({ timeout: 10_000 });
await expectNoPageHorizontalOverflow(page);
});