preview: full-screen modal replaces inline split + tags route reorg

The old SplitGrid + InlinePreview pane is replaced by a full-screen
PreviewModal mounted once at the layout root. Open via Space on the
focused tile or double-click; close on Esc (or X / Space again).
Inside, PreviewPane renders the focused photo, RightSidebar carries
the metadata, BulkActionBar reuses the existing per-photo actions,
and PreviewCarousel windows ±50 thumbs around the focused index.

Selection contract matches the grid: plain click reduces, shift
extends the range, ⌘/Ctrl toggles, plain arrow drops the multi-
selection, shift-arrow extends. New clearBulkToFirst() helper makes
Esc / Clear collapse a bulk back to single-focus on its first member
before the next press fully dismisses (modal closes, grid clears
focus).

Tags route reorganised into /tags/[category]/[[value]] with its own
+layout and TagsBrowserSidebar; the old monolithic /tags/+page is
trimmed to a legacy redirect.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 00:13:20 +02:00
parent 680fa90cbe
commit 0d5f380948
24 changed files with 1593 additions and 1068 deletions

View File

@@ -24,7 +24,8 @@ interface Persisted {
thumbnailSize?: ThumbnailSize;
leftSidebarWidth?: number;
rightSidebarWidth?: number;
previewPaneHeight?: number;
tagsBrowserWidth?: number;
tagsBrowserCollapsed?: boolean;
/**
* Per-section expanded state for the right-sidebar metadata panel
* (GPS, Credits, File). Keyed by section id; missing entries use a
@@ -40,14 +41,9 @@ export const DEFAULT_LEFT_WIDTH = 224;
export const MIN_RIGHT_WIDTH = 220;
export const MAX_RIGHT_WIDTH = 480;
export const DEFAULT_RIGHT_WIDTH = 280;
export const MIN_PREVIEW_HEIGHT = 160;
export const DEFAULT_PREVIEW_HEIGHT = 360;
/**
* Cap the preview pane at 70 % of the viewport so the grid is always
* visible underneath. Resolved against `window.innerHeight` at set-time
* (the localStorage load happens before any viewport size is known).
*/
export const MAX_PREVIEW_HEIGHT_FRAC = 0.7;
export const MIN_TAGS_BROWSER_WIDTH = 180;
export const MAX_TAGS_BROWSER_WIDTH = 480;
export const DEFAULT_TAGS_BROWSER_WIDTH = 240;
function clamp(n: number, lo: number, hi: number): number {
return Math.min(hi, Math.max(lo, n));
@@ -78,7 +74,13 @@ export const view = $state<{
thumbnailSize: ThumbnailSize;
leftSidebarWidth: number;
rightSidebarWidth: number;
previewPaneHeight: number;
tagsBrowserWidth: number;
tagsBrowserCollapsed: boolean;
/**
* Ephemeral: true while the full-screen preview modal is open. Not
* persisted — a refresh always returns to the grid.
*/
previewOpen: boolean;
metadataSections: Record<string, boolean>;
}>({
rightSidebarCollapsed: initial.rightSidebarCollapsed ?? false,
@@ -96,12 +98,15 @@ export const view = $state<{
MIN_RIGHT_WIDTH,
MAX_RIGHT_WIDTH
),
previewPaneHeight: Math.max(
MIN_PREVIEW_HEIGHT,
typeof initial.previewPaneHeight === 'number'
? initial.previewPaneHeight
: DEFAULT_PREVIEW_HEIGHT
tagsBrowserWidth: clamp(
typeof initial.tagsBrowserWidth === 'number'
? initial.tagsBrowserWidth
: DEFAULT_TAGS_BROWSER_WIDTH,
MIN_TAGS_BROWSER_WIDTH,
MAX_TAGS_BROWSER_WIDTH
),
tagsBrowserCollapsed: initial.tagsBrowserCollapsed ?? false,
previewOpen: false,
metadataSections:
initial.metadataSections && typeof initial.metadataSections === 'object'
? { ...initial.metadataSections }
@@ -116,7 +121,8 @@ function persist(): void {
thumbnailSize: view.thumbnailSize,
leftSidebarWidth: view.leftSidebarWidth,
rightSidebarWidth: view.rightSidebarWidth,
previewPaneHeight: view.previewPaneHeight,
tagsBrowserWidth: view.tagsBrowserWidth,
tagsBrowserCollapsed: view.tagsBrowserCollapsed,
metadataSections: view.metadataSections
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
@@ -132,14 +138,32 @@ export function setRightSidebarWidth(px: number): void {
persist();
}
export function setPreviewPaneHeight(px: number): void {
const maxH = browser
? Math.max(MIN_PREVIEW_HEIGHT + 1, Math.floor(window.innerHeight * MAX_PREVIEW_HEIGHT_FRAC))
: 1024;
view.previewPaneHeight = clamp(Math.round(px), MIN_PREVIEW_HEIGHT, maxH);
export function setTagsBrowserWidth(px: number): void {
view.tagsBrowserWidth = clamp(
Math.round(px),
MIN_TAGS_BROWSER_WIDTH,
MAX_TAGS_BROWSER_WIDTH
);
persist();
}
export function toggleTagsBrowser(): void {
view.tagsBrowserCollapsed = !view.tagsBrowserCollapsed;
persist();
}
export function openPreview(): void {
view.previewOpen = true;
}
export function closePreview(): void {
view.previewOpen = false;
}
export function togglePreview(): void {
view.previewOpen = !view.previewOpen;
}
export function setThumbnailSize(size: ThumbnailSize): void {
view.thumbnailSize = size;
persist();