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:
+106
-5
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user