From 1cf2c763efb0f60a22edfa5b45c4f550c29e0466 Mon Sep 17 00:00:00 2001 From: HexLab <8422520+HexLab98@users.noreply.github.com> Date: Mon, 20 Jul 2026 06:09:12 +0700 Subject: [PATCH] fix(dashboard): opaque MoA presets modal (stop page bleed-through) (#67410) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(dashboard): make MoA presets modal opaque and readable Card defaults to bg-background-base/80 glass, so the Mixture of Agents dialog let the Models page bleed through — especially on Cyberpunk/mobile. Portal an opaque dialog shell above the z-2 dashboard column, and ignore Escape while the nested model picker is open. * test(web): lock dashboard modal shell to opaque panel classes Guard the MoA/dialog shell contract so glass Card defaults cannot quietly return to modal panels, and Escape stays picker-aware. --- web/src/lib/dashboard-modal-shell.test.ts | 23 +++++++++ web/src/lib/dashboard-modal-shell.ts | 29 +++++++++++ web/src/pages/ModelsPage.tsx | 63 +++++++++++++++++++---- 3 files changed, 104 insertions(+), 11 deletions(-) create mode 100644 web/src/lib/dashboard-modal-shell.test.ts create mode 100644 web/src/lib/dashboard-modal-shell.ts diff --git a/web/src/lib/dashboard-modal-shell.test.ts b/web/src/lib/dashboard-modal-shell.test.ts new file mode 100644 index 000000000..2f358ebd2 --- /dev/null +++ b/web/src/lib/dashboard-modal-shell.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { + DASHBOARD_MODAL_BACKDROP, + DASHBOARD_MODAL_PANEL, + shouldCloseOuterModalOnEscape, +} from "./dashboard-modal-shell"; + +describe("dashboard modal shell", () => { + it("uses an opaque panel (bg-card), not the glass Card default", () => { + expect(DASHBOARD_MODAL_PANEL).toMatch(/\bbg-card\b/); + expect(DASHBOARD_MODAL_PANEL).not.toMatch(/bg-background-base\/\d+/); + }); + + it("keeps the backdrop above page chrome (z-[100])", () => { + expect(DASHBOARD_MODAL_BACKDROP).toMatch(/z-\[100\]/); + expect(DASHBOARD_MODAL_BACKDROP).toMatch(/\bbg-background\/85\b/); + }); + + it("does not close the outer modal on Escape while a nested picker is open", () => { + expect(shouldCloseOuterModalOnEscape(true)).toBe(false); + expect(shouldCloseOuterModalOnEscape(false)).toBe(true); + }); +}); diff --git a/web/src/lib/dashboard-modal-shell.ts b/web/src/lib/dashboard-modal-shell.ts new file mode 100644 index 000000000..64cabbd84 --- /dev/null +++ b/web/src/lib/dashboard-modal-shell.ts @@ -0,0 +1,29 @@ +/** + * Shared dashboard dialog shell classes. + * + * Page `` defaults to `bg-background-base/80` (glass). That looks fine + * on the page canvas, but as a modal panel it lets the Models page bleed + * through and kills readability — especially on Cyberpunk / mobile. + * + * Modal panels must use opaque `bg-card`; backdrops use the same z-index + * band as Auxiliary / Confirm dialogs so they sit above page chrome. + * + * Callers must `createPortal(..., document.body)` — `z-[100]` alone cannot + * escape the dashboard column's `relative z-2` stacking context (see + * ModelPickerDialog / ToolsetConfigDrawer). + */ +export const DASHBOARD_MODAL_BACKDROP = + "fixed inset-0 z-[100] flex items-center justify-center bg-background/85 p-4"; + +export const DASHBOARD_MODAL_PANEL = + "relative w-full border border-border bg-card shadow-2xl"; + +/** + * Outer modals that host a nested picker (e.g. MoA → ModelPickerDialog) + * must ignore Escape while the picker is open; the picker owns that key. + */ +export function shouldCloseOuterModalOnEscape( + nestedPickerOpen: boolean, +): boolean { + return !nestedPickerOpen; +} diff --git a/web/src/pages/ModelsPage.tsx b/web/src/pages/ModelsPage.tsx index 504c8ddb5..e4fb338bc 100644 --- a/web/src/pages/ModelsPage.tsx +++ b/web/src/pages/ModelsPage.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useLayoutEffect, useState } from "react"; +import { createPortal } from "react-dom"; import { Brain, ChevronDown, @@ -22,6 +23,11 @@ import type { ModelsAnalyticsResponse, } from "@/lib/api"; import { timeAgo, cn, themedBody } from "@/lib/utils"; +import { + DASHBOARD_MODAL_BACKDROP, + DASHBOARD_MODAL_PANEL, + shouldCloseOuterModalOnEscape, +} from "@/lib/dashboard-modal-shell"; import { formatTokenCount } from "@/lib/format"; import { Button } from "@nous-research/ui/ui/components/button"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; @@ -711,6 +717,17 @@ function MoaModelsModal({ const [busy, setBusy] = useState(false); const [error, setError] = useState(null); + // Nested ModelPickerDialog owns Escape while open — don't dismiss MoA too. + const closeMoaUnlessPickerOpen = useCallback(() => { + if (!shouldCloseOuterModalOnEscape(picker !== null)) return; + onClose(); + }, [picker, onClose]); + + const modalRef = useModalBehavior({ + open: true, + onClose: closeMoaUnlessPickerOpen, + }); + const presetNames = Object.keys(draft.presets || {}); const preset = draft.presets[selected] || draft.presets[presetNames[0]]; const slotLabel = (slot: MoaModelSlot) => `${slot.provider || "(provider)"} · ${slot.model || "(model)"}`; @@ -778,13 +795,36 @@ function MoaModelsModal({ if (!preset) return null; - return ( -
- - - Configure Mixture of Agents presets - - + // Portal to document.body: the main dashboard column is `relative z-2`, + // which traps fixed descendants below the sidebar (same as ModelPickerDialog). + return createPortal( +
{ + if (e.target === e.currentTarget) closeMoaUnlessPickerOpen(); + }} + role="dialog" + aria-modal="true" + aria-labelledby="moa-modal-title" + > + {/* Opaque panel — do not use here; Card defaults to bg-background-base/80. */} +
+
+

+ Configure Mixture of Agents presets +

+
+

Presets appear as models under the Mixture of Agents provider. References produce perspectives; the aggregator is the acting model that answers and calls tools.

@@ -835,10 +875,10 @@ function MoaModelsModal({ {error &&
{error}
}
- +
- - +
+
{picker && ( setPicker(null)} /> )} -
+
, + document.body, ); }