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; } }