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

@@ -9,91 +9,99 @@
necessary here.
-->
<script lang="ts">
const MULIMAGO_ASCII = `▖ ▖ ▜ ▘
const MULIMAGO_ASCII = `▖ ▖ ▜ ▘
▛▖▞▌▌▌▐ ▌▛▛▌▀▌▛▌█▌
▌▝ ▌▙▌▐▖▌▌▌▌█▌▙▌▙▖`;
interface Props {
children?: import('svelte').Snippet;
}
let { children }: Props = $props();
interface Props {
children?: import("svelte").Snippet;
}
let { children }: Props = $props();
</script>
<header class="mule-header relative flex h-16 items-center justify-between overflow-hidden border-b border-border px-4">
<div class="relative flex items-center gap-3">
<div class="mule-sprite h-12 w-14" aria-label="Mulimago" role="img"></div>
<pre
aria-label="Mulimago"
class="rounded bg-black px-2 py-1 font-mono text-[8px] leading-[1.05] text-white"
style="letter-spacing: 0;"
>{MULIMAGO_ASCII}</pre>
</div>
<header
class="mule-header relative flex h-14 items-center justify-between overflow-hidden border-b border-border px-4"
>
<div class="relative flex items-center gap-3">
<div class="mule-sprite h-12 w-14" aria-label="Mulimago" role="img"></div>
<pre
aria-label="Mulimago"
class="rounded bg-black px-2 py-1 font-mono text-[8px] leading-[1.05] text-white"
style="letter-spacing: 0;">{MULIMAGO_ASCII}</pre>
</div>
<div class="relative flex items-center gap-3">
{@render children?.()}
</div>
<div class="relative flex items-center gap-3">
{@render children?.()}
</div>
</header>
<style>
/*
/*
* Two layers in the background: tiled desert.png on top scrolling
* right→left, dusk-sky gradient underneath. The 200px tile width is
* fixed so the `desert-scroll` keyframe moves by exactly one tile and
* loops seamlessly.
*/
.mule-header {
background-image:
url('/mule/desert.png'),
linear-gradient(to bottom, #2b3a5c 0%, #6b6b8a 35%, #d68a5c 75%, #f0c188 100%);
background-repeat: repeat-x, no-repeat;
background-size:
200px 100%,
100% 100%;
background-position: 0 bottom, 0 0;
image-rendering: pixelated;
animation: desert-scroll 24s linear infinite;
}
.mule-header {
background-image: url("/mule/desert.png"),
linear-gradient(
to bottom,
#2b3a5c 0%,
#6b6b8a 35%,
#d68a5c 75%,
#f0c188 100%
);
background-repeat: repeat-x, no-repeat;
background-size:
200px 100%,
100% 100%;
background-position:
0 bottom,
0 0;
image-rendering: pixelated;
animation: desert-scroll 24s linear infinite;
}
/* 3×2 sprite-sheet, 6-frame walk cycle. `steps(1)` makes each keyframe
/* 3×2 sprite-sheet, 6-frame walk cycle. `steps(1)` makes each keyframe
* snap (no interpolation between frames). */
.mule-sprite {
background-image: url('/mule/mule-sprites.png');
background-size: 300% 200%;
background-repeat: no-repeat;
image-rendering: pixelated;
animation: mule-walk 0.6s steps(1) infinite;
}
.mule-sprite {
background-image: url("/mule/mule-sprites.png");
background-size: 300% 200%;
background-repeat: no-repeat;
image-rendering: pixelated;
animation: mule-walk 0.6s steps(1) infinite;
}
@keyframes mule-walk {
0% {
background-position: 0% 0%;
}
16.66% {
background-position: 50% 0%;
}
33.33% {
background-position: 100% 0%;
}
50% {
background-position: 0% 100%;
}
66.66% {
background-position: 50% 100%;
}
83.33% {
background-position: 100% 100%;
}
100% {
background-position: 0% 0%;
}
}
@keyframes mule-walk {
0% {
background-position: 0% 0%;
}
16.66% {
background-position: 50% 0%;
}
33.33% {
background-position: 100% 0%;
}
50% {
background-position: 0% 100%;
}
66.66% {
background-position: 50% 100%;
}
83.33% {
background-position: 100% 100%;
}
100% {
background-position: 0% 0%;
}
}
@keyframes desert-scroll {
from {
background-position-x: 0px, 0px;
}
to {
background-position-x: -200px, 0px;
}
}
@keyframes desert-scroll {
from {
background-position-x: 0px, 0px;
}
to {
background-position-x: -200px, 0px;
}
}
</style>