feat: add status bar theme toggle

This commit is contained in:
lucaronin
2026-04-24 23:58:26 +02:00
parent b9f3dc9e1a
commit cd49c81bd3
4 changed files with 71 additions and 8 deletions

View File

@@ -31,6 +31,7 @@ import { useAutoGit } from './hooks/useAutoGit'
import { useVaultLoader } from './hooks/useVaultLoader'
import { useAiAgentPreferences } from './hooks/useAiAgentPreferences'
import { useSettings } from './hooks/useSettings'
import { useDocumentThemeMode } from './hooks/useDocumentThemeMode'
import { useThemeMode } from './hooks/useThemeMode'
import { useNoteActions } from './hooks/useNoteActions'
import { planNewTypeCreation } from './hooks/useNoteCreation'
@@ -369,6 +370,11 @@ function App() {
}, [updateConfig, vaultConfig.inbox?.noteListProperties])
const { settings, loaded: settingsLoaded, saveSettings } = useSettings()
useThemeMode(settings.theme_mode, settingsLoaded)
const documentThemeMode = useDocumentThemeMode()
const handleToggleThemeMode = useCallback(() => {
const theme_mode = documentThemeMode === 'dark' ? 'light' : 'dark'
void saveSettings({ ...settings, theme_mode })
}, [documentThemeMode, saveSettings, settings])
const aiAgentPreferences = useAiAgentPreferences({
settings,
saveSettings,
@@ -1492,7 +1498,7 @@ function App() {
</div>
<UpdateBanner status={updateStatus} actions={updateActions} />
<RenameDetectedBanner renames={detectedRenames} onUpdate={handleUpdateWikilinks} onDismiss={handleDismissRenames} />
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={resolvedPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenFeedback={openFeedback} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} onZoomReset={zoom.zoomReset} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} />
<StatusBar noteCount={vault.entries.length} modifiedCount={vault.modifiedFiles.length} vaultPath={resolvedPath} vaults={vaultSwitcher.allVaults} onSwitchVault={vaultSwitcher.switchVault} onOpenSettings={dialogs.openSettings} onOpenFeedback={openFeedback} onOpenLocalFolder={vaultSwitcher.handleOpenLocalFolder} onCreateEmptyVault={vaultSwitcher.handleCreateEmptyVault} onCloneVault={dialogs.openCloneVault} onCloneGettingStarted={cloneGettingStartedVault} onClickPending={() => handleSetSelection({ kind: 'filter', filter: 'changes' })} onClickPulse={() => handleSetSelection({ kind: 'filter', filter: 'pulse' })} onCommitPush={handleCommitPush} isOffline={networkStatus.isOffline} isGitVault={isGitVault} syncStatus={autoSync.syncStatus} lastSyncTime={autoSync.lastSyncTime} conflictCount={autoSync.conflictFiles.length} remoteStatus={autoSync.remoteStatus} onTriggerSync={autoSync.triggerSync} onPullAndPush={autoSync.pullAndPush} onOpenConflictResolver={conflictFlow.handleOpenConflictResolver} zoomLevel={zoom.zoomLevel} themeMode={documentThemeMode} onZoomReset={zoom.zoomReset} onToggleThemeMode={settingsLoaded ? handleToggleThemeMode : undefined} buildNumber={buildNumber} onCheckForUpdates={handleCheckForUpdates} onRemoveVault={vaultSwitcher.removeVault} mcpStatus={mcpStatus} onInstallMcp={openMcpSetupDialog} aiAgentsStatus={aiAgentsStatus} vaultAiGuidanceStatus={vaultAiGuidanceStatus} defaultAiAgent={aiAgentPreferences.defaultAiAgent} onSetDefaultAiAgent={aiAgentPreferences.setDefaultAiAgent} onRestoreVaultAiGuidance={() => { void restoreVaultAiGuidance() }} />
<DeleteProgressNotice count={deleteActions.pendingDeleteCount} />
<Toast message={toastMessage} onDismiss={() => setToastMessage(null)} />
<QuickOpenPalette open={dialogs.showQuickOpen} entries={vault.entries} onSelect={notes.handleSelectNote} onClose={dialogs.closeQuickOpen} />

View File

@@ -84,6 +84,39 @@ describe('StatusBar', () => {
expect(onOpenFeedback).toHaveBeenCalledOnce()
})
it('shows a theme toggle instead of the notifications placeholder', () => {
render(
<StatusBar
noteCount={100}
vaultPath="/Users/luca/Laputa"
vaults={vaults}
onSwitchVault={vi.fn()}
themeMode="light"
onToggleThemeMode={vi.fn()}
/>,
)
expect(screen.getByTestId('status-theme-mode')).toHaveAccessibleName('Switch to dark mode')
expect(screen.queryByLabelText('Notifications are coming soon')).not.toBeInTheDocument()
})
it('calls onToggleThemeMode from the bottom bar', () => {
const onToggleThemeMode = vi.fn()
render(
<StatusBar
noteCount={100}
vaultPath="/Users/luca/Laputa"
vaults={vaults}
onSwitchVault={vi.fn()}
themeMode="dark"
onToggleThemeMode={onToggleThemeMode}
/>,
)
fireEvent.click(screen.getByRole('button', { name: 'Switch to light mode' }))
expect(onToggleThemeMode).toHaveBeenCalledOnce()
})
it('displays active vault name', () => {
render(<StatusBar noteCount={100} vaultPath="/Users/luca/Laputa" vaults={vaults} onSwitchVault={vi.fn()} />)
expect(screen.getByText('Main Vault')).toBeInTheDocument()

View File

@@ -3,6 +3,7 @@ import type { VaultAiGuidanceStatus } from '../lib/vaultAiGuidance'
import { useEffect, useState } from 'react'
import type { ClaudeCodeStatus } from '../hooks/useClaudeCodeStatus'
import type { McpStatus } from '../hooks/useMcpStatus'
import type { ThemeMode } from '../lib/themeMode'
import type { GitRemoteStatus, SyncStatus } from '../types'
import { TooltipProvider } from '@/components/ui/tooltip'
import {
@@ -37,7 +38,9 @@ interface StatusBarProps {
onPullAndPush?: () => void
onOpenConflictResolver?: () => void
zoomLevel?: number
themeMode?: ThemeMode
onZoomReset?: () => void
onToggleThemeMode?: () => void
onOpenFeedback?: () => void
buildNumber?: string
onCheckForUpdates?: () => void
@@ -77,7 +80,9 @@ export function StatusBar({
onPullAndPush,
onOpenConflictResolver,
zoomLevel = 100,
themeMode = 'light',
onZoomReset,
onToggleThemeMode,
onOpenFeedback,
buildNumber,
onCheckForUpdates,
@@ -154,7 +159,9 @@ export function StatusBar({
<StatusBarSecondarySection
noteCount={noteCount}
zoomLevel={zoomLevel}
themeMode={themeMode}
onZoomReset={onZoomReset}
onToggleThemeMode={onToggleThemeMode}
onOpenFeedback={onOpenFeedback}
onOpenSettings={onOpenSettings}
/>

View File

@@ -1,9 +1,10 @@
import { Bell, Package, Settings } from 'lucide-react'
import { Moon, Package, Settings, Sun } from 'lucide-react'
import { Megaphone } from '@phosphor-icons/react'
import type { AiAgentId, AiAgentsStatus } from '../../lib/aiAgents'
import type { VaultAiGuidanceStatus } from '../../lib/vaultAiGuidance'
import type { ClaudeCodeStatus } from '../../hooks/useClaudeCodeStatus'
import type { McpStatus } from '../../hooks/useMcpStatus'
import type { ThemeMode } from '../../lib/themeMode'
import { useStatusBarAddRemote } from '../../hooks/useStatusBarAddRemote'
import type { GitRemoteStatus, SyncStatus } from '../../types'
import { rememberFeedbackDialogOpener } from '../../lib/feedbackDialogOpener'
@@ -22,7 +23,7 @@ import {
PulseBadge,
SyncBadge,
} from './StatusBarBadges'
import { DISABLED_STYLE, ICON_STYLE, SEP_STYLE } from './styles'
import { ICON_STYLE, SEP_STYLE } from './styles'
import type { VaultOption } from './types'
import { VaultMenu } from './VaultMenu'
import { formatShortcutDisplay } from '../../hooks/appCommandCatalog'
@@ -33,7 +34,8 @@ const ZOOM_RESET_TOOLTIP = {
shortcut: formatShortcutDisplay({ display: '⌘0' }),
} as const
const FEEDBACK_TOOLTIP = { label: 'Contribute to Tolaria' } as const
const NOTIFICATIONS_TOOLTIP = { label: 'Notifications are coming soon' } as const
const LIGHT_MODE_TOOLTIP = { label: 'Switch to light mode' } as const
const DARK_MODE_TOOLTIP = { label: 'Switch to dark mode' } as const
const SETTINGS_TOOLTIP = {
label: 'Open settings',
shortcut: formatShortcutDisplay({ display: '⌘,' }),
@@ -78,7 +80,9 @@ interface StatusBarPrimarySectionProps {
interface StatusBarSecondarySectionProps {
noteCount: number
zoomLevel: number
themeMode?: ThemeMode
onZoomReset?: () => void
onToggleThemeMode?: () => void
onOpenFeedback?: () => void
onOpenSettings?: () => void
}
@@ -205,11 +209,15 @@ export function StatusBarPrimarySection({
export function StatusBarSecondarySection({
noteCount,
zoomLevel,
themeMode = 'light',
onZoomReset,
onToggleThemeMode,
onOpenFeedback,
onOpenSettings,
}: StatusBarSecondarySectionProps) {
void noteCount
const ThemeIcon = themeMode === 'dark' ? Sun : Moon
const themeTooltip = themeMode === 'dark' ? LIGHT_MODE_TOOLTIP : DARK_MODE_TOOLTIP
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 12, flexShrink: 0 }}>
@@ -247,10 +255,19 @@ export function StatusBarSecondarySection({
</Button>
</ActionTooltip>
)}
<ActionTooltip copy={NOTIFICATIONS_TOOLTIP} side="top">
<span style={DISABLED_STYLE} aria-label={NOTIFICATIONS_TOOLTIP.label}>
<Bell size={14} />
</span>
<ActionTooltip copy={themeTooltip} side="top">
<Button
type="button"
variant="ghost"
size="icon-xs"
className="text-muted-foreground hover:bg-[var(--hover)] hover:text-foreground"
onClick={onToggleThemeMode}
disabled={!onToggleThemeMode}
aria-label={themeTooltip.label}
data-testid="status-theme-mode"
>
<ThemeIcon size={14} />
</Button>
</ActionTooltip>
<ActionTooltip copy={SETTINGS_TOOLTIP} side="top" align="end">
<Button