UNPKG

aura-glass

Version:

A comprehensive glassmorphism design system for React applications with 142+ production-ready components

429 lines (426 loc) 16.3 kB
'use client'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { cn } from '../../lib/utilsComprehensive.js'; import { X, ChevronLeft, ChevronRight, ZoomOut, ZoomIn, RotateCcw, RotateCw, Download, Minimize2, Maximize2, Pause, Play, Home } from 'lucide-react'; import { useState, useRef, useCallback, useEffect } from 'react'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import { MotionFramer } from '../../primitives/motion/MotionFramer.js'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { GlassButton } from '../button/GlassButton.js'; import '../button/GlassFab.js'; import '../button/GlassMagneticButton.js'; import { CardContent } from '../card/index.js'; import { GlassCard } from '../card/GlassCard.js'; /** * GlassImageViewer component * A comprehensive image viewer with zoom, pan, rotation, and slideshow features */ const GlassImageViewer = ({ images, initialIndex = 0, enableZoom = true, enablePan = true, enableRotation = true, enableFullscreen = true, enableNavigation = true, showZoomControls = true, showRotationControls = true, showDownloadButton = true, showImageInfo = true, autoPlay = false, autoPlayInterval = 3000, zoomLevels = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4], minZoom = 0.1, maxZoom = 5, objectFit = "contain", loading = false, error, className, onImageChange, onZoomChange, onFullscreenChange, ...props }) => { const prefersReducedMotion = useReducedMotion(); const [currentIndex, setCurrentIndex] = useState(initialIndex); const [isFullscreen, setIsFullscreen] = useState(false); const [zoom, setZoom] = useState(1); const [rotation, setRotation] = useState(0); const [pan, setPan] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); const [isAutoPlaying, setIsAutoPlaying] = useState(autoPlay); const [imageLoading, setImageLoading] = useState(true); const [imageError, setImageError] = useState(false); const containerRef = useRef(null); const imageRef = useRef(null); const autoPlayRef = useRef(); const currentImage = images[currentIndex]; // Handle image change const handleImageChange = useCallback(index => { if (index < 0 || index >= images.length) return; setCurrentIndex(index); setZoom(1); setRotation(0); setPan({ x: 0, y: 0 }); setImageLoading(true); setImageError(false); onImageChange?.(index); }, [images.length, onImageChange]); // Handle zoom const handleZoom = useCallback(newZoom => { const clampedZoom = Math.max(minZoom, Math.min(maxZoom, newZoom)); setZoom(clampedZoom); onZoomChange?.(clampedZoom); }, [minZoom, maxZoom, onZoomChange]); // Handle zoom in/out with levels const handleZoomIn = useCallback(() => { const currentLevelIndex = zoomLevels.findIndex(level => level >= zoom); const nextLevel = zoomLevels[Math.min(currentLevelIndex + 1, zoomLevels.length - 1)]; handleZoom(nextLevel); }, [zoom, zoomLevels, handleZoom]); const handleZoomOut = useCallback(() => { const reversedLevels = [...zoomLevels].reverse(); const currentLevelIndex = zoomLevels.length - 1 - reversedLevels.findIndex(level => level <= zoom); const prevLevel = zoomLevels[Math.max(currentLevelIndex - 1, 0)]; handleZoom(prevLevel); }, [zoom, zoomLevels, handleZoom]); // Handle rotation const handleRotate = useCallback(degrees => { setRotation(prev => (prev + degrees) % 360); }, []); // Handle pan const handleMouseDown = useCallback(e => { if (!enablePan || zoom <= 1) return; setIsDragging(true); setDragStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }); }, [enablePan, zoom, pan]); const handleMouseMove = useCallback(e => { if (!isDragging || !enablePan) return; setPan({ x: e.clientX - dragStart.x, y: e.clientY - dragStart.y }); }, [isDragging, enablePan, dragStart]); const handleMouseUp = useCallback(() => { setIsDragging(false); }, []); // Handle wheel zoom const handleWheel = useCallback(e => { if (!enableZoom) return; e.preventDefault(); const zoomFactor = e.deltaY > 0 ? 0.9 : 1.1; handleZoom(zoom * zoomFactor); }, [enableZoom, zoom, handleZoom]); // Handle keyboard navigation useEffect(() => { const handleKeyPress = e => { switch (e.key) { case "ArrowLeft": if (enableNavigation) { handleImageChange(currentIndex - 1); } break; case "ArrowRight": if (enableNavigation) { handleImageChange(currentIndex + 1); } break; case "Escape": if (isFullscreen) { handleFullscreenToggle(); } break; case "+": case "=": if (enableZoom) { handleZoomIn(); } break; case "-": if (enableZoom) { handleZoomOut(); } break; case "0": handleZoom(1); break; case "r": case "R": if (enableRotation) { handleRotate(90); } break; } }; window.addEventListener("keydown", handleKeyPress); return () => window.removeEventListener("keydown", handleKeyPress); }, [currentIndex, isFullscreen, zoom, enableNavigation, enableZoom, enableRotation, handleImageChange, handleZoom, handleZoomIn, handleZoomOut, handleRotate]); // Handle fullscreen toggle const handleFullscreenToggle = useCallback(() => { const newFullscreen = !isFullscreen; setIsFullscreen(newFullscreen); onFullscreenChange?.(newFullscreen); }, [isFullscreen, onFullscreenChange]); // Handle download const handleDownload = useCallback(() => { if (!currentImage) return; const link = document.createElement("a"); link.href = currentImage.src; link.download = currentImage.title || `image-${currentIndex + 1}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); }, [currentImage, currentIndex]); // Handle image load const handleImageLoad = useCallback(() => { setImageLoading(false); setImageError(false); }, []); const handleImageError = useCallback(() => { setImageLoading(false); setImageError(true); }, []); // Auto-play functionality useEffect(() => { if (isAutoPlaying && images && images.length > 1) { autoPlayRef.current = setInterval(() => { setCurrentIndex(prev => (prev + 1) % images.length); }, autoPlayInterval); } else { if (autoPlayRef.current) { clearInterval(autoPlayRef.current); } } return () => { if (autoPlayRef.current) { clearInterval(autoPlayRef.current); } }; }, [isAutoPlaying, images.length, autoPlayInterval]); // Reset zoom and pan when image changes useEffect(() => { setZoom(1); setRotation(0); setPan({ x: 0, y: 0 }); }, [currentIndex]); if (!currentImage) { return jsx(GlassCard, { "data-glass-component": true, className: cn("p-8", className), children: jsx("div", { className: 'text-center text-primary/60', children: "No image to display" }) }); } return jsx(MotionFramer, { preset: "fadeIn", className: "glass-w-full", children: jsx(GlassCard, { className: cn("overflow-hidden relative", isFullscreen && "fixed inset-0 z-50 rounded-none", className), ...props, children: jsxs(CardContent, { className: "glass-p-0", children: [jsxs("div", { ref: containerRef, className: cn("relative bg-black/20 overflow-hidden", isFullscreen ? "h-screen" : "aspect-video"), onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onWheel: handleWheel, style: { cursor: isDragging ? "grabbing" : enablePan && zoom > 1 ? "grab" : "default" }, children: [imageLoading && jsx("div", { className: 'absolute inset-0 glass-flex glass-items-center glass-justify-center', children: jsx("div", { className: cn("glass-radius-full h-8 w-8 border-2 border-white/20 border-t-white/60", !prefersReducedMotion && "animate-spin") }) }), imageError && jsxs("div", { className: 'absolute inset-0 glass-flex glass-flex-col glass-items-center glass-justify-center text-primary/60', children: [jsx(X, { className: 'w-12 h-12 mb-4' }), jsx("p", { children: "Failed to load image" })] }), !imageError && jsx("img", { ref: imageRef, src: currentImage.src, alt: currentImage.alt || currentImage.title || `Image ${currentIndex + 1}`, className: cn("w-full h-full transition-transform duration-200", objectFit === "contain" && "object-contain", objectFit === "cover" && "object-cover", objectFit === "fill" && "object-fill", objectFit === "none" && "object-none", objectFit === "scale-down" && "object-scale-down"), style: { transform: `scale(${zoom}) rotate(${rotation}deg) translate(${pan.x}px, ${pan.y}px)`, transformOrigin: "center center" }, onLoad: handleImageLoad, onError: handleImageError, draggable: false }), showImageInfo && currentImage.title && jsxs("div", { className: 'absolute bottom-0 left-0 right-0 glass-surface-dark/50 glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-p-4 glass-contrast-guard', children: [jsx("h3", { className: 'text-primary font-medium', children: currentImage.title }), currentImage.description && jsx("p", { className: 'text-primary/80 glass-text-sm glass-mt-1', children: currentImage.description })] })] }), jsxs("div", { className: 'absolute top-4 left-4 right-4 glass-flex glass-justify-between glass-items-start', children: [jsx("div", { className: "glass-flex glass-items-center glass-gap-2", children: enableNavigation && images.length > 1 && jsxs(Fragment, { children: [jsx(GlassButton, { variant: "secondary", size: "sm", onClick: e => handleImageChange(currentIndex - 1), disabled: currentIndex === 0, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(ChevronLeft, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "secondary", size: "sm", onClick: e => handleImageChange(currentIndex + 1), disabled: currentIndex === images.length - 1, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(ChevronRight, { className: 'w-4 h-4' }) }), jsxs("span", { className: 'text-primary/80 glass-text-sm glass-px-2', children: [currentIndex + 1, " / ", images.length] })] }) }), jsxs("div", { className: "glass-flex glass-items-center glass-gap-2", children: [enableZoom && showZoomControls && jsxs(Fragment, { children: [jsx(GlassButton, { variant: "secondary", size: "sm", onClick: handleZoomOut, disabled: zoom <= minZoom, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(ZoomOut, { className: 'w-4 h-4' }) }), jsxs("span", { className: 'text-primary/80 glass-text-sm glass-px-2 min-w-16 text-center', children: [Math.round(zoom * 100), "%"] }), jsx(GlassButton, { variant: "secondary", size: "sm", onClick: handleZoomIn, disabled: zoom >= maxZoom, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(ZoomIn, { className: 'w-4 h-4' }) })] }), enableRotation && showRotationControls && jsxs(Fragment, { children: [jsx(GlassButton, { variant: "secondary", size: "sm", onClick: e => handleRotate(-90), className: "glass-p-2 glass-focus glass-touch-target", children: jsx(RotateCcw, { className: 'w-4 h-4' }) }), jsx(GlassButton, { variant: "secondary", size: "sm", onClick: e => handleRotate(90), className: "glass-p-2 glass-focus glass-touch-target", children: jsx(RotateCw, { className: 'w-4 h-4' }) })] }), showDownloadButton && jsx(GlassButton, { variant: "secondary", size: "sm", onClick: handleDownload, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(Download, { className: 'w-4 h-4' }) }), enableFullscreen && jsx(GlassButton, { variant: "secondary", size: "sm", onClick: handleFullscreenToggle, className: "glass-p-2 glass-focus glass-touch-target", children: isFullscreen ? jsx(Minimize2, { className: 'w-4 h-4' }) : jsx(Maximize2, { className: 'w-4 h-4' }) })] })] }), enableNavigation && images.length > 1 && jsx("div", { className: 'absolute bottom-4 left-1/2 transform -translate-x-1/2', children: jsxs("div", { className: "glass-flex glass-items-center glass-gap-2 glass-surface-dark/50 glass-glass-glass-backdrop-blur-md glass-contrast-guard glass-radius-full glass-px-4 glass-py-2 glass-contrast-guard", children: [jsx(GlassButton, { variant: "ghost", size: "sm", onClick: e => setIsAutoPlaying(!isAutoPlaying), className: "glass-p-1 glass-focus glass-touch-target", children: isAutoPlaying ? jsx(Pause, { className: 'w-4 h-4' }) : jsx(Play, { className: 'w-4 h-4' }) }), jsx("div", { className: "glass-flex glass-gap-1", children: images.map((_, index) => jsx("button", { onClick: e => handleImageChange(index), className: cn("w-2 h-2 glass-radius-full transition-all duration-200 glass-focus glass-touch-target glass-contrast-guard", index === currentIndex ? "bg-white" : "bg-white/40") }, index)) })] }) }), (zoom !== 1 || rotation !== 0 || pan.x !== 0 || pan.y !== 0) && jsx("div", { className: 'absolute glass-top-1/2 left-4 transform -translate-y-1/2', children: jsx(GlassButton, { variant: "secondary", size: "sm", onClick: e => { setZoom(1); setRotation(0); setPan({ x: 0, y: 0 }); }, className: "glass-p-2 glass-focus glass-touch-target", children: jsx(Home, { className: 'w-4 h-4' }) }) })] }) }) }); }; export { GlassImageViewer, GlassImageViewer as default }; //# sourceMappingURL=GlassImageViewer.js.map