import { useAppStore } from "../store"; const MenuItem = ({ onClick, children, className = "", }: { onClick: () => void; children: React.ReactNode; className?: string; }) => ( ); export const ContextMenu = () => { const { contextMenu, setContextMenu, setImages, selectedNodeIds, setZoom, setPan, zoom, pan, setPalettes, images, setIsAnnotationMode, setAnnotations, setTextNotes, } = useAppStore(); if (!contextMenu) return null; const handleDelete = () => { setImages((prev) => prev.filter((img) => !selectedNodeIds.includes(img.id)), ); setTextNotes((prev) => prev.filter((note) => !selectedNodeIds.includes(note.id)), ); setAnnotations((prev) => prev.filter((ann) => !selectedNodeIds.includes(ann.id)), ); setPalettes((prev) => prev.filter((p) => !selectedNodeIds.includes(p.imageId)), ); setContextMenu(null); }; const handleDesaturate = () => { setImages((prev) => prev.map((img) => selectedNodeIds.includes(img.id) ? { ...img, isDesaturated: !img.isDesaturated } : img, ), ); setContextMenu(null); }; const handleFlipH = () => { setImages((prev) => prev.map((img) => selectedNodeIds.includes(img.id) ? { ...img, isFlippedH: !img.isFlippedH } : img, ), ); setContextMenu(null); }; const handleFlipV = () => { setImages((prev) => prev.map((img) => selectedNodeIds.includes(img.id) ? { ...img, isFlippedV: !img.isFlippedV } : img, ), ); setContextMenu(null); }; const handleBringToFront = () => { setImages((prev) => { const selected = prev.filter((img) => selectedNodeIds.includes(img.id)); const others = prev.filter((img) => !selectedNodeIds.includes(img.id)); return [...others, ...selected]; }); setTextNotes((prev) => { const selected = prev.filter((n) => selectedNodeIds.includes(n.id)); const others = prev.filter((n) => !selectedNodeIds.includes(n.id)); return [...others, ...selected]; }); setContextMenu(null); }; const handleSendToBack = () => { setImages((prev) => { const selected = prev.filter((img) => selectedNodeIds.includes(img.id)); const others = prev.filter((img) => !selectedNodeIds.includes(img.id)); return [...selected, ...others]; }); setTextNotes((prev) => { const selected = prev.filter((n) => selectedNodeIds.includes(n.id)); const others = prev.filter((n) => !selectedNodeIds.includes(n.id)); return [...selected, ...others]; }); setContextMenu(null); }; const handleFit = () => { setZoom(1); setPan({ x: 0, y: 0 }); setContextMenu(null); }; const handleExtractPalette = () => { if (contextMenu.imageId) { const img = images.find((i) => i.id === contextMenu.imageId); if (img) { // Mock palette extraction with aesthetic colors const aestheticPalettes = [ ["#2A363B", "#E84A5F", "#FF847C", "#FECEAB", "#99B898"], ["#F8B195", "#F67280", "#C06C84", "#6C5B7B", "#355C7D"], ["#1A1A1D", "#4E4E50", "#6F2232", "#950740", "#C3073F"], ]; setPalettes((prev) => { const existing = prev.filter((p) => p.imageId !== img.id); return [ ...existing, { imageId: img.id, colors: aestheticPalettes[ Math.floor(Math.random() * aestheticPalettes.length) ], x: img.x, y: img.y + img.height + 20, }, ]; }); } } setContextMenu(null); }; const handleGroup = () => { const groupId = Math.random().toString(36).substr(2, 9); setImages((prev) => prev.map((img) => selectedNodeIds.includes(img.id) ? { ...img, groupId } : img, ), ); setTextNotes((prev) => prev.map((n) => selectedNodeIds.includes(n.id) ? { ...n, groupId } : n, ), ); setAnnotations((prev) => prev.map((ann) => selectedNodeIds.includes(ann.id) ? { ...ann, groupId } : ann, ), ); setContextMenu(null); }; const handleUngroup = () => { setImages((prev) => prev.map((img) => selectedNodeIds.includes(img.id) ? { ...img, groupId: undefined } : img, ), ); setTextNotes((prev) => prev.map((n) => selectedNodeIds.includes(n.id) ? { ...n, groupId: undefined } : n, ), ); setAnnotations((prev) => prev.map((ann) => selectedNodeIds.includes(ann.id) ? { ...ann, groupId: undefined } : ann, ), ); setContextMenu(null); }; const handleAddTextNote = () => { const newX = (contextMenu.x - pan.x) / zoom; const newY = (contextMenu.y - pan.y) / zoom; setTextNotes((prev) => [ ...prev, { id: Math.random().toString(36).substr(2, 9), text: "New Note", x: newX, y: newY, width: 200, }, ]); setContextMenu(null); }; return ( <>
{ e.stopPropagation(); if (e.button === 0) setContextMenu(null); }} onContextMenu={(e) => { e.preventDefault(); setContextMenu(null); }} />
{contextMenu.imageId ? ( <> Fit to canvas Desaturate / Restore
Flip horizontal Flip vertical
Extract palette { setIsAnnotationMode(true); setContextMenu(null); }} > Add annotation { if (contextMenu.imageId) { const img = images.find((i) => i.id === contextMenu.imageId); if (img) { try { const response = await fetch(img.url); const blob = await response.blob(); await navigator.clipboard.write([ new ClipboardItem({ [blob.type]: blob, }), ]); } catch (err) { // fallback to URL if image is cross-origin restricted navigator.clipboard.writeText(img.url); } } } setContextMenu(null); }} > Copy image { window.open( images.find((i) => i.id === contextMenu.imageId)?.url, "_blank", ); setContextMenu(null); }} > Open source URL
{selectedNodeIds.length > 1 && ( <> { // Layout in Row const selected = images.filter((img) => selectedNodeIds.includes(img.id), ); if (selected.length < 2) return; // Sort by current X position selected.sort((a, b) => a.x - b.x); const startX = selected[0].x; const startY = selected[0].y; const targetHeight = selected[0].height; let currentX = startX; const gap = 20; const newImages = images.map((img) => { if (selectedNodeIds.includes(img.id)) { const sortedIndex = selected.findIndex( (s) => s.id === img.id, ); // Scale to match height const scale = targetHeight / img.height; const newWidth = img.width * scale; // we have to calculate the X position based on all previous items' widths let curX = startX; for (let i = 0; i < sortedIndex; i++) { curX += selected[i].width * (targetHeight / selected[i].height) + gap; } return { ...img, x: curX, y: startY, width: newWidth, height: targetHeight, }; } return img; }); setImages(newImages); setContextMenu(null); }} > Organize in Row Group selection )} Ungroup
Bring to front Send to back
Delete selection ) : ( <> Fit to canvas (Reset View) { setAnnotations([]); setContextMenu(null); }} > Clear all annotations
Add Text Note { setIsAnnotationMode(true); setContextMenu(null); }} > Add annotation )}
); };