practice: the metadata box rides a body portal — never clipped again (round 17 fix)

Diagnosed: the transcript's MessageContent boxes carry overflow-hidden
(ai-elements/message.tsx) — the absolutely-positioned box rendered fine
over its own message block's content (the closing-words text under the
conclude card) and clipped the moment it crossed that block's bottom
edge onto the next message. Exactly the reported cut-off.

Fix: the box portals to document.body in fixed position (the dialogs'
law) — placed from the trigger's viewport rect at open time, below the
trigger, left-clamped to the viewport; z-50. Any-scroll-close means
there is nothing to track.
This commit is contained in:
amosblomqvist
2026-10-06 04:31:48 +03:00
parent 679188438f
commit cbbe1da825
+37 -10
View File
@@ -21,6 +21,7 @@ import type { ArtifactPartProps } from "@/src/core";
*/ */
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { ThinkingBlock } from "@/components/thinking-block"; import { ThinkingBlock } from "@/components/thinking-block";
import { usePopoverDismiss } from "@/components/popover-dismiss"; import { usePopoverDismiss } from "@/components/popover-dismiss";
@@ -177,7 +178,12 @@ function MetadataPopover({
problemInput: { mode?: unknown; target?: unknown }; problemInput: { mode?: unknown; target?: unknown };
}) { }) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const rootRef = useRef<HTMLDivElement | null>(null); const rootRef = useRef<HTMLDivElement | null>(null);
// The portal's anchor (owner fix, 2026-10-06): the box is placed from
// the trigger's viewport rect AT OPEN TIME — any scroll closes the
// box, so there is nothing to track.
const [anchor, setAnchor] = useState<{ top: number; left: number } | null>(null);
const [names, setNames] = useState<Record<string, string> | null>(null); const [names, setNames] = useState<Record<string, string> | null>(null);
const targets = Array.isArray(problemInput.target) const targets = Array.isArray(problemInput.target)
? problemInput.target.filter((t): t is string => typeof t === "string") ? problemInput.target.filter((t): t is string => typeof t === "string")
@@ -216,19 +222,39 @@ function MetadataPopover({
<button <button
type="button" type="button"
data-testid="metadata-toggle" data-testid="metadata-toggle"
onClick={() => setOpen((v) => !v)} ref={triggerRef}
onClick={() => {
setOpen((v) => {
if (v) return false;
const r = triggerRef.current?.getBoundingClientRect();
if (r) {
// Below the trigger, always; clamped so the 320px box
// never leaves the viewport's right edge.
const left = Math.max(8, Math.min(r.left, window.innerWidth - 328));
setAnchor({ top: r.bottom + 8, left });
}
return true;
});
}}
aria-expanded={open} aria-expanded={open}
className="flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground" className="flex items-center gap-1 text-xs font-medium uppercase tracking-wide text-muted-foreground transition-colors hover:text-foreground"
> >
Metadata Metadata
</button> </button>
{open && ( {open &&
<div anchor &&
data-testid="metadata-box" createPortal(
// The node-card info-box register (graph view), floating above <div
// the trigger — never clipped by the transcript scroller. data-testid="metadata-box"
className="absolute top-full left-0 z-20 mt-2 w-80 max-w-[calc(100%-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg" // PORTALED TO BODY (owner fix, 2026-10-06): the transcript's
> // MessageContent boxes carry overflow-hidden — an absolutely
// positioned child clips at its own message block's edge (the
// cut-off bug). A fixed-position portal sits on top of
// EVERYTHING, the dialogs' law. Node-card info-box register;
// opening BELOW the trigger — always.
style={{ position: "fixed", top: anchor.top, left: anchor.left }}
className="z-50 w-80 max-w-[calc(100vw-1rem)] rounded-xl border bg-card p-4 text-left shadow-lg"
>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs"> <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-xs">
<dt className="text-muted-foreground">Mode</dt> <dt className="text-muted-foreground">Mode</dt>
<dd className="font-mono">{mode || "—"}</dd> <dd className="font-mono">{mode || "—"}</dd>
@@ -256,8 +282,9 @@ function MetadataPopover({
<dt className="text-muted-foreground">Retest</dt> <dt className="text-muted-foreground">Retest</dt>
<dd className="font-mono">{retest === undefined ? "—" : retest ? "yes" : "no"}</dd> <dd className="font-mono">{retest === undefined ? "—" : retest ? "yes" : "no"}</dd>
</dl> </dl>
</div> </div>,
)} document.body,
)}
</div> </div>
); );
} }