feat(fase-16): integrate Spinner at the 5 loading sites + E2E coverage
Fase 16.2 + 16.3 — replace the bare `m.loading()` text at every
pre-existing async-loading site with `<Spinner size=...> + <span>`.
Text is kept (visual fallback + tooltip / SR redundancy). No new
loading semantics — only existing flags get a visual indicator.
Sites:
- (app)/+layout.svelte: auth splash gets `size="lg"` centred under the
loading label.
- (app)/search/+page.svelte: inline `size="sm"` next to the search-in-flight
copy.
- (app)/notes/archive/+page.svelte: inline `size="sm"` next to the
archive-load copy.
- lib/components/CreateCollectiveModal.svelte: inline `size="sm"` inside
the disabled Save button while the create RPC is in flight.
- (app)/collective/manage/+page.svelte: three sites — members-list load,
invitation-generate button, and add-common-item Save button.
E2E (tests/e2e/spinner.test.ts):
- SP-E-01 throttles search_in_collective RPC by 500ms and asserts the
spinner is visible in the loading paragraph.
- SP-E-02 throttles set_item_frequency_weight RPC and asserts the spinner
is scoped inside the Save button.
- SP-E-03 seeds an expired `sb-192-auth-token` so GoTrue is forced to
call /auth/v1/token, then hangs that endpoint indefinitely and
asserts the splash spinner is visible. Storage key derivation matches
Supabase's `sb-${hostname.split('.')[0]}-auth-token` default; we
populate both the LAN-IP variant and a loopback fallback for
resilience.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { getSupabase } from '$lib/supabase';
|
||||
import { currentCollective, userCollectives } from '$lib/stores/collective';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
let { onClose }: { onClose: () => void } = $props();
|
||||
@@ -102,11 +103,16 @@
|
||||
type="submit"
|
||||
data-testid="create-collective-modal-submit"
|
||||
disabled={!name.trim() || creating}
|
||||
class="rounded-lg bg-slate-900 px-4 py-1.5 text-sm font-semibold text-white
|
||||
class="inline-flex items-center gap-2 rounded-lg bg-slate-900 px-4 py-1.5 text-sm font-semibold text-white
|
||||
transition-opacity hover:opacity-90 disabled:opacity-50
|
||||
dark:bg-slate-50 dark:text-slate-900"
|
||||
>
|
||||
{creating ? m.loading() : m.create_collective_modal_button()}
|
||||
{#if creating}
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
{:else}
|
||||
{m.create_collective_modal_button()}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import BottomTabBar from '$lib/components/layout/BottomTabBar.svelte';
|
||||
import MobileDrawer from '$lib/components/layout/MobileDrawer.svelte';
|
||||
import UndoToast from '$lib/components/UndoToast.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
let { children }: { children: Snippet } = $props();
|
||||
@@ -149,8 +150,9 @@
|
||||
</script>
|
||||
|
||||
{#if $authLoading}
|
||||
<div class="flex h-screen items-center justify-center bg-background">
|
||||
<span class="text-sm text-text-secondary">{m.loading()}</span>
|
||||
<div class="flex h-screen flex-col items-center justify-center gap-3 bg-background text-text-secondary">
|
||||
<Spinner size="lg" />
|
||||
<span class="text-sm">{m.loading()}</span>
|
||||
</div>
|
||||
{:else if $isAuthenticated}
|
||||
<!-- Mobile: column stack (top bar → main → bottom tab bar).
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { currentUser } from '$lib/stores/auth';
|
||||
import { currentCollective, collectiveMembers, userCollectives } from '$lib/stores/collective';
|
||||
import Avatar from '$lib/components/Avatar.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import type { FeatureFlags, SectionKey, ItemFrequency } from '@colectivo/types';
|
||||
import { SECTION_KEYS } from '@colectivo/types';
|
||||
import { setCollectiveFeature } from '$lib/stores/features';
|
||||
@@ -551,7 +552,10 @@
|
||||
</h2>
|
||||
|
||||
{#if loading}
|
||||
<p class="text-sm text-text-secondary">{m.loading()}</p>
|
||||
<p class="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each members as member}
|
||||
@@ -766,10 +770,15 @@
|
||||
data-testid="generate-invite"
|
||||
onclick={generateInviteLink}
|
||||
disabled={generating}
|
||||
class="mb-3 w-full rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white
|
||||
class="mb-3 inline-flex w-full items-center justify-center gap-2 rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white
|
||||
hover:opacity-90 disabled:opacity-50 dark:bg-slate-50 dark:text-slate-900"
|
||||
>
|
||||
{generating ? m.loading() : m.manage_generate_link()}
|
||||
{#if generating}
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
{:else}
|
||||
{m.manage_generate_link()}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if generatedLink}
|
||||
@@ -1018,10 +1027,15 @@
|
||||
data-testid="common-item-add-save"
|
||||
disabled={!addName.trim() || addSaving}
|
||||
onclick={() => void confirmAdd()}
|
||||
class="rounded-md bg-slate-900 px-4 py-1.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50
|
||||
class="inline-flex items-center gap-2 rounded-md bg-slate-900 px-4 py-1.5 text-sm font-semibold text-white hover:opacity-90 disabled:opacity-50
|
||||
dark:bg-slate-50 dark:text-slate-900"
|
||||
>
|
||||
{addSaving ? m.loading() : m.common_items_add_save()}
|
||||
{#if addSaving}
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
{:else}
|
||||
{m.common_items_add_save()}
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { loadArchivedNotes, unarchiveNote, trashNote } from '$lib/stores/notes';
|
||||
import type { Note } from '@colectivo/types';
|
||||
import { ArrowLeft, ArchiveRestore, Trash2 } from 'lucide-svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
|
||||
let archived = $state<Note[]>([]);
|
||||
@@ -44,7 +45,10 @@
|
||||
|
||||
<div class="flex-1 overflow-y-auto px-6 py-6">
|
||||
{#if loading}
|
||||
<p class="text-sm text-text-secondary">{m.loading()}</p>
|
||||
<p class="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
</p>
|
||||
{:else if archived.length === 0}
|
||||
<p class="text-sm text-text-secondary">{m.notes_archived_empty()}</p>
|
||||
{:else}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Search, FileText, CheckSquare, ShoppingCart } from 'lucide-svelte';
|
||||
import * as m from '$lib/paraglide/messages';
|
||||
import ScreenMasthead from '$lib/components/ScreenMasthead.svelte';
|
||||
import Spinner from '$lib/components/Spinner.svelte';
|
||||
|
||||
let query = $state('');
|
||||
let results = $state<SearchRow[]>([]);
|
||||
@@ -119,7 +120,10 @@
|
||||
{#if query.trim().length < 2}
|
||||
<p class="text-sm text-text-secondary">{m.search_hint()}</p>
|
||||
{:else if loading}
|
||||
<p class="text-sm text-text-secondary">{m.loading()}</p>
|
||||
<p class="flex items-center gap-2 text-sm text-text-secondary">
|
||||
<Spinner size="sm" />
|
||||
<span>{m.loading()}</span>
|
||||
</p>
|
||||
{:else if results.length === 0}
|
||||
<p class="text-sm text-text-secondary">{m.search_empty()}</p>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user