feat: skeleton loading states for model list and details

Replaces the 'Loading model...' text with a static skeleton mirroring
the detail layout (header, metadata chips, download options box, readme
lines). List item skeletons now vary widths per row so the loading list
does not render as identical blocks.

Assisted-by: pi:GLM-5.3-Flash
This commit is contained in:
Aleksander Grygier
2026-09-04 20:07:36 +02:00
parent 025e876d1d
commit cb1bc7511c
5 changed files with 132 additions and 11 deletions
@@ -28,7 +28,7 @@
<ul class="space-y-0.5 p-2">
{#if loading}
{#each Array(loadingCount) as _, index (index)}
<ModelsDiscoverListItemSkeleton />
<ModelsDiscoverListItemSkeleton {index} />
{/each}
{:else}
{#each models as model (model.id)}
@@ -1,21 +1,38 @@
<script lang="ts">
import { Skeleton } from '$lib/components/ui/skeleton';
interface Props {
/** Row index; varies skeleton widths so the list does not look mechanical. */
index?: number;
}
let { index = 0 }: Props = $props();
// Deterministic width variations keyed by row position.
const NAME_WIDTHS = ['w-40', 'w-52', 'w-44', 'w-56'];
const BADGE_WIDTHS = [
['w-12', 'w-14', 'w-10'],
['w-16', 'w-12', 'w-12'],
['w-10', 'w-16', 'w-10'],
['w-14', 'w-10', 'w-14']
];
let nameWidth = $derived(NAME_WIDTHS[index % NAME_WIDTHS.length]);
let badgeWidths = $derived(BADGE_WIDTHS[index % BADGE_WIDTHS.length]);
</script>
<!-- Static skeleton of ModelsDiscoverListItem: avatar, name and badge rows. -->
<li>
<div class="flex w-full items-start gap-2.5 rounded-lg p-2.5 text-left">
<Skeleton class="h-9 w-9 rounded-md" />
<Skeleton class="h-9 w-9 shrink-0 rounded-md" />
<div class="min-w-0 flex-1 space-y-1.5">
<Skeleton class="h-4 w-48 max-w-full" />
<Skeleton class="{nameWidth} h-4 max-w-full" />
<div class="flex items-center gap-1">
<Skeleton class="h-3.5 w-12 rounded" />
<Skeleton class="h-3.5 w-14 rounded" />
<Skeleton class="h-3.5 w-10 rounded" />
{#each badgeWidths as width, i (i)}
<Skeleton class="{width} h-3.5 rounded" />
{/each}
</div>
</div>
</div>
@@ -1,6 +1,7 @@
<script lang="ts">
import ModelsDiscoverModelDetailsDownloadOptions from './ModelsDiscoverModelDetailsDownloadOptions.svelte';
import ModelsDiscoverModelDetailsHeader from './ModelsDiscoverModelDetailsHeader.svelte';
import ModelsDiscoverModelDetailsSkeleton from './ModelsDiscoverModelDetailsSkeleton.svelte';
import ModelsDiscoverModelDetailsReadme from './ModelsDiscoverModelDetailsReadme.svelte';
import { isAuxSidecar } from '$lib/constants';
import { HuggingFaceService } from '$lib/services';
@@ -74,9 +75,7 @@
</script>
{#if loading}
<div class="flex h-full items-center justify-center py-20">
<p class="text-sm text-muted-foreground">Loading model...</p>
</div>
<ModelsDiscoverModelDetailsSkeleton />
{:else if error}
<div class="flex h-full items-center justify-center py-20">
<p class="text-sm text-destructive">{error}</p>
@@ -0,0 +1,97 @@
<script lang="ts">
import { Skeleton } from '$lib/components/ui/skeleton';
</script>
<!-- Static skeleton of ModelsDiscoverModelDetails: header, metadata chips, download options and readme lines. -->
<div class="space-y-6 p-6" data-slot="model-details-skeleton">
<!-- Header: avatar, name with capability icons, base model line, HF link -->
<div class="space-y-3">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 items-center gap-2">
<Skeleton class="h-12 w-12 rounded-md" />
<div class="min-w-0 space-y-1.5">
<Skeleton class="h-5 w-56 max-w-full" />
<Skeleton class="h-3 w-40 max-w-full" />
</div>
</div>
<Skeleton class="h-7.5 w-44 shrink-0 rounded-md" />
</div>
<!-- downloads / likes / last updated -->
<div class="flex items-center gap-4">
<Skeleton class="h-3.5 w-16" />
<Skeleton class="h-3.5 w-12" />
<Skeleton class="h-3.5 w-24" />
</div>
<!-- description + metadata chips -->
<div class="space-y-3">
<Skeleton class="h-4 w-full max-w-2xl" />
<Skeleton class="h-4 w-2/3 max-w-lg" />
<div class="flex flex-wrap items-center gap-1.5">
<Skeleton class="h-7 w-36 rounded-md" />
<Skeleton class="h-7 w-28 rounded-md" />
<Skeleton class="h-7 w-32 rounded-md" />
<Skeleton class="h-7 w-24 rounded-md" />
</div>
</div>
</div>
<!-- Download options: quant rows, CTA, terminal command -->
<div
class="rounded-3xl border border-border/30 bg-muted/40 p-4 shadow-xs dark:border-border/20 dark:bg-muted/50"
>
<div class="space-y-3 divide-y divide-border/50 dark:divide-border/35 pb-1">
{#each [0, 1, 2] as _, index (index)}
<div class="grid grid-cols-[5rem_1fr] items-start gap-3 py-3">
<div class="space-y-1.5 pt-1">
<Skeleton class="h-4 w-14" />
<Skeleton class="h-2.5 w-24" />
</div>
<div class="flex flex-wrap items-center justify-end gap-1.5">
<Skeleton class="h-6 w-28 rounded-md" />
<Skeleton class="h-6 w-20 rounded-md" />
</div>
</div>
{/each}
</div>
<div class="space-y-2.5 border-t border-border/50 pt-3.5 dark:border-border/35">
<Skeleton class="h-9 w-full rounded-md" />
<div class="flex items-center gap-3">
<span class="h-px flex-1 bg-border/50"></span>
<Skeleton class="h-3 w-44" />
<span class="h-px flex-1 bg-border/50"></span>
</div>
<Skeleton class="h-11 w-full rounded-md" />
</div>
</div>
<!-- Readme -->
<div class="space-y-2.5">
<Skeleton class="h-3.5 w-full" />
<Skeleton class="h-3.5 w-full" />
<Skeleton class="h-3.5 w-5/6" />
<Skeleton class="h-3.5 w-2/3" />
</div>
</div>
@@ -109,6 +109,14 @@ export { default as ModelsDiscoverModelDetailsDownloadOptionsDownloadButton } fr
*/
export { default as ModelsDiscoverModelDetailsDownloadOptionsDownloadCommand } from './ModelsDiscoverModelDetailsDownloadOptionsDownloadCommand.svelte';
/**
* **ModelsDiscoverDetailsSkeleton** - Detail view loading skeleton
*
* Static placeholder matching the detail layout: header with avatar and name,
* metadata chips, the download options box and readme text lines.
*/
export { default as ModelsDiscoverModelDetailsSkeleton } from './ModelsDiscoverModelDetailsSkeleton.svelte';
/**
* **ModelsDiscoverChatTemplateDialog** - Chat template viewer
*