118 lines
3.2 KiB
Svelte
118 lines
3.2 KiB
Svelte
<script lang="ts">
|
|
interface PickerItem {
|
|
value: number | string;
|
|
label: string;
|
|
}
|
|
|
|
interface Props {
|
|
items: PickerItem[];
|
|
selectedValue?: number | string;
|
|
visibleCount?: number;
|
|
itemHeight?: number;
|
|
onSelect?: (value: number | string) => void;
|
|
}
|
|
|
|
let {
|
|
items,
|
|
selectedValue = items[0]?.value,
|
|
visibleCount = 3,
|
|
itemHeight = 56,
|
|
onSelect,
|
|
}: Props = $props();
|
|
|
|
let scrollContainer: HTMLDivElement | undefined = $state();
|
|
let selectedIndex = $state(0);
|
|
|
|
const paddingCount = $derived(Math.floor(visibleCount / 2));
|
|
const containerHeight = $derived(visibleCount * itemHeight);
|
|
|
|
function handleScroll() {
|
|
if (!scrollContainer) return;
|
|
const scrollTop = scrollContainer.scrollTop;
|
|
const newIndex = Math.round(scrollTop / itemHeight);
|
|
if (newIndex >= 0 && newIndex < items.length && newIndex !== selectedIndex) {
|
|
selectedIndex = newIndex;
|
|
onSelect?.(items[newIndex].value);
|
|
}
|
|
}
|
|
|
|
function scrollToIndex(index: number) {
|
|
scrollContainer?.scrollTo({
|
|
top: index * itemHeight,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
|
|
// Initialize scroll position
|
|
$effect(() => {
|
|
if (scrollContainer) {
|
|
const idx = Math.max(0, items.findIndex((i) => i.value === selectedValue));
|
|
scrollContainer.scrollTop = idx * itemHeight;
|
|
selectedIndex = idx;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<div
|
|
class="relative overflow-hidden rounded-lg"
|
|
style="height: {containerHeight}px"
|
|
>
|
|
<!-- Highlight band for center item -->
|
|
<div
|
|
class="absolute left-0 right-0 pointer-events-none z-10 border-y border-[var(--color-jellyfin)]/40 bg-[var(--color-jellyfin)]/5 rounded"
|
|
style="top: {paddingCount * itemHeight}px; height: {itemHeight}px"
|
|
></div>
|
|
|
|
<!-- Fade gradients -->
|
|
<div class="absolute top-0 left-0 right-0 h-10 bg-gradient-to-b from-[var(--color-surface)] to-transparent z-20 pointer-events-none"></div>
|
|
<div class="absolute bottom-0 left-0 right-0 h-10 bg-gradient-to-t from-[var(--color-surface)] to-transparent z-20 pointer-events-none"></div>
|
|
|
|
<!-- Scrollable container -->
|
|
<div
|
|
bind:this={scrollContainer}
|
|
onscroll={handleScroll}
|
|
class="h-full overflow-y-auto scroll-snap-y scrollbar-none"
|
|
style="-webkit-overflow-scrolling: touch"
|
|
>
|
|
<!-- Top padding -->
|
|
{#each Array(paddingCount) as _}
|
|
<div style="height: {itemHeight}px"></div>
|
|
{/each}
|
|
|
|
<!-- Items -->
|
|
{#each items as item, i}
|
|
<button
|
|
onclick={() => scrollToIndex(i)}
|
|
class="w-full snap-center flex items-center justify-center transition-all duration-150
|
|
{i === selectedIndex
|
|
? 'text-white text-2xl font-bold'
|
|
: 'text-gray-500 text-lg font-medium opacity-60'}"
|
|
style="height: {itemHeight}px"
|
|
>
|
|
{item.label}
|
|
</button>
|
|
{/each}
|
|
|
|
<!-- Bottom padding -->
|
|
{#each Array(paddingCount) as _}
|
|
<div style="height: {itemHeight}px"></div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.scrollbar-none::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.scrollbar-none {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
.scroll-snap-y {
|
|
scroll-snap-type: y mandatory;
|
|
}
|
|
.snap-center {
|
|
scroll-snap-align: center;
|
|
}
|
|
</style>
|