import { FC, ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { createPortal } from "react-dom"; export type UseInvoicePreviewOptions = { persistKey?: string; // clave para guardar el pin en localStorage pinnedWidthClass?: string; // ancho al anclar (Tailwind), p.ej. "w-[500px]" onOpenChange?: (open: boolean) => void; // callback opcional }; export type InvoicePreviewHook = { isOpen: boolean; isPinned: boolean; item: T | null; open: (item: T) => void; close: () => void; togglePin: () => void; /** Añade margen derecho al listado si está anclado */ containerClassName: string; /** Renderiza el preview en un portal (body). Debes pasar tu Card como children render-prop */ PreviewPortal: FC<{ children: (p: { item: T; isOpen: boolean; isPinned: boolean; onClose: () => void; onTogglePin: () => void; }) => ReactNode }>; }; export function useInvoicePreview( opts?: UseInvoicePreviewOptions ): InvoicePreviewHook { const { persistKey = "invoice-preview-pin", pinnedWidthClass = "w-[500px]", onOpenChange } = opts ?? {}; const [isOpen, setOpen] = useState(false); const [item, setItem] = useState(null); const [isPinned, setPinned] = useState(() => { try { return localStorage.getItem(persistKey) === "1"; } catch { return false; } }); // Guardar y restaurar foco al cerrar (mejor accesibilidad) const lastFocusedRef = useRef(null); const rememberFocus = () => { lastFocusedRef.current = (document.activeElement as HTMLElement) ?? null; }; const restoreFocus = () => { lastFocusedRef.current?.focus?.(); }; const open = useCallback((next: T) => { rememberFocus(); setItem(next); setOpen(true); onOpenChange?.(true); }, [onOpenChange]); const close = useCallback(() => { if (isPinned) return; // anclado no se cierra setOpen(false); onOpenChange?.(false); setTimeout(restoreFocus, 0); }, [isPinned, onOpenChange]); const togglePin = useCallback(() => { setPinned((prev) => { const next = !prev; try { localStorage.setItem(persistKey, next ? "1" : "0"); } catch { } return next; }); }, [persistKey]); // Bloqueo de scroll cuando está abierto y NO anclado useEffect(() => { if (isOpen && !isPinned) { const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; } }, [isOpen, isPinned]); // Cerrar con ESC (solo si no está anclado) useEffect(() => { if (!isOpen || isPinned) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") close(); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, [isOpen, isPinned, close]); const containerClassName = isPinned ? `mr-[--preview-width] [--preview-width:theme(spacing.0)] ${pinnedWidthClass ? "" : ""}` : ""; // Nota: preferimos aplicar el margen directamente en el layout (ver uso abajo) const PreviewPortal: InvoicePreviewHook["PreviewPortal"] = useCallback(({ children }) => { if (!item) return null; const node = children({ item, isOpen, isPinned, onClose: close, onTogglePin: togglePin, }); return createPortal(node as ReactNode, document.body); }, [item, isOpen, isPinned, close, togglePin]); return { isOpen, isPinned, item, open, close, togglePin, containerClassName, PreviewPortal }; }