SPB Git

Repository Public MIT

figure-lightbox.tsx 4.1 KB 92 lines tsx

1import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode } from "react";2import { ChevronLeft, ChevronRight, Download, X } from "lucide-react";3import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";4import { cn } from "@/lib/utils";56export interface LightboxItem {7  src: string;8  alt?: string;9  filename?: string;10}1112interface LightboxContextValue {13  open: (items: LightboxItem[], index?: number) => void;14}1516const LightboxContext = createContext<LightboxContextValue>({ open: () => undefined });1718export function useLightbox() {19  return useContext(LightboxContext);20}2122export function LightboxProvider({ children }: { children: ReactNode }) {23  const [items, setItems] = useState<LightboxItem[]>([]);24  const [index, setIndex] = useState(0);25  const [isOpen, setIsOpen] = useState(false);2627  const open = useCallback((list: LightboxItem[], i = 0) => {28    if (!list.length) return;29    setItems(list);30    setIndex(Math.min(Math.max(i, 0), list.length - 1));31    setIsOpen(true);32  }, []);3334  const prev = useCallback(() => setIndex((i) => (i - 1 + items.length) % items.length), [items.length]);35  const next = useCallback(() => setIndex((i) => (i + 1) % items.length), [items.length]);3637  useEffect(() => {38    if (!isOpen) return;39    const onKey = (e: KeyboardEvent) => {40      if (e.key === "ArrowLeft") prev();41      if (e.key === "ArrowRight") next();42    };43    window.addEventListener("keydown", onKey);44    return () => window.removeEventListener("keydown", onKey);45  }, [isOpen, prev, next]);4647  const value = useMemo(() => ({ open }), [open]);48  const current = items[index];4950  return (51    <LightboxContext.Provider value={value}>52      {children}53      <Dialog open={isOpen} onOpenChange={setIsOpen}>54        <DialogContent className="max-w-[min(96vw,1400px)] w-full p-0 overflow-hidden rounded-2xl border-border bg-background [&>button]:hidden">55          <DialogTitle className="sr-only">{current?.alt || "Figure"}</DialogTitle>56          {current && (57            <div className="flex flex-col">58              <div className="flex items-center gap-2 border-b border-border px-4 py-2.5">59                <span className="min-w-0 flex-1 truncate text-sm font-medium">{current.alt || "Figure"}</span>60                {items.length > 1 && <span className="text-[12px] tnum text-muted-foreground">{index + 1} / {items.length}</span>}61                <a62                  href={current.src}63                  download={current.filename || `vquant-figure-${index + 1}.png`}64                  className="inline-flex h-8 items-center gap-1.5 rounded-lg border border-border px-2.5 text-[12px] hover:bg-accent"65                >66                  <Download className="h-3.5 w-3.5" /> Télécharger67                </a>68                <button type="button" onClick={() => setIsOpen(false)} className="grid h-8 w-8 place-items-center rounded-lg hover:bg-accent" aria-label="Fermer">69                  <X className="h-4 w-4" />70                </button>71              </div>72              <div className="relative grid place-items-center bg-white dark:bg-[hsl(228_20%_9%)] p-2 md:p-4 min-h-[40vh] max-h-[82vh]">73                <img src={current.src} alt={current.alt || "Figure"} className="max-h-[78vh] w-auto max-w-full object-contain rounded-lg" />74                {items.length > 1 && (75                  <>76                    <button type="button" onClick={prev} className={cn("absolute left-3 top-1/2 -translate-y-1/2 grid h-10 w-10 place-items-center rounded-full bg-background/90 border border-border shadow hover:bg-accent")} aria-label="Précédente">77                      <ChevronLeft className="h-5 w-5" />78                    </button>79                    <button type="button" onClick={next} className={cn("absolute right-3 top-1/2 -translate-y-1/2 grid h-10 w-10 place-items-center rounded-full bg-background/90 border border-border shadow hover:bg-accent")} aria-label="Suivante">80                      <ChevronRight className="h-5 w-5" />81                    </button>82                  </>83                )}84              </div>85            </div>86          )}87        </DialogContent>88      </Dialog>89    </LightboxContext.Provider>90  );91}92
Files main
Menu

Theme