fix: better icon buttons for git

This commit is contained in:
2026-02-24 06:31:11 +01:00
parent ddec5bd2e4
commit 3488130953
3 changed files with 180 additions and 10 deletions

View File

@@ -36,6 +36,61 @@
padding: 0 12px 8px;
}
.git-sidebar-icon-button {
width: 20px;
height: 20px;
padding: 0;
}
.git-sidebar-icon-button svg {
width: 16px;
height: 16px;
}
.git-action-branch-line,
.git-action-stem,
.git-action-arrow,
.git-action-prune-mark {
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
}
.git-action-branch-line {
stroke-width: 1.3;
opacity: 0.9;
}
.git-action-branch-dot {
fill: currentColor;
}
.git-action-stem {
stroke-width: 1.4;
}
.git-action-stem--dotted {
stroke-dasharray: 1.4 1.8;
}
.git-action-stem--solid {
stroke-dasharray: none;
}
.git-action-arrow {
stroke-width: 1.4;
}
.git-action-prune-mark {
stroke-width: 1.3;
}
.git-sidebar-icon-button:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.git-sidebar-section {
display: flex;
flex-direction: column;

View File

@@ -491,35 +491,93 @@ export const GitSidebar: React.FC = () => {
<div className="git-sidebar-actions" role="group" aria-label={tr('gitSidebar.aria.repoActions')}>
<button
type="button"
className="git-sidebar-button"
className="sidebar-action git-sidebar-icon-button"
onClick={() => handleRepoAction('fetch')}
disabled={actionLoading !== null}
aria-label={tr('gitSidebar.action.fetch')}
title={tr('gitSidebar.action.fetch')}
>
{actionLoading === 'fetch' ? tr('gitSidebar.action.fetching') : tr('gitSidebar.action.fetch')}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
focusable="false"
data-testid="git-action-icon-fetch"
>
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
<line x1="8" y1="4.5" x2="8" y2="10.2" className="git-action-stem git-action-stem--dotted" data-testid="git-action-stem-fetch" />
<path d="M8 10.2l-2-2" className="git-action-arrow git-action-arrow--towards-branch" data-testid="git-action-arrow-fetch" />
<path d="M8 10.2l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
</svg>
</button>
<button
type="button"
className="git-sidebar-button"
className="sidebar-action git-sidebar-icon-button"
onClick={() => handleRepoAction('pull')}
disabled={actionLoading !== null}
aria-label={tr('gitSidebar.action.pull')}
title={tr('gitSidebar.action.pull')}
>
{actionLoading === 'pull' ? tr('gitSidebar.action.pulling') : tr('gitSidebar.action.pull')}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
focusable="false"
data-testid="git-action-icon-pull"
>
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
<line x1="8" y1="4.5" x2="8" y2="10.2" className="git-action-stem git-action-stem--solid" data-testid="git-action-stem-pull" />
<path d="M8 10.2l-2-2" className="git-action-arrow git-action-arrow--towards-branch" data-testid="git-action-arrow-pull" />
<path d="M8 10.2l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
</svg>
</button>
<button
type="button"
className="git-sidebar-button"
className="sidebar-action git-sidebar-icon-button"
onClick={() => handleRepoAction('push')}
disabled={actionLoading !== null}
aria-label={tr('gitSidebar.action.push')}
title={tr('gitSidebar.action.push')}
>
{actionLoading === 'push' ? tr('gitSidebar.action.pushing') : tr('gitSidebar.action.push')}
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
focusable="false"
data-testid="git-action-icon-push"
>
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" data-testid="git-action-branch-line" />
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" data-testid="git-action-branch-dot" />
<line x1="8" y1="10.2" x2="8" y2="4.5" className="git-action-stem git-action-stem--solid" data-testid="git-action-stem-push" />
<path d="M8 4.5l-2 2" className="git-action-arrow git-action-arrow--away-branch" data-testid="git-action-arrow-push" />
<path d="M8 4.5l2 2" className="git-action-arrow git-action-arrow--away-branch" />
</svg>
</button>
<button
type="button"
className="git-sidebar-button"
className="sidebar-action git-sidebar-icon-button"
onClick={() => handleRepoAction('prune-lfs')}
disabled={actionLoading !== null}
aria-label={tr('gitSidebar.action.pruneLfs')}
title={tr('gitSidebar.action.pruneLfs')}
>
{actionLoading === 'prune-lfs' ? tr('gitSidebar.action.pruning') : tr('gitSidebar.action.pruneLfs')}
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" focusable="false">
<line x1="2" y1="13" x2="14" y2="13" className="git-action-branch-line" />
<circle cx="8" cy="13" r="1.6" className="git-action-branch-dot" />
<line x1="8" y1="4.5" x2="8" y2="9.8" className="git-action-stem git-action-stem--solid" />
<path d="M8 9.8l-2-2" className="git-action-arrow git-action-arrow--towards-branch" />
<path d="M8 9.8l2-2" className="git-action-arrow git-action-arrow--towards-branch" />
<path d="M10.8 4.2l2 2" className="git-action-prune-mark" />
<path d="M12.8 4.2l-2 2" className="git-action-prune-mark" />
</svg>
</button>
</div>
{actionLoading && (

View File

@@ -661,6 +661,63 @@ describe('GitSidebar', () => {
});
});
it('renders repo actions as icon-only buttons with hover tooltips', async () => {
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
isRepo: true,
rootPath: '/repo/path',
currentBranch: 'main',
hasRemote: true,
});
render(<GitSidebar />);
const repoActions = await screen.findByRole('group', { name: /repository actions/i });
const fetchButton = within(repoActions).getByRole('button', { name: /^fetch$/i });
const pullButton = within(repoActions).getByRole('button', { name: /^pull$/i });
const pushButton = within(repoActions).getByRole('button', { name: /^push$/i });
const pruneButton = within(repoActions).getByRole('button', { name: /^prune lfs$/i });
expect(fetchButton).toHaveAttribute('title', 'Fetch');
expect(pullButton).toHaveAttribute('title', 'Pull');
expect(pushButton).toHaveAttribute('title', 'Push');
expect(pruneButton).toHaveAttribute('title', 'Prune LFS');
expect(within(repoActions).queryByText('Fetch')).not.toBeInTheDocument();
expect(within(repoActions).queryByText('Pull')).not.toBeInTheDocument();
expect(within(repoActions).queryByText('Push')).not.toBeInTheDocument();
expect(within(repoActions).queryByText('Prune LFS')).not.toBeInTheDocument();
});
it('uses unified branch baseline icons with action-specific arrow direction styles', async () => {
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
isRepo: true,
rootPath: '/repo/path',
currentBranch: 'main',
hasRemote: true,
});
render(<GitSidebar />);
const repoActions = await screen.findByRole('group', { name: /repository actions/i });
const fetchButton = within(repoActions).getByRole('button', { name: /^fetch$/i });
const pullButton = within(repoActions).getByRole('button', { name: /^pull$/i });
const pushButton = within(repoActions).getByRole('button', { name: /^push$/i });
const fetchIcon = within(fetchButton).getByTestId('git-action-icon-fetch');
expect(within(fetchIcon).getByTestId('git-action-branch-line')).toBeInTheDocument();
expect(within(fetchIcon).getByTestId('git-action-branch-dot')).toBeInTheDocument();
expect(within(fetchIcon).getByTestId('git-action-stem-fetch')).toHaveClass('git-action-stem--dotted');
expect(within(fetchIcon).getByTestId('git-action-arrow-fetch')).toHaveClass('git-action-arrow--towards-branch');
const pullIcon = within(pullButton).getByTestId('git-action-icon-pull');
expect(within(pullIcon).getByTestId('git-action-stem-pull')).toHaveClass('git-action-stem--solid');
expect(within(pullIcon).getByTestId('git-action-arrow-pull')).toHaveClass('git-action-arrow--towards-branch');
const pushIcon = within(pushButton).getByTestId('git-action-icon-push');
expect(within(pushIcon).getByTestId('git-action-stem-push')).toHaveClass('git-action-stem--solid');
expect(within(pushIcon).getByTestId('git-action-arrow-push')).toHaveClass('git-action-arrow--away-branch');
});
it('shows in-progress feedback while prune lfs is running', async () => {
let resolvePrune: ((value: { success: boolean; dryRun: boolean; verifyRemote: boolean; recentCommitsToKeep: number }) => void) | null = null;
(window as any).electronAPI.git.getRepoState = vi.fn().mockResolvedValue({
@@ -684,7 +741,7 @@ describe('GitSidebar', () => {
});
expect(screen.getByRole('status')).toHaveTextContent(/pruning local git lfs cache/i);
expect(screen.getByRole('button', { name: /pruning/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /prune lfs/i })).toBeDisabled();
await act(async () => {
resolvePrune?.({ success: true, dryRun: false, verifyRemote: true, recentCommitsToKeep: 2 });
@@ -818,7 +875,7 @@ describe('GitSidebar', () => {
});
expect(screen.getByRole('status')).toHaveTextContent(/pushing commits to remote/i);
expect(screen.getByRole('button', { name: /pushing/i })).toBeDisabled();
expect(screen.getByRole('button', { name: /^push$/i })).toBeDisabled();
await act(async () => {
resolvePush?.({ success: true });