web: unify empty + loading states behind EmptyState/InlineLoader

Replaces ad-hoc "Loading…" text and bare empty messages with two
shared feedback primitives that carry subtle lucide icons, consistent
muted-foreground/destructive tones, and a11y signaling (role=status,
aria-busy, role=alert on destructive empties). Loading copy gains
context ("Loading photos/folders/heaps/metadata…") and the right-
sidebar idle state moves from a "ⓘ" glyph to a MousePointerClick
icon. SkeletonGrid stays as the initial-grid loader.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:36:11 +02:00
parent d2a76fa58c
commit e364e4128f
14 changed files with 323 additions and 119 deletions

View File

@@ -56,6 +56,16 @@
import RightSidebar from "$lib/components/sidebar/RightSidebar.svelte";
import SkeletonGrid from "$lib/components/timeline/SkeletonGrid.svelte";
import Toolbar from "$lib/components/layout/Toolbar.svelte";
import { EmptyState, InlineLoader } from "$lib/components/feedback";
import {
AlertCircle,
Archive,
EyeOff,
ImageOff,
Layers,
MousePointerClick,
Sparkles,
} from "lucide-svelte";
import { type PpPhoto } from "$lib/types/photoprism";
// ── URL ↔ filter store sync ──────────────────────────────────────────────
@@ -831,29 +841,42 @@
{#if photosQuery.isPending}
<SkeletonGrid />
{:else if photosQuery.isError}
<p class="text-sm text-destructive">
Failed to load photos: {photosQuery.error instanceof Error
<EmptyState
tone="destructive"
icon={AlertCircle}
title="Failed to load photos"
description={photosQuery.error instanceof Error
? photosQuery.error.message
: "unknown error"}
</p>
/>
{:else if photos.length === 0}
<p class="text-sm text-muted-foreground">
{#if filters.section === "archive"}
Archive is empty.
{:else if filters.section === "review"}
Nothing left to review. Photos PhotoPrism's indexer wasn't
sure about land here — use Keep to accept them into the
timeline or Archive to set them aside.
{:else if filters.section === "hidden"}
No hidden photos. PhotoPrism auto-hides files it can't index
(broken files, very low quality); they only ever show up here.
{:else if filters.section === "heap"}
This heap has no photos yet. Select some photos and use the
bulk bar's " Add to heap" button.
{:else}
No photos. Index a folder via PhotoPrism's reindex command.
{/if}
</p>
{#if filters.section === "archive"}
<EmptyState icon={Archive} title="Archive is empty" />
{:else if filters.section === "review"}
<EmptyState
icon={Sparkles}
title="Nothing left to review"
description="Photos PhotoPrism's indexer wasn't sure about land here — use Keep to accept them into the timeline or Archive to set them aside."
/>
{:else if filters.section === "hidden"}
<EmptyState
icon={EyeOff}
title="No hidden photos"
description="PhotoPrism auto-hides files it can't index (broken files, very low quality); they only ever show up here."
/>
{:else if filters.section === "heap"}
<EmptyState
icon={Layers}
title="This heap has no photos yet"
description={'Select some photos and use the bulk bars “+ Add to heap” button.'}
/>
{:else}
<EmptyState
icon={ImageOff}
title="No photos"
description="Index a folder via PhotoPrism's reindex command."
/>
{/if}
{:else}
<div
data-photo-grid
@@ -920,9 +943,12 @@
}}
></div>
{#if photosQuery.isFetchingNextPage}
<p class="py-3 text-center text-xs text-muted-foreground">
Loading more
</p>
<InlineLoader
size="sm"
align="center"
polite={false}
label="Loading more photos…"
/>
{/if}
{/if}
</div>
@@ -944,15 +970,16 @@
{:else if focusedPhotoQuery.data}
<RightSidebar photo={focusedPhotoQuery.data} />
{:else if focusedPhotoQuery.isFetching}
<p class="px-3 py-2 text-xs text-muted-foreground">Loading</p>
<InlineLoader size="sm" label="Loading metadata…" />
{:else}
<div class="space-y-2 p-4 text-center">
<div class="text-xl"></div>
<p class="text-xs text-muted-foreground">
Use arrow keys or <kbd class="rounded bg-muted px-1"></kbd>+click
on a thumbnail to view its metadata here.
</p>
</div>
<EmptyState icon={MousePointerClick} title="No photo selected">
{#snippet descriptionSnippet()}
<p>
Use arrow keys or <kbd class="rounded bg-muted px-1"></kbd
>+click on a thumbnail to view its metadata here.
</p>
{/snippet}
</EmptyState>
{/if}
</div>
<!-- Resize handle on the left edge; mirrors the layout's left aside