Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/site-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,6 +13,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`.
- `/medications` - Medication index redirect. Source: `src/app/medications/page.tsx`.
- `/services` - Services home and search surface. Source: `src/app/services/page.tsx`.
- `/services-navigator-preview` - Route discovered from app directory Source: `src/app/services-navigator-preview/page.tsx`.

## Mode/query routes

Expand DownExpand Up@@ -92,6 +93,8 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/mockups/settings-search-clinical` - Route discovered from app directory Source: `src/app/mockups/settings-search-clinical/page.tsx`.
- `/mockups/settings-search-general` - Route discovered from app directory Source: `src/app/mockups/settings-search-general/page.tsx`.
- `/mockups/settings-search-privacy` - Route discovered from app directory Source: `src/app/mockups/settings-search-privacy/page.tsx`.
- `/mockups/tools-action-workbench` - Route discovered from app directory Source: `src/app/mockups/tools-action-workbench/page.tsx`.
- `/mockups/tools-clinical-lanes` - Route discovered from app directory Source: `src/app/mockups/tools-clinical-lanes/page.tsx`.
- `/mockups/tools-command-center` - Route discovered from app directory Source: `src/app/mockups/tools-command-center/page.tsx`.
- `/mockups/tools-split-pane` - Route discovered from app directory Source: `src/app/mockups/tools-split-pane/page.tsx`.
- `/mockups/tools-task-directory` - Route discovered from app directory Source: `src/app/mockups/tools-task-directory/page.tsx`.
Expand Down
12 changes: 12 additions & 0 deletions src/app/mockups/tools-action-workbench/page.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type { Metadata } from "next";

import { ToolsActionWorkbenchMockup } from "@/components/tools-page-mockups/rectangle-direction-mockups";

export const metadata: Metadata = {
title: "Tools Action Workbench Mockup - Clinical KB",
description: "Rectangle-first action workbench Tools page mockup for Clinical KB.",
};

export default function ToolsActionWorkbenchMockupRoute() {
return <ToolsActionWorkbenchMockup />;
}
12 changes: 12 additions & 0 deletions src/app/mockups/tools-clinical-lanes/page.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
import type { Metadata } from "next";

import { ToolsClinicalLanesMockup } from "@/components/tools-page-mockups/rectangle-direction-mockups";

export const metadata: Metadata = {
title: "Tools Clinical Lanes Mockup - Clinical KB",
description: "Rectangle-first clinical lane Tools page mockup for Clinical KB.",
};

export default function ToolsClinicalLanesMockupRoute() {
return <ToolsClinicalLanesMockup />;
}
5 changes: 5 additions & 0 deletions src/app/services-navigator-preview/page.tsx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
import { ServicesNavigatorPreview } from "@/components/services/services-navigator-preview";

export default function ServicesNavigatorPreviewRoute() {
return <ServicesNavigatorPreview />;
}
233 changes: 205 additions & 28 deletions src/components/clinical-dashboard/master-search-header.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,15 +14,20 @@ import { createPortal } from "react-dom";

import {
Activity,
BadgeCheck,
BrainCircuit,
CalendarDays,
Check,
CheckCircle2,
ChevronDown,
FileSignature,
FileText,
Filter,
FolderOpen,
GitBranch,
Globe2,
Heart,
ListChecks,
Loader2,
Menu,
MessageSquarePlus,
Expand DownExpand Up@@ -81,7 +86,7 @@ const appModeIcons: Record<AppModeId, typeof Search> = {
answer: Sparkles,
documents: FileText,
services: ShieldCheck,
forms: FileText,
forms: FileSignature,
favourites: Heart,
differentials: BrainCircuit,
prescribing: Pill,
Expand DownExpand Up@@ -893,14 +898,168 @@ export function MasterSearchHeader({
);
}

// A couple of footer chips are mode-local shortcuts rather than shared
// ModeActionIds. Keep them explicit so they cannot fall through to document
// library behavior on non-document pages.
type FooterChipActionId = ModeActionId | "open-evidence" | "forms-library";

type FooterActionChip = {
icon: typeof Search;
shortLabel: string;
longLabel: string;
actionId: FooterChipActionId;
ariaLabel: string;
};

// The first ("trust") chip on the universal small-screen footer. Every mode gets
// one, mirroring Answer's "Evidence-based" chip in tone, each wired to a real
// action from that mode's own action menu rather than being decorative.
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-library",
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",
};
// Differentials has no differential-specific evidence view yet — the only
// handler for "differentials-evidence" is the answer-mode evidence drawer,
// which would take the user out of differentials context. Omit the trust
// chip here until a differential-specific action exists; "Criteria" (the
// secondary chip below) still gives differentials a footer action.
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;
}
}

// The second footer chip. Answer/Documents use the shared document-scope
// trigger instead (see hasScopeFooterChip below) since scope is a real, existing
// concept for those two modes. Tools has no genuine second action yet, so it
// intentionally ships with a single chip rather than an invented one.
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;
}
if (actionId === "forms-library") {
onSearchModeChange("forms");
onQueryChange("");
return;
}
runModeAction(actionId);
}

function renderSearchComposer(placement: "default" | "desktop-home") {
const isDesktopHomeComposer = placement === "desktop-home";
const usesAnswerFooterStyle = isAnswerFooterComposer && !isDesktopHomeComposer;
const usesMobileBottomStyle = isMobileBottomComposer && !isDesktopHomeComposer;
const usesUniversalFooterStyle = usesAnswerFooterStyle || (usesMobileBottomStyle && usesPhoneSearchLayout);
const showFooterSearchChips = usesUniversalFooterStyle && searchMode === "answer";
// Only the Answer chat composer uses the send affordance; every search-mode home uses the magnifier.
// Every mode shows the universal footer chip row on its small-screen composer now;
// larger screens (sticky-top / hero composers) are untouched for now.
const showFooterSearchChips = usesUniversalFooterStyle;
// Answer keeps the send affordance everywhere (it's the one conversational compose
// mode). Every other mode swaps the magnifier for its own mode-identity glyph, but
// only on the small-screen floating composer — larger screens keep the magnifier.
const usesSendAffordance = usesAnswerFooterStyle;
const usesModeIdentityAffordance = usesUniversalFooterStyle && !usesSendAffordance;
const ModeIdentityIcon = appModeIcons[searchMode];
const hasScopeFooterChip = searchMode === "answer" || searchMode === "documents";
const trustFooterChip = footerTrustChipFor(searchMode);
const secondaryFooterChip = footerSecondaryChipFor(searchMode);
// Fallback icons here are never rendered — both are only used inside a JSX guard
// on the corresponding chip being non-null — but keep the icon variables typed as
// components (not `| null`) so the JSX below type-checks without a cast.
const TrustFooterChipIcon = trustFooterChip?.icon ?? BadgeCheck;
const SecondaryFooterChipIcon = secondaryFooterChip?.icon ?? ListChecks;
const composerPlaceholder =
usesMobileBottomStyle && searchMode === "differentials" ? "Search a presentation" : queryPlaceholder;

Expand DownExpand Up@@ -1004,38 +1163,56 @@ export function MasterSearchHeader({
<Loader2 className="h-4 w-4 animate-spin" />
) : usesSendAffordance ? (
<Send className="h-4 w-4" />
) : usesModeIdentityAffordance ? (
<ModeIdentityIcon className="h-4.5 w-4.5" />
) : (
<Search className="h-4.5 w-4.5" />
)}
<span className="sr-only">{submitLabel}</span>
</button>
</div>
{showFooterSearchChips ? (
{showFooterSearchChips && (trustFooterChip || hasScopeFooterChip || secondaryFooterChip) ? (
<div className="flex max-w-full flex-wrap items-center justify-center gap-2 px-2">
<button
type="button"
onClick={() => onOpenEvidence?.()}
className="answer-footer-search-chip"
aria-label="Open evidence-backed answer sources"
>
<ShieldCheck className="h-4 w-4" aria-hidden="true" />
<span className="sm:hidden">Evidence</span>
<span className="hidden sm:inline">Evidence-based</span>
</button>
<button
type="button"
ref={scopeSummaryRef}
data-testid="scope-trigger"
onClick={openScopePicker}
className="answer-footer-search-chip"
aria-expanded={usesScopeSheet ? scopeSheetOpen : scopeOpen}
aria-label="Open source scope"
>
<Filter className="h-4 w-4" aria-hidden="true" />
<span className="sm:hidden">{selectedDocumentIds.length === 0 ? "Sources" : footerScopeLabel}</span>
<span className="hidden sm:inline">{footerScopeLabel}</span>
</button>
{!usesScopeSheet && scopeOpen ? (
{trustFooterChip ? (
<button
type="button"
onClick={() => runFooterChipAction(trustFooterChip.actionId)}
className="answer-footer-search-chip"
aria-label={trustFooterChip.ariaLabel}
>
<TrustFooterChipIcon className="h-4 w-4" aria-hidden="true" />
<span className="sm:hidden">{trustFooterChip.shortLabel}</span>
<span className="hidden sm:inline">{trustFooterChip.longLabel}</span>
</button>
) : null}
{hasScopeFooterChip ? (
<button
type="button"
ref={scopeSummaryRef}
data-testid="scope-trigger"
onClick={openScopePicker}
className="answer-footer-search-chip"
aria-expanded={usesScopeSheet ? scopeSheetOpen : scopeOpen}
aria-label="Open source scope"
>
<Filter className="h-4 w-4" aria-hidden="true" />
<span className="sm:hidden">{selectedDocumentIds.length === 0 ? "Sources" : footerScopeLabel}</span>
<span className="hidden sm:inline">{footerScopeLabel}</span>
</button>
) : null}
{!hasScopeFooterChip && secondaryFooterChip ? (
<button
type="button"
onClick={() => runFooterChipAction(secondaryFooterChip.actionId)}
className="answer-footer-search-chip"
aria-label={secondaryFooterChip.ariaLabel}
>
<SecondaryFooterChipIcon className="h-4 w-4" aria-hidden="true" />
<span className="sm:hidden">{secondaryFooterChip.shortLabel}</span>
<span className="hidden sm:inline">{secondaryFooterChip.longLabel}</span>
</button>
) : null}
{hasScopeFooterChip && !usesScopeSheet && scopeOpen ? (
<div
ref={scopePopoverRef}
data-testid="scope-command-popover"
Expand Down
Loading
Loading