UNPKG

aura-glass

Version:

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

410 lines (407 loc) 13 kB
'use client'; import { jsxs, jsx, Fragment } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { useRef, useState, useCallback, useEffect } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; import { cn } from '../../lib/utilsComprehensive.js'; function GlassLiveCursorPresence({ children, className, roomId, currentUser, connectionUrl = "ws://localhost:8080", maxUsers = 20, showTrails = true, showLabels = true, cursorSize = 16, trailLength = 8, fadeTimeout = 5000, smoothing = 0.7, glassEffect = true, onUserJoin, onUserLeave, onCursorMove }) { useReducedMotion(); const containerRef = useRef(null); const wsRef = useRef(null); const [cursors, setCursors] = useState(new Map()); const [isConnected, setIsConnected] = useState(false); const [reconnectAttempts, setReconnectAttempts] = useState(0); // Local cursor tracking const lastPosition = useRef({ x: 0, y: 0 }); const lastSentTime = useRef(0); const sendThrottle = 50; // ms // WebSocket connection management const connect = useCallback(() => { try { const ws = new WebSocket(`${connectionUrl}/cursor-presence/${roomId}`); ws.onopen = () => { setIsConnected(true); setReconnectAttempts(0); // Send user join event ws.send(JSON.stringify({ type: "user:join", user: currentUser, timestamp: Date.now() })); }; ws.onmessage = event => { try { const message = JSON.parse(event.data); handleWebSocketMessage(message); } catch (error) { console.warn("Invalid WebSocket message:", error); } }; ws.onclose = () => { setIsConnected(false); // Attempt reconnection with exponential backoff if (reconnectAttempts < 5) { const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000); setTimeout(connect, delay); setReconnectAttempts(prev => prev + 1); } }; ws.onerror = error => { console.warn("WebSocket error:", error); }; wsRef.current = ws; } catch (error) { console.warn("Failed to connect to cursor presence server:", error); } }, [connectionUrl, roomId, currentUser, reconnectAttempts]); // Handle WebSocket messages const handleWebSocketMessage = useCallback(message => { switch (message.type) { case "user:join": if (message.user.id !== currentUser.id) { setCursors(prev => { const newCursors = new Map(prev); newCursors.set(message.user.id, { ...message.user, position: { x: 0, y: 0, timestamp: Date.now() }, velocity: { x: 0, y: 0 }, isVisible: false, trail: [] }); return newCursors; }); onUserJoin?.(message.user); } break; case "user:leave": setCursors(prev => { const newCursors = new Map(prev); newCursors.delete(message.userId); return newCursors; }); onUserLeave?.(message.userId); break; case "cursor:move": if (message.userId !== currentUser.id) { setCursors(prev => { const newCursors = new Map(prev); const existingCursor = newCursors.get(message.userId); if (existingCursor) { const newPosition = message.position; const oldPosition = existingCursor.position; // Calculate velocity const timeDiff = newPosition.timestamp - oldPosition.timestamp; const velocity = timeDiff > 0 ? { x: (newPosition.x - oldPosition.x) / timeDiff * 1000, y: (newPosition.y - oldPosition.y) / timeDiff * 1000 } : { x: 0, y: 0 }; // Update trail const newTrail = showTrails ? [newPosition, ...existingCursor.trail.slice(0, trailLength - 1)] : []; newCursors.set(message.userId, { ...existingCursor, position: newPosition, velocity, isVisible: true, trail: newTrail, lastSeen: Date.now() }); } return newCursors; }); onCursorMove?.(message.userId, message.position); } break; case "user:typing": setCursors(prev => { const newCursors = new Map(prev); const cursor = newCursors.get(message.userId); if (cursor) { newCursors.set(message.userId, { ...cursor, isTyping: message.isTyping }); } return newCursors; }); break; case "room:state": // Initial room state with all users message.users.forEach(user => { if (user.id !== currentUser.id) { setCursors(prev => { const newCursors = new Map(prev); newCursors.set(user.id, { ...user, position: { x: 0, y: 0, timestamp: Date.now() }, velocity: { x: 0, y: 0 }, isVisible: false, trail: [] }); return newCursors; }); } }); break; } }, [currentUser.id, onUserJoin, onUserLeave, onCursorMove, showTrails, trailLength]); // Track local cursor movement const handleMouseMove = useCallback(e => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; const now = Date.now(); if (now - lastSentTime.current < sendThrottle) return; const container = containerRef.current; if (!container) return; const rect = container.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; // Only send if position changed significantly const dx = x - lastPosition.current.x; const dy = y - lastPosition.current.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance > 2) { wsRef.current.send(JSON.stringify({ type: "cursor:move", userId: currentUser.id, position: { x, y, timestamp: now } })); lastPosition.current = { x, y }; lastSentTime.current = now; } }, [currentUser.id, sendThrottle]); // Handle mouse leave const handleMouseLeave = useCallback(() => { if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type: "cursor:leave", userId: currentUser.id, timestamp: Date.now() })); } }, [currentUser.id]); // Cleanup inactive cursors useEffect(() => { const interval = setInterval(() => { const now = Date.now(); setCursors(prev => { const newCursors = new Map(prev); let hasChanges = false; newCursors.forEach((cursor, userId) => { if (now - cursor.lastSeen > fadeTimeout) { newCursors.set(userId, { ...cursor, isVisible: false }); hasChanges = true; } }); return hasChanges ? newCursors : prev; }); }, 1000); return () => clearInterval(interval); }, [fadeTimeout]); // Setup event listeners and connection useEffect(() => { connect(); const container = containerRef.current; if (container) { container.addEventListener("mousemove", handleMouseMove); container.addEventListener("mouseleave", handleMouseLeave); } return () => { if (container) { container.removeEventListener("mousemove", handleMouseMove); container.removeEventListener("mouseleave", handleMouseLeave); } if (wsRef.current) { wsRef.current.close(); } }; }, [connect, handleMouseMove, handleMouseLeave]); return jsxs("div", { ref: containerRef, className: cn("relative", className), children: [children, jsx(AnimatePresence, { children: Array.from(cursors.values()).map(cursor => cursor.isVisible && jsx(LiveCursorComponent, { cursor: cursor, size: cursorSize, showTrail: showTrails, showLabel: showLabels, smoothing: smoothing, glassEffect: glassEffect }, cursor.id)) }), jsxs("div", { className: 'absolute glass-top-2 left-2 glass-flex glass-items-center glass-gap-2', children: [jsx("div", { className: cn("w-2 h-2 glass-radius-full transition-colors", isConnected ? "bg-green-400" : "bg-red-400") }), jsxs("span", { className: "glass-text-xs glass-text-secondary", children: [cursors.size, " user", cursors.size !== 1 ? "s" : "", " online"] })] })] }); } // Individual cursor component function LiveCursorComponent({ cursor, size, showTrail, showLabel, smoothing, glassEffect }) { const prefersReducedMotion = useReducedMotion(); return jsxs(Fragment, { children: [showTrail && cursor.trail.map((trailPoint, index) => jsx(motion.div, { className: 'absolute pointer-events-none', initial: { opacity: 0, scale: 0 }, animate: prefersReducedMotion ? {} : { opacity: (1 - index / cursor.trail.length) * 0.5, scale: 1 - index * 0.1, x: trailPoint.x, y: trailPoint.y }, exit: { opacity: 0, scale: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: jsx("div", { className: cn("glass-radius-full", glassEffect && "OptimizedGlass intensity={0.2} glassBlur={6} glass-blur-sm"), style: { width: size * 0.5, height: size * 0.5, backgroundColor: cursor.color } }) }, `trail-${cursor.id}-${index}`)), jsxs(motion.div, { className: 'absolute pointer-events-none z-50', initial: { opacity: 0, scale: 0, rotate: 0 }, animate: prefersReducedMotion ? {} : { opacity: 1, scale: 1, x: cursor.position.x, y: cursor.position.y, rotate: Math.atan2(cursor.velocity.y, cursor.velocity.x) * 180 / Math.PI }, exit: { opacity: 0, scale: 0 }, transition: prefersReducedMotion ? { duration: 0 } : { duration: 0.3 }, children: [jsxs("div", { className: cn("relative", glassEffect && "OptimizedGlass intensity={0.2} glassBlur={6} glass-blur-sm glass-elev-2"), style: { width: size, height: size, transform: "translate(-2px, -2px)" }, children: [jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", children: jsx("path", { d: "M5.5 3.21V20.79C5.5 21.17 5.96 21.32 6.23 21.05L11.5 15.77L14.5 20.79C14.74 21.18 15.26 21.18 15.5 20.79L17.27 18.15C17.51 17.76 17.51 17.24 17.27 16.85L14.27 11.83L20.79 5.31C21.17 5.04 21.02 4.5 20.64 4.5H3.36C2.98 4.5 2.83 5.04 3.21 5.31L5.5 3.21Z", fill: cursor.color, stroke: "white", strokeWidth: "1" }) }), cursor.isTyping && jsx(motion.div, { className: 'absolute glass--top-1 -right-1 w-3 h-3 glass-surface-green glass-radius-full', animate: prefersReducedMotion ? {} : { scale: [1, 1.2, 1] }, transition: prefersReducedMotion ? { duration: 0 } : { repeat: Infinity, duration: 1 } })] }), showLabel && jsxs(motion.div, { className: cn("absolute top-6 left-2 glass-px-2 glass-py-1 glass-text-xs glass-radius-md whitespace-nowrap", glassEffect ? "glass-surface-primary glass-text-primary" : "bg-black glass-text-primary"), initial: { opacity: 0, y: -10 }, animate: prefersReducedMotion ? {} : { opacity: 1, y: 0 }, exit: { opacity: 0, y: -10 }, style: { backgroundColor: glassEffect ? undefined : cursor.color }, children: [cursor.avatar && jsx("img", { src: cursor.avatar, alt: cursor.name, className: 'inline-block w-3 h-3 glass-radius-full glass-mr-1' }), cursor.name] })] })] }); } export { GlassLiveCursorPresence }; //# sourceMappingURL=GlassLiveCursorPresence.js.map