UNPKG

aura-glass

Version:

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

388 lines (385 loc) 16.4 kB
'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