feat: add vault switcher in status bar
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
18
src/App.tsx
18
src/App.tsx
@@ -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}
|
||||
|
||||
@@ -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' }}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user