feat(status-panel): enhance UI and add new functionality
publish / publish-tauri (appimage, 1, ubuntu-22.04, linux-x86_64) (release) Successful in 18m10s
publish / publish-tauri (nsis, , windows-latest, windows-x86_64) (release) Successful in 30m34s

This update introduces several improvements to the StatusPanel component, enhancing the user interface and experience. A new function has been added to simplify file path handling, and the overlay now features a more dynamic design with animated elements for better visual feedback during operations.

- Added baseName function for improved path handling
- Enhanced status overlay with animations and new design elements
- Updated file staging and unstaging messages for clarity
This commit is contained in:
Christoph Brandau
2026-07-09 19:51:51 +02:00
parent 710c6d4a53
commit 35d9fc969c
3 changed files with 155 additions and 21 deletions
+2 -1
View File
@@ -99,7 +99,8 @@
"Bash(xxd -l 16 src-tauri/icons/GitCat.ico)", "Bash(xxd -l 16 src-tauri/icons/GitCat.ico)",
"Bash(pkg-config --exists openssl)", "Bash(pkg-config --exists openssl)",
"Bash(sudo apt-get install -y libssl-dev pkg-config)", "Bash(sudo apt-get install -y libssl-dev pkg-config)",
"Bash(dpkg -L libssl3t64)" "Bash(dpkg -L libssl3t64)",
"Bash(grep *)"
] ]
} }
} }
+7 -3
View File
@@ -938,6 +938,10 @@
return repoKey(left) === repoKey(right); return repoKey(left) === repoKey(right);
} }
function baseName(path: string): string {
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
}
function uniqueRepoPaths(paths: string[]): string[] { function uniqueRepoPaths(paths: string[]): string[] {
const seen = new Set<string>(); const seen = new Set<string>();
const result: string[] = []; const result: string[] = [];
@@ -2554,7 +2558,7 @@
const targets = files.filter((file) => file.unstaged !== null); const targets = files.filter((file) => file.unstaged !== null);
if (targets.length === 0) return; if (targets.length === 0) return;
const paths = targets.map((file) => file.path); const paths = targets.map((file) => file.path);
await runOperation(targets.length === 1 ? `Staging ${targets[0].path}` : `Staging ${targets.length} files`, async () => { await runOperation(targets.length === 1 ? `Staging ${baseName(targets[0].path)}` : `Staging ${targets.length} files`, async () => {
applyStatus(await stageFiles(activeRepoPath, paths)); applyStatus(await stageFiles(activeRepoPath, paths));
await refreshExplorerFiles(activeRepoPath); await refreshExplorerFiles(activeRepoPath);
trackEvent("file_staged", { trackEvent("file_staged", {
@@ -2568,7 +2572,7 @@
const targets = files.filter((file) => file.staged !== null); const targets = files.filter((file) => file.staged !== null);
if (targets.length === 0) return; if (targets.length === 0) return;
const paths = targets.map((file) => file.path); const paths = targets.map((file) => file.path);
await runOperation(targets.length === 1 ? `Unstaging ${targets[0].path}` : `Unstaging ${targets.length} files`, async () => { await runOperation(targets.length === 1 ? `Unstaging ${baseName(targets[0].path)}` : `Unstaging ${targets.length} files`, async () => {
applyStatus(await unstageFiles(activeRepoPath, paths)); applyStatus(await unstageFiles(activeRepoPath, paths));
await refreshExplorerFiles(activeRepoPath); await refreshExplorerFiles(activeRepoPath);
trackEvent("file_unstaged", { trackEvent("file_unstaged", {
@@ -3372,7 +3376,7 @@
</section> </section>
{/if} {/if}
{#if operation && operation !== "Opening repository"} {#if operation && operation !== "Opening repository" && !hasRepository}
<section class="notice busy" aria-live="polite"> <section class="notice busy" aria-live="polite">
<LoaderCircle class="spin" size={17} aria-hidden="true" /> <LoaderCircle class="spin" size={17} aria-hidden="true" />
<span>{operation}</span> <span>{operation}</span>
+146 -17
View File
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { Check, FileDiff, LoaderCircle, RotateCcw, Undo2 } from "@lucide/svelte"; import { Check, FileDiff, RotateCcw, Undo2 } from "@lucide/svelte";
import iconUrl from "../../../src-tauri/icons/icon.png";
import type { FileStatusKind, GitFileStatus, GitStatus } from "../types"; import type { FileStatusKind, GitFileStatus, GitStatus } from "../types";
interface Props { interface Props {
@@ -284,8 +285,18 @@
{#if isBusy && hasRepository} {#if isBusy && hasRepository}
<div class="status-panel-overlay" role="status" aria-live="polite"> <div class="status-panel-overlay" role="status" aria-live="polite">
<div class="status-panel-overlay-card"> <div class="status-panel-overlay-card">
<LoaderCircle class="spin" size={22} aria-hidden="true" /> <div class="status-panel-overlay-mark">
<span>{operation || "Working"}</span> <span class="status-panel-overlay-halo halo-one"></span>
<span class="status-panel-overlay-halo halo-two"></span>
<svg class="status-panel-overlay-traces" viewBox="0 0 220 220" aria-hidden="true">
<path class="trace trace-main" d="M28 154 C72 114, 88 108, 110 110 S156 116, 192 68" />
<path class="trace trace-branch" d="M62 74 C100 88, 126 124, 158 166" />
<path class="trace trace-cut" d="M46 180 L174 180" />
</svg>
<img src={iconUrl} alt="" class="status-panel-overlay-icon" />
</div>
<span class="status-panel-overlay-label">{operation || "Working"}</span>
<div class="status-panel-overlay-bar"><span></span></div>
</div> </div>
</div> </div>
{/if} {/if}
@@ -298,31 +309,149 @@
z-index: 20; z-index: 20;
display: grid; display: grid;
place-items: center; place-items: center;
background: rgba(8, 9, 16, 0.5); background:
backdrop-filter: blur(3px); radial-gradient(circle at 50% 38%, rgba(137, 92, 255, 0.16), transparent 55%),
rgba(8, 9, 16, 0.58);
backdrop-filter: blur(4px);
animation: status-panel-overlay-in 120ms ease; animation: status-panel-overlay-in 120ms ease;
} }
.status-panel-overlay-card { .status-panel-overlay-card {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 10px; gap: 12px;
padding: 12px 20px; width: min(300px, calc(100% - 32px));
border: 1px solid var(--color-border); padding: 26px 28px 26px;
border-radius: 10px; border: 1px solid rgba(160, 124, 255, 0.24);
background: var(--color-surface-raised); border-radius: 16px;
background:
linear-gradient(180deg, rgba(23, 26, 43, 0.9), rgba(12, 15, 27, 0.92)),
var(--color-surface-raised);
color: var(--color-ink); color: var(--color-ink);
font-size: 13px; box-shadow: 0 18px 44px rgba(0, 0, 0, 0.38), inset 0 1px 0 rgba(255, 255, 255, 0.06);
font-weight: 600;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.38);
} }
@keyframes status-panel-overlay-in { .status-panel-overlay-mark {
from { opacity: 0; } position: relative;
to { opacity: 1; } width: 104px;
height: 104px;
display: grid;
place-items: center;
isolation: isolate;
}
.status-panel-overlay-halo {
position: absolute;
inset: 6px;
border: 1px solid rgba(151, 118, 255, 0.24);
border-radius: 22px;
transform: rotate(45deg);
}
.status-panel-overlay-halo.halo-one {
animation: status-panel-overlay-halo-breathe 2.4s ease-in-out infinite;
}
.status-panel-overlay-halo.halo-two {
inset: 16px;
border-color: rgba(255, 109, 38, 0.26);
animation: status-panel-overlay-halo-breathe 2.4s ease-in-out infinite reverse;
}
.status-panel-overlay-traces {
position: absolute;
inset: -12px;
width: 128px;
height: 128px;
overflow: visible;
z-index: 0;
}
.status-panel-overlay-traces .trace {
fill: none;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: 165;
stroke-dashoffset: 165;
filter: drop-shadow(0 0 6px rgba(151, 118, 255, 0.55));
animation: status-panel-overlay-trace-draw 2.6s ease-in-out infinite;
}
.status-panel-overlay-traces .trace-main { stroke: #9b70ff; }
.status-panel-overlay-traces .trace-branch {
stroke: #ff6d26;
animation-delay: 0.28s;
}
.status-panel-overlay-traces .trace-cut {
stroke: rgba(255, 255, 255, 0.42);
stroke-dasharray: 128;
stroke-dashoffset: 128;
animation-delay: 0.55s;
}
.status-panel-overlay-icon {
position: relative;
z-index: 1;
width: 64px;
height: 64px;
object-fit: contain;
filter:
drop-shadow(0 8px 12px rgba(0, 0, 0, 0.5))
drop-shadow(0 0 10px rgba(151, 118, 255, 0.28));
animation: status-panel-overlay-icon-float 2.4s ease-in-out infinite;
}
.status-panel-overlay-label {
max-width: 100%;
overflow: hidden;
color: var(--color-ink);
font-size: 14px;
font-weight: 700;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.status-panel-overlay-bar {
position: relative;
width: min(190px, 100%);
height: 4px;
overflow: hidden;
border-radius: 999px;
background: rgba(151, 118, 255, 0.14);
}
.status-panel-overlay-bar span {
position: absolute;
inset: 0;
width: 46%;
border-radius: inherit;
background: linear-gradient(90deg, transparent, #9b70ff 40%, #ff6d26 74%, transparent);
box-shadow: 0 0 12px rgba(255, 109, 38, 0.32);
animation: status-panel-overlay-bar-slide 1.35s ease-in-out infinite;
}
@keyframes status-panel-overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes status-panel-overlay-icon-float {
0%, 100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-3px) scale(1.015); }
}
@keyframes status-panel-overlay-halo-breathe {
0%, 100% { opacity: 0.35; transform: rotate(45deg) scale(0.95); }
50% { opacity: 0.8; transform: rotate(45deg) scale(1.04); }
}
@keyframes status-panel-overlay-bar-slide {
0% { transform: translateX(-120%); }
100% { transform: translateX(320%); }
}
@keyframes status-panel-overlay-trace-draw {
0% { stroke-dashoffset: 165; opacity: 0; }
36% { opacity: 1; }
64%, 100% { stroke-dashoffset: 0; opacity: 0.72; }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.status-panel-overlay { animation: none; } .status-panel-overlay,
.status-panel-overlay-halo,
.status-panel-overlay-traces .trace,
.status-panel-overlay-icon,
.status-panel-overlay-bar span { animation: none; }
.status-panel-overlay-traces .trace { stroke-dashoffset: 0; opacity: 0.72; }
} }
</style> </style>