128 lines
4.5 KiB
TypeScript
128 lines
4.5 KiB
TypeScript
import { Sheet, SheetContent } from "@repo/shadcn-ui/components";
|
|
// hooks/use-pinned-preview-sheet.ts
|
|
import * as React from "react";
|
|
import { createPortal } from 'react-dom';
|
|
|
|
type UsePinnedPreviewSheetOptions<T> = {
|
|
persistKey?: string; // clave localStorage para “pin”
|
|
widthClass?: string; // ancho del panel: p. ej. "w-[500px]"
|
|
onOpenChange?: (open: boolean) => void;
|
|
title?: string | ((item: T | null) => string); // Título del Sheet (no anclado)
|
|
};
|
|
|
|
export type PinnedPreviewSheet<T> = {
|
|
/** Estado */
|
|
isOpen: boolean;
|
|
isPinned: boolean;
|
|
item: T | null;
|
|
open: (item: T) => void;
|
|
close: () => void;
|
|
togglePin: () => void;
|
|
|
|
/** Añade margen al contenedor de la lista cuando está anclado */
|
|
listRightMarginClass: string;
|
|
|
|
/** Renderiza el panel (Sheet o aside) */
|
|
Preview: React.FC<{
|
|
children: (ctx: {
|
|
item: T;
|
|
isPinned: boolean;
|
|
close: () => void;
|
|
togglePin: () => void;
|
|
}) => React.ReactNode
|
|
}>;
|
|
};
|
|
|
|
export function usePinnedPreviewSheet<T = unknown>({
|
|
persistKey = "preview-pin",
|
|
widthClass = "w-[500px]",
|
|
onOpenChange,
|
|
title,
|
|
}: UsePinnedPreviewSheetOptions<T> = {}): PinnedPreviewSheet<T> {
|
|
const [isOpen, setOpen] = React.useState(false);
|
|
const [item, setItem] = React.useState<T | null>(null);
|
|
const [isPinned, setPinned] = React.useState<boolean>(() => {
|
|
try { return localStorage.getItem(persistKey) === "1"; } catch { return false; }
|
|
});
|
|
|
|
// recordar/restaurar foco para accesibilidad
|
|
const lastFocused = React.useRef<HTMLElement | null>(null);
|
|
const rememberFocus = () => { lastFocused.current = document.activeElement as HTMLElement | null; };
|
|
const restoreFocus = () => { lastFocused.current?.focus?.(); };
|
|
|
|
const open = React.useCallback((next: T) => {
|
|
rememberFocus();
|
|
setItem(next);
|
|
setOpen(true);
|
|
onOpenChange?.(true);
|
|
}, [onOpenChange]);
|
|
|
|
const close = React.useCallback(() => {
|
|
if (isPinned) return;
|
|
setOpen(false);
|
|
onOpenChange?.(false);
|
|
setTimeout(restoreFocus, 0);
|
|
}, [isPinned, onOpenChange]);
|
|
|
|
const togglePin = React.useCallback(() => {
|
|
setPinned((p) => {
|
|
const n = !p;
|
|
try { localStorage.setItem(persistKey, n ? "1" : "0"); } catch { }
|
|
return n;
|
|
});
|
|
}, [persistKey]);
|
|
|
|
// Bloquea scroll solo en modo Sheet
|
|
React.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 en modo Sheet
|
|
React.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 listRightMarginClass = isPinned ? "mr-[500px]" : ""; // ajusta si cambias widthClass
|
|
|
|
const HeaderTitle = React.useMemo(() => {
|
|
if (!item) return "";
|
|
if (typeof title === "function") return title(item);
|
|
return title ?? "";
|
|
}, [item, title]);
|
|
|
|
const Preview: PinnedPreviewSheet<T>["Preview"] = React.useCallback(({ children }) => {
|
|
if (!item) return null;
|
|
|
|
// Panel anclado: aside estático sin overlay
|
|
if (isPinned) {
|
|
return createPortal(
|
|
<aside
|
|
aria-label="Vista previa anclada"
|
|
className={`fixed inset-y-0 right-0 ${widthClass} bg-background border-l z-40`}
|
|
>
|
|
{children({ item, isPinned: true, close, togglePin })}
|
|
</aside>,
|
|
document.body
|
|
);
|
|
}
|
|
|
|
// Panel no anclado: Sheet de shadcn/ui (con overlay y accesibilidad)
|
|
return createPortal(
|
|
<Sheet open={isOpen} onOpenChange={(o) => (o ? setOpen(true) : close())}>
|
|
<SheetContent side="right" className={`${widthClass} p-0`}>
|
|
{children({ item, isPinned: false, close, togglePin })}
|
|
</SheetContent>
|
|
</Sheet>,
|
|
document.body
|
|
);
|
|
}, [item, isPinned, isOpen, widthClass, close, togglePin]);
|
|
|
|
return { isOpen, isPinned, item, open, close, togglePin, listRightMarginClass, Preview };
|
|
} |