diff --git a/components/practice-cards.tsx b/components/practice-cards.tsx index 7fd7c3b..7c686af 100644 --- a/components/practice-cards.tsx +++ b/components/practice-cards.tsx @@ -21,9 +21,10 @@ import type { ArtifactPartProps } from "@/src/core"; */ import { ChevronDownIcon } from "lucide-react"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { ThinkingBlock } from "@/components/thinking-block"; +import { usePopoverDismiss } from "@/components/popover-dismiss"; /* TARGET NAMES (owner round 17, 2026-10-06): targets are normalized to * opaque node ids at the record seam (owner round 9 — the mastery trace @@ -116,7 +117,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) { } return ( -
+
{/* The heading row (owner round 3, 2026-10-05): plain 'Concluded' eyebrow + the outcome in the quiz cards' EXACT chip register — no more two same-weight headings stacked. */} @@ -148,7 +149,7 @@ export function ConcludeCard({ part }: ArtifactPartProps) {
)} - @@ -156,13 +157,19 @@ export function ConcludeCard({ part }: ArtifactPartProps) { ); } -/* THE AGENT'S DATA DRAWER (owner round 17, 2026-10-06 — full - * transparency): a collapsible at the very bottom of the conclude card, - * arriving collapsed, showing every field the agent filled in both the - * problem call (mode, targets) and the conclude call (outcome, rung, - * miss class, retest). Statement and modelSolution stay out — they are - * the visible card above. Pure rows, the eyebrow register, mono values. */ -function AgentsDataDrawer({ +/* THE METADATA POPOVER (owner round 17 refinement, 2026-10-06 — full + * transparency): a quiet METADATA trigger at the very bottom of the + * conclude card (no divider — it just sits there); click opens the + * info-box register of the graph-view node cards (rounded-xl border + * bg-card p-4 shadow-lg, floating ABOVE the trigger); any scroll or + * click outside closes it (plus Esc, claimed from the layered chain + * per the popover law). Shows every field the agent filled in both the + * problem call (mode, targets — names resolved lazily) and the + * conclude call (outcome, rung, miss class, retest). Statement and + * modelSolution stay out — they are the visible card. The VALUES match + * the model solution's font size (text-[15px], owner-verified law); + * labels keep the eyebrow register. */ +function MetadataPopover({ concludeInput, problemInput, }: { @@ -170,6 +177,7 @@ function AgentsDataDrawer({ problemInput: { mode?: unknown; target?: unknown }; }) { const [open, setOpen] = useState(false); + const rootRef = useRef(null); const [names, setNames] = useState | null>(null); const targets = Array.isArray(problemInput.target) ? problemInput.target.filter((t): t is string => typeof t === "string") @@ -186,6 +194,16 @@ function AgentsDataDrawer({ }; }, [open, targets.length, names]); + usePopoverDismiss(rootRef, open, () => setOpen(false)); + // ANY scroll closes it (owner law): capture-phase, so every scroller + // in the tree counts — the transcript's own scroll included. + useEffect(() => { + if (!open) return; + const close = () => setOpen(false); + window.addEventListener("scroll", close, { capture: true, passive: true }); + return () => window.removeEventListener("scroll", close, { capture: true }); + }, [open]); + const mode = typeof problemInput.mode === "string" ? problemInput.mode : ""; const rung = typeof concludeInput.rung === "string" ? concludeInput.rung : ""; const miss = typeof concludeInput.missClass === "string" ? concludeInput.missClass : ""; @@ -194,25 +212,29 @@ function AgentsDataDrawer({ if (!mode && targets.length === 0 && !rung && !miss && retest === undefined) return null; return ( - <> -
-
- + {open && ( +
- - Agent’s data - - {open && ( -
-
Mode
-
{mode || "—"}
-
Targets
-
+
+
Mode
+
{mode || "—"}
+
Targets
+
{targets.length === 0 ? ( "untagged" ) : ( @@ -226,18 +248,18 @@ function AgentsDataDrawer({ )}
-
Outcome
-
{outcome || "—"}
-
Hint rung
-
{rung || "none"}
-
Miss class
-
{miss || "none"}
-
Retest
-
{retest === undefined ? "—" : retest ? "yes" : "no"}
+
Outcome
+
{outcome || "—"}
+
Hint rung
+
{rung || "none"}
+
Miss class
+
{miss || "none"}
+
Retest
+
{retest === undefined ? "—" : retest ? "yes" : "no"}
- )} -
- +
+ )} +
); } diff --git a/tests/render-practice-surface.test.tsx b/tests/render-practice-surface.test.tsx index b2546da..1ee4123 100644 --- a/tests/render-practice-surface.test.tsx +++ b/tests/render-practice-surface.test.tsx @@ -137,8 +137,8 @@ describe("the practice transcript cards", () => { assert.match(html, /Pair them; each pair is even\./); }); - it("ConcludeCard: THE AGENT'S DATA DRAWER (owner round 17) — arrives collapsed, opens to every field both calls filled", () => { - const collapsed = renderToStaticMarkup( + it("ConcludeCard: THE METADATA POPOVER (owner round 17 refinement) — quiet trigger at the bottom, box hidden until clicked", () => { + const html = renderToStaticMarkup( { })} />, ); - assert.ok(collapsed.includes('data-testid="agents-data-toggle"'), "the toggle row"); - assert.ok(collapsed.includes("Agent"), "the label"); - assert.ok(!collapsed.includes('data-testid="agents-data"'), "arrives COLLAPSED — no rows in the markup"); - // The drawer is a client useState fold; the OPEN face is pinned by the - // rows' data shape (view join) below — static render cannot toggle. + assert.ok(html.includes('data-testid="metadata-toggle"'), "the trigger row"); + assert.match(html, /Metadata/, "the label (CSS renders it mono-caps)"); + assert.ok(!html.includes('data-testid="metadata-box"'), "the info box is CLOSED until clicked — no rows in the markup"); + assert.ok(!html.includes("border-t \"/>"), "no separate divider — the trigger just sits at the bottom"); + // The open face (info-box rows at the model solution's 15px) is a + // client useState fold — static render cannot click; the rows' data + // shape is pinned by the view-join test in practice-record. }); - it("ConcludeCard: no drawer when the calls filled nothing beyond the visible card", () => { + it("ConcludeCard: no metadata trigger when the calls filled nothing beyond the visible card", () => { const html = renderToStaticMarkup( , ); - assert.ok(!html.includes("agents-data"), "the dismissal line carries no drawer"); + assert.ok(!html.includes("metadata-"), "the dismissal line carries no trigger"); const legacy = renderToStaticMarkup( , ); - assert.ok(!legacy.includes("agents-data"), "empty problemInput + bare outcome: nothing to reveal"); + assert.ok(!legacy.includes("metadata-"), "empty problemInput + bare outcome: nothing to reveal"); }); it("ConcludeCard: the dismissal synthesis is one muted line — no reveal card", () => {