web: collapsible Tags section + file size on its own row

Groups score, color label, keywords, and auto-labels under one
collapsible "Tags" section on the metadata sidebar (open by default,
choice persists). Moves file size onto its own row with a HardDrive
icon so dimensions and weight read as independent facts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 23:34:01 +02:00
parent 9bba097d91
commit 680fa90cbe

View File

@@ -14,6 +14,7 @@
ExternalLink,
File,
Folder,
HardDrive,
ImageIcon,
Loader2,
MapPin,
@@ -345,11 +346,19 @@
</div>
{/if}
<!-- Dimensions + file size -->
<!-- Dimensions -->
<div class="flex items-center gap-2">
<ImageIcon class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate px-1 py-0.5 text-muted-foreground">
{dims} · {sizeStr}
{dims}
</span>
</div>
<!-- File size -->
<div class="flex items-center gap-2">
<HardDrive class="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
<span class="min-w-0 flex-1 truncate px-1 py-0.5 text-muted-foreground">
{sizeStr}
</span>
</div>
@@ -386,11 +395,25 @@
></textarea>
</div>
<!-- Score + color label — two separate sections. Click a star/swatch to
set, click the active one to clear. Sits next to Keywords because
these are the per-photo culling marks the user reaches for in the
same workflow. Stored on the mule-sidecar; PhotoPrism's PUT can't
persist them. -->
<!-- Tags — score, color label, keywords, and auto-labels grouped under
one collapsible section. Score + color are stored on the mule-
sidecar (PhotoPrism's PUT can't persist them); keywords live on
Details; auto-labels come from PhotoPrism's TF classifier and are
read-only. Open by default since these are the culling marks the
user reaches for first. -->
<details
class="rounded border border-border"
open={getMetadataSectionOpen('tags', true)}
ontoggle={(e) => setMetadataSection('tags', e.currentTarget.open)}
>
<summary
class="cursor-pointer px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground"
>
<span class="inline-flex items-center gap-1">
<Tag class="h-3 w-3" /> Tags
</span>
</summary>
<div class="space-y-2 p-2 pt-1">
<div class="space-y-1">
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Score</div>
<div class="flex items-center gap-0.5" role="group" aria-label="Rating">
@@ -429,9 +452,7 @@
<!-- Keywords as chips -->
<div class="space-y-1">
<div class="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground">
<Tag class="h-3 w-3" /> Keywords
</div>
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">Keywords</div>
<div class="flex flex-wrap gap-1">
{#each keywords as kw (kw)}
<span
@@ -463,6 +484,33 @@
</div>
</div>
<!-- Auto-labels (PhotoPrism's TensorFlow classifier output). Read-
only: editing labels requires re-indexing on PhotoPrism's
side. The dashed border + lower contrast distinguishes them
from the user-editable Keywords chips above. -->
{#if (photo.Labels ?? []).length > 0}
<div class="space-y-1">
<div class="text-[10px] uppercase tracking-wide text-muted-foreground">
Labels (auto)
</div>
<div class="flex flex-wrap gap-1">
{#each photo.Labels ?? [] as lbl (lbl.UID ?? lbl.Label?.Slug)}
{@const slug = lbl.Label?.Slug}
{@const name = lbl.Label?.Name ?? slug ?? '(unknown)'}
<a
href={slug ? `/?q=${encodeURIComponent(`label:${slug}`)}` : '#'}
class="inline-flex items-center rounded-full border border-dashed border-border bg-muted/40 px-1.5 py-0.5 text-[10px] text-muted-foreground hover:text-foreground"
title={`Source: ${lbl.Source ?? 'classifier'}`}
>
{name}
</a>
{/each}
</div>
</div>
{/if}
</div>
</details>
<!-- Related strips (only the /review route opts in). The three
scopes match the three decisions the user usually makes here:
"did all these come from the same shoot?" (folder), "same
@@ -492,33 +540,6 @@
</div>
{/if}
<!-- Auto-labels (PhotoPrism's TensorFlow classifier output). Read-only:
editing labels requires re-indexing on PhotoPrism's side. The
dashed border + lower contrast distinguishes them from the user-
editable Keywords chips above. -->
{#if (photo.Labels ?? []).length > 0}
<div class="space-y-1">
<div
class="flex items-center gap-1 text-[10px] uppercase tracking-wide text-muted-foreground"
>
<Tag class="h-3 w-3" /> Labels (auto)
</div>
<div class="flex flex-wrap gap-1">
{#each photo.Labels ?? [] as lbl (lbl.UID ?? lbl.Label?.Slug)}
{@const slug = lbl.Label?.Slug}
{@const name = lbl.Label?.Name ?? slug ?? '(unknown)'}
<a
href={slug ? `/?q=${encodeURIComponent(`label:${slug}`)}` : '#'}
class="inline-flex items-center rounded-full border border-dashed border-border bg-muted/40 px-1.5 py-0.5 text-[10px] text-muted-foreground hover:text-foreground"
title={`Source: ${lbl.Source ?? 'classifier'}`}
>
{name}
</a>
{/each}
</div>
</div>
{/if}
<!-- GPS detail. Static default (closed); user's expand/collapse
choice persists across photo switches via the view store.
Avoid data-driven defaults here — they make the `open` attr