File size: 4,024 Bytes
c59fd93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
<script lang="ts">
	import { ChevronLeft, ChevronRight } from '@lucide/svelte';
	import { cn } from '$lib/components/ui/utils';
	import { ICON_CLASS_DEFAULT } from '$lib/constants';
	import { ScrollCarouselVariant } from '$lib/enums';
	import { useScrollCarousel } from '$lib/hooks/use-scroll-carousel.svelte';
	import type { Snippet } from 'svelte';

	interface Props {
		children: Snippet;
		/** External carousel hook for callers that need to drive it (e.g. scrollToCenter). */
		carousel?: ReturnType<typeof useScrollCarousel>;
		/** Classes for the outer relative wrapper. */
		class?: string;
		/** Classes for the scrollable overflow container. */
		containerClass?: string;
		/** Classes for the min-w-max content wrapper. */
		innerClass?: string;
		/** Tailwind gap class applied to the content wrapper. */
		gapSize?: string;
		/** Show the arrows whenever the content overflows, even without hover. */
		alwaysShowArrows?: boolean;
		/** Arrow placement and styling. */
		variant?: ScrollCarouselVariant;
	}

	let {
		alwaysShowArrows = false,
		carousel: externalCarousel,
		children,
		class: className = '',
		containerClass = '',
		gapSize = '3',
		innerClass = '',
		variant = ScrollCarouselVariant.TOP
	}: Props = $props();

	const internalCarousel = useScrollCarousel();
	const carousel = $derived(externalCarousel ?? internalCarousel);

	const isCenter = $derived(variant === ScrollCarouselVariant.CENTER);

	function scrollLeft(event?: MouseEvent) {
		event?.stopPropagation();
		event?.preventDefault();

		const container = carousel.scrollContainer;

		if (!container) return;

		container.scrollBy({ behavior: 'smooth', left: -(container.clientWidth * 0.67) });
	}

	function scrollRight(event?: MouseEvent) {
		event?.stopPropagation();
		event?.preventDefault();

		const container = carousel.scrollContainer;

		if (!container) return;

		container.scrollBy({ behavior: 'smooth', left: container.clientWidth * 0.67 });
	}

	export function resetScroll() {
		const container = carousel.scrollContainer;

		if (!container) return;

		container.scrollLeft = 0;
		setTimeout(() => carousel.updateScrollButtons(), 0);
	}
</script>

<div
	class={cn('group relative', !isCenter && 'flex items-center', className)}
	style={!isCenter ? 'scroll-padding: 1rem;' : undefined}
>
	<button
		class={cn(
			'absolute z-10 flex h-6 w-6 items-center justify-center rounded-full shadow-md transition-opacity',
			isCenter
				? 'top-1/2 left-4 -translate-y-1/2 bg-background/25 backdrop-blur-xs hover:bg-background/45 disabled:pointer-events-none disabled:opacity-0'
				: 'left-2 bg-muted backdrop-blur-sm hover:bg-accent',
			!isCenter &&
				(carousel.canScrollLeft
					? alwaysShowArrows
						? 'opacity-100'
						: 'opacity-0 group-hover:opacity-100'
					: 'pointer-events-none opacity-0')
		)}
		{...isCenter ? { disabled: !carousel.canScrollLeft } : {}}
		aria-label="Scroll left"
		onclick={scrollLeft}
	>
		<ChevronLeft class={ICON_CLASS_DEFAULT} />
	</button>

	<div
		bind:this={carousel.scrollContainer}
		class={cn('scrollbar-hide overflow-x-auto', containerClass)}
		onscroll={carousel.updateScrollButtons}
	>
		<div
			bind:this={carousel.contentContainer}
			class={cn('flex min-w-max', isCenter && 'items-start', `gap-${gapSize}`, innerClass)}
		>
			{@render children?.()}
		</div>
	</div>

	<button
		class={cn(
			'absolute z-10 flex h-6 w-6 items-center justify-center rounded-full shadow-md transition-opacity',
			isCenter
				? 'top-1/2 right-4 -translate-y-1/2 bg-background/25 backdrop-blur-xs hover:bg-background/45 disabled:pointer-events-none disabled:opacity-0'
				: 'right-2 bg-muted backdrop-blur-sm hover:bg-accent',
			!isCenter &&
				(carousel.canScrollRight
					? alwaysShowArrows
						? 'opacity-100'
						: 'opacity-0 group-hover:opacity-100'
					: 'pointer-events-none opacity-0')
		)}
		{...isCenter ? { disabled: !carousel.canScrollRight } : {}}
		aria-label="Scroll right"
		onclick={scrollRight}
	>
		<ChevronRight class={ICON_CLASS_DEFAULT} />
	</button>
</div>