import { useState, useRef, useEffect } from 'react' import { Package, RefreshCw, Sparkles, FileText, Bell, Settings, FolderOpen, Check, Github, CircleDot, AlertTriangle, Loader2, GitCommitHorizontal } from 'lucide-react' import type { LastCommitInfo, SyncStatus } from '../types' import { openExternalUrl } from '../utils/url' export interface VaultOption { label: string path: string } interface StatusBarProps { noteCount: number modifiedCount?: number vaultPath: string vaults: VaultOption[] onSwitchVault: (path: string) => void onOpenSettings?: () => void onOpenLocalFolder?: () => void onConnectGitHub?: () => void onClickPending?: () => void hasGitHub?: boolean syncStatus?: SyncStatus lastSyncTime?: number | null conflictCount?: number lastCommitInfo?: LastCommitInfo | null onTriggerSync?: () => void zoomLevel?: number onZoomReset?: () => void } function VaultMenuItem({ vault, isActive, onSelect }: { vault: VaultOption; isActive: boolean; onSelect: () => void }) { return (
{ if (!isActive) e.currentTarget.style.background = 'var(--hover)' }} onMouseLeave={(e) => { if (!isActive) e.currentTarget.style.background = 'transparent' }} > {isActive ? : } {vault.label}
) } function VaultMenu({ vaults, vaultPath, onSwitchVault, onOpenLocalFolder, onConnectGitHub, hasGitHub }: { vaults: VaultOption[]; vaultPath: string; onSwitchVault: (path: string) => void; onOpenLocalFolder?: () => void; onConnectGitHub?: () => void; hasGitHub?: boolean }) { const [open, setOpen] = useState(false) const menuRef = useRef(null) const activeVault = vaults.find((v) => v.path === vaultPath) useEffect(() => { if (!open) return const handleClick = (e: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(e.target as Node)) setOpen(false) } document.addEventListener('mousedown', handleClick) return () => document.removeEventListener('mousedown', handleClick) }, [open]) return (
setOpen((v) => !v)} style={{ display: 'flex', alignItems: 'center', gap: 4, cursor: 'pointer', padding: '2px 4px', borderRadius: 3, background: open ? 'var(--hover)' : 'transparent' }} title="Switch vault"> {activeVault?.label ?? 'Vault'} {open && (
{vaults.map((v) => { onSwitchVault(v.path); setOpen(false) }} />)}
{onOpenLocalFolder && (
{ onOpenLocalFolder(); setOpen(false) }} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4, cursor: 'pointer', background: 'transparent', color: 'var(--muted-foreground)', fontSize: 12, }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }} data-testid="vault-menu-open-local" > Open local folder
)} {onConnectGitHub && (
{ onConnectGitHub(); setOpen(false) }} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '4px 8px', borderRadius: 4, cursor: 'pointer', background: 'transparent', color: hasGitHub ? 'var(--muted-foreground)' : 'var(--accent-blue)', fontSize: 12, }} onMouseEnter={e => { e.currentTarget.style.background = 'var(--hover)' }} onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }} data-testid="vault-menu-connect-github" > Connect GitHub repo
)}
)}
) } const ICON_STYLE = { display: 'flex', alignItems: 'center', gap: 4 } as const const DISABLED_STYLE = { display: 'flex', alignItems: 'center', opacity: 0.4, cursor: 'not-allowed' } as const const SEP_STYLE = { color: 'var(--border)' } as const const SYNC_ICON_MAP: Record = { syncing: Loader2, conflict: AlertTriangle } function formatSyncLabel(status: SyncStatus, lastSyncTime: number | null): string { if (status === 'syncing') return 'Syncing…' if (status === 'conflict') return 'Conflict' if (status === 'error') return 'Sync failed' if (!lastSyncTime) return 'Not synced' const elapsed = Math.round((Date.now() - lastSyncTime) / 1000) if (elapsed < 60) return 'Synced just now' const mins = Math.floor(elapsed / 60) return `Synced ${mins}m ago` } function syncIconColor(status: SyncStatus): string { if (status === 'conflict') return 'var(--accent-orange)' if (status === 'error') return 'var(--muted-foreground)' return 'var(--accent-green)' } function CommitBadge({ info }: { info: LastCommitInfo }) { if (info.commitUrl) { return ( openExternalUrl(info.commitUrl!)} style={{ ...ICON_STYLE, color: 'var(--muted-foreground)', textDecoration: 'none', cursor: 'pointer', padding: '2px 4px', borderRadius: 3 }} title={`Open commit ${info.shortHash} on GitHub`} data-testid="status-commit-link" onMouseEnter={e => { e.currentTarget.style.color = 'var(--foreground)' }} onMouseLeave={e => { e.currentTarget.style.color = 'var(--muted-foreground)' }} > {info.shortHash} ) } return ( {info.shortHash} ) } export function StatusBar({ noteCount, modifiedCount = 0, vaultPath, vaults, onSwitchVault, onOpenSettings, onOpenLocalFolder, onConnectGitHub, onClickPending, hasGitHub, syncStatus = 'idle', lastSyncTime = null, conflictCount = 0, lastCommitInfo, onTriggerSync, zoomLevel = 100, onZoomReset }: StatusBarProps) { // Force re-render every 30s to keep relative time label fresh const [, setTick] = useState(0) useEffect(() => { const id = setInterval(() => setTick((t) => t + 1), 30_000) return () => clearInterval(id) }, []) const syncLabel = formatSyncLabel(syncStatus, lastSyncTime) const SyncIcon = SYNC_ICON_MAP[syncStatus] ?? RefreshCw return ( ) }