aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
504 lines (501 loc) • 17.4 kB
JavaScript
'use client';
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { Circle, Clock, Users, Activity, MapPin, MessageCircle, Phone, Video, MoreHorizontal } from 'lucide-react';
import { useState, useCallback } from 'react';
import '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import { MotionFramer } from '../../primitives/motion/MotionFramer.js';
import { GlassButton } from '../button/GlassButton.js';
import '../button/GlassFab.js';
import '../button/GlassMagneticButton.js';
import { CardHeader, CardTitle, CardContent } from '../card/index.js';
import '../data-display/GlassAccordion.js';
import '../data-display/GlassAlert.js';
import '../data-display/GlassAvatar.js';
import { GlassBadge } from '../data-display/GlassBadge.js';
import '../data-display/GlassBadgeLine.js';
import '../data-display/GlassDataGrid.js';
import '../data-display/GlassDataTable.js';
import '../data-display/GlassHeatmap.js';
import '../data-display/GlassLoadingSkeleton.js';
import '../data-display/GlassProgress.js';
import '../data-display/GlassTimeline.js';
import '../data-display/GlassSkeleton.js';
import '../data-display/GlassNotificationCenter.js';
import '../data-display/GlassAnimatedNumber.js';
import { GlassCard } from '../card/GlassCard.js';
/**
* GlassUserPresence component
* Display user presence status with avatars and online indicators
*/
const GlassUserPresence = ({
currentUser,
users,
showOnlineCount = true,
showRoles = false,
showLocations = false,
showActivities = false,
enableActions = false,
enableStatusUpdate = false,
groupByStatus = false,
compact = false,
maxUsers,
onUserClick,
onUserAction,
onStatusChange,
className,
...props
}) => {
const [showAllUsers, setShowAllUsers] = useState(false);
// Get status color and icon
const getStatusConfig = useCallback(status => {
switch (status) {
case "online":
return {
color: "bg-green-400",
borderColor: "border-green-400",
textColor: "text-green-400",
icon: Circle,
label: "Online"
};
case "away":
return {
color: "bg-yellow-400",
borderColor: "border-yellow-400",
textColor: "text-yellow-400",
icon: Clock,
label: "Away"
};
case "busy":
return {
color: "bg-red-400",
borderColor: "border-red-400",
textColor: "text-red-400",
icon: Circle,
label: "Busy"
};
case "invisible":
return {
color: "bg-gray-400",
borderColor: "border-gray-400",
textColor: "glass-text-secondary",
icon: Circle,
label: "Invisible"
};
default:
return {
color: "bg-gray-400",
borderColor: "border-gray-400",
textColor: "glass-text-secondary",
icon: Circle,
label: "Offline"
};
}
}, []);
// Get role badge
useCallback(role => {
switch (role) {
case "admin":
return jsx(GlassBadge, {
variant: "destructive",
size: "sm",
children: "Admin"
});
case "moderator":
return jsx(GlassBadge, {
variant: "warning",
size: "sm",
children: "Mod"
});
case "member":
return jsx(GlassBadge, {
variant: "secondary",
size: "sm",
children: "Member"
});
case "guest":
return jsx(GlassBadge, {
variant: "outline",
size: "sm",
children: "Guest"
});
default:
return null;
}
}, []);
// Format last seen time
useCallback(date => {
const now = new Date();
const diff = now.getTime() - date.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`;
if (days === 1) return "Yesterday";
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
}, []);
// Handle status change
const handleStatusChange = useCallback(status => {
onStatusChange?.(status);
}, [onStatusChange]);
// Filter and sort users
const processedUsers = users.filter(user => user.status !== "invisible" || user.id === currentUser?.id).sort((a, b) => {
// Sort by status priority
const statusOrder = {
online: 0,
away: 1,
busy: 2,
offline: 3,
invisible: 4
};
const aOrder = statusOrder[a.status] ?? 3;
const bOrder = statusOrder[b.status] ?? 3;
if (aOrder !== bOrder) return aOrder - bOrder;
// Then by name
return a.name.localeCompare(b.name);
});
const displayUsers = showAllUsers || !maxUsers ? processedUsers : processedUsers.slice(0, maxUsers);
const onlineCount = users.filter(user => user.status === "online").length;
// Group users by status
const groupedUsers = processedUsers.reduce((groups, user) => {
if (!groups[user.status]) {
groups[user.status] = [];
}
groups[user.status].push(user);
return groups;
}, {});
return jsx(MotionFramer, {
"data-glass-component": true,
preset: "fadeIn",
className: "glass-w-full",
children: jsxs(GlassCard, {
className: cn("overflow-hidden", className),
...props,
children: [jsx(CardHeader, {
className: 'pb-3',
children: jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs(CardTitle, {
className: 'text-primary glass-text-lg font-semibold glass-flex glass-items-center glass-gap-2',
children: [jsx(Users, {
className: 'w-5 h-5'
}), "Team Presence"]
}), showOnlineCount && jsxs(GlassBadge, {
variant: "secondary",
children: [onlineCount, " online"]
})]
})
}), jsxs(CardContent, {
className: 'pt-0',
children: [currentUser && enableStatusUpdate && jsx("div", {
className: 'mb-6 glass-p-3 glass-surface-subtle/5 glass-radius-lg',
children: jsxs("div", {
className: "glass-flex glass-items-center glass-justify-between",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3",
children: [jsxs("div", {
className: 'relative',
children: [jsx("div", {
className: 'w-10 h-10 glass-radius-full glass-surface-subtle/20 glass-flex glass-items-center glass-justify-center',
children: currentUser.avatar ? jsx("img", {
src: currentUser.avatar,
alt: currentUser.name,
className: 'glass-w-full glass-h-full glass-radius-full object-cover'
}) : jsx("span", {
className: 'text-primary font-medium',
children: currentUser.name.charAt(0).toUpperCase()
})
}), jsx("div", {
className: cn("absolute -bottom-1 -right-1 w-4 h-4 glass-radius-full border-2 border-black", getStatusConfig(currentUser.status).color)
})]
}), jsxs("div", {
children: [jsx("p", {
className: 'text-primary font-medium',
children: currentUser.name
}), jsx("p", {
className: 'text-primary/60 glass-text-sm',
children: "Set your status"
})]
})]
}), jsx("div", {
className: "glass-flex glass-gap-1",
children: ["online", "away", "busy", "invisible"].map(status => {
const config = getStatusConfig(status);
const IconComponent = config.icon;
return jsx(GlassButton, {
variant: currentUser.status === status ? "primary" : "ghost",
size: "sm",
onClick: e => handleStatusChange(status),
className: "glass-p-2",
title: config.label,
children: jsx(IconComponent, {
className: 'w-3 h-3'
})
}, status);
})
})]
})
}), groupByStatus ?
// Grouped by status
jsx("div", {
className: "glass-gap-4",
children: Object.entries(groupedUsers).map(([status, statusUsers]) => {
const config = getStatusConfig(status);
return jsxs("div", {
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 mb-2',
children: [jsx("div", {
className: cn("w-2 h-2 glass-radius-full", config.color)
}), jsxs("span", {
className: 'text-primary/80 glass-text-sm font-medium',
children: [config.label, " (", statusUsers.length, ")"]
})]
}), jsx("div", {
className: "glass-gap-2",
children: statusUsers.map(user => jsx(UserPresenceItem, {
user: user,
compact: compact,
showRoles: showRoles,
showLocations: showLocations,
showActivities: showActivities,
enableActions: enableActions,
onUserClick: onUserClick,
onUserAction: onUserAction
}, user.id))
})]
}, status);
})
}) :
// Flat list
jsxs("div", {
className: "glass-gap-2",
children: [displayUsers.map(user => jsx(UserPresenceItem, {
user: user,
compact: compact,
showRoles: showRoles,
showLocations: showLocations,
showActivities: showActivities,
enableActions: enableActions,
onUserClick: onUserClick,
onUserAction: onUserAction
}, user.id)), maxUsers && users.length > maxUsers && !showAllUsers && jsxs(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => setShowAllUsers(true),
className: "glass-w-full glass-mt-2",
children: ["Show ", users.length - maxUsers, " more"]
})]
})]
})]
})
});
};
const UserPresenceItem = ({
user,
compact = false,
showRoles = false,
showLocations = false,
showActivities = false,
enableActions = false,
onUserClick,
onUserAction
}) => {
const config = getStatusConfig(user.status);
// Helper function (defined here since it's used in this component)
function getStatusConfig(status) {
switch (status) {
case "online":
return {
color: "bg-green-400",
textColor: "text-green-400",
label: "Online"
};
case "away":
return {
color: "bg-yellow-400",
textColor: "text-yellow-400",
label: "Away"
};
case "busy":
return {
color: "bg-red-400",
textColor: "text-red-400",
label: "Busy"
};
case "invisible":
return {
color: "bg-gray-400",
textColor: "glass-text-secondary",
label: "Invisible"
};
default:
return {
color: "bg-gray-400",
textColor: "glass-text-secondary",
label: "Offline"
};
}
}
const formatLastSeen = date => {
const now = new Date();
const diff = now.getTime() - date.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`;
if (days === 1) return "Yesterday";
if (days < 7) return `${days}d ago`;
return date.toLocaleDateString();
};
const getRoleBadge = role => {
switch (role) {
case "admin":
return jsx(GlassBadge, {
variant: "destructive",
size: "sm",
children: "Admin"
});
case "moderator":
return jsx(GlassBadge, {
variant: "warning",
size: "sm",
children: "Mod"
});
case "member":
return jsx(GlassBadge, {
variant: "secondary",
size: "sm",
children: "Member"
});
case "guest":
return jsx(GlassBadge, {
variant: "outline",
size: "sm",
children: "Guest"
});
default:
return null;
}
};
return jsxs(MotionFramer, {
preset: "fadeIn",
className: cn("flex items-center glass-gap-3 glass-p-2 glass-radius-lg hover:bg-white/5 cursor-pointer transition-all duration-200", compact && "glass-p-1"),
onClick: e => onUserClick?.(user),
children: [jsxs("div", {
className: 'relative glass-flex-shrink-0',
children: [jsx("div", {
className: cn("glass-radius-full bg-white/20 flex items-center justify-center", compact ? "w-8 h-8" : "w-10 h-10"),
children: user.avatar ? jsx("img", {
src: user.avatar,
alt: user.name,
className: 'glass-w-full glass-h-full glass-radius-full object-cover'
}) : jsx("span", {
className: cn("glass-text-primary font-medium", compact ? "glass-text-sm" : "glass-text-base"),
children: user.name.charAt(0).toUpperCase()
})
}), jsx("div", {
className: cn("absolute -bottom-1 -right-1 w-3 h-3 glass-radius-full border-2 border-black", config.color, compact && "w-2 h-2 -bottom-0.5 -right-0.5")
})]
}), jsxs("div", {
className: "glass-flex-1 glass-min-w-0",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("p", {
className: cn("glass-text-primary font-medium truncate", compact ? "glass-text-sm" : "glass-text-base"),
children: user.name
}), showRoles && getRoleBadge(user.role)]
}), !compact && jsxs("div", {
className: "glass-gap-1",
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("span", {
className: cn("glass-text-xs", config.textColor),
children: config.label
}), user.status === "offline" && user.lastSeen && jsxs(Fragment, {
children: [jsx("span", {
className: 'text-primary/40 glass-text-xs',
children: "\u2022"
}), jsx("span", {
className: 'text-primary/60 glass-text-xs',
children: formatLastSeen(user.lastSeen)
})]
})]
}), user.customStatus && jsx("p", {
className: 'text-primary/70 glass-text-xs truncate',
children: user.customStatus
}), showActivities && user.activity && jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-1 text-primary/60 glass-text-xs',
children: [jsx(Activity, {
className: 'w-3 h-3'
}), jsx("span", {
className: 'truncate',
children: user.activity
})]
}), showLocations && user.location && jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-1 text-primary/60 glass-text-xs',
children: [jsx(MapPin, {
className: 'w-3 h-3'
}), jsx("span", {
className: 'truncate',
children: user.location
})]
})]
})]
}), enableActions && jsxs("div", {
className: "glass-flex glass-items-center glass-gap-1",
children: [jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => {
e.stopPropagation();
onUserAction?.(user.id, "message");
},
className: "glass-p-1",
children: jsx(MessageCircle, {
className: 'w-3 h-3'
})
}), jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => {
e.stopPropagation();
onUserAction?.(user.id, "call");
},
className: "glass-p-1",
children: jsx(Phone, {
className: 'w-3 h-3'
})
}), jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => {
e.stopPropagation();
onUserAction?.(user.id, "video");
},
className: "glass-p-1",
children: jsx(Video, {
className: 'w-3 h-3'
})
}), jsx(GlassButton, {
variant: "ghost",
size: "sm",
onClick: e => e.stopPropagation(),
className: "glass-p-1",
children: jsx(MoreHorizontal, {
className: 'w-3 h-3'
})
})]
})]
});
};
export { GlassUserPresence, GlassUserPresence as default };
//# sourceMappingURL=GlassUserPresence.js.map