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:
@@ -91,6 +91,10 @@ function _applyPhoto(avatar, photoUrl, name) {
|
||||
* When true (and showName is true), the primary email address is shown below
|
||||
* the name in a lighter style. Name and email are wrapped in a
|
||||
* `.user-vignette__info` column. Has no effect when showName is false.
|
||||
* @property {boolean} [noTooltip=false]
|
||||
* When true, skip the email hover-tooltip. Use for the user's own avatar in
|
||||
* the toolbar — the email is already shown in the open menu header, so the
|
||||
* tooltip is redundant (and would overlap the bell).
|
||||
* @property {boolean} [showOrigin=true]
|
||||
* When true (the default), an `is_external` badge overlays the
|
||||
* bottom-right of the avatar for external users only — internal
|
||||
@@ -108,7 +112,14 @@ function _applyPhoto(avatar, photoUrl, name) {
|
||||
* @param {VignetteOptions} [options]
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function createUserVignette(userId, size = 'sm', { showName = true, showEmail = false, showOrigin = true } = {}) {
|
||||
// Tracks the hover-tooltip cleanup for each vignette so a caller that
|
||||
// re-renders the vignette (e.g. replaceChildren on a storage update) can
|
||||
// dispose the previous tooltip — otherwise its body-portalled popover orphans
|
||||
// and, if it was visible at re-render time, stays stuck on screen.
|
||||
/** @type {WeakMap<HTMLElement, () => void>} */
|
||||
const _tooltipCleanups = new WeakMap();
|
||||
|
||||
export function createUserVignette(userId, size = 'sm', { showName = true, showEmail = false, showOrigin = true, noTooltip = false } = {}) {
|
||||
const colorIdx = _colorIndex(userId);
|
||||
|
||||
const wrapper = /** @type {HTMLElement} */ (document.createElement('span'));
|
||||
@@ -182,9 +193,9 @@ export function createUserVignette(userId, size = 'sm', { showName = true, showE
|
||||
// than the native `title` attribute's ~500–1500 ms wait).
|
||||
// `aria-label` is set in parallel so screen readers still get
|
||||
// the email — popover content is mouse/keyboard-hover only.
|
||||
if (email && !showEmail) {
|
||||
if (email && !showEmail && !noTooltip) {
|
||||
wrapper.setAttribute('aria-label', email);
|
||||
attachTooltip(wrapper, email);
|
||||
_tooltipCleanups.set(wrapper, attachTooltip(wrapper, email));
|
||||
}
|
||||
if (photo) {
|
||||
_applyPhoto(avatar, photo, name);
|
||||
@@ -214,3 +225,18 @@ export function createUserVignette(userId, size = 'sm', { showName = true, showE
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose a vignette before discarding it: tears down its hover-tooltip and
|
||||
* removes the body-portalled popover so nothing orphans on re-render.
|
||||
* Safe to call on any element (no-op if it has no tracked tooltip).
|
||||
* @param {HTMLElement | null} el
|
||||
*/
|
||||
export function disposeVignette(el) {
|
||||
if (!el) return;
|
||||
const cleanup = _tooltipCleanups.get(el);
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
_tooltipCleanups.delete(el);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user