fix: apply theme variables to Canvas background and drop overlay
Browse files- src/components/Canvas.tsx +7 -33
src/components/Canvas.tsx
CHANGED
|
@@ -29,38 +29,13 @@ export const Canvas = () => {
|
|
| 29 |
|
| 30 |
useEffect(() => {
|
| 31 |
let disposed = false; let unlisten: (() => void) | undefined;
|
| 32 |
-
async function setup() {
|
| 33 |
-
try {
|
| 34 |
-
unlisten = await getCurrentWebview().onDragDropEvent(async (event) => {
|
| 35 |
-
const payload: any = event.payload;
|
| 36 |
-
if (payload.type === 'enter' || payload.type === 'over') { setIsDropActive(true); return; }
|
| 37 |
-
if (payload.type === 'leave') { setIsDropActive(false); return; }
|
| 38 |
-
if (payload.type === 'drop') {
|
| 39 |
-
setIsDropActive(false);
|
| 40 |
-
const paths: string[] = (payload.paths || []).filter(isImagePath);
|
| 41 |
-
if (!paths.length) return;
|
| 42 |
-
const dpr = window.devicePixelRatio || 1;
|
| 43 |
-
const pos = payload.position ? { x: payload.position.x / dpr, y: payload.position.y / dpr } : undefined;
|
| 44 |
-
for (const filePath of paths) {
|
| 45 |
-
try { const item: any = await invoke('library_import_local', { path: filePath }); addLibraryItemToCanvas(item, pos?.x, pos?.y); }
|
| 46 |
-
catch (err) { console.error('[Canvas] native drop import failed:', filePath, err); }
|
| 47 |
-
}
|
| 48 |
-
}
|
| 49 |
-
});
|
| 50 |
-
if (disposed && unlisten) unlisten();
|
| 51 |
-
} catch (err) { console.error('[Canvas] failed to register native webview drop listener:', err); }
|
| 52 |
-
}
|
| 53 |
setup(); return () => { disposed = true; if (unlisten) unlisten(); };
|
| 54 |
}, [addLibraryItemToCanvas]);
|
| 55 |
|
| 56 |
useEffect(() => {
|
| 57 |
const handleDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; };
|
| 58 |
-
const handleDrop = async (e: DragEvent) => {
|
| 59 |
-
e.preventDefault(); setIsDropActive(false);
|
| 60 |
-
const museData = e.dataTransfer?.getData('application/x-muse-library-item') || e.dataTransfer?.getData('text/plain');
|
| 61 |
-
if (museData) { try { const payload = JSON.parse(museData); if (payload.data_url || payload.dataUrl) { addLibraryItemToCanvas(payload, e.clientX, e.clientY); return; } } catch {} }
|
| 62 |
-
if (e.dataTransfer?.files?.length) for (const file of Array.from(e.dataTransfer.files)) { if (!file.type.startsWith('image/')) continue; const reader = new FileReader(); reader.onload = async (ev) => { const dataUrl = ev.target?.result as string; if (!dataUrl) return; try { const item: any = await invoke('library_import_data_url', { dataUrl, title: file.name.replace(/\.[^/.]+$/, '') }); addLibraryItemToCanvas(item, e.clientX, e.clientY); } catch (err) { const img = new Image(); img.onload = () => addLibraryItemToCanvas({ url: dataUrl, data_url: dataUrl, width: img.width, height: img.height }, e.clientX, e.clientY); img.src = dataUrl; } }; reader.readAsDataURL(file); }
|
| 63 |
-
};
|
| 64 |
const handleDragEnter = (e: DragEvent) => { e.preventDefault(); setIsDropActive(true); };
|
| 65 |
const handleDragLeave = () => setIsDropActive(false);
|
| 66 |
document.addEventListener('dragover', handleDragOver); document.addEventListener('drop', handleDrop); document.addEventListener('dragenter', handleDragEnter); document.addEventListener('dragleave', handleDragLeave);
|
|
@@ -74,14 +49,13 @@ export const Canvas = () => {
|
|
| 74 |
const handlePointerMove = (e: React.PointerEvent) => { if (isDraggingCanvas) { setPan(p => ({ x: p.x + e.movementX, y: p.y + e.movementY })); } else if (isDrawing) { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); const x = (e.clientX - r.left - pan.x) / zoom, y = (e.clientY - r.top - pan.y) / zoom; if (isEraser) { const er = annotationSize / zoom * 2; setAnnotations(prev => prev.filter(ann => !ann.points.some(p => Math.hypot(p.x - x, p.y - y) <= er + ann.strokeWidth))); } else { setCurrentPath(prev => [...prev, { x, y }]); } } };
|
| 75 |
const handlePointerUp = (e: React.PointerEvent) => { setIsDraggingCanvas(false); if (isDrawing) { setIsDrawing(false); if (currentPath.length > 1 && !isEraser) setAnnotations(prev => [...prev, { id: crypto.randomUUID(), points: currentPath, color: annotationColor, strokeWidth: (annotationSize / zoom) * (isHighlighter ? 3 : 1), isHighlighter: isHighlighter || undefined }]); setCurrentPath([]); } try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {} };
|
| 76 |
|
| 77 |
-
return <div ref={containerRef} className={`absolute inset-0 w-full h-full overflow-hidden ${isSpaceDown ? 'cursor-grab' : isAnnotationMode ? 'cursor-crosshair' : 'cursor-default'} ${isDraggingCanvas ? '!cursor-grabbing' : ''}`} onWheel={handleWheel} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, imageId: null }); }}>
|
| 78 |
-
{isDropActive && <div className="absolute inset-0 z-[100] pointer-events-none flex items-center justify-center"><div className="absolute inset-4 border-2 border-dashed border-[
|
| 79 |
-
{showGrid && <div className="absolute inset-0 pointer-events-none opacity-[0.06]" style={{ backgroundImage: 'radial-gradient(circle,
|
| 80 |
<div id="canvas-inner" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: '0 0' }} className="w-full h-full absolute top-0 left-0 z-10">
|
| 81 |
{Array.from(new Set(images.filter(img => img.groupId).map(img => img.groupId!))).map(gid => { const gi = images.filter(img => img.groupId === gid); if (!gi.length) return null; const minX = Math.min(...gi.map(i => i.x)), minY = Math.min(...gi.map(i => i.y)), maxX = Math.max(...gi.map(i => i.x + i.width)), maxY = Math.max(...gi.map(i => i.y + i.height)); return <div key={`g-${gid}`} className="absolute bg-white/5 border border-white/20 rounded-xl pointer-events-none" style={{ left: minX-20, top: minY-40, width: maxX-minX+40, height: maxY-minY+60, zIndex: 0 }}><div className="text-gray-500 text-xs font-semibold uppercase tracking-wider pl-4 pt-2">Group</div></div>; })}
|
| 82 |
-
{images.map(img => <RefImageNode key={img.id} image={img} />)}
|
| 83 |
-
{
|
| 84 |
-
{palettes.map(p => { const img = images.find(i => i.id === p.imageId); if (!img) return null; const ss = Math.max(18, Math.min(42, img.width / Math.max(6, p.colors.length + 1))); return <div key={`pal-${p.imageId}`} className="absolute flex flex-row gap-1 bg-[#1C1C1E] p-1.5 rounded-lg shadow-xl cursor-default pointer-events-auto" style={{ left: img.x, top: img.y + img.height + 10, zIndex: 8000 }} onPointerDown={e => e.stopPropagation()}>{p.colors.map(c => <div key={c} className="rounded-md cursor-pointer hover:scale-110 transition-transform shadow-inner flex items-center justify-center group" style={{ backgroundColor: c, width: ss, height: ss }} title={c} onClick={e => { e.stopPropagation(); navigator.clipboard.writeText(c); }}><div className="opacity-0 group-hover:opacity-100 bg-black/60 text-white text-[9px] px-1 rounded backdrop-blur">COPY</div></div>)}<div className="flex items-center justify-center text-gray-500 hover:text-white cursor-pointer" style={{ width: Math.max(18, ss*0.7), height: ss }} onClick={() => setPalettes(prev => prev.filter(x => x.imageId !== p.imageId))}>×</div></div>; })}
|
| 85 |
<svg className="absolute inset-0 w-full h-full pointer-events-none z-[9999]" style={{ overflow: 'visible' }}>{annotations.map(ann => <polyline key={ann.id} points={ann.points.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke={ann.color} strokeWidth={ann.strokeWidth} strokeLinecap={ann.isHighlighter ? 'square' : 'round'} strokeLinejoin={ann.isHighlighter ? 'miter' : 'round'} opacity={ann.isHighlighter ? 0.35 : 1} style={ann.isHighlighter ? { mixBlendMode: 'screen' } : undefined} />)}{isDrawing && currentPath.length > 0 && !isEraser && <polyline points={currentPath.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke={annotationColor} strokeWidth={(annotationSize / zoom) * (isHighlighter ? 3 : 1)} strokeLinecap={isHighlighter ? 'square' : 'round'} strokeLinejoin={isHighlighter ? 'miter' : 'round'} opacity={isHighlighter ? 0.35 : 1} style={isHighlighter ? { mixBlendMode: 'screen' } : undefined} />}</svg>
|
| 86 |
</div>
|
| 87 |
</div>;
|
|
|
|
| 29 |
|
| 30 |
useEffect(() => {
|
| 31 |
let disposed = false; let unlisten: (() => void) | undefined;
|
| 32 |
+
async function setup() { try { unlisten = await getCurrentWebview().onDragDropEvent(async (event) => { const payload: any = event.payload; if (payload.type === 'enter' || payload.type === 'over') { setIsDropActive(true); return; } if (payload.type === 'leave') { setIsDropActive(false); return; } if (payload.type === 'drop') { setIsDropActive(false); const paths: string[] = (payload.paths || []).filter(isImagePath); const dpr = window.devicePixelRatio || 1; const pos = payload.position ? { x: payload.position.x / dpr, y: payload.position.y / dpr } : undefined; for (const filePath of paths) { try { const item: any = await invoke('library_import_local', { path: filePath }); addLibraryItemToCanvas(item, pos?.x, pos?.y); } catch (err) { console.error('[Canvas] native drop import failed:', filePath, err); } } } }); if (disposed && unlisten) unlisten(); } catch (err) { console.error('[Canvas] failed to register native webview drop listener:', err); } }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
setup(); return () => { disposed = true; if (unlisten) unlisten(); };
|
| 34 |
}, [addLibraryItemToCanvas]);
|
| 35 |
|
| 36 |
useEffect(() => {
|
| 37 |
const handleDragOver = (e: DragEvent) => { e.preventDefault(); if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy'; };
|
| 38 |
+
const handleDrop = async (e: DragEvent) => { e.preventDefault(); setIsDropActive(false); const museData = e.dataTransfer?.getData('application/x-muse-library-item') || e.dataTransfer?.getData('text/plain'); if (museData) { try { const payload = JSON.parse(museData); if (payload.data_url || payload.dataUrl) { addLibraryItemToCanvas(payload, e.clientX, e.clientY); return; } } catch {} } if (e.dataTransfer?.files?.length) for (const file of Array.from(e.dataTransfer.files)) { if (!file.type.startsWith('image/')) continue; const reader = new FileReader(); reader.onload = async (ev) => { const dataUrl = ev.target?.result as string; if (!dataUrl) return; try { const item: any = await invoke('library_import_data_url', { dataUrl, title: file.name.replace(/\.[^/.]+$/, '') }); addLibraryItemToCanvas(item, e.clientX, e.clientY); } catch { const img = new Image(); img.onload = () => addLibraryItemToCanvas({ url: dataUrl, data_url: dataUrl, width: img.width, height: img.height }, e.clientX, e.clientY); img.src = dataUrl; } }; reader.readAsDataURL(file); } };
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
const handleDragEnter = (e: DragEvent) => { e.preventDefault(); setIsDropActive(true); };
|
| 40 |
const handleDragLeave = () => setIsDropActive(false);
|
| 41 |
document.addEventListener('dragover', handleDragOver); document.addEventListener('drop', handleDrop); document.addEventListener('dragenter', handleDragEnter); document.addEventListener('dragleave', handleDragLeave);
|
|
|
|
| 49 |
const handlePointerMove = (e: React.PointerEvent) => { if (isDraggingCanvas) { setPan(p => ({ x: p.x + e.movementX, y: p.y + e.movementY })); } else if (isDrawing) { const r = (e.currentTarget as HTMLElement).getBoundingClientRect(); const x = (e.clientX - r.left - pan.x) / zoom, y = (e.clientY - r.top - pan.y) / zoom; if (isEraser) { const er = annotationSize / zoom * 2; setAnnotations(prev => prev.filter(ann => !ann.points.some(p => Math.hypot(p.x - x, p.y - y) <= er + ann.strokeWidth))); } else { setCurrentPath(prev => [...prev, { x, y }]); } } };
|
| 50 |
const handlePointerUp = (e: React.PointerEvent) => { setIsDraggingCanvas(false); if (isDrawing) { setIsDrawing(false); if (currentPath.length > 1 && !isEraser) setAnnotations(prev => [...prev, { id: crypto.randomUUID(), points: currentPath, color: annotationColor, strokeWidth: (annotationSize / zoom) * (isHighlighter ? 3 : 1), isHighlighter: isHighlighter || undefined }]); setCurrentPath([]); } try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {} };
|
| 51 |
|
| 52 |
+
return <div ref={containerRef} className={`absolute inset-0 w-full h-full overflow-hidden bg-[var(--canvas-bg)] ${isSpaceDown ? 'cursor-grab' : isAnnotationMode ? 'cursor-crosshair' : 'cursor-default'} ${isDraggingCanvas ? '!cursor-grabbing' : ''}`} onWheel={handleWheel} onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={handlePointerUp} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ x: e.clientX, y: e.clientY, imageId: null }); }}>
|
| 53 |
+
{isDropActive && <div className="absolute inset-0 z-[100] pointer-events-none flex items-center justify-center"><div className="absolute inset-4 border-2 border-dashed border-[var(--accent)] rounded-2xl bg-[var(--accent)]/5" /><div className="relative z-10 bg-[var(--panel-surface)] border border-[var(--accent)] rounded-xl px-6 py-4 shadow-2xl flex flex-col items-center gap-2"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg><span className="text-[var(--accent)] font-semibold text-sm">Drop images to add to canvas</span><span className="text-[var(--ui-secondary)] text-xs">Images will be saved to your Asset Library</span></div></div>}
|
| 54 |
+
{showGrid && <div className="absolute inset-0 pointer-events-none opacity-[0.06]" style={{ backgroundImage: 'radial-gradient(circle, var(--ui-primary) 1px, transparent 1px)', backgroundSize: `${24 * zoom}px ${24 * zoom}px`, backgroundPosition: `${pan.x % (24 * zoom)}px ${pan.y % (24 * zoom)}px` }} />}
|
| 55 |
<div id="canvas-inner" style={{ transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`, transformOrigin: '0 0' }} className="w-full h-full absolute top-0 left-0 z-10">
|
| 56 |
{Array.from(new Set(images.filter(img => img.groupId).map(img => img.groupId!))).map(gid => { const gi = images.filter(img => img.groupId === gid); if (!gi.length) return null; const minX = Math.min(...gi.map(i => i.x)), minY = Math.min(...gi.map(i => i.y)), maxX = Math.max(...gi.map(i => i.x + i.width)), maxY = Math.max(...gi.map(i => i.y + i.height)); return <div key={`g-${gid}`} className="absolute bg-white/5 border border-white/20 rounded-xl pointer-events-none" style={{ left: minX-20, top: minY-40, width: maxX-minX+40, height: maxY-minY+60, zIndex: 0 }}><div className="text-gray-500 text-xs font-semibold uppercase tracking-wider pl-4 pt-2">Group</div></div>; })}
|
| 57 |
+
{images.map(img => <RefImageNode key={img.id} image={img} />)}{textNotes?.map(note => <TextNoteNode key={note.id} note={note} />)}
|
| 58 |
+
{palettes.map(p => { const img = images.find(i => i.id === p.imageId); if (!img) return null; const ss = Math.max(18, Math.min(42, img.width / Math.max(6, p.colors.length + 1))); return <div key={`pal-${p.imageId}`} className="absolute flex flex-row gap-1 bg-[var(--panel-bg)] p-1.5 rounded-lg shadow-xl cursor-default pointer-events-auto" style={{ left: img.x, top: img.y + img.height + 10, zIndex: 8000 }} onPointerDown={e => e.stopPropagation()}>{p.colors.map(c => <div key={c} className="rounded-md cursor-pointer hover:scale-110 transition-transform shadow-inner flex items-center justify-center group" style={{ backgroundColor: c, width: ss, height: ss }} title={c} onClick={e => { e.stopPropagation(); navigator.clipboard.writeText(c); }}><div className="opacity-0 group-hover:opacity-100 bg-black/60 text-white text-[9px] px-1 rounded backdrop-blur">COPY</div></div>)}<div className="flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] cursor-pointer" style={{ width: Math.max(18, ss*0.7), height: ss }} onClick={() => setPalettes(prev => prev.filter(x => x.imageId !== p.imageId))}>×</div></div>; })}
|
|
|
|
| 59 |
<svg className="absolute inset-0 w-full h-full pointer-events-none z-[9999]" style={{ overflow: 'visible' }}>{annotations.map(ann => <polyline key={ann.id} points={ann.points.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke={ann.color} strokeWidth={ann.strokeWidth} strokeLinecap={ann.isHighlighter ? 'square' : 'round'} strokeLinejoin={ann.isHighlighter ? 'miter' : 'round'} opacity={ann.isHighlighter ? 0.35 : 1} style={ann.isHighlighter ? { mixBlendMode: 'screen' } : undefined} />)}{isDrawing && currentPath.length > 0 && !isEraser && <polyline points={currentPath.map(p => `${p.x},${p.y}`).join(' ')} fill="none" stroke={annotationColor} strokeWidth={(annotationSize / zoom) * (isHighlighter ? 3 : 1)} strokeLinecap={isHighlighter ? 'square' : 'round'} strokeLinejoin={isHighlighter ? 'miter' : 'round'} opacity={isHighlighter ? 0.35 : 1} style={isHighlighter ? { mixBlendMode: 'screen' } : undefined} />}</svg>
|
| 60 |
</div>
|
| 61 |
</div>;
|