import { useCallback, useRef, useState, type ReactNode, type Ref } from "react"; import { createPortal } from "react-dom"; import { motion, useReducedMotion } from "motion/react"; import { Maximize2, X } from "lucide-react"; import { Button } from "@/shared/ui/button"; import { Dialog, DialogClose, DialogContent, DialogTitle, DialogTrigger } from "@/shared/ui/dialog"; import { createReadingPreview } from "@/shared/ui/reading-preview"; import { cn } from "@/shared/ui/cn"; /** Report-specific progressive disclosure. The source stays mounted while reading. */ export function InvestigationExpandCard({ title, label = title, preview, children, wide = false, onOpen, contentRef, triggerLabel = `展开${title}`, className, revision, disabled = false, reuseContent = false, headingAccessory }: { title: string; label?: string; preview: ReactNode; children: ReactNode | ((close: (restoreFocus?: boolean) => void) => ReactNode); wide?: boolean; onOpen?: () => void; contentRef?: Ref; triggerLabel?: string; className?: string; /** Freeze versioned prose while reading; live controls must not use this. */ revision?: string; disabled?: boolean; /** Move one live content host between slots, preserving expensive renderers. */ reuseContent?: boolean; headingAccessory?: ReactNode; }) { const triggerRef = useRef(null); const restoreFocusRef = useRef(true); const [open, setOpen] = useState(false); const [keyboard, setKeyboard] = useState(false); const [reading, setReading] = useState<{ revision: string; content: typeof children } | null>(null); const [contentHost] = useState(() => reuseContent ? document.createElement("div") : null); const previewSlot = useRef(null); const snapshot = useRef(null); const attachPreview = useCallback((slot: HTMLDivElement | null) => { previewSlot.current = slot; if (slot && contentHost && !contentHost.isConnected) slot.appendChild(contentHost); }, [contentHost]); const attachDetail = useCallback((slot: HTMLDivElement | null) => { const preview = previewSlot.current; if (!preview || !contentHost) return; if (slot) { snapshot.current?.remove(); snapshot.current = createReadingPreview(contentHost); preview.append(snapshot.current); slot.appendChild(contentHost); } else { snapshot.current?.remove(); snapshot.current = null; preview.appendChild(contentHost); } }, [contentHost]); const reduced = useReducedMotion(); const close = (restoreFocus = true) => { restoreFocusRef.current = restoreFocus; setOpen(false); }; const content = reading?.content ?? children; return ( { setOpen(next); if (next) { restoreFocusRef.current = true; setReading(revision === undefined ? null : { revision, content: children }); onOpen?.(); } }}>

{title}

{headingAccessory}
{/* Contain child margins so the occupied and vacated slots measure alike. */} {contentHost ?
: preview}
{contentHost ? createPortal(typeof children === "function" ? children(close) : children, contentHost) : null} { event.stopPropagation(); setKeyboard(true); }} onCloseAutoFocus={(event) => { event.preventDefault(); if (restoreFocusRef.current) triggerRef.current?.focus({ preventScroll: true }); }}>
{title}
{contentHost ?
: typeof content === "function" ? content(close) : content}
{reading && revision !== undefined && reading.revision !== revision ? (
已有更新,当前保留正在阅读的版本。
) : null}
); }