feat: implement UI state persistence and theme toggle
Major improvements to UI state management and user preferences: - Add theme toggle with dark/light mode support - Implement Zustand persist middleware for UI state - Add ui-store for panel layout preferences (dock width, heights, tabs) - Persist tool settings (active tool, size, opacity, hardness, etc.) - Persist canvas view preferences (grid, rulers, snap-to-grid) - Persist shape tool settings - Persist collapsible section states - Fix canvas coordinate transformation for centered rendering - Constrain checkerboard and grid to canvas bounds - Add icons to all tab buttons and collapsible sections - Restructure panel-dock to use persisted state Storage impact: ~3.5KB total across all preferences Storage keys: tool-storage, canvas-view-storage, shape-storage, ui-storage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -102,8 +102,8 @@ export function CanvasWithTools() {
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-width / 2, -height / 2);
|
||||
|
||||
// Draw checkerboard background
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0');
|
||||
// Draw checkerboard background (only within canvas bounds)
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0', width, height);
|
||||
|
||||
// Draw background color if not transparent
|
||||
if (backgroundColor && backgroundColor !== 'transparent') {
|
||||
@@ -127,9 +127,9 @@ export function CanvasWithTools() {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw grid if enabled
|
||||
// Draw grid if enabled (only within canvas bounds)
|
||||
if (showGrid) {
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)');
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)', width, height);
|
||||
}
|
||||
|
||||
// Draw selection if active (marching ants)
|
||||
@@ -172,7 +172,7 @@ export function CanvasWithTools() {
|
||||
|
||||
const screenX = e.clientX - rect.left;
|
||||
const screenY = e.clientY - rect.top;
|
||||
const canvasPos = screenToCanvas(screenX, screenY);
|
||||
const canvasPos = screenToCanvas(screenX, screenY, rect.width, rect.height);
|
||||
|
||||
// Check for panning
|
||||
if (e.button === 1 || (e.button === 0 && e.shiftKey)) {
|
||||
@@ -259,7 +259,7 @@ export function CanvasWithTools() {
|
||||
|
||||
const screenX = e.clientX - rect.left;
|
||||
const screenY = e.clientY - rect.top;
|
||||
const canvasPos = screenToCanvas(screenX, screenY);
|
||||
const canvasPos = screenToCanvas(screenX, screenY, rect.width, rect.height);
|
||||
|
||||
// Panning
|
||||
if (isPanning) {
|
||||
|
||||
@@ -51,8 +51,8 @@ export function CanvasWrapper() {
|
||||
ctx.scale(zoom, zoom);
|
||||
ctx.translate(-width / 2, -height / 2);
|
||||
|
||||
// Draw checkerboard background
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0');
|
||||
// Draw checkerboard background (only within canvas bounds)
|
||||
drawCheckerboard(ctx, 10, '#ffffff', '#e0e0e0', width, height);
|
||||
|
||||
// Draw background color if not transparent
|
||||
if (backgroundColor && backgroundColor !== 'transparent') {
|
||||
@@ -76,9 +76,9 @@ export function CanvasWrapper() {
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// Draw grid if enabled
|
||||
// Draw grid if enabled (only within canvas bounds)
|
||||
if (showGrid) {
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)');
|
||||
drawGrid(ctx, gridSize, 'rgba(0, 0, 0, 0.15)', width, height);
|
||||
}
|
||||
|
||||
// Draw selection if active
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useColorStore } from '@/store/color-store';
|
||||
import { useToolStore } from '@/store';
|
||||
import { ColorPicker } from './color-picker';
|
||||
import { ColorSwatches } from './color-swatches';
|
||||
import { ArrowLeftRight, Palette, Clock } from 'lucide-react';
|
||||
import { ArrowLeftRight, Palette, Clock, Grid3x3 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
type Tab = 'picker' | 'swatches' | 'recent';
|
||||
@@ -32,15 +32,10 @@ export function ColorPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full bg-card border-r border-border w-64">
|
||||
{/* Header */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Colors</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col h-full bg-card w-full">
|
||||
{/* Current colors display */}
|
||||
<div className="p-3 border-b border-border">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="border-b border-border">
|
||||
<div className="flex items-center gap-2 p-3">
|
||||
{/* Primary/Secondary color squares */}
|
||||
<div className="relative">
|
||||
<button
|
||||
@@ -75,42 +70,45 @@ export function ColorPanel() {
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('picker')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'picker'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
Picker
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('swatches')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'swatches'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
Swatches
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('recent')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'recent'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Recent
|
||||
</button>
|
||||
<div className="border-b border-border">
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => setActiveTab('picker')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'picker'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Palette className="h-4 w-4" />
|
||||
Picker
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('swatches')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'swatches'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Grid3x3 className="h-4 w-4" />
|
||||
Swatches
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('recent')}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center gap-2 py-2 text-sm transition-colors',
|
||||
activeTab === 'recent'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
||||
)}
|
||||
>
|
||||
<Clock className="h-4 w-4" />
|
||||
Recent
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { CanvasWithTools } from '@/components/canvas/canvas-with-tools';
|
||||
import { FileMenu } from './file-menu';
|
||||
import { ToolOptions } from './tool-options';
|
||||
import { PanelDock } from './panel-dock';
|
||||
import { ThemeToggle } from './theme-toggle';
|
||||
import { ToolPalette } from '@/components/tools';
|
||||
import { useKeyboardShortcuts } from '@/hooks/use-keyboard-shortcuts';
|
||||
import { useFileOperations } from '@/hooks/use-file-operations';
|
||||
@@ -155,6 +156,11 @@ export function EditorLayout() {
|
||||
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
|
||||
<div className="w-px h-6 bg-border mx-1" />
|
||||
|
||||
{/* New Layer */}
|
||||
<button
|
||||
onClick={handleNewLayer}
|
||||
@@ -167,7 +173,7 @@ export function EditorLayout() {
|
||||
</div>
|
||||
|
||||
{/* Tool Adjustments Bar */}
|
||||
<div className="flex h-10 items-center border-b border-border bg-card/50 px-4">
|
||||
<div className="flex h-10 items-center border-b border-border bg-card/50">
|
||||
<ToolOptions />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,13 +8,6 @@ export function HistoryPanel() {
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-card">
|
||||
<div className="border-b border-border p-3">
|
||||
<h2 className="flex items-center gap-2 text-sm font-semibold text-card-foreground">
|
||||
<History className="h-4 w-4" />
|
||||
History
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-2 space-y-2">
|
||||
{undoStack.length === 0 && redoStack.length === 0 ? (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { ChevronDown, ChevronRight, Wand2, Square, Move, Hexagon, History, Sliders, Wrench } from 'lucide-react';
|
||||
import { useUIStore } from '@/store';
|
||||
import { LayersPanel } from '@/components/layers/layers-panel';
|
||||
import { ColorPanel } from '@/components/colors';
|
||||
import { FilterPanel } from '@/components/filters';
|
||||
@@ -12,73 +13,208 @@ import { HistoryPanel } from './history-panel';
|
||||
|
||||
interface CollapsibleSectionProps {
|
||||
title: string;
|
||||
icon?: React.ComponentType<{ className?: string }>;
|
||||
children: React.ReactNode;
|
||||
defaultOpen?: boolean;
|
||||
id: 'filters' | 'selection' | 'transform' | 'shapeSettings';
|
||||
}
|
||||
|
||||
function CollapsibleSection({ title, children, defaultOpen = true }: CollapsibleSectionProps) {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
function CollapsibleSection({ title, icon: Icon, children, id }: CollapsibleSectionProps) {
|
||||
const { collapsed, toggleCollapsed } = useUIStore();
|
||||
const isOpen = collapsed[id];
|
||||
|
||||
return (
|
||||
<div className="border-b border-border">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="w-full flex items-center justify-between p-3 hover:bg-accent/50 transition-colors"
|
||||
onClick={() => toggleCollapsed(id)}
|
||||
className="w-full flex items-center justify-between px-3 py-2 hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<h3 className="text-sm font-semibold text-card-foreground">{title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
{Icon && <Icon className="h-4 w-4 text-primary" />}
|
||||
<h3 className="text-sm font-semibold text-card-foreground">{title}</h3>
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
</button>
|
||||
{isOpen && <div className="pb-2">{children}</div>}
|
||||
{isOpen && <div>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function PanelDock() {
|
||||
const [activeTab, setActiveTab] = useState<'adjustments' | 'tools' | 'history'>('adjustments');
|
||||
const {
|
||||
panelDock,
|
||||
setActiveTab,
|
||||
setPanelWidth,
|
||||
setLayersHeight,
|
||||
setColorsHeight,
|
||||
} = useUIStore();
|
||||
const { activeTab, width, layersHeight, colorsHeight } = panelDock;
|
||||
|
||||
const [isResizingWidth, setIsResizingWidth] = useState(false);
|
||||
const [isResizingLayersHeight, setIsResizingLayersHeight] = useState(false);
|
||||
const [isResizingColorsHeight, setIsResizingColorsHeight] = useState(false);
|
||||
const dockRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Handle width resize
|
||||
useEffect(() => {
|
||||
if (!isResizingWidth) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const containerRect = dockRef.current.parentElement?.getBoundingClientRect();
|
||||
if (!containerRect) return;
|
||||
|
||||
const newWidth = containerRect.right - e.clientX;
|
||||
setPanelWidth(newWidth);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingWidth(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingWidth]);
|
||||
|
||||
// Handle layers height resize
|
||||
useEffect(() => {
|
||||
if (!isResizingLayersHeight) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const dockRect = dockRef.current.getBoundingClientRect();
|
||||
const relativeY = e.clientY - dockRect.top;
|
||||
const percentage = (relativeY / dockRect.height) * 100;
|
||||
setLayersHeight(percentage);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingLayersHeight(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingLayersHeight]);
|
||||
|
||||
// Handle colors height resize
|
||||
useEffect(() => {
|
||||
if (!isResizingColorsHeight) return;
|
||||
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (!dockRef.current) return;
|
||||
const dockRect = dockRef.current.getBoundingClientRect();
|
||||
const layersBottom = (layersHeight / 100) * dockRect.height;
|
||||
const relativeY = e.clientY - dockRect.top - layersBottom;
|
||||
const percentage = (relativeY / dockRect.height) * 100;
|
||||
setColorsHeight(percentage);
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
setIsResizingColorsHeight(false);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [isResizingColorsHeight, layersHeight]);
|
||||
|
||||
return (
|
||||
<div className="w-[280px] border-l border-border bg-card flex flex-col overflow-hidden">
|
||||
{/* Always visible: Layers Panel */}
|
||||
<div className="border-b border-border" style={{ height: '50%', minHeight: '300px' }}>
|
||||
<div
|
||||
ref={dockRef}
|
||||
className="border-l border-border bg-card flex flex-col overflow-hidden relative"
|
||||
style={{ width: `${width}px` }}
|
||||
>
|
||||
{/* Width resize handle (left edge) */}
|
||||
<div
|
||||
className="absolute left-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-primary/50 transition-colors z-10"
|
||||
onMouseDown={() => setIsResizingWidth(true)}
|
||||
/>
|
||||
|
||||
{/* Layers Panel */}
|
||||
<div
|
||||
className="overflow-hidden"
|
||||
style={{ height: `${layersHeight}%` }}
|
||||
>
|
||||
<LayersPanel />
|
||||
</div>
|
||||
|
||||
{/* Height resize handle (between layers and colors) */}
|
||||
<div
|
||||
className="h-1 cursor-row-resize hover:bg-primary/50 transition-colors border-b border-border flex-shrink-0"
|
||||
onMouseDown={() => setIsResizingLayersHeight(true)}
|
||||
/>
|
||||
|
||||
{/* Colors Panel */}
|
||||
<div
|
||||
className="overflow-hidden flex flex-col bg-card"
|
||||
style={{ height: `${colorsHeight}%` }}
|
||||
>
|
||||
<div className="border-b border-border p-3 flex-shrink-0">
|
||||
<h2 className="text-sm font-semibold text-card-foreground">Colors</h2>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<ColorPanel />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Height resize handle (between colors and tabs) */}
|
||||
<div
|
||||
className="h-1 cursor-row-resize hover:bg-primary/50 transition-colors border-b border-border flex-shrink-0"
|
||||
onMouseDown={() => setIsResizingColorsHeight(true)}
|
||||
/>
|
||||
|
||||
{/* Tabbed section */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<div className="flex-1 flex flex-col overflow-hidden" style={{ height: `${100 - layersHeight - colorsHeight}%` }}>
|
||||
{/* Tab buttons */}
|
||||
<div className="flex border-b border-border">
|
||||
<button
|
||||
onClick={() => setActiveTab('adjustments')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'adjustments'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<Sliders className="h-4 w-4" />
|
||||
Adjustments
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('tools')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'tools'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<Wrench className="h-4 w-4" />
|
||||
Tools
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
className={`flex-1 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-2 text-sm font-medium transition-colors ${
|
||||
activeTab === 'history'
|
||||
? 'bg-background text-foreground border-b-2 border-primary'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-accent/50'
|
||||
}`}
|
||||
>
|
||||
<History className="h-4 w-4" />
|
||||
History
|
||||
</button>
|
||||
</div>
|
||||
@@ -87,16 +223,13 @@ export function PanelDock() {
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{activeTab === 'adjustments' && (
|
||||
<div>
|
||||
<CollapsibleSection title="Colors">
|
||||
<ColorPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Filters">
|
||||
<CollapsibleSection title="Filters" icon={Wand2} id="filters">
|
||||
<FilterPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Selection">
|
||||
<CollapsibleSection title="Selection" icon={Square} id="selection">
|
||||
<SelectionPanel />
|
||||
</CollapsibleSection>
|
||||
<CollapsibleSection title="Transform">
|
||||
<CollapsibleSection title="Transform" icon={Move} id="transform">
|
||||
<TransformPanel />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
@@ -104,7 +237,7 @@ export function PanelDock() {
|
||||
|
||||
{activeTab === 'tools' && (
|
||||
<div>
|
||||
<CollapsibleSection title="Shape Settings">
|
||||
<CollapsibleSection title="Shape Settings" icon={Hexagon} id="shapeSettings">
|
||||
<ShapePanel />
|
||||
</CollapsibleSection>
|
||||
</div>
|
||||
|
||||
55
components/editor/theme-toggle.tsx
Normal file
55
components/editor/theme-toggle.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Moon, Sun } from 'lucide-react';
|
||||
|
||||
export function ThemeToggle() {
|
||||
const [theme, setTheme] = useState<'light' | 'dark'>('light');
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const currentTheme = savedTheme === 'dark' || (!savedTheme && prefersDark) ? 'dark' : 'light';
|
||||
setTheme(currentTheme);
|
||||
}, []);
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light';
|
||||
setTheme(newTheme);
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
};
|
||||
|
||||
// Prevent hydration mismatch
|
||||
if (!mounted) {
|
||||
return (
|
||||
<button
|
||||
className="rounded-md p-2 text-muted-foreground"
|
||||
disabled
|
||||
>
|
||||
<Sun className="h-4 w-4" />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="rounded-md p-2 hover:bg-accent transition-colors"
|
||||
title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
|
||||
>
|
||||
{theme === 'light' ? (
|
||||
<Moon className="h-4 w-4" />
|
||||
) : (
|
||||
<Sun className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -109,13 +109,7 @@ export function FilterPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Wand2 className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Filters</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Filter list */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="p-2 space-y-1">
|
||||
|
||||
@@ -128,13 +128,7 @@ export function SelectionPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Square className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Selection</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Selection Tools */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
@@ -63,13 +63,7 @@ export function ShapePanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Square className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Shapes</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Shape Types */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
@@ -51,13 +51,7 @@ export function TransformPanel() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 border-l border-border bg-card flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 border-b border-border p-3">
|
||||
<Move className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-sm font-semibold">Transform</h2>
|
||||
</div>
|
||||
|
||||
<div className="w-full border-l border-border bg-card flex flex-col">
|
||||
{/* Transform Tools */}
|
||||
<div className="border-b border-border p-3">
|
||||
<h3 className="text-xs font-semibold text-muted-foreground uppercase mb-2">
|
||||
|
||||
Reference in New Issue
Block a user