From 803b98de36f252a9eda73f5833bff832e179fb20 Mon Sep 17 00:00:00 2001 From: mptyl Date: Sun, 19 Jul 2026 17:08:20 +0200 Subject: [PATCH] fix(frontend): center gate dialogs on the app area, not the browser viewport MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Embedded in the Omics Portal the SPA sits to the right of the portal sidebar, but dialogs used `fixed left-1/2` — centered on the whole browser window, so gates rendered visibly off-center (overlapping the sidebar). - AppShell publishes the app area's horizontal geometry (--app-area-left/width) as CSS vars on via ResizeObserver + window resize. - DialogContent centers on those vars (falls back to the viewport when the app is standalone, where the two coincide). - ArtifactGateWidget: narrower — cap at 52rem instead of 70vw. - tools/replay: resume now returns {id, alreadyActive} JSON — the post-f979ada frontend reads result.alreadyActive and a bare 204 broke replay resume. Verified on the replay harness: with a simulated 300px portal sidebar the gate centers on the app area (center 790 = 300 + 980/2); standalone unchanged. Co-Authored-By: Claude Fable 5 --- frontend/src/components/ui/dialog.tsx | 4 +++- .../src/shell/AppShell.session-mgmt.test.tsx | 4 +++- frontend/src/shell/AppShell.tsx | 23 +++++++++++++++++++ frontend/src/widgets/ArtifactGateWidget.tsx | 2 +- tools/replay/server.mjs | 4 +++- 5 files changed, 33 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/ui/dialog.tsx b/frontend/src/components/ui/dialog.tsx index b25b556b..62162b80 100644 --- a/frontend/src/components/ui/dialog.tsx +++ b/frontend/src/components/ui/dialog.tsx @@ -53,7 +53,9 @@ function DialogContent({ ControlledResizeObserver.instances.at(-1)?.trigger()); + // AppShell registers more than one observer (activity split + app-area CSS vars): + // trigger them all so the layout sync isn't skipped. + act(() => ControlledResizeObserver.instances.forEach((o) => o.trigger())); } function deferred() { diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 4921ba57..68a83a9e 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -34,6 +34,29 @@ import type { CSSProperties } from "react"; */ export function AppShell() { const { containerRef, panelWidth, desktopSplit, resizing, separatorProps } = useActivityPanelResize(); + // Publish the app area's horizontal geometry as CSS vars on : when the app + // is embedded beside the portal's left sidebar, viewport-fixed dialogs (dialog.tsx) + // must center on the ThothII area, not on the whole browser window. + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const doc = document.documentElement; + const publish = () => { + const r = el.getBoundingClientRect(); + doc.style.setProperty("--app-area-left", `${r.left}px`); + doc.style.setProperty("--app-area-width", `${r.width}px`); + }; + publish(); + const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(publish); + observer?.observe(el); + window.addEventListener("resize", publish); + return () => { + observer?.disconnect(); + window.removeEventListener("resize", publish); + doc.style.removeProperty("--app-area-left"); + doc.style.removeProperty("--app-area-width"); + }; + }, [containerRef]); const activityWidthStyle = { "--activity-panel-width": `${panelWidth}px`, } as CSSProperties; diff --git a/frontend/src/widgets/ArtifactGateWidget.tsx b/frontend/src/widgets/ArtifactGateWidget.tsx index cdc78fd7..110bccf1 100644 --- a/frontend/src/widgets/ArtifactGateWidget.tsx +++ b/frontend/src/widgets/ArtifactGateWidget.tsx @@ -30,7 +30,7 @@ export function ArtifactGateWidget({ descriptor, onRespond, sessionId }: WidgetP {descriptor.title ?? "Artifact review"} diff --git a/tools/replay/server.mjs b/tools/replay/server.mjs index 2a68fb12..b7d44c56 100644 --- a/tools/replay/server.mjs +++ b/tools/replay/server.mjs @@ -397,7 +397,9 @@ const server = createServer(async (req, res) => { } else { console.error("[resume] reset cursore a 0 (nessun client SSE attivo)"); } - return sendNoContent(res); + // The frontend's runResume reads result.alreadyActive (post-f979ada contract): + // a bare 204 makes it throw and toast "Failed to resume session". + return sendJson(res, 200, { id: REPLAY_SESSION_ID, alreadyActive: Boolean(sseClient) }); } if (method === "POST" && /^\/sessions\/[^/]+\/(close|steer|rename|group|archive|unarchive)$/.test(path)) { return sendNoContent(res);