diff --git a/src/plays/virtual-whiteboard/VirtualWhiteboard.jsx b/src/plays/virtual-whiteboard/VirtualWhiteboard.jsx new file mode 100644 index 000000000..55b6de29c --- /dev/null +++ b/src/plays/virtual-whiteboard/VirtualWhiteboard.jsx @@ -0,0 +1,202 @@ +import React, { useState, useRef, useCallback } from 'react'; +import PlayHeader from 'common/playlists/PlayHeader'; +import Toolbar from './components/Toolbar'; +import Canvas from './components/Canvas'; +import LayerPanel from './components/LayerPanel'; +import './styles.css'; + +function VirtualWhiteboard(props) { + // Drawing state + const [tool, setTool] = useState('pen'); + const [color, setColor] = useState('#000000'); + const [lineWidth, setLineWidth] = useState(2); + const [fillColor, setFillColor] = useState('#ffffff'); + const [fontSize, setFontSize] = useState(16); + + // Layer state + const [layers, setLayers] = useState([ + { id: 1, name: 'Layer 1', visible: true, locked: false, data: [] } + ]); + const [activeLayerId, setActiveLayerId] = useState(1); + + // History state for undo/redo + const [history, setHistory] = useState([]); + const [historyStep, setHistoryStep] = useState(-1); + + // Refs + const canvasRef = useRef(null); + + // Save state to history + const saveToHistory = useCallback(() => { + const newHistory = history.slice(0, historyStep + 1); + newHistory.push(JSON.parse(JSON.stringify(layers))); + setHistory(newHistory); + setHistoryStep(newHistory.length - 1); + }, [history, historyStep, layers]); + + // Undo + const handleUndo = useCallback(() => { + if (historyStep > 0) { + setHistoryStep(historyStep - 1); + setLayers(JSON.parse(JSON.stringify(history[historyStep - 1]))); + } + }, [historyStep, history]); + + // Redo + const handleRedo = useCallback(() => { + if (historyStep < history.length - 1) { + setHistoryStep(historyStep + 1); + setLayers(JSON.parse(JSON.stringify(history[historyStep + 1]))); + } + }, [historyStep, history]); + + // Layer management + const addLayer = () => { + const newLayer = { + id: Date.now(), + name: `Layer ${layers.length + 1}`, + visible: true, + locked: false, + data: [] + }; + setLayers([...layers, newLayer]); + setActiveLayerId(newLayer.id); + }; + + const deleteLayer = (layerId) => { + if (layers.length === 1) return; // Keep at least one layer + const newLayers = layers.filter((layer) => layer.id !== layerId); + setLayers(newLayers); + if (activeLayerId === layerId) { + setActiveLayerId(newLayers[0].id); + } + }; + + const toggleLayerVisibility = (layerId) => { + setLayers( + layers.map((layer) => (layer.id === layerId ? { ...layer, visible: !layer.visible } : layer)) + ); + }; + + const toggleLayerLock = (layerId) => { + setLayers( + layers.map((layer) => (layer.id === layerId ? { ...layer, locked: !layer.locked } : layer)) + ); + }; + + const renameLayer = (layerId, newName) => { + setLayers(layers.map((layer) => (layer.id === layerId ? { ...layer, name: newName } : layer))); + }; + + // Update layer data + const updateLayerData = (layerId, newData) => { + setLayers(layers.map((layer) => (layer.id === layerId ? { ...layer, data: newData } : layer))); + }; + + // Clear canvas + const handleClear = () => { + if (window.confirm('Clear all layers? This cannot be undone.')) { + setLayers([{ id: Date.now(), name: 'Layer 1', visible: true, locked: false, data: [] }]); + setHistory([]); + setHistoryStep(-1); + } + }; + + // Export to image + const handleExportImage = () => { + if (canvasRef.current) { + const canvas = canvasRef.current; + const link = document.createElement('a'); + link.download = `whiteboard-${Date.now()}.png`; + link.href = canvas.toDataURL(); + link.click(); + } + }; + + // Export to PDF (using html2canvas approach) + const handleExportPDF = async () => { + if (canvasRef.current) { + try { + const canvas = canvasRef.current; + const imgData = canvas.toDataURL('image/png'); + + // Create a simple PDF export using a new window + const pdfWindow = window.open('', '_blank'); + pdfWindow.document.write(` + + Whiteboard Export + + + + + + `); + pdfWindow.document.close(); + } catch (error) { + console.error('Export failed:', error); + alert('Export failed. Please try again.'); + } + } + }; + + return ( +
+ +
+
+ 0} + color={color} + fillColor={fillColor} + fontSize={fontSize} + lineWidth={lineWidth} + setColor={setColor} + setFillColor={setFillColor} + setFontSize={setFontSize} + setLineWidth={setLineWidth} + setTool={setTool} + tool={tool} + onClear={handleClear} + onExportImage={handleExportImage} + onExportPDF={handleExportPDF} + onRedo={handleRedo} + onUndo={handleUndo} + /> + +
+ + + +
+
+
+
+ ); +} + +export default VirtualWhiteboard; diff --git a/src/plays/virtual-whiteboard/components/Canvas.jsx b/src/plays/virtual-whiteboard/components/Canvas.jsx new file mode 100644 index 000000000..3e0fb7c01 --- /dev/null +++ b/src/plays/virtual-whiteboard/components/Canvas.jsx @@ -0,0 +1,260 @@ +import React, { useEffect, useState, forwardRef } from 'react'; + +const Canvas = forwardRef( + ( + { + layers, + activeLayerId, + tool, + color, + lineWidth, + fillColor, + fontSize, + updateLayerData, + saveToHistory + }, + ref + ) => { + const [isDrawing, setIsDrawing] = useState(false); + const [startPos, setStartPos] = useState({ x: 0, y: 0 }); + const [currentShape, setCurrentShape] = useState(null); + + const activeLayer = layers.find((layer) => layer.id === activeLayerId); + + useEffect(() => { + const canvas = ref.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + // Draw all visible layers + layers.forEach((layer) => { + if (layer.visible) { + layer.data.forEach((item) => { + drawItem(ctx, item); + }); + } + }); + + // Draw current shape being created + if (currentShape) { + drawItem(ctx, currentShape); + } + }, [layers, currentShape]); + + // Keyboard shortcuts + useEffect(() => { + const handleKeyDown = (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + e.preventDefault(); + // Undo handled in parent + } + if ((e.ctrlKey || e.metaKey) && e.key === 'y') { + e.preventDefault(); + // Redo handled in parent + } + }; + + window.addEventListener('keydown', handleKeyDown); + + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + const drawItem = (ctx, item) => { + ctx.strokeStyle = item.color; + ctx.lineWidth = item.lineWidth; + ctx.fillStyle = item.fillColor || 'transparent'; + ctx.lineCap = 'round'; + ctx.lineJoin = 'round'; + + switch (item.type) { + case 'pen': + ctx.beginPath(); + item.points.forEach((point, index) => { + if (index === 0) { + ctx.moveTo(point.x, point.y); + } else { + ctx.lineTo(point.x, point.y); + } + }); + ctx.stroke(); + + break; + + case 'eraser': + ctx.save(); + ctx.globalCompositeOperation = 'destination-out'; + ctx.beginPath(); + item.points.forEach((point, index) => { + if (index === 0) { + ctx.moveTo(point.x, point.y); + } else { + ctx.lineTo(point.x, point.y); + } + }); + ctx.lineWidth = item.lineWidth * 2; + ctx.stroke(); + ctx.restore(); + + break; + + case 'line': + ctx.beginPath(); + ctx.moveTo(item.startX, item.startY); + ctx.lineTo(item.endX, item.endY); + ctx.stroke(); + + break; + + case 'rectangle': { + const width = item.endX - item.startX; + const height = item.endY - item.startY; + if (item.fillColor && item.fillColor !== '#ffffff') { + ctx.fillRect(item.startX, item.startY, width, height); + } + ctx.strokeRect(item.startX, item.startY, width, height); + + break; + } + + case 'circle': { + const radius = Math.sqrt( + Math.pow(item.endX - item.startX, 2) + Math.pow(item.endY - item.startY, 2) + ); + ctx.beginPath(); + ctx.arc(item.startX, item.startY, radius, 0, 2 * Math.PI); + if (item.fillColor && item.fillColor !== '#ffffff') { + ctx.fill(); + } + ctx.stroke(); + + break; + } + + case 'text': + ctx.font = `${item.fontSize}px Arial`; + ctx.fillStyle = item.color; + ctx.fillText(item.text, item.x, item.y); + + break; + + default: + break; + } + }; + + const getMousePos = (e) => { + const canvas = ref.current; + const rect = canvas.getBoundingClientRect(); + + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + }; + + const handleMouseDown = (e) => { + if (!activeLayer || activeLayer.locked) return; + + const pos = getMousePos(e); + setIsDrawing(true); + setStartPos(pos); + + if (tool === 'pen' || tool === 'eraser') { + const newItem = { + type: tool, + color: color, + lineWidth: lineWidth, + points: [pos] + }; + setCurrentShape(newItem); + } else if (tool === 'text') { + const text = prompt('Enter text:'); + if (text) { + const newItem = { + type: 'text', + text: text, + x: pos.x, + y: pos.y, + color: color, + fontSize: fontSize + }; + const newData = [...activeLayer.data, newItem]; + updateLayerData(activeLayerId, newData); + saveToHistory(); + } + } + }; + + const handleMouseMove = (e) => { + if (!isDrawing || !activeLayer || activeLayer.locked) return; + + const pos = getMousePos(e); + + if (tool === 'pen' || tool === 'eraser') { + setCurrentShape((prev) => ({ + ...prev, + points: [...prev.points, pos] + })); + } else if (tool === 'line' || tool === 'rectangle' || tool === 'circle') { + setCurrentShape({ + type: tool, + startX: startPos.x, + startY: startPos.y, + endX: pos.x, + endY: pos.y, + color: color, + lineWidth: lineWidth, + fillColor: fillColor + }); + } + }; + + const handleMouseUp = () => { + if (!isDrawing || !currentShape || !activeLayer || activeLayer.locked) { + setIsDrawing(false); + setCurrentShape(null); + + return; + } + + const newData = [...activeLayer.data, currentShape]; + updateLayerData(activeLayerId, newData); + saveToHistory(); + + setIsDrawing(false); + setCurrentShape(null); + }; + + const handleMouseLeave = () => { + if (isDrawing) { + handleMouseUp(); + } + }; + + return ( +
+ + {activeLayer && activeLayer.locked && ( +
+
🔒 Layer is locked
+
+ )} +
+ ); + } +); + +Canvas.displayName = 'Canvas'; + +export default Canvas; diff --git a/src/plays/virtual-whiteboard/components/LayerPanel.jsx b/src/plays/virtual-whiteboard/components/LayerPanel.jsx new file mode 100644 index 000000000..a8d4e35c8 --- /dev/null +++ b/src/plays/virtual-whiteboard/components/LayerPanel.jsx @@ -0,0 +1,146 @@ +import React, { useState } from 'react'; +import { FaPlus, FaTrash, FaEye, FaEyeSlash, FaLock, FaUnlock, FaEdit } from 'react-icons/fa'; + +const LayerPanel = ({ + layers, + activeLayerId, + setActiveLayerId, + onAddLayer, + onDeleteLayer, + onToggleVisibility, + onToggleLock, + onRenameLayer +}) => { + const [editingLayerId, setEditingLayerId] = useState(null); + const [editName, setEditName] = useState(''); + + const handleStartEdit = (layer) => { + setEditingLayerId(layer.id); + setEditName(layer.name); + }; + + const handleFinishEdit = (layerId) => { + if (editName.trim()) { + onRenameLayer(layerId, editName.trim()); + } + setEditingLayerId(null); + setEditName(''); + }; + + const handleKeyDown = (e, layerId) => { + if (e.key === 'Enter') { + handleFinishEdit(layerId); + } else if (e.key === 'Escape') { + setEditingLayerId(null); + setEditName(''); + } + }; + + return ( +
+
+

Layers

+ +
+ +
+ {[...layers].reverse().map((layer) => ( +
!layer.locked && setActiveLayerId(layer.id)} + > +
+ + + +
+ +
+ {editingLayerId === layer.id ? ( + handleFinishEdit(layer.id)} + onChange={(e) => setEditName(e.target.value)} + onClick={(e) => e.stopPropagation()} + onKeyDown={(e) => handleKeyDown(e, layer.id)} + /> + ) : ( + {layer.name} + )} + +
+ {layer.data.length} object{layer.data.length !== 1 ? 's' : ''} +
+
+ +
+ + + {layers.length > 1 && ( + + )} +
+
+ ))} +
+ +
+

Layer Tips

+ +
+
+ ); +}; + +export default LayerPanel; diff --git a/src/plays/virtual-whiteboard/components/Toolbar.jsx b/src/plays/virtual-whiteboard/components/Toolbar.jsx new file mode 100644 index 000000000..44125eb2c --- /dev/null +++ b/src/plays/virtual-whiteboard/components/Toolbar.jsx @@ -0,0 +1,162 @@ +import React from 'react'; +import { + FaPen, + FaEraser, + FaSquare, + FaCircle, + FaArrowRight, + FaFont, + FaUndo, + FaRedo, + FaTrash, + FaDownload, + FaFilePdf +} from 'react-icons/fa'; + +const Toolbar = ({ + tool, + setTool, + color, + setColor, + lineWidth, + setLineWidth, + fillColor, + setFillColor, + fontSize, + setFontSize, + onUndo, + onRedo, + onClear, + onExportImage, + onExportPDF, + canUndo, + canRedo +}) => { + const tools = [ + { id: 'pen', icon: , label: 'Pen' }, + { id: 'eraser', icon: , label: 'Eraser' }, + { id: 'line', icon: , label: 'Line' }, + { id: 'rectangle', icon: , label: 'Rectangle' }, + { id: 'circle', icon: , label: 'Circle' }, + { id: 'text', icon: , label: 'Text' } + ]; + + return ( +
+
+

Tools

+
+ {tools.map((t) => ( + + ))} +
+
+ +
+

Colors

+
+
+ + setColor(e.target.value)} + /> +
+
+ + setFillColor(e.target.value)} + /> +
+
+
+ +
+

Size

+ {tool === 'text' ? ( +
+ + setFontSize(Number(e.target.value))} + /> +
+ ) : ( +
+ + setLineWidth(Number(e.target.value))} + /> +
+ )} +
+ +
+

Actions

+
+ + + +
+
+ +
+

Export

+
+ + +
+
+ +
+
+

Shortcuts

+
    +
  • + Ctrl+Z Undo +
  • +
  • + Ctrl+Y Redo +
  • +
  • + Del Clear +
  • +
+
+
+
+ ); +}; + +export default Toolbar; diff --git a/src/plays/virtual-whiteboard/readme.md b/src/plays/virtual-whiteboard/readme.md new file mode 100644 index 000000000..09519e128 --- /dev/null +++ b/src/plays/virtual-whiteboard/readme.md @@ -0,0 +1,119 @@ +# Virtual Whiteboard + +A powerful virtual whiteboard with real-time drawing capabilities, layer management, and export functionality. Perfect for brainstorming, teaching, or collaborative design work. + +## Play Demographic + +- Language: js +- Level: Advanced + +## Creator Information + +- User: Abhrxdip +- Github Link: https://github.com/Abhrxdip +- Blog: +- Video: + +## Features + +### 🎨 Drawing Tools +- **Pen Tool**: Freehand drawing with customizable colors and widths +- **Eraser**: Remove unwanted strokes +- **Line Tool**: Draw straight lines +- **Rectangle Tool**: Create rectangles with fill options +- **Circle Tool**: Draw circles with customizable appearance +- **Text Tool**: Add text with custom font sizes + +### 🎯 Advanced Features +- **Layer Management**: + - Create multiple layers for organized drawing + - Show/hide layers individually + - Lock layers to prevent accidental edits + - Rename layers for better organization + - Delete unnecessary layers + - See object count per layer + +- **Undo/Redo**: + - Full history tracking + - Unlimited undo/redo steps + - Keyboard shortcuts (Ctrl+Z, Ctrl+Y) + +- **Export Options**: + - Export as PNG image + - Export as PDF (print-friendly) + - High-quality output + +- **Customization**: + - Adjustable stroke colors + - Fill colors for shapes + - Line width control (1-20px) + - Font size control (12-72px) + +## Implementation Details + +### React Concepts Used +- **useState**: Managing drawing state, tool selection, colors, and layers +- **useRef**: Canvas reference for drawing operations +- **useCallback**: Optimizing undo/redo functions +- **useEffect**: Canvas rendering and keyboard event handling +- **forwardRef**: Canvas component ref forwarding + +### Technical Implementation +- **Canvas API**: HTML5 Canvas for all drawing operations +- **Layer System**: Array-based layer management with visibility and lock states +- **History Management**: JSON-based state snapshots for undo/redo +- **Event Handling**: Mouse events for drawing interactions +- **Export Functionality**: Canvas.toDataURL() for image export + +### Architecture +``` +VirtualWhiteboard (Parent) +├── Toolbar (Tool selection & controls) +├── Canvas (Drawing surface) +└── LayerPanel (Layer management) +``` + +## Usage Instructions + +1. **Select a Tool**: Click on pen, eraser, line, rectangle, circle, or text tool +2. **Customize**: Adjust colors, line width, or font size as needed +3. **Draw**: Click and drag on the canvas to create your artwork +4. **Manage Layers**: + - Click "+" to add a new layer + - Click on a layer to make it active + - Use eye icon to show/hide + - Use lock icon to prevent editing +5. **Undo/Redo**: Use toolbar buttons or keyboard shortcuts +6. **Export**: Click PNG or PDF to save your work + +## Keyboard Shortcuts +- `Ctrl+Z` - Undo +- `Ctrl+Y` - Redo +- `Del` - Clear all (with confirmation) + +## Potential Enhancements +- WebSocket integration for real-time collaboration +- Cloud save/load functionality +- More shape tools (triangle, polygon, etc.) +- Background image support +- Grid/ruler overlay +- Color palette presets +- Brush texture options +- Layer opacity control +- Transform tools (rotate, scale) + +## Considerations +- Canvas size is fixed at 1200x700px for optimal performance +- History is stored in memory (cleared on page refresh) +- Export quality depends on canvas resolution +- Locked layers prevent all editing operations +- At least one layer must exist at all times + +## Resources +- [HTML5 Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) +- [React Icons](https://react-icons.github.io/react-icons/) +- [Canvas Drawing Tutorial](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial) + +--- + +[@Abhrxdip](https://github.com/Abhrxdip) diff --git a/src/plays/virtual-whiteboard/styles.css b/src/plays/virtual-whiteboard/styles.css new file mode 100644 index 000000000..c461ceaf2 --- /dev/null +++ b/src/plays/virtual-whiteboard/styles.css @@ -0,0 +1,454 @@ +.virtual-whiteboard { + width: 100%; + min-height: 100vh; + background: #f5f5f5; +} + +.whiteboard-container { + display: flex; + flex-direction: column; + height: 100%; + padding: 20px; + gap: 20px; +} + +/* Toolbar Styles */ +.toolbar { + background: white; + border-radius: 12px; + padding: 20px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-wrap: wrap; + gap: 30px; + align-items: flex-start; +} + +.toolbar-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.toolbar-title { + font-size: 14px; + font-weight: 600; + color: #333; + margin: 0; + margin-bottom: 8px; +} + +.tool-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.tool-btn { + width: 44px; + height: 44px; + border: 2px solid #e0e0e0; + background: white; + border-radius: 8px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 18px; + color: #666; + transition: all 0.2s; +} + +.tool-btn:hover { + border-color: #4a90e2; + color: #4a90e2; + transform: translateY(-2px); +} + +.tool-btn.active { + background: #4a90e2; + border-color: #4a90e2; + color: white; +} + +.color-controls { + display: flex; + gap: 15px; +} + +.control-group { + display: flex; + flex-direction: column; + gap: 5px; +} + +.control-group label { + font-size: 12px; + color: #666; + font-weight: 500; +} + +.color-picker { + width: 60px; + height: 40px; + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: pointer; + padding: 2px; +} + +.slider { + width: 150px; + cursor: pointer; +} + +.action-buttons, +.export-buttons { + display: flex; + gap: 8px; +} + +.action-btn, +.export-btn { + padding: 10px 16px; + border: 2px solid #e0e0e0; + background: white; + border-radius: 8px; + cursor: pointer; + font-size: 16px; + color: #666; + transition: all 0.2s; + display: flex; + align-items: center; + gap: 6px; +} + +.action-btn:hover:not(:disabled), +.export-btn:hover { + border-color: #4a90e2; + color: #4a90e2; + transform: translateY(-2px); +} + +.action-btn:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.action-btn.danger:hover { + border-color: #e74c3c; + color: #e74c3c; +} + +.export-btn { + font-size: 14px; + font-weight: 500; +} + +.keyboard-shortcuts { + padding: 12px; + background: #f9f9f9; + border-radius: 8px; + font-size: 12px; +} + +.keyboard-shortcuts h4 { + margin: 0 0 8px 0; + font-size: 13px; + color: #333; +} + +.keyboard-shortcuts ul { + list-style: none; + padding: 0; + margin: 0; +} + +.keyboard-shortcuts li { + margin: 4px 0; + color: #666; +} + +.keyboard-shortcuts kbd { + background: white; + border: 1px solid #ddd; + border-radius: 4px; + padding: 2px 6px; + font-size: 11px; + font-family: monospace; + margin-right: 6px; +} + +/* Main Area */ +.whiteboard-main { + display: flex; + gap: 20px; + height: calc(100vh - 280px); +} + +/* Canvas Styles */ +.canvas-container { + flex: 1; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: auto; + padding: 20px; +} + +.whiteboard-canvas { + border: 2px solid #e0e0e0; + border-radius: 8px; + cursor: crosshair; + background: white; +} + +.canvas-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.05); + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + pointer-events: none; +} + +.lock-message { + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + font-weight: 500; +} + +/* Layer Panel Styles */ +.layer-panel { + width: 280px; + background: white; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.layer-panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 2px solid #f0f0f0; +} + +.layer-panel-header h3 { + margin: 0; + font-size: 16px; + color: #333; +} + +.add-layer-btn { + width: 32px; + height: 32px; + border: none; + background: #4a90e2; + color: white; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all 0.2s; +} + +.add-layer-btn:hover { + background: #357abd; + transform: scale(1.05); +} + +.layer-list { + flex: 1; + overflow-y: auto; + padding: 12px; +} + +.layer-item { + background: #f9f9f9; + border: 2px solid #e0e0e0; + border-radius: 8px; + padding: 12px; + margin-bottom: 8px; + cursor: pointer; + transition: all 0.2s; + display: flex; + gap: 10px; + align-items: center; +} + +.layer-item:hover { + border-color: #4a90e2; + transform: translateX(2px); +} + +.layer-item.active { + background: #e3f2fd; + border-color: #4a90e2; +} + +.layer-item.locked { + opacity: 0.7; +} + +.layer-controls { + display: flex; + flex-direction: column; + gap: 6px; +} + +.layer-control-btn { + width: 28px; + height: 28px; + border: none; + background: white; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #666; + transition: all 0.2s; +} + +.layer-control-btn:hover { + color: #4a90e2; + transform: scale(1.1); +} + +.layer-info { + flex: 1; + min-width: 0; +} + +.layer-name { + font-size: 14px; + font-weight: 500; + color: #333; + display: block; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.layer-name-input { + width: 100%; + padding: 4px 8px; + border: 2px solid #4a90e2; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + outline: none; +} + +.layer-stats { + font-size: 11px; + color: #999; + margin-top: 2px; +} + +.layer-actions { + display: flex; + gap: 4px; +} + +.layer-action-btn { + width: 28px; + height: 28px; + border: none; + background: white; + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + color: #666; + transition: all 0.2s; +} + +.layer-action-btn:hover { + color: #4a90e2; + transform: scale(1.1); +} + +.layer-action-btn.danger:hover { + color: #e74c3c; +} + +.layer-info-box { + padding: 16px; + background: #f9f9f9; + border-top: 2px solid #f0f0f0; +} + +.layer-info-box h4 { + margin: 0 0 8px 0; + font-size: 13px; + color: #333; +} + +.layer-info-box ul { + list-style: none; + padding: 0; + margin: 0; + font-size: 12px; + color: #666; +} + +.layer-info-box li { + margin: 4px 0; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .whiteboard-main { + flex-direction: column; + height: auto; + } + + .layer-panel { + width: 100%; + } + + .canvas-container { + min-height: 500px; + } +} + +@media (max-width: 768px) { + .toolbar { + gap: 15px; + } + + .toolbar-section { + width: 100%; + } + + .whiteboard-canvas { + width: 100% !important; + height: 400px !important; + } + + .slider { + width: 100%; + } +}