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

@@ -120,11 +120,11 @@
with the px-2 of Views/Heaps rows; +12px per nested level.
-->
<div
class="group flex h-[24px] items-center rounded pr-2 text-[12px] leading-tight hover:bg-accent"
class="group flex h-[22px] items-center rounded pr-2 text-[12px] leading-tight hover:bg-accent"
class:bg-primary={active}
class:text-primary-foreground={active}
class:hover:bg-primary={active}
style="padding-left: {8 + depth * 12}px;"
style="padding-left: {4 + depth * 12}px;"
>
{#if hasChildren}
<button
@@ -136,10 +136,10 @@
>
{open ? '▾' : '▸'}
</button>
{:else if depth > 0}
{:else}
<!-- Spacer keeps childless siblings aligned with their chevroned
peers at nested depths. Skipped at depth 0 so root folders
left-align with the Views/Heaps rows. -->
peers at every depth, so labels share a common left edge
across the sidebar (folders, heaps, views, manage). -->
<span class="inline-block h-[18px] w-4" aria-hidden="true"></span>
{/if}
<!--
@@ -148,8 +148,7 @@
zone right where the user's eye lands.
-->
<button
class="flex min-w-0 flex-1 items-center text-left"
class:px-1={hasChildren || depth > 0}
class="flex min-w-0 flex-1 items-center pl-1 text-left"
onclick={() => onPick(node.path)}
ondblclick={readonly ? undefined : () => onRename?.(node.path)}
title={node.path}
@@ -158,7 +157,7 @@
{#if counts && counts[node.path] !== undefined}
{@const n = counts[node.path]}
<span
class="ml-auto shrink-0 rounded px-1 text-[10px] tabular-nums {active
class="ml-auto flex h-4 min-w-[24px] flex-shrink-0 items-center justify-center rounded px-1 text-[10px] tabular-nums {active
? 'bg-primary-foreground/15 text-primary-foreground'
: 'bg-secondary text-muted-foreground'}"
>