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