feat(preview): inline split-pane preview + sidebar metadata pass

Replace the fullscreen PreviewOverlay with an inline top pane above
each grid surface. SplitGrid + InlinePreview render the focused
photo/video inside the host page; a resizableVertical action drives
the divider and the height persists via the view store. Applied to
the timeline, /tags drill-in, /review cause tabs, /photo/[uid], and
/map. selection.focused is now the single source of truth for both
the inline pane and the right sidebar — preview.svelte store and
PreviewOverlay are removed.

Sidebar: drop the thumb; lead with icon-led filename and folder
rows that match the date/place rhythm. Move dims+size to the top
(below date) and camera/lens/exposure into the collapsible File
section. Read-only spans share the input padding so the text column
aligns across rows. Folder row sits between date and dims+size.

VideoPlayer: stop forcing width/height: 100% so videos honour their
intrinsic aspect ratio inside the pane. Key the player on file hash
in InlinePreview so navigating between videos remounts the element
and autoplay fires again.

Sidebar (LeftSidebar): switch the labels badge to a dedicated
countPhotos('label:*') query so it reports photos with a label
rather than PhotoPrism's category roll-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 22:46:02 +02:00
parent 2a75896274
commit e36f1939c6
19 changed files with 701 additions and 630 deletions

View File

@@ -33,7 +33,6 @@
setFocused,
setOrder,
} from "$lib/stores/selection.svelte";
import { openPreview, preview } from "$lib/stores/preview.svelte";
import {
setRightSidebarWidth,
setThumbnailSize,
@@ -51,9 +50,11 @@
} from "$lib/actions/visibleRange";
import BulkActionBar from "$lib/components/timeline/BulkActionBar.svelte";
import BulkMetadataSidebar from "$lib/components/sidebar/BulkMetadataSidebar.svelte";
import InlinePreview from "$lib/components/preview/InlinePreview.svelte";
import PhotoTile from "$lib/components/timeline/PhotoTile.svelte";
import RightSidebar from "$lib/components/sidebar/RightSidebar.svelte";
import SkeletonGrid from "$lib/components/timeline/SkeletonGrid.svelte";
import SplitGrid from "$lib/components/preview/SplitGrid.svelte";
import Toolbar from "$lib/components/layout/Toolbar.svelte";
import { type PpPhoto } from "$lib/types/photoprism";
@@ -408,25 +409,6 @@
if (el) scrollTileIntoView(el);
}
// When the preview overlay closes, scroll the just-shown photo back
// into the timeline window. PreviewOverlay already keeps
// selection.focused in lockstep with preview.uid, so we just need to
// make sure that tile is mounted (forcedExpand) and visible — the
// blue ring renders itself once the inner button is in the DOM.
let wasPreviewOpen = $state(false);
$effect(() => {
const open = preview.uid !== null;
const closing = wasPreviewOpen && !open;
wasPreviewOpen = open;
if (!closing) return;
const uid = selection.focused;
if (!uid) return;
untrack(() => {
const i = photos.findIndex((p) => p.UID === uid);
if (i >= 0) void scrollToIndex(i);
});
});
// ── Visual rows for keyboard navigation ──────────────────────────────────
// The CSS Grid lays each photo into a cell with column count derived from
// `repeat(auto-fill, minmax(thumbnailSize, 1fr))`. Month headers span the
@@ -675,24 +657,19 @@
}
function onTileDblclick(e: MouseEvent, uid: string) {
// Modifier-modified dblclicks shouldn't open the preview either —
// gridKeyNav already handled the underlying click.
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
e.preventDefault();
openPreview(
uid,
photos.map((p) => p.UID),
);
selection.ids.clear();
selection.ids.add(uid);
setFocused(uid);
setAnchor(uid);
}
// Single-click fallback for the dblclick preview gesture. Wired to the
// hover-only Maximize icon in PhotoTile so users who haven't discovered
// dblclick can still get to the preview.
function onTileOpenPreview(uid: string) {
openPreview(
uid,
photos.map((p) => p.UID),
);
selection.ids.clear();
selection.ids.add(uid);
setFocused(uid);
setAnchor(uid);
}
// Scroll root for the infinite-scroll IntersectionObserver. Bound by
@@ -840,6 +817,11 @@
sibling at row level and stays full height when the bar appears.
-->
<div class="flex min-w-0 flex-1 flex-col overflow-hidden">
<SplitGrid>
{#snippet preview()}
<InlinePreview uid={selection.focused} order={selection.order} />
{/snippet}
{#snippet grid()}
<main
bind:this={scrollRoot}
class="flex-1 overflow-y-auto outline-none focus:outline-none"
@@ -952,6 +934,8 @@
{/if}
</div>
</main>
{/snippet}
</SplitGrid>
<BulkActionBar />
</div>