fix(web): replace generic spinners with content-shaped loading skeletons
The six loading states across the Knowledge wiki (initial app load, the reader's note/history fetches, and the four Cleanup tabs) all showed a centered spinner with no relation to what was about to render — costing a full reflow the instant real content landed. Replaces each with a skeleton shaped like its actual content (tree rows, reader header + prose, revision list + diff, cluster cards, table rows, flat lists) using the existing shadcn Skeleton primitive already used elsewhere. Verified each of the six by temporarily injecting a delay into fetchWithAuth and screenshotting the transient state. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Input } from '$lib/components/ui/input'
|
||||
import { Badge } from '$lib/components/ui/badge'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import { Skeleton } from '$lib/components/ui/skeleton'
|
||||
import { toast } from 'svelte-sonner'
|
||||
import { kindMeta } from './kinds'
|
||||
import { relativeTime } from '$lib/utils'
|
||||
@@ -89,7 +89,7 @@
|
||||
mergeSources = sources
|
||||
} catch (e) {
|
||||
duplicatesError = errMsg(e)
|
||||
clusters = [] // stop the spinner — the error message above explains the empty state
|
||||
clusters = [] // clear the loading skeleton — the error message above explains the empty state
|
||||
}
|
||||
}
|
||||
|
||||
@@ -289,7 +289,23 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#if clusters === null}
|
||||
<div class="flex justify-center py-8"><Spinner /></div>
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each Array(2) as _, ci (ci)}
|
||||
<div class="overflow-hidden rounded-lg border">
|
||||
<div
|
||||
class="flex items-center justify-between gap-2 border-b bg-muted/30 px-2.5 py-1.5"
|
||||
>
|
||||
<Skeleton class="h-3 w-28" />
|
||||
<Skeleton class="h-6 w-28" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-2.5">
|
||||
{#each Array(ci === 0 ? 3 : 2) as _, ri (ri)}
|
||||
<Skeleton class="h-3.5" style="width: {70 - ri * 10}%" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if clusters.length === 0}
|
||||
<p class="py-8 text-center text-xs text-muted-foreground">No likely duplicates found.</p>
|
||||
{:else}
|
||||
@@ -412,7 +428,31 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#if tags === null}
|
||||
<div class="flex justify-center py-8"><Spinner /></div>
|
||||
<table class="w-full text-xs">
|
||||
<thead>
|
||||
<tr class="border-b text-left text-muted-foreground">
|
||||
<th class="py-1 font-normal">Tag</th>
|
||||
<th class="py-1 font-normal" colspan="2">Uses</th>
|
||||
<th class="py-1 font-normal">Variants</th>
|
||||
<th class="py-1"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Array(10) as _, i (i)}
|
||||
<tr class="border-b border-border/50">
|
||||
<td class="w-32 py-1.5 pr-2"
|
||||
><Skeleton class="h-3" style="width: {60 - i * 3}%" /></td
|
||||
>
|
||||
<td class="w-8 py-1.5 pr-1"><Skeleton class="ml-auto h-3 w-4" /></td>
|
||||
<td class="w-24 py-1.5 pr-3">
|
||||
<Skeleton class="h-1 rounded-full" style="width: {90 - i * 8}%" />
|
||||
</td>
|
||||
<td class="py-1.5 pr-2"><Skeleton class="h-3 w-6" /></td>
|
||||
<td class="py-1.5"></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
{@const maxUses = Math.max(1, ...tags.map((t) => t.uses))}
|
||||
<table class="w-full text-xs">
|
||||
@@ -509,7 +549,15 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#if orphans === null}
|
||||
<div class="flex justify-center py-8"><Spinner /></div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{#each Array(7) as _, i (i)}
|
||||
<div class="flex items-center gap-2 px-1.5 py-1.5">
|
||||
<Skeleton class="h-3.5 flex-1" style="max-width: {60 - (i % 4) * 8}%" />
|
||||
<Skeleton class="h-4 w-14 shrink-0 rounded-full" />
|
||||
<Skeleton class="h-3 w-10 shrink-0" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if orphans.length === 0}
|
||||
<p class="py-8 text-center text-xs text-muted-foreground">Nothing orphaned.</p>
|
||||
{:else}
|
||||
@@ -541,7 +589,15 @@
|
||||
</p>
|
||||
{/if}
|
||||
{#if trash === null}
|
||||
<div class="flex justify-center py-8"><Spinner /></div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="flex items-center gap-2 px-1.5 py-1.5">
|
||||
<Skeleton class="h-3.5 flex-1" style="max-width: {55 - i * 6}%" />
|
||||
<Skeleton class="h-3 w-32 shrink-0" />
|
||||
<Skeleton class="h-6 w-16 shrink-0" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else if trash.length === 0}
|
||||
<p class="py-8 text-center text-xs text-muted-foreground">Trash is empty.</p>
|
||||
{:else}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Input } from '$lib/components/ui/input'
|
||||
import { Textarea } from '$lib/components/ui/textarea'
|
||||
import Spinner from '$lib/components/Spinner.svelte'
|
||||
import { Skeleton } from '$lib/components/ui/skeleton'
|
||||
import PencilIcon from '@lucide/svelte/icons/pencil'
|
||||
import TrashIcon from '@lucide/svelte/icons/trash-2'
|
||||
import HistoryIcon from '@lucide/svelte/icons/history'
|
||||
@@ -224,7 +224,33 @@
|
||||
{#if !item}
|
||||
<WikiOverview items={allItems} onSelect={onNavigate} {onNew} />
|
||||
{:else if loading}
|
||||
<div class="flex h-full items-center justify-center"><Spinner /></div>
|
||||
<!-- Shaped like the loaded header/tags/body below rather than a
|
||||
centered spinner, so the switch from "loading" to "loaded" is a
|
||||
content swap, not a layout jump — the title, meta line, tag row,
|
||||
and first few lines of body all keep their real position. -->
|
||||
<div class="flex items-start justify-between gap-2 border-b pb-2.5">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Skeleton class="size-4 shrink-0 rounded" />
|
||||
<Skeleton class="h-5 w-56" />
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Skeleton class="h-3 w-16" />
|
||||
<Skeleton class="h-3 w-20" />
|
||||
<Skeleton class="h-3 w-24" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton class="h-7 w-16 shrink-0" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5 pt-3">
|
||||
<Skeleton class="h-5 w-14 rounded-full" />
|
||||
<Skeleton class="h-5 w-16 rounded-full" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2.5 pt-2">
|
||||
{#each Array(6) as _, i (i)}
|
||||
<Skeleton class="h-4" style="width: {i === 5 ? 45 : 96 - i * 4}%" />
|
||||
{/each}
|
||||
</div>
|
||||
{:else if !content}
|
||||
<div class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Couldn't load this note.
|
||||
@@ -374,7 +400,21 @@
|
||||
|
||||
<Tabs.Content value="history" class="min-h-0 flex-1 overflow-y-auto pt-2">
|
||||
{#if revisionsLoading}
|
||||
<div class="flex justify-center py-8"><Spinner /></div>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex w-40 shrink-0 flex-col gap-2 px-2 py-1">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="flex flex-col gap-1">
|
||||
<Skeleton class="h-3 w-16" />
|
||||
<Skeleton class="h-2.5 w-20" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-1.5 rounded border p-2">
|
||||
{#each Array(8) as _, i (i)}
|
||||
<Skeleton class="h-3" style="width: {90 - (i % 4) * 15}%" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if !revisions || revisions.length === 0}
|
||||
<p class="py-8 text-center text-xs text-muted-foreground">
|
||||
No prior revisions — this is the first version.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
import WikiQuickOpen from '$lib/components/knowledge/WikiQuickOpen.svelte'
|
||||
import * as Dialog from '$lib/components/ui/dialog'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Skeleton } from '$lib/components/ui/skeleton'
|
||||
import WrenchIcon from '@lucide/svelte/icons/wrench'
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open'
|
||||
|
||||
@@ -149,9 +150,51 @@
|
||||
|
||||
<div class="min-h-0 flex-1">
|
||||
{#if itemsLoading}
|
||||
<div class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
Loading…
|
||||
</div>
|
||||
<!-- Shaped like the real three-pane layout below (same Splitpanes
|
||||
proportions) rather than a centered spinner: a spinner tells the
|
||||
operator nothing about what's about to render and costs a full
|
||||
reflow the instant real content lands. This costs none — the
|
||||
panes are already the right size. -->
|
||||
<Splitpanes theme="oikos-theme" dblClickSplitter={false} class="h-full">
|
||||
<Pane size={22} minSize={15} maxSize={40}>
|
||||
<div class="flex h-full flex-col gap-3 overflow-hidden p-1.5">
|
||||
<Skeleton class="h-7 w-full" />
|
||||
<Skeleton class="h-3 w-16" />
|
||||
<div class="flex flex-col gap-3">
|
||||
{#each Array(4) as _, gi (gi)}
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Skeleton class="h-3 w-20" />
|
||||
<div class="ml-3 flex flex-col gap-2 border-l pl-2">
|
||||
{#each Array(3) as _, ri (ri)}
|
||||
<Skeleton class="h-3.5" style="width: {75 - ri * 15}%" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
<Pane size={56} minSize={30}>
|
||||
<div class="flex h-full flex-col gap-3 overflow-hidden p-2">
|
||||
<Skeleton class="h-4 w-32" />
|
||||
<Skeleton class="h-9 w-40" />
|
||||
<div class="mt-1 grid grid-cols-4 gap-px overflow-hidden rounded-lg bg-border/60">
|
||||
{#each Array(4) as _, i (i)}
|
||||
<div class="flex flex-col gap-1.5 bg-card px-3 py-2.5">
|
||||
<Skeleton class="h-3 w-14" />
|
||||
<Skeleton class="h-5 w-8" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<Skeleton class="mt-2 h-3 w-full" />
|
||||
<div class="mt-2 flex flex-col gap-2.5">
|
||||
{#each Array(5) as _, i (i)}
|
||||
<Skeleton class="h-4" style="width: {85 - i * 8}%" />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
{:else if loadError}
|
||||
<div class="flex h-full flex-col items-center justify-center gap-2 text-sm">
|
||||
<p class="text-destructive">{loadError}</p>
|
||||
|
||||
Reference in New Issue
Block a user