UNPKG

aura-glass

Version:

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

501 lines (498 loc) 14.4 kB
'use client'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useMotionValue, motion, AnimatePresence } from 'framer-motion'; import { forwardRef, useState, useRef, useCallback, useEffect } from 'react'; import { cn } from '../../lib/utilsComprehensive.js'; import { createGlassStyle } from '../../core/mixins/glassMixins.js'; const TouchOptimizedGlass = /*#__PURE__*/forwardRef(({ children, onTap, onLongPress, onSwipe, className, touchFeedback = true, rippleEffect = true, hapticsEnabled = true, glassIntensity = "medium", ...rest }, ref) => { const prefersReducedMotion = useReducedMotion(); const [isPressed, setIsPressed] = useState(false); const [ripples, setRipples] = useState([]); const [longPressTimer, setLongPressTimer] = useState(null); const containerRef = useRef(null); const scale = useMotionValue(1); const opacity = useMotionValue(1); // Haptic feedback function const triggerHaptics = useCallback((type = "light") => { if (!hapticsEnabled || typeof window === "undefined") return; if ("navigator" in window && "vibrate" in navigator) { const patterns = { light: [10], medium: [20], heavy: [30, 10, 30] }; navigator.vibrate(patterns[type]); } }, [hapticsEnabled]); // Handle touch start const handleTouchStart = useCallback(e => { e.preventDefault(); setIsPressed(true); scale.set(0.95); if (touchFeedback) { triggerHaptics("light"); } // Start long press timer if (onLongPress) { const timer = setTimeout(() => { triggerHaptics("medium"); onLongPress(); setLongPressTimer(null); }, 500); setLongPressTimer(timer); } // Add ripple effect if (rippleEffect && containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; const newRipple = { id: Date.now(), x, y, timestamp: Date.now() }; setRipples(prev => [...prev, newRipple]); // Remove ripple after animation setTimeout(() => { setRipples(prev => prev.filter(r => r.id !== newRipple.id)); }, 600); } }, [touchFeedback, rippleEffect, onLongPress, triggerHaptics, scale]); // Handle touch end const handleTouchEnd = useCallback(() => { setIsPressed(false); scale.set(1); // Clear long press timer if (longPressTimer) { clearTimeout(longPressTimer); setLongPressTimer(null); } // Trigger tap if no long press occurred if (onTap && !longPressTimer) { triggerHaptics("light"); onTap(); } }, [longPressTimer, onTap, triggerHaptics, scale]); // Handle pan gesture for swipe detection const handlePanEnd = useCallback((event, info) => { const { offset, velocity } = info; const swipeThreshold = 50; const velocityThreshold = 500; if (Math.abs(offset.x) > swipeThreshold || Math.abs(velocity.x) > velocityThreshold) { const direction = offset.x > 0 ? "right" : "left"; onSwipe?.(direction); triggerHaptics("medium"); } else if (Math.abs(offset.y) > swipeThreshold || Math.abs(velocity.y) > velocityThreshold) { const direction = offset.y > 0 ? "down" : "up"; onSwipe?.(direction); triggerHaptics("medium"); } }, [onSwipe, triggerHaptics]); const glassStyles = { light: createGlassStyle({ intent: "neutral", elevation: "level2" }), medium: createGlassStyle({ intent: "neutral", elevation: "level2" }), heavy: createGlassStyle({ intent: "neutral", elevation: "level2" }) }; const glassStyle = glassStyles[glassIntensity]; return jsxs(motion.div, { ref: node => { containerRef.current = node; if (typeof ref === 'function') { ref(node); } else if (ref) { ref.current = node; } }, className: cn('relative overflow-hidden touch-none select-none', className), style: { ...glassStyle, // Use createGlassStyle() instead, borderRadius: "12px", minHeight: "44px", // iOS minimum touch target minWidth: "44px", scale, opacity }, onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd, onPanEnd: handlePanEnd, drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 0.1, ...rest, children: [children, jsx(AnimatePresence, { children: ripples.map(ripple => jsx(motion.div, { className: 'absolute pointer-events-none', style: { left: ripple.x - 20, top: ripple.y - 20, width: 40, height: 40, borderRadius: "50%", background: '/* Use createGlassStyle({ intent: "neutral", elevation: "level2" }) */' }, initial: { scale: 0, opacity: 1 }, animate: prefersReducedMotion ? {} : { scale: 3, opacity: 0 }, exit: { opacity: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.6, ease: "easeOut" } }, ripple.id)) }), touchFeedback && jsx(AnimatePresence, { children: isPressed && jsx(motion.div, { className: 'absolute inset-0 pointer-events-none', style: { background: '/* Use createGlassStyle({ intent: "neutral", elevation: "level2" }) */', borderRadius: "inherit" }, initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.1 } }) })] }); }); TouchOptimizedGlass.displayName = "TouchOptimizedGlass"; const TouchGlassOptimization = /*#__PURE__*/forwardRef((props, ref) => jsx(TouchOptimizedGlass, { ref: ref, ...props })); TouchGlassOptimization.displayName = "TouchGlassOptimization"; function MobileGlassNavigation({ children, swipeThreshold = 50, onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, className = "" }) { const prefersReducedMotion = useReducedMotion(); const [swipeDirection, setSwipeDirection] = useState(null); const containerRef = useRef(null); const handleSwipe = useCallback(direction => { setSwipeDirection(direction); switch (direction) { case "left": onSwipeLeft?.(); break; case "right": onSwipeRight?.(); break; case "up": onSwipeUp?.(); break; case "down": onSwipeDown?.(); break; } // Reset swipe direction after animation setTimeout(() => setSwipeDirection(null), 300); }, [onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown]); return jsx(motion.div, { ref: containerRef, className: `relative ${className}`, style: createGlassStyle({ intent: "neutral", elevation: "level2" }), drag: "x", dragConstraints: { left: 0, right: 0 }, dragElastic: 0.2, onDragEnd: (event, info) => { const { offset, velocity } = info; if (Math.abs(offset.x) > swipeThreshold || Math.abs(velocity.x) > 500) { const direction = offset.x > 0 ? "right" : "left"; handleSwipe(direction); } else if (Math.abs(offset.y) > swipeThreshold || Math.abs(velocity.y) > 500) { const direction = offset.y > 0 ? "down" : "up"; handleSwipe(direction); } }, animate: swipeDirection ? { x: swipeDirection === "left" ? -50 : swipeDirection === "right" ? 50 : 0, y: swipeDirection === "up" ? -50 : swipeDirection === "down" ? 50 : 0, opacity: 0.7 } : { x: 0, y: 0, opacity: 1 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: children }); } function AdaptiveGlassDensity({ children, screenSize, devicePixelRatio, autoAdapt = true, className = "" }) { const prefersReducedMotion = useReducedMotion(); const [currentDensity, setCurrentDensity] = useState("medium"); useEffect(() => { if (!autoAdapt) return; const updateDensity = () => { const width = window.innerWidth; const pixelRatio = devicePixelRatio || window.devicePixelRatio || 1; let density = "medium"; if (width < 768) { density = pixelRatio > 2 ? "light" : "medium"; } else if (width < 1200) { density = pixelRatio > 1.5 ? "medium" : "heavy"; } else { density = pixelRatio > 1.5 ? "heavy" : "medium"; } setCurrentDensity(density); }; updateDensity(); window.addEventListener("resize", updateDensity); return () => window.removeEventListener("resize", updateDensity); }, [autoAdapt, devicePixelRatio]); const densityStyles = { light: createGlassStyle({ intent: "neutral", elevation: "level2" }), medium: createGlassStyle({ intent: "neutral", elevation: "level2" }), heavy: createGlassStyle({ intent: "neutral", elevation: "level2" }) }; const style = densityStyles[currentDensity]; return jsx(motion.div, { className: className, style: { ...style, // Use createGlassStyle() instead, borderRadius: "12px", transition: "all 0.3s ease-in-out" }, animate: prefersReducedMotion ? {} : { opacity: 1 }, initial: { opacity: 0 }, children: children }); } function TouchRippleEffects({ children, color = "var(--glass-bg-hover)", maxRipples = 3, rippleDuration = 600, className = "" }) { const prefersReducedMotion = useReducedMotion(); const [ripples, setRipples] = useState([]); const containerRef = useRef(null); const handleTouchStart = useCallback(e => { if (!containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const touch = e.touches[0]; const x = touch.clientX - rect.left; const y = touch.clientY - rect.top; const newRipple = { id: Date.now(), x, y }; setRipples(prev => { const updated = [...prev, newRipple]; return updated.slice(-maxRipples); // Keep only the latest ripples }); // Remove ripple after animation setTimeout(() => { setRipples(prev => prev.filter(r => r.id !== newRipple.id)); }, rippleDuration); }, [maxRipples, rippleDuration]); return jsxs("div", { ref: containerRef, className: `relative overflow-hidden ${className}`, onTouchStart: handleTouchStart, children: [children, jsx(AnimatePresence, { children: ripples.map(ripple => jsx(motion.div, { className: 'absolute pointer-events-none glass-radius-full', style: { left: ripple.x - 20, top: ripple.y - 20, width: 40, height: 40, background: color }, initial: { scale: 0, opacity: 1 }, animate: prefersReducedMotion ? {} : { scale: 4, opacity: 0 }, exit: { opacity: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: rippleDuration / 1000, ease: "easeOut" } }, ripple.id)) })] }); } function MobileGlassBottomSheet({ isOpen, onClose, children, height = "50vh", snapPoints = ["25vh", "50vh", "75vh"], className = "" }) { const prefersReducedMotion = useReducedMotion(); const [currentHeight, setCurrentHeight] = useState(height); const handleDragEnd = useCallback((event, info) => { const { offset, velocity } = info; const swipeThreshold = 100; if (offset.y > swipeThreshold || velocity.y > 500) { // Close the sheet if dragged down significantly onClose(); } else { // Snap to nearest snap point const currentY = info.point.y; const closestSnapPoint = snapPoints.reduce((closest, snapPoint) => { const snapY = parseFloat(snapPoint); const currentDiff = Math.abs(currentY - snapY); const closestDiff = Math.abs(currentY - parseFloat(closest)); return currentDiff < closestDiff ? snapPoint : closest; }); setCurrentHeight(closestSnapPoint); } }, [onClose, snapPoints]); return jsx(AnimatePresence, { children: isOpen && jsxs(Fragment, { children: [jsx(motion.div, { className: cn("glass-foundation-complete glass-position-fixed glass-inset-0 glass-surface-overlay glass-z-40"), initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, exit: { opacity: 0 }, onClick: onClose }), jsxs(motion.div, { className: `fixed bottom-0 left-0 right-0 z-50 ${className}`, style: createGlassStyle({ intent: "neutral", elevation: "level2" }), initial: { y: "100%" }, animate: prefersReducedMotion ? {} : { y: 0 }, exit: { y: "100%" }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, drag: "y", dragConstraints: { top: 0, bottom: 0 }, dragElastic: 0.1, onDragEnd: handleDragEnd, children: [jsx("div", { className: "glass-flex glass-justify-center glass-py-3", children: jsx("div", { className: cn("glass-w-12 glass-h-1.5 glass-surface-secondary glass-radius-full") }) }), jsx("div", { className: 'glass-px-6 pb-6 overflow-y-auto max-h-full', children: children })] })] }) }); } export { AdaptiveGlassDensity, MobileGlassBottomSheet, MobileGlassNavigation, TouchGlassOptimization, TouchOptimizedGlass, TouchRippleEffects, TouchGlassOptimization as default }; //# sourceMappingURL=TouchGlassOptimization.js.map