Collaborative-pixel-art/frontend/src/components/ui/Toolbar.tsx
martin 1da96f34a6 Modernize collaborative pixel art platform to production-ready architecture
Major refactor from simple HTML/JS app to modern full-stack TypeScript application:

## Architecture Changes
- Migrated to monorepo structure with workspaces (backend, frontend, shared)
- Backend: Node.js + Express + TypeScript + Socket.IO
- Frontend: Next.js 15.5 + React 19 + TypeScript + Tailwind CSS
- Shared: Common types and utilities across packages

## Key Features Implemented
- Real-time WebSocket collaboration via Socket.IO
- Virtual canvas with chunked loading for performance
- Modern UI with dark mode and responsive design
- Mock database system for easy development (Redis/PostgreSQL compatible)
- Comprehensive error handling and rate limiting
- User presence and cursor tracking
- Infinite canvas support with zoom/pan controls

## Performance Optimizations
- Canvas virtualization - only renders visible viewport
- Chunked pixel data loading (64x64 pixel chunks)
- Optimized WebSocket protocol
- Memory-efficient state management with Zustand

## Development Experience
- Full TypeScript support across all packages
- Hot reload for both frontend and backend
- Docker support for production deployment
- Comprehensive linting and formatting
- Automated development server startup

## Fixed Issues
- Corrected start script paths
- Updated environment configuration
- Fixed ESLint configuration issues
- Ensured all dependencies are properly installed
- Verified build process works correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-22 19:28:05 +02:00

146 lines
No EOL
5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useCanvasStore } from '../../store/canvasStore';
import { useTheme } from '../ThemeProvider';
export function Toolbar() {
const {
selectedTool,
setSelectedTool,
brushSize,
setBrushSize,
showGrid,
showCursors,
viewport,
setZoom,
setViewport,
} = useCanvasStore();
const { theme, setTheme } = useTheme();
const tools = [
{ id: 'pixel', name: 'Pixel', icon: '🖊️' },
{ id: 'fill', name: 'Fill', icon: '🪣' },
{ id: 'eyedropper', name: 'Eyedropper', icon: '💉' },
] as const;
const handleZoomIn = () => {
setZoom(viewport.zoom * 1.2);
};
const handleZoomOut = () => {
setZoom(viewport.zoom * 0.8);
};
const handleResetView = () => {
setViewport({ x: 0, y: 0, zoom: 1 });
};
return (
<div className="fixed top-6 left-1/2 transform -translate-x-1/2 z-40 flex flex-wrap items-center gap-2 md:gap-3 bg-gradient-to-r from-gray-900/95 to-black/90 backdrop-blur-xl rounded-2xl p-2 md:p-4 border border-white/10 shadow-2xl max-w-[calc(100vw-2rem)]">
{/* Tools */}
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
{tools.map((tool) => (
<button
key={tool.id}
className={`px-2 md:px-3 py-1 md:py-2 rounded-lg text-xs md:text-sm font-medium transition-all duration-200 ${
selectedTool === tool.id
? 'bg-blue-500 text-white shadow-lg'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
onClick={() => setSelectedTool(tool.id)}
title={tool.name}
>
<span className="text-base md:mr-1">{tool.icon}</span>
<span className="hidden md:inline">{tool.name}</span>
</button>
))}
</div>
{/* Brush size (only for pixel tool) */}
{selectedTool === 'pixel' && (
<div className="flex items-center gap-3 bg-white/5 rounded-xl p-3 border border-white/10">
<span className="text-sm text-white/80 font-medium">Size:</span>
<input
type="range"
min={1}
max={10}
value={brushSize}
onChange={(e) => setBrushSize(parseInt(e.target.value))}
className="w-20 h-2 bg-white/20 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #3b82f6 0%, #3b82f6 ${(brushSize - 1) * 11.11}%, rgba(255,255,255,0.2) ${(brushSize - 1) * 11.11}%, rgba(255,255,255,0.2) 100%)`
}}
/>
<span className="text-sm text-white/80 font-medium min-w-[1.5rem] text-center">
{brushSize}
</span>
</div>
)}
{/* Zoom controls */}
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
<button
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
onClick={handleZoomOut}
title="Zoom Out"
>
🔍
</button>
<div className="px-3 py-2 text-sm text-white/80 font-mono min-w-[4rem] text-center">
{Math.round(viewport.zoom * 100)}%
</div>
<button
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
onClick={handleZoomIn}
title="Zoom In"
>
🔍
</button>
<div className="w-px h-6 bg-white/20 mx-1" />
<button
className="px-3 py-2 rounded-lg text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
onClick={handleResetView}
title="Reset View"
>
🏠
</button>
</div>
{/* View options */}
<div className="flex items-center gap-1 bg-white/5 rounded-xl p-1 border border-white/10">
<button
className={`px-3 py-2 rounded-lg transition-all duration-200 ${
showGrid
? 'bg-green-500 text-white shadow-lg'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
onClick={() => useCanvasStore.setState({ showGrid: !showGrid })}
title="Toggle Grid"
>
</button>
<button
className={`px-3 py-2 rounded-lg transition-all duration-200 ${
showCursors
? 'bg-purple-500 text-white shadow-lg'
: 'text-white/70 hover:text-white hover:bg-white/10'
}`}
onClick={() => useCanvasStore.setState({ showCursors: !showCursors })}
title="Toggle Cursors"
>
👁
</button>
</div>
{/* Theme toggle */}
<button
className="px-3 py-2 rounded-xl bg-white/5 border border-white/10 text-white/70 hover:text-white hover:bg-white/10 transition-all duration-200"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
title="Toggle Theme"
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
</div>
);
}