1
0
forked from wrenn/wrenn

Fix capsules table blink on background poll refresh

Poll fetches now silently update data without triggering loading
states, spinner animations, or row fadeUp re-animations. Only manual
refresh shows the spin indicator.
This commit is contained in:
2026-03-25 19:44:13 +06:00
parent b0e6f5ffb3
commit 8d5ba3873a

View File

@ -56,6 +56,9 @@
// Briefly highlight a newly created capsule row // Briefly highlight a newly created capsule row
let newCapsuleId = $state<string | null>(null); let newCapsuleId = $state<string | null>(null);
// Track whether initial load animation has played (suppress on poll refreshes)
let initialAnimationDone = $state(false);
let filteredCapsules = $derived.by(() => { let filteredCapsules = $derived.by(() => {
let list = searchQuery let list = searchQuery
? capsules.filter((c) => c.id.toLowerCase().includes(searchQuery.toLowerCase())) ? capsules.filter((c) => c.id.toLowerCase().includes(searchQuery.toLowerCase()))
@ -121,26 +124,32 @@
} }
} }
async function fetchCapsules() { async function fetchCapsules(manual = false) {
const wasEmpty = capsules.length === 0; const wasEmpty = capsules.length === 0;
if (wasEmpty) loading = true; if (wasEmpty) loading = true;
spinning = true; if (manual) {
const spinTimer = new Promise<void>((resolve) => setTimeout(resolve, SPIN_DURATION)); spinning = true;
var spinTimer = new Promise<void>((resolve) => setTimeout(resolve, SPIN_DURATION));
}
error = null;
const result = await listCapsules(); const result = await listCapsules();
if (result.ok) { if (result.ok) {
capsules = result.data; capsules = result.data;
} else {
error = result.error;
} }
loading = false; loading = false;
// Mark initial entrance animation as done after first successful fetch
if (!initialAnimationDone) {
setTimeout(() => { initialAnimationDone = true; }, 400 + (capsules.length * 40));
}
if (autoRefresh) countdown = REFRESH_INTERVAL; if (autoRefresh) countdown = REFRESH_INTERVAL;
await spinTimer; if (manual) {
spinning = false; await spinTimer!;
spinning = false;
}
} }
async function handlePause(id: string) { async function handlePause(id: string) {
@ -297,7 +306,7 @@
<!-- Refresh button --> <!-- Refresh button -->
<button <button
onclick={fetchCapsules} onclick={() => fetchCapsules(true)}
disabled={spinning} disabled={spinning}
class="flex h-8 w-8 items-center justify-center rounded-[var(--radius-button)] border border-[var(--color-border)] text-[var(--color-text-tertiary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-secondary)] disabled:opacity-50" class="flex h-8 w-8 items-center justify-center rounded-[var(--radius-button)] border border-[var(--color-border)] text-[var(--color-text-tertiary)] transition-colors duration-150 hover:border-[var(--color-border-mid)] hover:text-[var(--color-text-secondary)] disabled:opacity-50"
title="Refresh" title="Refresh"
@ -415,7 +424,7 @@
{@const stripeColor = capsule.status === 'running' ? 'bg-[var(--color-accent)]' : capsule.status === 'paused' ? 'bg-[var(--color-amber)]' : 'bg-[var(--color-text-muted)]'} {@const stripeColor = capsule.status === 'running' ? 'bg-[var(--color-accent)]' : capsule.status === 'paused' ? 'bg-[var(--color-amber)]' : 'bg-[var(--color-text-muted)]'}
<div <div
class="capsule-row relative grid grid-cols-[1.6fr_0.8fr_0.5fr_0.5fr_0.6fr_1fr_0.9fr] items-center overflow-hidden border-b border-[var(--color-border)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] last:border-b-0 {newCapsuleId === capsule.id ? 'capsule-born' : ''}" class="capsule-row relative grid grid-cols-[1.6fr_0.8fr_0.5fr_0.5fr_0.6fr_1fr_0.9fr] items-center overflow-hidden border-b border-[var(--color-border)] transition-colors duration-150 hover:bg-[var(--color-bg-3)] last:border-b-0 {newCapsuleId === capsule.id ? 'capsule-born' : ''}"
style="animation: fadeUp 0.35s ease both; animation-delay: {i * 40}ms" style={initialAnimationDone ? '' : `animation: fadeUp 0.35s ease both; animation-delay: ${i * 40}ms`}
> >
<!-- Left accent stripe --> <!-- Left accent stripe -->
<div class="row-stripe pointer-events-none absolute left-0 top-0 h-full w-0.5 {stripeColor}"></div> <div class="row-stripe pointer-events-none absolute left-0 top-0 h-full w-0.5 {stripeColor}"></div>