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>
5.6 KiB
OxiCloud Design System
The single source of truth for how the frontend looks, behaves, and stays accessible. Pairs with TOKENS.md (auto-generated token reference) and UIUX-ROADMAP.md (the work plan).
1. Design tokens
Everything visual routes through a token in
static/css/base/variables.css. Six scales,
plus a curated color system. Never hand-write a raw px/hex for these — consume
a token so the next change happens in one place.
| Axis | Tokens | Notes |
|---|---|---|
| Spacing | --space-0 … --space-24 |
4px grid (+ half-steps 2/6/10/14). |
| Radius | --radius-xs … --radius-4xl, --radius-full |
--radius aliases --radius-2xl. |
| Type | --text-2xs … --text-6xl, --leading-*, --weight-*, --tracking-* |
rem-based (honors zoom). .heading-page/-section/-card utilities decouple role from element. |
| Z-index | --z-base … --z-max |
Semantic layers with gaps; no magic numbers. |
| Motion | --motion-fast/base/moderate/slow, --ease-standard/-emphasized/... |
--ease-standard is the decelerate default for entrances. |
| Elevation | --shadow-xs … --shadow-2xl |
Composed recipes layered on the --color-shadow-* alphas. |
Color. One brand accent (orange #ff5e3a), one neutral slate ramp, and five
semantics (success/warning/danger/info + accent), each unified to a single hue.
Text tiers collapse to a handful that all clear WCAG AA (4.5:1) — verified by
scripts/check-contrast.mjs. Use
--color-accent-text (not --color-accent) for accent text/links.
Governance: one brand accent, one neutral ramp, five semantics — no new hero
hues. Run node scripts/gen-token-docs.mjs after editing tokens.
2. Accessibility rules (non-negotiable)
These are enforced/aided by CI scripts and must hold for every new surface.
- Keyboard. Everything interactive is reachable and operable by keyboard.
Use
<button>/<a>(not clickable<div>s). Nav exposesaria-current="page". - Focus. A global
:focus-visiblering lives inbase/a11y.css. Neveroutline: nonewithout a paired:focus-visiblestyle. Mouse focus stays ring-free; keyboard focus never. - Contrast. Every text/background pair clears 4.5:1 in light AND dark —
node scripts/check-contrast.mjsfails the build otherwise. - Landmarks + skip link.
<nav aria-label>,role="main"/<main id="main">, a skip link as the first focusable element, andlangon<html>. - Headings. Exactly one
h1per page, no skipped levels —node scripts/check-headings.mjs. - Motion.
@media (prefers-reduced-motion: reduce)neutralizes animation globally. Also honorprefers-contrastandforced-colors. - Dialogs.
role="dialog"+aria-modal+aria-labelledby, focus trapped while open, focus restored to the trigger on close (seemodal.js). - Icon-only buttons. Always an
aria-label; decorative icons getaria-hidden="true". - Touch targets. ≥44×44px on phones.
3. Brand
- Mark. The cloud glyph (
logo-plain.svg). Always rendered from the real SVG (never a stockfa-cloud). On surfaces it sits in an accent-gradient tile via the.brand-markcomponent. - Wordmark. "OxiCloud", weight 700, tight tracking. The accent-coloured "Oxi" lockup is the ownable treatment.
- Accent. Orange→coral
#ff5e3a → #ff2d55— warm and distinctive in a blue-dominated cloud-storage market; nods to Rust oxidation. It is the only brand/primary hue (blue/indigo/purple were demoted to the single info ramp). - Logo gradient. One token (
--color-logo-gradient) everywhere. - Clear-space / min-size. Keep ≥ the tile's corner-radius of padding around the mark; don't render the wordmark below ~16px.
- Don't: recolor the mark, stretch it, place it on a low-contrast background, or introduce a second "primary" hue.
- Maskable / OG.
logo-maskable.svg(PWA, safe-zone) andog-image.svg(social). Export both to PNG for full platform support (see roadmap).
4. CI guardrails (what keeps the score from regressing)
Run all the dependency-free checks with just frontend-check:
| Script | Enforces |
|---|---|
check-contrast.mjs |
WCAG AA on every text/bg + semantic token pair (light + dark). |
check-headings.mjs |
One h1, no skipped levels, per HTML page. |
check-locales.mjs |
Locale completeness + {placeholder} integrity across all 16 locales. |
check-dead-tokens.mjs |
Reports tokens defined but never referenced (cleanup aid). |
check-brand-drift.mjs |
Locks the logo SVG hash + --color-logo-gradient against silent change. |
gen-token-docs.mjs |
Regenerates TOKENS.md. |
Build pipeline. build.rs (release) bundles + content-hashes + minifies all
CSS/JS into static-dist/ using the Rust crates lightningcss and oxc — no
npm. It also injects early <link rel="preload"> / <link rel="modulepreload">
hints for the hashed bundles. Debug builds (cargo run) serve raw static/
unprocessed, so any source CSS must be valid as-authored (e.g. @custom-media
is off the table — it has no native browser support).
Still to wire (genuinely need npm devDependencies / browsers): stylelint rules
banning raw px/hex outside :root, axe-core + Playwright (keyboard,
visual-regression, dark-parity, cross-browser), and Lighthouse-CI perf budgets.