From 7b9866c075f54f194d9b7f12f38ac3a37cfb0c66 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:30:03 +0800 Subject: [PATCH 1/4] chore(ui): implement Phase 2 ClinicalDashboard container scaling and state extraction --- eslint.config.mjs | 11 +++ src/components/ClinicalDashboard.tsx | 92 ++++++++++--------- .../global-search-shell.tsx | 15 +-- .../clinical-dashboard/image-lightbox.tsx | 2 +- .../clinical-dashboard/mode-action-popup.tsx | 2 +- .../clinical-dashboard/signed-image.tsx | 2 +- .../document-viewer/pdf-canvas-viewer.tsx | 2 +- src/components/privacy-input-notice.tsx | 2 +- src/components/ui-primitives.tsx | 9 +- 9 files changed, 78 insertions(+), 59 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 0a67d4ed7a..9a2539d8a2 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,6 +4,7 @@ import nextTs from "eslint-config-next/typescript"; import requireLucideIconAria from "./eslint-rules/require-lucide-icon-aria.mjs"; import requireButtonWiring from "./eslint-rules/require-button-wiring.mjs"; +import requireZIndexLadder from "./eslint-rules/require-z-index-ladder.mjs"; // Shared `local` plugin object. ESLint flat config requires every config block // that references a plugin namespace to point at the *same* object, so the two @@ -12,6 +13,7 @@ const localRulesPlugin = { rules: { "require-lucide-icon-aria": requireLucideIconAria, "require-button-wiring": requireButtonWiring, + "require-z-index-ladder": requireZIndexLadder, }, }; @@ -66,6 +68,15 @@ const eslintConfig = defineConfig([ "local/require-button-wiring": "error", }, }, + // Z-index ladder enforcement + { + files: ["src/**/*.{js,jsx,ts,tsx}"], + ignores: MOCKUP_IGNORES, + plugins: { local: localRulesPlugin }, + rules: { + "local/require-z-index-ladder": "error", + }, + }, // Import boundary: production source must not import design-scratch mockup // modules. Every legitimate mockup import lives under `src/app/mockups/**` (all // 404 in production) or inside the `*-mockups` component sources themselves; diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 0a15b3ec26..13d29b6a1a 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -1,3 +1,4 @@ +import { useSettingsState } from "./clinical-dashboard/SettingsStateProvider"; "use client"; import { useRouter, useSearchParams } from "next/navigation"; @@ -545,16 +546,18 @@ export function ClinicalDashboard({ const [pendingFeedback, setPendingFeedback] = useState(null); const [actionNotice, setActionNotice] = useState<{ tone: "success" | "warning"; message: string } | null>(null); const [activeHash, setActiveHash] = useState("#search"); - const [guideOpen, setGuideOpen] = useState(false); - const [settingsOpen, setSettingsOpen] = useState(false); - const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); - const [sidebarCollapsed, setSidebarCollapsed] = useSidebarCollapsed(); - const [documentsDrawerOpen, setDocumentsDrawerOpen] = useState(false); - const [documentScopeOpen, setDocumentScopeOpen] = useState(false); - const [documentsDrawerMode, setDocumentsDrawerMode] = useState("library"); - const [uploadDrawerOpen, setUploadDrawerOpen] = useState(false); - const [uploadMobileTab, setUploadMobileTab] = useState("upload"); - const uploadUsesDesktopRegions = useUploadDesktopLayout(); + const { + guideOpen, setGuideOpen, + settingsOpen, setSettingsOpen, + mobileSidebarOpen, setMobileSidebarOpen, + sidebarCollapsed, setSidebarCollapsed, + documentsDrawerOpen, setDocumentsDrawerOpen, + documentScopeOpen, setDocumentScopeOpen, + documentsDrawerMode, setDocumentsDrawerMode, + uploadDrawerOpen, setUploadDrawerOpen, + uploadMobileTab, setUploadMobileTab + } = useSettingsState(); + const uploadUsesDesktopRegions = useUploadDesktopLayout(); const uploadTabRefs = useRef(new Map()); const [documentDrawerStatusFilter, setDocumentDrawerStatusFilter] = useState("indexed"); const [indexingMonitorFilter, setIndexingMonitorFilter] = useState("all"); @@ -3313,10 +3316,9 @@ export function ClinicalDashboard({
-
+
@@ -3466,7 +3468,7 @@ export function ClinicalDashboard({ role="alert" data-testid="private-scope-unavailable" className={cn( - "sticky z-20 mx-3 mt-3 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-sm text-[color:var(--text)] sm:mx-4 lg:mx-8", + "sticky z-20 mx-3 mt-3 flex flex-wrap items-center justify-between gap-3 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-sm text-[color:var(--text)] @sm:mx-4 @lg:mx-8", searchMode === "answer" ? "top-[calc(4.5rem+max(0.5rem,env(safe-area-inset-top)))]" : "top-2", )} > @@ -3490,8 +3492,8 @@ export function ClinicalDashboard({ // overflow-x-CLIP, not -hidden: hidden makes this wrapper a scroll // container (overflow-y computes to auto), which clips the composer's // command dropdown mid-panel and shows a phantom inner scrollbar. - "mx-auto max-w-7xl space-y-4 overflow-x-clip px-3 py-4 sm:space-y-5 sm:px-4 sm:py-5 lg:px-8", - compactMobileModeHome && "max-sm:px-0", + "mx-auto max-w-7xl space-y-4 overflow-x-clip px-3 py-4 @sm:space-y-5 @sm:px-4 @sm:py-5 @lg:px-8", + compactMobileModeHome && "@max-sm:px-0", // Centred mode homes carry little content, so drop the large // mobile bottom padding (the fixed composer already has its own // reserved margin on
) to avoid a needless scrollbar. @@ -3504,14 +3506,14 @@ export function ClinicalDashboard({ // long answer's last line high above the dock (and padded a short // answer's empty space further). Keep it small here; sm+/desktop // keep the original generous padding. - "pb-4 sm:pb-36 lg:pb-40" + "pb-4 @sm:pb-36 @lg:pb-40" : hasMobileBottomSearch ? // The
reserve clears the compact dock on phones, so // content keeps only a small pad of its own. compactMobileModeHome - ? "pb-4 sm:pb-10 lg:pb-12" - : "pb-8 sm:pb-10 lg:pb-12" - : "pb-8 sm:pb-10 lg:pb-12", + ? "pb-4 @sm:pb-10 @lg:pb-12" + : "pb-8 @sm:pb-10 @lg:pb-12" + : "pb-8 @sm:pb-10 @lg:pb-12", )} > {actionNotice && ( @@ -3520,7 +3522,7 @@ export function ClinicalDashboard({ )} {showDegradedNotice && renderDegradedNotice()} - {showSystemNotice && answer ? renderSystemNotice("hidden sm:block") : null} + {showSystemNotice && answer ? renderSystemNotice("hidden @sm:block") : null}
- {showSystemNotice && answer ? renderSystemNotice("sm:hidden") : null} + {showSystemNotice && answer ? renderSystemNotice("@sm:hidden") : null} {activeModeResultKind === "answer" && answer && ( )} {(documentsDrawerOpen || uploadDrawerOpen) && ( -
+

{drawerGroupTitle}

@@ -3877,16 +3879,16 @@ export function ClinicalDashboard({ } sheetTitleAccessory={ documentsDrawerIsAdmin ? ( - + {indexedDocumentTotal.toLocaleString()} indexed ) : null } sheetDescription={documentsDrawerSummary} - sheetHeaderClassName="bg-[color:var(--surface-raised)] px-4 py-3 sm:px-5 sm:py-4" + sheetHeaderClassName="bg-[color:var(--surface-raised)] px-4 py-3 @sm:px-5 @sm:py-4" sheetCloseButtonClassName="grid h-tap w-tap shrink-0 place-items-center rounded-xl border border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" - sheetContentClassName="max-h-[min(82dvh,40rem)] sm:max-h-[min(88dvh,46rem)] sm:max-w-2xl lg:max-w-3xl" - sheetBodyClassName="bg-[color:var(--surface-subtle)] p-3 sm:p-4" + sheetContentClassName="max-h-[min(82dvh,40rem)] @sm:max-h-[min(88dvh,46rem)] @sm:max-w-2xl @lg:max-w-3xl" + sheetBodyClassName="bg-[color:var(--surface-subtle)] p-3 @sm:p-4" sheetChildrenClassName="space-y-3" > {documentsDrawerIsAdmin ? ( @@ -3944,7 +3946,7 @@ export function ClinicalDashboard({ role="tablist" aria-label="Upload and indexing sections" onKeyDown={handleUploadTabKeyDown} - className="grid grid-cols-4 gap-2 lg:hidden" + className="grid grid-cols-4 gap-2 @lg:hidden" > {uploadTabs.map((tab) => { const active = uploadMobileTab === tab.id; @@ -3986,7 +3988,7 @@ export function ClinicalDashboard({ ); })}
-
+

{rendersClinicalDashboard ? ( - + + + ) : ( )} diff --git a/src/components/clinical-dashboard/image-lightbox.tsx b/src/components/clinical-dashboard/image-lightbox.tsx index 69c3e639bf..bb7e8a47da 100644 --- a/src/components/clinical-dashboard/image-lightbox.tsx +++ b/src/components/clinical-dashboard/image-lightbox.tsx @@ -144,7 +144,7 @@ export function ImageLightbox({ {/* Control bar. stopPropagation keeps button taps from starting a pan/pinch on the stage. */}

event.stopPropagation()} >
diff --git a/src/components/clinical-dashboard/mode-action-popup.tsx b/src/components/clinical-dashboard/mode-action-popup.tsx index dfb1e872fa..2ced0e6e21 100644 --- a/src/components/clinical-dashboard/mode-action-popup.tsx +++ b/src/components/clinical-dashboard/mode-action-popup.tsx @@ -818,7 +818,7 @@ export function ModeActionPopup({ // subtitle sits on its own full-width line so it can never be crushed. function renderPopoverHeader() { return ( -
+