fix(FontList): address the bug with selected font transition animations
This commit is contained in:
@@ -8,65 +8,67 @@ import {
|
||||
FontApplicator,
|
||||
FontVirtualList,
|
||||
type UnifiedFont,
|
||||
VIRTUAL_INDEX_NOT_LOADED,
|
||||
fontStore,
|
||||
} from '$entities/Font';
|
||||
import {
|
||||
Button,
|
||||
Label,
|
||||
} from '$shared/ui';
|
||||
import DotIcon from '@lucide/svelte/icons/dot';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
import { crossfade } from 'svelte/transition';
|
||||
import { comparisonStore } from '../../model';
|
||||
import {
|
||||
createDotCrossfade,
|
||||
getDotTransitionParams,
|
||||
} from '../../lib';
|
||||
import {
|
||||
type Side,
|
||||
comparisonStore,
|
||||
} from '../../model';
|
||||
|
||||
const side = $derived(comparisonStore.side);
|
||||
let prevIndexA: number | null = null;
|
||||
let prevIndexB: number | null = null;
|
||||
let selectedIndexA: number | null = null;
|
||||
let selectedIndexB: number | null = null;
|
||||
let pendingDirection: 1 | -1 = 1;
|
||||
|
||||
const [send, receive] = crossfade({
|
||||
duration: 300,
|
||||
easing: cubicOut,
|
||||
fallback(node) {
|
||||
// Read pendingDirection synchronously — no reactive timing issues
|
||||
const fromY = pendingDirection * (node.closest('[data-font-list]')?.clientHeight ?? 300);
|
||||
return {
|
||||
duration: 300,
|
||||
easing: cubicOut,
|
||||
css: t => `transform: translateY(${fromY * (1 - t)}px);`,
|
||||
};
|
||||
},
|
||||
// Treat -1 (not loaded) as being at the very bottom of the infinite list
|
||||
function getVirtualIndex(fontId: string | undefined): number {
|
||||
if (!fontId) {
|
||||
return -1;
|
||||
}
|
||||
const idx = fontStore.fonts.findIndex(f => f.id === fontId);
|
||||
if (idx === -1) {
|
||||
return VIRTUAL_INDEX_NOT_LOADED;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
// Reactive indices of the currently selected fonts in the full list
|
||||
const indexA = $derived(getVirtualIndex(comparisonStore.fontA?.id));
|
||||
const indexB = $derived(getVirtualIndex(comparisonStore.fontB?.id));
|
||||
|
||||
// Track previous state for directional fallback transitions.
|
||||
// We use plain variables here. In Svelte 5, updates to these in an $effect
|
||||
// happen AFTER the render/DOM update, so transitions starting as a result
|
||||
// of that update will see the "old" values of these variables.
|
||||
let prevIndexA = indexA;
|
||||
let prevIndexB = indexB;
|
||||
let prevSide: Side = side;
|
||||
|
||||
const [send, receive] = createDotCrossfade();
|
||||
|
||||
$effect(() => {
|
||||
// This effect runs after every change to indexA, indexB, or side.
|
||||
// It captures the "current" values which will serve as "previous" values
|
||||
// for the NEXT transition.
|
||||
prevIndexA = indexA;
|
||||
prevIndexB = indexB;
|
||||
prevSide = side;
|
||||
});
|
||||
|
||||
function handleSelect(font: UnifiedFont, index: number) {
|
||||
function handleSelect(font: UnifiedFont) {
|
||||
if (side === 'A') {
|
||||
if (prevIndexA !== null) {
|
||||
pendingDirection = index > prevIndexA ? -1 : 1;
|
||||
}
|
||||
prevIndexA = index;
|
||||
selectedIndexA = index;
|
||||
comparisonStore.fontA = font;
|
||||
} else if (side === 'B') {
|
||||
if (prevIndexB !== null) {
|
||||
pendingDirection = index > prevIndexB ? -1 : 1;
|
||||
}
|
||||
prevIndexB = index;
|
||||
selectedIndexB = index;
|
||||
} else {
|
||||
comparisonStore.fontB = font;
|
||||
}
|
||||
}
|
||||
|
||||
// When side switches, compute direction from relative positions of A vs B
|
||||
$effect(() => {
|
||||
const _ = side; // track side
|
||||
if (selectedIndexA !== null && selectedIndexB !== null) {
|
||||
// Switching TO B means dot moves toward B's position relative to A
|
||||
pendingDirection = side === 'B'
|
||||
? (selectedIndexB > selectedIndexA ? 1 : -1)
|
||||
: (selectedIndexA > selectedIndexB ? 1 : -1);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex-1 min-h-0 h-full">
|
||||
@@ -90,7 +92,7 @@ $effect(() => {
|
||||
<Button
|
||||
variant="tertiary"
|
||||
{active}
|
||||
onclick={() => handleSelect(font, index)}
|
||||
onclick={() => handleSelect(font)}
|
||||
class="w-full px-3 md:px-4 py-2.5 md:py-3 flex !justify-between text-left text-sm"
|
||||
iconPosition="right"
|
||||
>
|
||||
@@ -99,15 +101,32 @@ $effect(() => {
|
||||
{#snippet icon()}
|
||||
{#if active}
|
||||
<div
|
||||
in:receive={{ key: 'active-dot' }}
|
||||
out:send={{ key: 'active-dot' }}
|
||||
in:receive={getDotTransitionParams(
|
||||
'active-dot',
|
||||
index,
|
||||
prevSide === 'A' ? prevIndexA : prevIndexB,
|
||||
)}
|
||||
out:send={getDotTransitionParams(
|
||||
'active-dot',
|
||||
index,
|
||||
side === 'A' ? indexA : indexB,
|
||||
)}
|
||||
>
|
||||
<DotIcon class="size-8 stroke-brand" />
|
||||
</div>
|
||||
{:else if isSelectedA || isSelectedB}
|
||||
{@const isA = isSelectedA}
|
||||
<div
|
||||
in:receive={{ key: 'inactive-dot' }}
|
||||
out:send={{ key: 'inactive-dot' }}
|
||||
in:receive={getDotTransitionParams(
|
||||
'inactive-dot',
|
||||
index,
|
||||
isA ? prevIndexB : prevIndexA,
|
||||
)}
|
||||
out:send={getDotTransitionParams(
|
||||
'inactive-dot',
|
||||
index,
|
||||
isA ? indexB : indexA,
|
||||
)}
|
||||
>
|
||||
<DotIcon class="size-8 stroke-neutral-300 dark:stroke-neutral-600" />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user