asdf98 commited on
Commit
caa556a
·
verified ·
1 Parent(s): 26b030d

feat: SettingsPanel storage tab - add .refs export/import with toast, data size stats, storage mode info

Browse files
Files changed (1) hide show
  1. src/components/SettingsPanel.tsx +146 -57
src/components/SettingsPanel.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, Grid3X3, Palette, Navigation, MousePointer2, FolderOpen, FileDown, Check } from 'lucide-react';
2
  import { useAppStore, type ThemeId } from '../store';
3
  import { useState, useEffect } from 'react';
4
  import { invoke } from '@tauri-apps/api/core';
@@ -13,32 +13,115 @@ const THEMES: { id: ThemeId; name: string; description: string; colors: string[]
13
  ];
14
 
15
  export const SettingsPanel = () => {
16
- const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid, theme, setTheme, images, textNotes, annotations, palettes, zoom, pan, boardTitle } = useAppStore();
17
  const [activeTab, setActiveTab] = useState<Tab>('general');
18
  const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
19
- const [exportMsg, setExportMsg] = useState('');
 
20
 
21
  useEffect(() => { if (isSettingsOpen) invoke<ShieldReport>('shield_get_report').then(setShieldReport).catch(() => {}); }, [isSettingsOpen]);
 
22
 
23
- // Export board as JSON file download (works without any Rust command)
24
- const handleExportBoard = () => {
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  try {
26
- const state = { title: boardTitle, images, textNotes, annotations, palettes, zoom, pan };
27
- const json = JSON.stringify(state, null, 2);
28
- const blob = new Blob([json], { type: 'application/json' });
29
  const url = URL.createObjectURL(blob);
 
30
  const a = document.createElement('a');
31
- a.href = url;
32
- a.download = `${(boardTitle || 'board').replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}.refstudio.json`;
33
- document.body.appendChild(a);
34
- a.click();
35
- document.body.removeChild(a);
36
- URL.revokeObjectURL(url);
37
- setExportMsg('✓ Board exported');
38
- setTimeout(() => setExportMsg(''), 3000);
39
  } catch (e) {
40
- setExportMsg(`Export failed: ${e}`);
41
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  };
43
 
44
  const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
@@ -65,29 +148,13 @@ export const SettingsPanel = () => {
65
 
66
  {activeTab === 'general' && <>
67
  <Header title="General" subtitle="Core behavior for the canvas, window, and workspace." />
68
- <Section title="Window">
69
- <ToggleRow label="Always on Top" description="Keep above other apps while drawing." active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />
70
- {isAlwaysOnTop && <RangeRow label="Overlay opacity" value={bgOpacity} min={10} max={100} suffix="%" onChange={setBgOpacity} />}
71
- </Section>
72
- <Section title="Canvas">
73
- <ToggleRow icon={<Grid3X3 size={16} />} label="Grid" description="Show dotted canvas grid." active={showGrid} onToggle={() => setShowGrid(!showGrid)} />
74
- <ToggleRow icon={<Navigation size={16} />} label="Minimap" description="Compact board overview in bottom-right." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} />
75
- </Section>
76
  </>}
77
 
78
  {activeTab === 'appearance' && <>
79
  <Header title="Appearance" subtitle="Choose a workspace theme for long sessions." />
80
- <Section title="Theme">
81
- <div className="p-4 grid gap-3">
82
- {THEMES.map(t => (
83
- <button key={t.id} onClick={() => setTheme(t.id)} className={`p-4 rounded-xl border text-left transition-all ${theme === t.id ? 'border-[#0A84FF] bg-[#0A84FF]/8' : 'border-white/10 bg-black/20 hover:border-white/20'}`}>
84
- <div className="flex items-center justify-between mb-3"><div className="flex gap-2">{t.colors.map(c => <div key={c} className="w-8 h-8 rounded-lg border border-black/30" style={{ backgroundColor: c }} />)}</div>{theme === t.id && <div className="w-6 h-6 rounded-full bg-[#0A84FF] flex items-center justify-center"><Check size={14} /></div>}</div>
85
- <div className="text-[14px] font-semibold">{t.name}</div>
86
- <div className="text-[12px] text-[#808080] mt-1">{t.description}</div>
87
- </button>
88
- ))}
89
- </div>
90
- </Section>
91
  </>}
92
 
93
  {activeTab === 'shortcuts' && <>
@@ -99,32 +166,50 @@ export const SettingsPanel = () => {
99
 
100
  {activeTab === 'privacy' && <>
101
  <Header title="Privacy" subtitle="Local-first browsing and capture." />
102
- <Section title="Muse Shield">
103
- <div className="p-4 grid grid-cols-2 gap-3">
104
- <Stat value={shieldReport.engine_rules.toLocaleString()} label="Filter rules" />
105
- <Stat value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" />
106
- <Stat value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" />
107
- <Stat value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" />
108
- </div>
109
- <div className="px-4 pb-4"><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></div>
110
- </Section>
111
- <Section title="Data"><InfoRow label="Privacy model" value="Local-first" description="Boards, library, and history stay on your machine. Nothing leaves your device." /></Section>
112
  </>}
113
 
114
  {activeTab === 'storage' && <>
115
- <Header title="Storage & Export" subtitle="Export boards and manage local data." />
116
- <Section title="Export Current Board">
 
117
  <div className="p-4 space-y-3">
118
- <p className="text-[12px] text-[#808080]">Download the current board as a JSON file. Contains all image URLs, positions, annotations, and settings. Images stored as external URLs (not embedded).</p>
119
- <button onClick={handleExportBoard} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><FileDown size={14} /> Export Board (.json)</button>
120
- {exportMsg && <div className="text-xs text-[#FFD60A] bg-[#FFD60A]/10 border border-[#FFD60A]/20 rounded-lg px-3 py-2">{exportMsg}</div>}
121
  </div>
122
  </Section>
123
- <Section title="Info">
124
- <InfoRow label="Images on board" value={String(images.length)} description="Reference images currently on canvas." />
125
- <InfoRow label="Annotations" value={String(annotations.length)} description="Freehand drawing strokes." />
126
- <InfoRow label="Text notes" value={String(textNotes.length)} description="Rich text note nodes." />
127
- <InfoRow label="Auto-save" value="800ms" description="Debounced write after every edit." />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  </Section>
129
  </>}
130
 
@@ -134,11 +219,15 @@ export const SettingsPanel = () => {
134
  <h1 className="text-2xl font-semibold">Refstudio</h1>
135
  <p className="text-[#808080]">1.0.0-alpha</p>
136
  <p className="max-w-sm text-[#808080] text-sm leading-relaxed">A local-first artist reference board with embedded browser capture, asset library, annotations, and privacy-respecting storage.</p>
 
137
  </div>
138
  </>}
139
  </div>
140
  </main>
141
  </div>
 
 
 
142
  </div>
143
  );
144
  };
@@ -147,6 +236,6 @@ function Header({ title, subtitle }: { title: string; subtitle: string }) { retu
147
  function Section({ title, children }: { title: string; children: React.ReactNode }) { return <section><h3 className="text-[12px] font-semibold uppercase tracking-wider text-[#808080] mb-3">{title}</h3><div className="rounded-2xl border border-white/8 bg-black/20 overflow-hidden divide-y divide-white/5">{children}</div></section>; }
148
  function ToggleRow({ label, description, active, onToggle, icon }: { label: string; description: string; active: boolean; onToggle: () => void; icon?: React.ReactNode }) { return <button type="button" onClick={onToggle} className="w-full px-4 py-3 flex items-center justify-between gap-4 text-left hover:bg-white/[0.03] transition-colors"><div className="flex items-start gap-3 min-w-0 flex-1">{icon && <div className="mt-0.5 text-[#0A84FF] shrink-0">{icon}</div>}<div><div className="text-[14px] font-medium">{label}</div><div className="text-[12px] text-[#808080] mt-0.5">{description}</div></div></div><div className={`relative shrink-0 w-[44px] h-[24px] rounded-full transition-colors ${active ? 'bg-[#0A84FF]' : 'bg-[#3A3A3E]'}`}><div className={`absolute top-[2px] w-5 h-5 rounded-full bg-white shadow-md transition-all ${active ? 'left-[22px]' : 'left-[2px]'}`} /></div></button>; }
149
  function RangeRow({ label, value, min, max, suffix, onChange }: { label: string; value: number; min: number; max: number; suffix: string; onChange: (v: number) => void }) { return <div className="px-4 py-3 flex items-center justify-between gap-4"><span className="text-[13px]">{label}</span><div className="flex items-center gap-3"><span className="text-xs text-[#808080] w-8 text-right">{value}{suffix}</span><input type="range" min={min} max={max} value={value} onChange={e => onChange(Number(e.target.value))} className="w-28 accent-[#0A84FF]" /></div></div>; }
150
- function InfoRow({ label, value, description }: { label: string; value: string; description: string }) { return <div className="px-4 py-3 flex items-center justify-between gap-4"><div><div className="text-[13px] font-medium">{label}</div><div className="text-[11px] text-[#808080] mt-0.5">{description}</div></div><div className="text-[11px] text-[#808080] bg-white/5 border border-white/8 rounded-lg px-2 py-1">{value}</div></div>; }
151
  function SK({ combo, label }: { combo: string; label: string }) { return <div className="px-4 py-2 flex items-center justify-between gap-4"><span className="text-[13px]">{label}</span><kbd className="bg-black/30 border border-white/10 px-2 py-0.5 rounded-lg text-[11px] text-[#808080] font-mono">{combo}</kbd></div>; }
152
  function Stat({ value, label }: { value: string; label: string }) { return <div className="bg-black/20 rounded-xl p-3 text-center border border-white/5"><div className="text-lg font-bold text-[#0A84FF]">{value}</div><div className="text-[10px] text-[#808080] mt-1">{label}</div></div>; }
 
1
+ import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, Grid3X3, Navigation, FileDown, FileUp, FileArchive, Check, Trash2 } from 'lucide-react';
2
  import { useAppStore, type ThemeId } from '../store';
3
  import { useState, useEffect } from 'react';
4
  import { invoke } from '@tauri-apps/api/core';
 
13
  ];
14
 
15
  export const SettingsPanel = () => {
16
+ const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid, theme, setTheme, images, textNotes, annotations, palettes, zoom, pan, boardTitle, setImages, setTextNotes, setAnnotations, setPalettes, setZoom, setPan, setBoardTitle, setCurrentScreen } = useAppStore();
17
  const [activeTab, setActiveTab] = useState<Tab>('general');
18
  const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
19
+ const [toastMsg, setToastMsg] = useState('');
20
+ const [isExporting, setIsExporting] = useState(false);
21
 
22
  useEffect(() => { if (isSettingsOpen) invoke<ShieldReport>('shield_get_report').then(setShieldReport).catch(() => {}); }, [isSettingsOpen]);
23
+ useEffect(() => { if (toastMsg) { const t = setTimeout(() => setToastMsg(''), 4000); return () => clearTimeout(t); } }, [toastMsg]);
24
 
25
+ // Estimate data size (images as base64 take ~1.37x original size)
26
+ const dataSize = (() => {
27
+ let bytes = 0;
28
+ images.forEach(img => { bytes += (img.url?.length || 0); });
29
+ textNotes.forEach(n => { bytes += (n.text?.length || 0) * 2; });
30
+ annotations.forEach(a => { bytes += a.points.length * 16; });
31
+ if (bytes > 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
32
+ if (bytes > 1024) return `${(bytes / 1024).toFixed(0)} KB`;
33
+ return `${bytes} bytes`;
34
+ })();
35
+
36
+ // Export as .refs (portable ZIP with all assets embedded)
37
+ const handleExportRefs = async () => {
38
+ if (isExporting) return;
39
+ setIsExporting(true);
40
  try {
41
+ const state = JSON.stringify({ title: boardTitle, images, textNotes, annotations, palettes, zoom, pan });
42
+ const bytes = await invoke<number[]>('refs_export', { stateJson: state });
43
+ const blob = new Blob([new Uint8Array(bytes)], { type: 'application/octet-stream' });
44
  const url = URL.createObjectURL(blob);
45
+ const filename = `${(boardTitle || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}.refs`;
46
  const a = document.createElement('a');
47
+ a.href = url; a.download = filename;
48
+ document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
49
+ setToastMsg(`✓ Exported "${filename}" — portable board with all images embedded`);
 
 
 
 
 
50
  } catch (e) {
51
+ setToastMsg(`Export failed: ${e}`);
52
  }
53
+ setIsExporting(false);
54
+ };
55
+
56
+ // Export as JSON (lightweight, URLs only)
57
+ const handleExportJson = () => {
58
+ try {
59
+ const state = { title: boardTitle, images, textNotes, annotations, palettes, zoom, pan };
60
+ const json = JSON.stringify(state, null, 2);
61
+ const blob = new Blob([json], { type: 'application/json' });
62
+ const url = URL.createObjectURL(blob);
63
+ const filename = `${(boardTitle || 'board').replace(/[^a-zA-Z0-9 ]/g, '').trim().replace(/\s+/g, '-').toLowerCase() || 'board'}.json`;
64
+ const a = document.createElement('a'); a.href = url; a.download = filename;
65
+ document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
66
+ setToastMsg(`✓ Exported "${filename}" — lightweight JSON (images as URLs/base64)`);
67
+ } catch (e) { setToastMsg(`✗ Export failed: ${e}`); }
68
+ };
69
+
70
+ // Import .refs file
71
+ const handleImportRefs = () => {
72
+ const input = document.createElement('input');
73
+ input.type = 'file'; input.accept = '.refs';
74
+ input.onchange = async (e) => {
75
+ const file = (e.target as HTMLInputElement).files?.[0];
76
+ if (!file) return;
77
+ try {
78
+ const buffer = await file.arrayBuffer();
79
+ const bytes = Array.from(new Uint8Array(buffer));
80
+ const stateJson = await invoke<string>('refs_import', { data: bytes });
81
+ const state = JSON.parse(stateJson);
82
+ if (state.images) setImages(state.images);
83
+ if (state.textNotes) setTextNotes(state.textNotes);
84
+ if (state.annotations) setAnnotations(state.annotations);
85
+ if (state.palettes) setPalettes(state.palettes);
86
+ if (state.zoom) setZoom(state.zoom);
87
+ if (state.pan) setPan(state.pan);
88
+ if (state.title) setBoardTitle(state.title);
89
+ setCurrentScreen('board');
90
+ setToastMsg(`✓ Imported "${file.name}" — ${state.images?.length || 0} images loaded`);
91
+ } catch (e) { setToastMsg(`✗ Import failed: ${e}`); }
92
+ };
93
+ input.click();
94
+ };
95
+
96
+ // Import JSON
97
+ const handleImportJson = () => {
98
+ const input = document.createElement('input');
99
+ input.type = 'file'; input.accept = '.json';
100
+ input.onchange = async (e) => {
101
+ const file = (e.target as HTMLInputElement).files?.[0];
102
+ if (!file) return;
103
+ try {
104
+ const text = await file.text();
105
+ const state = JSON.parse(text);
106
+ if (state.images) setImages(state.images);
107
+ if (state.textNotes) setTextNotes(state.textNotes);
108
+ if (state.annotations) setAnnotations(state.annotations);
109
+ if (state.palettes) setPalettes(state.palettes);
110
+ if (state.zoom) setZoom(state.zoom);
111
+ if (state.pan) setPan(state.pan);
112
+ if (state.title) setBoardTitle(state.title);
113
+ setCurrentScreen('board');
114
+ setToastMsg(`✓ Imported "${file.name}"`);
115
+ } catch (e) { setToastMsg(`✗ Import failed: ${e}`); }
116
+ };
117
+ input.click();
118
+ };
119
+
120
+ // Clear board
121
+ const handleClearBoard = () => {
122
+ if (!confirm('Clear all images, notes, and annotations from the current board? This cannot be undone.')) return;
123
+ setImages([]); setTextNotes([]); setAnnotations([]); setPalettes([]);
124
+ setToastMsg('Board cleared');
125
  };
126
 
127
  const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [
 
148
 
149
  {activeTab === 'general' && <>
150
  <Header title="General" subtitle="Core behavior for the canvas, window, and workspace." />
151
+ <Section title="Window"><ToggleRow label="Always on Top" description="Keep above other apps while drawing." active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />{isAlwaysOnTop && <RangeRow label="Overlay opacity" value={bgOpacity} min={10} max={100} suffix="%" onChange={setBgOpacity} />}</Section>
152
+ <Section title="Canvas"><ToggleRow icon={<Grid3X3 size={16} />} label="Grid" description="Show dotted canvas grid." active={showGrid} onToggle={() => setShowGrid(!showGrid)} /><ToggleRow icon={<Navigation size={16} />} label="Minimap" description="Compact board overview in bottom-right." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} /></Section>
 
 
 
 
 
 
153
  </>}
154
 
155
  {activeTab === 'appearance' && <>
156
  <Header title="Appearance" subtitle="Choose a workspace theme for long sessions." />
157
+ <Section title="Theme"><div className="p-4 grid gap-3">{THEMES.map(t => <button key={t.id} onClick={() => setTheme(t.id)} className={`p-4 rounded-xl border text-left transition-all ${theme === t.id ? 'border-[#0A84FF] bg-[#0A84FF]/8' : 'border-white/10 bg-black/20 hover:border-white/20'}`}><div className="flex items-center justify-between mb-3"><div className="flex gap-2">{t.colors.map(c => <div key={c} className="w-8 h-8 rounded-lg border border-black/30" style={{ backgroundColor: c }} />)}</div>{theme === t.id && <div className="w-6 h-6 rounded-full bg-[#0A84FF] flex items-center justify-center"><Check size={14} /></div>}</div><div className="text-[14px] font-semibold">{t.name}</div><div className="text-[12px] text-[#808080] mt-1">{t.description}</div></button>)}</div></Section>
 
 
 
 
 
 
 
 
 
 
158
  </>}
159
 
160
  {activeTab === 'shortcuts' && <>
 
166
 
167
  {activeTab === 'privacy' && <>
168
  <Header title="Privacy" subtitle="Local-first browsing and capture." />
169
+ <Section title="Muse Shield"><div className="p-4 grid grid-cols-2 gap-3"><Stat value={shieldReport.engine_rules.toLocaleString()} label="Filter rules" /><Stat value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" /><Stat value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" /><Stat value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" /></div><div className="px-4 pb-4"><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></div></Section>
170
+ <Section title="Data"><InfoRow label="Privacy model" value="Local-first" description="Everything stays on your machine. No telemetry." /></Section>
 
 
 
 
 
 
 
 
171
  </>}
172
 
173
  {activeTab === 'storage' && <>
174
+ <Header title="Storage & Export" subtitle="Export, import, and manage board data." />
175
+
176
+ <Section title="Export as .refs (Portable Archive)">
177
  <div className="p-4 space-y-3">
178
+ <p className="text-[12px] text-[#808080] leading-relaxed">Creates a standalone <code className="text-white/70">.refs</code> ZIP archive containing all images, metadata, annotations, and layout. Fully portable can be opened on any machine without network access.</p>
179
+ <button onClick={handleExportRefs} disabled={isExporting} className={`w-full py-2.5 bg-[#0A84FF]/10 hover:bg-[#0A84FF]/20 border border-[#0A84FF]/30 rounded-xl text-sm flex items-center justify-center gap-2 text-[#0A84FF] font-medium ${isExporting ? 'opacity-50' : ''}`}><FileArchive size={14} />{isExporting ? 'Exporting...' : 'Export Board as .refs'}</button>
 
180
  </div>
181
  </Section>
182
+
183
+ <Section title="Export as JSON (Lightweight)">
184
+ <div className="p-4 space-y-3">
185
+ <p className="text-[12px] text-[#808080] leading-relaxed">Exports board as a JSON file. Images are stored as base64 data URLs or external URLs depending on how they were captured. Smaller file, requires embedded data to be preserved.</p>
186
+ <button onClick={handleExportJson} className="w-full py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><FileDown size={14} /> Export Board as .json</button>
187
+ </div>
188
+ </Section>
189
+
190
+ <Section title="Import">
191
+ <div className="p-4 space-y-3">
192
+ <p className="text-[12px] text-[#808080]">Load a previously exported board file. Replaces current board content.</p>
193
+ <div className="flex gap-2">
194
+ <button onClick={handleImportRefs} className="flex-1 py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><FileUp size={14} /> Import .refs</button>
195
+ <button onClick={handleImportJson} className="flex-1 py-2.5 bg-white/5 hover:bg-white/10 rounded-xl border border-white/10 text-sm flex items-center justify-center gap-2"><FileUp size={14} /> Import .json</button>
196
+ </div>
197
+ </div>
198
+ </Section>
199
+
200
+ <Section title="Board Statistics">
201
+ <InfoRow label="Images" value={String(images.length)} description="Reference images on canvas" />
202
+ <InfoRow label="Annotations" value={String(annotations.length)} description="Freehand drawing strokes" />
203
+ <InfoRow label="Text Notes" value={String(textNotes.length)} description="Rich text note nodes" />
204
+ <InfoRow label="Estimated Size" value={dataSize} description="Approximate in-memory data footprint" />
205
+ <InfoRow label="Storage Mode" value="Embedded" description="Images stored as base64 inside project — fully portable, no broken links" />
206
+ <InfoRow label="Auto-save" value="Every 800ms" description="Debounced write after each canvas change" />
207
+ </Section>
208
+
209
+ <Section title="Danger Zone">
210
+ <div className="p-4 space-y-3">
211
+ <button onClick={handleClearBoard} className="w-full py-2.5 bg-[#FF453A]/10 hover:bg-[#FF453A]/20 border border-[#FF453A]/30 rounded-xl text-sm flex items-center justify-center gap-2 text-[#FF453A]"><Trash2 size={14} /> Clear Current Board</button>
212
+ </div>
213
  </Section>
214
  </>}
215
 
 
219
  <h1 className="text-2xl font-semibold">Refstudio</h1>
220
  <p className="text-[#808080]">1.0.0-alpha</p>
221
  <p className="max-w-sm text-[#808080] text-sm leading-relaxed">A local-first artist reference board with embedded browser capture, asset library, annotations, and privacy-respecting storage.</p>
222
+ <div className="text-[11px] text-[#606060] mt-4">Built with Tauri v2 + Rust + React<br/>{shieldReport.engine_rules.toLocaleString()} adblock filter rules loaded</div>
223
  </div>
224
  </>}
225
  </div>
226
  </main>
227
  </div>
228
+
229
+ {/* Toast notification */}
230
+ {toastMsg && <div className="absolute bottom-4 left-4 right-4 z-50 pointer-events-none flex justify-center"><div className="bg-[#2A2A2E] border border-white/10 text-white text-[12px] font-medium px-4 py-2.5 rounded-full shadow-2xl backdrop-blur pointer-events-auto max-w-[400px] truncate">{toastMsg}</div></div>}
231
  </div>
232
  );
233
  };
 
236
  function Section({ title, children }: { title: string; children: React.ReactNode }) { return <section><h3 className="text-[12px] font-semibold uppercase tracking-wider text-[#808080] mb-3">{title}</h3><div className="rounded-2xl border border-white/8 bg-black/20 overflow-hidden divide-y divide-white/5">{children}</div></section>; }
237
  function ToggleRow({ label, description, active, onToggle, icon }: { label: string; description: string; active: boolean; onToggle: () => void; icon?: React.ReactNode }) { return <button type="button" onClick={onToggle} className="w-full px-4 py-3 flex items-center justify-between gap-4 text-left hover:bg-white/[0.03] transition-colors"><div className="flex items-start gap-3 min-w-0 flex-1">{icon && <div className="mt-0.5 text-[#0A84FF] shrink-0">{icon}</div>}<div><div className="text-[14px] font-medium">{label}</div><div className="text-[12px] text-[#808080] mt-0.5">{description}</div></div></div><div className={`relative shrink-0 w-[44px] h-[24px] rounded-full transition-colors ${active ? 'bg-[#0A84FF]' : 'bg-[#3A3A3E]'}`}><div className={`absolute top-[2px] w-5 h-5 rounded-full bg-white shadow-md transition-all ${active ? 'left-[22px]' : 'left-[2px]'}`} /></div></button>; }
238
  function RangeRow({ label, value, min, max, suffix, onChange }: { label: string; value: number; min: number; max: number; suffix: string; onChange: (v: number) => void }) { return <div className="px-4 py-3 flex items-center justify-between gap-4"><span className="text-[13px]">{label}</span><div className="flex items-center gap-3"><span className="text-xs text-[#808080] w-8 text-right">{value}{suffix}</span><input type="range" min={min} max={max} value={value} onChange={e => onChange(Number(e.target.value))} className="w-28 accent-[#0A84FF]" /></div></div>; }
239
+ function InfoRow({ label, value, description }: { label: string; value: string; description: string }) { return <div className="px-4 py-3 flex items-center justify-between gap-4"><div><div className="text-[13px] font-medium">{label}</div><div className="text-[11px] text-[#808080] mt-0.5">{description}</div></div><div className="text-[11px] text-[#808080] bg-white/5 border border-white/8 rounded-lg px-2 py-1 shrink-0">{value}</div></div>; }
240
  function SK({ combo, label }: { combo: string; label: string }) { return <div className="px-4 py-2 flex items-center justify-between gap-4"><span className="text-[13px]">{label}</span><kbd className="bg-black/30 border border-white/10 px-2 py-0.5 rounded-lg text-[11px] text-[#808080] font-mono">{combo}</kbd></div>; }
241
  function Stat({ value, label }: { value: string; label: string }) { return <div className="bg-black/20 rounded-xl p-3 text-center border border-white/5"><div className="text-lg font-bold text-[#0A84FF]">{value}</div><div className="text-[10px] text-[#808080] mt-1">{label}</div></div>; }