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