asdf98 commited on
Commit
8a98dfb
·
verified ·
1 Parent(s): 1ad851c

fix: always-on-top behavior and remove opacity side-effect

Browse files
Files changed (1) hide show
  1. src/App.tsx +29 -3
src/App.tsx CHANGED
@@ -16,13 +16,39 @@ const SettingsPanel = lazy(() => import('./components/SettingsPanel').then(m =>
16
  const appWindow = getCurrentWindow();
17
  const SAVE_PATH_KEY = 'lumaref.manualSavePath.v1';
18
  const safeFilename = (title: string) => `${(title || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}`;
 
 
 
19
  const MainApp = () => {
20
- const { currentScreen, setCurrentScreen, setIsBrowserOpen, setIsLibraryOpen, isBrowserOpen, isLibraryOpen, isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, isClickThrough, setIsClickThrough, isAnnotationMode, setIsAnnotationMode, globalDesaturate, setGlobalDesaturate, setZoom, setPan, pan, zoom, images, setImages, textNotes, setTextNotes, annotations, setAnnotations, palettes, setPalettes, selectedNodeIds, setSelectedNodeIds, undo, redo, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, showMinimap, setShowMinimap, showGrid, setShowGrid, setIsEraser, setIsHighlighter, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle } = useAppStore();
21
  const [toastMsg, setToastMsg] = useState<string | null>(null); const [isCapturing, setIsCapturing] = useState(false); const lastBPress = useRef(0);
22
  const boardState = () => ({ textNotes, images, annotations, palettes, zoom, pan, title: boardTitle }); const jsonState = () => JSON.stringify(boardState(), null, 2);
23
  useEffect(() => { const onToast = (event: Event) => setToastMsg(String((event as CustomEvent).detail || '')); const onError = (event: Event) => setToastMsg(`✗ ${String((event as CustomEvent).detail || 'Error')}`); window.addEventListener('lumaref:toast', onToast); window.addEventListener('lumaref:error', onError); return () => { window.removeEventListener('lumaref:toast', onToast); window.removeEventListener('lumaref:error', onError); }; }, []);
24
  useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
25
- useEffect(() => { appWindow.setAlwaysOnTop(isAlwaysOnTop).catch(() => {}); }, [isAlwaysOnTop]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const persistInternal = () => activeProjectId ? invoke('project_save', { id: activeProjectId, state: JSON.stringify(boardState()), title: boardTitle }).catch(() => {}) : Promise.resolve();
27
  const saveManual = async () => { try { let path = localStorage.getItem(SAVE_PATH_KEY); if (!path) path = await saveDialog({ title: 'Save LumaRef board', defaultPath: `${safeFilename(boardTitle)}.json`, filters: [{ name: 'LumaRef Board JSON', extensions: ['json'] }] }) as string | null; if (!path) return; await invoke('board_export_file', { filepath: path, state: jsonState() }); localStorage.setItem(SAVE_PATH_KEY, path); await persistInternal(); setToastMsg(`Saved to ${path}`); } catch (e) { setToastMsg(`Save failed: ${e}`); } };
28
  const exportJson = async () => { try { const path = await saveDialog({ title: 'Export board as JSON', defaultPath: `${safeFilename(boardTitle)}.json`, filters: [{ name: 'JSON', extensions: ['json'] }] }); if (!path) return; await invoke('board_export_file', { filepath: path, state: jsonState() }); setToastMsg(`Exported JSON to ${path}`); } catch (e) { setToastMsg(`Export failed: ${e}`); } };
@@ -33,7 +59,7 @@ const MainApp = () => {
33
  useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (isEditableTarget(e.target)) return; const settings = loadShortcutSettings(); const run = (id: Parameters<typeof matchesShortcut>[0]) => matchesShortcut(id, e, settings); if (run('edit.undo')) { e.preventDefault(); undo(); return; } if (run('edit.redo')) { e.preventDefault(); redo(); return; } if (run('file.save')) { e.preventDefault(); saveManual(); return; } if (run('file.newBoard')) { e.preventDefault(); newBoard(); return; } if (run('file.exportJson')) { e.preventDefault(); exportJson(); return; } if (run('file.exportRefs')) { e.preventDefault(); exportRefs(); return; } if (run('capture.screen')) { e.preventDefault(); doScreenCapture(); return; } if (run('view.zoomIn')) { e.preventDefault(); setZoom(z => Math.min(12, z * 1.15)); return; } if (run('view.zoomOut')) { e.preventDefault(); setZoom(z => Math.max(0.08, z / 1.15)); return; } if (run('view.panHome')) { e.preventDefault(); setPan({ x: 0, y: 0 }); return; } if (run('view.toggleGrid')) { e.preventDefault(); setShowGrid(v => !v); return; } if (run('view.toggleMinimap')) { setShowMinimap(v => !v); return; } if (run('view.focus')) { if (focusedImageId) setFocusedImageId(null); else if (selectedNodeIds.length > 0) setFocusedImageId(selectedNodeIds[0]); return; } if (run('view.valueMirror') && 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; } if (run('view.zoomLens')) { setIsZoomLensActive(true); return; } if (run('view.fitAll')) { e.preventDefault(); fitAll(); return; } if (run('view.zoom100')) { e.preventDefault(); setZoom(1); return; } if (run('selection.duplicate')) { 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; } if (run('selection.group')) { e.preventDefault(); 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; } if (run('selection.ungroup')) { e.preventDefault(); 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'); return; } if (run('selection.flipH') && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedH: !i.isFlippedH } : i)); return; } if (run('selection.flipV') && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedV: !i.isFlippedV } : i)); return; } if (run('selection.bringFront')) { setImages(prev => [...prev.filter(i => !selectedNodeIds.includes(i.id)), ...prev.filter(i => selectedNodeIds.includes(i.id))]); return; } if (run('selection.sendBack')) { setImages(prev => [...prev.filter(i => selectedNodeIds.includes(i.id)), ...prev.filter(i => !selectedNodeIds.includes(i.id))]); return; } if (run('selection.desaturate')) { if (selectedNodeIds.length > 0) setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isDesaturated: !i.isDesaturated } : i)); return; } if (run('selection.delete')) { if (selectedNodeIds.length > 0) { setImages(prev => prev.filter(i => !selectedNodeIds.includes(i.id))); setTextNotes(prev => prev.filter(n => !selectedNodeIds.includes(n.id))); setSelectedNodeIds([]); } return; } if (run('selection.selectAll')) { e.preventDefault(); setSelectedNodeIds(images.map(i => i.id)); return; } if (run('panel.browser')) { const now = Date.now(); if (now - lastBPress.current < 300) { setIsWhisperBrowser(true); setIsBrowserOpen(true); } else { setIsBrowserOpen(p => !p); setIsWhisperBrowser(false); } lastBPress.current = now; return; } if (run('panel.library')) { setIsLibraryOpen(p => !p); return; } if (run('panel.settings')) { e.preventDefault(); setIsSettingsOpen(p => !p); return; } if (run('panel.closeAll')) { e.preventDefault(); if (isAnnotationMode) { setIsAnnotationMode(false); return; } setIsBrowserOpen(false); setIsLibraryOpen(false); setIsSettingsOpen(false); setSelectedNodeIds([]); setFocusedImageId(null); return; } if (run('tool.annotate')) { e.preventDefault(); setIsAnnotationMode(p => !p); return; } if (run('tool.annotationPen')) { setIsAnnotationMode(true); setIsEraser(false); setIsHighlighter(false); return; } if (run('tool.annotationHighlighter')) { setIsAnnotationMode(true); setIsEraser(false); setIsHighlighter(true); return; } if (run('tool.annotationEraser')) { setIsAnnotationMode(true); setIsEraser(true); setIsHighlighter(false); return; } if (run('tool.annotationClear')) { e.preventDefault(); if (annotations.length && confirm(`Clear all ${annotations.length} annotation strokes?`)) setAnnotations([]); return; } if (run('tool.globalDesaturate')) { setGlobalDesaturate(p => !p); return; } if (run('window.alwaysOnTop')) { setIsAlwaysOnTop(p => !p); return; } if (run('window.clickThrough')) { setIsClickThrough(p => !p); return; } 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; } }; const onKeyUp = (e: KeyboardEvent) => { if (matchesShortcut('view.zoomLens', e)) setIsZoomLensActive(false); }; window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); }; }, [selectedNodeIds, images, textNotes, annotations, palettes, pan, zoom, focusedImageId, undo, redo, isAnnotationMode, isCapturing, activeProjectId, boardTitle]);
34
  const handleCtxMenu = (e: React.MouseEvent) => { if (!(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement) && !(e.target as HTMLElement).isContentEditable) e.preventDefault(); };
35
  if (currentScreen === 'hub') return <div className="w-screen h-screen relative overflow-hidden bg-[var(--panel-bg)]" onContextMenu={handleCtxMenu}><StarterHub /><Suspense fallback={null}>{isSettingsOpen && <SettingsPanel />}</Suspense>{toastMsg && <Toast msg={toastMsg} />}</div>;
36
- return <div className="w-screen h-screen relative overflow-hidden bg-[var(--canvas-bg)]" style={{ opacity: isAlwaysOnTop ? bgOpacity / 100 : 1 }} onContextMenu={handleCtxMenu}><Canvas />{(isBrowserOpen || isLibraryOpen || isSettingsOpen) && <div className="absolute inset-0 bg-black/40 pointer-events-none z-30 transition-opacity" />}<Toolbar /><Suspense fallback={null}>{isBrowserOpen && <BrowserPanel />}{isLibraryOpen && <LibraryPanel />}{isSettingsOpen && <SettingsPanel />}</Suspense><AnnotationWidget />{showMinimap && <Minimap />}<ContextMenu />{toastMsg && <Toast msg={toastMsg} />}{globalDesaturate && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[var(--accent-amber)]/20 border border-[var(--accent-amber)] text-[var(--ui-primary)] px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Desaturation Active (Shift+D)</div></div>}{focusedImageId && <div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[var(--accent)]/20 border border-[var(--accent)] text-[var(--ui-primary)] px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Focus Mode — ←→ cycle, F/Esc exit</div></div>}{isCapturing && <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] pointer-events-none"><div className="bg-[var(--panel-surface)] border border-[var(--panel-border)] rounded-xl px-6 py-4 shadow-2xl text-[var(--ui-primary)] text-sm">Capturing...</div></div>}</div>;
37
  };
38
  function Toast({ msg }: { msg: string }) { return <div className="absolute top-16 left-1/2 -translate-x-1/2 z-[120] pointer-events-none"><div className="bg-[var(--panel-bg)]/90 border border-white/10 text-[var(--ui-primary)] font-medium px-4 py-2 rounded-full shadow-2xl backdrop-blur text-xs max-w-[520px] truncate">{msg}</div></div>; }
39
  export default function App() { return <AppProvider><MainApp /></AppProvider>; }
 
16
  const appWindow = getCurrentWindow();
17
  const SAVE_PATH_KEY = 'lumaref.manualSavePath.v1';
18
  const safeFilename = (title: string) => `${(title || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}`;
19
+ function emitToast(msg: string) { window.dispatchEvent(new CustomEvent('lumaref:toast', { detail: msg })); }
20
+ function emitError(msg: string) { window.dispatchEvent(new CustomEvent('lumaref:error', { detail: msg })); }
21
+
22
  const MainApp = () => {
23
+ const { currentScreen, setCurrentScreen, setIsBrowserOpen, setIsLibraryOpen, isBrowserOpen, isLibraryOpen, isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, isClickThrough, setIsClickThrough, isAnnotationMode, setIsAnnotationMode, globalDesaturate, setGlobalDesaturate, setZoom, setPan, pan, zoom, images, setImages, textNotes, setTextNotes, annotations, setAnnotations, palettes, setPalettes, selectedNodeIds, setSelectedNodeIds, undo, redo, focusedImageId, setFocusedImageId, valueMirrorIds, setValueMirrorIds, setIsZoomLensActive, isWhisperBrowser, setIsWhisperBrowser, showMinimap, setShowMinimap, showGrid, setShowGrid, setIsEraser, setIsHighlighter, activeProjectId, setActiveProjectId, boardTitle, setBoardTitle } = useAppStore();
24
  const [toastMsg, setToastMsg] = useState<string | null>(null); const [isCapturing, setIsCapturing] = useState(false); const lastBPress = useRef(0);
25
  const boardState = () => ({ textNotes, images, annotations, palettes, zoom, pan, title: boardTitle }); const jsonState = () => JSON.stringify(boardState(), null, 2);
26
  useEffect(() => { const onToast = (event: Event) => setToastMsg(String((event as CustomEvent).detail || '')); const onError = (event: Event) => setToastMsg(`✗ ${String((event as CustomEvent).detail || 'Error')}`); window.addEventListener('lumaref:toast', onToast); window.addEventListener('lumaref:error', onError); return () => { window.removeEventListener('lumaref:toast', onToast); window.removeEventListener('lumaref:error', onError); }; }, []);
27
  useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(null), 3000); return () => clearTimeout(t); } }, [toastMsg]);
28
+ useEffect(() => {
29
+ let cancelled = false;
30
+ async function applyAlwaysOnTop() {
31
+ try {
32
+ await appWindow.setAlwaysOnTop(isAlwaysOnTop);
33
+ if (!cancelled && isAlwaysOnTop) emitToast('Always on top enabled');
34
+ } catch (err) {
35
+ console.error('[LumaRef] setAlwaysOnTop failed:', err);
36
+ if (!cancelled) {
37
+ setIsAlwaysOnTop(false);
38
+ emitError(`Always on top failed: ${err}. Restart the app after updating permissions; some Linux Wayland/fullscreen contexts may restrict this.`);
39
+ }
40
+ }
41
+ }
42
+ applyAlwaysOnTop();
43
+ return () => { cancelled = true; };
44
+ }, [isAlwaysOnTop, setIsAlwaysOnTop]);
45
+ useEffect(() => {
46
+ appWindow.setIgnoreCursorEvents(isClickThrough).catch(err => {
47
+ console.error('[LumaRef] setIgnoreCursorEvents failed:', err);
48
+ setIsClickThrough(false);
49
+ emitError(`Click-through failed: ${err}`);
50
+ });
51
+ }, [isClickThrough, setIsClickThrough]);
52
  const persistInternal = () => activeProjectId ? invoke('project_save', { id: activeProjectId, state: JSON.stringify(boardState()), title: boardTitle }).catch(() => {}) : Promise.resolve();
53
  const saveManual = async () => { try { let path = localStorage.getItem(SAVE_PATH_KEY); if (!path) path = await saveDialog({ title: 'Save LumaRef board', defaultPath: `${safeFilename(boardTitle)}.json`, filters: [{ name: 'LumaRef Board JSON', extensions: ['json'] }] }) as string | null; if (!path) return; await invoke('board_export_file', { filepath: path, state: jsonState() }); localStorage.setItem(SAVE_PATH_KEY, path); await persistInternal(); setToastMsg(`Saved to ${path}`); } catch (e) { setToastMsg(`Save failed: ${e}`); } };
54
  const exportJson = async () => { try { const path = await saveDialog({ title: 'Export board as JSON', defaultPath: `${safeFilename(boardTitle)}.json`, filters: [{ name: 'JSON', extensions: ['json'] }] }); if (!path) return; await invoke('board_export_file', { filepath: path, state: jsonState() }); setToastMsg(`Exported JSON to ${path}`); } catch (e) { setToastMsg(`Export failed: ${e}`); } };
 
59
  useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { if (isEditableTarget(e.target)) return; const settings = loadShortcutSettings(); const run = (id: Parameters<typeof matchesShortcut>[0]) => matchesShortcut(id, e, settings); if (run('edit.undo')) { e.preventDefault(); undo(); return; } if (run('edit.redo')) { e.preventDefault(); redo(); return; } if (run('file.save')) { e.preventDefault(); saveManual(); return; } if (run('file.newBoard')) { e.preventDefault(); newBoard(); return; } if (run('file.exportJson')) { e.preventDefault(); exportJson(); return; } if (run('file.exportRefs')) { e.preventDefault(); exportRefs(); return; } if (run('capture.screen')) { e.preventDefault(); doScreenCapture(); return; } if (run('view.zoomIn')) { e.preventDefault(); setZoom(z => Math.min(12, z * 1.15)); return; } if (run('view.zoomOut')) { e.preventDefault(); setZoom(z => Math.max(0.08, z / 1.15)); return; } if (run('view.panHome')) { e.preventDefault(); setPan({ x: 0, y: 0 }); return; } if (run('view.toggleGrid')) { e.preventDefault(); setShowGrid(v => !v); return; } if (run('view.toggleMinimap')) { setShowMinimap(v => !v); return; } if (run('view.focus')) { if (focusedImageId) setFocusedImageId(null); else if (selectedNodeIds.length > 0) setFocusedImageId(selectedNodeIds[0]); return; } if (run('view.valueMirror') && 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; } if (run('view.zoomLens')) { setIsZoomLensActive(true); return; } if (run('view.fitAll')) { e.preventDefault(); fitAll(); return; } if (run('view.zoom100')) { e.preventDefault(); setZoom(1); return; } if (run('selection.duplicate')) { 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; } if (run('selection.group')) { e.preventDefault(); 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; } if (run('selection.ungroup')) { e.preventDefault(); 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'); return; } if (run('selection.flipH') && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedH: !i.isFlippedH } : i)); return; } if (run('selection.flipV') && selectedNodeIds.length > 0) { setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isFlippedV: !i.isFlippedV } : i)); return; } if (run('selection.bringFront')) { setImages(prev => [...prev.filter(i => !selectedNodeIds.includes(i.id)), ...prev.filter(i => selectedNodeIds.includes(i.id))]); return; } if (run('selection.sendBack')) { setImages(prev => [...prev.filter(i => selectedNodeIds.includes(i.id)), ...prev.filter(i => !selectedNodeIds.includes(i.id))]); return; } if (run('selection.desaturate')) { if (selectedNodeIds.length > 0) setImages(prev => prev.map(i => selectedNodeIds.includes(i.id) ? { ...i, isDesaturated: !i.isDesaturated } : i)); return; } if (run('selection.delete')) { if (selectedNodeIds.length > 0) { setImages(prev => prev.filter(i => !selectedNodeIds.includes(i.id))); setTextNotes(prev => prev.filter(n => !selectedNodeIds.includes(n.id))); setSelectedNodeIds([]); } return; } if (run('selection.selectAll')) { e.preventDefault(); setSelectedNodeIds(images.map(i => i.id)); return; } if (run('panel.browser')) { const now = Date.now(); if (now - lastBPress.current < 300) { setIsWhisperBrowser(true); setIsBrowserOpen(true); } else { setIsBrowserOpen(p => !p); setIsWhisperBrowser(false); } lastBPress.current = now; return; } if (run('panel.library')) { setIsLibraryOpen(p => !p); return; } if (run('panel.settings')) { e.preventDefault(); setIsSettingsOpen(p => !p); return; } if (run('panel.closeAll')) { e.preventDefault(); if (isAnnotationMode) { setIsAnnotationMode(false); return; } setIsBrowserOpen(false); setIsLibraryOpen(false); setIsSettingsOpen(false); setSelectedNodeIds([]); setFocusedImageId(null); return; } if (run('tool.annotate')) { e.preventDefault(); setIsAnnotationMode(p => !p); return; } if (run('tool.annotationPen')) { setIsAnnotationMode(true); setIsEraser(false); setIsHighlighter(false); return; } if (run('tool.annotationHighlighter')) { setIsAnnotationMode(true); setIsEraser(false); setIsHighlighter(true); return; } if (run('tool.annotationEraser')) { setIsAnnotationMode(true); setIsEraser(true); setIsHighlighter(false); return; } if (run('tool.annotationClear')) { e.preventDefault(); if (annotations.length && confirm(`Clear all ${annotations.length} annotation strokes?`)) setAnnotations([]); return; } if (run('tool.globalDesaturate')) { setGlobalDesaturate(p => !p); return; } if (run('window.alwaysOnTop')) { setIsAlwaysOnTop(p => !p); return; } if (run('window.clickThrough')) { setIsClickThrough(p => !p); return; } 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; } }; const onKeyUp = (e: KeyboardEvent) => { if (matchesShortcut('view.zoomLens', e)) setIsZoomLensActive(false); }; window.addEventListener('keydown', onKeyDown); window.addEventListener('keyup', onKeyUp); return () => { window.removeEventListener('keydown', onKeyDown); window.removeEventListener('keyup', onKeyUp); }; }, [selectedNodeIds, images, textNotes, annotations, palettes, pan, zoom, focusedImageId, undo, redo, isAnnotationMode, isCapturing, activeProjectId, boardTitle]);
60
  const handleCtxMenu = (e: React.MouseEvent) => { if (!(e.target instanceof HTMLInputElement) && !(e.target instanceof HTMLTextAreaElement) && !(e.target as HTMLElement).isContentEditable) e.preventDefault(); };
61
  if (currentScreen === 'hub') return <div className="w-screen h-screen relative overflow-hidden bg-[var(--panel-bg)]" onContextMenu={handleCtxMenu}><StarterHub /><Suspense fallback={null}>{isSettingsOpen && <SettingsPanel />}</Suspense>{toastMsg && <Toast msg={toastMsg} />}</div>;
62
+ return <div className="w-screen h-screen relative overflow-hidden bg-[var(--canvas-bg)]" onContextMenu={handleCtxMenu}><Canvas />{(isBrowserOpen || isLibraryOpen || isSettingsOpen) && <div className="absolute inset-0 bg-black/40 pointer-events-none z-30 transition-opacity" />}<Toolbar /><Suspense fallback={null}>{isBrowserOpen && <BrowserPanel />}{isLibraryOpen && <LibraryPanel />}{isSettingsOpen && <SettingsPanel />}</Suspense><AnnotationWidget />{showMinimap && <Minimap />}<ContextMenu />{toastMsg && <Toast msg={toastMsg} />}{globalDesaturate && <div className="absolute top-16 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[var(--accent-amber)]/20 border border-[var(--accent-amber)] text-[var(--ui-primary)] px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Desaturation Active (Shift+D)</div></div>}{focusedImageId && <div className="absolute top-4 left-1/2 -translate-x-1/2 z-50 pointer-events-none"><div className="bg-[var(--accent)]/20 border border-[var(--accent)] text-[var(--ui-primary)] px-3 py-1 rounded text-xs shadow-lg backdrop-blur">Focus Mode — ←→ cycle, F/Esc exit</div></div>}{isAlwaysOnTop && <div className="absolute top-4 right-4 z-50 pointer-events-none"><div className="bg-[var(--accent)]/15 border border-[var(--accent)]/40 text-[var(--ui-primary)] px-2.5 py-1 rounded text-[10px] shadow-lg backdrop-blur">Always on top</div></div>}{isCapturing && <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-[100] pointer-events-none"><div className="bg-[var(--panel-surface)] border border-[var(--panel-border)] rounded-xl px-6 py-4 shadow-2xl text-[var(--ui-primary)] text-sm">Capturing...</div></div>}</div>;
63
  };
64
  function Toast({ msg }: { msg: string }) { return <div className="absolute top-16 left-1/2 -translate-x-1/2 z-[120] pointer-events-none"><div className="bg-[var(--panel-bg)]/90 border border-white/10 text-[var(--ui-primary)] font-medium px-4 py-2 rounded-full shadow-2xl backdrop-blur text-xs max-w-[520px] truncate">{msg}</div></div>; }
65
  export default function App() { return <AppProvider><MainApp /></AppProvider>; }