feat(FontList): unified skeleton — rows stay skeletal until font file loaded

This commit is contained in:
Ilia Mashkov
2026-04-21 12:58:46 +03:00
parent a801903fd3
commit 6664beec25
@@ -9,6 +9,7 @@ import {
FontVirtualList, FontVirtualList,
type UnifiedFont, type UnifiedFont,
VIRTUAL_INDEX_NOT_LOADED, VIRTUAL_INDEX_NOT_LOADED,
appliedFontsManager,
fontStore, fontStore,
} from '$entities/Font'; } from '$entities/Font';
import { getSkeletonWidth } from '$shared/lib/utils'; import { getSkeletonWidth } from '$shared/lib/utils';
@@ -18,6 +19,7 @@ import {
Skeleton, Skeleton,
} from '$shared/ui'; } from '$shared/ui';
import DotIcon from '@lucide/svelte/icons/dot'; import DotIcon from '@lucide/svelte/icons/dot';
import { fade } from 'svelte/transition';
import { import {
createDotCrossfade, createDotCrossfade,
getDotTransitionParams, getDotTransitionParams,
@@ -71,6 +73,21 @@ function handleSelect(font: UnifiedFont) {
comparisonStore.fontB = font; comparisonStore.fontB = font;
} }
} }
/**
* Returns true once the font file is loaded (or errored) and safe to render.
* Called inside the template — Svelte 5 tracks the $state reads inside
* appliedFontsManager.getFontStatus(), so each row re-renders reactively
* when its file arrives.
*/
function isFontReady(font: UnifiedFont): boolean {
const status = appliedFontsManager.getFontStatus(
font.id,
DEFAULT_FONT_WEIGHT,
font.features?.isVariable,
);
return status === 'loaded' || status === 'error';
}
</script> </script>
<div class="flex-1 min-h-0 h-full"> <div class="flex-1 min-h-0 h-full">
@@ -83,39 +100,52 @@ function handleSelect(font: UnifiedFont) {
<FontVirtualList <FontVirtualList
data-font-list data-font-list
weight={DEFAULT_FONT_WEIGHT} weight={DEFAULT_FONT_WEIGHT}
itemHeight={45} itemHeight={44}
class="bg-transparent min-h-0 h-full scroll-stable py-2 pl-6 pr-4" class="bg-transparent min-h-0 h-full scroll-stable py-2 pl-6 pr-4"
> >
{#snippet skeleton()} {#snippet skeleton()}
{#each { length: 12 } as _, i (i)} <div class="py-2.5 md:py-3 px-7">
<div class="w-full px-4 py-3 flex items-center justify-between border border-transparent bg-white/50 dark:bg-[#1e1e1e]/50"> {#each { length: 50 } as _, index (index)}
<div class="w-full px-3 py-3 flex items-center justify-between">
<div class="flex-1 flex items-center gap-3"> <div class="flex-1 flex items-center gap-3">
<Skeleton <Skeleton
class="h-4 bg-neutral-200/70 dark:bg-neutral-800/70" class="h-4 w-32 bg-neutral-200/70 dark:bg-neutral-800/70"
style="width: {getSkeletonWidth(i)}" style="width: {getSkeletonWidth(index)}"
/> />
</div> </div>
<Skeleton class="w-1.5 h-1.5 rounded-full bg-neutral-200/70 dark:bg-neutral-800/70" /> <Skeleton class="w-1.5 h-1.5 rounded-full bg-neutral-200/70 dark:bg-neutral-800/70" />
</div> </div>
{/each} {/each}
</div>
{/snippet} {/snippet}
{#snippet children({ item: font, index })} {#snippet children({ item: font, index })}
<div class="relative h-[44px] w-full">
{#if !isFontReady(font)}
<div
class="absolute inset-0 px-3 md:px-4 flex items-center justify-between border border-transparent"
transition:fade={{ duration: 300 }}
>
<Skeleton
class="h-4 bg-neutral-200/70 dark:bg-neutral-800/70"
style="width: {getSkeletonWidth(index)}"
/>
<Skeleton class="w-1.5 h-1.5 rounded-full bg-neutral-200/70 dark:bg-neutral-800/70" />
</div>
{:else}
{@const isSelectedA = font.id === comparisonStore.fontA?.id} {@const isSelectedA = font.id === comparisonStore.fontA?.id}
{@const isSelectedB = font.id === comparisonStore.fontB?.id} {@const isSelectedB = font.id === comparisonStore.fontB?.id}
{@const active = (side === 'A' && isSelectedA) || (side === 'B' && isSelectedB)} {@const active = (side === 'A' && isSelectedA) || (side === 'B' && isSelectedB)}
<div transition:fade={{ duration: 300 }} class="h-full">
<Button <Button
variant="tertiary" variant="tertiary"
{active} {active}
onclick={() => handleSelect(font)} onclick={() => handleSelect(font)}
class="w-full px-3 md:px-4 py-2.5 md:py-3 flex !justify-between text-left text-sm" class="w-full h-full px-3 md:px-4 py-2.5 md:py-3 flex !justify-between text-left text-sm"
iconPosition="right" iconPosition="right"
> >
<FontApplicator {font}> <FontApplicator {font}>
{#snippet skeleton()}
<Skeleton class="h-4 w-32 bg-neutral-200/70 dark:bg-neutral-800/70" />
{/snippet}
{font.name} {font.name}
</FontApplicator> </FontApplicator>
@@ -154,6 +184,9 @@ function handleSelect(font: UnifiedFont) {
{/if} {/if}
{/snippet} {/snippet}
</Button> </Button>
</div>
{/if}
</div>
{/snippet} {/snippet}
</FontVirtualList> </FontVirtualList>
</div> </div>