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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user