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:
@@ -0,0 +1,146 @@
|
||||
/* ============================================================
|
||||
* Accessibility baseline — keyboard focus.
|
||||
*
|
||||
* Pointer / programmatic focus stays ring-free (no "ring on every
|
||||
* click" noise); KEYBOARD focus (:focus-visible) always gets a clear
|
||||
* accent ring. Every interactive element inherits this automatically,
|
||||
* so components only need their own :focus-visible rule when they want
|
||||
* a custom ring — and must never strip it for keyboard users.
|
||||
*
|
||||
* The outline follows the element's border-radius in modern browsers,
|
||||
* so rounded controls get a rounded ring for free.
|
||||
* ============================================================ */
|
||||
|
||||
:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--color-focus-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Skip link — visually hidden until focused, then slides in at top-left.
|
||||
Lets keyboard users jump straight to <main id="main">. */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: var(--space-2);
|
||||
left: var(--space-2);
|
||||
z-index: var(--z-max);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: var(--color-bg-surface);
|
||||
color: var(--color-text);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
transform: translateY(-150%);
|
||||
transition: transform var(--motion-fast) var(--ease-standard);
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* ── prefers-reduced-motion ──────────────────────────────────
|
||||
Vestibular safety: near-instant transitions/animations and no
|
||||
smooth scroll for users who ask the OS to reduce motion. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── prefers-contrast: more ──────────────────────────────────
|
||||
Collapse the muted text tiers up to the stronger secondary tier and
|
||||
thicken the focus ring. The high-contrast border ramp (raw colour) lives
|
||||
in base/variables.css — the token layer — so this file stays hex-free. */
|
||||
@media (prefers-contrast: more) {
|
||||
:root {
|
||||
--color-text-muted: var(--color-text-secondary);
|
||||
--color-text-subtle: var(--color-text-secondary);
|
||||
--color-text-faint: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline-width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── forced-colors (Windows High Contrast) ───────────────────
|
||||
Custom colors are overridden by the OS; ensure the keyboard
|
||||
focus ring uses a real system colour. */
|
||||
@media (forced-colors: active) {
|
||||
:focus-visible {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Global error-boundary toast (js/core/errorBoundary.js) ─── */
|
||||
.error-toast {
|
||||
position: fixed;
|
||||
bottom: var(--space-5);
|
||||
left: 50%;
|
||||
z-index: var(--z-toast);
|
||||
max-width: min(90vw, 420px);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error-text);
|
||||
border: 1px solid var(--color-badge-error-border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-lg);
|
||||
font-size: var(--text-sm);
|
||||
transform: translate(-50%, calc(100% + var(--space-5)));
|
||||
transition: transform var(--motion-base) var(--ease-standard);
|
||||
}
|
||||
|
||||
.error-toast.is-visible {
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
/* ── Print: drop the app chrome, show clean content ────────── */
|
||||
@media print {
|
||||
.sidebar,
|
||||
.sidebar-overlay,
|
||||
.top-bar,
|
||||
.actions-bar,
|
||||
.page-sticky-header,
|
||||
.cmdk-overlay,
|
||||
.skip-link,
|
||||
.error-toast {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.content-area,
|
||||
.main-content {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
* {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Touch targets ───────────────────────────────────────────
|
||||
≥44px hit areas for the key controls on touch/phone widths. */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar-toggle,
|
||||
.search-toggle-btn,
|
||||
.search-back-btn,
|
||||
.notif-bell-btn,
|
||||
.user-avatar-btn {
|
||||
min-width: 44px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.files-list-view .file-item {
|
||||
min-height: 48px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user