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