refactor(ui): extract repository tab bar into dedicated component

The complex logic and markup for rendering the repository tabs have been extracted from App.svelte into a new, reusable RepoTabs component. This refactoring significantly cleans up the main application view, improves separation of concerns, and makes the UI structure easier to maintain and extend. Corresponding CSS updates were applied to ensure the visual fidelity and responsiveness of the tab bar remain consistent across different states.

- Encapsulates all tab rendering logic in src/lib/RepoTabs.svelte
- Simplifies App.svelte by replacing large block of HTML with component usage
- Updates styling for better alignment and modern aesthetics
This commit is contained in:
Christoph Brandau
2026-07-12 22:56:39 +02:00
parent ff00925b13
commit f9c0c00618
3 changed files with 150 additions and 96 deletions
+14 -60
View File
@@ -3,10 +3,11 @@
import { invoke } from "@tauri-apps/api/core";
import { open as openDialog } from "@tauri-apps/plugin-dialog";
import { check, type DownloadEvent, type Update } from "@tauri-apps/plugin-updater";
import { AlertCircle, BookOpen, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
import { AlertCircle, Cherry, Download, FolderOpen, GitBranch, GitMerge, LoaderCircle, Plus, Search, Star, X } from "@lucide/svelte";
import TitleBar from "./lib/TitleBar.svelte";
import RepoToolbar from "./lib/RepoToolbar.svelte";
import RepoTabs from "./lib/RepoTabs.svelte";
import AiSettingsDialog from "./lib/components/AiSettingsDialog.svelte";
import AnalyticsNoticeDialog from "./lib/components/AnalyticsNoticeDialog.svelte";
import AppSettingsDialog from "./lib/components/AppSettingsDialog.svelte";
@@ -3537,65 +3538,18 @@
<div class="shell-body">
<header class="repo-tabbar" aria-label="Repository tabs">
<button
class="repo-tab management"
class:active={activeView === "management"}
type="button"
onclick={openRepoManagement}
disabled={isBusy}
title="Repository Management"
>
<BookOpen size={14} aria-hidden="true" />
Repository Management
</button>
<div class="repo-tabs-scroll">
{#each repoTabs as repo (repo.path)}
<div
class="repo-tab-wrap"
class:active={activeView === "repository" && sameRepoPath(activeRepoPath, repo.path)}
role="presentation"
oncontextmenu={(event) => openRepoTabContextMenu(repo.path, event)}
>
<button
class="repo-tab"
type="button"
onclick={() => selectRepoTab(repo.path)}
disabled={isBusy}
title={repo.path}
>
<FolderOpen size={14} aria-hidden="true" />
<span>{repo.name}</span>
{#if repo.branch}
<strong>{repo.branch}</strong>
{/if}
</button>
<button
class="repo-tab-close"
type="button"
onclick={(event) => closeRepoTab(repo.path, event)}
disabled={isBusy}
aria-label={`Close ${repo.name}`}
title="Close repository tab"
>
<X size={13} aria-hidden="true" />
</button>
</div>
{/each}
</div>
<button
class="repo-tab-add"
type="button"
onclick={chooseRepositoryFolder}
disabled={isBusy}
title="Open repository folder"
aria-label="Open repository folder"
>
<Plus size={15} aria-hidden="true" />
</button>
</header>
<RepoTabs
{activeView}
{repoTabs}
{isBusy}
language={appLanguage}
onOpenManagement={openRepoManagement}
isActive={(path) => activeView === "repository" && sameRepoPath(activeRepoPath, path)}
onSelect={selectRepoTab}
onClose={closeRepoTab}
onContextMenu={openRepoTabContextMenu}
onAdd={chooseRepositoryFolder}
/>
<!-- Repo actions live under the tab bar and disappear in Repository
Management, where none of them are applicable. -->
+59 -36
View File
@@ -896,10 +896,10 @@
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
align-items: stretch;
min-height: 40px;
min-height: 44px;
border: 1px solid var(--color-border-subtle);
border-radius: 10px;
background: rgba(16, 17, 29, 0.9);
background: rgba(13, 15, 25, 0.92);
overflow: hidden;
}
@@ -907,76 +907,89 @@
display: flex;
align-items: stretch;
min-width: 0;
border-left: 1px solid var(--color-border-subtle);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
}
.repo-tab-wrap {
display: flex;
align-items: stretch;
min-width: 0;
flex: 0 1 230px;
min-width: 150px;
max-width: 260px;
position: relative;
border-right: 1px solid var(--color-border-subtle);
background: rgba(255,255,255,0.015);
background: rgba(255,255,255,0.012);
transition: background 120ms ease;
}
.repo-tab-wrap:hover { background: rgba(255,255,255,0.035); }
.repo-tab-wrap.active {
background: rgba(90, 140, 248, 0.16);
box-shadow: inset 0 -2px 0 var(--color-primary);
background: linear-gradient(180deg, rgba(90,140,248,0.14), rgba(23,29,43,0.96));
box-shadow: inset 0 -3px 0 var(--color-primary);
}
.repo-tab {
min-height: 38px;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 8px;
flex: 1 1 auto;
min-height: 42px;
min-width: 0;
max-width: 250px;
padding: 0 10px;
max-width: none;
padding: 0 8px 0 13px;
border: 0;
border-right: 1px solid var(--color-border-subtle);
border-radius: 0;
background: transparent;
color: var(--color-ink-muted);
font-size: 12px;
font-weight: 700;
font-weight: 720;
text-align: left;
}
.repo-tab > svg { flex: 0 0 auto; color: var(--color-ink-faint); }
.repo-tab.management {
max-width: none;
min-width: 190px;
min-width: 184px;
border: 0;
box-shadow: inset 0 -3px 0 transparent;
}
.repo-tab.active,
.repo-tab.management.active {
color: var(--color-ink);
background: linear-gradient(180deg, rgba(90,140,248,0.14), rgba(23,29,43,0.96));
box-shadow: inset 0 -3px 0 var(--color-primary);
}
.repo-tab.management:hover:not(:disabled) { background: var(--color-surface-hover); }
.repo-tab-wrap.active .repo-tab {
color: var(--color-ink);
}
.repo-tab-wrap.active .repo-tab > svg,
.repo-tab.management.active > svg { color: var(--color-accent); }
.repo-tab span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.repo-tab strong {
flex: 0 0 auto;
max-width: 90px;
overflow: hidden;
padding: 2px 6px;
border-radius: 4px;
background: rgba(94, 110, 156, 0.18);
color: var(--color-ink-dim);
font-size: 10.5px;
font-family: var(--font-mono);
text-overflow: ellipsis;
white-space: nowrap;
}
.repo-tab-close,
.repo-tab-add {
min-height: 38px;
min-width: 38px;
min-height: 42px;
min-width: 36px;
padding: 0;
border: 0;
border-radius: 0;
background: transparent;
color: var(--color-ink-faint);
}
.repo-tab-close { opacity: 0.58; transition: opacity 120ms ease, color 120ms ease, background 120ms ease; }
.repo-tab-wrap:hover .repo-tab-close,
.repo-tab-wrap.active .repo-tab-close { opacity: 1; }
.repo-tab-close:hover:not(:disabled) { color: var(--color-ink); background: var(--color-surface-hover); }
.repo-tab-add {
min-width: 44px;
border-left: 1px solid var(--color-border-subtle);
}
.repo-tab-add:hover:not(:disabled) { color: var(--color-accent); background: var(--color-surface-hover); }
.repo-management {
display: grid;
@@ -4958,15 +4971,24 @@
:root[data-theme="light"] .repo-tabbar,
:root[data-theme="light"] .repo-management-tools {
background: rgba(255,255,255,0.78);
background: rgba(247,249,252,0.96);
}
:root[data-theme="light"] .repo-tab-wrap {
background: rgba(244,247,252,0.72);
background: rgba(255,255,255,0.5);
}
:root[data-theme="light"] .repo-tab-wrap.active {
background: rgba(49,95,214,0.1);
background: #ffffff;
}
:root[data-theme="light"] .repo-tab-wrap:hover,
:root[data-theme="light"] .repo-tab.management:hover:not(:disabled) {
background: rgba(255,255,255,0.9);
}
:root[data-theme="light"] .repo-tab.management.active {
background: #ffffff;
}
:root[data-theme="light"] .repo-row {
@@ -5419,9 +5441,10 @@
}
.top-section { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) 14px var(--commit-panel-height, 190px); }
.repo-form { grid-template-columns: 1fr; }
.repo-tabbar { grid-template-columns: minmax(0, 1fr) auto; }
.repo-tab.management { min-width: 0; }
.repo-tabs-scroll { grid-column: 1 / -1; order: 2; border-top: 1px solid var(--color-border-subtle); }
.repo-tabbar { grid-template-columns: auto minmax(0, 1fr) auto; }
.repo-tab.management { min-width: 44px; width: 44px; padding: 0; justify-content: center; }
.repo-tab.management span { display: none; }
.repo-tabs-scroll { grid-column: auto; order: initial; border-top: 0; }
.repo-management-head,
.repo-management-tools { align-items: stretch; flex-direction: column; }
.repo-management-actions { justify-content: flex-start; }
+77
View File
@@ -0,0 +1,77 @@
<script lang="ts">
import { BookOpen, Database, Plus, X } from "@lucide/svelte";
interface RepositoryTabItem {
path: string;
name: string;
branch: string | null;
}
export let activeView: "management" | "repository" = "management";
export let repoTabs: RepositoryTabItem[] = [];
export let isBusy: boolean = false;
export let language: "en" | "de" = "en";
export let onOpenManagement: () => void = () => {};
export let isActive: (path: string) => boolean = () => false;
export let onSelect: (path: string) => void | Promise<void> = () => {};
export let onClose: (path: string, event: MouseEvent) => void | Promise<void> = () => {};
export let onContextMenu: (path: string, event: MouseEvent) => void = () => {};
export let onAdd: () => void | Promise<void> = () => {};
</script>
<header class="repo-tabbar" aria-label={language === "de" ? "Repository-Reiter" : "Repository tabs"}>
<button
class="repo-tab management"
class:active={activeView === "management"}
type="button"
onclick={onOpenManagement}
disabled={isBusy}
title={language === "de" ? "Repository-Verwaltung" : "Repository Management"}
>
<BookOpen size={14} aria-hidden="true" />
<span>{language === "de" ? "Repository-Verwaltung" : "Repository Management"}</span>
</button>
<div class="repo-tabs-scroll">
{#each repoTabs as repo (repo.path)}
<div
class="repo-tab-wrap"
class:active={isActive(repo.path)}
role="presentation"
oncontextmenu={(event) => onContextMenu(repo.path, event)}
>
<button
class="repo-tab"
type="button"
onclick={() => onSelect(repo.path)}
disabled={isBusy}
title={repo.path}
>
<Database size={15} aria-hidden="true" />
<span>{repo.name}</span>
</button>
<button
class="repo-tab-close"
type="button"
onclick={(event) => onClose(repo.path, event)}
disabled={isBusy}
aria-label={language === "de" ? `${repo.name} schließen` : `Close ${repo.name}`}
title={language === "de" ? "Repository-Tab schließen" : "Close repository tab"}
>
<X size={13} aria-hidden="true" />
</button>
</div>
{/each}
</div>
<button
class="repo-tab-add"
type="button"
onclick={onAdd}
disabled={isBusy}
title={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
aria-label={language === "de" ? "Repository-Ordner öffnen" : "Open repository folder"}
>
<Plus size={15} aria-hidden="true" />
</button>
</header>