feat: add Export .refs to toolbar dropdown; implement full CSS variable theming with 3 themes and Settings controls
Browse files- src/components/Toolbar.tsx +37 -18
src/components/Toolbar.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
import { Pin, Paintbrush2, Droplet, Settings, Save, Upload, Minus, Square, X, MousePointer2, Globe, FolderOpen, ChevronDown, ImageDown, LogOut, FilePlus, GripVertical } from 'lucide-react';
|
| 2 |
import { useAppStore } from '../store';
|
| 3 |
import { useRef, useState } from 'react';
|
| 4 |
import { invoke } from '@tauri-apps/api/core';
|
|
@@ -20,6 +20,17 @@ export const Toolbar = () => {
|
|
| 20 |
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
| 21 |
};
|
| 22 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
const handleQuickSave = () => { if (!activeProjectId) return; invoke('project_save', { id: activeProjectId, state: JSON.stringify({ textNotes, images, annotations, palettes, zoom, pan, title: boardTitle }), title: boardTitle }).catch(() => {}); };
|
| 24 |
const handleLoad = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const d = JSON.parse(ev.target?.result as string); if (d.images) setImages(d.images); if (d.textNotes) setTextNotes(d.textNotes); if (d.annotations) setAnnotations(d.annotations); if (d.palettes) setPalettes(d.palettes); if (d.zoom) setZoom(d.zoom); if (d.pan) setPan(d.pan); if (d.title) setBoardTitle(d.title); } catch {} }; reader.readAsText(file); e.target.value = ''; };
|
| 25 |
const handleCloseProject = () => { setIsBoardMenuOpen(false); handleQuickSave(); setCurrentScreen('hub'); };
|
|
@@ -27,25 +38,33 @@ export const Toolbar = () => {
|
|
| 27 |
const handleRename = (newTitle: string) => { const title = newTitle.trim() || 'Untitled Board'; setBoardTitle(title); if (activeProjectId) invoke('project_rename', { id: activeProjectId, title }).catch(() => {}); };
|
| 28 |
|
| 29 |
return <div className={`absolute top-0 w-full h-16 pointer-events-none group/toolbar ${isBrowserOpen || isSettingsOpen ? 'z-20' : 'z-50'}`}>
|
| 30 |
-
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-[
|
| 31 |
<div className="flex items-center pl-2 pr-1 h-full cursor-grab active:cursor-grabbing text-white/20 hover:text-white/40 transition-colors" data-tauri-drag-region><GripVertical size={12} className="pointer-events-none" /></div>
|
| 32 |
-
<div className="flex items-center pr-2 border-r border-[
|
| 33 |
-
<div className="text-[
|
| 34 |
-
<button className="ml-1 text-[
|
| 35 |
-
{isBoardMenuOpen && <><div className="fixed inset-0 z-40" onClick={() => setIsBoardMenuOpen(false)} /><div className="absolute top-full left-0 mt-2 w-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
</div>
|
| 37 |
-
<div className="flex items-center px-1 h-full gap-1"><button className="w-8 h-8 flex items-center justify-center text-[
|
| 38 |
-
<div className="w-px h-[22px] bg-[
|
| 39 |
-
<div className="flex items-center px-2 h-full"><div className="text-[
|
| 40 |
-
<div className="w-px h-[22px] bg-[
|
| 41 |
-
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md transition-colors ${isBrowserOpen ? 'text-[
|
| 42 |
-
<div className="w-px h-[22px] bg-[
|
| 43 |
-
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md ${isAlwaysOnTop ? 'text-[
|
| 44 |
-
<div className="w-px h-[22px] bg-[
|
| 45 |
-
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md ${isAnnotationMode ? 'text-[#32D74B] bg-[#32D74B]/10' : 'text-[
|
| 46 |
-
<div className="w-px h-[22px] bg-[
|
| 47 |
-
<div className="flex items-center px-1 h-full"><button className="w-8 h-8 flex items-center justify-center text-[
|
| 48 |
-
<div className="flex items-center gap-1 pl-2 pr-2 h-full border-l border-[
|
| 49 |
</div>
|
| 50 |
</div>;
|
| 51 |
};
|
|
|
|
| 1 |
+
import { Pin, Paintbrush2, Droplet, Settings, Save, Upload, Minus, Square, X, MousePointer2, Globe, FolderOpen, ChevronDown, ImageDown, LogOut, FilePlus, GripVertical, FileArchive } from 'lucide-react';
|
| 2 |
import { useAppStore } from '../store';
|
| 3 |
import { useRef, useState } from 'react';
|
| 4 |
import { invoke } from '@tauri-apps/api/core';
|
|
|
|
| 20 |
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
| 21 |
};
|
| 22 |
|
| 23 |
+
const handleExportRefs = async () => {
|
| 24 |
+
const state = JSON.stringify({ textNotes, images, annotations, palettes, zoom, pan, title: boardTitle });
|
| 25 |
+
try {
|
| 26 |
+
const bytes = await invoke<number[]>('refs_export', { stateJson: state });
|
| 27 |
+
const blob = new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' });
|
| 28 |
+
const url = URL.createObjectURL(blob);
|
| 29 |
+
const a = document.createElement('a'); a.href = url; a.download = `${boardTitle.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase() || 'board'}.refs`;
|
| 30 |
+
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
| 31 |
+
} catch (e) { console.error('Export .refs failed:', e); }
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
const handleQuickSave = () => { if (!activeProjectId) return; invoke('project_save', { id: activeProjectId, state: JSON.stringify({ textNotes, images, annotations, palettes, zoom, pan, title: boardTitle }), title: boardTitle }).catch(() => {}); };
|
| 35 |
const handleLoad = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const d = JSON.parse(ev.target?.result as string); if (d.images) setImages(d.images); if (d.textNotes) setTextNotes(d.textNotes); if (d.annotations) setAnnotations(d.annotations); if (d.palettes) setPalettes(d.palettes); if (d.zoom) setZoom(d.zoom); if (d.pan) setPan(d.pan); if (d.title) setBoardTitle(d.title); } catch {} }; reader.readAsText(file); e.target.value = ''; };
|
| 36 |
const handleCloseProject = () => { setIsBoardMenuOpen(false); handleQuickSave(); setCurrentScreen('hub'); };
|
|
|
|
| 38 |
const handleRename = (newTitle: string) => { const title = newTitle.trim() || 'Untitled Board'; setBoardTitle(title); if (activeProjectId) invoke('project_rename', { id: activeProjectId, title }).catch(() => {}); };
|
| 39 |
|
| 40 |
return <div className={`absolute top-0 w-full h-16 pointer-events-none group/toolbar ${isBrowserOpen || isSettingsOpen ? 'z-20' : 'z-50'}`}>
|
| 41 |
+
<div className="absolute top-4 left-1/2 -translate-x-1/2 bg-[var(--panel-bg)]/95 backdrop-blur-md border border-[var(--panel-border)] shadow-2xl flex items-center h-[38px] rounded-lg pointer-events-auto transition-all duration-300 opacity-0 -translate-y-4 group-hover/toolbar:opacity-100 group-hover/toolbar:translate-y-0 focus-within:opacity-100 focus-within:translate-y-0 text-[var(--ui-secondary)] select-none">
|
| 42 |
<div className="flex items-center pl-2 pr-1 h-full cursor-grab active:cursor-grabbing text-white/20 hover:text-white/40 transition-colors" data-tauri-drag-region><GripVertical size={12} className="pointer-events-none" /></div>
|
| 43 |
+
<div className="flex items-center pr-2 border-r border-[var(--panel-border)] h-full relative">
|
| 44 |
+
<div className="text-[var(--ui-primary)] text-[13px] font-medium outline-none min-w-[30px] max-w-[160px] truncate" contentEditable suppressContentEditableWarning onBlur={(e) => handleRename(e.currentTarget.textContent || '')} onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); (e.target as HTMLElement).blur(); } }}>{boardTitle}</div>
|
| 45 |
+
<button className="ml-1 text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] rounded hover:bg-white/5 p-0.5" onClick={() => setIsBoardMenuOpen(!isBoardMenuOpen)}><ChevronDown size={14} /></button>
|
| 46 |
+
{isBoardMenuOpen && <><div className="fixed inset-0 z-40" onClick={() => setIsBoardMenuOpen(false)} /><div className="absolute top-full left-0 mt-2 w-56 bg-[var(--panel-bg)] border border-[var(--panel-border)] rounded-lg shadow-2xl z-50 py-1.5 pointer-events-auto">
|
| 47 |
+
<button onClick={handleNewBoard} className="w-full text-left px-3 py-2 text-[12px] text-[#C0C0C0] hover:bg-[var(--accent)] hover:text-white flex items-center gap-2.5"><FilePlus size={14} /> New Board</button>
|
| 48 |
+
<button onClick={() => { setIsBoardMenuOpen(false); fileInputRef.current?.click(); }} className="w-full text-left px-3 py-2 text-[12px] text-[#C0C0C0] hover:bg-[var(--accent)] hover:text-white flex items-center gap-2.5"><FolderOpen size={14} /> Open File</button>
|
| 49 |
+
<div className="h-px bg-[var(--panel-border)] my-1.5 mx-2" />
|
| 50 |
+
<button onClick={() => { setIsBoardMenuOpen(false); handleExportFile(); }} className="w-full text-left px-3 py-2 text-[12px] text-[#C0C0C0] hover:bg-[var(--accent)] hover:text-white flex items-center gap-2.5"><ImageDown size={14} /> Export as JSON</button>
|
| 51 |
+
<button onClick={() => { setIsBoardMenuOpen(false); handleExportRefs(); }} className="w-full text-left px-3 py-2 text-[12px] text-[#C0C0C0] hover:bg-[var(--accent)] hover:text-white flex items-center gap-2.5"><FileArchive size={14} /> Export as .refs</button>
|
| 52 |
+
<div className="h-px bg-[var(--panel-border)] my-1.5 mx-2" />
|
| 53 |
+
<button onClick={handleCloseProject} className="w-full text-left px-3 py-2 text-[12px] text-[#FF453A] hover:bg-[#FF453A] hover:text-white flex items-center gap-2.5"><LogOut size={14} /> Close Project</button>
|
| 54 |
+
</div></>}
|
| 55 |
</div>
|
| 56 |
+
<div className="flex items-center px-1 h-full gap-1"><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5 rounded-md" onClick={handleQuickSave} title="Save (Ctrl+S)"><Save size={14} /></button><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5 rounded-md" onClick={() => fileInputRef.current?.click()} title="Open File"><Upload size={14} /></button><input type="file" accept=".json,.refs" className="hidden" ref={fileInputRef} onChange={handleLoad} /></div>
|
| 57 |
+
<div className="w-px h-[22px] bg-[var(--panel-border)]" />
|
| 58 |
+
<div className="flex items-center px-2 h-full"><div className="text-[var(--ui-secondary)] text-[11px] w-10 text-center font-medium">{Math.round(zoom * 100)}%</div></div>
|
| 59 |
+
<div className="w-px h-[22px] bg-[var(--panel-border)]" />
|
| 60 |
+
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md transition-colors ${isBrowserOpen ? 'text-[var(--accent)] bg-[var(--accent)]/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setIsBrowserOpen(!isBrowserOpen)} title="Browser (B)"><Globe size={14} /></button><button className={`w-8 h-8 flex items-center justify-center rounded-md transition-colors ${isLibraryOpen ? 'text-[var(--accent)] bg-[var(--accent)]/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setIsLibraryOpen(!isLibraryOpen)} title="Library (L)"><FolderOpen size={14} /></button></div>
|
| 61 |
+
<div className="w-px h-[22px] bg-[var(--panel-border)]" />
|
| 62 |
+
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md ${isAlwaysOnTop ? 'text-[var(--ui-primary)] bg-white/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setIsAlwaysOnTop(!isAlwaysOnTop)} title="Always on Top (T)"><Pin size={14} className={isAlwaysOnTop ? '' : '-rotate-45'} /></button>{isAlwaysOnTop && <><button className={`w-8 h-8 flex items-center justify-center rounded-md ${isClickThrough ? 'text-[var(--accent)] bg-[var(--accent)]/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setIsClickThrough(!isClickThrough)}><MousePointer2 size={14} /></button><div className="flex items-center gap-2 pl-2"><span className="text-[10px] text-[var(--ui-secondary)] w-6">{bgOpacity}%</span><input type="range" min="10" max="100" value={bgOpacity} onChange={e => setBgOpacity(Number(e.target.value))} className="w-16 accent-[var(--accent)]" /></div></>}</div>
|
| 63 |
+
<div className="w-px h-[22px] bg-[var(--panel-border)]" />
|
| 64 |
+
<div className="flex items-center px-1 h-full gap-1"><button className={`w-8 h-8 flex items-center justify-center rounded-md ${isAnnotationMode ? 'text-[#32D74B] bg-[#32D74B]/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setIsAnnotationMode(!isAnnotationMode)} title="Annotate (A)"><Paintbrush2 size={14} /></button><button className={`w-8 h-8 flex items-center justify-center rounded-md ${globalDesaturate ? 'text-[#FFD60A] bg-[#FFD60A]/10' : 'text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5'}`} onClick={() => setGlobalDesaturate(!globalDesaturate)} title="Grayscale (D)"><Droplet size={14} /></button></div>
|
| 65 |
+
<div className="w-px h-[22px] bg-[var(--panel-border)]" />
|
| 66 |
+
<div className="flex items-center px-1 h-full"><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5 rounded-md" onClick={() => setIsSettingsOpen(!isSettingsOpen)} title="Settings (Ctrl+,)"><Settings size={14} /></button></div>
|
| 67 |
+
<div className="flex items-center gap-1 pl-2 pr-2 h-full border-l border-[var(--panel-border)]"><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5 rounded-md" onClick={() => appWindow.minimize()}><Minus size={15} /></button><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[var(--ui-primary)] hover:bg-white/5 rounded-md" onClick={() => appWindow.toggleMaximize()}><Square size={12} /></button><button className="w-8 h-8 flex items-center justify-center text-[var(--ui-secondary)] hover:text-[#FF453A] hover:bg-[#FF453A]/10 rounded-md" onClick={() => appWindow.close()}><X size={16} /></button></div>
|
| 68 |
</div>
|
| 69 |
</div>;
|
| 70 |
};
|