improvements to the sleep timer
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user