feat: add status bar theme toggle
This commit is contained in:
@@ -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} />
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user