diff --git a/app/layout.tsx b/app/layout.tsx index 044d75f..8d73368 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next'; import './globals.css'; import { ToastProvider } from '@/components/providers/toast-provider'; +import { ContextMenu } from '@/components/ui/context-menu'; export const metadata: Metadata = { title: 'Paint UI - Browser Image Editor', @@ -36,6 +37,7 @@ export default function RootLayout({ {children} + ); diff --git a/components/layers/layers-panel.tsx b/components/layers/layers-panel.tsx index 1fe246a..b1c684e 100644 --- a/components/layers/layers-panel.tsx +++ b/components/layers/layers-panel.tsx @@ -1,7 +1,8 @@ 'use client'; import { useLayerStore } from '@/store'; -import { Eye, EyeOff, Trash2, Copy } from 'lucide-react'; +import { useContextMenuStore } from '@/store/context-menu-store'; +import { Eye, EyeOff, Trash2, Copy, Layers, MoveUp, MoveDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { deleteLayerWithHistory, @@ -10,11 +11,88 @@ import { } from '@/lib/layer-operations'; export function LayersPanel() { - const { layers, activeLayerId, setActiveLayer } = useLayerStore(); + const { layers, activeLayerId, setActiveLayer, reorderLayer } = useLayerStore(); + const { showContextMenu } = useContextMenuStore(); // Sort layers by order (highest first) const sortedLayers = [...layers].sort((a, b) => b.order - a.order); + const handleMoveLayer = (layerId: string, direction: 'up' | 'down') => { + const layer = layers.find((l) => l.id === layerId); + if (!layer) return; + + const layerIndex = sortedLayers.findIndex((l) => l.id === layerId); + if (direction === 'up' && layerIndex > 0) { + const targetLayer = sortedLayers[layerIndex - 1]; + reorderLayer(layerId, targetLayer.order); + } else if (direction === 'down' && layerIndex < sortedLayers.length - 1) { + const targetLayer = sortedLayers[layerIndex + 1]; + reorderLayer(layerId, targetLayer.order); + } + }; + + const handleContextMenu = (e: React.MouseEvent, layerId: string) => { + e.preventDefault(); + const layer = layers.find((l) => l.id === layerId); + if (!layer) return; + + const layerIndex = sortedLayers.findIndex((l) => l.id === layerId); + const canMoveUp = layerIndex > 0; + const canMoveDown = layerIndex < sortedLayers.length - 1; + const canDelete = layers.length > 1; + + showContextMenu(e.clientX, e.clientY, [ + { + label: 'Duplicate Layer', + icon: , + onClick: () => duplicateLayerWithHistory(layerId), + }, + { + separator: true, + label: '', + onClick: () => {}, + }, + { + label: 'Move Up', + icon: , + onClick: () => handleMoveLayer(layerId, 'up'), + disabled: !canMoveUp, + }, + { + label: 'Move Down', + icon: , + onClick: () => handleMoveLayer(layerId, 'down'), + disabled: !canMoveDown, + }, + { + separator: true, + label: '', + onClick: () => {}, + }, + { + label: layer.visible ? 'Hide Layer' : 'Show Layer', + icon: layer.visible ? : , + onClick: () => updateLayerWithHistory(layerId, { visible: !layer.visible }, 'Toggle Visibility'), + }, + { + separator: true, + label: '', + onClick: () => {}, + }, + { + label: 'Delete Layer', + icon: , + onClick: () => { + if (confirm('Delete this layer?')) { + deleteLayerWithHistory(layerId); + } + }, + disabled: !canDelete, + danger: true, + }, + ]); + }; + return (
@@ -37,6 +115,7 @@ export function LayersPanel() { : 'border-border hover:border-primary/50 hover:bg-accent/50' )} onClick={() => setActiveLayer(layer.id)} + onContextMenu={(e) => handleContextMenu(e, layer.id)} > + ); + })} +
+ + ); +} diff --git a/store/context-menu-store.ts b/store/context-menu-store.ts new file mode 100644 index 0000000..1c94091 --- /dev/null +++ b/store/context-menu-store.ts @@ -0,0 +1,40 @@ +import { create } from 'zustand'; + +export interface ContextMenuItem { + label: string; + icon?: React.ReactNode; + onClick: () => void; + disabled?: boolean; + separator?: boolean; + danger?: boolean; +} + +interface ContextMenuState { + isOpen: boolean; + position: { x: number; y: number } | null; + items: ContextMenuItem[]; + showContextMenu: (x: number, y: number, items: ContextMenuItem[]) => void; + hideContextMenu: () => void; +} + +export const useContextMenuStore = create((set) => ({ + isOpen: false, + position: null, + items: [], + + showContextMenu: (x, y, items) => { + set({ + isOpen: true, + position: { x, y }, + items, + }); + }, + + hideContextMenu: () => { + set({ + isOpen: false, + position: null, + items: [], + }); + }, +})); diff --git a/store/index.ts b/store/index.ts index 6904d0c..04e7911 100644 --- a/store/index.ts +++ b/store/index.ts @@ -10,3 +10,4 @@ export * from './shape-store'; export * from './text-store'; export * from './ui-store'; export * from './toast-store'; +export * from './context-menu-store';