Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
6 changes: 3 additions & 3 deletions docs/site-map.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
## Main product pages

- `/` - Main Clinical KB shell. Source: `src/app/page.tsx`.
- `/applications` - Application and tool launcher. Source: `src/app/applications/page.tsx`.
- `/applications` - Route discovered from app directory Source: `src/app/applications/page.tsx`.
- `/differentials` - Differentials home and search surface. Source: `src/app/differentials/page.tsx`.
- `/differentials/diagnoses` - Diagnosis stream. Source: `src/app/differentials/diagnoses/page.tsx`.
- `/differentials/presentations` - Presentation workflow stream. Source: `src/app/differentials/presentations/page.tsx`.
Expand DownExpand Up@@ -40,7 +40,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Favourites | `/favourites` | `/favourites?q=clozapine+set&focus=1&run=1` | Saved set and saved item detail render inside the favourites page surface. |
| Differentials | `/differentials` | `/differentials?q=acute+confusion&focus=1&run=1` | `/differentials/diagnoses`, `/differentials/diagnoses/[slug]`, and `/differentials/presentations`. |
| Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | `/applications` launcher and tool detail panels inside tools mode. |
| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). |

## Documents flow index

Expand DownExpand Up@@ -601,5 +601,5 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
| Differentials | `src/app/differentials, src/lib/differentials.ts` |
| Medications | `src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx` |
| Documents | `src/app/documents, src/lib/document-flow-routes.ts` |
| Applications and tools | `src/app/applications, src/components/applications-launcher-page.tsx` |
| Tools | `src/components/applications-launcher-page.tsx` |
| Mockups | `src/app/mockups` |
5 changes: 2 additions & 3 deletions scripts/generate-site-map.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,7 +40,6 @@ type SiteMapData = {

const routeDescriptions: Record<string, string> = {
"/": "Main Clinical KB shell.",
"/applications": "Application and tool launcher.",
"/differentials": "Differentials home and search surface.",
"/differentials/diagnoses": "Diagnosis stream.",
"/differentials/diagnoses/[slug]": "Differential diagnosis detail.",
Expand DownExpand Up@@ -97,7 +96,7 @@ const routeOwnershipRows = [
["Differentials", "src/app/differentials, src/lib/differentials.ts"],
["Medications", "src/app/medications, src/components/clinical-dashboard/medication-prescribing-workspace.tsx"],
["Documents", "src/app/documents, src/lib/document-flow-routes.ts"],
["Applications and tools", "src/app/applications, src/components/applications-launcher-page.tsx"],
["Tools", "src/components/applications-launcher-page.tsx"],
["Mockups", "src/app/mockups"],
] as const;

Expand DownExpand Up@@ -279,7 +278,7 @@ function renderModePageIndex() {
mode: "Tools",
home: appModeHomeHref("tools"),
search: appModeHomeHref("tools", { query: "medications", focus: true, run: true }),
detail: "`/applications` launcher and tool detail panels inside tools mode.",
detail: "Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`).",
},
]);
}
Expand Down
18 changes: 10 additions & 8 deletions src/app/globals.css
Original file line numberDiff line numberDiff line change
Expand Up@@ -1101,14 +1101,14 @@ summary::-webkit-details-marker {

.answer-footer-search-action,
.answer-footer-search-send {
height: 2.05rem;
width: 2.05rem;
height: 2.75rem;
width: 2.75rem;
}

.answer-footer-search-action svg,
.answer-footer-search-send svg {
height: 1rem;
width: 1rem;
height: 1.1rem;
width: 1.1rem;
}
}

Expand DownExpand Up@@ -1172,14 +1172,16 @@ summary::-webkit-details-marker {
@media (max-width: 430px) {
.answer-footer-search-action,
.answer-footer-search-send {
height: 2.05rem !important;
width: 2.05rem !important;
height: 2.75rem !important;
width: 2.75rem !important;
min-height: 2.75rem;
min-width: 2.75rem;
}

.answer-footer-search-action svg,
.answer-footer-search-send svg {
height: 1rem;
width: 1rem;
height: 1.1rem;
width: 1.1rem;
}
}

Expand Down
32 changes: 4 additions & 28 deletions src/components/ClinicalDashboard.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1046,26 +1046,8 @@
);
}

function ToolsHub({
query,
onQueryChange,
desktopComposerSlotId,
showDetailPanel,
}: {
query: string;
onQueryChange: (nextQuery: string) => void;
desktopComposerSlotId?: string;
showDetailPanel?: boolean;
}) {
return (
<ApplicationsLauncherWorkspace
variant="dashboard-tools"
query={query}
onQueryChange={onQueryChange}
desktopComposerSlotId={desktopComposerSlotId}
showDetailPanel={showDetailPanel}
/>
);
function ToolsHub({ query, desktopComposerSlotId }: { query: string; desktopComposerSlotId?: string }) {
return <ApplicationsLauncherWorkspace query={query} desktopComposerSlotId={desktopComposerSlotId} />;
}

type MobileSectionFabItem = {
Expand DownExpand Up@@ -1566,7 +1548,7 @@
const activeModeSearch = appModeSearchConfig(searchMode);
const activeModeResultKind = appModeResultKind(searchMode);
const requestQueryMode = appModeQueryMode(searchMode, queryMode);
const requestedRun = searchParams.get("run") === "1";

// Record matches come from the owner-scoped registry API (mock fixtures in
// demo mode); ranking stays client-side so live-typing behaviour is
// unchanged and the registry is fetched once per active mode.
Expand DownExpand Up@@ -2460,8 +2442,7 @@
urlDocumentSearchBootstrappedRef.current = true;
void executeSearch(searchText, mode, scopeFilters);
// URL search intentionally runs once when the selected mode can execute.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [canRunSearch, answerThreadBootstrapped]);

Check warning on line 2445 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

React Hook useEffect has missing dependencies: 'executeSearch' and 'scopeFilters'. Either include them or remove the dependency array

useEffect(() => {
const updateHash = () => {
Expand DownExpand Up@@ -3190,7 +3171,7 @@
});
}

function stageAnswerFollowUpDraft(draft: string) {

Check warning on line 3174 in src/components/ClinicalDashboard.tsx

View workflow job for this annotation

GitHub Actions/ verify

'stageAnswerFollowUpDraft' is defined but never used
setQuery(draft);
focusComposerInput();
}
Expand DownExpand Up@@ -3954,12 +3935,7 @@
}}
/>
) : activeModeResultKind === "tools" ? (
<ToolsHub
query={query}
onQueryChange={setQuery}
desktopComposerSlotId={desktopHomeComposerSlotId}
showDetailPanel={!requestedRun}
/>
<ToolsHub query={query} desktopComposerSlotId={desktopHomeComposerSlotId} />
) : activeModeResultKind === "favourites" ? (
<FavouritesHub
query={query}
Expand Down
116 changes: 38 additions & 78 deletions src/components/applications-launcher-page.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,10 +26,8 @@ import { type FormEvent, useEffect, useMemo, useState } from "react";

import { ModeHomeVerificationFooter } from "@/components/mode-home-template";
import { cn } from "@/components/ui-primitives";

type LauncherStatus = "ready" | "recent" | "review_due";
type LauncherArea = "assessment" | "reference" | "care" | "coordination" | "saved";
type LauncherVariant = "standalone" | "dashboard-tools";
type LauncherFilter = "all" | LauncherArea | "more";

type LauncherApp = {
Expand All@@ -54,19 +52,6 @@ type LauncherApp = {
output: string;
};

type LauncherCopy = {
heading: string;
description: string;
searchAriaLabel: string;
searchPlaceholder: string;
openSelectedAriaLabel: string;
allSectionLabel: string;
allColumnLabel: string;
countNoun: string;
emptyTitle: string;
emptyBody: string;
};

const focusRing =
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]";

Expand DownExpand Up@@ -311,32 +296,17 @@ const launcherApps: LauncherApp[] = [
},
];

const standaloneLauncherCopy: LauncherCopy = {
heading: "Applications",
description:
"Open the clinical applications and connected workflows you use for assessment, prescribing, documents, and saved work.",
searchAriaLabel: "Search applications",
searchPlaceholder: "Search applications...",
openSelectedAriaLabel: "Open selected application",
allSectionLabel: "All applications",
allColumnLabel: "Application",
countNoun: "applications",
emptyTitle: "No applications match",
emptyBody: "Clear the search or try another clinical workflow, app name, or category.",
};

const dashboardToolsLauncherCopy: LauncherCopy = {
const toolsLauncherCopy = {
heading: "Tools",
description:
"Open the clinical tools and connected workflows you use for assessment, prescribing, documents, and saved work.",
searchAriaLabel: "Search tools",
searchPlaceholder: "Search tools...",
openSelectedAriaLabel: "Open selected tool",
allSectionLabel: "All tools",
allColumnLabel: "Tool",
countNoun: "tools",
emptyTitle: "No tools match",
emptyBody: "Clear the search or try another clinical workflow, tool name, or category.",
searchAriaLabel: "Search tools",
searchPlaceholder: "Search tools...",
openSelectedAriaLabel: "Open selected tool",
};

const quickActions = [
Expand DownExpand Up@@ -430,20 +400,6 @@ function StatusChip({ label, tone = "neutral" }: { label: string; tone?: "neutra
);
}

function ToolChips({ app, includeStatus = false }: { app: LauncherApp; includeStatus?: boolean }) {
return (
<span className="flex flex-wrap items-center gap-1.5">
{includeStatus ? <StatusChip label={statusLabels[app.status]} /> : null}
{app.sourceBacked ? <StatusChip label="Source-backed" tone="source" /> : <StatusChip label="Private" />}
{app.safetyFirst ? (
<StatusChip label="Safety-first" tone="safety" />
) : app.highYield ? (
<StatusChip label="High yield" tone="high" />
) : null}
</span>
);
}

function ToolSearch({
value,
onChange,
Expand All@@ -454,7 +410,7 @@ function ToolSearch({
value: string;
onChange: (query: string) => void;
onSubmit: () => void;
copy: LauncherCopy;
copy: typeof toolsLauncherCopy;
className?: string;
}) {
return (
Expand DownExpand Up@@ -496,6 +452,20 @@ function ToolSearch({
);
}

function ToolChips({ app, includeStatus = false }: { app: LauncherApp; includeStatus?: boolean }) {
return (
<span className="flex flex-wrap items-center gap-1.5">
{includeStatus ? <StatusChip label={statusLabels[app.status]} /> : null}
{app.sourceBacked ? <StatusChip label="Source-backed" tone="source" /> : <StatusChip label="Private" />}
{app.safetyFirst ? (
<StatusChip label="Safety-first" tone="safety" />
) : app.highYield ? (
<StatusChip label="High yield" tone="high" />
) : null}
</span>
);
}

function QuickActions({ onSelect, mobile }: { onSelect: (id: string) => void; mobile?: boolean }) {
return (
<section
Expand DownExpand Up@@ -888,34 +858,27 @@ function DetailDialog({ app, open, onClose }: { app: LauncherApp; open: boolean;
}

type ApplicationsLauncherWorkspaceProps = {
variant?: LauncherVariant;
query?: string;
onQueryChange?: (query: string) => void;
desktopComposerSlotId?: string;
showDetailPanel?: boolean;
className?: string;
};

export function ApplicationsLauncherWorkspace({
variant = "standalone",
query: controlledQuery,
onQueryChange,
desktopComposerSlotId,
showDetailPanel,
className,
}: ApplicationsLauncherWorkspaceProps) {
const [uncontrolledQuery, setUncontrolledQuery] = useState("");
const [localQuery, setLocalQuery] = useState("");
const [activeFilter, setActiveFilter] = useState<LauncherFilter>("all");
const isDashboardTools = variant === "dashboard-tools";
const [detailOpen, setDetailOpen] = useState(!isDashboardTools && showDetailPanel === true);
const copy = isDashboardTools ? dashboardToolsLauncherCopy : standaloneLauncherCopy;
const query = controlledQuery ?? uncontrolledQuery;
const [detailOpen, setDetailOpen] = useState(false);
const copy = toolsLauncherCopy;
const query = controlledQuery ?? localQuery;
const normalizedQuery = query.trim().toLowerCase();
const queryDerivedId = useMemo(() => initialToolId(query), [query]);
const [selection, setSelection] = useState(() => ({
queryKey: (controlledQuery ?? "").trim().toLowerCase(),
id: initialToolId(controlledQuery),
}));
const [selection, setSelection] = useState({
queryKey: normalizedQuery,
id: initialToolId(query),
});
const selectedId = selection.queryKey === normalizedQuery ? selection.id : queryDerivedId;

const filteredApps = useMemo(() => {
Expand All@@ -941,8 +904,7 @@ export function ApplicationsLauncherWorkspace({
const selectedApp = appById(effectiveSelectedId);

function updateQuery(nextQuery: string) {
if (controlledQuery === undefined) setUncontrolledQuery(nextQuery);
onQueryChange?.(nextQuery);
if (controlledQuery === undefined) setLocalQuery(nextQuery);
}

function openTool(id: string) {
Expand All@@ -956,26 +918,26 @@ export function ApplicationsLauncherWorkspace({

return (
<main
data-testid={isDashboardTools ? "tools-hub" : "applications-launcher"}
aria-labelledby={isDashboardTools ? "tools-home-heading" : "applications-launcher-heading"}
data-testid="tools-hub"
aria-labelledby="tools-home-heading"
className={cn(
"mx-auto w-full max-w-[90rem] overflow-x-hidden px-4 pb-8 text-[color:var(--text)] sm:px-6 lg:px-8",
"pb-[calc(12rem+env(safe-area-inset-bottom))] sm:pb-8",
isDashboardTools ? "pt-7 sm:pt-10 lg:pt-14" : "pt-8 sm:pt-10",
"pt-7 sm:pt-10 lg:pt-14",
className,
)}
>
<section
aria-label={isDashboardTools ? "Tools home" : "Applications home"}
data-testid={isDashboardTools ? "tools-home" : "applications-home"}
aria-label="Tools home"
data-testid="tools-home"
className="mx-auto grid max-w-5xl justify-items-center gap-5 text-center sm:gap-6"
>
<span className="grid h-14 w-14 place-items-center rounded-2xl border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)] sm:h-16 sm:w-16">
<Grid2X2 className="h-7 w-7 sm:h-8 sm:w-8" aria-hidden />
</span>
<div className="grid gap-2">
<h1
id={isDashboardTools ? "tools-home-heading" : "applications-launcher-heading"}
id="tools-home-heading"
className="text-balance text-[2rem] font-extrabold leading-none tracking-normal text-[color:var(--text-heading)] sm:text-[2.7rem]"
>
{copy.heading}
Expand All@@ -1000,7 +962,7 @@ export function ApplicationsLauncherWorkspace({
/>
)}

<div className="w-full max-w-6xl" data-testid={isDashboardTools ? "tools-shortcuts" : "application-shortcuts"}>
<div className="w-full max-w-6xl" data-testid="tools-shortcuts">
<div className="hidden sm:block">
<QuickActions onSelect={openTool} />
</div>
Expand All@@ -1012,7 +974,7 @@ export function ApplicationsLauncherWorkspace({

<section
aria-label={copy.allSectionLabel}
data-testid={isDashboardTools ? "tools-all-tools" : "applications-all-applications"}
data-testid="tools-all-tools"
className="mx-auto mt-8 grid max-w-[86rem] gap-4 sm:mt-10"
>
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
Expand DownExpand Up@@ -1055,15 +1017,13 @@ export function ApplicationsLauncherWorkspace({
</p>
</section>

{isDashboardTools ? (
<ModeHomeVerificationFooter icon={ShieldCheck} label="Clinical tools" body="Source-backed workflows" />
) : null}
<ModeHomeVerificationFooter icon={ShieldCheck} label="Clinical tools" body="Source-backed workflows" />

<DetailDialog app={selectedApp} open={detailOpen} onClose={() => setDetailOpen(false)} />
</main>
);
}

export function ApplicationsLauncherPage() {
return <ApplicationsLauncherWorkspace variant="standalone" />;
return <ApplicationsLauncherWorkspace />;
}
Loading
Loading