feat: photo/video capture-date pipeline + premium UI/UX overhaul

Backend — Photos timeline now groups by real capture date instead of upload time. New MediaMetadataService (FileLifecycleHook) extracts EXIF DateTimeOriginal from images and container creation_time from videos (mov/mp4/mkv) via nom-exif, timezone-correct (OffsetTimeOriginal), persisting captured_at so the existing media_sort_date trigger takes over. Adds POST /admin/photos/metadata/reextract to backfill existing media. Falls back to upload date when no embedded date exists.

Frontend — premium grid cards: combined metadata line (relative date · size, owner avatar when shared), custom selection checkbox with a clear checked state, uniform full-width 4:3 thumbnail tiles independent of filename length, centered file-type icons, and a hit-test fix so checkbox/star/kebab clicks reach the controls (the decorative thumbnail no longer captures pointer events). Notification messages internationalised across all 16 locales. Broader polish: design tokens, a11y/focus-visible states, brand + PWA assets.

Chore — bump semver-compatible dependencies (cargo upgrade); add nom-exif 3.6.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
DioCrafts
2026-06-15 00:17:17 +02:00
parent dac299fea6
commit 81a93a489b
129 changed files with 8194 additions and 2473 deletions
+81 -81
View File
@@ -19,7 +19,7 @@
.smd-section {
border-top: 0.5px solid var(--color-border);
padding: 16px 20px;
padding: var(--space-4) var(--space-5);
}
.smd-section:first-child {
@@ -27,12 +27,12 @@
}
.smd-section-title {
font-size: 13px;
font-weight: 600;
font-size: var(--text-sm);
font-weight: var(--weight-semibold);
color: var(--color-text-subtle);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 12px;
margin-bottom: var(--space-3);
}
/* ── Loading skeleton ────────────────────────────────────────────────────────── */
@@ -40,14 +40,14 @@
.smd-skeleton {
display: flex;
flex-direction: column;
gap: 12px;
padding: 20px;
gap: var(--space-3);
padding: var(--space-5);
}
.smd-skeleton-line {
height: 14px;
background: var(--color-bg-muted);
border-radius: 6px;
border-radius: var(--radius-md);
animation: smdSkeletonPulse 1.4s ease-in-out infinite;
}
@@ -73,10 +73,10 @@
.smd-search-row {
display: flex;
gap: 8px;
gap: var(--space-2);
align-items: flex-start;
flex-wrap: wrap;
margin-bottom: 12px;
margin-bottom: var(--space-3);
}
.smd-search-wrap {
@@ -87,10 +87,10 @@
.smd-search-input {
width: 100%;
padding: 9px 12px;
font-size: 14px;
padding: 9px var(--space-3);
font-size: var(--text-base);
border: 1px solid var(--color-border);
border-radius: 8px;
border-radius: var(--radius-lg);
background: var(--color-bg-hover);
color: var(--color-text-heading);
outline: none;
@@ -112,7 +112,7 @@
right: 0;
background: var(--color-bg-surface);
border: 0.5px solid var(--color-border);
border-radius: 8px;
border-radius: var(--radius-lg);
box-shadow: 0 8px 24px var(--color-shadow-xl);
z-index: 100;
overflow: hidden;
@@ -121,8 +121,8 @@
.smd-suggestion-item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 12px;
gap: var(--space-2-5);
padding: 9px var(--space-3);
cursor: pointer;
transition: background 0.1s;
}
@@ -149,17 +149,17 @@
.smd-suggestion-hint {
margin-left: auto;
color: var(--color-text-faint);
font-size: 12px;
font-size: var(--text-xs);
font-style: italic;
flex-shrink: 0;
}
/* Role picker beside the search box */
.smd-role-select {
padding: 9px 10px;
font-size: 13px;
padding: 9px var(--space-2-5);
font-size: var(--text-sm);
border: 1px solid var(--color-border);
border-radius: 8px;
border-radius: var(--radius-lg);
background: var(--color-bg-hover);
color: var(--color-text-heading);
cursor: pointer;
@@ -170,10 +170,10 @@
.smd-add-btn {
min-height: 36px;
min-width: 44px;
padding: 8px 14px;
font-size: 13px;
font-weight: 500;
border-radius: 8px;
padding: var(--space-2) var(--space-3-5);
font-size: var(--text-sm);
font-weight: var(--weight-medium);
border-radius: var(--radius-lg);
}
/* ── Staged chips ────────────────────────────────────────────────────────────── */
@@ -181,19 +181,19 @@
.smd-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 8px;
gap: var(--space-1-5);
margin-bottom: var(--space-2);
}
.smd-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px 4px 4px;
gap: var(--space-1-5);
padding: var(--space-1) var(--space-2) var(--space-1) var(--space-1);
border: 0.5px solid var(--color-border-medium);
border-radius: 20px;
border-radius: var(--radius-4xl);
background: var(--color-bg-hover);
font-size: 14px;
font-size: var(--text-base);
color: var(--color-text-heading);
}
@@ -222,7 +222,7 @@
/* ── Member group headings ───────────────────────────────────────────────────── */
.smd-group {
margin-top: 12px;
margin-top: var(--space-3);
}
.smd-group:first-child {
@@ -232,13 +232,13 @@
.smd-group-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
gap: var(--space-1-5);
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
color: var(--color-text-subtle);
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 6px;
margin-bottom: var(--space-1-5);
}
.smd-group-badge {
@@ -249,8 +249,8 @@
height: 18px;
padding: 0 5px;
border-radius: 9px;
font-size: 11px;
font-weight: 700;
font-size: var(--text-2xs);
font-weight: var(--weight-bold);
background: var(--color-bg-muted);
color: var(--color-text-subtle);
}
@@ -260,7 +260,7 @@
.smd-member-row {
display: flex;
align-items: center;
gap: 10px;
gap: var(--space-2-5);
padding: 7px 0;
}
@@ -271,10 +271,10 @@
}
.smd-member-role-select {
font-size: 13px;
padding: 4px 8px;
font-size: var(--text-sm);
padding: var(--space-1) var(--space-2);
border: 0.5px solid var(--color-border);
border-radius: 6px;
border-radius: var(--radius-md);
background: var(--color-bg-hover);
color: var(--color-text-heading);
cursor: pointer;
@@ -291,7 +291,7 @@
background: none;
cursor: pointer;
color: var(--color-text-faint);
border-radius: 6px;
border-radius: var(--radius-md);
padding: 0;
transition:
background 0.1s,
@@ -309,10 +309,10 @@
/* ── Fallback when user-directory is unavailable ────────────────────────────── */
.smd-directory-unavailable {
font-size: 13px;
font-size: var(--text-sm);
color: var(--color-text-faint);
font-style: italic;
padding: 4px 0 8px;
padding: var(--space-1) 0 var(--space-2);
}
/* ── Link rows ───────────────────────────────────────────────────────────────── */
@@ -320,8 +320,8 @@
.smd-link-row {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 0;
gap: var(--space-2-5);
padding: var(--space-2) 0;
}
.smd-link-icon {
@@ -335,7 +335,7 @@
justify-content: center;
color: var(--color-text-subtle);
flex-shrink: 0;
font-size: 14px;
font-size: var(--text-base);
}
.smd-link-info {
@@ -346,7 +346,7 @@
.smd-link-name {
flex: 1;
min-width: 0;
font-size: 14px;
font-size: var(--text-base);
color: var(--color-text-heading);
overflow: hidden;
text-overflow: ellipsis;
@@ -355,15 +355,15 @@
.smd-link-tags {
display: flex;
gap: 6px;
gap: var(--space-1-5);
margin-top: 3px;
flex-wrap: wrap;
}
.smd-link-tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 4px;
font-size: var(--text-2xs);
padding: var(--space-0-5) var(--space-1-5);
border-radius: var(--radius-sm);
background: var(--color-bg-muted);
color: var(--color-text-subtle);
border: 0.5px solid var(--color-border);
@@ -372,37 +372,37 @@
.smd-link-actions {
display: flex;
gap: 2px;
gap: var(--space-0-5);
flex-shrink: 0;
}
/* ── Inline edit sub-panel ───────────────────────────────────────────────────── */
.smd-edit-panel {
margin: 4px 0 8px 42px;
padding: 12px;
margin: var(--space-1) 0 var(--space-2) 42px;
padding: var(--space-3);
background: var(--color-bg-hover);
border: 0.5px solid var(--color-border);
border-radius: 8px;
border-radius: var(--radius-lg);
display: flex;
flex-direction: column;
gap: 10px;
gap: var(--space-2-5);
}
.smd-edit-panel label {
font-size: 13px;
font-weight: 500;
font-size: var(--text-sm);
font-weight: var(--weight-medium);
color: var(--color-text-secondary);
display: block;
margin-bottom: 4px;
margin-bottom: var(--space-1);
}
.smd-edit-input {
width: 100%;
padding: 8px 10px;
font-size: 13px;
padding: var(--space-2) var(--space-2-5);
font-size: var(--text-sm);
border: 1px solid var(--color-border);
border-radius: 6px;
border-radius: var(--radius-md);
background: var(--color-bg-surface);
color: var(--color-text-heading);
outline: none;
@@ -418,7 +418,7 @@
.smd-edit-panel-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
gap: var(--space-2);
}
/* ── New-link creation button + form ─────────────────────────────────────────── */
@@ -427,15 +427,15 @@
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
gap: var(--space-2);
width: 100%;
padding: 10px;
margin-top: 8px;
font-size: 13px;
padding: var(--space-2-5);
margin-top: var(--space-2);
font-size: var(--text-sm);
color: var(--color-text-subtle);
background: none;
border: 1.5px dashed var(--color-border-medium);
border-radius: 8px;
border-radius: var(--radius-lg);
cursor: pointer;
transition:
background 0.15s,
@@ -454,8 +454,8 @@
.smd-pw-toggle {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
gap: var(--space-2);
font-size: var(--text-sm);
color: var(--color-text-secondary);
cursor: pointer;
user-select: none;
@@ -472,7 +472,7 @@
border-radius: 50%;
animation: smdSpin 0.6s linear infinite;
vertical-align: middle;
margin-left: 6px;
margin-left: var(--space-1-5);
}
@keyframes smdSpin {
@@ -500,10 +500,10 @@
gap: 5px;
width: 100%;
box-sizing: border-box;
padding: 4px 8px;
font-size: 14px;
padding: var(--space-1) var(--space-2);
font-size: var(--text-base);
border: 1px dashed var(--color-border-medium);
border-radius: 6px;
border-radius: var(--radius-md);
background: transparent;
color: var(--color-text-faint);
cursor: pointer;
@@ -534,8 +534,8 @@
}
.smd-expiry-chip-clear {
font-size: 13px;
line-height: 1;
font-size: var(--text-sm);
line-height: var(--leading-none);
color: var(--color-text-faint);
margin-left: auto;
padding: 0;
@@ -549,10 +549,10 @@
.smd-expiry-date-input {
width: 100%;
box-sizing: border-box;
padding: 4px 8px;
font-size: 12px;
padding: var(--space-1) var(--space-2);
font-size: var(--text-xs);
border: 1px solid var(--color-accent);
border-radius: 6px;
border-radius: var(--radius-md);
background: var(--color-bg-surface);
color: var(--color-text-heading);
outline: none;
@@ -563,6 +563,6 @@
Add button (both use padding: 9px, font-size: 13px). */
.smd-search-row .smd-expiry-chip,
.smd-search-row .smd-expiry-date-input {
padding: 9px 10px;
font-size: 13px;
padding: 9px var(--space-2-5);
font-size: var(--text-sm);
}