feat(status-panel): overhaul status lanes and icons

The status panel now uses a grid-based layout with a flow divider
between unstaged and staged sections and clearer headers. Icons
were swapped to arrows for stage/unstage actions, and labels were
updated to show Working tree and Next commit for clarity.

- Replace action icons with arrows to indicate staging
- Redesign lanes and headers for a compact, responsive UI
- Add a divider and contextual labels for unstaged and staged sections
This commit is contained in:
Christoph Brandau
2026-08-17 21:30:53 +02:00
parent cd01df0108
commit fac22ff173
2 changed files with 128 additions and 17 deletions
+106 -5
View File
@@ -6191,20 +6191,97 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
background: var(--color-surface-dim);
}
.status-toolbar .danger { margin-left: auto; }
.status-lanes { display: grid; align-content: start; min-height: 0; }
.status-lane + .status-lane { border-top: 1px solid var(--color-border); }
.status-panel { container-type: inline-size; }
.status-lanes {
display: grid;
grid-template-columns: minmax(0, 1fr) 25px minmax(280px, 1fr);
align-items: stretch;
min-height: 0;
overflow: hidden;
background: var(--color-border-subtle);
}
.status-lane {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
min-width: 0;
min-height: 0;
overflow: hidden;
background: var(--color-surface-solid);
}
.status-lane.staged-lane {
background:
linear-gradient(180deg, color-mix(in srgb, #2da44e 2.5%, transparent), transparent 120px),
var(--color-surface-solid);
}
.status-flow-divider {
position: relative;
display: grid;
place-items: center;
min-width: 0;
color: var(--color-primary);
background: var(--color-surface-dim);
}
.status-flow-divider::before {
content: "";
position: absolute;
inset-block: 0;
left: 50%;
width: 1px;
background: var(--color-border);
}
.status-flow-divider span {
position: relative;
z-index: 1;
display: grid;
place-items: center;
width: 21px;
height: 21px;
border: 1px solid color-mix(in srgb, var(--color-primary) 28%, var(--color-border));
border-radius: 50%;
background: var(--color-surface-solid);
box-shadow: 0 0 0 4px var(--color-surface-dim);
}
.status-lane-head {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 42px;
padding: 5px 12px;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-surface-dim);
}
.status-lane-head > div { display: flex; align-items: center; gap: 7px; }
.status-lane-head strong { color: var(--color-ink); font-size: 12px; }
.status-lane-head span { display: grid; place-items: center; min-width: 20px; height: 20px; border-radius: 5px; color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 10%, transparent); font-size: 10.5px; font-weight: 750; }
.status-file-list { padding: 5px 0; }
.status-lane-copy { display: grid; gap: 1px; }
.status-lane-copy small {
color: var(--color-ink-faint);
font-size: 8px;
font-weight: 700;
letter-spacing: .08em;
line-height: 1;
text-transform: uppercase;
}
.status-lane-count {
display: grid;
place-items: center;
min-width: 20px;
height: 20px;
border-radius: 5px;
color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 10%, transparent);
font-size: 10.5px;
font-weight: 750;
}
.staged-lane .status-lane-count {
color: #55bd72;
background: color-mix(in srgb, #2da44e 12%, transparent);
}
.status-file-list {
min-height: 0;
padding: 5px 0;
overflow: auto;
scrollbar-gutter: stable;
}
.status-file-row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
@@ -6227,7 +6304,16 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
.status-file-row:hover .status-file-actions, .status-file-row:focus-within .status-file-actions { opacity: 1; }
.status-file-actions button { width: 28px; min-width: 28px; min-height: 28px; padding: 0; border-color: transparent; background: transparent; }
.status-file-actions .danger:hover:not(:disabled) { color: #e86060; background: rgba(232,96,96,.1); }
.status-lane-empty { margin: 0; padding: 14px 16px; color: var(--color-ink-faint); font-size: 11.5px; }
.status-lane-empty {
display: grid;
place-items: center;
min-height: 86px;
margin: 0;
padding: 14px 16px;
color: var(--color-ink-faint);
font-size: 11.5px;
text-align: center;
}
.status-badge { min-width: 58px; padding-inline: 5px; border: 0; border-radius: 4px; font-size: 9px; letter-spacing: .04em; }
.commit-panel { border-top: 1px solid var(--color-border); }
@@ -7457,6 +7543,21 @@ input:focus, textarea:focus, select:focus { box-shadow: 0 0 0 3px color-mix(in s
font-size: 9.5px;
}
@container (max-width: 680px) {
.status-lanes {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: minmax(0, 1fr) 25px minmax(0, 1fr);
}
.status-flow-divider::before {
inset-block: auto;
inset-inline: 0;
top: 50%;
width: auto;
height: 1px;
}
.status-flow-divider span { transform: rotate(90deg); }
}
@media (max-width: 760px) {
.status-lane-head { align-items: flex-start; flex-direction: column; }
.status-lane-actions { width: 100%; justify-content: flex-start; }
+22 -12
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { Check, FileDiff, RotateCcw, Undo2 } from "@lucide/svelte";
import { ArrowLeft, ArrowRight, FileDiff, RotateCcw } from "@lucide/svelte";
import iconUrl from "../../../src-tauri/icons/icon.png";
import type { FileStatusKind, GitFileStatus, GitStatus } from "../types";
@@ -181,19 +181,22 @@
{:else if changedFiles.length === 0}
<div class="blank-state">No file changes returned.</div>
{:else}
<div class="status-lanes overflow-auto">
<section class="status-lane" aria-label="Unstaged changes">
<div class="status-lanes">
<section class="status-lane unstaged-lane" aria-label="Unstaged changes">
<header class="status-lane-head">
<div class="status-lane-title"><strong>Unstaged</strong><span>{unstagedCount}</span></div>
<div class="status-lane-title">
<div class="status-lane-copy"><strong>Unstaged</strong><small>Working tree</small></div>
<span class="status-lane-count">{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}
<ArrowRight 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
<ArrowRight 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`}>
@@ -212,7 +215,7 @@
</button>
<span class={`status-badge ${file.unstaged ?? "none"}`}>{statusLabel(file.unstaged)}</span>
<div class="status-file-actions">
<button type="button" onclick={() => stageFromFile(file)} disabled={isBusy || stageTargets.length === 0} title="Stage file"><Check size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => stageFromFile(file)} disabled={isBusy || stageTargets.length === 0} title="Stage file"><ArrowRight size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => onPatch(file, false)} disabled={isBusy || !canPatch(file.unstaged)} title="Show unstaged details"><FileDiff size={14} aria-hidden="true" /></button>
<button class="danger" type="button" onclick={() => discardUnstagedFromFile(file)} disabled={isBusy || stageTargets.length === 0} title="Discard unstaged changes"><RotateCcw size={14} aria-hidden="true" /></button>
</div>
@@ -222,18 +225,25 @@
</div>
</section>
<section class="status-lane" aria-label="Staged changes">
<div class="status-flow-divider" aria-hidden="true">
<span><ArrowRight size={12} /></span>
</div>
<section class="status-lane staged-lane" aria-label="Staged changes">
<header class="status-lane-head">
<div class="status-lane-title"><strong>Staged</strong><span>{stagedCount}</span></div>
<div class="status-lane-title">
<div class="status-lane-copy"><strong>Staged</strong><small>Next commit</small></div>
<span class="status-lane-count">{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}
<ArrowLeft 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
<ArrowLeft 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`}>
@@ -252,7 +262,7 @@
</button>
<span class={`status-badge ${file.staged ?? "none"}`}>{statusLabel(file.staged)}</span>
<div class="status-file-actions">
<button type="button" onclick={() => unstageFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title="Unstage file"><Undo2 size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => unstageFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title="Unstage file"><ArrowLeft size={14} aria-hidden="true" /></button>
<button type="button" onclick={() => onPatch(file, true)} disabled={isBusy || !canPatch(file.staged)} title="Show staged details"><FileDiff size={14} aria-hidden="true" /></button>
<button class="danger" type="button" onclick={() => discardStagedFromFile(file)} disabled={isBusy || unstageTargets.length === 0} title="Discard staged changes"><RotateCcw size={14} aria-hidden="true" /></button>
</div>