diff --git a/src/app/globals.css b/src/app/globals.css index d0971df7b1..20eb977e70 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -568,6 +568,7 @@ summary::-webkit-details-marker { width: min(100%, clamp(19rem, 90vw, 52rem)); max-width: calc(100vw - 1rem - var(--safe-area-left) - var(--safe-area-right)); margin-inline: auto; + overflow: visible; } .universal-home-search-edge { diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 14ae434f90..de0809e93b 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3882,7 +3882,9 @@ export function ClinicalDashboard({ ? // On tall phones the centred home leans slightly toward the // bottom composer (matches the committed vertical-weighting // guard); short phones skip the bias so content still fits. - "grid w-full place-items-center max-sm:[@media(min-height:800px)]:pt-[5vh]" + // Mobile uses top alignment so the integrated action menu is + // not clipped by the dead space below vertically centred homes. + "grid w-full place-items-center max-sm:place-content-start max-sm:justify-items-center max-sm:pt-[clamp(0.75rem,3vh,2rem)] max-sm:[@media(min-height:800px)]:pt-[5vh]" : activeModeResultKind === "tools" || activeModeResultKind === "favourites" || activeModeResultKind === "differentials" diff --git a/src/components/clinical-dashboard/master-search-header.tsx b/src/components/clinical-dashboard/master-search-header.tsx index 002dcf7dbf..6feda36919 100644 --- a/src/components/clinical-dashboard/master-search-header.tsx +++ b/src/components/clinical-dashboard/master-search-header.tsx @@ -16,13 +16,17 @@ import { createPortal } from "react-dom"; import { Activity, + BadgeCheck, CalendarDays, Check, CheckCircle2, ChevronDown, FileText, Filter, + FolderOpen, + GitBranch, Globe2, + ListChecks, Loader2, Menu, MessageSquarePlus, @@ -349,6 +353,7 @@ export function MasterSearchHeader({ [documentById, selectedDocumentIds], ); const scopeSummary = selectedDocumentIds.length === 0 ? "All documents" : `${selectedDocumentIds.length} scoped`; + const footerScopeLabel = selectedDocumentIds.length === 0 ? "All sources" : `${selectedDocumentIds.length} scoped`; const scopePreview = useMemo( () => selectedDocuments @@ -641,11 +646,20 @@ export function MasterSearchHeader({ } } + const restoreActionMenuFocusRef = useRef(false); const closeScope = useCallback((restoreFocus = false) => { + restoreActionMenuFocusRef.current = restoreFocus; setScopeOpen(false); - if (restoreFocus) actionMenuTriggerRef.current?.focus(); }, []); + useEffect(() => { + if (scopeOpen || !restoreActionMenuFocusRef.current) return; + restoreActionMenuFocusRef.current = false; + window.requestAnimationFrame(() => { + actionMenuTriggerRef.current?.focus({ preventScroll: true }); + }); + }, [scopeOpen]); + const closeScopeSheet = useCallback(() => { setScopeSheetOpen(false); window.requestAnimationFrame(() => actionMenuTriggerRef.current?.focus()); @@ -1040,6 +1054,137 @@ export function MasterSearchHeader({ ); } + // "open-evidence" is the one footer-chip action that isn't already a mode-action + // id — every other chip dispatches through the existing runModeAction handler + // (the same dispatcher the "+" action menu already uses for these ids). + type FooterChipActionId = ModeActionId | "open-evidence"; + + type FooterActionChip = { + icon: typeof Search; + shortLabel: string; + longLabel: string; + actionId: FooterChipActionId; + ariaLabel: string; + }; + + function footerTrustChipFor(mode: AppModeId): FooterActionChip | null { + switch (mode) { + case "answer": + return { + icon: ListChecks, + shortLabel: "Evidence", + longLabel: "Evidence-based", + actionId: "open-evidence", + ariaLabel: "Open evidence-backed answer sources", + }; + case "documents": + return { + icon: BadgeCheck, + shortLabel: "Indexed", + longLabel: "Fully indexed", + actionId: "documents-collections", + ariaLabel: "Open the indexed document library", + }; + case "forms": + return { + icon: BadgeCheck, + shortLabel: "Library", + longLabel: "Form library", + actionId: "forms-records", + ariaLabel: "Open the form library", + }; + case "services": + return { + icon: BadgeCheck, + shortLabel: "Verified", + longLabel: "Verified directory", + actionId: "services-records", + ariaLabel: "Browse verified service records", + }; + case "favourites": + return { + icon: BadgeCheck, + shortLabel: "Trusted", + longLabel: "Trusted picks", + actionId: "favourites-browse", + ariaLabel: "Browse trusted favourites", + }; + case "differentials": + return { + icon: ListChecks, + shortLabel: "Evidence", + longLabel: "Evidence-linked", + actionId: "differentials-evidence", + ariaLabel: "Review cited differential evidence", + }; + case "prescribing": + return { + icon: ShieldCheck, + shortLabel: "Safety", + longLabel: "Safety-checked", + actionId: "medication-safety", + ariaLabel: "Review contraindications and cautions", + }; + case "tools": + return { + icon: BadgeCheck, + shortLabel: "Curated", + longLabel: "Curated registry", + actionId: "tools-browse", + ariaLabel: "Browse the curated tools registry", + }; + default: + return null; + } + } + + function footerSecondaryChipFor(mode: AppModeId): FooterActionChip | null { + switch (mode) { + case "services": + return { + icon: ListChecks, + shortLabel: "Pathways", + longLabel: "Pathways", + actionId: "services-pathways", + ariaLabel: "Browse referral pathways", + }; + case "favourites": + return { + icon: FolderOpen, + shortLabel: "Sets", + longLabel: "Sets", + actionId: "favourites-sets", + ariaLabel: "Open saved sets", + }; + case "differentials": + return { + icon: GitBranch, + shortLabel: "Criteria", + longLabel: "Criteria", + actionId: "differentials-criteria", + ariaLabel: "Compare distinguishing criteria", + }; + case "prescribing": + return { + icon: Activity, + shortLabel: "Monitor", + longLabel: "Monitoring", + actionId: "medication-monitoring", + ariaLabel: "Review the monitoring schedule", + }; + default: + return null; + } + } + + function runFooterChipAction(actionId: FooterChipActionId) { + if (actionId === "open-evidence") { + onOpenEvidence?.(); + return; + } + runModeAction(actionId); + } + function renderSearchComposer(placement: "default" | "desktop-home") { const isDesktopHomeComposer = placement === "desktop-home"; const usesAnswerFooterStyle = isAnswerFooterComposer && !isDesktopHomeComposer; @@ -1047,12 +1192,16 @@ export function MasterSearchHeader({ const usesCompactMobileBottomStyle = usesMobileBottomStyle && mobileBottomSearchVariant === "compact"; const usesBottomComposerPlacement = usesAnswerFooterStyle || (usesMobileBottomStyle && usesPhoneSearchLayout); const usesFooterChipLayout = usesBottomComposerPlacement || isDesktopHomeComposer; - // The visible footer/hero composer chrome is universal; submit semantics still - // come from the active mode. + // Keep footer suggestion chips on tablet/desktop; phones reach the same actions via "+". + const showFooterSearchChips = usesFooterChipLayout && !usesPhoneSearchLayout; const usesSendAffordance = searchMode === "answer" || usesFooterChipLayout; const usesModeIdentityAffordance = usesBottomComposerPlacement && !usesSendAffordance; const ModeIdentityIcon = appModeIcons[searchMode]; - const supportsDocumentScope = searchMode === "answer" || searchMode === "documents" || searchMode === "forms"; + const hasScopeFooterChip = searchMode === "answer" || searchMode === "documents" || searchMode === "forms"; + const trustFooterChip = footerTrustChipFor(searchMode); + const secondaryFooterChip = footerSecondaryChipFor(searchMode); + const TrustFooterChipIcon = trustFooterChip?.icon ?? BadgeCheck; + const SecondaryFooterChipIcon = secondaryFooterChip?.icon ?? ListChecks; const composerPlaceholder = usesMobileBottomStyle && searchMode === "differentials" ? "Search a presentation" : queryPlaceholder; @@ -1083,6 +1232,7 @@ export function MasterSearchHeader({ usesBottomComposerPlacement && "answer-footer-search-edge", usesPhoneFooterDock && "answer-footer-search-dock", usesCompactMobileBottomStyle && "document-mobile-search-compact", + showFooterSearchChips && "flex flex-col items-center gap-2.5", shouldHideBottomOnScroll && "max-sm:transition-transform max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none", )} @@ -1173,7 +1323,7 @@ export function MasterSearchHeader({ triggerClassName="answer-footer-search-action" triggerRef={actionMenuTriggerRef} integrated={usesFooterChipLayout} - integratedChipRow={false} + integratedChipRow={showFooterSearchChips} dismissIgnoreRefs={[modeMenuRef]} /> @@ -1237,9 +1387,51 @@ export function MasterSearchHeader({ + {showFooterSearchChips && (trustFooterChip || hasScopeFooterChip || secondaryFooterChip) ? ( +