aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
429 lines (426 loc) • 16.3 kB
JavaScript
'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