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