refactor(ui): update shared components and add ControlGroup, SidebarContainer
This commit is contained in:
@@ -13,100 +13,57 @@ import { createVirtualizer } from '$shared/lib';
|
||||
import { throttle } from '$shared/lib/utils';
|
||||
import { cn } from '$shared/shadcn/utils/shadcn-utils';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
interface Props {
|
||||
interface Props extends
|
||||
Omit<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
'children'
|
||||
>
|
||||
{
|
||||
/**
|
||||
* Array of items to render in the virtual list.
|
||||
*
|
||||
* @template T - The type of items in the list
|
||||
* Items array
|
||||
*/
|
||||
items: T[];
|
||||
/**
|
||||
* Total number of items (including not-yet-loaded items for pagination).
|
||||
* If not provided, defaults to items.length.
|
||||
*
|
||||
* Use this when implementing pagination to ensure the scrollbar
|
||||
* reflects the total count of items, not just the loaded ones.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // Pagination scenario: 1920 total fonts, but only 50 loaded
|
||||
* <VirtualList items={loadedFonts} total={1920}>
|
||||
* ```
|
||||
* Total item count
|
||||
* @default items.length
|
||||
*/
|
||||
total?: number;
|
||||
/**
|
||||
* Height for each item, either as a fixed number
|
||||
* or a function that returns height per index.
|
||||
* Item height
|
||||
* @default 80
|
||||
*/
|
||||
itemHeight?: number | ((index: number) => number);
|
||||
/**
|
||||
* Optional overscan value for the virtual list.
|
||||
* Overscan items
|
||||
* @default 5
|
||||
*/
|
||||
overscan?: number;
|
||||
/**
|
||||
* Optional CSS class string for styling the container
|
||||
* (follows shadcn convention for className prop)
|
||||
* CSS classes
|
||||
*/
|
||||
class?: string;
|
||||
/**
|
||||
* Number of columns for grid layout.
|
||||
* Grid columns
|
||||
* @default 1
|
||||
*/
|
||||
columns?: number;
|
||||
/**
|
||||
* Gap between items in pixels.
|
||||
* Item gap in pixels
|
||||
* @default 0
|
||||
*/
|
||||
gap?: number;
|
||||
/**
|
||||
* An optional callback that will be called for each new set of loaded items
|
||||
* @param items - Loaded items
|
||||
* Visible items change callback
|
||||
*/
|
||||
onVisibleItemsChange?: (items: T[]) => void;
|
||||
/**
|
||||
* An optional callback that will be called when user scrolls near the end of the list.
|
||||
* Useful for triggering auto-pagination.
|
||||
*
|
||||
* The callback receives the index of the last visible item. You can use this
|
||||
* to determine if you should load more data.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* onNearBottom={(lastVisibleIndex) => {
|
||||
* const itemsRemaining = total - lastVisibleIndex;
|
||||
* if (itemsRemaining < 5 && hasMore && !isFetching) {
|
||||
* loadMore();
|
||||
* }
|
||||
* }}
|
||||
* ```
|
||||
* Near bottom callback
|
||||
*/
|
||||
onNearBottom?: (lastVisibleIndex: number) => void;
|
||||
/**
|
||||
* Snippet for rendering individual list items.
|
||||
*
|
||||
* The snippet receives an object containing:
|
||||
* - `item`: The item from the items array (type T)
|
||||
* - `index`: The current item's index in the array
|
||||
*
|
||||
* This pattern provides type safety and flexibility for
|
||||
* rendering different item types without prop drilling.
|
||||
*
|
||||
* @template T - The type of items in the list
|
||||
*/
|
||||
/**
|
||||
* Snippet for rendering individual list items.
|
||||
*
|
||||
* The snippet receives an object containing:
|
||||
* - `item`: The item from the items array (type T)
|
||||
* - `index`: The current item's index in the array
|
||||
*
|
||||
* This pattern provides type safety and flexibility for
|
||||
* rendering different item types without prop drilling.
|
||||
*
|
||||
* @template T - The type of items in the list
|
||||
* Item render snippet
|
||||
*/
|
||||
children: Snippet<
|
||||
[
|
||||
@@ -120,12 +77,12 @@ interface Props {
|
||||
]
|
||||
>;
|
||||
/**
|
||||
* Whether to use the window as the scroll container.
|
||||
* Use window scroll
|
||||
* @default false
|
||||
*/
|
||||
useWindowScroll?: boolean;
|
||||
/**
|
||||
* Flag to show loading state
|
||||
* Loading state
|
||||
*/
|
||||
isLoading?: boolean;
|
||||
}
|
||||
@@ -143,6 +100,7 @@ let {
|
||||
isLoading = false,
|
||||
columns = 1,
|
||||
gap = 0,
|
||||
...rest
|
||||
}: Props = $props();
|
||||
|
||||
// Reference to the scroll container element for attaching the virtualizer
|
||||
@@ -208,7 +166,7 @@ const throttledVisibleChange = throttle((visibleItems: T[]) => {
|
||||
|
||||
const throttledNearBottom = throttle((lastVisibleIndex: number) => {
|
||||
onNearBottom?.(lastVisibleIndex);
|
||||
}, 200); // 200ms debounce
|
||||
}, 200); // 200ms throttle
|
||||
|
||||
// Calculate top/bottom padding for spacer elements
|
||||
// In CSS Grid, gap creates space BETWEEN elements.
|
||||
@@ -245,8 +203,11 @@ $effect(() => {
|
||||
|
||||
$effect(() => {
|
||||
// Trigger onNearBottom when user scrolls near the end of loaded items (within 5 items)
|
||||
// Only trigger if container has sufficient height to avoid false positives
|
||||
if (virtualizer.items.length > 0 && onNearBottom && virtualizer.containerHeight > 100) {
|
||||
if (
|
||||
virtualizer.items.length > 0
|
||||
&& onNearBottom
|
||||
&& virtualizer.containerHeight > 100
|
||||
) {
|
||||
const lastVisibleRow = virtualizer.items[virtualizer.items.length - 1];
|
||||
// Convert row index to last item index in that row
|
||||
const lastVisibleItemIndex = Math.min(
|
||||
@@ -256,7 +217,10 @@ $effect(() => {
|
||||
// Compare against loaded items length, not total
|
||||
const itemsRemaining = items.length - lastVisibleItemIndex;
|
||||
|
||||
if (itemsRemaining <= 5) {
|
||||
// Only trigger if user has scrolled (prevents loading on mount)
|
||||
const hasScrolled = virtualizer.scrollOffset > 0;
|
||||
|
||||
if (itemsRemaining <= 5 && hasScrolled) {
|
||||
throttledNearBottom(lastVisibleItemIndex);
|
||||
}
|
||||
}
|
||||
@@ -329,7 +293,7 @@ $effect(() => {
|
||||
{/snippet}
|
||||
|
||||
{#if useWindowScroll}
|
||||
<div class={cn('relative w-full', className)} bind:this={viewportRef}>
|
||||
<div class={cn('relative w-full', className)} bind:this={viewportRef} {...rest}>
|
||||
{@render content()}
|
||||
</div>
|
||||
{:else}
|
||||
@@ -338,9 +302,10 @@ $effect(() => {
|
||||
class={cn(
|
||||
'relative overflow-y-auto overflow-x-hidden',
|
||||
'rounded-md bg-background',
|
||||
'w-full min-h-[200px]',
|
||||
'w-full',
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{@render content()}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user