feat: add vault switcher in status bar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lucaronin
2026-02-17 17:45:31 +01:00
parent 2f59d1d6c8
commit 829cd2ad95
4 changed files with 133 additions and 19 deletions

View File

@@ -22,6 +22,11 @@ declare global {
const DEFAULT_SELECTION: SidebarSelection = { kind: 'filter', filter: 'all' }
const VAULTS = [
{ label: 'Laputa', path: '/Users/luca/Laputa' },
{ label: 'Demo', path: '/Users/luca/Workspace/laputa-app/demo-vault' },
]
function App() {
const [selection, setSelection] = useState<SidebarSelection>(DEFAULT_SELECTION)
const [sidebarWidth, setSidebarWidth] = useState(250)
@@ -33,10 +38,19 @@ function App() {
const [showQuickOpen, setShowQuickOpen] = useState(false)
const [showCommitDialog, setShowCommitDialog] = useState(false)
const [toastMessage, setToastMessage] = useState<string | null>(null)
const [vaultPath, setVaultPath] = useState(VAULTS[0].path)
const vault = useVaultLoader()
const vault = useVaultLoader(vaultPath)
const notes = useNoteActions(vault.addEntry, vault.updateContent, vault.entries, setToastMessage)
// Reset UI state when vault changes
const handleSwitchVault = useCallback((path: string) => {
setVaultPath(path)
setSelection(DEFAULT_SELECTION)
setGitHistory([])
notes.closeAllTabs()
}, [notes])
// Load git history when active tab changes
useEffect(() => {
if (!notes.activeTabPath) {
@@ -131,7 +145,7 @@ function App() {
/>
</div>
</div>
<StatusBar />
<StatusBar noteCount={vault.entries.length} vaultPath={vaultPath} vaults={VAULTS} onSwitchVault={handleSwitchVault} />
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
<QuickOpenPalette
open={showQuickOpen}

View File

@@ -1,6 +1,35 @@
import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { Package, GitBranch, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check } from 'lucide-react'
export interface VaultOption {
label: string
path: string
}
interface StatusBarProps {
noteCount: number
vaultPath: string
vaults: VaultOption[]
onSwitchVault: (path: string) => void
}
export function StatusBar({ noteCount, vaultPath, vaults, onSwitchVault }: StatusBarProps) {
const [showVaultMenu, setShowVaultMenu] = useState(false)
const menuRef = useRef<HTMLDivElement>(null)
const activeVault = vaults.find((v) => v.path === vaultPath)
useEffect(() => {
if (!showVaultMenu) return
const handleClick = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
setShowVaultMenu(false)
}
}
document.addEventListener('mousedown', handleClick)
return () => document.removeEventListener('mousedown', handleClick)
}, [showVaultMenu])
export function StatusBar() {
return (
<footer
style={{
@@ -18,6 +47,74 @@ export function StatusBar() {
>
{/* Left section */}
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div ref={menuRef} style={{ position: 'relative' }}>
<span
role="button"
onClick={() => setShowVaultMenu((v) => !v)}
style={{
display: 'flex',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
padding: '2px 4px',
borderRadius: 3,
background: showVaultMenu ? 'var(--hover)' : 'transparent',
}}
title="Switch vault"
>
<FolderOpen size={13} />
{activeVault?.label ?? 'Vault'}
</span>
{showVaultMenu && (
<div
style={{
position: 'absolute',
bottom: '100%',
left: 0,
marginBottom: 4,
background: 'var(--sidebar)',
border: '1px solid var(--border)',
borderRadius: 6,
padding: 4,
minWidth: 160,
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
zIndex: 1000,
}}
>
{vaults.map((v) => (
<div
key={v.path}
role="button"
onClick={() => {
onSwitchVault(v.path)
setShowVaultMenu(false)
}}
style={{
display: 'flex',
alignItems: 'center',
gap: 6,
padding: '4px 8px',
borderRadius: 4,
cursor: 'pointer',
background: v.path === vaultPath ? 'var(--hover)' : 'transparent',
color: v.path === vaultPath ? 'var(--foreground)' : 'var(--muted-foreground)',
fontSize: 12,
}}
onMouseEnter={(e) => {
if (v.path !== vaultPath) e.currentTarget.style.background = 'var(--hover)'
}}
onMouseLeave={(e) => {
if (v.path !== vaultPath) e.currentTarget.style.background = 'transparent'
}}
>
{v.path === vaultPath ? <Check size={12} /> : <span style={{ width: 12 }} />}
{v.label}
</div>
))}
</div>
)}
</div>
<span style={{ color: 'var(--border)' }}>|</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Package size={13} />
v0.4.2
@@ -42,7 +139,7 @@ export function StatusBar() {
</span>
<span style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<FileText size={13} />
1,247 notes
{noteCount.toLocaleString()} notes
</span>
<span
style={{ display: 'flex', alignItems: 'center', opacity: 0.4, cursor: 'not-allowed' }}

View File

@@ -257,6 +257,11 @@ export function useNoteActions(
return handleUpdateFrontmatter(path, key, value)
}, [handleUpdateFrontmatter])
const closeAllTabs = useCallback(() => {
setTabs([])
setActiveTabPath(null)
}, [])
return {
tabs,
activeTabPath,
@@ -270,5 +275,6 @@ export function useNoteActions(
handleUpdateFrontmatter,
handleDeleteProperty,
handleAddProperty,
closeAllTabs,
}
}

View File

@@ -3,20 +3,21 @@ import { invoke } from '@tauri-apps/api/core'
import { isTauri, mockInvoke } from '../mock-tauri'
import type { VaultEntry, GitCommit, ModifiedFile } from '../types'
const TEST_VAULT_PATH = '~/Laputa'
export function useVaultLoader() {
export function useVaultLoader(vaultPath: string) {
const [entries, setEntries] = useState<VaultEntry[]>([])
const [allContent, setAllContent] = useState<Record<string, string>>({})
const [modifiedFiles, setModifiedFiles] = useState<ModifiedFile[]>([])
useEffect(() => {
setEntries([])
setAllContent({})
setModifiedFiles([])
const loadVault = async () => {
try {
let result: VaultEntry[]
if (isTauri()) {
const path = TEST_VAULT_PATH.replace('~', '/Users/luca')
result = await invoke<VaultEntry[]>('list_vault', { path })
result = await invoke<VaultEntry[]>('list_vault', { path: vaultPath })
} else {
console.info('[mock] Using mock Tauri data for browser testing')
result = await mockInvoke<VaultEntry[]>('list_vault', {})
@@ -36,13 +37,12 @@ export function useVaultLoader() {
}
}
loadVault()
}, [])
}, [vaultPath])
const loadModifiedFiles = useCallback(async () => {
try {
let files: ModifiedFile[]
if (isTauri()) {
const vaultPath = TEST_VAULT_PATH.replace('~', '/Users/luca')
files = await invoke<ModifiedFile[]>('get_modified_files', { vaultPath })
} else {
files = await mockInvoke<ModifiedFile[]>('get_modified_files', {})
@@ -52,7 +52,7 @@ export function useVaultLoader() {
console.warn('Failed to load modified files:', err)
setModifiedFiles([])
}
}, [])
}, [vaultPath])
useEffect(() => {
loadModifiedFiles()
@@ -70,7 +70,6 @@ export function useVaultLoader() {
const loadGitHistory = useCallback(async (path: string): Promise<GitCommit[]> => {
try {
if (isTauri()) {
const vaultPath = TEST_VAULT_PATH.replace('~', '/Users/luca')
return await invoke<GitCommit[]>('get_file_history', { vaultPath, path })
} else {
return await mockInvoke<GitCommit[]>('get_file_history', { path })
@@ -79,23 +78,21 @@ export function useVaultLoader() {
console.warn('Failed to load git history:', err)
return []
}
}, [])
}, [vaultPath])
const loadDiff = useCallback(async (path: string): Promise<string> => {
if (isTauri()) {
const vaultPath = TEST_VAULT_PATH.replace('~', '/Users/luca')
return invoke<string>('get_file_diff', { vaultPath, path })
} else {
return mockInvoke<string>('get_file_diff', { path })
}
}, [])
}, [vaultPath])
const isFileModified = useCallback((path: string): boolean => {
return modifiedFiles.some((f) => f.path === path)
}, [modifiedFiles])
const commitAndPush = useCallback(async (message: string): Promise<string> => {
const vaultPath = TEST_VAULT_PATH.replace('~', '/Users/luca')
if (isTauri()) {
await invoke<string>('git_commit', { vaultPath, message })
try {
@@ -109,7 +106,7 @@ export function useVaultLoader() {
await mockInvoke<string>('git_push', {})
return 'Committed and pushed'
}
}, [])
}, [vaultPath])
return {
entries,