asdf98 commited on
Commit
d6e9b93
·
verified ·
1 Parent(s): d1203ac

fix: Settings canvas rows with proper grid/minimap toggles and non-compressed switches

Browse files
Files changed (1) hide show
  1. src/components/SettingsPanel.tsx +21 -21
src/components/SettingsPanel.tsx CHANGED
@@ -1,5 +1,5 @@
1
  import type React from 'react';
2
- import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, KeyRound, Lock, Unlock, Trash2, Plus, Navigation } from 'lucide-react';
3
  import { useAppStore } from '../store';
4
  import { useState, useEffect } from 'react';
5
  import { invoke } from '@tauri-apps/api/core';
@@ -9,7 +9,7 @@ type Tab = 'general' | 'appearance' | 'shortcuts' | 'privacy' | 'vault' | 'stora
9
  interface ShieldReport { blocked_requests: number; blocked_cosmetic: number; https_upgrades: number; engine_rules: number; }
10
 
11
  export const SettingsPanel = () => {
12
- const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap } = useAppStore();
13
  const [activeTab, setActiveTab] = useState<Tab>('general');
14
  const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
15
  const [vaultUnlocked, setVaultUnlocked] = useState(isVaultUnlocked());
@@ -37,7 +37,7 @@ export const SettingsPanel = () => {
37
  const saveCred = async () => { try { await saveCredential({ origin, username, password }); setOrigin(''); setUsername(''); setPassword(''); setVaultMsg('Credential saved'); await reloadCreds(); } catch (e) { setVaultMsg(`Save failed: ${String(e)}`); } };
38
 
39
  return (
40
- <div className={`absolute right-4 top-4 bottom-4 w-[min(640px,calc(100vw-32px))] z-[80] bg-[#1C1C1E]/98 shadow-2xl border border-[#3A3A3E] rounded-2xl flex flex-col transform transition-transform duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] overflow-hidden ${isSettingsOpen ? 'translate-x-0' : 'translate-x-[calc(100%+32px)]'}`}>
41
  <div className="h-14 border-b border-white/5 flex items-center justify-between px-5 bg-black/20 shrink-0">
42
  <h2 className="text-[#E0E0E0] font-medium flex items-center gap-2"><Settings size={16} className="text-[#808080]" /> Settings</h2>
43
  <button onClick={() => setIsSettingsOpen(false)} className="text-[#808080] hover:text-[#E0E0E0] p-1 rounded-md hover:bg-white/5"><X size={18} /></button>
@@ -47,24 +47,24 @@ export const SettingsPanel = () => {
47
  {tabs.map(tab => <button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`flex items-center gap-2.5 px-3 py-2 rounded-lg text-[13px] text-left transition-colors ${activeTab === tab.id ? 'bg-[#0A84FF] text-white font-medium' : 'text-[#808080] hover:text-[#E0E0E0] hover:bg-white/5'}`}>{tab.icon}{tab.label}</button>)}
48
  </div>
49
  <div className="flex-1 p-6 overflow-y-auto custom-scrollbar text-[#E0E0E0] text-sm min-w-0">
50
- {activeTab === 'general' && <>
51
- <Section title="Startup & Window">
52
- <Toggle label="Open last board on startup" desc="Load the last saved board automatically" active={true} onToggle={() => {}} />
53
- <Toggle label="Always on Top" desc="Keep Refstudio above other windows" active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />
54
- {isAlwaysOnTop && <div className="flex items-center justify-between mt-4 pl-4 border-l border-[#0A84FF]/40"><span>Opacity</span><input type="range" min="10" max="100" value={bgOpacity} onChange={e => setBgOpacity(Number(e.target.value))} className="w-28 accent-[#0A84FF]" /></div>}
55
- </Section>
56
- <Section title="Canvas">
57
- <Toggle label="Navigator / Minimap" desc="Show a compact board overview in the bottom-right corner. Off by default to keep the canvas clean." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} />
58
- <p className="text-[#808080] text-xs mt-2 flex items-center gap-2"><Navigation size={13} /> The navigator is padded inside the window and never appears outside the canvas.</p>
59
- </Section>
60
- </>}
61
 
62
- {activeTab === 'privacy' && <div className="space-y-6"><Section title="Muse Shield"><div className="grid grid-cols-2 gap-3"><StatBox value={shieldReport.engine_rules.toLocaleString()} label="Rules" /><StatBox value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" /><StatBox value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" /><StatBox value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" /></div><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="mt-4 w-full py-2 bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></Section><Section title="Cookies, Cache & History"><p className="text-[#909090] text-xs leading-relaxed">Cookies and web cache are handled by the system WebView profile and persist automatically. Muse tracks app-level navigation history separately for UI/search. Fine-grained cross-platform cookie/cache inspection is not exposed by stable Tauri APIs.</p><button onClick={() => invoke('history_clear').catch(() => {})} className="mt-4 px-3 py-2 rounded-lg bg-red-500/10 text-red-300 border border-red-500/20 text-xs">Clear app history</button></Section></div>}
63
 
64
- {activeTab === 'vault' && <div className="space-y-6"><Section title="Password Vault"><p className="text-[#909090] text-xs mb-4">Passwords are stored with the documented Tauri Stronghold frontend plugin. The vault is unlocked only with your master password.</p>{!vaultUnlocked ? <div className="flex gap-2"><input type="password" value={master} onChange={e => setMaster(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') unlock(); }} placeholder="Master password" className="flex-1 bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={unlock} className="px-3 py-2 bg-[#0A84FF] rounded-lg text-white flex items-center gap-2"><Unlock size={14} /> Unlock</button></div> : <button onClick={() => { lockVault(); setVaultUnlocked(false); setCreds([]); }} className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white flex items-center gap-2"><Lock size={14} /> Lock vault</button>}{vaultMsg && <div className="mt-3 text-xs text-[#FFD60A]">{vaultMsg}</div>}</Section>{vaultUnlocked && <Section title="Save credential"><div className="grid grid-cols-1 gap-2"><input value={origin} onChange={e => setOrigin(e.target.value)} placeholder="Site / origin (example.com)" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><input value={username} onChange={e => setUsername(e.target.value)} placeholder="Username / email" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={saveCred} className="px-3 py-2 bg-[#0A84FF] rounded-lg text-white flex items-center justify-center gap-2"><Plus size={14} /> Save credential</button></div><div className="mt-5 space-y-2">{creds.map(c => <div key={c.id} className="flex items-center justify-between bg-black/20 border border-white/5 rounded-lg p-3"><div><div className="text-white text-sm">{c.username}</div><div className="text-[#808080] text-xs">{c.origin}</div></div><button onClick={() => deleteCredential(c.id).then(reloadCreds)} className="text-red-300 hover:bg-red-500/10 rounded p-1"><Trash2 size={14} /></button></div>)}</div></Section>}</div>}
65
- {activeTab === 'shortcuts' && <Section title="Keyboard Shortcuts">{['B Browser','L Library','A Annotate','D Desaturate','T Always on Top','Ctrl+Z Undo','Ctrl+Shift+Z Redo','Ctrl+0 Fit view','Esc Close panels'].map(x => <div key={x} className="flex justify-between py-2 border-b border-white/5"><span>{x.split(' ').slice(1).join(' ')}</span><kbd className="bg-black/30 border border-white/10 px-2 rounded text-xs">{x.split(' ')[0]}</kbd></div>)}</Section>}
66
- {activeTab === 'storage' && <Section title="Storage"><p className="text-[#909090] text-xs leading-relaxed">Boards are saved to the app data directory through Rust persistence and can also be exported/imported as JSON files. Images embedded from browser capture are stored locally in board state.</p></Section>}
67
- {activeTab === 'appearance' && <Section title="Appearance"><div className="border border-[#0A84FF] bg-black/20 p-4 rounded-lg w-48"><div className="h-20 bg-[#1C1C1E] rounded border border-[#3A3A3E] mb-3" /><div className="text-center">Dark</div></div></Section>}
68
  {activeTab === 'about' && <div className="flex flex-col items-center text-center mt-10 gap-3"><div className="w-20 h-20 bg-[#2A2A2E] rounded-2xl flex items-center justify-center"><Monitor size={32} className="text-[#0A84FF]" /></div><h1 className="text-2xl font-semibold">Refstudio</h1><p className="text-[#808080]">1.0.0-alpha</p><p className="max-w-sm text-[#909090] text-sm">Local-first reference board with embedded ad-blocked browser, Stronghold vault, and app-managed history.</p></div>}
69
  </div>
70
  </div>
@@ -72,6 +72,6 @@ export const SettingsPanel = () => {
72
  );
73
  };
74
 
75
- function Section({ title, children }: { title: string; children: React.ReactNode }) { return <section className="mb-6"><h3 className="text-base font-medium mb-4 text-white">{title}</h3>{children}</section>; }
76
- function Toggle({ label, desc, active, onToggle }: { label: string; desc: string; active: boolean; onToggle: () => void }) { return <label className="flex items-center justify-between cursor-pointer mb-4" onClick={onToggle}><div><div className="font-medium">{label}</div><div className="text-[#808080] text-xs mt-0.5">{desc}</div></div><div className={`w-10 h-6 rounded-full relative ${active ? 'bg-[#0A84FF]' : 'bg-[#3A3A3E]'}`}><div className={`absolute top-1 w-4 h-4 rounded-full transition-all ${active ? 'right-1 bg-white' : 'left-1 bg-[#808080]'}`} /></div></label>; }
77
  function StatBox({ 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 type React from 'react';
2
+ import { X, Settings, Keyboard, Monitor, Shield, HardDrive, Info, RefreshCw, KeyRound, Lock, Unlock, Trash2, Plus, Navigation, Grid3X3 } from 'lucide-react';
3
  import { useAppStore } from '../store';
4
  import { useState, useEffect } from 'react';
5
  import { invoke } from '@tauri-apps/api/core';
 
9
  interface ShieldReport { blocked_requests: number; blocked_cosmetic: number; https_upgrades: number; engine_rules: number; }
10
 
11
  export const SettingsPanel = () => {
12
+ const { isSettingsOpen, setIsSettingsOpen, isAlwaysOnTop, setIsAlwaysOnTop, bgOpacity, setBgOpacity, showMinimap, setShowMinimap, showGrid, setShowGrid } = useAppStore();
13
  const [activeTab, setActiveTab] = useState<Tab>('general');
14
  const [shieldReport, setShieldReport] = useState<ShieldReport>({ blocked_requests: 0, blocked_cosmetic: 0, https_upgrades: 0, engine_rules: 0 });
15
  const [vaultUnlocked, setVaultUnlocked] = useState(isVaultUnlocked());
 
37
  const saveCred = async () => { try { await saveCredential({ origin, username, password }); setOrigin(''); setUsername(''); setPassword(''); setVaultMsg('Credential saved'); await reloadCreds(); } catch (e) { setVaultMsg(`Save failed: ${String(e)}`); } };
38
 
39
  return (
40
+ <div className={`absolute right-4 top-4 bottom-4 w-[min(680px,calc(100vw-32px))] z-[80] bg-[#1C1C1E]/98 shadow-2xl border border-[#3A3A3E] rounded-2xl flex flex-col transform transition-transform duration-500 ease-[cubic-bezier(0.19,1,0.22,1)] overflow-hidden ${isSettingsOpen ? 'translate-x-0' : 'translate-x-[calc(100%+32px)]'}`}>
41
  <div className="h-14 border-b border-white/5 flex items-center justify-between px-5 bg-black/20 shrink-0">
42
  <h2 className="text-[#E0E0E0] font-medium flex items-center gap-2"><Settings size={16} className="text-[#808080]" /> Settings</h2>
43
  <button onClick={() => setIsSettingsOpen(false)} className="text-[#808080] hover:text-[#E0E0E0] p-1 rounded-md hover:bg-white/5"><X size={18} /></button>
 
47
  {tabs.map(tab => <button key={tab.id} onClick={() => setActiveTab(tab.id)} className={`flex items-center gap-2.5 px-3 py-2 rounded-lg text-[13px] text-left transition-colors ${activeTab === tab.id ? 'bg-[#0A84FF] text-white font-medium' : 'text-[#808080] hover:text-[#E0E0E0] hover:bg-white/5'}`}>{tab.icon}{tab.label}</button>)}
48
  </div>
49
  <div className="flex-1 p-6 overflow-y-auto custom-scrollbar text-[#E0E0E0] text-sm min-w-0">
50
+ {activeTab === 'general' && <div className="space-y-6">
51
+ <SettingsGroup title="Startup & Window">
52
+ <SettingRow label="Open last board on startup" description="Load the last saved board automatically." active={true} onToggle={() => {}} disabled />
53
+ <SettingRow label="Always on Top" description="Keep Refstudio above other windows while drawing in another app." active={isAlwaysOnTop} onToggle={() => setIsAlwaysOnTop(!isAlwaysOnTop)} />
54
+ {isAlwaysOnTop && <div className="px-4 py-3 rounded-xl bg-black/20 border border-white/5 flex items-center justify-between gap-4"><div><div className="text-sm text-white">Overlay opacity</div><div className="text-xs text-[#808080] mt-0.5">Adjust transparency while pinned above your drawing app.</div></div><div className="flex items-center gap-3 shrink-0"><span className="text-xs text-[#A0A0A0] w-9 text-right">{bgOpacity}%</span><input type="range" min="10" max="100" value={bgOpacity} onChange={e => setBgOpacity(Number(e.target.value))} className="w-32 accent-[#0A84FF]" /></div></div>}
55
+ </SettingsGroup>
56
+ <SettingsGroup title="Canvas">
57
+ <SettingRow icon={<Grid3X3 size={16} />} label="Grid" description="Show the subtle dotted canvas grid. Kept in Settings so the toolbar stays minimal." active={showGrid} onToggle={() => setShowGrid(!showGrid)} />
58
+ <SettingRow icon={<Navigation size={16} />} label="Navigator / Minimap" description="Show a compact board overview in the bottom-right corner. Off by default to keep the canvas clean." active={showMinimap} onToggle={() => setShowMinimap(!showMinimap)} />
59
+ </SettingsGroup>
60
+ </div>}
61
 
62
+ {activeTab === 'privacy' && <div className="space-y-6"><SettingsGroup title="Muse Shield"><div className="grid grid-cols-2 gap-3"><StatBox value={shieldReport.engine_rules.toLocaleString()} label="Rules" /><StatBox value={shieldReport.blocked_requests.toLocaleString()} label="Requests blocked" /><StatBox value={shieldReport.blocked_cosmetic.toLocaleString()} label="Elements hidden" /><StatBox value={shieldReport.https_upgrades.toLocaleString()} label="HTTPS upgrades" /></div><button onClick={() => invoke('shield_update_lists').catch(() => {})} className="mt-4 w-full py-2 bg-white/5 hover:bg-white/10 rounded-lg border border-white/10 text-sm flex items-center justify-center gap-2"><RefreshCw size={14} /> Update filters</button></SettingsGroup><SettingsGroup title="Cookies, Cache & History"><p className="text-[#909090] text-xs leading-relaxed">Cookies and web cache are handled by the system WebView profile and persist automatically. Muse tracks app-level navigation history separately for UI/search. Fine-grained cross-platform cookie/cache inspection is not exposed by stable Tauri APIs.</p><button onClick={() => invoke('history_clear').catch(() => {})} className="mt-4 px-3 py-2 rounded-lg bg-red-500/10 text-red-300 border border-red-500/20 text-xs">Clear app history</button></SettingsGroup></div>}
63
 
64
+ {activeTab === 'vault' && <div className="space-y-6"><SettingsGroup title="Password Vault"><p className="text-[#909090] text-xs mb-4">Passwords are stored with the documented Tauri Stronghold frontend plugin. The vault is unlocked only with your master password.</p>{!vaultUnlocked ? <div className="flex gap-2"><input type="password" value={master} onChange={e => setMaster(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') unlock(); }} placeholder="Master password" className="flex-1 bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={unlock} className="px-3 py-2 bg-[#0A84FF] rounded-lg text-white flex items-center gap-2"><Unlock size={14} /> Unlock</button></div> : <button onClick={() => { lockVault(); setVaultUnlocked(false); setCreds([]); }} className="px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-white flex items-center gap-2"><Lock size={14} /> Lock vault</button>}{vaultMsg && <div className="mt-3 text-xs text-[#FFD60A]">{vaultMsg}</div>}</SettingsGroup>{vaultUnlocked && <SettingsGroup title="Save credential"><div className="grid grid-cols-1 gap-2"><input value={origin} onChange={e => setOrigin(e.target.value)} placeholder="Site / origin (example.com)" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><input value={username} onChange={e => setUsername(e.target.value)} placeholder="Username / email" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" className="bg-black/30 border border-white/10 rounded-lg px-3 py-2 outline-none focus:border-[#0A84FF]" /><button onClick={saveCred} className="px-3 py-2 bg-[#0A84FF] rounded-lg text-white flex items-center justify-center gap-2"><Plus size={14} /> Save credential</button></div><div className="mt-5 space-y-2">{creds.map(c => <div key={c.id} className="flex items-center justify-between bg-black/20 border border-white/5 rounded-lg p-3"><div><div className="text-white text-sm">{c.username}</div><div className="text-[#808080] text-xs">{c.origin}</div></div><button onClick={() => deleteCredential(c.id).then(reloadCreds)} className="text-red-300 hover:bg-red-500/10 rounded p-1"><Trash2 size={14} /></button></div>)}</div></SettingsGroup>}</div>}
65
+ {activeTab === 'shortcuts' && <SettingsGroup title="Keyboard Shortcuts">{['B Browser','L Library','A Annotate','D Desaturate','T Always on Top','Ctrl+Z Undo','Ctrl+Shift+Z Redo','Ctrl+0 Fit view','Esc Close panels'].map(x => <div key={x} className="flex justify-between py-2 border-b border-white/5"><span>{x.split(' ').slice(1).join(' ')}</span><kbd className="bg-black/30 border border-white/10 px-2 rounded text-xs">{x.split(' ')[0]}</kbd></div>)}</SettingsGroup>}
66
+ {activeTab === 'storage' && <SettingsGroup title="Storage"><p className="text-[#909090] text-xs leading-relaxed">Boards are saved to the app data directory through Rust persistence and can also be exported/imported as JSON files. Images embedded from browser capture are stored locally in board state.</p></SettingsGroup>}
67
+ {activeTab === 'appearance' && <SettingsGroup title="Appearance"><div className="border border-[#0A84FF] bg-black/20 p-4 rounded-lg w-48"><div className="h-20 bg-[#1C1C1E] rounded border border-[#3A3A3E] mb-3" /><div className="text-center">Dark</div></div></SettingsGroup>}
68
  {activeTab === 'about' && <div className="flex flex-col items-center text-center mt-10 gap-3"><div className="w-20 h-20 bg-[#2A2A2E] rounded-2xl flex items-center justify-center"><Monitor size={32} className="text-[#0A84FF]" /></div><h1 className="text-2xl font-semibold">Refstudio</h1><p className="text-[#808080]">1.0.0-alpha</p><p className="max-w-sm text-[#909090] text-sm">Local-first reference board with embedded ad-blocked browser, Stronghold vault, and app-managed history.</p></div>}
69
  </div>
70
  </div>
 
72
  );
73
  };
74
 
75
+ function SettingsGroup({ title, children }: { title: string; children: React.ReactNode }) { return <section><h3 className="text-[13px] font-semibold uppercase tracking-[0.12em] text-[#808080] mb-3">{title}</h3><div className="rounded-2xl border border-white/8 bg-black/18 overflow-hidden divide-y divide-white/6">{children}</div></section>; }
76
+ function SettingRow({ label, description, active, onToggle, disabled, icon }: { label: string; description: string; active: boolean; onToggle: () => void; disabled?: boolean; icon?: React.ReactNode }) { return <button type="button" disabled={disabled} onClick={onToggle} className="w-full min-h-[72px] px-4 py-3 flex items-center justify-between gap-5 text-left hover:bg-white/[0.035] disabled:opacity-60 disabled:cursor-default 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 className="min-w-0"><div className="text-[14px] font-medium text-white leading-5">{label}</div><div className="text-[12px] text-[#8A8A8C] leading-4 mt-1 max-w-[360px]">{description}</div></div></div><div className={`relative shrink-0 w-[52px] h-[30px] rounded-full transition-colors ${active ? 'bg-[#0A84FF]' : 'bg-[#3A3A3E]'}`}><div className={`absolute top-[3px] w-6 h-6 rounded-full bg-white shadow-md transition-all ${active ? 'left-[25px]' : 'left-[3px]'}`} /></div></button>; }
77
  function StatBox({ 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>; }