This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

web/settings: reserve layout with skeletons instead of a load spinner

Renders each settings tab's structure immediately and swaps same-size skeleton
placeholders for the real controls while data loads, so a hard refresh doesn't
flash a spinner or shift the bounding box. Adds a shared Skeleton component.

author
Anirudh Oppiliappan
committer
Tangled
date (Aug 4, 2026, 10:45 AM UTC) commit d4006429 parent 176b0159 change-id xqqqzsrz
+61 -31
+16 -6
web/src/lib/components/settings/tabs/KeysTab.svelte
··· 8 8 type PublicKeyRecord 9 9 } from "$lib/api/settings"; 10 10 import Button from "$lib/components/ui/Button.svelte"; 11 - import Spinner from "$lib/components/ui/Spinner.svelte"; 11 + import Skeleton from "$lib/components/ui/Skeleton.svelte"; 12 12 import ErrorAlert from "$lib/components/ui/Error.svelte"; 13 13 import SettingsEmpty from "../SettingsEmpty.svelte"; 14 14 import TimeAgo from "$lib/components/ui/TimeAgo.svelte"; ··· 91 91 <ErrorAlert label={removeKey.error} /> 92 92 93 93 {#if loaded.loading} 94 - <div 95 - class="flex items-center justify-center gap-2 p-4 typography-paragraph-regular text-foreground-subtle" 96 - > 97 - <Spinner /> Loading… 98 - </div> 94 + <!-- skeleton rows hold the list's height so it doesn't collapse then expand --> 95 + <SettingsList> 96 + {#each [0, 1] as row (row)} 97 + <div class="flex w-full items-center justify-between gap-4"> 98 + <div class="flex min-w-0 items-center gap-3"> 99 + <Skeleton class="size-4 shrink-0 rounded-full" /> 100 + <div class="flex flex-col gap-1.5"> 101 + <Skeleton class="h-4 w-32 rounded-sm" /> 102 + <Skeleton class="h-3 w-56 rounded-sm" /> 103 + </div> 104 + </div> 105 + <Skeleton class="h-8 w-20 shrink-0 rounded-sm" /> 106 + </div> 107 + {/each} 108 + </SettingsList> 99 109 {:else if loaded.data?.keys.length === 0} 100 110 <SettingsEmpty message="No keys added yet" /> 101 111 {:else if loaded.data}
+22 -21
web/src/lib/components/settings/tabs/NotificationsTab.svelte
··· 11 11 } from "$lib/api/notifications"; 12 12 import Button from "$lib/components/ui/Button.svelte"; 13 13 import Toggle from "$lib/components/ui/Toggle.svelte"; 14 - import Spinner from "$lib/components/ui/Spinner.svelte"; 14 + import Skeleton from "$lib/components/ui/Skeleton.svelte"; 15 15 import ErrorAlert from "$lib/components/ui/Error.svelte"; 16 16 import SettingsHeader from "../SettingsHeader.svelte"; 17 17 import SettingsList from "../SettingsList.svelte"; ··· 158 158 <ErrorAlert label={loaded.error} /> 159 159 <ErrorAlert label={save.error} /> 160 160 161 - {#if loaded.loading} 162 - <div class="flex items-center justify-center gap-2 p-4 typography-paragraph-small text-foreground-subtle"> 163 - <Spinner /> Loading… 164 - </div> 165 - {:else} 166 - <SettingsList> 167 - {#each prefs as pref (pref.key)} 168 - {@const Glyph = pref.icon} 169 - <div class="flex w-full items-center justify-between gap-4"> 170 - <div class="flex min-w-0 flex-col gap-1"> 171 - <span class="flex items-center gap-2"> 172 - <Glyph class="size-3.5 shrink-0 text-foreground-default" aria-hidden="true" /> 173 - <span class="typography-paragraph-regular text-foreground-default">{pref.label}</span> 174 - </span> 175 - <span class="typography-paragraph-regular text-foreground-muted">{pref.description}</span> 176 - </div> 161 + <!-- the full list renders immediately (even server-side) so the layout is 162 + stable; only the toggle values fill in once loaded, swapping a same-size 163 + skeleton for the real control to avoid a spinner and any bounding-box shift. --> 164 + <SettingsList> 165 + {#each prefs as pref (pref.key)} 166 + {@const Glyph = pref.icon} 167 + <div class="flex w-full items-center justify-between gap-4"> 168 + <div class="flex min-w-0 flex-col gap-1"> 169 + <span class="flex items-center gap-2"> 170 + <Glyph class="size-3.5 shrink-0 text-foreground-default" aria-hidden="true" /> 171 + <span class="typography-paragraph-regular text-foreground-default">{pref.label}</span> 172 + </span> 173 + <span class="typography-paragraph-regular text-foreground-muted">{pref.description}</span> 174 + </div> 175 + {#if loaded.loading} 176 + <Skeleton class="h-4.5 w-8.25 shrink-0 rounded-full" /> 177 + {:else} 177 178 <Toggle bind:checked={pref.enabled} disabled={save.loading} aria-label={pref.label} /> 178 - </div> 179 - {/each} 180 - </SettingsList> 181 - {/if} 179 + {/if} 180 + </div> 181 + {/each} 182 + </SettingsList>
+11 -4
web/src/lib/components/settings/tabs/SitesTab.svelte
··· 7 7 import Button from "$lib/components/ui/Button.svelte"; 8 8 import Input from "$lib/components/ui/Input.svelte"; 9 9 import Tag from "$lib/components/ui/Tag.svelte"; 10 - import Spinner from "$lib/components/ui/Spinner.svelte"; 11 10 import ErrorAlert from "$lib/components/ui/Error.svelte"; 11 + import Skeleton from "$lib/components/ui/Skeleton.svelte"; 12 12 import SettingsEmpty from "../SettingsEmpty.svelte"; 13 13 import SettingsHeader from "../SettingsHeader.svelte"; 14 14 import SettingsToolbar from "../SettingsToolbar.svelte"; ··· 99 99 {/if} 100 100 101 101 {#if loaded.loading} 102 - <div class="flex items-center justify-center gap-2 p-4 typography-paragraph-small text-foreground-subtle"> 103 - <Spinner /> Loading… 104 - </div> 102 + <!-- stable-height skeleton row so the box doesn't collapse then expand --> 103 + <SettingsList> 104 + <div class="flex w-full items-center justify-between gap-4"> 105 + <div class="flex items-center gap-2"> 106 + <Skeleton class="size-4 shrink-0 rounded-full" /> 107 + <Skeleton class="h-4 w-40 rounded-sm" /> 108 + </div> 109 + <Skeleton class="h-8 w-24 shrink-0 rounded-sm" /> 110 + </div> 111 + </SettingsList> 105 112 {:else if loaded.data} 106 113 <SettingsList> 107 114 <SettingsEntry icon={Globe} title={loaded.data}>
+12
web/src/lib/components/ui/Skeleton.svelte
··· 1 + <script lang="ts"> 2 + // a neutral pulsing placeholder used to reserve a control's space while data 3 + // loads, so tabs fill in without a spinner or bounding-box shift. the caller 4 + // sets size and rounding via `class` (e.g. "h-4.5 w-8.25 rounded-full"). 5 + interface Props { 6 + class?: string; 7 + } 8 + 9 + let { class: className = "" }: Props = $props(); 10 + </script> 11 + 12 + <span class="block animate-pulse bg-background-muted {className}" aria-hidden="true"></span>