aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
388 lines (385 loc) • 16.4 kB
JavaScript
'use client';
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
import React, { useState, useRef, useEffect } from 'react';
import { GlassCore } from '../../primitives/GlassCore.js';
import '../../primitives/glass/GlassAdvanced.js';
import '../../primitives/OptimizedGlassCore.js';
import '../../primitives/glass/OptimizedGlassAdvanced.js';
import '../../primitives/MotionNative.js';
import '../../primitives/motion/MotionFramer.js';
import { useCollaboration } from './GlassCollaborationProvider.js';
import { cn } from '../../lib/utilsComprehensive.js';
const CommentBubble = ({
comment,
user,
onReply,
onResolve,
isOwner
}) => {
const [showReplies, setShowReplies] = useState(false);
const [replyText, setReplyText] = useState("");
const [isReplying, setIsReplying] = useState(false);
const bubbleRef = useRef(null);
const avatarRef = useRef(null);
const pointerRef = useRef(null);
// Apply dynamic positioning and colors without JSX style attribute
useEffect(() => {
const el = bubbleRef.current;
if (!el) return;
el.style.left = `${comment.position.x}px`;
el.style.top = `${comment.position.y}px`;
el.style.transform = "translate(-50%, -100%)";
}, [comment.position.x, comment.position.y]);
useEffect(() => {
const ptr = pointerRef.current;
if (!ptr) return;
ptr.style.left = "50%";
ptr.style.top = "100%";
ptr.style.transform = "translateX(-50%)";
ptr.style.filter = "drop-shadow(0 2px 4px rgba(var(--glass-color-black) / var(--glass-opacity-10)))";
}, []);
useEffect(() => {
if (avatarRef.current) {
avatarRef.current.style.backgroundColor = user?.color || "var(--glass-gray-500)";
}
}, [user?.color]);
const handleReply = () => {
if (replyText.trim()) {
onReply(comment.id, replyText.trim());
setReplyText("");
setIsReplying(false);
}
};
const formatTime = timestamp => {
const now = Date.now();
const diff = now - timestamp;
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`;
};
return jsx("div", {
"data-glass-component": true,
ref: bubbleRef,
className: cn("glass-absolute glass-z-40 glass-container-xs", comment.resolved && "opacity-60"),
children: jsxs("div", {
className: 'relative',
children: [jsxs(GlassCore, {
className: cn("p-3 mb-2 shadow-lg border-l-4 glass-contrast-guard", comment.resolved ? "border-gray-400" : "border-blue-500"),
children: [jsxs("div", {
className: 'glass-flex glass-items-start glass-justify-between mb-2',
children: [jsxs("div", {
className: "glass-flex glass-items-center glass-gap-2",
children: [jsx("div", {
ref: avatarRef,
className: 'w-6 h-6 glass-radius-full glass-flex glass-items-center glass-justify-center text-primary glass-text-xs font-medium',
children: user?.name?.[0]?.toUpperCase() || "?"
}), jsx("span", {
className: 'glass-text-sm font-medium glass-text-secondary',
children: user?.name || "Unknown User"
}), jsx("span", {
className: "glass-text-xs glass-text-secondary",
children: formatTime(comment.timestamp)
})]
}), jsxs("div", {
className: "glass-flex glass-gap-1",
children: [!comment.resolved && (isOwner || user?.id === comment.userId) && jsx("button", {
onClick: () => onResolve(comment.id),
className: 'text-primary hover:text-primary glass-text-xs glass-p-1 glass-focus glass-touch-target glass-contrast-guard',
title: "Resolve comment",
children: "\u2713"
}), comment.resolved && jsx("span", {
className: 'text-primary glass-text-xs',
children: "Resolved"
})]
})]
}), jsx("p", {
className: 'glass-text-sm glass-text-secondary mb-2',
children: comment.content
}), jsxs("div", {
className: "glass-flex glass-items-center glass-gap-3 glass-text-xs",
children: [jsx("button", {
onClick: () => setIsReplying(!isReplying),
className: 'text-primary hover:text-primary glass-focus glass-touch-target glass-contrast-guard',
children: "Reply"
}), (comment.replies?.length || 0) > 0 && jsxs("button", {
onClick: () => setShowReplies(!showReplies),
className: 'glass-text-secondary hover:glass-text-secondary glass-focus glass-touch-target glass-contrast-guard',
children: [showReplies ? "Hide" : "Show", " ", comment.replies?.length, " ", "replies"]
})]
})]
}), isReplying && jsxs(GlassCore, {
className: 'glass-p-3 mb-2 glass-surface-subtle glass-contrast-guard',
children: [jsx("textarea", {
value: replyText,
onChange: e => setReplyText(e.target.value),
placeholder: "Write a reply...",
className: 'glass-w-full glass-p-2 glass-text-sm glass-border glass-border-subtle glass-radius resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue',
rows: 2,
autoFocus: true
}), jsxs("div", {
className: 'glass-flex glass-justify-end glass-gap-2 mt-2',
children: [jsx("button", {
onClick: () => setIsReplying(false),
className: 'glass-px-3 glass-py-1 glass-text-xs glass-text-secondary hover:glass-text-secondary glass-focus glass-touch-target glass-contrast-guard',
children: "Cancel"
}), jsx("button", {
onClick: handleReply,
disabled: !replyText.trim(),
className: 'glass-px-3 glass-py-1 glass-text-xs glass-surface-blue text-primary glass-radius hover:glass-surface-blue disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-focus glass-touch-target glass-contrast-guard',
children: "Reply"
})]
})]
}), showReplies && comment.replies && comment.replies.length > 0 && jsx("div", {
className: 'ml-4 space-y-2',
children: comment.replies.map(reply => {
const replyUser = user; // In real app, would look up by reply.userId
return jsxs(GlassCore, {
className: "glass-p-2 glass-surface-subtle glass-contrast-guard",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 mb-1',
children: [jsx("div", {
ref: el => {
if (el) el.style.backgroundColor = replyUser?.color || "var(--glass-gray-500)";
},
className: 'w-4 h-4 glass-radius-full glass-flex glass-items-center glass-justify-center text-primary glass-text-xs',
children: replyUser?.name?.[0]?.toUpperCase() || "?"
}), jsx("span", {
className: 'glass-text-xs font-medium glass-text-secondary',
children: replyUser?.name || "Unknown User"
}), jsx("span", {
className: "glass-text-xs glass-text-secondary",
children: formatTime(reply.timestamp)
})]
}), jsx("p", {
className: "glass-text-xs glass-text-secondary",
children: reply.content
})]
}, reply.id);
})
}), jsx("div", {
ref: pointerRef,
className: 'absolute w-0 h-0 glass-border-l-4 glass-border-r-4 glass-border-t-4 glass-border-transparent glass-border-t-white'
})]
})
});
};
const CommentDot = ({
position,
color,
count = 1,
onClick,
resolved
}) => {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
el.style.left = `${position.x}px`;
el.style.top = `${position.y}px`;
el.style.transform = "translate(-50%, -50%)";
el.style.backgroundColor = resolved ? "var(--glass-gray-500)" : color;
}, [position.x, position.y, color, resolved]);
return jsx("button", {
ref: ref,
className: cn("glass-absolute glass-z-30 glass-w-6 glass-h-6 glass-radius-full glass-border glass-shadow-lg glass-transition glass-focus glass-touch-target glass-contrast-guard", resolved ? "opacity-60" : "animate-pulse"),
onClick: onClick,
"aria-label": resolved ? "Resolved comment" : "Comment",
children: jsx("span", {
className: 'text-primary glass-text-xs font-bold',
children: count
})
});
};
const GlassCollaborativeComments = ({
className,
allowComments = true
}) => {
const {
comments,
users,
currentUser,
addComment,
resolveComment,
replyToComment,
showComments
} = useCollaboration();
const [isAddingComment, setIsAddingComment] = useState(false);
const [newCommentPosition, setNewCommentPosition] = useState(null);
const [newCommentText, setNewCommentText] = useState("");
const [selectedComment, setSelectedComment] = useState(null);
const containerRef = useRef(null);
const newBubbleRef = useRef(null);
const newAvatarRef = useRef(null);
const newPointerRef = useRef(null);
// Position and color for the new comment bubble (no JSX style attr)
useEffect(() => {
const el = newBubbleRef.current;
if (!el || !newCommentPosition) return;
el.style.left = `${newCommentPosition.x}px`;
el.style.top = `${newCommentPosition.y}px`;
el.style.transform = "translate(-50%, -100%)";
}, [newCommentPosition]);
useEffect(() => {
if (newAvatarRef.current) {
newAvatarRef.current.style.backgroundColor = currentUser?.color || "var(--glass-gray-500)";
}
}, [currentUser?.color]);
useEffect(() => {
if (newPointerRef.current) {
const ptr = newPointerRef.current;
ptr.style.left = "50%";
ptr.style.top = "100%";
ptr.style.transform = "translateX(-50%)";
ptr.style.filter = "drop-shadow(0 2px 4px rgba(var(--glass-color-black) / var(--glass-opacity-10)))";
}
}, [isAddingComment, newCommentPosition]);
useEffect(() => {
if (!allowComments || !showComments) return;
const handleDoubleClick = e => {
if (!currentUser) return;
// Don't add comments on UI elements
const target = e.target;
if (target.closest("button, input, textarea, select, a")) return;
const rect = containerRef.current?.getBoundingClientRect();
if (!rect) return;
const position = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
setNewCommentPosition(position);
setIsAddingComment(true);
};
const container = containerRef.current;
if (container) {
container.addEventListener("dblclick", handleDoubleClick);
}
return () => {
if (container) {
container.removeEventListener("dblclick", handleDoubleClick);
}
};
}, [allowComments, showComments, currentUser]);
const handleAddComment = () => {
if (!newCommentText.trim() || !newCommentPosition || !currentUser) return;
addComment({
userId: currentUser.id,
content: newCommentText.trim(),
position: newCommentPosition
});
setNewCommentText("");
setIsAddingComment(false);
setNewCommentPosition(null);
};
const handleReply = (commentId, content) => {
if (!currentUser) return;
replyToComment(commentId, {
userId: currentUser.id,
content
});
};
const handleResolve = commentId => {
resolveComment(commentId);
};
// Group comments by position for clustering
const groupedComments = React.useMemo(() => {
const groups = {};
comments.forEach(comment => {
const key = `${Math.floor(comment.position.x / 50)}-${Math.floor(comment.position.y / 50)}`;
if (!groups[key]) groups[key] = [];
groups[key].push(comment);
});
return Object.values(groups);
}, [comments]);
if (!showComments || !allowComments) {
return null;
}
return jsxs("div", {
ref: containerRef,
className: cn("relative w-full h-full", className),
children: [groupedComments.map((group, index) => {
const firstComment = group[0];
const user = users.find(u => u.id === firstComment.userId) || currentUser;
const hasUnresolved = group.some(c => !c.resolved);
return jsx(CommentDot, {
position: firstComment.position,
color: user?.color || "var(--glass-gray-500)",
count: group.length,
resolved: !hasUnresolved,
onClick: () => setSelectedComment(selectedComment === firstComment.id ? null : firstComment.id)
}, `group-${index}`);
}), selectedComment && jsx(Fragment, {
children: comments.filter(comment => groupedComments.find(group => group.some(c => c.id === selectedComment) && group.includes(comment))).map(comment => {
const user = users.find(u => u.id === comment.userId) || currentUser;
return jsx(CommentBubble, {
comment: comment,
user: user,
onReply: handleReply,
onResolve: handleResolve,
isOwner: currentUser?.id === comment.userId
}, comment.id);
})
}), isAddingComment && newCommentPosition && jsxs("div", {
ref: newBubbleRef,
className: 'absolute glass-z-40 glass-container-xs',
children: [jsxs(GlassCore, {
className: "glass-p-3 glass-shadow-lg glass-border-l-4 glass-border-blue glass-contrast-guard",
children: [jsxs("div", {
className: 'glass-flex glass-items-center glass-gap-2 mb-2',
children: [jsx("div", {
ref: newAvatarRef,
className: 'w-6 h-6 glass-radius-full glass-flex glass-items-center glass-justify-center text-primary glass-text-xs font-medium',
children: currentUser?.name?.[0]?.toUpperCase() || "?"
}), jsx("span", {
className: 'glass-text-sm font-medium glass-text-secondary',
children: currentUser?.name || "You"
})]
}), jsx("textarea", {
value: newCommentText,
onChange: e => setNewCommentText(e.target.value),
placeholder: "Write a comment...",
className: 'glass-w-full glass-p-2 glass-text-sm glass-border glass-border-subtle glass-radius resize-none focus:ring-2 focus:ring-blue-500 focus:border-blue',
rows: 3,
autoFocus: true
}), jsxs("div", {
className: 'glass-flex glass-justify-end glass-gap-2 mt-2',
children: [jsx("button", {
onClick: () => {
setIsAddingComment(false);
setNewCommentPosition(null);
setNewCommentText("");
},
className: 'glass-px-3 glass-py-1 glass-text-xs glass-text-secondary hover:glass-text-secondary glass-focus glass-touch-target glass-contrast-guard',
children: "Cancel"
}), jsx("button", {
onClick: handleAddComment,
disabled: !newCommentText.trim(),
className: 'glass-px-3 glass-py-1 glass-text-xs glass-surface-blue text-primary glass-radius hover:glass-surface-blue disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-focus glass-touch-target glass-contrast-guard',
children: "Comment"
})]
})]
}), jsx("div", {
ref: newPointerRef,
className: 'absolute w-0 h-0 glass-border-l-4 glass-border-r-4 glass-border-t-4 glass-border-transparent glass-border-t-white'
})]
}), (selectedComment || isAddingComment) && jsx("div", {
className: 'fixed inset-0 z-20',
onClick: () => {
setSelectedComment(null);
if (isAddingComment) {
setIsAddingComment(false);
setNewCommentPosition(null);
setNewCommentText("");
}
}
}), comments.length === 0 && currentUser && jsx("div", {
className: 'absolute bottom-4 right-4 glass-surface-subtle text-primary glass-p-3 glass-radius-lg glass-text-sm max-w-xs',
children: "\uD83D\uDCA1 Double-click anywhere to add a comment"
})]
});
};
export { GlassCollaborativeComments };
//# sourceMappingURL=GlassCollaborativeComments.js.map