diff --git a/src/components/BreadcrumbBar.test.tsx b/src/components/BreadcrumbBar.test.tsx index ceb2dda3..09469f93 100644 --- a/src/components/BreadcrumbBar.test.tsx +++ b/src/components/BreadcrumbBar.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent } from '@testing-library/react' +import { render, screen, fireEvent, act } from '@testing-library/react' import { describe, it, expect, vi } from 'vitest' import { BreadcrumbBar } from './BreadcrumbBar' import type { VaultEntry } from '../types' @@ -49,6 +49,19 @@ const defaultProps = { onToggleDiff: vi.fn(), } +async function expectTooltip(trigger: HTMLElement, ...parts: string[]) { + act(() => { + fireEvent.focus(trigger) + }) + const tooltip = await screen.findByRole('tooltip') + for (const part of parts) { + expect(tooltip).toHaveTextContent(part) + } + act(() => { + fireEvent.blur(trigger) + }) +} + describe('BreadcrumbBar — drag region', () => { it('has data-tauri-drag-region on the container', () => { const { container } = render() @@ -67,13 +80,13 @@ describe('BreadcrumbBar — drag region', () => { describe('BreadcrumbBar — delete', () => { it('shows delete button', () => { render() - expect(screen.getByTitle('Delete (Cmd+Delete)')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Delete this note' })).toBeInTheDocument() }) it('calls onDelete when delete button is clicked', () => { const onDelete = vi.fn() render() - fireEvent.click(screen.getByTitle('Delete (Cmd+Delete)')) + fireEvent.click(screen.getByRole('button', { name: 'Delete this note' })) expect(onDelete).toHaveBeenCalledOnce() }) }) @@ -81,40 +94,40 @@ describe('BreadcrumbBar — delete', () => { describe('BreadcrumbBar — archive/unarchive', () => { it('shows archive button for non-archived note', () => { render() - expect(screen.getByTitle('Archive')).toBeInTheDocument() - expect(screen.queryByTitle('Unarchive')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Archive this note' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Restore this archived note' })).not.toBeInTheDocument() }) it('shows unarchive button for archived note', () => { render() - expect(screen.getByTitle('Unarchive')).toBeInTheDocument() - expect(screen.queryByTitle('Archive')).not.toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Restore this archived note' })).toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Archive this note' })).not.toBeInTheDocument() }) it('calls onArchive when archive button is clicked', () => { const onArchive = vi.fn() render() - fireEvent.click(screen.getByTitle('Archive')) + fireEvent.click(screen.getByRole('button', { name: 'Archive this note' })) expect(onArchive).toHaveBeenCalledOnce() }) it('calls onUnarchive when unarchive button is clicked', () => { const onUnarchive = vi.fn() render() - fireEvent.click(screen.getByTitle('Unarchive')) + fireEvent.click(screen.getByRole('button', { name: 'Restore this archived note' })) expect(onUnarchive).toHaveBeenCalledOnce() }) }) describe('BreadcrumbBar — organized shortcut hint', () => { - it('shows Cmd+E on the organized toggle tooltip', () => { + it('shows Cmd+E on the organized toggle tooltip', async () => { render() - expect(screen.getByTitle('Mark as organized (remove from Inbox) (Cmd+E)')).toBeInTheDocument() + await expectTooltip(screen.getByRole('button', { name: 'Set note as organized' }), 'Set note as organized', '⌘E') }) it('hides the organized toggle when the workflow is disabled', () => { render() - expect(screen.queryByTitle('Mark as organized (remove from Inbox) (Cmd+E)')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Set note as organized' })).not.toBeInTheDocument() }) }) @@ -238,32 +251,32 @@ describe('BreadcrumbBar — raw editor toggle', () => { it('shows Raw editor button with tooltip "Raw editor" when rawMode is off', () => { const onToggleRaw = vi.fn() render() - expect(screen.getByTitle('Raw editor')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Open the raw editor' })).toBeInTheDocument() }) it('shows "Back to editor" tooltip when rawMode is on', () => { const onToggleRaw = vi.fn() render() - expect(screen.getByTitle('Back to editor')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Return to the editor' })).toBeInTheDocument() }) it('calls onToggleRaw when raw button is clicked', () => { const onToggleRaw = vi.fn() render() - fireEvent.click(screen.getByTitle('Raw editor')) + fireEvent.click(screen.getByRole('button', { name: 'Open the raw editor' })) expect(onToggleRaw).toHaveBeenCalledOnce() }) it('hides raw toggle when forceRawMode is true (non-markdown file)', () => { const onToggleRaw = vi.fn() render() - expect(screen.queryByTitle('Raw editor')).not.toBeInTheDocument() - expect(screen.queryByTitle('Back to editor')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Open the raw editor' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Return to the editor' })).not.toBeInTheDocument() }) it('shows raw toggle when forceRawMode is false (markdown file)', () => { const onToggleRaw = vi.fn() render() - expect(screen.getByTitle('Raw editor')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Open the raw editor' })).toBeInTheDocument() }) }) diff --git a/src/components/BreadcrumbBar.tsx b/src/components/BreadcrumbBar.tsx index 28fc2014..c22928de 100644 --- a/src/components/BreadcrumbBar.tsx +++ b/src/components/BreadcrumbBar.tsx @@ -3,7 +3,8 @@ import type { VaultEntry } from '../types' import { cn } from '@/lib/utils' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' +import { ActionTooltip, type ActionTooltipCopy } from '@/components/ui/action-tooltip' +import { TooltipProvider } from '@/components/ui/tooltip' import { MagnifyingGlass, GitBranch, @@ -96,46 +97,44 @@ function handleFilenameInputKeyDown( } function IconActionButton({ - title, + copy, onClick, className, style, - disabled, - tabIndex, children, testId, }: { - title: string + copy: ActionTooltipCopy onClick?: () => void className?: string style?: CSSProperties - disabled?: boolean - tabIndex?: number children: ReactNode testId?: string }) { return ( - + + + ) } function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggleRaw?: () => void }) { + const copy: ActionTooltipCopy = { label: rawMode ? 'Return to the editor' : 'Open the raw editor' } return ( @@ -145,9 +144,13 @@ function RawToggleButton({ rawMode, onToggleRaw }: { rawMode?: boolean; onToggle } function FavoriteAction({ favorite, onToggleFavorite }: { favorite: boolean; onToggleFavorite?: () => void }) { + const copy: ActionTooltipCopy = { + label: favorite ? 'Remove from favorites' : 'Add to favorites', + shortcut: '⌘D', + } return ( @@ -164,9 +167,13 @@ function OrganizedAction({ onToggleOrganized?: () => void }) { if (!onToggleOrganized) return null + const copy: ActionTooltipCopy = { + label: organized ? 'Set note as not organized' : 'Set note as organized', + shortcut: '⌘E', + } return ( @@ -177,7 +184,7 @@ function OrganizedAction({ function SearchAction() { return ( - + ) @@ -191,17 +198,19 @@ function DiffAction({ }: Pick) { if (!showDiffToggle) { return ( - + ) } + const copy: ActionTooltipCopy = diffLoading + ? { label: 'Loading the diff' } + : { label: diffMode ? 'Return to the editor' : 'Show the current diff' } return ( @@ -209,18 +218,22 @@ function DiffAction({ ) } -function PlaceholderAction({ title, children }: { title: string; children: ReactNode }) { +function PlaceholderAction({ copy, children }: { copy: ActionTooltipCopy; children: ReactNode }) { return ( - + {children} ) } function AIChatAction({ showAIChat, onToggleAIChat }: Pick) { + const copy: ActionTooltipCopy = { + label: showAIChat ? 'Close the AI panel' : 'Open the AI panel', + shortcut: '⇧⌘L', + } return ( @@ -236,14 +249,14 @@ function ArchiveAction({ }: Pick & Pick) { if (archived) { return ( - + ) } return ( - + ) @@ -251,7 +264,7 @@ function ArchiveAction({ function DeleteAction({ onDelete }: Pick) { return ( - + ) @@ -263,7 +276,7 @@ function InspectorAction({ }: Pick) { if (!inspectorCollapsed) return null return ( - + ) @@ -351,26 +364,19 @@ function SyncFilenameButton({ }) { if (!syncStem || !onRenameFilename) return null return ( - - - - - - - Rename file to match title - - - + + + ) } @@ -480,14 +486,14 @@ function BreadcrumbActions({ onToggleDiff={onToggleDiff} /> {!forceRawMode && } - + - + @@ -517,27 +523,29 @@ export const BreadcrumbBar = memo(function BreadcrumbBar({ ...actionProps }: BreadcrumbBarProps) { return ( -
-
- -
+ + data-title-hidden="" + className="breadcrumb-bar flex shrink-0 items-center border-b border-transparent" + style={{ + height: 52, + background: 'var(--background)', + padding: '6px 16px', + boxSizing: 'border-box', + }} + > +
+ +
+ +
) }) diff --git a/src/components/Editor.test.tsx b/src/components/Editor.test.tsx index ca155a6e..0c07d87a 100644 --- a/src/components/Editor.test.tsx +++ b/src/components/Editor.test.tsx @@ -208,7 +208,7 @@ describe('Editor', () => { activeTabPath: mockEntry.path, }) - expect(screen.getByTitle('Search in file')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Search within this note' })).toBeInTheDocument() }) it('hides the legacy title field for untitled draft notes', () => { @@ -248,7 +248,7 @@ describe('Editor', () => { onLoadDiff={async () => '+ added line'} /> ) - const diffBtn = screen.getByTitle('Show diff') + const diffBtn = screen.getByRole('button', { name: 'Show the current diff' }) expect(diffBtn).toBeInTheDocument() }) diff --git a/src/components/StatusBar.test.tsx b/src/components/StatusBar.test.tsx index fa9579b7..e0384b74 100644 --- a/src/components/StatusBar.test.tsx +++ b/src/components/StatusBar.test.tsx @@ -19,6 +19,19 @@ const installedAiAgentsStatus = { codex: { status: 'installed' as const, version: '0.37.0' }, } +async function expectTooltip(trigger: HTMLElement, ...parts: string[]) { + act(() => { + fireEvent.focus(trigger) + }) + const tooltip = await screen.findByRole('tooltip') + for (const part of parts) { + expect(tooltip).toHaveTextContent(part) + } + act(() => { + fireEvent.blur(trigger) + }) +} + describe('StatusBar', () => { beforeEach(() => { vi.clearAllMocks() @@ -46,9 +59,9 @@ describe('StatusBar', () => { expect(onCheckForUpdates).toHaveBeenCalledOnce() }) - it('build number has "Check for updates" title', () => { + it('build number shows the update tooltip on focus', async () => { render() - expect(screen.getByTitle('Check for updates')).toBeInTheDocument() + await expectTooltip(screen.getByRole('button', { name: 'Check for updates' }), 'Check for updates') }) it('does not display branch name', () => { @@ -83,7 +96,7 @@ describe('StatusBar', () => { render() // Click the vault button to open menu - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) expect(screen.getByText('Work Vault')).toBeInTheDocument() }) @@ -92,7 +105,7 @@ describe('StatusBar', () => { const onSwitchVault = vi.fn() render() - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) // Click "Work Vault" fireEvent.click(screen.getByText('Work Vault')) @@ -102,7 +115,7 @@ describe('StatusBar', () => { it('closes vault menu when clicking outside', () => { render() - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) expect(screen.getByText('Work Vault')).toBeInTheDocument() // Click outside the menu @@ -114,7 +127,7 @@ describe('StatusBar', () => { it('toggles vault menu open and closed', () => { render() - const vaultButton = screen.getByTitle('Switch vault') + const vaultButton = screen.getByRole('button', { name: 'Switch vault' }) fireEvent.click(vaultButton) expect(screen.getByText('Work Vault')).toBeInTheDocument() @@ -127,7 +140,7 @@ describe('StatusBar', () => { render( ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) expect(screen.getByText('Open local folder')).toBeInTheDocument() }) @@ -136,7 +149,7 @@ describe('StatusBar', () => { render( ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) fireEvent.click(screen.getByText('Open local folder')) expect(onOpenLocalFolder).toHaveBeenCalledOnce() }) @@ -152,7 +165,7 @@ describe('StatusBar', () => { onCloneVault={vi.fn()} /> ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) expect(screen.getByText('Open local folder')).toBeInTheDocument() expect(screen.getByText('Clone Git repo')).toBeInTheDocument() }) @@ -168,7 +181,7 @@ describe('StatusBar', () => { /> ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) expect(screen.getByText('Clone Getting Started Vault')).toBeInTheDocument() }) @@ -184,7 +197,7 @@ describe('StatusBar', () => { /> ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) fireEvent.click(screen.getByText('Clone Getting Started Vault')) expect(onCloneGettingStarted).toHaveBeenCalledOnce() }) @@ -210,7 +223,7 @@ describe('StatusBar', () => { render( ) - fireEvent.click(screen.getByTitle('Switch vault')) + fireEvent.click(screen.getByRole('button', { name: 'Switch vault' })) fireEvent.click(screen.getByText('Open local folder')) // Menu should close after clicking an action expect(screen.queryByText('Open local folder')).not.toBeInTheDocument() @@ -225,27 +238,27 @@ describe('StatusBar', () => { expect(onClickPending).toHaveBeenCalledOnce() }) - it('pending count has title for accessibility', () => { + it('pending changes tooltip is available on keyboard focus', async () => { render( ) - expect(screen.getByTitle('View pending changes')).toBeInTheDocument() + await expectTooltip(screen.getByRole('button', { name: 'View pending changes' }), 'View pending changes') }) - it('shows MCP warning badge when status is not_installed', () => { + it('shows MCP warning badge when status is not_installed', async () => { render( ) expect(screen.getByTestId('status-mcp')).toBeInTheDocument() - expect(screen.getByTitle('MCP server not installed — click to install')).toBeInTheDocument() + await expectTooltip(screen.getByRole('button', { name: 'MCP server not installed — click to install' }), 'MCP server not installed — click to install') }) - it('shows MCP warning badge when status is no_claude_cli', () => { + it('shows MCP warning badge when status is no_claude_cli', async () => { render( ) expect(screen.getByTestId('status-mcp')).toBeInTheDocument() - expect(screen.getByTitle('Claude CLI not found — install it first')).toBeInTheDocument() + await expectTooltip(screen.getByRole('button', { name: 'Claude CLI not found — install it first' }), 'Claude CLI not found — install it first') }) it('hides MCP badge when status is installed', () => { @@ -378,7 +391,7 @@ describe('StatusBar', () => { expect(onCommitPush).toHaveBeenCalledOnce() }) - it('uses a local-only tooltip for the commit button when no remote is configured', () => { + it('uses a local-only tooltip for the commit button when no remote is configured', async () => { render( { remoteStatus={{ branch: 'main', ahead: 0, behind: 0, hasRemote: false }} /> ) - expect(screen.getByTestId('status-commit-push')).toHaveAttribute('title', 'Commit locally (no remote configured)') + await expectTooltip(screen.getByRole('button', { name: 'Commit changes locally' }), 'Commit changes locally') }) it('shows Commit button even when no modified files', () => { @@ -403,20 +416,20 @@ describe('StatusBar', () => { expect(screen.queryByTestId('status-commit-push')).not.toBeInTheDocument() }) - it('shows Claude Code badge when installed', () => { + it('shows Claude Code badge when installed', async () => { render() const badge = screen.getByTestId('status-claude-code') expect(badge).toBeInTheDocument() expect(screen.getByText('Claude Code')).toBeInTheDocument() - expect(badge.getAttribute('title')).toBe('Claude Code 1.0.20') + await expectTooltip(screen.getByRole('button', { name: 'Claude Code 1.0.20' }), 'Claude Code 1.0.20') }) - it('shows Claude Code missing badge with warning when missing', () => { + it('shows Claude Code missing badge with warning when missing', async () => { render() const badge = screen.getByTestId('status-claude-code') expect(badge).toBeInTheDocument() expect(screen.getByText('Claude Code missing')).toBeInTheDocument() - expect(badge.getAttribute('title')).toBe('Claude Code not found — click to install') + await expectTooltip(screen.getByRole('button', { name: 'Claude Code not found — click to install' }), 'Claude Code not found — click to install') }) it('opens install URL when clicking missing Claude Code badge', () => { diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx index cfde2aa1..884baee0 100644 --- a/src/components/StatusBar.tsx +++ b/src/components/StatusBar.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react' import type { ClaudeCodeStatus } from '../hooks/useClaudeCodeStatus' import type { McpStatus } from '../hooks/useMcpStatus' import type { GitRemoteStatus, SyncStatus } from '../types' +import { TooltipProvider } from '@/components/ui/tooltip' import { StatusBarPrimarySection, StatusBarSecondarySection, @@ -97,62 +98,64 @@ export function StatusBar({ }, []) return ( -
- - -
+ +
+ + +
+
) } diff --git a/src/components/status-bar/AiAgentsBadge.test.tsx b/src/components/status-bar/AiAgentsBadge.test.tsx index 0a0ee7ea..5b8b40e0 100644 --- a/src/components/status-bar/AiAgentsBadge.test.tsx +++ b/src/components/status-bar/AiAgentsBadge.test.tsx @@ -1,5 +1,7 @@ -import { act, fireEvent, render, screen } from '@testing-library/react' +import { act, fireEvent, render as rtlRender, screen } from '@testing-library/react' +import type { ReactElement } from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { TooltipProvider } from '@/components/ui/tooltip' import { AiAgentsBadge } from './AiAgentsBadge' vi.mock('../../utils/url', async () => { @@ -12,6 +14,10 @@ const installedStatuses = { codex: { status: 'installed' as const, version: '0.37.0' }, } +function render(ui: ReactElement) { + return rtlRender(ui, { wrapper: TooltipProvider }) +} + describe('AiAgentsBadge', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/src/components/status-bar/AiAgentsBadge.tsx b/src/components/status-bar/AiAgentsBadge.tsx index 33609c54..32b8e51b 100644 --- a/src/components/status-bar/AiAgentsBadge.tsx +++ b/src/components/status-bar/AiAgentsBadge.tsx @@ -1,4 +1,5 @@ import { AlertTriangle, ChevronsUpDown, Terminal } from 'lucide-react' +import { ActionTooltip } from '@/components/ui/action-tooltip' import { Button } from '@/components/ui/button' import { AI_AGENT_DEFINITIONS, @@ -199,23 +200,25 @@ export function AiAgentsBadge({ <> | - - - + + + + + > = { const CLAUDE_INSTALL_URL = 'https://docs.anthropic.com/en/docs/claude-code' -type HoverHandlers = { - onMouseEnter?: (event: ReactMouseEvent) => void - onMouseLeave?: (event: ReactMouseEvent) => void -} - -interface ClaudeCodeRenderState extends HoverHandlers { - role?: 'button' - tabIndex?: number - onKeyDown?: (event: ReactKeyboardEvent) => void - color: string - cursor: string - warningIcon: ReactNode -} - -function createHoverHandlers(interactive: boolean): HoverHandlers { - if (!interactive) { - return { - onMouseEnter: undefined, - onMouseLeave: undefined, - } - } - - return { - onMouseEnter: (event) => { - event.currentTarget.style.background = 'var(--hover)' - }, - onMouseLeave: (event) => { - event.currentTarget.style.background = 'transparent' - }, - } -} - -function createEnterKeyHandler(onActivate?: () => void) { - return (event: ReactKeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - onActivate?.() - } - } -} - function formatElapsedSync(lastSyncTime: number | null): string { if (!lastSyncTime) return 'Not synced' const secs = Math.round((Date.now() - lastSyncTime) / 1000) @@ -103,11 +60,12 @@ function syncIconColor(status: SyncStatus): string { return SYNC_COLORS[status] ?? 'var(--accent-green)' } -function syncBadgeTitle(status: SyncStatus): string { - if (status === 'conflict') return 'Click to resolve conflicts' - if (status === 'syncing') return 'Syncing…' - if (status === 'pull_required') return 'Click to pull from remote and push' - return 'Click to sync now' +function syncBadgeTooltipCopy(status: SyncStatus): ActionTooltipCopy { + if (status === 'conflict') return { label: 'Resolve merge conflicts' } + if (status === 'syncing') return { label: 'Sync in progress' } + if (status === 'pull_required') return { label: 'Pull from remote and push' } + if (status === 'error') return { label: 'Retry sync' } + return { label: 'Sync now' } } function syncStatusText(status: SyncStatus): string { @@ -127,10 +85,12 @@ function isRemoteMissing(remoteStatus: GitRemoteStatus | null | undefined): bool return remoteStatus?.hasRemote === false } -function commitButtonTitle(remoteStatus: GitRemoteStatus | null | undefined): string { - return isRemoteMissing(remoteStatus) - ? 'Commit locally (no remote configured)' - : 'Commit & Push' +function commitButtonTooltipCopy(remoteStatus: GitRemoteStatus | null | undefined): ActionTooltipCopy { + return { + label: isRemoteMissing(remoteStatus) + ? 'Commit changes locally' + : 'Commit and push changes', + } } function getMcpBadgeConfig(status: McpStatus, onInstall?: () => void) { @@ -154,30 +114,57 @@ function getClaudeCodeBadgeConfig(status: ClaudeCodeStatus, version?: string | n } } -function createClaudeCodeRenderState( - config: NonNullable>, -): ClaudeCodeRenderState { - if (!config.missing) { - return { - role: undefined, - tabIndex: undefined, - onKeyDown: undefined, - color: 'var(--muted-foreground)', - cursor: 'default', - warningIcon: null, - ...createHoverHandlers(false), - } - } +function handleStatusBarActionKeyDown( + event: ReactKeyboardEvent, + onClick?: () => void, +) { + if (!onClick) return + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + onClick() +} - return { - role: 'button', - tabIndex: 0, - onKeyDown: createEnterKeyHandler(config.onActivate), - color: 'var(--accent-orange)', - cursor: 'pointer', - warningIcon: , - ...createHoverHandlers(true), - } +function StatusBarAction({ + copy, + children, + onClick, + testId, + ariaLabel, + className, + style, + disabled = false, +}: { + copy: ActionTooltipCopy + children: ReactNode + onClick?: () => void + testId?: string + ariaLabel?: string + className?: string + style?: CSSProperties + disabled?: boolean +}) { + return ( + + + + ) } function RemoteStatusSummary({ remoteStatus }: { remoteStatus: GitRemoteStatus | null }) { @@ -405,16 +392,12 @@ export function SyncBadge({ return (
- - - {formatSyncLabel(status, lastSyncTime)} - + + + + {formatSyncLabel(status, lastSyncTime)} + + {showPopup && ( | - { event.currentTarget.style.background = 'var(--hover)' } : undefined} - onMouseLeave={onClick ? (event) => { event.currentTarget.style.background = 'transparent' } : undefined} - data-testid="status-conflict-count" + testId="status-conflict-count" + className="text-[var(--destructive,#e03e3e)]" > - - {count} conflict{count > 1 ? 's' : ''} - + + + {count} conflict{count > 1 ? 's' : ''} + + ) } @@ -462,35 +437,29 @@ export function ChangesBadge({ count, onClick }: { count: number; onClick?: () = return ( <> | - { event.currentTarget.style.background = 'var(--hover)' }} - onMouseLeave={(event) => { event.currentTarget.style.background = 'transparent' }} - data-testid="status-modified-count" - > - - - {count} + + + + + {count} + + Changes - Changes - + ) } @@ -507,20 +476,12 @@ export function CommitButton({ return ( <> | - { event.currentTarget.style.background = 'var(--hover)' }} - onMouseLeave={(event) => { event.currentTarget.style.background = 'transparent' }} - data-testid="status-commit-push" - > - - Commit - + + + + Commit + + ) } @@ -529,25 +490,17 @@ export function PulseBadge({ onClick, disabled }: { onClick?: () => void; disabl return ( <> | - { event.currentTarget.style.background = 'var(--hover)' }} - onMouseLeave={disabled ? undefined : (event) => { event.currentTarget.style.background = 'transparent' }} - data-testid="status-pulse" + testId="status-pulse" + disabled={Boolean(disabled)} > - - History - + + + History + + ) } @@ -559,26 +512,18 @@ export function McpBadge({ status, onInstall }: { status: McpStatus; onInstall?: return ( <> | - { event.currentTarget.style.background = 'var(--hover)' } : undefined} - onMouseLeave={config.clickable ? (event) => { event.currentTarget.style.background = 'transparent' } : undefined} + testId="status-mcp" + className="text-[var(--accent-orange)]" > - - MCP - - + + + MCP + + + ) } @@ -587,33 +532,21 @@ export function ClaudeCodeBadge({ status, version }: { status: ClaudeCodeStatus; const config = getClaudeCodeBadgeConfig(status, version) if (!config) return null - const renderState = createClaudeCodeRenderState(config) - return ( <> | - - - {config.label} - {renderState.warningIcon} - + + + {config.label} + {config.missing && } + + ) } diff --git a/src/components/status-bar/StatusBarSections.tsx b/src/components/status-bar/StatusBarSections.tsx index 970cb71e..0e7205c6 100644 --- a/src/components/status-bar/StatusBarSections.tsx +++ b/src/components/status-bar/StatusBarSections.tsx @@ -5,6 +5,7 @@ import type { VaultAiGuidanceStatus } from '../../lib/vaultAiGuidance' import type { ClaudeCodeStatus } from '../../hooks/useClaudeCodeStatus' import type { McpStatus } from '../../hooks/useMcpStatus' import type { GitRemoteStatus, SyncStatus } from '../../types' +import { ActionTooltip } from '@/components/ui/action-tooltip' import { AiAgentsBadge } from './AiAgentsBadge' import { Button } from '@/components/ui/button' import { @@ -22,6 +23,12 @@ import { DISABLED_STYLE, ICON_STYLE, SEP_STYLE } from './styles' import type { VaultOption } from './types' import { VaultMenu } from './VaultMenu' +const UPDATE_TOOLTIP = { label: 'Check for updates' } as const +const ZOOM_RESET_TOOLTIP = { label: 'Reset the zoom level', shortcut: '⌘0' } as const +const FEEDBACK_TOOLTIP = { label: 'Share feedback' } as const +const NOTIFICATIONS_TOOLTIP = { label: 'Notifications are coming soon' } as const +const SETTINGS_TOOLTIP = { label: 'Open settings', shortcut: '⌘,' } as const + interface StatusBarPrimarySectionProps { modifiedCount: number vaultPath: string @@ -109,18 +116,23 @@ export function StatusBarPrimarySection({ onRemoveVault={onRemoveVault} /> | - { event.currentTarget.style.background = 'var(--hover)' } : undefined} - onMouseLeave={onCheckForUpdates ? (event) => { event.currentTarget.style.background = 'transparent' } : undefined} - > - - {buildNumber ?? 'b?'} - + + + @@ -163,45 +175,54 @@ export function StatusBarSecondarySection({ return (
{zoomLevel === 100 ? null : ( - { event.currentTarget.style.background = 'var(--hover)' }} - onMouseLeave={(event) => { event.currentTarget.style.background = 'transparent' }} - data-testid="status-zoom" - > - {zoomLevel}% - + + + )} {onOpenFeedback && ( + + + + )} + + + + + + - )} - - - - { event.currentTarget.style.background = 'var(--hover)' }} - onMouseLeave={(event) => { event.currentTarget.style.background = 'transparent' }} - > - - +
) } diff --git a/src/components/status-bar/VaultMenu.tsx b/src/components/status-bar/VaultMenu.tsx index 536cac0f..2e3fa2ae 100644 --- a/src/components/status-bar/VaultMenu.tsx +++ b/src/components/status-bar/VaultMenu.tsx @@ -1,6 +1,8 @@ import { useMemo, useRef, useState } from 'react' import type { CSSProperties, ReactNode } from 'react' import { AlertTriangle, Check, FolderOpen, GitBranch, Rocket, X } from 'lucide-react' +import { ActionTooltip } from '@/components/ui/action-tooltip' +import { Button } from '@/components/ui/button' import type { VaultOption } from './types' import { useDismissibleLayer } from './useDismissibleLayer' @@ -204,23 +206,22 @@ export function VaultMenu({ return (
- setOpen((value) => !value)} - 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 && (
['side'] + sideOffset?: number +} + +export function ActionTooltip({ + copy, + children, + className, + side = 'top', + sideOffset = 6, +}: ActionTooltipProps) { + return ( + + {children} + +
+ {copy.label} + {copy.shortcut && ( + + {copy.shortcut} + + )} +
+
+
+ ) +}