feat(toolbar): overhaul repository action bar UI and status display

This refactors the entire appearance and structure of the repository toolbar, modernizing its layout and improving visual consistency across different states. The component now dynamically displays remote tracking information (ahead/behind) directly on Pull and Push buttons for immediate user feedback.

- Implemented a comprehensive CSS overhaul using grid and flexbox for better responsiveness.
- Added support for German localization within the toolbar actions.
- Enhanced visibility of sync status by displaying commit counts next to pull/push buttons.
This commit is contained in:
Christoph Brandau
2026-07-11 23:11:34 +02:00
parent 646dcc341e
commit 982dbf136d
3 changed files with 361 additions and 133 deletions
+3
View File
@@ -3604,8 +3604,11 @@
hasRepository={workspaceActive}
{isBusy}
{operation}
ahead={status?.ahead ?? 0}
behind={status?.behind ?? 0}
{autoRefreshEnabled}
{autoRefreshInFlight}
language={appLanguage}
onFetch={fetchRepo}
onPull={pullRepo}
onPush={pushRepo}
+171 -8
View File
@@ -541,7 +541,7 @@
.shell {
--app-titlebar-height: 42px;
--app-toolbar-height: 34px;
--app-toolbar-height: 58px;
display: flex;
flex-direction: column;
height: 100%;
@@ -648,22 +648,179 @@
height: 100%;
}
/* Repo action toolbar: its own row under the repo tab bar (hidden in Repository
Management). Its own row means a narrow window can never push the window
controls out of the titlebar; overflow-x is the last-resort safety net. */
/* Repository actions are ordered by workflow: sync, inspect/history, utilities. */
.repo-toolbar {
display: flex;
align-items: stretch;
align-items: flex-end;
gap: 8px;
height: var(--app-toolbar-height);
padding: 5px 8px;
border: 1px solid var(--color-border-subtle);
border-radius: 10px;
background: rgba(16, 17, 29, 0.9);
flex-shrink: 0;
overflow-x: auto;
scrollbar-width: none;
overflow: visible;
user-select: none;
}
.repo-toolbar::-webkit-scrollbar { display: none; }
.repo-toolbar-sync {
display: grid;
grid-template-rows: 11px 34px;
gap: 2px;
align-self: stretch;
min-width: 0;
}
.repo-toolbar-group-label {
padding-left: 6px;
color: var(--color-ink-faint);
font-size: 8px;
font-weight: 850;
line-height: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.repo-action-group {
display: flex;
align-items: stretch;
height: 34px;
min-width: 0;
border: 1px solid var(--color-border-subtle);
border-radius: 7px;
background: rgba(255,255,255,0.018);
}
.repo-action-group > .repo-action,
.repo-action-group > .repo-history-wrap > .repo-action {
border-right: 1px solid var(--color-border-subtle);
}
.repo-action-group > .repo-action:last-child,
.repo-action-group > .repo-history-wrap:last-child > .repo-action {
border-right: 0;
}
.repo-action {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 38px;
height: 32px;
min-height: 32px;
padding: 0 12px;
border: 0;
border-radius: 0;
color: var(--color-ink-muted);
background: transparent;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
cursor: pointer;
transition: color 120ms ease, background 120ms ease;
-webkit-app-region: no-drag;
}
.repo-action:first-child { border-radius: 6px 0 0 6px; }
.repo-action:last-child { border-radius: 0 6px 6px 0; }
.repo-action:only-child { border-radius: 6px; }
.repo-action:hover:not(:disabled),
.repo-action.active {
color: var(--color-ink);
background: var(--color-surface-hover);
}
.repo-action:disabled { opacity: 0.34; cursor: not-allowed; }
.repo-action.sync-primary { color: var(--color-accent); }
.repo-action.sync-primary:hover:not(:disabled) { background: rgba(90,140,248,0.1); }
.repo-action-label { font-size: 12px; font-weight: 750; }
.repo-action-count {
color: currentColor;
font-family: var(--font-mono);
font-size: 9.5px;
font-weight: 850;
}
.repo-action-count.behind { color: #7aacff; }
.repo-action-count.ahead { color: #e0a040; }
.repo-toolbar-divider {
width: 1px;
height: 30px;
margin: 0 1px 2px;
background: var(--color-border-subtle);
flex: 0 0 auto;
}
.repo-toolbar-spacer { flex: 1 1 auto; min-width: 6px; }
.repo-inspect-actions,
.repo-utility-actions { margin-bottom: 1px; }
.repo-history-wrap { position: relative; height: 32px; }
.history-trigger { border-radius: 0 6px 6px 0; }
.history-trigger .history-chevron { transition: transform 120ms ease; }
.history-trigger.active .history-chevron { transform: rotate(180deg); }
.repo-history-menu {
position: absolute;
z-index: 90;
top: calc(100% + 7px);
right: -1px;
display: grid;
width: 240px;
padding: 5px;
border: 1px solid var(--color-border);
border-radius: 9px;
background: var(--color-surface-raised);
box-shadow: 0 16px 42px rgba(0,0,0,0.36);
}
.repo-history-menu > button {
display: grid;
grid-template-columns: 24px minmax(0, 1fr);
align-items: center;
gap: 8px;
min-height: 48px;
padding: 7px 8px;
border: 0;
border-radius: 6px;
color: var(--color-ink-muted);
background: transparent;
text-align: left;
}
.repo-history-menu > button:hover { color: var(--color-ink); background: var(--color-surface-hover); }
.repo-history-menu > button > svg { color: var(--color-accent); }
.repo-history-menu > button > span { display: grid; gap: 2px; min-width: 0; }
.repo-history-menu strong { color: inherit; font-size: 11px; }
.repo-history-menu small { overflow: hidden; color: var(--color-ink-faint); font-size: 9.5px; text-overflow: ellipsis; white-space: nowrap; }
.repo-utility-actions { flex: 0 0 auto; }
.repo-action.icon-action { width: 36px; padding: 0; }
.repo-auto-toggle {
display: inline-flex;
align-items: center;
gap: 8px;
height: 32px;
min-height: 32px;
padding: 0 8px 0 10px;
border: 0;
border-radius: 0 6px 6px 0;
color: var(--color-ink-muted);
background: transparent;
font-size: 11.5px;
font-weight: 750;
}
.repo-auto-toggle:hover { color: var(--color-ink); background: var(--color-surface-hover); }
.repo-switch {
position: relative;
width: 27px;
height: 15px;
border-radius: 999px;
background: var(--color-border-input);
transition: background 140ms ease;
}
.repo-switch > span {
position: absolute;
top: 2px;
left: 2px;
width: 11px;
height: 11px;
border-radius: 50%;
background: #ffffff;
box-shadow: 0 1px 3px rgba(0,0,0,0.28);
transition: transform 140ms ease;
}
.repo-auto-toggle.active { color: #4eca76; }
.repo-auto-toggle.active .repo-switch { background: #26924f; }
.repo-auto-toggle.active .repo-switch > span { transform: translateX(12px); }
.repo-switch.busy { box-shadow: 0 0 0 3px rgba(78,202,118,0.12); }
.tb-action {
display: flex;
@@ -5220,6 +5377,7 @@
.branch-actions { flex-direction: column; align-items: flex-end; gap: 4px; }
.branch-actions button { width: 100%; }
.tb-action-label { display: none; }
.repo-action-label.utility-label { display: none; }
}
/* Narrow: single column */
@@ -5293,4 +5451,9 @@
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
.branch-actions { flex-direction: row; justify-content: flex-start; }
.tb-action-label { display: none; }
.repo-action-label { display: none; }
.repo-action { padding-inline: 8px; }
.repo-toolbar { gap: 5px; padding-inline: 6px; }
.repo-toolbar-divider { margin-inline: 0; }
.repo-auto-toggle { padding-left: 7px; gap: 6px; }
}
+187 -125
View File
@@ -1,11 +1,26 @@
<script lang="ts">
import { CloudDownload, Download, FolderOpen, GitCompare, History, ListRestart, LoaderCircle, RefreshCw, Search, Upload } from "@lucide/svelte";
import {
ChevronDown,
CloudDownload,
Download,
FolderOpen,
GitCompare,
History,
ListRestart,
LoaderCircle,
RefreshCw,
Search,
Upload,
} from "@lucide/svelte";
export let hasRepository: boolean = false;
export let isBusy: boolean = false;
export let operation: string = "";
export let ahead: number = 0;
export let behind: number = 0;
export let autoRefreshEnabled: boolean = true;
export let autoRefreshInFlight: boolean = false;
export let language: "en" | "de" = "en";
export let onFetch: () => void = () => {};
export let onPull: () => void = () => {};
export let onPush: () => void = () => {};
@@ -16,141 +31,188 @@
export let onReflog: () => void = () => {};
export let onOpenInExplorer: () => void = () => {};
export let onToggleAutoRefresh: () => void = () => {};
let historyOpen = false;
let toolbarElement: HTMLDivElement;
$: isGerman = language === "de";
function runHistoryAction(action: () => void) {
historyOpen = false;
action();
}
function handleWindowClick(event: MouseEvent) {
if (historyOpen && toolbarElement && !toolbarElement.contains(event.target as Node)) {
historyOpen = false;
}
}
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && historyOpen) {
event.stopPropagation();
historyOpen = false;
}
}
</script>
<div class="repo-toolbar" role="toolbar" aria-label="Repository actions">
<button
class="tb-action"
onclick={onOpenInExplorer}
disabled={!hasRepository || isBusy}
title="Open repository in Explorer"
aria-label="Open repository in Explorer"
>
<FolderOpen size={14} aria-hidden="true" />
<span class="tb-action-label">Explorer</span>
</button>
<svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
<button
class="tb-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title="Global search"
aria-label="Global search"
>
<Search size={14} aria-hidden="true" />
<span class="tb-action-label">Search</span>
</button>
<div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
<div class="repo-toolbar-sync">
<span class="repo-toolbar-group-label">Sync</span>
<div class="repo-action-group repo-sync-actions">
<button
class="repo-action fetch"
onclick={onFetch}
disabled={!hasRepository || isBusy}
title="Fetch"
aria-label="Fetch"
>
{#if operation === "Fetching"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<CloudDownload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Fetch</span>
</button>
<button
class="tb-action"
onclick={onCompare}
disabled={!hasRepository || isBusy}
title="Compare commits"
aria-label="Compare commits"
>
<GitCompare size={14} aria-hidden="true" />
<span class="tb-action-label">Compare</span>
</button>
<button
class="repo-action sync-primary"
onclick={onPull}
disabled={!hasRepository || isBusy}
title="Pull"
aria-label={behind > 0
? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
: "Pull"}
>
{#if operation === "Pulling"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Download size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Pull</span>
{#if behind > 0}<span class="repo-action-count behind">{behind}</span>{/if}
</button>
<button
class="tb-action"
onclick={onInteractiveRebase}
disabled={!hasRepository || isBusy}
title="Interactive rebase"
aria-label="Interactive rebase"
>
<ListRestart size={14} aria-hidden="true" />
<span class="tb-action-label">Rebase</span>
</button>
<button
class="repo-action sync-primary"
onclick={onPush}
disabled={!hasRepository || isBusy}
title="Push"
aria-label={ahead > 0
? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
: "Push"}
>
{#if operation === "Pushing"}
<LoaderCircle class="spin" size={15} aria-hidden="true" />
{:else}
<Upload size={15} aria-hidden="true" />
{/if}
<span class="repo-action-label">Push</span>
{#if ahead > 0}<span class="repo-action-count ahead">{ahead}</span>{/if}
</button>
</div>
</div>
<button
class="tb-action"
onclick={onReflog}
disabled={!hasRepository || isBusy}
title="Reflog"
aria-label="Reflog"
>
<History size={14} aria-hidden="true" />
<span class="tb-action-label">Reflog</span>
</button>
<div class="repo-toolbar-divider" aria-hidden="true"></div>
<div class="titlebar-divider" aria-hidden="true"></div>
<div class="repo-action-group repo-inspect-actions">
<button
class="repo-action"
onclick={onSearch}
disabled={!hasRepository || isBusy}
title={isGerman ? "Globale Suche" : "Global search"}
aria-label={isGerman ? "Globale Suche" : "Global search"}
>
<Search size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Suchen" : "Search"}</span>
</button>
<button
class="tb-action"
onclick={onFetch}
disabled={!hasRepository || isBusy}
title="Fetch"
aria-label="Fetch"
>
{#if operation === "Fetching"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<CloudDownload size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Fetch</span>
</button>
<button
class="repo-action"
onclick={onCompare}
disabled={!hasRepository || isBusy}
title={isGerman ? "Commits vergleichen" : "Compare commits"}
aria-label={isGerman ? "Commits vergleichen" : "Compare commits"}
>
<GitCompare size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
</button>
<button
class="tb-action"
onclick={onPull}
disabled={!hasRepository || isBusy}
title="Pull"
aria-label="Pull"
>
{#if operation === "Pulling"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<Download size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Pull</span>
</button>
<div class="repo-history-wrap">
<button
class="repo-action history-trigger"
class:active={historyOpen}
type="button"
onclick={() => { historyOpen = !historyOpen; }}
disabled={!hasRepository || isBusy}
aria-haspopup="menu"
aria-expanded={historyOpen}
title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
>
<History size={15} aria-hidden="true" />
<span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
<ChevronDown class="history-chevron" size={13} aria-hidden="true" />
</button>
<button
class="tb-action"
onclick={onPush}
disabled={!hasRepository || isBusy}
title="Push"
aria-label="Push"
>
{#if operation === "Pushing"}
<LoaderCircle class="spin" size={14} aria-hidden="true" />
{:else}
<Upload size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Push</span>
</button>
{#if historyOpen}
<div class="repo-history-menu" role="menu">
<button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
<History size={15} aria-hidden="true" />
<span>
<strong>Reflog</strong>
<small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
</span>
</button>
<button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
<ListRestart size={15} aria-hidden="true" />
<span>
<strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
<small>{isGerman ? "Commits ordnen und zusammenfassen" : "Reorder and combine commits"}</small>
</span>
</button>
</div>
{/if}
</div>
</div>
<div class="titlebar-divider" aria-hidden="true"></div>
<div class="repo-toolbar-spacer"></div>
<div class="repo-toolbar-divider utility" aria-hidden="true"></div>
<button
class="tb-action"
onclick={onRefresh}
disabled={isBusy || !hasRepository}
title="Refresh"
aria-label="Refresh"
>
<RefreshCw
class={operation === "Refreshing" ? "spin" : ""}
size={14}
aria-hidden="true"
/>
<span class="tb-action-label">Refresh</span>
</button>
<div class="repo-action-group repo-utility-actions">
<button
class="repo-action"
onclick={onOpenInExplorer}
disabled={!hasRepository || isBusy}
title={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
aria-label={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
>
<FolderOpen size={15} aria-hidden="true" />
<span class="repo-action-label utility-label">Explorer</span>
</button>
<button
class="tb-action auto-toggle"
class:active={autoRefreshEnabled}
onclick={onToggleAutoRefresh}
aria-pressed={autoRefreshEnabled}
title={autoRefreshEnabled ? "Auto-refresh on — click to disable" : "Auto-refresh off — click to enable"}
aria-label="Toggle auto-refresh"
>
<RefreshCw
class={autoRefreshInFlight ? "spin" : ""}
size={14}
aria-hidden="true"
/>
<span class="tb-action-label">Auto</span>
</button>
<button
class="repo-action icon-action"
onclick={onRefresh}
disabled={isBusy || !hasRepository}
title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
>
<RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={15} aria-hidden="true" />
</button>
<button
class="repo-auto-toggle"
class:active={autoRefreshEnabled}
onclick={onToggleAutoRefresh}
aria-pressed={autoRefreshEnabled}
title={autoRefreshEnabled
? (isGerman ? "Auto-Aktualisierung ausschalten" : "Turn auto-refresh off")
: (isGerman ? "Auto-Aktualisierung einschalten" : "Turn auto-refresh on")}
>
<span>{isGerman ? "Auto" : "Auto"}</span>
<span class="repo-switch" class:busy={autoRefreshInFlight} aria-hidden="true"><span></span></span>
</button>
</div>
</div>