From 4d766d853576d2e0386f1c9f4fb17541bbd67208 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 15:53:47 +0800 Subject: [PATCH 1/6] feat(ui): unify mode home screens on the shared Answer foundation All mode home screens (Services, Forms, Favourites, Differentials, Medications, Tools, Documents) now share the Answer home structure: centred hero with the search composer below the title, symmetric 3-card actions, centred quick-link pills, and a shared verification footer via ModeHomeTemplate/ModeHomeHero/ModeHomeMain. - Rebuild Services and Forms homes on ModeHomeTemplate; delete the diverged CompactRecordHomePage copy - Give FavouritesHub and the dashboard Tools launcher the shared centred hero and desktop composer slot - Render the shared desktop/mobile sidebar, Guide and Settings dialogs, and header utility menu in GlobalMockupSearchShell so /services, /forms, /favourites, and /differentials match the dashboard shell - Unify standalone header branding to "Clinical Guide" + mode description (removes per-mode Navigator titles) - Vertically centre Documents/Medications/Differentials home states in the dashboard and the standalone differentials home Co-Authored-By: Claude Fable 5 --- src/components/ClinicalDashboard.tsx | 14 +- src/components/applications-launcher-page.tsx | 21 +- .../document-search-results.tsx | 2 +- .../favourites-home-page.tsx | 3 + .../clinical-dashboard/favourites-hub.tsx | 33 +- .../global-mockup-search-shell.tsx | 306 ++++++++++-------- .../master-search-header.tsx | 24 +- src/components/compact-record-home-page.tsx | 167 ---------- .../differentials/differentials-home-page.tsx | 5 +- src/components/forms/forms-home-page.tsx | 141 ++------ src/components/mode-home-template.tsx | 221 +++++++++---- .../services/services-home-page.tsx | 50 +-- 12 files changed, 437 insertions(+), 550 deletions(-) delete mode 100644 src/components/compact-record-home-page.tsx diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 7c6bff3b0b..a3517cd099 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -266,7 +266,7 @@ function useMobilePreviewSheet() { } const authEmailChangeEvent = "clinical-kb-auth-email-change"; -const recentQueryStorageKey = "clinical-kb-recent-queries"; +export const recentQueryStorageKey = "clinical-kb-recent-queries"; const documentPageSize = 150; const activeIndexingPollFallbackMs = 5_000; const setupRecheckPollMs = 60_000; @@ -4848,7 +4848,7 @@ function DrawerGroupLabel({ title }: { title: string }) { ); } -function SettingsDialog({ +export function SettingsDialog({ open, onClose, identity, @@ -7300,8 +7300,13 @@ export function ClinicalDashboard({ !modeSearchSubmitted) || (searchMode === "prescribing" && activeModeResultKind === "documents" && !modeSearchSubmitted) || (activeModeResultKind === "differentials" && !modeSearchSubmitted) || + activeModeResultKind === "favourites" || activeModeResultKind === "tools"); const desktopHomeComposerSlotId = showDesktopHomeComposer ? modeHomeDesktopComposerSlotId : undefined; + // Favourites and Tools are content-rich hubs: they share the centred hero but + // stay top-aligned so their lists start in a stable position. + const centeredModeHome = + showDesktopHomeComposer && activeModeResultKind !== "tools" && activeModeResultKind !== "favourites"; const renderDegradedNotice = () => ( setActionNotice({ tone: "success", message: "Favourite creation is ready to connect." }) } + desktopComposerSlotId={desktopHomeComposerSlotId} /> ) : activeModeResultKind === "documents" ? ( searchMode === "prescribing" ? ( diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index dc5473fb88..9ecf17f443 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -27,6 +27,7 @@ import { } from "lucide-react"; import { type FormEvent, useMemo, useState } from "react"; +import { ModeHomeHero } from "@/components/mode-home-template"; import { Sheet } from "@/components/ui/sheet"; import { appBackdrop, @@ -1002,18 +1003,16 @@ export function ApplicationsLauncherWorkspace({ <> {isDashboardTools ? (
-
- - - -
-

- {copy.heading} -

-

{copy.description}

-
+
+ {desktopComposerSlotId ? ( -
+
) : null}
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..51da5aab25 100644 --- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx +++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx @@ -1,34 +1,24 @@ "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 { 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,17 +31,6 @@ 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; @@ -103,6 +82,13 @@ function GlobalMockupSearchShellClient({ const [queryMode, setQueryMode] = useState("auto"); const [scopeFilters, setScopeFilters] = useState({}); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + 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 +125,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 +190,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 +218,122 @@ 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)} + onOpenSettings={openSettings} + theme={theme} + onToggleTheme={toggleTheme} + 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} +
- 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 e2cff142be..8607e3fc51 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -308,26 +308,10 @@ 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; + // Every mode home shares the same header identity; only the mode description + // varies, matching the dashboard shell. + const homeHeaderTitle = "Clinical Guide"; + const homeHeaderDescription = selectedAppMode.description; function currentUsesScopeSheet() { return window.matchMedia(mobileSheetMediaQuery).matches; 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 ( -
-
-
- - - -
-

- What do you need from forms? -

-

- 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..608a6d0d35 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 ( +

+ + + + {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} + - -
+ })} +
+ ) : 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 ( - + + + } + /> + ); } From d8d0add1c19dc648cb9f2d840b0428962fb7238a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:29:08 +0800 Subject: [PATCH 2/6] feat(ui): global footer composer on all mobile homes + modernised chat bar - Render the Evidence/Sources footer chips for every mobile home composer (universal footer style), not just Answer, so all modes share the same floating global footer on phones - Reserve taller bottom padding on mobile so home content clears the taller footer (mode home main + shell content wrapper) - Modernise the floating chat pill: frosted translucent surface with backdrop blur, layered elevation shadows, hover/focus states, softer border, roomier internal spacing; matching treatment for the desktop hero pill and larger touch-friendly footer chips with hover states and a press affordance on the send button (reduced-motion safe) Co-Authored-By: Claude Fable 5 --- src/app/globals.css | 97 ++++++++++++++----- .../global-mockup-search-shell.tsx | 4 +- .../master-search-header.tsx | 2 +- src/components/mode-home-template.tsx | 2 +- 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/src/app/globals.css b/src/app/globals.css index b67d9c0619..dc8b91a0af 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -486,24 +486,36 @@ summary::-webkit-details-marker { .answer-footer-search-pill { min-height: 3.8rem; - gap: 0.25rem; - border: 1px solid var(--border-strong); - background: var(--surface); - padding-inline: 0.375rem; + gap: 0.375rem; + border: 1px solid color-mix(in srgb, var(--border-strong) 80%, transparent); + background: color-mix(in srgb, var(--surface) 97%, transparent); + padding-inline: 0.5rem; box-shadow: - 0 1px 2px rgb(16 24 40 / 5%), - 0 8px 22px rgb(16 24 40 / 8%); + 0 1px 2px rgb(16 24 40 / 4%), + 0 6px 16px rgb(16 24 40 / 7%), + 0 20px 48px rgb(16 24 40 / 12%); + backdrop-filter: blur(18px) saturate(140%); + -webkit-backdrop-filter: blur(18px) saturate(140%); transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease; } +.answer-footer-search-pill:hover { + border-color: var(--border-strong); + box-shadow: + 0 1px 2px rgb(16 24 40 / 5%), + 0 8px 20px rgb(16 24 40 / 9%), + 0 24px 56px rgb(16 24 40 / 14%); +} + .answer-footer-search-pill:focus-within { border-color: var(--clinical-accent); box-shadow: - 0 0 0 3px color-mix(in srgb, var(--clinical-accent) 18%, transparent), - 0 8px 22px rgb(16 24 40 / 10%); + 0 0 0 3px color-mix(in srgb, var(--clinical-accent) 16%, transparent), + 0 8px 20px rgb(16 24 40 / 8%), + 0 20px 48px rgb(16 24 40 / 12%); } .answer-footer-search-action { @@ -548,26 +560,49 @@ summary::-webkit-details-marker { width: 2.8rem; background: var(--clinical-accent); color: var(--clinical-accent-contrast); - box-shadow: 0 2px 8px color-mix(in srgb, var(--clinical-accent) 30%, transparent); + box-shadow: 0 3px 10px color-mix(in srgb, var(--clinical-accent) 32%, transparent); + transition: + background-color 160ms ease, + box-shadow 160ms ease, + transform 160ms ease; } .answer-footer-search-send:hover { background: var(--clinical-accent-hover); + box-shadow: 0 5px 14px color-mix(in srgb, var(--clinical-accent) 38%, transparent); +} + +@media (prefers-reduced-motion: no-preference) { + .answer-footer-search-send:active { + transform: scale(0.96); + } } .answer-footer-search-chip { display: inline-flex; - min-height: 1.9rem; + min-height: 2.25rem; align-items: center; - gap: 0.35rem; - border: 1px solid var(--border); + gap: 0.4rem; + border: 1px solid color-mix(in srgb, var(--border) 85%, transparent); border-radius: 999px; - background: var(--surface); - padding-inline: 0.65rem; + background: color-mix(in srgb, var(--surface) 94%, transparent); + padding-inline: 0.8rem; color: var(--text-muted); font-size: 0.75rem; font-weight: 700; - box-shadow: none; + box-shadow: 0 2px 10px rgb(16 24 40 / 6%); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + transition: + border-color 160ms ease, + background-color 160ms ease, + color 160ms ease; +} + +.answer-footer-search-chip:hover { + border-color: var(--clinical-accent-border); + background: var(--surface); + color: var(--text); } .answer-footer-search-chip svg { @@ -610,23 +645,35 @@ summary::-webkit-details-marker { .desktop-home-search-pill { min-height: 3.45rem; - gap: 0.35rem; - border-color: var(--border-strong); - background: var(--surface); - padding-inline: 0.45rem; + gap: 0.375rem; + border-color: color-mix(in srgb, var(--border-strong) 80%, transparent); + background: color-mix(in srgb, var(--surface) 96%, transparent); + padding-inline: 0.5rem; box-shadow: - 0 1px 2px rgb(16 24 40 / 5%), - 0 8px 22px rgb(16 24 40 / 8%); + 0 1px 2px rgb(16 24 40 / 4%), + 0 6px 16px rgb(16 24 40 / 6%), + 0 18px 40px rgb(16 24 40 / 9%); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); transition: border-color 180ms ease, box-shadow 180ms ease; } +.desktop-home-search-pill:hover { + border-color: var(--border-strong); + box-shadow: + 0 1px 2px rgb(16 24 40 / 5%), + 0 8px 20px rgb(16 24 40 / 8%), + 0 22px 48px rgb(16 24 40 / 11%); +} + .desktop-home-search-pill:focus-within { border-color: var(--clinical-accent); box-shadow: - 0 0 0 3px color-mix(in srgb, var(--clinical-accent) 18%, transparent), - 0 8px 22px rgb(16 24 40 / 10%); + 0 0 0 3px color-mix(in srgb, var(--clinical-accent) 16%, transparent), + 0 8px 20px rgb(16 24 40 / 8%), + 0 18px 40px rgb(16 24 40 / 9%); } .desktop-home-search-input { @@ -693,8 +740,8 @@ summary::-webkit-details-marker { } .answer-footer-search-chip { - min-height: 2rem; - padding-inline: 0.82rem; + min-height: 2.25rem; + padding-inline: 0.9rem; font-size: 0.8125rem; } diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx index 51da5aab25..f0fc4794b4 100644 --- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx +++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx @@ -302,8 +302,8 @@ function GlobalMockupSearchShellClient({ className={cn( "min-h-[calc(100dvh-4rem)] min-w-0 overflow-x-hidden", searchMode === "answer" - ? "pb-[calc(6.5rem+env(safe-area-inset-bottom))]" - : "pb-[calc(6.5rem+env(safe-area-inset-bottom))] sm:pb-8", + ? "pb-[calc(9rem+env(safe-area-inset-bottom))]" + : "pb-[calc(9rem+env(safe-area-inset-bottom))] sm:pb-8", )} > {children} diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 8607e3fc51..ce835dfe38 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -939,7 +939,7 @@ export function MasterSearchHeader({ {submitLabel}
- {usesAnswerFooterStyle ? ( + {usesUniversalFooterStyle ? (
+
-
+
- {isStandaloneModeHomeHeader ? ( -
- - - - - - {homeHeaderTitle} - - - {homeHeaderDescription} - - -
- ) : null}
{ setActionMenuOpen(false); - setUtilityMenuOpen(false); closeScope(false); setModeMenuOpen((open) => !open); }} @@ -1221,23 +1173,6 @@ export function MasterSearchHeader({ Copy after review - ) : 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 ? ( - {utilityMenuOpen ? ( - - ) : null} - - ) : null}
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/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 9201e18403..4e1a826681 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -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); }); From 8539b4a9052496b1cbac15b18155a59eeecd6cb2 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:53:31 +0800 Subject: [PATCH 4/6] feat(ui): add Differentials to sidebar tools grid and collapsed rail Gives /differentials the same active-state treatment as the other modes: a "Diffs" tile in the sidebar Tools grid (now a symmetric 4x2) and a BrainCircuit rail icon with the accent-soft active highlight. Co-Authored-By: Claude Fable 5 --- src/components/clinical-dashboard/ClinicalSidebar.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index 505fcfdc6b..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, @@ -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; @@ -391,6 +393,15 @@ export function ClinicalDesktopSidebar({ > + + + Date: Thu, 2 Jul 2026 18:36:03 +0800 Subject: [PATCH 5/6] feat(ui): roll the global header + sidebar out to every page - Shell /applications and /medications routes with the shared GlobalSearchShell (sidebar + global header) instead of their hand-rolled local header copies: delete the launcher's imitation header/mode-menu/mobile-menu (~280 lines) and demote the medication page's local header to an in-page back link - Add a searchComposerVisible shell prop so routes with their own search surface (/applications) show the header without a duplicate composer, with bottom padding adjusted to match - Header polish: mode pill always shows the MODE eyebrow + label and auto-sizes (no per-page description variants), one New chat treatment everywhere, and the redundant absolute-centring modeAlignment prop is removed (the symmetric grid centres the pill) - Update the launcher test to the shared header's Tools mode label Co-Authored-By: Claude Fable 5 --- src/app/applications/layout.tsx | 11 + src/app/medications/layout.tsx | 7 + src/components/applications-launcher-page.tsx | 296 +----------------- .../global-mockup-search-shell.tsx | 14 +- .../master-search-header.tsx | 40 +-- .../medication-prescribing-workspace.tsx | 38 +-- tests/ui-tools.spec.ts | 2 +- 7 files changed, 45 insertions(+), 363 deletions(-) create mode 100644 src/app/applications/layout.tsx create mode 100644 src/app/medications/layout.tsx diff --git a/src/app/applications/layout.tsx b/src/app/applications/layout.tsx new file mode 100644 index 0000000000..c9a0125402 --- /dev/null +++ b/src/app/applications/layout.tsx @@ -0,0 +1,11 @@ +import type { ReactNode } from "react"; + +import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; + +export default function ApplicationsLayout({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/src/app/medications/layout.tsx b/src/app/medications/layout.tsx new file mode 100644 index 0000000000..31812964b3 --- /dev/null +++ b/src/app/medications/layout.tsx @@ -0,0 +1,7 @@ +import type { ReactNode } from "react"; + +import { GlobalSearchShell } from "@/components/clinical-dashboard/global-search-shell"; + +export default function MedicationsLayout({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index 9ecf17f443..aa405b2b52 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -3,15 +3,12 @@ import Link from "next/link"; import { BookOpen, - Check, ChevronRight, ClipboardList, ExternalLink, FileText, Grid2X2, - Globe2, Mic, - Menu, MoreVertical, Pill, Pin, @@ -19,8 +16,6 @@ import { Plus, Search, Send, - Settings, - Sparkles, Star, X, type LucideIcon, @@ -30,14 +25,11 @@ import { type FormEvent, useMemo, useState } from "react"; import { ModeHomeHero } from "@/components/mode-home-template"; import { Sheet } from "@/components/ui/sheet"; import { - appBackdrop, chatComposerIconButton, chatComposerInput, chatComposerShell, chatSendButton, cn, - sidebarItem, - sidebarToolTile as sidebarApplicationTile, textMuted, } from "@/components/ui-primitives"; type LauncherStatus = "ready" | "recent" | "review_due"; @@ -81,14 +73,6 @@ const filterOptions = [ { id: "recent", label: "Recent" }, ] as const; -const sidebarApplicationItems = [ - { label: "Answer", icon: Sparkles, href: "/?mode=answer" }, - { label: "Documents", icon: FileText, href: "/?mode=documents" }, - { label: "Services", icon: ClipboardList, href: "/services" }, - { label: "Forms", icon: FileText, href: "/forms" }, - { label: "Meds", icon: Pill, href: "/?mode=prescribing" }, -] as const; - const launcherApps: LauncherApp[] = [ { id: "medication-prescribing", @@ -665,270 +649,6 @@ function DetailPanel({ ); } -function ApplicationsMobileMenu({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { - const recentQueries = ["lithium", "tables", "dosing for lithium", "management of bulimia nervosa"]; - - return ( - onOpenChange(false)} - title="Clinical Guide" - description="Recent chats, applications, help, and settings." - closeLabel="Close Clinical Guide menu" - placement="left" - contentClassName="font-sans lg:hidden" - > -
- onOpenChange(false)} - className="inline-flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-[color:var(--clinical-accent)] px-3 text-sm font-semibold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)] hover:bg-[color:var(--clinical-accent-hover)]" - > - - New chat - - - - -
-

- Recent chats -

-
- {recentQueries.map((recent) => ( - - ))} -
-
- -
-

- Applications -

-
- {sidebarApplicationItems.map((item) => { - const Icon = item.icon; - return ( - onOpenChange(false)} - className={sidebarApplicationTile} - > - - {item.label} - - ); - })} -
-
- -
- - -
- - AK - - - Dr A. Khan - Ready - -
-
-
-
- ); -} - -function ApplicationsModeMenu({ open, onOpenChange }: { open: boolean; onOpenChange: (open: boolean) => void }) { - const options = [ - { - label: "Answer", - description: "Source-backed clinical answer", - href: "/?mode=answer", - icon: Sparkles, - active: false, - }, - { - label: "Documents", - description: "Search indexed PDFs and notes", - href: "/?mode=documents", - icon: FileText, - active: false, - }, - { - label: "Services", - description: "Service records and referral pathways", - href: "/services", - icon: ClipboardList, - active: false, - }, - { - label: "Forms", - description: "Clinical forms and pathways", - href: "/forms", - icon: FileText, - active: false, - }, - { - label: "Applications", - description: "Launch connected applications", - href: "/applications", - icon: Grid2X2, - active: true, - }, - ] as const; - - if (!open) return null; - - return ( -
- {options.map((option) => { - const Icon = option.icon; - return ( - onOpenChange(false)} - aria-current={option.active ? "page" : undefined} - className={cn( - "grid min-h-[3.25rem] w-full grid-cols-[2rem_minmax(0,1fr)_auto] items-center gap-2 rounded-md px-2.5 py-2 text-left transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", - option.active - ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" - : "text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]", - )} - > - - - - - {option.label} - - {option.description} - - - {option.active ? : null} - - ); - })} -
- ); -} - -function ApplicationsHeader({ - mobileMenuOpen, - modeMenuOpen, - onMobileMenuOpenChange, - onModeMenuOpenChange, -}: { - mobileMenuOpen: boolean; - modeMenuOpen: boolean; - onMobileMenuOpenChange: (open: boolean) => void; - onModeMenuOpenChange: (open: boolean) => void; -}) { - return ( - - ); -} - type ApplicationsLauncherWorkspaceProps = { variant?: LauncherVariant; query?: string; @@ -948,8 +668,6 @@ export function ApplicationsLauncherWorkspace({ const [activeFilter, setActiveFilter] = useState<(typeof filterOptions)[number]["id"]>("all"); const [selectedId, setSelectedId] = useState("clinical-kb-search"); const [pinnedIds, setPinnedIds] = useState(seedPinnedIds); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - const [modeMenuOpen, setModeMenuOpen] = useState(false); const [mobileDetailOpen, setMobileDetailOpen] = useState(false); const isDashboardTools = variant === "dashboard-tools"; const copy = isDashboardTools ? dashboardToolsLauncherCopy : standaloneLauncherCopy; @@ -1227,19 +945,7 @@ export function ApplicationsLauncherWorkspace({ } return ( -
- - - -
- {workspace} -
-
+
{workspace}
); } diff --git a/src/components/clinical-dashboard/global-mockup-search-shell.tsx b/src/components/clinical-dashboard/global-mockup-search-shell.tsx index 634510df3b..a05e1387b5 100644 --- a/src/components/clinical-dashboard/global-mockup-search-shell.tsx +++ b/src/components/clinical-dashboard/global-mockup-search-shell.tsx @@ -37,6 +37,8 @@ type GlobalMockupSearchShellProps = { 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) { @@ -60,6 +62,7 @@ function GlobalMockupSearchShellClient({ initialMode = "answer", availableModeIds, desktopSearchPlacement = "default", + searchComposerVisible = true, }: GlobalMockupSearchShellProps) { const router = useRouter(); const pathname = usePathname(); @@ -281,12 +284,11 @@ function GlobalMockupSearchShellClient({ queryModeOptions={mockupQueryModeOptions} queryInputRef={inputRef} headerVariant={isDifferentialPresentationWorkflow ? "workflow" : "default"} - modeAlignment={isDifferentialPresentationWorkflow ? "default" : "center"} mobileSearchPlacement="bottom" desktopSearchPlacement={ (desktopSearchPlacement === "hero" || isFormsOnlyShell) && isStandaloneModeHome ? "hero" : "default" } - searchComposerVisible={!isDifferentialPresentationWorkflow} + searchComposerVisible={searchComposerVisible && !isDifferentialPresentationWorkflow} workflowCopyText={ isDifferentialPresentationWorkflow ? "Acute confusion / encephalopathy differential comparison. Stabilise ABCs, check BGL, sats, attention test, collateral, and review medications/substances before handoff." @@ -299,9 +301,11 @@ function GlobalMockupSearchShellClient({ id="main-content" className={cn( "min-h-[calc(100dvh-4rem)] min-w-0 overflow-x-hidden", - searchMode === "answer" - ? "pb-[calc(9rem+env(safe-area-inset-bottom))]" - : "pb-[calc(9rem+env(safe-area-inset-bottom))] sm:pb-8", + !searchComposerVisible + ? "pb-8" + : searchMode === "answer" + ? "pb-[calc(9rem+env(safe-area-inset-bottom))]" + : "pb-[calc(9rem+env(safe-area-inset-bottom))] sm:pb-8", )} > {children} diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 4b6f0e16ec..f06ea2e096 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -161,7 +161,6 @@ export function MasterSearchHeader({ queryInputRef, queryInputAutoFocus = false, headerVariant = "default", - modeAlignment = "default", mobileSearchPlacement = "default", desktopSearchPlacement = "default", searchComposerVisible = true, @@ -199,7 +198,6 @@ export function MasterSearchHeader({ queryInputRef?: Ref; queryInputAutoFocus?: boolean; headerVariant?: "default" | "workflow"; - modeAlignment?: "default" | "center"; mobileSearchPlacement?: "default" | "bottom"; desktopSearchPlacement?: "default" | "hero"; searchComposerVisible?: boolean; @@ -1010,14 +1008,7 @@ export function MasterSearchHeader({
) : 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 ( -
-
-
- -
-
+
+
+ +
diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 4e1a826681..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); From 6d43156ee42e4e10501a2c3764bc3ad8fcf383b4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:59:46 +0800 Subject: [PATCH 6/6] chore: drop unused chatComposerIconButton import after merge --- src/components/clinical-dashboard/master-search-header.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index d462b6b5c8..257a185db6 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -52,7 +52,6 @@ import { } from "@/components/clinical-dashboard/mode-action-popup"; import { cn, - chatComposerIconButton, chatComposerInput, chatComposerShell, chatSendButton,