Files
jellytau/src/lib/components/home/Carousel.svelte
T
dtourolle 589f08b873 feat(home): tap opens detail, long-press plays from home cards
Home carousel cards route a tap to the item's detail / Episode Focus
View and a ~500ms long-press to a confirm-then-play flow. MediaCard gains
an onLongPress prop with pointer-based detection (cancelled on >10px move
so carousel scroll is unaffected, trailing click suppressed). Episode
taps route to /library/<seriesId>?episode=<id>; the bare-episode detail
page links back to its parent series/season.

TRACES: UR-058 | DR-087
2026-07-24 23:49:03 +02:00

105 lines
3.0 KiB
Svelte

<!-- TRACES: UR-034 | DR-039 -->
<script lang="ts">
import type { MediaItem } from "$lib/api/types";
import MediaCard from "$lib/components/library/MediaCard.svelte";
interface Props {
title: string;
items: MediaItem[];
onItemClick?: (item: MediaItem) => void;
onItemLongPress?: (item: MediaItem) => void;
showAll?: () => void;
}
let { title, items, onItemClick, onItemLongPress, showAll }: Props = $props();
let scrollContainer: HTMLDivElement | null = $state(null);
let showLeftArrow = $state(false);
let showRightArrow = $state(true);
function handleScroll() {
if (!scrollContainer) return;
showLeftArrow = scrollContainer.scrollLeft > 0;
showRightArrow =
scrollContainer.scrollLeft <
scrollContainer.scrollWidth - scrollContainer.clientWidth - 10;
}
function scrollLeft() {
scrollContainer?.scrollBy({ left: -600, behavior: "smooth" });
}
function scrollRight() {
scrollContainer?.scrollBy({ left: 600, behavior: "smooth" });
}
</script>
<div class="space-y-3">
<!-- Header -->
<div class="flex items-center justify-between px-4">
<h2 class="text-2xl font-semibold text-white">{title}</h2>
{#if showAll}
<button
onclick={showAll}
class="text-sm text-gray-400 hover:text-white transition-colors"
>
See all
</button>
{/if}
</div>
<!-- Scrollable row -->
<div class="relative group">
<div
bind:this={scrollContainer}
onscroll={handleScroll}
class="flex gap-4 overflow-x-auto scrollbar-hide scroll-smooth px-4 pb-4"
>
{#each items as item (item.id)}
<MediaCard
{item}
size="medium"
showProgress={true}
onclick={() => onItemClick?.(item)}
onLongPress={onItemLongPress ? () => onItemLongPress(item) : undefined}
/>
{/each}
</div>
<!-- Navigation arrows (Spotify-style - only show on hover) -->
{#if showLeftArrow}
<button
onclick={scrollLeft}
class="absolute left-0 top-1/2 -translate-y-1/2 p-2 bg-black/80 hover:bg-black rounded-full opacity-0 group-hover:opacity-100 transition-opacity z-10 ml-2"
aria-label="Scroll left"
>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/>
</svg>
</button>
{/if}
{#if showRightArrow}
<button
onclick={scrollRight}
class="absolute right-0 top-1/2 -translate-y-1/2 p-2 bg-black/80 hover:bg-black rounded-full opacity-0 group-hover:opacity-100 transition-opacity z-10 mr-2"
aria-label="Scroll right"
>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/>
</svg>
</button>
{/if}
</div>
</div>
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
scrollbar-width: none;
-ms-overflow-style: none;
}
</style>