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} hasRepository={workspaceActive}
{isBusy} {isBusy}
{operation} {operation}
ahead={status?.ahead ?? 0}
behind={status?.behind ?? 0}
{autoRefreshEnabled} {autoRefreshEnabled}
{autoRefreshInFlight} {autoRefreshInFlight}
language={appLanguage}
onFetch={fetchRepo} onFetch={fetchRepo}
onPull={pullRepo} onPull={pullRepo}
onPush={pushRepo} onPush={pushRepo}
+171 -8
View File
@@ -541,7 +541,7 @@
.shell { .shell {
--app-titlebar-height: 42px; --app-titlebar-height: 42px;
--app-toolbar-height: 34px; --app-toolbar-height: 58px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
@@ -648,22 +648,179 @@
height: 100%; height: 100%;
} }
/* Repo action toolbar: its own row under the repo tab bar (hidden in Repository /* Repository actions are ordered by workflow: sync, inspect/history, utilities. */
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. */
.repo-toolbar { .repo-toolbar {
display: flex; display: flex;
align-items: stretch; align-items: flex-end;
gap: 8px;
height: var(--app-toolbar-height); height: var(--app-toolbar-height);
padding: 5px 8px;
border: 1px solid var(--color-border-subtle); border: 1px solid var(--color-border-subtle);
border-radius: 10px; border-radius: 10px;
background: rgba(16, 17, 29, 0.9); background: rgba(16, 17, 29, 0.9);
flex-shrink: 0; flex-shrink: 0;
overflow-x: auto; overflow: visible;
scrollbar-width: none;
user-select: none; 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 { .tb-action {
display: flex; display: flex;
@@ -5220,6 +5377,7 @@
.branch-actions { flex-direction: column; align-items: flex-end; gap: 4px; } .branch-actions { flex-direction: column; align-items: flex-end; gap: 4px; }
.branch-actions button { width: 100%; } .branch-actions button { width: 100%; }
.tb-action-label { display: none; } .tb-action-label { display: none; }
.repo-action-label.utility-label { display: none; }
} }
/* Narrow: single column */ /* Narrow: single column */
@@ -5293,4 +5451,9 @@
.dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); } .dialog-files { border-right: none; border-bottom: 1px solid var(--color-border-subtle); }
.branch-actions { flex-direction: row; justify-content: flex-start; } .branch-actions { flex-direction: row; justify-content: flex-start; }
.tb-action-label { display: none; } .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"> <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 hasRepository: boolean = false;
export let isBusy: boolean = false; export let isBusy: boolean = false;
export let operation: string = ""; export let operation: string = "";
export let ahead: number = 0;
export let behind: number = 0;
export let autoRefreshEnabled: boolean = true; export let autoRefreshEnabled: boolean = true;
export let autoRefreshInFlight: boolean = false; export let autoRefreshInFlight: boolean = false;
export let language: "en" | "de" = "en";
export let onFetch: () => void = () => {}; export let onFetch: () => void = () => {};
export let onPull: () => void = () => {}; export let onPull: () => void = () => {};
export let onPush: () => void = () => {}; export let onPush: () => void = () => {};
@@ -16,141 +31,188 @@
export let onReflog: () => void = () => {}; export let onReflog: () => void = () => {};
export let onOpenInExplorer: () => void = () => {}; export let onOpenInExplorer: () => void = () => {};
export let onToggleAutoRefresh: () => 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> </script>
<div class="repo-toolbar" role="toolbar" aria-label="Repository actions"> <svelte:window onclick={handleWindowClick} onkeydown={handleWindowKeydown} />
<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>
<button <div bind:this={toolbarElement} class="repo-toolbar" role="toolbar" aria-label={isGerman ? "Repository-Aktionen" : "Repository actions"}>
class="tb-action" <div class="repo-toolbar-sync">
onclick={onSearch} <span class="repo-toolbar-group-label">Sync</span>
disabled={!hasRepository || isBusy} <div class="repo-action-group repo-sync-actions">
title="Global search" <button
aria-label="Global search" class="repo-action fetch"
> onclick={onFetch}
<Search size={14} aria-hidden="true" /> disabled={!hasRepository || isBusy}
<span class="tb-action-label">Search</span> title="Fetch"
</button> 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 <button
class="tb-action" class="repo-action sync-primary"
onclick={onCompare} onclick={onPull}
disabled={!hasRepository || isBusy} disabled={!hasRepository || isBusy}
title="Compare commits" title="Pull"
aria-label="Compare commits" aria-label={behind > 0
> ? `Pull, ${behind} ${isGerman ? (behind === 1 ? "Remote-Commit voraus" : "Remote-Commits voraus") : (behind === 1 ? "commit behind" : "commits behind")}`
<GitCompare size={14} aria-hidden="true" /> : "Pull"}
<span class="tb-action-label">Compare</span> >
</button> {#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 <button
class="tb-action" class="repo-action sync-primary"
onclick={onInteractiveRebase} onclick={onPush}
disabled={!hasRepository || isBusy} disabled={!hasRepository || isBusy}
title="Interactive rebase" title="Push"
aria-label="Interactive rebase" aria-label={ahead > 0
> ? `Push, ${ahead} ${isGerman ? (ahead === 1 ? "lokaler Commit voraus" : "lokale Commits voraus") : (ahead === 1 ? "commit ahead" : "commits ahead")}`
<ListRestart size={14} aria-hidden="true" /> : "Push"}
<span class="tb-action-label">Rebase</span> >
</button> {#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 <div class="repo-toolbar-divider" aria-hidden="true"></div>
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="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 <button
class="tb-action" class="repo-action"
onclick={onFetch} onclick={onCompare}
disabled={!hasRepository || isBusy} disabled={!hasRepository || isBusy}
title="Fetch" title={isGerman ? "Commits vergleichen" : "Compare commits"}
aria-label="Fetch" aria-label={isGerman ? "Commits vergleichen" : "Compare commits"}
> >
{#if operation === "Fetching"} <GitCompare size={15} aria-hidden="true" />
<LoaderCircle class="spin" size={14} aria-hidden="true" /> <span class="repo-action-label">{isGerman ? "Vergleichen" : "Compare"}</span>
{:else} </button>
<CloudDownload size={14} aria-hidden="true" />
{/if}
<span class="tb-action-label">Fetch</span>
</button>
<button <div class="repo-history-wrap">
class="tb-action" <button
onclick={onPull} class="repo-action history-trigger"
disabled={!hasRepository || isBusy} class:active={historyOpen}
title="Pull" type="button"
aria-label="Pull" onclick={() => { historyOpen = !historyOpen; }}
> disabled={!hasRepository || isBusy}
{#if operation === "Pulling"} aria-haspopup="menu"
<LoaderCircle class="spin" size={14} aria-hidden="true" /> aria-expanded={historyOpen}
{:else} title={isGerman ? "Verlauf und Rebase" : "History and rebase"}
<Download size={14} aria-hidden="true" /> >
{/if} <History size={15} aria-hidden="true" />
<span class="tb-action-label">Pull</span> <span class="repo-action-label">{isGerman ? "Verlauf" : "History"}</span>
</button> <ChevronDown class="history-chevron" size={13} aria-hidden="true" />
</button>
<button {#if historyOpen}
class="tb-action" <div class="repo-history-menu" role="menu">
onclick={onPush} <button type="button" role="menuitem" onclick={() => runHistoryAction(onReflog)}>
disabled={!hasRepository || isBusy} <History size={15} aria-hidden="true" />
title="Push" <span>
aria-label="Push" <strong>Reflog</strong>
> <small>{isGerman ? "Lokale Referenzbewegungen" : "Local reference movements"}</small>
{#if operation === "Pushing"} </span>
<LoaderCircle class="spin" size={14} aria-hidden="true" /> </button>
{:else} <button type="button" role="menuitem" onclick={() => runHistoryAction(onInteractiveRebase)}>
<Upload size={14} aria-hidden="true" /> <ListRestart size={15} aria-hidden="true" />
{/if} <span>
<span class="tb-action-label">Push</span> <strong>{isGerman ? "Interaktiver Rebase" : "Interactive rebase"}</strong>
</button> <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 <div class="repo-action-group repo-utility-actions">
class="tb-action" <button
onclick={onRefresh} class="repo-action"
disabled={isBusy || !hasRepository} onclick={onOpenInExplorer}
title="Refresh" disabled={!hasRepository || isBusy}
aria-label="Refresh" title={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
> aria-label={isGerman ? "Repository im Explorer öffnen" : "Open repository in Explorer"}
<RefreshCw >
class={operation === "Refreshing" ? "spin" : ""} <FolderOpen size={15} aria-hidden="true" />
size={14} <span class="repo-action-label utility-label">Explorer</span>
aria-hidden="true" </button>
/>
<span class="tb-action-label">Refresh</span>
</button>
<button <button
class="tb-action auto-toggle" class="repo-action icon-action"
class:active={autoRefreshEnabled} onclick={onRefresh}
onclick={onToggleAutoRefresh} disabled={isBusy || !hasRepository}
aria-pressed={autoRefreshEnabled} title={isGerman ? "Manuell aktualisieren" : "Refresh now"}
title={autoRefreshEnabled ? "Auto-refresh on — click to disable" : "Auto-refresh off — click to enable"} aria-label={isGerman ? "Manuell aktualisieren" : "Refresh now"}
aria-label="Toggle auto-refresh" >
> <RefreshCw class={operation === "Refreshing" ? "spin" : ""} size={15} aria-hidden="true" />
<RefreshCw </button>
class={autoRefreshInFlight ? "spin" : ""}
size={14} <button
aria-hidden="true" class="repo-auto-toggle"
/> class:active={autoRefreshEnabled}
<span class="tb-action-label">Auto</span> onclick={onToggleAutoRefresh}
</button> 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> </div>