diff --git a/scripts/check-maintainability-budgets.mjs b/scripts/check-maintainability-budgets.mjs index c6f08042..464df05f 100644 --- a/scripts/check-maintainability-budgets.mjs +++ b/scripts/check-maintainability-budgets.mjs @@ -2,7 +2,7 @@ import { readFileSync } from "node:fs"; const budgets = new Map([ - ["src/components/ClinicalDashboard.tsx", 4160], + ["src/components/ClinicalDashboard.tsx", 4140], ["src/lib/rag/rag.ts", 5030], ["src/components/DocumentViewer.tsx", 1734], ["supabase/functions/indexing-v3-agent/index.ts", 2191], diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 93e48b71..b35f9fce 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -31,9 +31,9 @@ import { useReducer, useRef, useState, - useSyncExternalStore, } from "react"; import { type DocumentDeleteResult } from "@/components/DocumentManagementActions"; +import { useUploadDesktopLayout } from "@/components/clinical-dashboard/use-upload-desktop-layout"; import { extractSafetyFindings } from "@/lib/clinical-safety"; import { resolveScrollBehavior } from "@/lib/scroll-behavior"; import { isLocalNoAuthMode, resolveClientDemoMode, resolveUploadReadOnlyMode } from "@/lib/client-env"; @@ -123,22 +123,6 @@ const FavouritesHub = dynamic( { ssr: false }, ); -const uploadDesktopMediaQuery = "(min-width: 1024px)"; - -function subscribeToUploadDesktopLayout(callback: () => void) { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {}; - const media = window.matchMedia(uploadDesktopMediaQuery); - media.addEventListener("change", callback); - return () => media.removeEventListener("change", callback); -} - -function getUploadDesktopLayoutSnapshot() { - return ( - typeof window !== "undefined" && - typeof window.matchMedia === "function" && - window.matchMedia(uploadDesktopMediaQuery).matches - ); -} const MedicationPrescribingWorkspace = dynamic( () => import("@/components/clinical-dashboard/medication-prescribing-workspace").then( @@ -570,11 +554,7 @@ export function ClinicalDashboard({ const [documentsDrawerMode, setDocumentsDrawerMode] = useState("library"); const [uploadDrawerOpen, setUploadDrawerOpen] = useState(false); const [uploadMobileTab, setUploadMobileTab] = useState("upload"); - const uploadUsesDesktopRegions = useSyncExternalStore( - subscribeToUploadDesktopLayout, - getUploadDesktopLayoutSnapshot, - () => false, - ); + const uploadUsesDesktopRegions = useUploadDesktopLayout(); const uploadTabRefs = useRef(new Map()); const [documentDrawerStatusFilter, setDocumentDrawerStatusFilter] = useState("indexed"); const [indexingMonitorFilter, setIndexingMonitorFilter] = useState("all"); diff --git a/src/components/clinical-dashboard/use-upload-desktop-layout.ts b/src/components/clinical-dashboard/use-upload-desktop-layout.ts new file mode 100644 index 00000000..1748d1de --- /dev/null +++ b/src/components/clinical-dashboard/use-upload-desktop-layout.ts @@ -0,0 +1,31 @@ +import { useSyncExternalStore } from "react"; + +// The upload view switches to a two-region desktop layout at >=1024px. This is a +// useSyncExternalStore-backed media-query subscription so the value stays correct +// across SSR (no window) and live viewport changes. Extracted from +// ClinicalDashboard.tsx (maturity X3) as a pure move. +const uploadDesktopMediaQuery = "(min-width: 1024px)"; + +function subscribeToUploadDesktopLayout(callback: () => void) { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => {}; + const media = window.matchMedia(uploadDesktopMediaQuery); + media.addEventListener("change", callback); + return () => media.removeEventListener("change", callback); +} + +function getUploadDesktopLayoutSnapshot() { + return ( + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia(uploadDesktopMediaQuery).matches + ); +} + +/** + * Whether the upload view has room for its desktop two-region layout (viewport >= 1024px). + * Server-safe: the getServerSnapshot returns false during SSR to avoid a hydration + * mismatch, and the value updates on viewport changes on the client. + */ +export function useUploadDesktopLayout() { + return useSyncExternalStore(subscribeToUploadDesktopLayout, getUploadDesktopLayoutSnapshot, () => false); +} diff --git a/tests/audit-navigation-auth-regressions.test.ts b/tests/audit-navigation-auth-regressions.test.ts index cd96f6f2..c9c79904 100644 --- a/tests/audit-navigation-auth-regressions.test.ts +++ b/tests/audit-navigation-auth-regressions.test.ts @@ -174,7 +174,12 @@ describe("audit navigation and auth regressions", () => { for (const section of ["setup", "upload", "indexing", "quality"]) { expect(clinicalDashboardSource).toContain(`id="dashboard-${section}-section-heading"`); } - expect(clinicalDashboardSource).toContain("subscribeToUploadDesktopLayout"); + // The viewport-driven region/tabpanel role is wired through the extracted hook, whose + // media-query subscription carries the guard with it. + expect(clinicalDashboardSource).toContain("useUploadDesktopLayout()"); + expect(source("src/components/clinical-dashboard/use-upload-desktop-layout.ts")).toContain( + "subscribeToUploadDesktopLayout", + ); expect(clinicalDashboardSource).toContain('event.key === "ArrowRight"'); expect(clinicalDashboardSource).toContain('event.key === "ArrowLeft"'); expect(clinicalDashboardSource).toContain('event.key === "Home"');