fix: render minimap only when enabled in Settings; default off
Browse files- src/App.tsx +108 -137
src/App.tsx
CHANGED
|
@@ -1,137 +1,108 @@
|
|
| 1 |
-
import { useEffect, useState, useRef, lazy, Suspense } from 'react';
|
| 2 |
-
import { AppProvider, useAppStore } from './store';
|
| 3 |
-
import { Canvas } from './components/Canvas';
|
| 4 |
-
import { Toolbar } from './components/Toolbar';
|
| 5 |
-
import { ContextMenu } from './components/ContextMenu';
|
| 6 |
-
import { Minimap } from './components/Minimap';
|
| 7 |
-
import { StarterHub } from './components/StarterHub';
|
| 8 |
-
import { getCurrentWindow } from '@tauri-apps/api/window';
|
| 9 |
-
import { invoke } from '@tauri-apps/api/core';
|
| 10 |
-
import { listen } from '@tauri-apps/api/event';
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
const
|
| 14 |
-
const
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
const
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
} = useAppStore();
|
| 30 |
-
const [toastMsg, setToastMsg] = useState<string | null>(null);
|
| 31 |
-
const [isCapturing, setIsCapturing] = useState(false);
|
| 32 |
-
const lastBPress = useRef(0);
|
| 33 |
-
|
| 34 |
-
useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
|
| 35 |
-
useEffect(() => { appWindow.setAlwaysOnTop(isAlwaysOnTop).catch(() => {}); }, [isAlwaysOnTop]);
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
const img =
|
| 43 |
-
img.
|
| 44 |
-
img.
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
setIsCapturing(
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
if (
|
| 61 |
-
|
| 62 |
-
if (
|
| 63 |
-
if (
|
| 64 |
-
if (
|
| 65 |
-
if (key === '
|
| 66 |
-
if (key === '
|
| 67 |
-
if (key === '
|
| 68 |
-
if (key === '
|
| 69 |
-
if (
|
| 70 |
-
if (
|
| 71 |
-
if (
|
| 72 |
-
if (key === '
|
| 73 |
-
if (key === '
|
| 74 |
-
if (
|
| 75 |
-
if (key === '
|
| 76 |
-
if (key === '
|
| 77 |
-
if (key === '
|
| 78 |
-
if (key === '
|
| 79 |
-
if (
|
| 80 |
-
if (
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
};
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
<
|
| 95 |
-
<
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
<
|
| 102 |
-
{
|
| 103 |
-
<
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
</Suspense>
|
| 110 |
-
<Minimap />
|
| 111 |
-
<ContextMenu />
|
| 112 |
-
<div className="absolute bottom-4 left-4 text-[11px] opacity-40 text-white pointer-events-none select-none z-10 mix-blend-difference">Space+Drag Pan · Scroll Zoom · B Browser · L Library · A Annotate · G Grid · Shift+S Screenshot</div>
|
| 113 |
-
{toastMsg && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[#1C1C1E]/90 border border-white/10 text-white font-medium px-4 py-2 rounded-full shadow-2xl backdrop-blur text-xs">{toastMsg}</div></div>}
|
| 114 |
-
{globalDesaturate && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-yellow-500/20 border border-yellow-500 text-white px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Desaturation Active (Shift+D)</div></div>}
|
| 115 |
-
{focusedImageId && <div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[#0A84FF]/20 border border-[#0A84FF] text-white px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Focus Mode — ←→ cycle, F/Esc exit</div></div>}
|
| 116 |
-
{isAnnotationMode && (
|
| 117 |
-
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-[#2A2A2E]/90 backdrop-blur-md border border-[#3A3A3E] shadow-2xl rounded-xl px-4 py-3 flex items-center gap-3 z-50 transition-all">
|
| 118 |
-
<div className="flex gap-1 items-center">
|
| 119 |
-
<button onClick={() => { setIsEraser(false); setIsHighlighter(false); }} className={`p-2 rounded-lg transition-all ${!isEraser && !isHighlighter ? 'bg-white/15 text-white' : 'text-gray-500 hover:bg-white/5 hover:text-white'}`} title="Pen"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/></svg></button>
|
| 120 |
-
<button onClick={() => { setIsEraser(false); setIsHighlighter(true); }} className={`p-2 rounded-lg transition-all ${!isEraser && isHighlighter ? 'bg-white/15 text-white' : 'text-gray-500 hover:bg-white/5 hover:text-white'}`} title="Highlighter"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m9 11-6 6v3h9l3-3"/><path d="m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4"/></svg></button>
|
| 121 |
-
<button onClick={() => setIsEraser(true)} className={`p-2 rounded-lg transition-all ${isEraser ? 'bg-white/15 text-white' : 'text-gray-500 hover:bg-white/5 hover:text-white'}`} title="Eraser"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m7 21-4.3-4.3c-1-1-1-2.5 0-3.4l9.6-9.6c1-1 2.5-1 3.4 0l5.6 5.6c1 1 1 2.5 0 3.4L13 21"/><path d="M22 21H7"/><path d="m5 11 9 9"/></svg></button>
|
| 122 |
-
</div>
|
| 123 |
-
<div className="w-px h-6 bg-[#3A3A3E] mx-1" />
|
| 124 |
-
<div className="flex gap-2 items-center">{['#FFFFFF','#0A84FF','#30D158','#FFD60A','#FF453A'].map(c => <button key={c} className={`w-6 h-6 rounded-full transition-all ${annotationColor === c && !isEraser ? 'scale-110 ring-[2px] ring-offset-2 ring-offset-[#2A2A2E] ring-white' : 'opacity-60 hover:opacity-100 hover:scale-105'}`} style={{ backgroundColor: c }} onClick={() => { setAnnotationColor(c); if (isEraser) setIsEraser(false); }} />)}</div>
|
| 125 |
-
<div className="w-px h-6 bg-[#3A3A3E] mx-1" />
|
| 126 |
-
<div className="flex items-center gap-1.5 px-1">{[4,10,20,30].map(s => <button key={s} className={`w-6 h-6 flex items-center justify-center rounded-md transition-colors ${annotationSize === s ? 'bg-white/15 text-white' : 'text-gray-500 hover:bg-white/10 hover:text-white'}`} onClick={() => setAnnotationSize(s)}><div className="bg-current rounded-full" style={{ width: Math.max(3, s * 0.4), height: Math.max(3, s * 0.4) }} /></button>)}</div>
|
| 127 |
-
<div className="w-px h-6 bg-[#3A3A3E] mx-1" />
|
| 128 |
-
<button className="px-3 py-1.5 rounded-lg text-sm font-medium text-[#FF453A] bg-[#FF453A]/10 hover:bg-[#FF453A]/20 transition-colors ml-1" onClick={() => setIsAnnotationMode(false)}>Close</button>
|
| 129 |
-
</div>
|
| 130 |
-
)}
|
| 131 |
-
{isZoomLensActive && <div className="absolute top-4 right-4 z-50 pointer-events-none"><div className="bg-[#2A2A2E]/80 border border-[#3A3A3E] text-white px-2 py-1 rounded text-[10px] backdrop-blur">🔍 Zoom Lens (release Z)</div></div>}
|
| 132 |
-
{isCapturing && <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] pointer-events-none"><div className="bg-[#2A2A2E] border border-[#3A3A3E] rounded-xl px-6 py-4 shadow-2xl text-white text-sm">Capturing...</div></div>}
|
| 133 |
-
</div>
|
| 134 |
-
);
|
| 135 |
-
};
|
| 136 |
-
|
| 137 |
-
export default function App() { return <AppProvider><MainApp /></AppProvider>; }
|
|
|
|
| 1 |
+
import { useEffect, useState, useRef, lazy, Suspense } from 'react';
|
| 2 |
+
import { AppProvider, useAppStore } from './store';
|
| 3 |
+
import { Canvas } from './components/Canvas';
|
| 4 |
+
import { Toolbar } from './components/Toolbar';
|
| 5 |
+
import { ContextMenu } from './components/ContextMenu';
|
| 6 |
+
import { Minimap } from './components/Minimap';
|
| 7 |
+
import { StarterHub } from './components/StarterHub';
|
| 8 |
+
import { getCurrentWindow } from '@tauri-apps/api/window';
|
| 9 |
+
import { invoke } from '@tauri-apps/api/core';
|
| 10 |
+
import { listen } from '@tauri-apps/api/event';
|
| 11 |
+
|
| 12 |
+
const BrowserPanel = lazy(() => import('./components/BrowserPanel').then(m => ({ default: m.BrowserPanel })));
|
| 13 |
+
const LibraryPanel = lazy(() => import('./components/LibraryPanel').then(m => ({ default: m.LibraryPanel })));
|
| 14 |
+
const SettingsPanel = lazy(() => import('./components/SettingsPanel').then(m => ({ default: m.SettingsPanel })));
|
| 15 |
+
|
| 16 |
+
const appWindow = getCurrentWindow();
|
| 17 |
+
|
| 18 |
+
const MainApp = () => {
|
| 19 |
+
const {
|
| 20 |
+
currentScreen, setIsBrowserOpen, setIsLibraryOpen, isBrowserOpen, isLibraryOpen,
|
| 21 |
+
isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity,
|
| 22 |
+
isAnnotationMode, setIsAnnotationMode, globalDesaturate, setGlobalDesaturate,
|
| 23 |
+
setZoom, setPan, pan, zoom, images, setImages, setTextNotes, selectedNodeIds, setSelectedNodeIds,
|
| 24 |
+
annotationColor, setAnnotationColor, annotationSize, setAnnotationSize,
|
| 25 |
+
isEraser, setIsEraser, isHighlighter, setIsHighlighter, undo, redo,
|
| 26 |
+
focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds,
|
| 27 |
+
isZoomLensActive, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser,
|
| 28 |
+
showMinimap,
|
| 29 |
+
} = useAppStore();
|
| 30 |
+
const [toastMsg, setToastMsg] = useState<string | null>(null);
|
| 31 |
+
const [isCapturing, setIsCapturing] = useState(false);
|
| 32 |
+
const lastBPress = useRef(0);
|
| 33 |
+
|
| 34 |
+
useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
|
| 35 |
+
useEffect(() => { appWindow.setAlwaysOnTop(isAlwaysOnTop).catch(() => {}); }, [isAlwaysOnTop]);
|
| 36 |
+
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
const unlisten = listen<any>('board://image_added', (event) => {
|
| 39 |
+
const data = event.payload;
|
| 40 |
+
if (!data || !data.url) return;
|
| 41 |
+
const img = new Image();
|
| 42 |
+
img.onload = () => { const ratio = img.width / img.height; const w = Math.min(400, img.width); const h = w / ratio; setImages(prev => [...prev, { id: data.id || crypto.randomUUID(), url: data.url, x: (-pan.x + window.innerWidth / 3 + Math.random() * 80) / zoom, y: (-pan.y + window.innerHeight / 3 + Math.random() * 80) / zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('✓ Image added'); };
|
| 43 |
+
img.onerror = () => { const w = Math.min(400, data.width || 300); const h = data.height ? w * (data.height / data.width) : w; setImages(prev => [...prev, { id: data.id || crypto.randomUUID(), url: data.url, x: (-pan.x + window.innerWidth / 3 + Math.random() * 80) / zoom, y: (-pan.y + window.innerHeight / 3 + Math.random() * 80) / zoom, width: w, height: h, aspectRatio: w / h }]); setToastMsg('✓ Image added'); };
|
| 44 |
+
img.src = data.url;
|
| 45 |
+
});
|
| 46 |
+
return () => { unlisten.then(fn => fn()); };
|
| 47 |
+
}, [pan, zoom, setImages]);
|
| 48 |
+
|
| 49 |
+
const doScreenCapture = async () => {
|
| 50 |
+
setIsCapturing(true); setToastMsg('Capturing...');
|
| 51 |
+
try { await appWindow.minimize(); await new Promise(r => setTimeout(r, 300)); const dataUrl = await invoke<string>('screen_capture_full'); await appWindow.unminimize(); await appWindow.setFocus(); if (dataUrl) { const img = new Image(); img.onload = () => { const ratio = img.width / img.height; const w = Math.min(800, img.width); const h = w / ratio; setImages(prev => [...prev, { id: crypto.randomUUID(), url: dataUrl, x: (-pan.x + window.innerWidth/2 - w/2)/zoom, y: (-pan.y + window.innerHeight/2 - h/2)/zoom, width: w, height: h, aspectRatio: ratio }]); setToastMsg('Screenshot captured!'); }; img.src = dataUrl; } } catch { setToastMsg('Capture failed'); await appWindow.unminimize().catch(() => {}); }
|
| 52 |
+
setIsCapturing(false);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
useEffect(() => {
|
| 56 |
+
const onKeyDown = (e: KeyboardEvent) => {
|
| 57 |
+
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement)?.isContentEditable) return;
|
| 58 |
+
const key = e.key.toLowerCase(); const ctrl = e.ctrlKey || e.metaKey;
|
| 59 |
+
if (ctrl && key === 'z') { e.preventDefault(); if (e.shiftKey) redo(); else undo(); return; }
|
| 60 |
+
if (ctrl && key === 's') { e.preventDefault(); setToastMsg('Saved'); return; }
|
| 61 |
+
if (e.shiftKey && key === 's' && !ctrl) { e.preventDefault(); doScreenCapture(); return; }
|
| 62 |
+
if (key === 'f' && !ctrl && !e.shiftKey) { if (focusedImageId) setFocusedImageId(null); else if (selectedNodeIds.length > 0) setFocusedImageId(selectedNodeIds[0]); return; }
|
| 63 |
+
if (key === 'v' && !ctrl && !e.shiftKey && selectedNodeIds.length > 0) { setValueMirrorIds(prev => { const all = selectedNodeIds.every(id => prev.includes(id)); return all ? prev.filter(id => !selectedNodeIds.includes(id)) : [...new Set([...prev, ...selectedNodeIds])]; }); return; }
|
| 64 |
+
if (key === 'h' && !ctrl && !e.shiftKey && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedH: !i.isFlippedH } : i)); return; }
|
| 65 |
+
if (key === 'z' && !ctrl && !e.shiftKey) { setIsZoomLensActive(true); return; }
|
| 66 |
+
if (ctrl && key === 'd') { e.preventDefault(); if (selectedNodeIds.length > 0) { const dupes = images.filter(i => selectedNodeIds.includes(i.id)).map(i => ({ ...i, id: crypto.randomUUID(), x: i.x + 30, y: i.y + 30 })); setImages(prev => [...prev, ...dupes]); setSelectedNodeIds(dupes.map(d => d.id)); setToastMsg(`Duplicated ${dupes.length}`); } return; }
|
| 67 |
+
if (ctrl && key === 'g') { e.preventDefault(); if (e.shiftKey) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: undefined } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: undefined } : n)); setToastMsg('Ungrouped'); } else if (selectedNodeIds.length > 1) { const gid = crypto.randomUUID(); setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, groupId: gid } : i)); setTextNotes(prev => prev.map(n => selectedNodeIds.includes(n.id) ? { ...n, groupId: gid } : n)); setToastMsg('Grouped'); } return; }
|
| 68 |
+
if (focusedImageId && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) { const idx = images.findIndex(i => i.id === focusedImageId); if (idx >= 0) { const next = e.key === 'ArrowRight' ? (idx + 1) % images.length : (idx - 1 + images.length) % images.length; setFocusedImageId(images[next].id); setSelectedNodeIds([images[next].id]); } return; }
|
| 69 |
+
if (key === 'b' && !ctrl && !e.shiftKey) { const now = Date.now(); if (now - lastBPress.current < 300) { setIsWhisperBrowser(true); setIsBrowserOpen(true); } else { setIsBrowserOpen(p => !p); setIsWhisperBrowser(false); } lastBPress.current = now; return; }
|
| 70 |
+
if (key === 'l' && !ctrl && !e.shiftKey) { setIsLibraryOpen(p => !p); return; }
|
| 71 |
+
if (ctrl && key === ',') { e.preventDefault(); setIsSettingsOpen(p => !p); return; }
|
| 72 |
+
if (key === 'a' && !ctrl && !e.shiftKey) { setIsAnnotationMode(p => !p); return; }
|
| 73 |
+
if (key === 'g' && !ctrl && !e.shiftKey) { window.dispatchEvent(new CustomEvent('muse:toggle-grid', { detail: true })); return; }
|
| 74 |
+
if (key === 'd' && !ctrl) { if (e.shiftKey) setGlobalDesaturate(p => !p); else if (selectedNodeIds.length > 0) setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isDesaturated: !i.isDesaturated } : i)); return; }
|
| 75 |
+
if (key === 't' && !ctrl && !e.shiftKey) { setIsAlwaysOnTop(p => !p); return; }
|
| 76 |
+
if (e.key === 'Escape') { setIsBrowserOpen(false); setIsLibraryOpen(false); setIsSettingsOpen(false); setSelectedNodeIds([]); setFocusedImageId(null); return; }
|
| 77 |
+
if (e.key === 'Delete' || e.key === 'Backspace') { if (selectedNodeIds.length > 0) { setImages(prev => prev.filter(i => !selectedNodeIds.includes(i.id))); setTextNotes(prev => prev.filter(n => !selectedNodeIds.includes(n.id))); setSelectedNodeIds([]); } return; }
|
| 78 |
+
if (ctrl && key === 'a') { e.preventDefault(); setSelectedNodeIds(images.map(i => i.id)); return; }
|
| 79 |
+
if (ctrl && key === '0') { e.preventDefault(); if (images.length === 0) { setZoom(1); setPan({ x: 0, y: 0 }); return; } let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; images.forEach(i => { minX = Math.min(minX, i.x); minY = Math.min(minY, i.y); maxX = Math.max(maxX, i.x + i.width); maxY = Math.max(maxY, i.y + i.height); }); const pad = 100, w = maxX - minX + pad*2, h = maxY - minY + pad*2; const nz = Math.max(0.1, Math.min(window.innerWidth/w, window.innerHeight/h)); setZoom(nz); setPan({ x: -((minX+maxX)/2)*nz + window.innerWidth/2, y: -((minY+maxY)/2)*nz + window.innerHeight/2 }); }
|
| 80 |
+
if (ctrl && key === '1') { e.preventDefault(); setZoom(1); }
|
| 81 |
+
};
|
| 82 |
+
const onKeyUp = (e: KeyboardEvent) => { if (e.key.toLowerCase() === 'z') setIsZoomLensActive(false); };
|
| 83 |
+
window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp);
|
| 84 |
+
return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); };
|
| 85 |
+
}, [selectedNodeIds, images, pan, zoom, focusedImageId, undo, redo]);
|
| 86 |
+
|
| 87 |
+
const handleCtxMenu = (e: React.MouseEvent) => { if (!(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement) && !(e.target as HTMLElement).isContentEditable) e.preventDefault(); };
|
| 88 |
+
|
| 89 |
+
if (currentScreen === 'hub') return <div className="w-screen h-screen relative overflow-hidden bg-[#0A0A0B]" onContextMenu={handleCtxMenu}><StarterHub /><Suspense fallback={null}>{isSettingsOpen && <SettingsPanel />}</Suspense></div>;
|
| 90 |
+
|
| 91 |
+
return (
|
| 92 |
+
<div className="w-screen h-screen relative overflow-hidden" style={{ backgroundColor: `rgba(28, 28, 30, ${isAlwaysOnTop ? bgOpacity / 100 : 1})` }} onContextMenu={handleCtxMenu}>
|
| 93 |
+
<Canvas />
|
| 94 |
+
{(isBrowserOpen || isLibraryOpen || isSettingsOpen) && <div className="absolute inset-0 bg-black/40 pointer-events-none z-30 transition-opacity" />}
|
| 95 |
+
<Toolbar />
|
| 96 |
+
<Suspense fallback={null}>{isBrowserOpen && <BrowserPanel />}{isLibraryOpen && <LibraryPanel />}{isSettingsOpen && <SettingsPanel />}</Suspense>
|
| 97 |
+
{showMinimap && <Minimap />}
|
| 98 |
+
<ContextMenu />
|
| 99 |
+
<div className="absolute bottom-4 left-4 text-[11px] opacity-40 text-white pointer-events-none select-none z-10 mix-blend-difference">Space+Drag Pan · Scroll Zoom · B Browser · L Library · A Annotate · G Grid · Shift+S Screenshot</div>
|
| 100 |
+
{toastMsg && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[#1C1C1E]/90 border border-white/10 text-white font-medium px-4 py-2 rounded-full shadow-2xl backdrop-blur text-xs">{toastMsg}</div></div>}
|
| 101 |
+
{globalDesaturate && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-yellow-500/20 border border-yellow-500 text-white px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Desaturation Active (Shift+D)</div></div>}
|
| 102 |
+
{focusedImageId && <div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[#0A84FF]/20 border border-[#0A84FF] text-white px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Focus Mode — ←→ cycle, F/Esc exit</div></div>}
|
| 103 |
+
{isCapturing && <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] pointer-events-none"><div className="bg-[#2A2A2E] border border-[#3A3A3E] rounded-xl px-6 py-4 shadow-2xl text-white text-sm">Capturing...</div></div>}
|
| 104 |
+
</div>
|
| 105 |
+
);
|
| 106 |
+
};
|
| 107 |
+
|
| 108 |
+
export default function App() { return <AppProvider><MainApp /></AppProvider>; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|