feat(web): smart empty-states with action affordances (Tier B6)
test-web / test (push) Successful in 32s

Replace dead-end empty copy with EmptyState cards that include a
clear next step.

- EmptyState.svelte: reusable card with `block` (whole-tab) and
  `inline` (sub-section) variants, an actions snippet for buttons.
- Artists / Albums: when the whole library is empty, link to
  /settings (the operator can scan a folder there).
- Liked: when all three sub-sections are empty, show a single
  whole-tab card ("No likes yet" with Explore Home + Browse albums).
  When only one sub-section is empty, an inline hint with a link
  to the corresponding library tab.
- History: "Listen to something" button → Home.
- Playlists: "Create a playlist" button calls the existing create
  flow; renamed from "New playlist" to avoid colliding with the
  header's button-by-name lookup in tests.

Liked tab tests updated to match: the previous "three 'no liked X
yet'" assertion is now a single onboarding card test + a sibling
test for the mixed populated/empty case.
This commit is contained in:
2026-06-01 13:53:38 -04:00
parent be7be6f617
commit 859aff8d30
7 changed files with 167 additions and 12 deletions
+11 -1
View File
@@ -7,6 +7,7 @@
import LibrarySkeleton from '$lib/components/LibrarySkeleton.svelte';
import ApiErrorBanner from '$lib/components/ApiErrorBanner.svelte';
import QuickFilter from '$lib/components/QuickFilter.svelte';
import EmptyState from '$lib/components/EmptyState.svelte';
const queryStore = createHistoryQuery();
const query = $derived($queryStore);
@@ -40,7 +41,16 @@
{:else if query.isError}
<ApiErrorBanner error={query.error} onRetry={query.refetch} />
{:else if flatEvents.length === 0}
<p class="p-8 text-center text-text-secondary">No listening history yet.</p>
<EmptyState
title="No listening history yet"
hint="Once you've played something, it'll show up here, grouped by day."
>
{#snippet actions()}
<a href="/" class="inline-flex items-center rounded-md bg-action-secondary px-4 py-2 text-sm text-action-fg hover:opacity-90">
Listen to something
</a>
{/snippet}
</EmptyState>
{:else if filter.trim() && filteredEvents.length === 0}
<p class="text-text-secondary">
No history in your loaded events matches