UNPKG

aura-glass

Version:

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

418 lines (415 loc) 12.7 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useRef, useState, useCallback, useEffect } from 'react'; import { AnimatePresence, useMotionValue, motion } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; import { glassSoundDesign } from '../../utils/soundDesign.js'; const defaultReactionTypes = [{ emoji: "👍", name: "Like", color: "var(--glass-color-primary)", sound: "success", shortcut: "1" }, { emoji: "❤️", name: "Love", color: "var(--glass-color-danger)", sound: "notification", shortcut: "2" }, { emoji: "😂", name: "Laugh", color: "var(--glass-color-warning)", sound: "tap", shortcut: "3" }, { emoji: "😮", name: "Wow", color: "#8b5cf6", sound: "morph", shortcut: "4" }, { emoji: "😢", name: "Sad", color: "var(--glass-gray-500)", sound: "slide", shortcut: "5" }, { emoji: "😡", name: "Angry", color: "var(--glass-color-danger-dark)", sound: "error", shortcut: "6" }, { emoji: "🎉", name: "Celebrate", color: "var(--glass-color-success)", sound: "success", shortcut: "7" }, { emoji: "🤔", name: "Think", color: "#f97316", sound: "hover", shortcut: "8" }]; function GlassReactions({ children, className, reactions = [], reactionTypes = defaultReactionTypes, maxReactions = 50, autoExpire = 5000, enablePhysics = true, enableSounds = true, enableShortcuts = true, enableBurst = true, glassEffect = true, onReactionAdd, onReactionExpire }) { useReducedMotion(); const containerRef = useRef(null); const [localReactions, setLocalReactions] = useState([]); const [showPicker, setShowPicker] = useState(false); const [pickerPosition, setPickerPosition] = useState({ x: 0, y: 0 }); const lastClickTime = useRef(0); const burstCount = useRef(0); // Combine local and external reactions const allReactions = [...reactions, ...localReactions].slice(-maxReactions); // Handle click for reactions const handleClick = useCallback(e => { const now = Date.now(); const timeSinceLastClick = now - lastClickTime.current; // Double click detection for quick reactions if (timeSinceLastClick < 300) { // Quick double-click reaction const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Use first reaction type for quick reaction addReaction(reactionTypes[0].emoji, { x, y }, 1); // Burst effect for triple+ clicks if (enableBurst && timeSinceLastClick < 150) { burstCount.current++; if (burstCount.current >= 2) { // Create burst of reactions for (let i = 0; i < 5; i++) { setTimeout(() => { const randomType = reactionTypes[Math.floor(Math.random() * reactionTypes.length)]; addReaction(randomType.emoji, { x: x + (Math.random() - 0.5) * 100, y: y + (Math.random() - 0.5) * 100 }, Math.random() * 0.5 + 0.5); }, i * 100); } } } } else { burstCount.current = 0; } lastClickTime.current = now; }, [reactionTypes, enableBurst]); // Handle right click for reaction picker const handleContextMenu = useCallback(e => { e.preventDefault(); const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setPickerPosition({ x, y }); setShowPicker(true); }, []); // Add reaction const addReaction = useCallback((emoji, position, intensity = 1) => { const reactionType = reactionTypes.find(r => r.emoji === emoji); const reaction = { id: `${Date.now()}-${Math.random()}`, emoji, position, timestamp: Date.now(), intensity, physics: enablePhysics ? { velocity: { x: (Math.random() - 0.5) * 200 * intensity, y: -Math.random() * 100 * intensity - 50 }, rotation: (Math.random() - 0.5) * 180, scale: 0.8 + Math.random() * 0.4 } : undefined }; setLocalReactions(prev => [...prev, reaction]); // Play sound if (enableSounds && reactionType?.sound) { glassSoundDesign.playGlassSound(reactionType.sound); } // Trigger haptic feedback if ("vibrate" in navigator) { navigator.vibrate(50 * intensity); } onReactionAdd?.(reaction); // Auto expire if (autoExpire > 0) { setTimeout(() => { setLocalReactions(prev => prev.filter(r => r.id !== reaction.id)); onReactionExpire?.(reaction.id); }, autoExpire); } }, [reactionTypes, enablePhysics, enableSounds, autoExpire, onReactionAdd, onReactionExpire]); // Handle keyboard shortcuts useEffect(() => { if (!enableShortcuts) return; const handleKeyPress = e => { // Only trigger on number keys if (e.key >= "1" && e.key <= "9") { const index = parseInt(e.key) - 1; const reactionType = reactionTypes[index]; if (reactionType) { // Add reaction at center of screen const container = containerRef.current; if (container) { const rect = container.getBoundingClientRect(); addReaction(reactionType.emoji, { x: rect.width / 2, y: rect.height / 2 }); } } } }; window.addEventListener("keypress", handleKeyPress); return () => window.removeEventListener("keypress", handleKeyPress); }, [enableShortcuts, reactionTypes, addReaction]); return jsxs("div", { ref: containerRef, className: cn("relative", className), onClick: handleClick, onContextMenu: handleContextMenu, children: [children, jsx("div", { className: 'absolute inset-0 pointer-events-none overflow-hidden', children: jsx(AnimatePresence, { children: allReactions.map(reaction => jsx(ReactionComponent, { reaction: reaction, enablePhysics: enablePhysics, glassEffect: glassEffect }, reaction.id)) }) }), jsx(AnimatePresence, { children: showPicker && jsx(ReactionPicker, { position: pickerPosition, reactionTypes: reactionTypes, onReactionSelect: emoji => { addReaction(emoji, pickerPosition); setShowPicker(false); }, onClose: () => setShowPicker(false), glassEffect: glassEffect }) }), enableShortcuts && jsxs("div", { className: 'absolute bottom-2 right-2 glass-surface-primary glass-p-2 glass-radius-sm glass-text-xs opacity-50', children: ["Press 1-", reactionTypes.length, " for quick reactions"] })] }); } // Individual reaction component function ReactionComponent({ reaction, enablePhysics, glassEffect }) { const prefersReducedMotion = useReducedMotion(); const { emoji, position, physics, intensity, timestamp } = reaction; const age = (Date.now() - timestamp) / 1000; // seconds // Physics animation const x = useMotionValue(position.x); const y = useMotionValue(position.y); const rotation = useMotionValue(physics?.rotation || 0); const scale = useMotionValue(physics?.scale || 1); useEffect(() => { if (!enablePhysics || !physics) return; let animationId; let startTime = Date.now(); const animate = () => { const now = Date.now(); const deltaTime = (now - startTime) / 1000; startTime = now; // Apply gravity and velocity physics.velocity.y += 980 * deltaTime; // Gravity // Update position x.set(x.get() + physics.velocity.x * deltaTime); y.set(y.get() + physics.velocity.y * deltaTime); // Apply rotation rotation.set(rotation.get() + physics.rotation * deltaTime); // Apply scaling based on age (fade out) const ageScale = Math.max(0, 1 - age * 0.2); scale.set((physics.scale || 1) * ageScale); // Continue animation if still visible if (ageScale > 0) { animationId = requestAnimationFrame(animate); } }; animationId = requestAnimationFrame(animate); return () => { if (animationId) { cancelAnimationFrame(animationId); } }; }, [enablePhysics, physics, age, x, y, rotation, scale]); return jsxs(motion.div, { className: cn("absolute select-none", glassEffect && "OptimizedGlass intensity={0.2} glassBlur={6} glass-blur-sm"), style: { x: enablePhysics ? x : position.x, y: enablePhysics ? y : position.y, rotate: enablePhysics ? rotation : 0, scale: enablePhysics ? scale : 1, fontSize: `${24 + intensity * 12}px` }, initial: { opacity: 0, scale: 0, rotate: -180 }, animate: prefersReducedMotion ? {} : { opacity: 1, scale: enablePhysics ? undefined : 1 + intensity * 0.5, rotate: enablePhysics ? undefined : 0 }, exit: { opacity: 0, scale: 0, rotate: 180 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: [jsx("span", { className: 'block transform -translate-x-1/2 -translate-y-1/2', children: emoji }), glassEffect && jsx(motion.div, { className: 'absolute inset-0 glass-gradient-primary glass-gradient-primary via-white glass-gradient-primary opacity-30', animate: prefersReducedMotion ? {} : { x: [-100, 100] }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 2, repeat: Infinity, ease: "linear" } })] }); } // Reaction picker component function ReactionPicker({ position, reactionTypes, onReactionSelect, onClose, glassEffect }) { const prefersReducedMotion = useReducedMotion(); const pickerRef = useRef(null); // Close on outside click useEffect(() => { const handleClickOutside = e => { if (pickerRef.current && !pickerRef.current.contains(e.target)) { onClose(); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [onClose]); // Close on escape key useEffect(() => { const handleKeyDown = e => { if (e.key === "Escape") { onClose(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [onClose]); return jsxs(motion.div, { ref: pickerRef, className: cn("absolute z-50 pointer-events-auto", glassEffect ? "glass-surface-primary glass-elev-3" : "bg-white shadow-lg", "glass-radius-lg glass-p-2"), style: { left: position.x, top: position.y, transform: "translate(-50%, -100%)" }, initial: { opacity: 0, scale: 0.8, y: 10 }, animate: prefersReducedMotion ? {} : { opacity: 1, scale: 1, y: 0 }, exit: { opacity: 0, scale: 0.8, y: 10 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: [jsx("div", { className: "glass-grid glass-grid-cols-4 glass-gap-1", children: reactionTypes.map((reactionType, index) => jsx(motion.button, { className: cn("w-10 h-10 flex items-center justify-center glass-radius-lg", "hover:bg-white/10 transition-colors glass-text-xl", glassEffect && "glass-button-secondary"), whileHover: { scale: 1.1 }, whileTap: { scale: 0.9 }, onClick: () => onReactionSelect(reactionType.emoji), title: `${reactionType.name} (${reactionType.shortcut})`, initial: { opacity: 0, scale: 0 }, animate: { opacity: 1, scale: 1 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: reactionType.emoji }, reactionType.emoji)) }), jsx("div", { className: cn("absolute top-full left-1/2 transform -translate-x-1/2", "w-0 h-0 border-l-4 border-r-4 border-t-4", "border-l-transparent border-r-transparent", glassEffect ? "border-t-white/20" : "border-t-white") })] }); } export { GlassReactions }; //# sourceMappingURL=GlassReactions.js.map