aura-glass
Version:
A comprehensive glassmorphism design system for React applications with 142+ production-ready components
510 lines (507 loc) • 19.1 kB
JavaScript
'use client';
import { jsxs, jsx } from 'react/jsx-runtime';
import { cn } from '../../lib/utilsComprehensive.js';
import { useState, useRef, useCallback, useEffect } from 'react';
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';
const defaultColors = ["var(--glass-white)", "var(--glass-black)", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff"];
const defaultTools = ["pen", "eraser", "rectangle", "circle", "line", "text", "select"];
const GlassWhiteboard = ({
initialData = [],
collaborative = false,
userId = "user",
enabledTools = defaultTools,
availableColors = defaultColors,
width = 800,
height = 600,
backgroundPattern = "grid",
showToolbar = true,
showMinimap = false,
className = "",
onDrawingChange,
onToolChange,
onColorChange
}) => {
const [currentTool, setCurrentTool] = useState("pen");
const [currentColor, setCurrentColor] = useState("var(--glass-black)");
const [brushSize, setBrushSize] = useState(2);
const [opacity, setOpacity] = useState(1);
const [isDrawing, setIsDrawing] = useState(false);
const [currentPath, setCurrentPath] = useState(null);
const [currentShape, setCurrentShape] = useState(null);
const [drawingData, setDrawingData] = useState(initialData);
const [selectedElements, setSelectedElements] = useState(new Set());
const [isSelecting, setIsSelecting] = useState(false);
const [selectionBox, setSelectionBox] = useState(null);
const canvasRef = useRef(null);
const overlayCanvasRef = useRef(null);
const containerRef = useRef(null);
// Update drawing data and notify parent
const updateDrawingData = useCallback(newData => {
setDrawingData(newData);
onDrawingChange?.(newData);
}, [onDrawingChange]);
// Handle tool change
const handleToolChange = tool => {
setCurrentTool(tool);
setSelectedElements(new Set());
setIsSelecting(false);
onToolChange?.(tool);
};
// Handle color change
const handleColorChange = color => {
setCurrentColor(color);
onColorChange?.(color);
};
// Get mouse/touch position relative to canvas
const getCanvasPosition = useCallback(e => {
const canvas = canvasRef.current;
if (!canvas) return {
x: 0,
y: 0
};
const rect = canvas.getBoundingClientRect();
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
return {
x: clientX - rect.left,
y: clientY - rect.top
};
}, []);
// Start drawing
const handleMouseDown = e => {
const pos = getCanvasPosition(e);
setIsDrawing(true);
if (currentTool === "select") {
setIsSelecting(true);
setSelectionBox({
x: pos.x,
y: pos.y,
width: 0,
height: 0
});
return;
}
if (currentTool === "pen" || currentTool === "eraser") {
const newPath = {
id: `path-${Date.now()}-${Math.random()}`,
tool: currentTool,
points: [pos],
color: currentTool === "eraser" ? "var(--glass-white)" : currentColor,
width: brushSize,
opacity,
timestamp: Date.now()
};
setCurrentPath(newPath);
} else if (["rectangle", "circle", "line"].includes(currentTool)) {
const newShape = {
id: `shape-${Date.now()}-${Math.random()}`,
type: currentTool,
startX: pos.x,
startY: pos.y,
endX: pos.x,
endY: pos.y,
color: currentColor,
width: brushSize,
opacity,
timestamp: Date.now()
};
setCurrentShape(newShape);
} else if (currentTool === "text") {
const newShape = {
id: `text-${Date.now()}-${Math.random()}`,
type: "text",
startX: pos.x,
startY: pos.y,
endX: pos.x,
endY: pos.y,
color: currentColor,
width: brushSize,
opacity,
text: "",
timestamp: Date.now()
};
setCurrentShape(newShape);
}
};
// Continue drawing
const handleMouseMove = useCallback(e => {
if (!isDrawing) return;
const pos = getCanvasPosition(e);
if (currentTool === "select" && isSelecting && selectionBox) {
setSelectionBox({
...selectionBox,
width: pos.x - selectionBox.x,
height: pos.y - selectionBox.y
});
return;
}
if (currentPath) {
setCurrentPath({
...currentPath,
points: [...currentPath.points, pos]
});
} else if (currentShape) {
setCurrentShape({
...currentShape,
endX: pos.x,
endY: pos.y
});
}
}, [isDrawing, currentTool, isSelecting, selectionBox, currentPath, currentShape, getCanvasPosition]);
// Finish drawing
const handleMouseUp = useCallback(() => {
if (!isDrawing) return;
setIsDrawing(false);
if (currentTool === "select" && selectionBox && isSelecting) {
// Find elements within selection box
const selected = new Set();
const {
x,
y,
width,
height
} = selectionBox;
drawingData.forEach(item => {
if ("points" in item) {
// Check if path intersects with selection box
const intersects = item?.points.some(point => point.x >= x && point.x <= x + width && point.y >= y && point.y <= y + height);
if (intersects) selected.add(item?.id);
} else {
// Check if shape intersects with selection box
const intersects = item?.startX >= x && item?.startX <= x + width && item?.startY >= y && item?.startY <= y + height || item?.endX >= x && item?.endX <= x + width && item?.endY >= y && item?.endY <= y + height;
if (intersects) selected.add(item?.id);
}
});
setSelectedElements(selected);
setSelectionBox(null);
setIsSelecting(false);
return;
}
if (currentPath) {
updateDrawingData([...drawingData, currentPath]);
setCurrentPath(null);
} else if (currentShape) {
updateDrawingData([...drawingData, currentShape]);
setCurrentShape(null);
}
}, [isDrawing, currentTool, selectionBox, isSelecting, currentPath, currentShape, drawingData, updateDrawingData]);
// Draw background pattern
const drawBackground = useCallback(ctx => {
ctx.clearRect(0, 0, width, height);
switch (backgroundPattern) {
case "grid":
ctx.strokeStyle = "var(--glass-white)10";
ctx.lineWidth = 1;
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();
}
break;
case "dots":
ctx.fillStyle = "var(--glass-white)20";
for (let x = 20; x < width; x += 20) {
for (let y = 20; y < height; y += 20) {
ctx.beginPath();
ctx.arc(x, y, 1, 0, Math.PI * 2);
ctx.fill();
}
}
break;
case "lines":
ctx.strokeStyle = "var(--glass-white)10";
ctx.lineWidth = 1;
for (let y = 0; y < height; y += 40) {
ctx.beginPath();
ctx.moveTo(0, y);
ctx.lineTo(width, y);
ctx.stroke();
}
break;
}
}, [width, height, backgroundPattern]);
// Draw all elements
const drawElements = useCallback((ctx, elements) => {
elements.forEach(element => {
ctx.globalAlpha = element.opacity;
if ("points" in element) {
// Draw path
if ((element.points?.length || 0) > 1) {
ctx.strokeStyle = element.color;
ctx.lineWidth = element.width;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
ctx.moveTo(element.points[0].x, element.points[0].y);
for (let i = 1; i < (element.points?.length || 0); i++) {
ctx.lineTo(element.points[i].x, element.points[i].y);
}
ctx.stroke();
}
} else {
// Draw shape
ctx.strokeStyle = element.color;
ctx.lineWidth = element.width;
ctx.fillStyle = element.color;
const width = Math.abs(element.endX - element.startX);
const height = Math.abs(element.endY - element.startY);
const x = Math.min(element.startX, element.endX);
const y = Math.min(element.startY, element.endY);
switch (element.type) {
case "rectangle":
ctx.strokeRect(x, y, width, height);
break;
case "circle":
const radius = Math.sqrt(width * width + height * height) / 2;
const centerX = x + width / 2;
const centerY = y + height / 2;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, 0, Math.PI * 2);
ctx.stroke();
break;
case "line":
ctx.beginPath();
ctx.moveTo(element.startX, element.startY);
ctx.lineTo(element.endX, element.endY);
ctx.stroke();
break;
case "text":
if (element.text) {
ctx.font = `${element.width * 10}px Arial`;
ctx.fillStyle = element.color;
ctx.fillText(element.text, element.startX, element.startY);
}
break;
}
}
ctx.globalAlpha = 1;
});
}, []);
// Draw selection highlights
const drawSelection = useCallback(ctx => {
selectedElements.forEach(elementId => {
const element = drawingData.find(item => item?.id === elementId);
if (!element) return;
ctx.strokeStyle = "var(--glass-white)";
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
if ("points" in element && (element.points?.length || 0) > 0) {
const bounds = element.points.reduce((acc, point) => ({
minX: Math.min(acc.minX, point.x),
maxX: Math.max(acc.maxX, point.x),
minY: Math.min(acc.minY, point.y),
maxY: Math.max(acc.maxY, point.y)
}), {
minX: Infinity,
maxX: -Infinity,
minY: Infinity,
maxY: -Infinity
});
ctx.strokeRect(bounds.minX - 10, bounds.minY - 10, bounds.maxX - bounds.minX + 20, bounds.maxY - bounds.minY + 20);
} else {
const bounds = {
minX: Math.min(element.startX, element.endX),
maxX: Math.max(element.startX, element.endX),
minY: Math.min(element.startY, element.endY),
maxY: Math.max(element.startY, element.endY)
};
ctx.strokeRect(bounds.minX - 10, bounds.minY - 10, bounds.maxX - bounds.minX + 20, bounds.maxY - bounds.minY + 20);
}
ctx.setLineDash([]);
});
}, [selectedElements, drawingData]);
// Main render loop
useEffect(() => {
const canvas = canvasRef.current;
const overlayCanvas = overlayCanvasRef.current;
if (!canvas || !overlayCanvas) return;
const ctx = canvas.getContext("2d");
const overlayCtx = overlayCanvas.getContext("2d");
if (!ctx || !overlayCtx) return;
// Clear canvases
ctx.clearRect(0, 0, width, height);
overlayCtx.clearRect(0, 0, width, height);
// Draw background
drawBackground(ctx);
// Draw all elements
drawElements(ctx, drawingData);
// Draw current path/shape being drawn
if (currentPath) {
drawElements(overlayCtx, [currentPath]);
}
if (currentShape) {
drawElements(overlayCtx, [currentShape]);
}
// Draw selection
drawSelection(ctx);
// Draw selection box
if (selectionBox && isSelecting) {
overlayCtx.strokeStyle = "var(--glass-white)80";
overlayCtx.lineWidth = 1;
overlayCtx.setLineDash([5, 5]);
overlayCtx.strokeRect(selectionBox.x, selectionBox.y, selectionBox.width, selectionBox.height);
overlayCtx.setLineDash([]);
}
}, [drawingData, currentPath, currentShape, selectionBox, isSelecting, drawBackground, drawElements, drawSelection, width, height]);
// Handle text input
const handleTextInput = useCallback(text => {
if (currentShape && currentShape.type === "text") {
const updatedShape = {
...currentShape,
text
};
setCurrentShape(updatedShape);
updateDrawingData([...drawingData, updatedShape]);
setCurrentShape(null);
}
}, [currentShape, drawingData, updateDrawingData]);
// Clear canvas
const clearCanvas = () => {
updateDrawingData([]);
setSelectedElements(new Set());
};
// Delete selected elements
const deleteSelected = () => {
const newData = drawingData.filter(item => !selectedElements.has(item?.id));
updateDrawingData(newData);
setSelectedElements(new Set());
};
// Export canvas as image
const exportAsImage = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const link = document.createElement("a");
link.download = `whiteboard-${Date.now()}.png`;
link.href = canvas.toDataURL();
link.click();
};
return jsxs(OptimizedGlassCore, {
"data-glass-component": true,
className: cn("glass-relative", className),
intent: "neutral",
elevation: "level1",
children: [showToolbar && jsxs("div", {
className: cn("glass-flex glass-flex-wrap glass-items-center glass-gap-2 glass-p-4 glass-border-b glass-border-white-10"),
children: [jsx("div", {
className: cn("glass-flex glass-gap-1"),
children: enabledTools.map(tool => jsx("button", {
onClick: e => handleToolChange(tool),
className: cn("glass-px-3 glass-py-2 glass-radius-md glass-text-sm glass-font-medium glass-transition-colors", "glass-focus glass-touch-target glass-contrast-guard", currentTool === tool ? "glass-surface-elevated glass-text-primary" : "glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle"),
children: tool.charAt(0).toUpperCase() + tool.slice(1)
}, tool))
}), jsx("div", {
className: cn("glass-flex glass-gap-1"),
children: availableColors.map((color, i) => jsx("button", {
onClick: e => handleColorChange(color),
className: cn("glass-w-8 glass-h-8 glass-radius-md glass-border-2 glass-transition-all", "glass-focus glass-touch-target glass-contrast-guard", currentColor === color ? "glass-border-white glass-scale-110" : "glass-border-white-30 glass-hover-border-white-60"),
style: {
backgroundColor: color
}
}, `${color}-${i}`))
}), jsxs("div", {
className: cn("glass-flex glass-items-center glass-gap-2"),
children: [jsx("span", {
className: cn("glass-text-sm glass-text-primary-70"),
children: "Size:"
}), jsx("input", {
type: "range",
min: "1",
max: "20",
value: brushSize,
onChange: e => setBrushSize(Number(e.target.value)),
className: cn("glass-w-16 glass-focus glass-touch-target glass-contrast-guard")
}), jsx("span", {
className: cn("glass-text-sm glass-text-primary-70 glass-w-6"),
children: brushSize
})]
}), jsxs("div", {
className: cn("glass-flex glass-items-center glass-gap-2"),
children: [jsx("span", {
className: cn("glass-text-sm glass-text-primary-70"),
children: "Opacity:"
}), jsx("input", {
type: "range",
min: "0.1",
max: "1",
step: "0.1",
value: opacity,
onChange: e => setOpacity(Number(e.target.value)),
className: cn("glass-w-16 glass-focus glass-touch-target glass-contrast-guard")
}), jsxs("span", {
className: cn("glass-text-sm glass-text-primary-70 glass-w-8"),
children: [(opacity * 100).toFixed(0), "%"]
})]
}), jsxs("div", {
className: cn("glass-flex glass-gap-1 glass-ml-4"),
children: [jsx("button", {
onClick: clearCanvas,
className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"),
children: "Clear"
}), selectedElements.size > 0 && jsx("button", {
onClick: deleteSelected,
className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-red-400 glass-hover-text-red-300 glass-hover-surface-red-10 glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"),
children: "Delete Selected"
}), jsx("button", {
onClick: exportAsImage,
className: cn("glass-px-3 glass-py-2 glass-text-sm glass-text-primary-70 glass-hover-text-primary glass-hover-surface-subtle glass-radius-md glass-transition-colors glass-focus glass-touch-target glass-contrast-guard"),
children: "Export"
})]
})]
}), currentShape && currentShape.type === "text" && jsx("div", {
className: cn("glass-absolute glass-inset-0 glass-surface-black-50 glass-flex glass-items-center glass-justify-center glass-z-20"),
children: jsx(OptimizedGlassCore, {
className: cn("glass-p-6 glass-max-w-sm glass-w-full glass-mx-4"),
blur: "medium",
elevation: "level2",
children: jsx("input", {
autoFocus: true,
placeholder: "Enter text...",
className: cn("glass-w-full glass-px-3 glass-py-2 glass-surface-white-10 glass-border glass-border-white-20 glass-radius-md glass-text-primary glass-placeholder-white-50 glass-focus-outline-none glass-focus-border-white-40 glass-focus glass-touch-target glass-contrast-guard"),
onKeyDown: e => {
if (e.key === "Enter") {
handleTextInput(e.target.value);
} else if (e.key === "Escape") {
setCurrentShape(null);
}
}
})
})
}), jsxs("div", {
ref: containerRef,
className: cn("glass-relative glass-overflow-hidden glass-surface-black-20"),
style: {
width,
height
},
children: [jsx("canvas", {
ref: canvasRef,
width: width,
height: height,
className: cn("glass-absolute glass-inset-0 glass-cursor-crosshair"),
onMouseDown: handleMouseDown,
onMouseMove: handleMouseMove,
onMouseUp: handleMouseUp,
onMouseLeave: handleMouseUp
}), jsx("canvas", {
ref: overlayCanvasRef,
width: width,
height: height,
className: cn("glass-absolute glass-inset-0 glass-pointer-events-none")
})]
})]
});
};
export { GlassWhiteboard };
//# sourceMappingURL=GlassWhiteboard.js.map