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
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
import BulkMetadataSidebar from '$lib/components/sidebar/BulkMetadataSidebar.svelte';
|
||||
import CauseGroupCard from '$lib/components/review/CauseGroupCard.svelte';
|
||||
import DuplicatesView from '$lib/components/duplicates/DuplicatesView.svelte';
|
||||
import { EmptyState, InlineLoader } from '$lib/components/feedback';
|
||||
import { AlertCircle, Sparkles } from 'lucide-svelte';
|
||||
|
||||
type DupTab = 'stacks' | 'cross-folder';
|
||||
type Tab = CauseKey | DupTab;
|
||||
@@ -226,23 +228,27 @@
|
||||
<div class="flex min-w-0 flex-1 flex-col">
|
||||
<main class="min-h-0 flex-1 overflow-y-auto px-6 py-4 pb-6" use:gridKeyNav={{}}>
|
||||
{#if reviewQuery.isPending}
|
||||
<p class="text-sm text-muted-foreground">Loading review queue…</p>
|
||||
<InlineLoader label="Loading review queue…" />
|
||||
{:else if reviewQuery.error}
|
||||
<p class="text-sm text-destructive">
|
||||
Could not load review queue: {reviewQuery.error instanceof Error
|
||||
<EmptyState
|
||||
tone="destructive"
|
||||
icon={AlertCircle}
|
||||
title="Could not load review queue"
|
||||
description={reviewQuery.error instanceof Error
|
||||
? reviewQuery.error.message
|
||||
: 'unknown error'}
|
||||
</p>
|
||||
/>
|
||||
{:else if groups.length === 0}
|
||||
<div class="max-w-prose space-y-2 text-sm text-muted-foreground">
|
||||
<p>The review queue is empty.</p>
|
||||
<p class="text-xs">
|
||||
PhotoPrism's indexer flags photos with a low quality score for human
|
||||
review. New arrivals with missing EXIF, low resolution, or unknown
|
||||
cameras will land here. The Stacks and Cross-folder tabs above stay
|
||||
available for duplicate cleanup.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState icon={Sparkles} title="Nothing to review">
|
||||
{#snippet descriptionSnippet()}
|
||||
<p>
|
||||
PhotoPrism's indexer flags photos with a low quality score for human
|
||||
review. New arrivals with missing EXIF, low resolution, or unknown
|
||||
cameras will land here. The Stacks and Cross-folder tabs above stay
|
||||
available for duplicate cleanup.
|
||||
</p>
|
||||
{/snippet}
|
||||
</EmptyState>
|
||||
{:else if activeGroup}
|
||||
{#key activeGroup.cause}
|
||||
<CauseGroupCard group={activeGroup} />
|
||||
@@ -263,7 +269,7 @@
|
||||
{:else if focusedPhotoQuery.data}
|
||||
<RightSidebar photo={focusedPhotoQuery.data} showRelated />
|
||||
{:else if focusedPhotoQuery.isFetching}
|
||||
<p class="px-3 py-2 text-xs text-muted-foreground">Loading…</p>
|
||||
<InlineLoader size="sm" label="Loading metadata…" />
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
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, ImageOff, MousePointerClick, Tag } from 'lucide-svelte';
|
||||
import type { PpPhoto } from '$lib/types/photoprism';
|
||||
|
||||
// URL-driven category + value. `isTagCategory` rejects typos so a stray
|
||||
@@ -188,12 +190,11 @@
|
||||
|
||||
{#if !selectedValue}
|
||||
<main class="flex min-h-0 flex-1 items-center justify-center p-8">
|
||||
<div class="max-w-sm space-y-2 text-center">
|
||||
<p class="text-sm font-medium">Pick a {category ?? 'tag'} from the sidebar</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Click a row in the panel on the left to filter the photo grid by that tag.
|
||||
</p>
|
||||
</div>
|
||||
<EmptyState
|
||||
icon={Tag}
|
||||
title={`Pick a ${category ?? 'tag'} from the sidebar`}
|
||||
description="Click a row in the panel on the left to filter the photo grid by that tag."
|
||||
/>
|
||||
</main>
|
||||
{:else}
|
||||
<div class="flex min-h-0 flex-1">
|
||||
@@ -205,9 +206,9 @@
|
||||
{#if showSkeleton}
|
||||
<SkeletonGrid />
|
||||
{:else if showError}
|
||||
<p class="text-sm text-destructive">Failed to load photos.</p>
|
||||
<EmptyState tone="destructive" icon={AlertCircle} title="Failed to load photos" />
|
||||
{:else if drillPhotos.length === 0}
|
||||
<p class="text-sm text-muted-foreground">No photos under this tag.</p>
|
||||
<EmptyState icon={ImageOff} title="No photos under this tag" />
|
||||
{:else}
|
||||
<PhotoGrid photos={drillPhotos} />
|
||||
{/if}
|
||||
@@ -225,15 +226,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>
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user