UNPKG

aura-glass

Version:

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

467 lines (464 loc) 17.1 kB
'use client'; import { jsx, jsxs, Fragment } from 'react/jsx-runtime'; import { useReducedMotion } from '../../hooks/useReducedMotion.js'; import { forwardRef, useRef, useState, useEffect, useCallback } from 'react'; import { AnimatePresence, 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 tools = [{ id: "pen", name: "Pen", icon: "✏️" }, { id: "marker", name: "Marker", icon: "🖍️" }, { id: "eraser", name: "Eraser", icon: "🧽" }, { id: "shape", name: "Shape", icon: "📐" }]; const colors = ["var(--glass-black)", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", "#FF8000", "#8000FF", "#FF0080"]; const sizes = [2, 4, 8, 12, 16, 24]; const GlassSharedWhiteboard = /*#__PURE__*/forwardRef(({ width = 800, height = 600, users, currentUserId, strokes = [], backgroundColor = "var(--glass-white)", gridVisible = false, showUserCursors = true, showToolbar = true, showUserList = true, showUndoRedo = true, maxStrokes = 1000, realTimeSync = false, soundEnabled = true, canDraw = true, readOnly = false, onStroke, onClear, onUndo, onRedo, onUserCursorMove, className = "", ...props }, ref) => { const prefersReducedMotion = useReducedMotion(); const canvasRef = useRef(null); const [isDrawing, setIsDrawing] = useState(false); const [currentStroke, setCurrentStroke] = useState(null); const [selectedTool, setSelectedTool] = useState("pen"); const [selectedColor, setSelectedColor] = useState("var(--glass-black)"); const [selectedSize, setSelectedSize] = useState(4); const [localStrokes, setLocalStrokes] = useState(strokes); const [undoStack, setUndoStack] = useState([]); const [redoStack, setRedoStack] = useState([]); const [simulatedUsers, setSimulatedUsers] = useState(users); const [gridVisibleState, setGridVisible] = useState(gridVisible); const { play } = useGlassSound(); useA11yId("glass-shared-whiteboard"); const { shouldAnimate } = useMotionPreference(); // Helper function to respect motion preferences const respectMotionPreference = config => shouldAnimate ? config : { duration: 0 }; const currentUser = users.find(u => u.id === currentUserId); // Simulated collaborative activity useEffect(() => { if (!realTimeSync) return; const interval = setInterval(() => { setSimulatedUsers(prev => prev.map(user => { if (user.id === currentUserId) return user; // Randomly move cursors and simulate drawing const deltaX = (Math.random() - 0.5) * 50; const deltaY = (Math.random() - 0.5) * 50; const newX = Math.max(0, Math.min(width, user.cursorX + deltaX)); const newY = Math.max(0, Math.min(height, user.cursorY + deltaY)); const isDrawing = Math.random() < 0.1; if (isDrawing && Math.random() < 0.3) { // Simulate a simple drawing stroke const stroke = { id: `stroke-${Date.now()}-${user.id}`, userId: user.id, userName: user.name, userColor: user.color, points: [{ x: newX, y: newY }, { x: newX + Math.random() * 20, y: newY + Math.random() * 20 }], tool: "pen", color: user.color, size: 3, opacity: 0.8, timestamp: Date.now(), isComplete: true }; setLocalStrokes(prev => [...prev.slice(-maxStrokes + 1), stroke]); onStroke?.(stroke); if (soundEnabled) { play("draw"); } } onUserCursorMove?.(user.id, newX, newY); return { ...user, cursorX: newX, cursorY: newY, isDrawing, lastActivity: Date.now() }; })); }, 200); return () => clearInterval(interval); }, [realTimeSync, currentUserId, width, height, maxStrokes, onStroke, onUserCursorMove, soundEnabled, play]); // Draw on canvas const drawStrokes = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext("2d"); if (!ctx) return; // Clear canvas ctx.fillStyle = backgroundColor; ctx.fillRect(0, 0, width, height); // Draw grid if visible if (gridVisibleState) { ctx.strokeStyle = "#E5E5E5"; ctx.lineWidth = 1; ctx.globalAlpha = 0.3; for (let x = 0; x <= width; x += 20) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, height); ctx.stroke(); } for (let y = 0; y <= height; y += 20) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(width, y); ctx.stroke(); } } ctx.globalAlpha = 1; // Draw all strokes localStrokes.forEach(stroke => { if (stroke.points.length < 2) return; ctx.strokeStyle = stroke.color; ctx.lineWidth = stroke.size; ctx.globalAlpha = stroke.opacity; ctx.lineCap = "round"; ctx.lineJoin = "round"; if (stroke.tool === "eraser") { ctx.globalCompositeOperation = "destination-out"; } else { ctx.globalCompositeOperation = "source-over"; } ctx.beginPath(); ctx.moveTo(stroke.points[0].x, stroke.points[0].y); for (let i = 1; i < stroke.points.length; i++) { ctx.lineTo(stroke.points[i].x, stroke.points[i].y); } ctx.stroke(); }); ctx.globalAlpha = 1; ctx.globalCompositeOperation = "source-over"; }, [localStrokes, backgroundColor, gridVisibleState, width, height]); useEffect(() => { drawStrokes(); }, [drawStrokes]); const startDrawing = useCallback(e => { if (readOnly || !canDraw) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; setIsDrawing(true); const stroke = { id: `stroke-${Date.now()}-${currentUserId}`, userId: currentUserId, userName: currentUser?.name || "Unknown", userColor: currentUser?.color || selectedColor, points: [{ x, y }], tool: selectedTool, color: selectedColor, size: selectedSize, opacity: selectedTool === "marker" ? 0.7 : 1, timestamp: Date.now(), isComplete: false }; setCurrentStroke(stroke); if (soundEnabled) { play("draw"); } }, [readOnly, canDraw, currentUserId, currentUser, selectedTool, selectedColor, selectedSize, soundEnabled, play]); const draw = useCallback(e => { if (!isDrawing || !currentStroke || readOnly) return; const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const updatedStroke = { ...currentStroke, points: [...currentStroke.points, { x, y }] }; setCurrentStroke(updatedStroke); setLocalStrokes(prev => [...prev.filter(s => s.id !== updatedStroke.id), updatedStroke]); }, [isDrawing, currentStroke, readOnly]); const stopDrawing = useCallback(() => { if (!currentStroke || readOnly) return; const completedStroke = { ...currentStroke, isComplete: true }; setLocalStrokes(prev => { const newStrokes = [...prev.filter(s => s.id !== completedStroke.id), completedStroke].slice(-maxStrokes); setUndoStack(undoPrev => [...undoPrev, prev]); setRedoStack([]); return newStrokes; }); onStroke?.(completedStroke); setCurrentStroke(null); setIsDrawing(false); }, [currentStroke, readOnly, maxStrokes, onStroke]); const handleUndo = () => { if (undoStack.length === 0) return; const previousState = undoStack[undoStack.length - 1]; setRedoStack(prev => [localStrokes, ...prev]); setUndoStack(prev => prev.slice(0, -1)); setLocalStrokes(previousState); onUndo?.(); }; const handleRedo = () => { if (redoStack.length === 0) return; const nextState = redoStack[0]; setUndoStack(prev => [...prev, localStrokes]); setRedoStack(prev => prev.slice(1)); setLocalStrokes(nextState); onRedo?.(); }; const handleClear = () => { setUndoStack(prev => [...prev, localStrokes]); setRedoStack([]); setLocalStrokes([]); onClear?.(); }; const UserCursor = ({ user }) => jsxs(motion.div, { className: 'absolute pointer-events-none z-20', style: { left: user.cursorX, top: user.cursorY, transform: "translate(-2px, -2px)" }, animate: prefersReducedMotion ? {} : { scale: user.isDrawing ? 1.2 : 1, opacity: Date.now() - user.lastActivity < 5000 ? 1 : 0.5 }, transition: respectMotionPreference({ type: "spring", stiffness: 400, damping: 30 }), children: [jsx("svg", { width: "20", height: "20", viewBox: "0 0 24 24", children: jsx("path", { d: "M5 3L19 12L12 14L9 21L5 3Z", fill: user.color, stroke: "white", strokeWidth: "1" }) }), jsx("div", { className: ` mt-2 px-2 py-1 text-xs font-medium text-white rounded ${createGlassStyle({ variant: "default" })} `, children: user.name })] }); return jsx(OptimizedGlassCore, { ref: ref, intensity: "subtle", className: `relative ${className}`, ...props, children: jsxs("div", { className: 'glass-flex glass-flex-col space-y-4', children: [showToolbar && !readOnly && jsxs("div", { className: ` flex items-center justify-between p-3 rounded-lg ${createGlassStyle({ variant: "default" })} `, children: [jsxs("div", { className: 'glass-flex glass-items-center space-x-4', children: [jsx("div", { className: 'glass-flex space-x-2', children: tools.map(tool => jsx("button", { onClick: () => setSelectedTool(tool.id), className: ` p-2 rounded text-sm font-medium transition-colors duration-200 glass-focus glass-touch-target glass-contrast-guard ${selectedTool === tool.id ? "bg-white/20 text-white" : "text-white/70 hover:text-white hover:bg-white/10"} `, title: tool.name, children: tool.icon }, tool.id)) }), jsx("div", { className: 'glass-flex space-x-1', children: colors.map((color, i) => jsx("button", { onClick: () => setSelectedColor(color), className: ` w-6 h-6 rounded border-2 transition-transform duration-200 glass-focus glass-touch-target glass-contrast-guard ${selectedColor === color ? "border-white scale-110" : "border-white/30 hover:scale-105"} `, style: { backgroundColor: color } }, `${color}-${i}`)) }), jsx("div", { className: 'glass-flex space-x-1', children: sizes.map(size => jsx("button", { onClick: () => setSelectedSize(size), className: ` w-8 h-8 rounded flex items-center justify-center transition-colors duration-200 glass-focus glass-touch-target glass-contrast-guard ${selectedSize === size ? "bg-white/20 text-white" : "text-white/70 hover:text-white hover:bg-white/10"} `, children: jsx("div", { className: 'bg-transparent glass-radius-full', style: { width: Math.min(size, 16), height: Math.min(size, 16) } }) }, size)) })] }), jsxs("div", { className: 'glass-flex glass-items-center space-x-2', children: [showUndoRedo && jsxs(Fragment, { children: [jsx("button", { onClick: handleUndo, disabled: undoStack.length === 0, className: 'glass-px-3 glass-py-1 glass-text-sm font-medium text-primary/70 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', children: "\u21B6 Undo" }), jsx("button", { onClick: handleRedo, disabled: redoStack.length === 0, className: 'glass-px-3 glass-py-1 glass-text-sm font-medium text-primary/70 hover:text-primary disabled:opacity-50 disabled:cursor-not-allowed glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', children: "\u21B7 Redo" })] }), jsx("button", { onClick: handleClear, className: 'glass-px-3 glass-py-1 glass-text-sm font-medium text-primary hover:glass-text-secondary glass-focus glass-touch-target glass-contrast-guard glass-focus glass-touch-target glass-contrast-guard', children: "Clear" })] })] }), jsxs("div", { className: 'glass-flex space-x-4', children: [jsxs("div", { className: 'relative glass-flex-1', children: [jsx("canvas", { ref: canvasRef, width: width, height: height, className: ` border border-white/20 rounded-lg cursor-crosshair ${readOnly ? "cursor-not-allowed" : ""} `, onMouseDown: startDrawing, onMouseMove: draw, onMouseUp: stopDrawing, onMouseLeave: stopDrawing }), showUserCursors && jsx(AnimatePresence, { children: simulatedUsers.filter(user => user.id !== currentUserId).map(user => jsx(UserCursor, { user: user }, user.id)) }), !readOnly && jsx("button", { onClick: () => setGridVisible(!gridVisible), className: 'absolute glass-top-2 right-2 glass-p-2 text-primary/60 hover:text-primary glass-focus glass-touch-target glass-contrast-guard', title: "Toggle Grid", children: "#" })] }), showUserList && jsxs("div", { className: ` w-48 p-3 rounded-lg space-y-2 ${createGlassStyle({ variant: "default" })} `, children: [jsxs("h3", { className: 'glass-text-sm font-medium text-primary/90 mb-3', children: ["Active Users (", simulatedUsers.length, ")"] }), simulatedUsers.map(user => jsxs("div", { className: 'glass-flex glass-items-center space-x-2 glass-p-2 glass-radius hover:glass-surface-subtle/5', children: [jsx("div", { className: 'w-3 h-3 glass-radius-full glass-border glass-border-white/30', style: { backgroundColor: user.color } }), jsxs("span", { className: 'glass-text-sm text-primary/80 truncate', children: [user.name, user.id === currentUserId && " (You)"] }), user.isDrawing && jsx("span", { className: 'glass-text-xs text-primary', children: "\u270F\uFE0F" })] }, user.id))] })] }), jsxs("div", { className: 'glass-flex glass-items-center glass-justify-between glass-text-xs text-primary/50', children: [jsxs("div", { className: 'glass-flex glass-items-center space-x-4', children: [jsxs("span", { children: [localStrokes.length, " strokes"] }), realTimeSync && jsxs("span", { className: 'glass-flex glass-items-center space-x-1', children: [jsx("div", { className: 'w-2 h-2 glass-surface-green glass-radius-full animate-pulse' }), jsx("span", { children: "Synced" })] }), readOnly && jsx("span", { className: 'text-primary', children: "Read Only" })] }), jsxs("div", { children: ["Canvas: ", width, "\u00D7", height] })] })] }) }); }); export { GlassSharedWhiteboard }; //# sourceMappingURL=GlassSharedWhiteboard.js.map