UNPKG

aura-glass

Version:

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

333 lines (330 loc) 11 kB
'use client'; import { jsxs, jsx } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { forwardRef, useState, useEffect, useMemo } from 'react'; import { motion } from 'framer-motion'; 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 statusColors = { online: "var(--glass-color-success)", away: "var(--glass-color-warning)", busy: "var(--glass-color-danger)", offline: "var(--glass-gray-500)" }; const GlassPresenceIndicator = /*#__PURE__*/forwardRef(({ users, maxVisible = 5, showAvatars = true, showNames = true, showStatus = true, showActivity = false, showLastSeen = true, showTypingIndicator = true, layout = "horizontal", size = "medium", groupSimilarStatus = false, realTimeSync = false, soundEnabled = true, animateChanges = true, theme = "auto", onUserClick, onStatusChange, className = "", ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const [presenceData, setPresenceData] = useState(users); const [typingUsers, setTypingUsers] = useState([]); const { play } = useGlassSound(); useA11yId("glass-presence"); const { shouldAnimate } = useMotionPreference(); // Helper function to respect motion preferences const respectMotionPreference = config => shouldAnimate ? config : { duration: 0 }; // Simulated real-time updates useEffect(() => { if (!realTimeSync) return; const interval = setInterval(() => { setPresenceData(prev => prev.map(user => ({ ...user, lastSeen: user.status !== "offline" ? new Date() : user.lastSeen, isTyping: Math.random() < 0.1 ? !user.isTyping : user.isTyping }))); }, 5000); return () => clearInterval(interval); }, [realTimeSync]); // Track typing users useEffect(() => { const typing = presenceData.filter(user => user.isTyping).map(user => user.id); setTypingUsers(typing); }, [presenceData]); // Sound notifications for status changes useEffect(() => { if (soundEnabled) { presenceData.forEach(user => { if (user.status === "online") { play("notification"); } }); } }, [presenceData, soundEnabled, play]); const processedUsers = useMemo(() => { let processedData = [...presenceData]; // Group similar statuses if enabled if (groupSimilarStatus) { processedData.sort((a, b) => { const statusOrder = { online: 0, away: 1, busy: 2, offline: 3 }; return statusOrder[a.status] - statusOrder[b.status]; }); } // Limit visible users const visibleUsers = processedData.slice(0, maxVisible); const hiddenCount = Math.max(0, processedData.length - maxVisible); return { visibleUsers, hiddenCount, totalOnline: processedData.filter(u => u.status === "online").length }; }, [presenceData, maxVisible, groupSimilarStatus]); const formatLastSeen = lastSeen => { if (!lastSeen) return "Never"; const now = new Date(); const diff = now.getTime() - lastSeen.getTime(); const minutes = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (minutes < 1) return "Just now"; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; return `${days}d ago`; }; const getSizeClasses = () => { switch (size) { case "small": return "text-xs"; case "large": return "text-lg"; default: return "text-sm"; } }; const getLayoutClasses = () => { switch (layout) { case "vertical": return "flex flex-col space-y-2"; case "grid": return "grid grid-cols-2 gap-2"; case "stack": return "flex flex-col space-y-1"; default: return "flex flex-wrap gap-2"; } }; const UserPresence = ({ user, index }) => jsxs(motion.div, { layout: animateChanges, initial: { opacity: 0, scale: 0.8 }, animate: prefersReducedMotion ? {} : { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, transition: respectMotionPreference({ duration: 0.3, delay: index * 0.05 }), className: ` flex items-center space-x-2 p-2 rounded-lg cursor-pointer ${createGlassStyle({ variant: "default" })} hover:bg-white/10 transition-colors duration-200 `, onClick: () => onUserClick?.(user.id), children: [showAvatars && jsxs("div", { className: 'relative', children: [jsx("div", { className: ` ${size === "small" ? "w-6 h-6" : size === "large" ? "w-12 h-12" : "w-8 h-8"} rounded-full bg-gradient-to-br from-gray-300 to-gray-500 flex items-center justify-center text-white font-semibold ${createGlassStyle({ variant: "default" })} `, children: user.avatar ? jsx("img", { src: user.avatar, alt: user.name, className: 'glass-w-full glass-h-full glass-radius-full object-cover' }) : user.name.charAt(0).toUpperCase() }), showStatus && jsx(motion.div, { className: ` absolute -bottom-0.5 -right-0.5 ${size === "small" ? "w-3 h-3" : size === "large" ? "w-4 h-4" : "w-3 h-3"} rounded-full border-2 border-white `, style: { backgroundColor: statusColors[user.status] }, animate: user.status === "online" ? { scale: [1, 1.2, 1] } : {}, transition: respectMotionPreference({ duration: 2, repeat: Infinity, repeatType: "loop" }) })] }), jsxs("div", { className: "glass-flex-1 glass-min-w-0", children: [showNames && jsxs("div", { className: 'glass-flex glass-items-center space-x-1', children: [jsx("p", { className: `font-medium text-white/90 truncate ${getSizeClasses()}`, children: user.name }), user.customStatus?.emoji && jsx("span", { className: "glass-text-xs", children: user.customStatus.emoji })] }), showActivity && user.activity && jsx("p", { className: `text-white/60 truncate ${size === "large" ? "text-sm" : "text-xs"}`, children: user.activity }), user.customStatus?.text && jsx("p", { className: `text-white/60 truncate ${size === "large" ? "text-sm" : "text-xs"}`, children: user.customStatus.text }), showLastSeen && user.status === "offline" && jsx("p", { className: `text-white/40 ${size === "large" ? "text-sm" : "text-xs"}`, children: formatLastSeen(user.lastSeen) })] }), showTypingIndicator && user.isTyping && jsx(motion.div, { className: 'glass-flex space-x-1', animate: prefersReducedMotion ? {} : { opacity: [0.4, 1, 0.4] }, transition: respectMotionPreference({ duration: 1.5, repeat: Infinity, ease: "easeInOut" }), children: [0, 1, 2].map(i => jsx(motion.div, { className: 'w-1.5 h-1.5 glass-surface-blue glass-radius-full', animate: prefersReducedMotion ? {} : { y: [-2, 0, -2] }, transition: respectMotionPreference({ duration: 0.6, repeat: Infinity, delay: i * 0.1, ease: "easeInOut" }) }, i)) }), showStatus && !showAvatars && jsx("div", { className: 'w-3 h-3 glass-radius-full', style: { backgroundColor: statusColors[user.status] } })] }, user.id); return jsxs(OptimizedGlassCore, { ref: ref, intensity: "subtle", className: `p-4 ${className}`, ...props, children: [jsxs("div", { className: getLayoutClasses(), children: [processedUsers.visibleUsers.map((user, index) => jsx(UserPresence, { user: user, index: index }, user.id)), processedUsers.hiddenCount > 0 && jsxs(motion.div, { className: ` flex items-center space-x-2 p-2 rounded-lg text-white/60 ${createGlassStyle({ variant: "default" })} `, initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, transition: respectMotionPreference({ delay: 0.3 }), children: [jsxs("div", { className: ` ${size === "small" ? "w-6 h-6" : size === "large" ? "w-12 h-12" : "w-8 h-8"} rounded-full bg-white/20 flex items-center justify-center ${getSizeClasses()} font-medium `, children: ["+", processedUsers.hiddenCount] }), jsxs("span", { className: getSizeClasses(), children: [processedUsers.hiddenCount, " more", " ", processedUsers.hiddenCount === 1 ? "user" : "users"] })] })] }), showTypingIndicator && typingUsers.length > 0 && jsx(motion.div, { className: 'mt-3 pt-3 glass-border-t glass-border-white/10', initial: { opacity: 0, y: 10 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, exit: { opacity: 0, y: 10 }, children: jsx("p", { className: 'glass-text-xs text-primary/60', children: typingUsers.length === 1 ? `${presenceData.find(u => u.id === typingUsers[0])?.name} is typing...` : typingUsers.length === 2 ? `${presenceData.find(u => u.id === typingUsers[0])?.name} and ${presenceData.find(u => u.id === typingUsers[1])?.name} are typing...` : `${typingUsers.length} people are typing...` }) }), jsxs(motion.div, { className: 'mt-3 pt-3 glass-border-t glass-border-white/10 glass-flex glass-justify-between glass-items-center glass-text-xs text-primary/50', initial: { opacity: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1 }, transition: respectMotionPreference({ delay: 0.5 }), children: [jsxs("span", { children: [processedUsers.totalOnline, " online"] }), jsxs("span", { children: [users.length, " total"] })] })] }); }); export { GlassPresenceIndicator }; //# sourceMappingURL=GlassPresenceIndicator.js.map