feat(repo-tabs): add context menu for managing repository tabs

This update introduces a context menu for repository tabs, allowing users
to easily close individual tabs, close all other tabs, or close tabs to the
right. Additionally, several visual elements related to loading animations
have been removed to streamline the user interface.

- Implement context menu actions for repository tab management
- Remove unnecessary loading animations from the splash screen and overlays
- Enhance user experience with improved tab management functionality
This commit is contained in:
Christoph Brandau
2026-07-08 15:34:30 +02:00
parent cc179e9eef
commit 8793d3de40
5 changed files with 124 additions and 98 deletions
+112 -3
View File
@@ -145,6 +145,12 @@
lastOpened: number;
}
interface RepoTabContextMenu {
path: string;
x: number;
y: number;
}
interface CloneRequest {
remoteUrl: string;
parentPath: string;
@@ -173,6 +179,7 @@
let activeRepoPath = "";
let activeView: AppView = "management";
let repoTabs: RepoTab[] = [];
let repoTabContextMenu: RepoTabContextMenu | null = null;
let recentRepoPaths: string[] = [];
let favoriteRepoPaths: string[] = [];
// Last-seen branch/ahead/behind/changed for repos that are known (recent/favorites)
@@ -1427,15 +1434,41 @@
async function selectRepoTab(path: string) {
if (isBusy) return;
if (activeView === "repository" && sameRepoPath(activeRepoPath, path)) return;
closeRepoTabContextMenu();
trackEvent("repository_tab_selected", {
open_repositories: repoTabs.length,
});
await openRepo(path);
}
function repoTabIndex(path: string): number {
return repoTabs.findIndex((tab) => sameRepoPath(tab.path, path));
}
function closeRepoTabContextMenu() {
repoTabContextMenu = null;
}
function openRepoTabContextMenu(path: string, event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (isBusy) return;
repoTabContextMenu = {
path,
x: Math.max(8, Math.min(event.clientX, window.innerWidth - 210)),
y: Math.max(8, Math.min(event.clientY, window.innerHeight - 128)),
};
}
async function closeRepoTabFromContext(path: string) {
closeRepoTabContextMenu();
await closeRepoTab(path);
}
async function closeRepoTab(path: string, event?: MouseEvent) {
event?.stopPropagation();
if (isBusy) return;
closeRepoTabContextMenu();
const index = repoTabs.findIndex((tab) => sameRepoPath(tab.path, path));
const remaining = repoTabs.filter((tab) => !sameRepoPath(tab.path, path));
@@ -1457,6 +1490,45 @@
}
}
async function closeOtherRepoTabs(path: string) {
if (isBusy) return;
closeRepoTabContextMenu();
const target = repoTabs.find((tab) => sameRepoPath(tab.path, path));
if (!target || repoTabs.length <= 1) return;
const closedCount = repoTabs.length - 1;
const wasActive = sameRepoPath(activeRepoPath, path);
repoTabs = [target];
persistRepoLists();
trackEvent("repository_tabs_closed", {
mode: "others",
closed_tabs: closedCount,
open_repositories: repoTabs.length,
});
if (!wasActive) await openRepo(path);
}
async function closeRepoTabsToRight(path: string) {
if (isBusy) return;
closeRepoTabContextMenu();
const index = repoTabIndex(path);
if (index < 0 || index >= repoTabs.length - 1) return;
const remaining = repoTabs.slice(0, index + 1);
const closedCount = repoTabs.length - remaining.length;
const activeStillOpen = remaining.some((tab) => sameRepoPath(tab.path, activeRepoPath));
repoTabs = remaining;
persistRepoLists();
trackEvent("repository_tabs_closed", {
mode: "right",
closed_tabs: closedCount,
open_repositories: repoTabs.length,
});
if (!activeStillOpen) await openRepo(path);
}
async function removeRepoFromManagement(path: string, event?: MouseEvent) {
event?.stopPropagation();
if (isBusy) return;
@@ -2717,7 +2789,8 @@
// ── Event handlers ─────────────────────────────────────────────────────────
function handleWindowKeydown(event: KeyboardEvent) {
if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
if (event.key === "Escape" && repoTabContextMenu) closeRepoTabContextMenu();
else if (event.key === "Escape" && pendingDiscard && !isBusy) closeDiscardConfirm();
else if (event.key === "Escape" && compareDialogOpen) closeCompareDialog();
else if (event.key === "Escape" && newBranchCommit) newBranchCommit = null;
else if (event.key === "Escape" && renameBranchTarget) renameBranchTarget = null;
@@ -2729,13 +2802,17 @@
function handleWindowContextMenu(event: MouseEvent) {
event.preventDefault();
}
function handleWindowClick() {
if (repoTabContextMenu) closeRepoTabContextMenu();
}
</script>
<svelte:head>
<title>GitLite</title>
</svelte:head>
<svelte:window on:keydown={handleWindowKeydown} on:contextmenu={handleWindowContextMenu} />
<svelte:window on:click={handleWindowClick} on:keydown={handleWindowKeydown} on:contextmenu={handleWindowContextMenu} />
<main class="shell">
<TitleBar
@@ -2776,7 +2853,12 @@
<div class="repo-tabs-scroll">
{#each repoTabs as repo (repo.path)}
<div class="repo-tab-wrap" class:active={activeView === "repository" && sameRepoPath(activeRepoPath, 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"
@@ -2816,6 +2898,33 @@
</button>
</header>
{#if repoTabContextMenu}
{@const menu = repoTabContextMenu}
{@const contextTab = repoTabs.find((tab) => sameRepoPath(tab.path, menu.path))}
{@const contextIndex = repoTabIndex(menu.path)}
{#if contextTab}
<div
class="repo-tab-context-menu"
style={`left: ${menu.x}px; top: ${menu.y}px;`}
role="menu"
tabindex="-1"
aria-label={`Tab actions for ${contextTab.name}`}
oncontextmenu={(event) => { event.preventDefault(); event.stopPropagation(); }}
>
<button type="button" role="menuitem" onclick={() => closeRepoTabFromContext(contextTab.path)} disabled={isBusy}>
Close tab
</button>
<div class="menu-separator" aria-hidden="true"></div>
<button type="button" role="menuitem" onclick={() => closeOtherRepoTabs(contextTab.path)} disabled={isBusy || repoTabs.length <= 1}>
Close other tabs
</button>
<button type="button" role="menuitem" onclick={() => closeRepoTabsToRight(contextTab.path)} disabled={isBusy || contextIndex < 0 || contextIndex >= repoTabs.length - 1}>
Close tabs to the right
</button>
</div>
{/if}
{/if}
<!-- Status notices -->
{#if errorMessage}
<section class="notice error" role="alert">