import { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; import { Braces, Copy, Check, X } from "lucide-react"; interface JsonCellPopoverProps { value: unknown; anchorRect: DOMRect | null; onClose: () => void; } function safeJsonParse(value: unknown): object | null { if (typeof value === "object" && value !== null) return value as object; if (typeof value !== "string") return null; try { const parsed = JSON.parse(value); return typeof parsed === "object" && parsed !== null ? parsed : null; } catch { return null; } } function formatJson(obj: object): string { try { return JSON.stringify(obj, null, 2); } catch { return String(obj); } } export function JsonCellPopover({ value, anchorRect, onClose }: JsonCellPopoverProps) { const [tab, setTab] = useState<"formatted" | "raw">("formatted"); const [copied, setCopied] = useState(false); const popoverRef = useRef(null); const parsed = safeJsonParse(value); const rawText = typeof value === "string" ? value : JSON.stringify(value); const formattedText = parsed ? formatJson(parsed) : rawText; // Close on Escape useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [onClose]); // Close on outside click useEffect(() => { const onClick = (e: MouseEvent) => { if (popoverRef.current && !popoverRef.current.contains(e.target as Node)) { onClose(); } }; const id = setTimeout(() => document.addEventListener("mousedown", onClick), 0); return () => { clearTimeout(id); document.removeEventListener("mousedown", onClick); }; }, [onClose]); if (!anchorRect) return null; const popoverWidth = 420; const popoverMaxHeight = 360; const gap = 8; let left = anchorRect.left; let top = anchorRect.bottom + gap; if (left + popoverWidth > window.innerWidth - 16) { left = Math.max(16, window.innerWidth - popoverWidth - 16); } if (top + popoverMaxHeight > window.innerHeight - 16) { top = anchorRect.top - popoverMaxHeight - gap; if (top < 16) top = 16; } const handleCopy = async () => { const text = tab === "formatted" ? formattedText : rawText; await navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }; return createPortal(
{/* Header */}
JSON
{/* Tabs */}
{/* Copy */} {/* Close */}
{/* Body */}
          {tab === "formatted" ? formattedText : rawText}
        
, document.body, ); } /** Extract a brief label for the collapsed JSON preview shown in the cell. */ export function jsonPreview(value: unknown): { label: string; isJson: boolean } { const parsed = safeJsonParse(value); if (!parsed) return { label: "", isJson: false }; if (Array.isArray(parsed)) { return { label: `[ ${parsed.length} item${parsed.length !== 1 ? "s" : ""} ]`, isJson: true }; } const keys = Object.keys(parsed); return { label: `{ ${keys.length} key${keys.length !== 1 ? "s" : ""} }`, isJson: true }; }