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 (
)
}