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)}
-
+
openRepoTabContextMenu(repo.path, event)}
+ >
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; }
}