fix(frontend): center gate dialogs on the app area, not the browser viewport
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 <html> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -53,7 +53,9 @@ function DialogContent({
|
|||||||
<DialogPrimitive.Popup
|
<DialogPrimitive.Popup
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
// Horizontal center follows the app area (CSS vars published by AppShell),
|
||||||
|
// so embedded-in-portal dialogs center on the ThothII pane, not the browser.
|
||||||
|
"fixed top-1/2 left-[calc(var(--app-area-left,0px)+var(--app-area-width,100vw)/2)] z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-2xl bg-popover p-4 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ class ControlledResizeObserver {
|
|||||||
|
|
||||||
function setContainerWidth(element: HTMLElement, width: number) {
|
function setContainerWidth(element: HTMLElement, width: number) {
|
||||||
Object.defineProperty(element, "clientWidth", { configurable: true, value: width });
|
Object.defineProperty(element, "clientWidth", { configurable: true, value: width });
|
||||||
act(() => 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() {
|
function deferred() {
|
||||||
|
|||||||
@@ -34,6 +34,29 @@ import type { CSSProperties } from "react";
|
|||||||
*/
|
*/
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { containerRef, panelWidth, desktopSplit, resizing, separatorProps } = useActivityPanelResize();
|
const { containerRef, panelWidth, desktopSplit, resizing, separatorProps } = useActivityPanelResize();
|
||||||
|
// Publish the app area's horizontal geometry as CSS vars on <html>: 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 = {
|
const activityWidthStyle = {
|
||||||
"--activity-panel-width": `${panelWidth}px`,
|
"--activity-panel-width": `${panelWidth}px`,
|
||||||
} as CSSProperties;
|
} as CSSProperties;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export function ArtifactGateWidget({ descriptor, onRespond, sessionId }: WidgetP
|
|||||||
<Dialog open>
|
<Dialog open>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
showCloseButton={false}
|
showCloseButton={false}
|
||||||
className="grid h-[70vh] w-[70vw] max-w-[70vw] grid-rows-[auto_1fr_auto] gap-3.5 sm:max-w-[70vw]"
|
className="grid h-[70vh] w-[min(52rem,calc(var(--app-area-width,100vw)-3rem))] max-w-none grid-rows-[auto_1fr_auto] gap-3.5 sm:max-w-none"
|
||||||
>
|
>
|
||||||
<DialogTitle className="text-lg leading-snug">
|
<DialogTitle className="text-lg leading-snug">
|
||||||
{descriptor.title ?? "Artifact review"}
|
{descriptor.title ?? "Artifact review"}
|
||||||
|
|||||||
@@ -397,7 +397,9 @@ const server = createServer(async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
console.error("[resume] reset cursore a 0 (nessun client SSE attivo)");
|
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)) {
|
if (method === "POST" && /^\/sessions\/[^/]+\/(close|steer|rename|group|archive|unarchive)$/.test(path)) {
|
||||||
return sendNoContent(res);
|
return sendNoContent(res);
|
||||||
|
|||||||
Reference in New Issue
Block a user