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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user