|
|
@@ -0,0 +1,72 @@
|
|
|
+<script generics="T" lang="ts">
|
|
|
+ import { type Snippet, untrack } from 'svelte';
|
|
|
+ import { sineInOut } from 'svelte/easing';
|
|
|
+ import type { ClassValue } from 'svelte/elements';
|
|
|
+ import { fly } from 'svelte/transition';
|
|
|
+
|
|
|
+ let {
|
|
|
+ class: className = '',
|
|
|
+ items,
|
|
|
+ key,
|
|
|
+ itemComponent,
|
|
|
+ }: {
|
|
|
+ class?: ClassValue;
|
|
|
+ items: T[];
|
|
|
+ key: (item: T) => string;
|
|
|
+ itemComponent: Snippet<[T, number]>;
|
|
|
+ } = $props();
|
|
|
+
|
|
|
+ type ViewportItem = { item: T; inDelay: number };
|
|
|
+ const DELAY = 20;
|
|
|
+
|
|
|
+ let viewportItems: ViewportItem[] = $state([]);
|
|
|
+ let initialized = false;
|
|
|
+
|
|
|
+ $effect(() => {
|
|
|
+ const newItems = items;
|
|
|
+ const current = untrack(() => viewportItems);
|
|
|
+
|
|
|
+ if (!initialized) {
|
|
|
+ initialized = true;
|
|
|
+ viewportItems = newItems.map((item, idx) => ({ item, inDelay: idx * DELAY }));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const currentKeyMap = new Map(current.map((v) => [key(v.item), v]));
|
|
|
+ const newViewportItems: ViewportItem[] = [];
|
|
|
+ let addedIdx = 0;
|
|
|
+ for (const item of newItems) {
|
|
|
+ const k = key(item);
|
|
|
+ if (currentKeyMap.has(k)) {
|
|
|
+ // Preserve existing item with no delay
|
|
|
+ newViewportItems.push({ item, inDelay: 0 });
|
|
|
+ } else {
|
|
|
+ // New item with staggered in delay
|
|
|
+ newViewportItems.push({ item, inDelay: addedIdx++ * DELAY });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ document.startViewTransition(() => {
|
|
|
+ viewportItems = newViewportItems;
|
|
|
+ });
|
|
|
+ });
|
|
|
+</script>
|
|
|
+
|
|
|
+<div class={['', className]}>
|
|
|
+ {#each viewportItems as { item, inDelay }, idx (key(item))}
|
|
|
+ {@const itemKey = key(item)}
|
|
|
+ <div
|
|
|
+ style={`view-transition-name: ${itemKey};`}
|
|
|
+ in:fly={{ x: -20, y: -20, duration: 200, opacity: 0, delay: inDelay, easing: sineInOut }}
|
|
|
+ >
|
|
|
+ {@render itemComponent(item, idx)}
|
|
|
+ </div>
|
|
|
+ {/each}
|
|
|
+</div>
|
|
|
+
|
|
|
+<style>
|
|
|
+ ::view-transition-old(*),
|
|
|
+ ::view-transition-new(*) {
|
|
|
+ animation-duration: 0.2s;
|
|
|
+ }
|
|
|
+</style>
|