feat(web): fold duplicates+inbox into /review; sidebar UX cleanup

- /duplicates and /inbox routes removed and folded into /review as
  additional tabs alongside cause tabs; /duplicates keeps a redirect
  for bookmarks.
- LeftSidebar: drop import/inbox tile and favorites; show per-user
  BasePath label at the folder root.
- RightSidebar: split file header into read-only path over editable
  basename (matches sidecar rename contract); date field switches to
  plain-text ISO YYYY-MM-DD (no native datetime picker) with strict
  validation and revert-on-invalid-blur; preserves original hour.
- BulkMetadataSidebar: same ISO-only date input with invalid-state
  styling and apply-button gating.
- BulkActionBar: drop redundant Restore and Undo buttons; ⌘Z still
  reachable via gridKeyNav.
- gridKeyNav: remove favorite toggle (F) alongside the favorites view
  retirement.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 21:48:38 +02:00
parent a38c3c6e9b
commit d70244f17e
15 changed files with 294 additions and 703 deletions

View File

@@ -12,6 +12,7 @@
import {
buildTakenAtPatch,
bulkSetMarks,
isValidISODate,
type PhotoMark,
type PhotoMarksMap,
type UpdatePhotoBody
@@ -56,12 +57,12 @@
noteDraft = '';
}
const dateDraftValid = $derived(dateDraft === '' || isValidISODate(dateDraft));
async function applyDate() {
if (busy || !dateDraft) return;
// datetime-local omits the timezone; treat the input as UTC (same
// convention as the single-photo sidebar) and let PhotoPrism's
// backwrite stamp the local timezone field downstream.
const iso = `${dateDraft}:00Z`;
if (busy || !dateDraft || !isValidISODate(dateDraft)) return;
// Date-only input — stamp midnight UTC and let PhotoPrism's backwrite
// fill the local timezone field downstream.
const iso = `${dateDraft}T00:00:00Z`;
await withBusy(() =>
patchTargets(
ids,
@@ -200,14 +201,18 @@
<Calendar class="h-3 w-3" /> Date taken
</div>
<input
type="datetime-local"
class="w-full rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring"
type="text"
inputmode="numeric"
placeholder="YYYY-MM-DD"
pattern="\d{4}-\d{2}-\d{2}"
aria-invalid={!dateDraftValid}
class="w-full rounded border border-input bg-background px-1.5 py-1 text-xs shadow-sm focus:outline-none focus:ring-1 focus:ring-ring aria-invalid:border-destructive aria-invalid:text-destructive aria-invalid:focus:ring-destructive"
bind:value={dateDraft}
disabled={busy}
/>
<button
class="w-full rounded-md border border-border px-2 py-1 text-xs hover:bg-accent disabled:opacity-50"
disabled={busy || !dateDraft}
disabled={busy || !dateDraft || !dateDraftValid}
onclick={applyDate}
>
Apply date to {ids.length}