From 8793d3de400c096e8bc41136485d0cee022ee2f4 Mon Sep 17 00:00:00 2001 From: Christoph Brandau Date: Wed, 8 Jul 2026 15:34:30 +0200 Subject: [PATCH] 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 --- public/splashscreen.html | 35 ------ src/App.svelte | 115 ++++++++++++++++++- src/app.css | 36 ++---- src/lib/components/FileHistoryPanel.svelte | 1 - src/lib/components/RepoLoadingOverlay.svelte | 35 ------ 5 files changed, 124 insertions(+), 98 deletions(-) diff --git a/public/splashscreen.html b/public/splashscreen.html index 21fa21c..ceabb6c 100644 --- a/public/splashscreen.html +++ b/public/splashscreen.html @@ -66,15 +66,6 @@ overflow: hidden; } - .splash::before { - content: ""; - position: absolute; - inset: 0; - background: linear-gradient(110deg, transparent 0%, rgba(160, 124, 255, 0.12) 48%, transparent 54%); - transform: translateX(-100%); - animation: card-scan 2.8s ease-in-out infinite; - } - .mark { position: relative; display: grid; @@ -145,18 +136,6 @@ animation: icon-float 2.4s ease-in-out infinite; } - .sheen { - position: absolute; - z-index: 2; - width: 112px; - height: 172px; - border-radius: 50%; - background: linear-gradient(90deg, transparent 8%, rgba(255, 255, 255, 0.18), transparent 72%); - mix-blend-mode: screen; - transform: translateX(-98px) rotate(16deg); - animation: icon-sheen 2.8s ease-in-out infinite; - } - .copy { position: relative; display: grid; @@ -202,22 +181,11 @@ to { transform: translate3d(42px, 42px, 0); } } - @keyframes card-scan { - 0%, 34% { transform: translateX(-100%); } - 72%, 100% { transform: translateX(100%); } - } - @keyframes icon-float { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-5px) scale(1.015); } } - @keyframes icon-sheen { - 0%, 18% { transform: translateX(-98px) rotate(16deg); opacity: 0; } - 44% { opacity: 0.9; } - 68%, 100% { transform: translateX(98px) rotate(16deg); opacity: 0; } - } - @keyframes halo-breathe { 0%, 100% { opacity: 0.35; transform: rotate(45deg) scale(0.95); } 50% { opacity: 0.8; transform: rotate(45deg) scale(1.04); } @@ -236,11 +204,9 @@ @media (prefers-reduced-motion: reduce) { .grid, - .splash::before, .halo, .trace, .icon, - .sheen, .bar span { animation: none; } @@ -264,7 +230,6 @@ -
GitLite diff --git a/src/App.svelte b/src/App.svelte index 7eab195..ec2ff12 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -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(); + } GitLite - +
{#each repoTabs as repo (repo.path)} -
+ + {/if} + {/if} + {#if errorMessage}
Loading history… diff --git a/src/lib/components/RepoLoadingOverlay.svelte b/src/lib/components/RepoLoadingOverlay.svelte index 7a2f3c1..1048264 100644 --- a/src/lib/components/RepoLoadingOverlay.svelte +++ b/src/lib/components/RepoLoadingOverlay.svelte @@ -17,7 +17,6 @@ -
@@ -78,17 +77,6 @@ inset 0 1px 0 rgba(255, 255, 255, 0.06); } - .repo-loading-card::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - pointer-events: none; - background: linear-gradient(110deg, transparent 0%, rgba(160, 124, 255, 0.1) 48%, transparent 54%); - transform: translateX(-100%); - animation: card-scan 2.8s ease-in-out infinite; - } - .repo-loading-mark { position: relative; width: 168px; @@ -157,18 +145,6 @@ animation: icon-float 2.4s ease-in-out infinite; } - .repo-loading-sheen { - position: absolute; - z-index: 2; - width: 108px; - height: 168px; - border-radius: 50%; - background: linear-gradient(90deg, transparent 8%, rgba(255, 255, 255, 0.18), transparent 72%); - transform: translateX(-96px) rotate(16deg); - mix-blend-mode: screen; - animation: icon-sheen 2.8s ease-in-out infinite; - } - .repo-loading-text { display: flex; flex-direction: column; @@ -213,19 +189,10 @@ @keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } } @keyframes grid-drift { to { transform: translate3d(42px, 42px, 0); } } - @keyframes card-scan { - 0%, 34% { transform: translateX(-100%); } - 72%, 100% { transform: translateX(100%); } - } @keyframes icon-float { 0%, 100% { transform: translateY(0) scale(1); } 50% { transform: translateY(-5px) scale(1.015); } } - @keyframes icon-sheen { - 0%, 18% { transform: translateX(-96px) rotate(16deg); opacity: 0; } - 44% { opacity: 0.9; } - 68%, 100% { transform: translateX(98px) rotate(16deg); opacity: 0; } - } @keyframes halo-breathe { 0%, 100% { opacity: 0.35; transform: rotate(45deg) scale(0.95); } 50% { opacity: 0.8; transform: rotate(45deg) scale(1.04); } @@ -242,11 +209,9 @@ @media (prefers-reduced-motion: reduce) { .repo-loading-grid, - .repo-loading-card::before, .repo-loading-halo, .repo-loading-traces .trace, .repo-loading-icon, - .repo-loading-sheen, .repo-loading-bar span { animation: none; } .repo-loading-traces .trace { stroke-dashoffset: 0; opacity: 0.72; } }