mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2026-09-19 17:24:57 +02:00
ui : add unified ScrollCarousel component
Single carousel component with top/center variants, gap and scroll options, and hover-revealed chevrons. Rename the HorizontalScrollCarousel accessibility story accordingly. Assisted-by: pi
This commit is contained in:
committed by
GitHub
parent
2e875fa3d5
commit
0d574e77c0
@@ -0,0 +1,3 @@
|
||||
import ScrollCarousel from './scroll-carousel.svelte';
|
||||
|
||||
export { ScrollCarousel };
|
||||
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
|
||||
import { cn } from '$lib/components/ui/utils';
|
||||
import { ICON_CLASS_DEFAULT } from '$lib/constants';
|
||||
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;
|
||||
/** Arrow placement and styling. */
|
||||
variant?: 'top' | 'center';
|
||||
/** Called with whether the content overflows the container. */
|
||||
onScrollableChange?: (isScrollable: boolean) => void;
|
||||
/** Pixels scrolled per arrow click; defaults to ~2/3 of the container width. */
|
||||
scrollBy?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
carousel: externalCarousel,
|
||||
children,
|
||||
class: className = '',
|
||||
containerClass = '',
|
||||
gapSize = '3',
|
||||
innerClass = '',
|
||||
onScrollableChange,
|
||||
scrollBy,
|
||||
variant = 'top'
|
||||
}: Props = $props();
|
||||
|
||||
const internalCarousel = $derived(useScrollCarousel(onScrollableChange));
|
||||
const carousel = $derived(externalCarousel ?? internalCarousel);
|
||||
|
||||
const isCenter = $derived(variant === 'center');
|
||||
|
||||
function scrollLeft(event?: MouseEvent) {
|
||||
event?.stopPropagation();
|
||||
event?.preventDefault();
|
||||
|
||||
const container = carousel.scrollContainer;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
container.scrollBy({ behavior: 'smooth', left: -(scrollBy ?? container.clientWidth * 0.67) });
|
||||
}
|
||||
|
||||
function scrollRight(event?: MouseEvent) {
|
||||
event?.stopPropagation();
|
||||
event?.preventDefault();
|
||||
|
||||
const container = carousel.scrollContainer;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
container.scrollBy({ behavior: 'smooth', left: scrollBy ?? 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
|
||||
? 'opacity-0 group-hover:opacity-100'
|
||||
: 'pointer-events-none opacity-0')
|
||||
)}
|
||||
{...isCenter ? { disabled: !carousel.canScrollLeft } : {}}
|
||||
onclick={scrollLeft}
|
||||
aria-label="Scroll left"
|
||||
>
|
||||
<ChevronLeft class={ICON_CLASS_DEFAULT} />
|
||||
</button>
|
||||
|
||||
<div
|
||||
class={cn('scrollbar-hide overflow-x-auto', containerClass)}
|
||||
bind:this={carousel.scrollContainer}
|
||||
onscroll={carousel.updateScrollButtons}
|
||||
>
|
||||
<div
|
||||
class={cn('flex min-w-max', `gap-${gapSize}`, innerClass)}
|
||||
bind:this={carousel.contentContainer}
|
||||
>
|
||||
{@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
|
||||
? 'opacity-0 group-hover:opacity-100'
|
||||
: 'pointer-events-none opacity-0')
|
||||
)}
|
||||
{...isCenter ? { disabled: !carousel.canScrollRight } : {}}
|
||||
onclick={scrollRight}
|
||||
aria-label="Scroll right"
|
||||
>
|
||||
<ChevronRight class={ICON_CLASS_DEFAULT} />
|
||||
</button>
|
||||
</div>
|
||||
+7
-7
@@ -1,15 +1,15 @@
|
||||
<script module lang="ts">
|
||||
import { defineMeta } from '@storybook/addon-svelte-csf';
|
||||
import HorizontalScrollCarousel from '$lib/components/app/misc/HorizontalScrollCarousel.svelte';
|
||||
import { ScrollCarousel } from '$lib/components/ui/scroll-carousel';
|
||||
import { expect, waitFor } from 'storybook/test';
|
||||
|
||||
const { Story } = defineMeta({
|
||||
component: HorizontalScrollCarousel,
|
||||
component: ScrollCarousel,
|
||||
parameters: {
|
||||
layout: 'centered'
|
||||
},
|
||||
tags: ['!dev'],
|
||||
title: 'Components/HorizontalScrollCarousel/Accessibility'
|
||||
title: 'Components/ScrollCarousel/Accessibility'
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -33,10 +33,10 @@
|
||||
>
|
||||
<div>
|
||||
<button type="button">before</button>
|
||||
<HorizontalScrollCarousel class="w-96">
|
||||
<ScrollCarousel class="w-96" variant="center">
|
||||
<div class="h-12 w-12 shrink-0 bg-muted"></div>
|
||||
<div class="h-12 w-12 shrink-0 bg-muted"></div>
|
||||
</HorizontalScrollCarousel>
|
||||
</ScrollCarousel>
|
||||
<button type="button">after</button>
|
||||
</div>
|
||||
</Story>
|
||||
@@ -60,10 +60,10 @@
|
||||
>
|
||||
<div>
|
||||
<button type="button">before</button>
|
||||
<HorizontalScrollCarousel class="w-48">
|
||||
<ScrollCarousel class="w-48" variant="center">
|
||||
{#each [...Array(20).keys()] as i (i)}
|
||||
<div class="h-12 w-24 shrink-0 bg-muted">{i}</div>
|
||||
{/each}
|
||||
</HorizontalScrollCarousel>
|
||||
</ScrollCarousel>
|
||||
</div>
|
||||
</Story>
|
||||
Reference in New Issue
Block a user