diff --git a/src/App.tsx b/src/App.tsx index 7f1c36f4..317172d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() { - 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() }} /> + 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() }} /> setToastMessage(null)} /> diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index 0991c150..1810e59c 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -84,6 +84,39 @@ describe('StatusBar', () => { expect(onOpenFeedback).toHaveBeenCalledOnce() }) + it('shows a theme toggle instead of the notifications placeholder', () => { + render( + , + ) + + 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( + , + ) + + fireEvent.click(screen.getByRole('button', { name: 'Switch to light mode' })) + expect(onToggleThemeMode).toHaveBeenCalledOnce() + }) + it('displays active vault name', () => { render() expect(screen.getByText('Main Vault')).toBeInTheDocument() diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index 4949c044..58503f45 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -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({ diff --git a/src/components/status-bar/StatusBarSections.tsx b/src/components/status-bar/StatusBarSections.tsx index 1119af5f..f792b2dd 100644 --- a/src/components/status-bar/StatusBarSections.tsx +++ b/src/components/status-bar/StatusBarSections.tsx @@ -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 ( @@ -247,10 +255,19 @@ export function StatusBarSecondarySection({ )} - - - - + + + +