fix(home): restart the hero banner timer on a manual change
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 14m18s
🏗️ Build and Test JellyTau / Android Compile Check (push) Skipped
Publish Documentation / Build & publish docs to gitea-pages (push) Successful in 5m31s
Traceability Validation / Check Requirement Traces (push) Successful in 17s
Build & Release / Run Tests (push) Failing after 14m7s
Build & Release / Build Linux (push) Skipped
Build & Release / Build Windows (push) Skipped
Build & Release / Build Android (push) Skipped
Build & Release / Create Release (push) Skipped

The rotation interval was installed once when the banner mounted and never
touched again, so a swipe, arrow or dot tap inherited whatever was left of the
running countdown — swiping 5.5s into a 6s interval moved the banner on half a
second later.

The timer moves into heroRotation.ts as a small restartable object so it can be
unit-tested, and every manual navigation path restarts it from that moment.
Verified red-first: with restart() reverted to leave a running timer alone, the
regression test fails.

Release 0.9.1.
This commit is contained in:
2026-08-20 23:11:30 +02:00
parent 98b2ede8bd
commit 16658889a2
11 changed files with 168 additions and 12 deletions
+25 -7
View File
@@ -3,6 +3,7 @@
import { goto } from "$app/navigation";
import type { MediaItem } from "$lib/api/types";
import CachedImage from "$lib/components/common/CachedImage.svelte";
import { createRotationTimer, type RotationTimer } from "./heroRotation";
interface Props {
items: MediaItem[];
@@ -13,7 +14,7 @@
let { items, autoRotate = true, interval = 6000 }: Props = $props();
let currentIndex = $state(0);
let intervalId: number | null = null;
let rotation: RotationTimer | null = null;
// Touch/swipe state
let touchStartX = $state(0);
@@ -70,8 +71,22 @@
currentIndex = (currentIndex - 1 + items.length) % items.length;
}
// Manual navigation (swipe, arrows, dots) restarts the countdown, so the
// banner always waits a full interval after the last change instead of
// firing whatever was left of the previous one.
function showNext() {
next();
rotation?.restart();
}
function showPrev() {
prev();
rotation?.restart();
}
function goToIndex(idx: number) {
currentIndex = idx;
rotation?.restart();
}
// Touch/swipe handlers
@@ -96,10 +111,10 @@
if (Math.abs(diff) > swipeThreshold) {
if (diff > 0) {
// Swiped left - go to next
next();
showNext();
} else {
// Swiped right - go to previous
prev();
showPrev();
}
}
@@ -110,9 +125,12 @@
// Auto-rotate logic
$effect(() => {
if (autoRotate && items.length > 1) {
intervalId = window.setInterval(next, interval);
const timer = createRotationTimer(interval, next);
rotation = timer;
timer.restart();
return () => {
if (intervalId) clearInterval(intervalId);
timer.stop();
rotation = null;
};
}
});
@@ -219,7 +237,7 @@
<!-- Swipe Indicators (Desktop hover) -->
<button
onclick={prev}
onclick={showPrev}
class="absolute left-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/40 backdrop-blur-sm rounded-full items-center justify-center transition-opacity opacity-0 group-hover:opacity-100 hover:bg-black/60 hidden md:flex"
aria-label="Previous item"
>
@@ -229,7 +247,7 @@
</button>
<button
onclick={next}
onclick={showNext}
class="absolute right-4 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-black/40 backdrop-blur-sm rounded-full items-center justify-center transition-opacity opacity-0 group-hover:opacity-100 hover:bg-black/60 hidden md:flex"
aria-label="Next item"
>