feat(ui): enhance status panel actions and styling

This update significantly refactors the visual layout and functionality of the working tree status panel. It introduces dedicated action groups for discarding changes, staging, and unstaging files, improving user interaction and clarity.

- Added contextual action buttons (discard all/selected) to both staged and unstaged sections.
- Updated CSS structure to accommodate new status action containers.
- Improved handling of selected file counts within the panel headers.
This commit is contained in:
Christoph Brandau
2026-07-13 00:09:28 +02:00
parent 82c47c8d03
commit 0dd5441754
2 changed files with 80 additions and 58 deletions
+40
View File
@@ -6028,3 +6028,43 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.ai-review-footer { align-items: stretch; flex-direction: column; }
.ai-review-footer > div { justify-content: flex-end; }
}
/* Contextual status actions: global discard in the panel header, selection
actions beside their matching Stage/Unstage command. */
.status-head-actions,
.status-lane-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
min-width: 0;
}
.status-head-actions { flex-wrap: wrap; }
.status-discard-all {
margin-left: 3px;
border-color: color-mix(in srgb, var(--code-delete-strong) 30%, var(--color-border));
color: var(--code-delete-strong);
background: color-mix(in srgb, var(--code-delete-strong) 8%, transparent);
}
.status-discard-all:hover:not(:disabled) {
border-color: color-mix(in srgb, var(--code-delete-strong) 52%, var(--color-border));
color: var(--code-delete-strong);
background: color-mix(in srgb, var(--code-delete-strong) 14%, transparent);
}
.status-lane-title { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; }
.status-lane-actions { flex-wrap: wrap; }
.status-lane-actions .status-selection-count {
display: inline-flex;
width: auto;
min-width: 0;
height: 22px;
padding: 0 5px;
color: var(--color-ink-dim);
background: transparent;
font-size: 9.5px;
}
@media (max-width: 760px) {
.status-lane-head { align-items: flex-start; flex-direction: column; }
.status-lane-actions { width: 100%; justify-content: flex-start; }
}
+33 -51
View File
@@ -144,7 +144,6 @@
let hasUnstaged = $derived(changedFiles.some((f) => f.unstaged !== null));
let hasStaged = $derived(changedFiles.some((f) => f.staged !== null));
let selectedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f))).length);
let selectedUnstagedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f)) && f.unstaged !== null).length);
let selectedStagedCount = $derived(changedFiles.filter((f) => selectedStatusPaths.has(fileKey(f)) && f.staged !== null).length);
@@ -156,65 +155,22 @@
});
</script>
<section class="panel status-panel relative grid grid-rows-[auto_auto_1fr] overflow-hidden" aria-label="Working tree status">
<section class="panel status-panel relative grid grid-rows-[auto_1fr] overflow-hidden" aria-label="Working tree status">
<div class="section-head">
<div>
<span class="eyebrow">Workspace</span>
<h2 class="mt-0.5 text-ink text-base font-bold leading-tight">Changes</h2>
</div>
<div class="flex items-center gap-1.5">
<div class="status-head-actions">
<span class="pill pill-count">{stagedCount} staged</span>
<span class="pill pill-count">{unstagedCount} unstaged</span>
</div>
</div>
{#if hasRepository && changedFiles.length > 0}
<div class="status-toolbar">
<button
class="btn-sm danger"
type="button"
onclick={() => onDiscardMany(changedFiles)}
disabled={isBusy || changedFiles.length === 0}
title="Discard all changes"
>
<RotateCcw size={14} aria-hidden="true" />
Discard all
</button>
{#if selectedCount > 1}
<span class="status-selection-count">{selectedCount} selected</span>
<button
class="btn-sm"
type="button"
onclick={() => onStage(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.unstaged !== null))}
disabled={isBusy || selectedUnstagedCount === 0}
title="Stage selected unstaged files"
>
<Check size={14} aria-hidden="true" />
Stage selected
</button>
<button
class="btn-sm"
type="button"
onclick={() => onUnstage(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.staged !== null))}
disabled={isBusy || selectedStagedCount === 0}
title="Unstage selected staged files"
>
<Undo2 size={14} aria-hidden="true" />
Unstage selected
</button>
<button
class="btn-sm danger"
type="button"
onclick={() => onDiscardMany(selectedFiles())}
disabled={isBusy || selectedCount === 0}
title="Discard changes in selected files"
>
<RotateCcw size={14} aria-hidden="true" />
Discard selected
<button class="btn-sm status-discard-all" type="button" onclick={() => onDiscardMany(changedFiles)} disabled={isBusy} title="Discard all staged and unstaged changes">
<RotateCcw size={13} aria-hidden="true" /> Discard all
</button>
{/if}
</div>
{/if}
</div>
{#if !hasRepository}
<div class="blank-state">No repository loaded.</div>
@@ -226,10 +182,23 @@
<div class="status-lanes overflow-auto">
<section class="status-lane" aria-label="Unstaged changes">
<header class="status-lane-head">
<div><strong>Unstaged</strong><span>{unstagedCount}</span></div>
<div class="status-lane-title"><strong>Unstaged</strong><span>{unstagedCount}</span></div>
<div class="status-lane-actions">
{#if selectedUnstagedCount > 1}
<span class="status-selection-count">{selectedUnstagedCount} selected</span>
<button class="btn-sm" type="button" onclick={() => onStage(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.unstaged !== null))} disabled={isBusy} title={`Stage ${selectedUnstagedCount} selected files`}>
<Check size={13} aria-hidden="true" /> Stage {selectedUnstagedCount}
</button>
{/if}
<button class="btn-sm" type="button" onclick={onStageAll} disabled={isBusy || !hasUnstaged} title="Stage all unstaged files">
<Check size={13} aria-hidden="true" /> Stage all
</button>
{#if selectedUnstagedCount > 1}
<button class="btn-sm danger" type="button" onclick={() => onDiscard(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.unstaged !== null), false)} disabled={isBusy} title={`Discard unstaged changes in ${selectedUnstagedCount} selected files`}>
<RotateCcw size={13} aria-hidden="true" /> Discard {selectedUnstagedCount}
</button>
{/if}
</div>
</header>
<div class="status-file-list">
{#each changedFiles.filter((file) => file.unstaged !== null) as file (`unstaged:${fileKey(file)}`)}
@@ -253,10 +222,23 @@
<section class="status-lane" aria-label="Staged changes">
<header class="status-lane-head">
<div><strong>Staged</strong><span>{stagedCount}</span></div>
<div class="status-lane-title"><strong>Staged</strong><span>{stagedCount}</span></div>
<div class="status-lane-actions">
{#if selectedStagedCount > 1}
<span class="status-selection-count">{selectedStagedCount} selected</span>
<button class="btn-sm" type="button" onclick={() => onUnstage(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.staged !== null))} disabled={isBusy} title={`Unstage ${selectedStagedCount} selected files`}>
<Undo2 size={13} aria-hidden="true" /> Unstage {selectedStagedCount}
</button>
{/if}
<button class="btn-sm" type="button" onclick={onUnstageAll} disabled={isBusy || !hasStaged} title="Unstage all staged files">
<Undo2 size={13} aria-hidden="true" /> Unstage all
</button>
{#if selectedStagedCount > 1}
<button class="btn-sm danger" type="button" onclick={() => onDiscard(changedFiles.filter((file) => selectedStatusPaths.has(fileKey(file)) && file.staged !== null), true)} disabled={isBusy} title={`Discard staged changes in ${selectedStagedCount} selected files`}>
<RotateCcw size={13} aria-hidden="true" /> Discard {selectedStagedCount}
</button>
{/if}
</div>
</header>
<div class="status-file-list">
{#each changedFiles.filter((file) => file.staged !== null) as file (`staged:${fileKey(file)}`)}