UNPKG

aura-glass

Version:

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

461 lines (458 loc) 14.9 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { forwardRef, useState, useRef, useEffect, useCallback, useMemo } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; import '../../primitives/GlassCore.js'; import '../../primitives/glass/GlassAdvanced.js'; import { OptimizedGlassCore } from '../../primitives/OptimizedGlassCore.js'; import '../../primitives/glass/OptimizedGlassAdvanced.js'; import '../../primitives/MotionNative.js'; import '../../primitives/motion/MotionFramer.js'; import { useGlassSound } from '../../utils/soundDesign.js'; import { useA11yId } from '../../utils/a11y.js'; import { useMotionPreference } from '../../hooks/useMotionPreference.js'; import { createGlassStyle } from '../../utils/createGlassStyle.js'; const defaultEmojis = ['❤️', '😀', '😂', '🎉', '👏', '🔥', '💯', '⭐', '👍', '👎', '😍', '🤔', '😮', '😢', '💪', '🙌']; const reactionColors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3', '#54A0FF', '#5F27CD', '#00D2D3', '#FF9F43']; const createSeededRandom = (seed = 1234) => { let value = seed >>> 0; return () => { value = (value * 1664525 + 1013904223) % 4294967296; return value / 4294967296; }; }; const GlassReactionBubbles = /*#__PURE__*/forwardRef(({ width = 600, height = 400, reactions = [], availableEmojis = defaultEmojis, showControls = true, showUserNames = true, bubbleLifetime = 5000, maxBubbles = 50, gravity = 0.1, windForce = 0.05, bounceEnabled = true, fadeOut = true, soundEnabled = true, realTimeMode = false, interactive = true, onReactionAdd, onReactionClick, className = '', ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const [bubbles, setBubbles] = useState(reactions); const [selectedEmoji, setSelectedEmoji] = useState(availableEmojis[0]); useState(false); const { play } = useGlassSound(); useA11yId('glass-reaction-bubbles'); const { shouldAnimate } = useMotionPreference(); const randomRef = useRef(); const isTestEnv = typeof process !== 'undefined' && process.env?.JEST_WORKER_ID !== undefined; if (!randomRef.current) { randomRef.current = process.env.NODE_ENV === 'test' ? createSeededRandom() : () => Math.random(); } const random = randomRef.current; // Simulated reactions in real-time mode useEffect(() => { if (isTestEnv) return; if (!realTimeMode) return; const interval = setInterval(() => { if (random() < 0.3) { const emoji = availableEmojis[Math.floor(random() * availableEmojis.length)]; const x = random() * (width - 40) + 20; const y = random() * (height - 40) + 20; addReaction(emoji, x, y, `user-${Date.now()}`, 'Anonymous'); } }, 2000); return () => clearInterval(interval); }, [realTimeMode, availableEmojis, width, height]); // Physics simulation useEffect(() => { if (isTestEnv) return; if (bubbles.length === 0) return; const animationFrame = requestAnimationFrame(() => { setBubbles(prev => prev.map(bubble => { // Update lifetime const newLife = bubble.life - 16; // Assuming ~60fps if (newLife <= 0) return null; // Update position based on physics const newVelocity = { ...bubble.velocity }; newVelocity.y += gravity; // Apply gravity newVelocity.x += (random() - 0.5) * windForce; // Apply wind let newX = bubble.x + newVelocity.x; let newY = bubble.y + newVelocity.y; // Bounce off walls if enabled if (bounceEnabled) { if (newX <= 0 || newX >= width - 40) { newVelocity.x *= -0.7; // Damping on bounce newX = Math.max(0, Math.min(width - 40, newX)); } if (newY <= 0 || newY >= height - 40) { newVelocity.y *= -0.7; // Damping on bounce newY = Math.max(0, Math.min(height - 40, newY)); } } else { // Wrap around if (newX < -40) newX = width; if (newX > width) newX = -40; if (newY < -40) newY = height; if (newY > height) newY = -40; } return { ...bubble, x: newX, y: newY, velocity: newVelocity, life: newLife }; }).filter(bubble => bubble !== null)); }); return () => cancelAnimationFrame(animationFrame); }, [bubbles, gravity, windForce, bounceEnabled, width, height]); const addReaction = useCallback((emoji, x, y, userId, userName) => { const newBubble = { id: `reaction-${Date.now()}-${random()}`, emoji, userId: userId || 'current', userName: userName || 'You', userColor: reactionColors[Math.floor(random() * reactionColors.length)], x: x ?? random() * (width - 40) + 20, y: y ?? random() * (height - 40) + 20, timestamp: Date.now(), size: 30 + random() * 20, velocity: { x: (random() - 0.5) * 4, y: (random() - 0.5) * 4 - 2 // Slight upward bias }, life: bubbleLifetime, maxLife: bubbleLifetime }; setBubbles(prev => { const updated = [...prev, newBubble].slice(-maxBubbles); return updated; }); if (soundEnabled) { play('notification'); } onReactionAdd?.(emoji, newBubble.x, newBubble.y); }, [width, height, bubbleLifetime, maxBubbles, soundEnabled, play, onReactionAdd]); const handleCanvasClick = useCallback(e => { if (!interactive) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; addReaction(selectedEmoji, x, y); }, [interactive, selectedEmoji, addReaction]); const handleBubbleClick = useCallback((bubble, e) => { e.stopPropagation(); onReactionClick?.(bubble); // Add a small burst effect for (let i = 0; i < 3; i++) { addReaction(bubble.emoji, bubble.x + (random() - 0.5) * 20, bubble.y + (random() - 0.5) * 20); } }, [onReactionClick, addReaction]); const getBubbleOpacity = bubble => { if (!fadeOut) return 1; return Math.max(0.1, bubble.life / bubble.maxLife); }; const getBubbleScale = bubble => { const ageRatio = 1 - bubble.life / bubble.maxLife; return 0.8 + Math.sin(ageRatio * Math.PI) * 0.4; }; const ReactionBubbleComponent = ({ bubble }) => jsx(motion.div, { className: cn('glass-absolute glass-cursor-pointer glass-select-none glass-z-10'), style: { left: bubble.x, top: bubble.y, fontSize: bubble.size || 30 }, initial: { scale: 0, opacity: 0 }, animate: prefersReducedMotion ? {} : { scale: getBubbleScale(bubble), opacity: getBubbleOpacity(bubble), rotate: Math.sin(Date.now() / 1000 + bubble.timestamp) * 10 }, exit: { scale: 0, opacity: 0, y: bubble.y - 50 }, transition: shouldAnimate ? { type: 'spring', stiffness: 300, damping: 20 } : { duration: 0 }, onClick: e => handleBubbleClick(bubble, e), whileHover: { scale: getBubbleScale(bubble) * 1.1 }, whileTap: { scale: getBubbleScale(bubble) * 0.9 }, children: jsxs("div", { className: ` relative inline-flex items-center justify-center rounded-full ${createGlassStyle({ blur: 'sm', opacity: 0.8 }).background} border border-white/20 `, children: [jsx("span", { className: cn('glass-text-2xl'), children: bubble.emoji }), showUserNames && jsx(motion.div, { className: cn('glass-absolute glass-bottom-8-neg glass-left-1/2 glass-transform glass-translate-x-1/2-neg'), initial: { opacity: 0, y: 10 }, animate: prefersReducedMotion ? {} : { opacity: 0.8, y: 0 }, exit: { opacity: 0, y: 10 }, transition: shouldAnimate ? { delay: 0.2 } : { duration: 0 }, children: jsx("div", { className: ` px-2 py-1 text-xs font-medium text-white rounded ${createGlassStyle({ blur: 'sm', opacity: 0.8 }).background} border border-white/20 whitespace-nowrap `, children: bubble.userName }) }), jsx(motion.div, { className: cn('glass-absolute glass-inset-0 glass-radius-full'), style: { background: `radial-gradient(circle, ${bubble.userColor || '#FF6B6B'}40 0%, transparent 70%)` }, animate: prefersReducedMotion ? {} : { scale: [1, 1.5, 1], opacity: [0.3, 0.1, 0.3] }, transition: shouldAnimate ? { duration: 2, repeat: Infinity, ease: 'easeInOut' } : { duration: 0 } })] }) }); const EmojiSelector = () => jsx(motion.div, { className: ` flex flex-wrap gap-2 p-3 rounded-lg max-w-xs ${createGlassStyle({ blur: 'sm', opacity: 0.8 }).background} `, initial: { opacity: 0, y: 20 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, transition: shouldAnimate ? { duration: 0.3 } : { duration: 0 }, children: availableEmojis.map(emoji => jsx("button", { onClick: () => setSelectedEmoji(emoji), className: ` w-10 h-10 rounded-lg flex items-center justify-center text-xl transition-all duration-200 hover:scale-110 glass-focus glass-touch-target glass-contrast-guard ${selectedEmoji === emoji ? 'bg-white/20 ring-2 ring-blue-400/50' : 'hover:bg-white/10'} `, children: emoji }, emoji)) }); const stats = { totalReactions: bubbles.length, recentReactions: bubbles.filter(b => Date.now() - b.timestamp < 5000).length, mostUsedEmoji: bubbles.reduce((acc, bubble) => { acc[bubble.emoji] = (acc[bubble.emoji] || 0) + 1; return acc; }, {}) }; const mostUsed = Object.entries(stats.mostUsedEmoji).sort(([, a], [, b]) => b - a)[0]; const ambientParticles = useMemo(() => { if (isTestEnv) { return Array.from({ length: 5 }, (_, i) => ({ left: width / 6 * (i + 1), top: height / 6 * (i + 1), duration: 3 + i * 0.25, delay: i * 0.2 })); } return Array.from({ length: 5 }, () => ({ left: random() * width, top: random() * height, duration: 3 + random() * 2, delay: random() * 2 })); }, [height, isTestEnv, random, width]); return jsxs(OptimizedGlassCore, { ref: ref, intensity: "subtle", className: cn('glass-relative glass-overflow-hidden', className), style: { width, height }, ...props, children: [jsxs("div", { className: cn('glass-absolute glass-inset-0 glass-cursor-crosshair'), onClick: handleCanvasClick, style: { width, height }, children: [jsx(AnimatePresence, { children: bubbles.map(bubble => jsx(ReactionBubbleComponent, { bubble: bubble }, bubble.id)) }), ambientParticles.map((particle, i) => jsx(motion.div, { className: cn('glass-absolute glass-w-2 glass-h-2 glass-surface-muted glass-radius-full'), style: { left: particle.left, top: particle.top }, animate: prefersReducedMotion ? {} : { y: [0, -20, 0], opacity: [0.2, 0.8, 0.2], scale: [0.5, 1, 0.5] }, transition: shouldAnimate ? { duration: particle.duration, repeat: Infinity, delay: particle.delay, ease: 'easeInOut' } : { duration: 0 } }, i))] }), showControls && jsx("div", { className: cn('glass-absolute glass-top-4 glass-left-4 glass-z-20'), children: jsx(EmojiSelector, {}) }), jsx(motion.div, { className: ` absolute top-4 right-4 z-20 p-3 rounded-lg ${createGlassStyle({ blur: 'sm', opacity: 0.8 }).background} `, initial: { opacity: 0, x: 20 }, animate: prefersReducedMotion ? {} : { opacity: 1, x: 0 }, transition: shouldAnimate ? { delay: 0.5 } : { duration: 0 }, children: jsxs("div", { className: cn('glass-text-sm glass-text-secondary glass-space-y-1'), children: [jsxs("div", { className: cn('glass-flex glass-items-center glass-space-x-2'), children: [jsx("span", { children: stats.totalReactions }), jsx("span", { className: cn('glass-text-muted'), children: "total" })] }), jsxs("div", { className: cn('glass-flex glass-items-center glass-space-x-2'), children: [jsx("span", { children: stats.recentReactions }), jsx("span", { className: cn('glass-text-muted'), children: "recent" })] }), mostUsed && jsxs("div", { className: cn('glass-flex glass-items-center glass-space-x-2'), children: [jsx("span", { children: mostUsed[0] }), jsxs("span", { className: cn('glass-text-muted'), children: [mostUsed[1], "x"] })] })] }) }), interactive && showControls && jsxs(motion.div, { className: ` absolute bottom-4 left-1/2 transform -translate-x-1/2 z-20 px-4 py-2 rounded-lg text-sm text-white/70 ${createGlassStyle({ blur: 'sm', opacity: 0.8 }).background} `, initial: { opacity: 0, y: 20 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, transition: shouldAnimate ? { delay: 1 } : { duration: 0 }, children: ["Click anywhere to add ", selectedEmoji, " \u2022 Click bubbles to multiply them"] }), realTimeMode && jsxs("div", { className: cn('glass-absolute glass-bottom-4 glass-right-4 glass-z-20 glass-flex glass-items-center glass-space-x-2 glass-text-sm glass-text-muted'), children: [jsx("div", { className: cn('glass-w-2 glass-h-2 glass-surface-success glass-radius-full') }), jsx("span", { children: "Live reactions" })] })] }); }); export { GlassReactionBubbles }; //# sourceMappingURL=GlassReactionBubbles.js.map