aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
467 lines (464 loc) • 17.1 kB
JavaScript
'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