// docs/STRUCTURED-VIEW.md: the web client's user, agent, thought, tool-detail // and notice text blocks copy their complete source text when clicked or // keyboard-activated. The visible node is whatever it wraps; this only adds // the affordance and the "copied"/"error" state the CSS reads. import { useCallback, useEffect, useRef, useState, type ElementType, type ReactNode } from "react"; async function writeClipboard(text: string): Promise { try { await navigator.clipboard.writeText(text); return; } catch { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); const copied = document.execCommand?.("copy") ?? false; textarea.remove(); if (!copied) throw new Error("clipboard unavailable"); } } export interface CopyableProps { /** The complete source text — not whatever the DOM happens to show. */ text: string; /** Names the thing in the "Copy …" label ("message", "diff", "path", …). */ label: string; as?: ElementType; className?: string; title?: string; children?: ReactNode; /** Extra attributes the row builders set (`data-row-id`, `lang`, …). */ [key: string]: unknown; } /** Wrapping a node in a copy affordance. Empty `text` renders the node plain — * a control that copies nothing is worse than no control. */ export function Copyable({ text, label, as: Tag = "div", className, children, ...rest }: CopyableProps) { const [copyState, setCopyState] = useState<"copied" | "error" | null>(null); const timer = useRef(null); useEffect( () => () => { if (timer.current !== null) window.clearTimeout(timer.current); }, [], ); const copy = useCallback(() => { void writeClipboard(text).then( () => { setCopyState("copied"); if (timer.current !== null) window.clearTimeout(timer.current); timer.current = window.setTimeout(() => setCopyState(null), 1200); }, () => setCopyState("error"), ); }, [text]); if (!text) { return ( {children} ); } const ariaLabel = copyState === "copied" ? `${label} copied` : `Copy ${label}`; return ( { // A link inside the block is a link, not a copy target. if ((event.target as Element | null)?.closest("a")) return; copy(); }} onKeyDown={(event: React.KeyboardEvent) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); copy(); }} {...rest} > {children} ); }