From 1f1e7ceacea80b5e444de1d9d741540ce3c4ebe3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 13:46:59 +0000 Subject: [PATCH 01/31] refactor(dashboard): cut over ToolsHub + MobileSectionFab to dashboard-nav module (move-only) The prepared dashboard-nav.tsx sibling from #250 had drifted from the live monolith (missing prop members, stale colour tokens), so it was regenerated verbatim from the current monolith block (1072-1436) before wiring. Exports: ToolsHub, buildMobileSectionFabState, MobileSectionFab; back-imports (ApplicationsLauncherWorkspace, navigationHashes, mobileSectionFabMediaQuery) follow the existing benign back-edge pattern. Monolith 4373 -> 4007 lines. Live-surface data-testid/aria-label corpus verified byte-identical pre/post. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01XPVNBVo4cg9PEYtNhJZBQY --- src/components/ClinicalDashboard.tsx | 368 +----------------- .../clinical-dashboard/dashboard-nav.tsx | 10 +- 2 files changed, 6 insertions(+), 372 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 19de0de082..f312508ee0 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -42,7 +42,6 @@ import { } from "lucide-react"; import { type CSSProperties, type FormEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; -import { useDismissableLayer } from "@/components/use-dismissable-layer"; import { extractSafetyFindings } from "@/lib/clinical-safety"; import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity"; import { isDeployedClinicalKb } from "@/lib/deployed-app"; @@ -65,6 +64,7 @@ import { AccountSetupDialog } from "@/components/clinical-dashboard/account-setu import { StagedAnswerResultSurface } from "@/components/clinical-dashboard/answer-result-surface"; import { RelatedDocumentsPanel } from "@/components/clinical-dashboard/document-results"; import { AuthPanel } from "@/components/clinical-dashboard/auth-panel"; +import { buildMobileSectionFabState, MobileSectionFab, ToolsHub } from "@/components/clinical-dashboard/dashboard-nav"; import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed"; import { useTheme } from "@/components/clinical-dashboard/use-theme"; import { @@ -1069,372 +1069,6 @@ function SettingsHelpFooter({ onClick }: { onClick: () => void }) { ); } -function ToolsHub({ query, desktopComposerSlotId }: { query: string; desktopComposerSlotId?: string }) { - return ; -} - -type MobileSectionFabItem = { - label: string; - description: string; - icon: typeof FileText; - href: (typeof navigationHashes)[number]; - count: number | null; - empty?: boolean; -}; - -type MobileSectionFabTone = "neutral" | "ready" | "warning" | "empty"; - -type MobileSectionFabState = { - statusLabel: string; - statusTone: MobileSectionFabTone; - nextStep: string; - badgeLabel: string | null; - badgeTone: MobileSectionFabTone; -}; - -function mobileSectionItemLabel(item: MobileSectionFabItem) { - if (item.count === null) return item.label; - return `${item.label}, ${item.count} item${item.count === 1 ? "" : "s"}`; -} - -function fabToneClassName(tone: MobileSectionFabTone) { - if (tone === "ready") { - return "border-[color:var(--success)]/25 bg-[color:var(--success-soft)] text-[color:var(--success)]"; - } - if (tone === "warning") { - return "border-[color:var(--warning)]/25 bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; - } - if (tone === "empty") { - return "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]"; - } - return "border-[color:var(--clinical-accent)]/20 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; -} - -function buildMobileSectionFabState({ - hasAnswer, - searchMode, - sourceCount, - quoteCount, - weakEvidence, - governanceWarningCount, -}: { - hasAnswer: boolean; - searchMode: AppModeId; - sourceCount: number; - quoteCount: number; - weakEvidence: boolean; - governanceWarningCount: number; -}): MobileSectionFabState { - const modeSearch = appModeSearchConfig(searchMode); - if (!hasAnswer) { - if (modeSearch.resultKind === "tools") { - return { - statusLabel: "Tools", - statusTone: "neutral", - nextStep: "Launch a clinical tool", - badgeLabel: null, - badgeTone: "neutral", - }; - } - if (modeSearch.resultKind === "differentials") { - return { - statusLabel: "Diffs", - statusTone: "neutral", - nextStep: modeSearch.nextStep, - badgeLabel: null, - badgeTone: "neutral", - }; - } - return { - statusLabel: - modeSearch.resultKind === "documents" || modeSearch.resultKind === "forms" - ? modeSearch.statusLabel - : "No answer yet", - statusTone: "empty", - nextStep: modeSearch.nextStep, - badgeLabel: modeSearch.badgeLabel, - badgeTone: "empty", - }; - } - - if (weakEvidence) { - return { - statusLabel: "Weak support", - statusTone: "warning", - nextStep: "Verify source before using", - badgeLabel: "!", - badgeTone: "warning", - }; - } - - if (governanceWarningCount > 0) { - return { - statusLabel: "Needs source check", - statusTone: "warning", - nextStep: `${governanceWarningCount} source warning${governanceWarningCount === 1 ? "" : "s"}`, - badgeLabel: "!", - badgeTone: "warning", - }; - } - - if (quoteCount > 0) { - return { - statusLabel: "Ready to verify", - statusTone: "ready", - nextStep: "Next: review exact quotes", - badgeLabel: String(quoteCount), - badgeTone: "ready", - }; - } - - if (sourceCount > 0) { - return { - statusLabel: "Ready to verify", - statusTone: "ready", - nextStep: "Next: verify sources", - badgeLabel: String(sourceCount), - badgeTone: "ready", - }; - } - - return { - statusLabel: "Answer ready", - statusTone: "neutral", - nextStep: "Review answer structure", - badgeLabel: null, - badgeTone: "neutral", - }; -} - -function MobileSectionFab({ - items, - activeHash, - state, - hidden = false, - onNavigate, -}: { - items: readonly MobileSectionFabItem[]; - activeHash: string; - state: MobileSectionFabState; - hidden?: boolean; - onNavigate: (href: MobileSectionFabItem["href"]) => void; -}) { - const [open, setOpen] = useState(false); - const [active, setActive] = useState(false); - const buttonRef = useRef(null); - const panelRef = useRef(null); - const panelId = "mobile-section-fab-menu"; - const labelId = "mobile-section-fab-label"; - const activeItem = items.find((item) => item.href === activeHash) ?? items[0]; - const ActiveIcon = activeItem.icon; - const activeItemLabel = mobileSectionItemLabel(activeItem); - - const closeMenu = useCallback((options: { restoreFocus?: boolean } = {}) => { - setOpen(false); - if (options.restoreFocus ?? true) { - window.requestAnimationFrame(() => buttonRef.current?.focus()); - } - }, []); - const dismissMobileSectionMenu = useCallback(() => closeMenu(), [closeMenu]); - - useDismissableLayer({ - enabled: open, - refs: [buttonRef, panelRef], - restoreFocusRef: buttonRef, - onDismiss: dismissMobileSectionMenu, - }); - - useEffect(() => { - const mediaQuery = window.matchMedia(mobileSectionFabMediaQuery); - const syncActivation = () => { - const matches = mediaQuery.matches; - setActive(matches); - if (!matches) closeMenu({ restoreFocus: false }); - }; - - const frame = window.requestAnimationFrame(syncActivation); - mediaQuery.addEventListener("change", syncActivation); - return () => { - window.cancelAnimationFrame(frame); - mediaQuery.removeEventListener("change", syncActivation); - }; - }, [closeMenu]); - - useEffect(() => { - if (!open) return; - const closeForRouteChange = () => closeMenu({ restoreFocus: false }); - window.addEventListener("hashchange", closeForRouteChange); - return () => window.removeEventListener("hashchange", closeForRouteChange); - }, [closeMenu, open]); - - useEffect(() => { - if (!hidden) return; - const frame = window.requestAnimationFrame(() => closeMenu({ restoreFocus: false })); - return () => window.cancelAnimationFrame(frame); - }, [closeMenu, hidden]); - - if (hidden || !active) return null; - - return ( -
- {open ? ( - - ); -} - function answerReferencesDocument(answer: RagAnswer | null, documentId: string) { if (!answer) return false; return ( diff --git a/src/components/clinical-dashboard/dashboard-nav.tsx b/src/components/clinical-dashboard/dashboard-nav.tsx index 3a34cbc91a..434278a049 100644 --- a/src/components/clinical-dashboard/dashboard-nav.tsx +++ b/src/components/clinical-dashboard/dashboard-nav.tsx @@ -50,7 +50,7 @@ function fabToneClassName(tone: MobileSectionFabTone) { if (tone === "empty") { return "border-[color:var(--border)] bg-[color:var(--surface-subtle)] text-[color:var(--text-muted)]"; } - return "border-[color:var(--primary)]/20 bg-[color:var(--primary-soft)] text-[color:var(--primary-strong)]"; + return "border-[color:var(--clinical-accent)]/20 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; } export function buildMobileSectionFabState({ @@ -330,14 +330,14 @@ export function MobileSectionFab({ "relative grid min-h-[58px] grid-cols-[38px_minmax(0,1fr)_auto] items-center gap-2 rounded-lg border border-transparent py-1.5 pl-3 pr-2 text-sm font-semibold text-[color:var(--text-muted)] transition hover:border-[color:var(--border)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]", item.empty && !active && "opacity-75", active && - "border-[color:var(--primary)]/25 bg-[color:var(--primary-soft)] text-[color:var(--primary-strong)] shadow-[var(--shadow-inset)]", + "border-[color:var(--clinical-accent)]/25 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)] shadow-[var(--shadow-inset)]", )} >
@@ -237,7 +251,7 @@ function MedicationRecordDetail({ record }: { record: MedicationRecord }) { -