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:
@@ -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 bar’s “+ 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
|
||||
|
||||
Reference in New Issue
Block a user