Files
Oxicloud/docs/UIUX-ROADMAP.md
T
DioCrafts 81a93a489b 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>
2026-06-15 00:24:27 +02:00

56 KiB
Raw Blame History

OxiCloud — Roadmap UI/UX: de 4.8 a 10

Plan de trabajo derivado de la auditoría multi-agente del frontend (/static). Cada tarea está anclada a archivos reales. Las fases son secuenciales: Fase 0 (fundamentos) debe ir primero porque casi todo lo demás depende de los tokens. Esfuerzo: S <2h · M ~medio día · L ~1-2 días · XL ~3 días+.

Trayectoria de puntuación

Fase Qué entrega Nota al terminar
— Estado actual 4.8
0 · Fundamentos Las 6 escalas de tokens que faltan (espaciado, tipo, radios, elevación, z-index, movimiento) + rampas de color OKLCH + higiene 5.3
1 · Migración Aplicar las escalas en los ~55 archivos CSS, gobernar la paleta, unificar la marca en superficies externas, arreglar jerarquía de headings 6.5
2 · Estados y A11y :focus-visible, disabled/loading, contraste WCAG AA, navegación por teclado, landmarks, reduced-motion, ARIA, touch targets 8.0
3 · Pulido Empty states + skeletons, placeholders de fotos, lightbox completo, feel del movimiento, login atmosférico, reflow responsive, voz de marca 9.2
4 · Clase mundial + guardarraíles Command palette, View Transitions, undo, i18n con Intl, offline, onboarding, microcopy + CI (axe, contraste, visual regression) y design system documentado que sostienen el 10 10

FASE 0 — Fundamentos (las escalas de tokens que faltan)

El problema raíz de la auditoría: solo el color está sistematizado. Aquí se crean los ejes que faltan en variables.css. 4-5 áreas propusieron estas mismas tareas de forma independiente → son la base correcta.

Escalas de tokens ✅ (resuelto — definidas en variables.css, reset.css, typography.css)

  • f0-space (S) — Escala de espaciado --space-* en grid de 4px (--space-0…--space-24, con medios pasos 2/6/10/14px). Migración de los px crudos → Fase 1.
  • f0-radius (S) — Escala --radius-* (xs…4xl,full) y token huérfano --radius ya definido (alias de --radius-2xl; resuelve los var(--radius,12px) de share-public/device-verify).
  • f0-type (M) — Escala tipográfica en rem --text-2xs…--text-6xl con --leading-*/--weight-*/--tracking-*, --font-sans/--font-mono, --measure-prose (65ch) y --icon-* separada del texto.
  • f0-type-base (S) — font-size: var(--text-base) + line-height: 1.5 en body; html { font-size: 100% } (honra zoom); clases .heading-page/-section/-card/-eyebrow + .prose en typography.css.
  • f0-zindex (S) — Capas semánticas --z-* (base…max) con huecos para insertar.
  • f0-motion (S) — --motion-* (instant…spinner) + --ease-* con 5 curvas cubic-bezier (standard/emphasized de desaceleración por defecto) + --spin-duration.
  • f0-elevation (M) — Recetas --shadow-xs…2xl compuestas; --color-shadow-5xl muerto eliminado y colisión dark arreglada (base 0.3 < md 0.34 < lg 0.38).
  • [~] f0-breakpoints (S) — Tokens --bp-xs…xl definidos como referencia para JS/docs. ⚠️ Diferido (razón real, no "dependencia"): lightningcss SÍ soporta @custom-media, pero web/mod.rs sirve el CSS crudo desde static/ en debug (cargo run), sin preprocesar. @custom-media no tiene soporte nativo de navegador → @media (--bp-sm) rompería todas las media queries en dev. Solo viable si dev también preprocesa CSS (no lo hace). Las custom props tampoco funcionan en condiciones @media de forma nativa. Quedan como referencia.
  • f0-density (S) — Tokens --density-* (row/gap/control) + override html[data-density="compact"]. Consumidos por filas/controles en Fase 1.
  • f0-sidebar-fluid (S) — --sidebar-width: clamp(220px,18vw,280px) + tokens min/max/collapsed. Rail redimensionable → Fase 1.
  • f0-dvh (S) — body { height:100vh; height:100dvh } con fallback. Migración de 100vh en views → Fase 1 (f1-vh-views).

Fundamentos de color (curado, Opción B) ✅ — validado WCAG por script

  • [~] f0-oklch-ramps (L) — Adaptado: curé los semánticos directamente a un hue sobrio cada uno con valores validados; no construí la capa formal de primitivas OKLCH (arquitectura opcional a futuro — el resultado de armonización ya se logró).
  • [~] f0-offwhites (M) — Diferido: bajo valor y riesgo de aplanar el micro-layering de superficies sin poder renderizar. Revisar con la app en navegador.
  • f0-brand-orange (M) — Naranja confirmado como único acento de marca; --color-accent-text/--color-on-accent añadidos. Degradar azul/índigo/púrpura → Fase 1 (f1-demote-*).
  • f0-textgray (M) — 14 grises → 4 tiers AA-safe (secondary/muted/subtle/faint) + todos los nombres legacy como alias (0 roturas). muted/faint/placeholder oscurecidos para pasar AA (antes 2.3–4.0:1). Adelanta f2-contrast-text.
  • f0-semantic-success (L) — Unificado a esmeralda sobrio (#16a34a); 6 verdes → alias canónicos.
  • f0-semantic-warning (L) — Unificado a ámbar (text #b45309 AA, border #f59e0b); el oro brillante #ffc107 se retira. 7 variantes → alias.
  • f0-semantic-danger (L) — Unificado a #ef4444/#dc2626; danger-text-alt ahora usa el rojo AA.
  • f0-semantic-info (L) — Unificado a azul (text #1d4ed8 AA + tier dark); variantes → alias. Calendar dots + iconos de nav regenerados a familia armonizada (S55/L62).

Tokens de acento, foco y marca

  • f0-accent-text (S) — --color-accent-text: light-dark(#d23c18, #ff8a5c) + --color-focus-ring añadidos. Migración de enlaces → Fase 2 (f2-contrast-link).
  • f0-on-accent (S) — --color-on-accent: #ffffff añadido. Repunteo de consumidores de --color-danger-text → Fase 1.
  • [~] f0-brand-mark (M) — Parcial: --color-logo-gradient canónico definido (unifica los dos gradientes divergentes). SVG único + componente de lockup + wordmark/settings-layout tokens → Fase 1 (f1-logo-*, f1-settings-container).

Higiene y andamiaje (rápidas, habilitan el resto) ✅

  • f0-stylelint-typo (S) — Glob arreglado (statc→static). 0 hex/rgba crudos fuera de variables/themes → CI no se romperá por colores. (Auditoría 2026-06: una fase posterior había metido 3 hex en base/a11y.css (@media prefers-contrast) que stylelint habría marcado; movidos a variables.css (capa de tokens, whitelisted) → invariante restaurado y reverificado.)
  • f0-debug-red (S) — Los tokens "debug" sí se usaban (badge de conteo de drag); repunteados a --color-notification-badge/--color-danger-text y eliminados de variables.css.
  • f0-legacy-badge (S) — Bloque FIXME (11 tokens *-dark-*) eliminado — confirmado 0 consumidores.
  • f0-animations-file (S) — base/animations.css creado con el @keyframes spin canónico, conectado temprano en main.css.
  • f0-delete-spin-dupes (S) — 6 @keyframes spin duplicados eliminados. oxi-spin/smdSpin (1 uso c/u) diferidos a Fase 1 (requieren tocar consumidores).
  • f0-dead-upload-toast (S) — Bloque muerto .upload-toast (162 líneas) + <div> oculto de index.html eliminados (0 referencias JS).
  • f0-sr-only (S) — .sr-only añadida en reset.css.
  • [~] f0-perf-baseline (M) — Diferida: requiere correr la app + build + Lighthouse (no disponible en este entorno). El archivo de presupuestos se crea junto al primer baseline.

FASE 1 — Migración (aplicar las escalas en todo el código)

Migrar CSS a las escalas ✅ — codemod value-preserving (1645 reemplazos, 41 archivos, 0 cambio visual)

  • f1-mig-base (S) — forms.css migrado (reset.css ya estaba; main.css no tiene valores).
  • f1-mig-layout (M) — sidebar/topbar/content migrados.
  • f1-mig-comp-core (L) — buttons/fileManager/resourceList/fileType/breadcrumb migrados.
  • f1-mig-comp-overlays (L) — dialogs/modals/shareModal/groupsModal/shareDialog/contextMenu/uploadDropdown migrados.
  • f1-mig-comp-misc (L) — notifications/tooltip/itemTooltip/search/spinner/batchToolbar/chips/userVignette/userMenu/languageSelector/csp-utilities migrados.
  • f1-mig-views (XL) — los 14 views migrados (admin/music/profile/auth/photos/trash/recent/favorites/sharedWithMe/mySharesView/inlineViewer/device-verify/share-public/photosLightbox).
  • f1-weight-normalize (M) — bold/normal + numéricos → tokens --weight-*.
  • f1-leading-normalize (S) — line-height unitless exactos → tokens --leading-*.
  • [~] f1-snap-fractional (M) — Parcial: los exactos ya van por token; quedan 184 px off-grid (15/17/11/13px) sin token exacto → requieren decisión de snapping (cambio visual). Pendiente.
  • f1-mig-root-dups (M) — Reconciliar shims raíz vs views/ (separado de la migración de valores).
  • [~] f1-mig-motion-sweep (L) — Diferido: las duraciones dominantes (0.15s/0.25s) no tienen token exacto → tokenizarlas es un cambio de feel, no value-preserving. Decisión aparte.
  • f1-spin-duration-token (S) — Diferido (los spinners usan 0.7s/1s; unificar a --spin-duration cambia el valor).
  • f1-icon-size-scale (M) — Diferido (separar font-size de icono vs texto no es automatizable sin ambigüedad).
  • f1-tracking-normalize (M) — Diferido (los letter-spacing en px/em no tienen token exacto).

Jerarquía de headings ✅ — cada página con un único h1, sin saltos

  • f1-h-index (S) — modal-title h3→h2 (selector .modal-header h2,h3 actualizado) → orden h1→h2→h2.
  • f1-h-login (S) — <h1 class="sr-only">OxiCloud</h1> añadido (usa la utilidad .sr-only de Fase 0); paneles quedan h2.
  • f1-h-admin (M) — <h1 class="sr-only">Admin panel</h1> añadido; secciones h2, subsecciones h3.
  • f1-h-profile (S) — panel de error h2→h1 (queda h1 propio en cada panel excluyente).
  • f1-h-rest (S) — share/device-verify ya tenían h1 (verificado); nextcloud-login/success/error h2→h1.

Gobernanza de paleta ✅ (parcial donde es decorativo)

  • [~] f1-fileicon-pairs (M) — Diferido: re-derivar pares bg/text de iconos (pdf/doc/image/audio/video) de los semánticos — es pulido visual, revisar con la app.
  • [—] f1-fileicon-langs (L) — Descartado por tu elección (mantener colores por lenguaje).
  • f1-calendar-dots (M) — Hecho en Fase 0 (familia armonizada S55/L62).
  • f1-demote-blue (M) — --color-primary aliasado a --color-accent → naranja único (flipa device-verify/share/userMenu).
  • f1-demote-oidc (S) — --color-oidc-bg → --color-info-blue (botón SSO distinto de la marca, en el azul info único).
  • f1-demote-purple (M) — Familia --color-purple-* eliminada (0 consumidores) de variables.css y del fallback de dark.css.
  • [~] f1-demote-misc-blue (M) — Parcial: share-link → info-text. admin-blue y avatar-gradient (decorativos) los dejé — bajo valor, requerirían restyle del admin.
  • f1-named-colors (S) — white/black → #ffffff/#000000; confirmado 0 named colors fuera de variables/themes.

Unificación de marca en superficies externas (primera impresión) ✅

  • f1-share-accent (S) — Hecho vía el alias --color-primary → --color-accent (wordmark + spinner de share ahora naranjas).
  • f1-device-accent (S) — Acento vía alias + bug del Deny arreglado (--color-error-text como fondo → --color-danger-bg).
  • f1-auth-foreground (S) — 3× --color-danger-text foreground → --color-on-accent en auth.css (+ 2× en device-verify).
  • f1-share-logo (S) — Tile de marca canónico (.brand-mark con el SVG real) añadido a share.html.
  • f1-device-logo (S) — Tile .brand-mark añadido a device-verify.html (SVG inline → no hizo falta icons.js).
  • f1-share-emoji-expired (S) — 🚫 &#128683; → SVG alert-circle (color danger).
  • f1-share-emoji-file (M) — 📄 &#128196; → SVG de documento (color accent).
  • [~] f1-share-card-icons (S) — Diferido: los cards de folder/file los construye publicShare.js (JS, no HTML).
  • f1-share-download-icon (S) — SVG de descarga añadido al botón Download.
  • [~] f1-login-nojs (M) — Diferido: requiere entender la lógica de paneles de auth.js (riesgo de mostrar panel equivocado).
  • f1-share-nojs (S) — <noscript> con mensaje de fallback añadido.

Marca: componente canónico

  • f1-about-real-mark (S) — fa-cloud genérico → marca SVG real en el modal "Acerca de" (+ gradiente unificado a --color-logo-gradient).
  • (nuevo) brandLogo.css — Componente .brand-mark canónico creado (gradiente único + SVG), usado en share + device + base para el resto.
  • [~] f1-logo-sidebar/admin/profile/auth (M) — Diferido: sus tiles existentes funcionan y ya usan tokens de gradiente; migrarlos al componente es bajo valor / más riesgo sin render.
  • [~] f1-settings-container (M) — Diferido: unificar admin 1080 vs profile 720 es una decisión de layout (los dashboards suelen ir más anchos).
  • [~] f1-extract-toggle (M) — Diferido: refactor de organización (el toggle solo vive en admin).

Responsive: reflow del file manager ✅ (overrides solo-móvil → desktop intacto)

  • f1-list-mobile (L) — Reflow robusto basado en flex en @media (max-width:640px): cada fila → línea compacta (icono+nombre … tamaño+acciones), header de columnas oculto. Flex ignora los tracks del grid → sin desalineación header/fila.
  • f1-list-trash-mobile (M) — Cubierto por el reflow universal (oculta .path-cell en móvil).
  • f1-list-fav-recent (M) — Cubierto por el reflow universal (mismo .file-item).
  • f1-grid-density (M) — --grid-card-min tokenizado (200px → 140px en móvil) + gap apretado.
  • f1-actions-bar-responsive (M) — flex-wrap + height:auto en móvil.
  • f1-gutter-align (S) — Token --gutter (24px) en topbar y content → comparten borde izquierdo; baja a 16px en móvil vía override de :root en media query.
  • f1-vh-views (M) — 100vh → 100dvh en 6 vistas (music/auth/admin/profile/device/share).
  • [~] f1-mobile-first-sidebar / topbar (M) — Diferido: invertir a mobile-first es refactor con riesgo de regresión desktop, valor de usuario bajo (ya funcionan).
  • [~] f1-sidebar-bp-sync (M) — Diferido (requiere revisar el supuesto 768px en JS).
  • [~] f1-mq-named (L) — Diferido (mismo bloqueo que f0-breakpoints: @custom-media no es nativo y dev sirve CSS crudo sin preprocesar → rompería las media queries en cargo run).
  • [~] f1-container-query-list (L) — Diferido (el reflow flex ya logra el objetivo móvil; container query es mejora opcional).
  • [~] f1-sidebar-resizable (L) — Diferido (feature: JS + localStorage).
  • [~] f1-density-toggle (M) — Diferido (feature: UI + wiring de los tokens --density-* ya definidos).

Rendimiento (build)

Corrección (2026-06): SÍ existe un build pipeline en Rust — build.rs (release) copia static/→static-dist/, bundlea CSS/JS a app.{hash}.css|js (content-hash FNV-1a), minifica con lightningcss (CSS) y oxc (JS), reescribe index.html y actualiza sw.js. Sin npm/devDependencies — todo son crates Rust ya en Cargo.toml. Esto desbloquea trabajo antes diferido "por no haber build step", pero cada tarea tiene su propio bloqueo real (abajo), distinto de la falsa premisa anterior.

  • f1-resource-hints (M) — Hecho (build.rs): rewrite_index_html inyecta <link rel="preload" as="style"> + <link rel="modulepreload"> para los bundles hasheados, justo tras <meta charset> (charset queda primero). El theme-init.js clásico es render-blocking y retrasa el descubrimiento de los bundles; los hints arrancan ambas descargas en paralelo, ahorrando un RTT serializado. Verificado: harness rustc aislado sobre el index.html real → 1 stylesheet + hints presentes; build release regenera static-dist/index.html.
  • f1-inline-theme-init (S) — Hecho (build.rs): theme-init.js (clásico, render-blocking, ~200 B) ahora se inlinea minificado en el <head> de index.html durante rewrite_index_html → una petición menos en el critical path en la primera carga (antes de tener SW). Debe correr antes del paint (evita flash de tema) y se mantiene en su posición. Las otras 4 páginas (login/admin/profile/share) conservan la referencia externa (archivo cacheado). Fallback al <script src> si la lectura falla. Verificado en static-dist/index.html: 0 <script src=…theme-init>, 1 script inline; archivo externo intacto.
  • [~] f1-critical-css (L) — Diferido (razón real, no "build step"): inline crítico exige extraer el subconjunto above-the-fold (necesita render headless / penthouse) y cargar el resto async sin FOUC → requiere render-review. Inlinear el bundle entero infla el HTML (que va por include_str! al binario) y mata el cache de CSS. No es seguro a ciegas.
  • [~] f1-code-split (XL) — Diferido (razón real): el bundler de build.rs es un concatenador del grafo completo (un IIFE, elimina import/export). Code-splitting de verdad necesita emisión de chunks + dedup de deps compartidas en build.rs y refactor de los entry points (photos/music) a import() dinámico. admin/profile ya están fuera del bundle (páginas aparte, minificadas sueltas por minify_tree_js). Cambio grande y arriesgado, ROI bajo en una SPA de un solo shell.
  • f1-thumb-content-type (S) — Hecho (backend Rust): thumbnail_content_type() en mime_detect.rs (reusa infer, 0 deps nuevas) detecta el formato real por magic bytes; los 3 image/jpeg hardcodeados en file_handler.rs → Content-Type correcto. El fast-path (PNG/GIF/WebP guardado tal cual) ya no se sirve como JPEG. Verificado: fmt + clippy -D warnings + 15 tests.

FASE 2 — Estados e accesibilidad (el salto a "profesional")

Foco, disabled, loading ✅ — baseline global + matriz de .btn

  • f2-focus-token (S) — base/a11y.css: :focus-visible { outline: 2px solid var(--color-focus-ring); offset 2px } global + :focus:not(:focus-visible){outline:none} (suprime el anillo en click, lo muestra con teclado).
  • f2-focus-rollout (L) — El baseline da anillo de teclado a todo elemento sin override propio (botones, nav, links, context-menu, chips…). Mouse users: 0 cambio; teclado: foco visible en toda la app.
  • f2-remove-outline-important (S) — Los 2 outline:none !important del languageSelector eliminados → el toggle de idioma recupera anillo de teclado.
  • f2-btn-focus (S) — .btn:focus-visible explícito (anillo crujiente sobre los gradientes).
  • f2-btn-disabled (M) — .btn:disabled / [disabled] / .is-disabled: dimmed, sin lift, pointer-events:none.
  • f2-btn-loading (M) — .btn.is-loading / [aria-busy]: oculta el label + spinner inline (@keyframes spin canónico + --spin-duration), bloquea interacción.
  • f2-audit-outline-none (L) — Auditado regla por regla: son 8 reglas (no 24 — el conteo previo contaba ocurrencias e ignoraba el anillo), todas en inputs de texto y todas con indicador visible → 0 peladas/rotas. 7 ya usan el patrón correcto (outline:none + anillo acento box-shadow), que para inputs debe mostrarse en :focus (ratón+teclado) — convertirlos a :focus-visible sería peor UX. La única inconsistente (.music-shares-input: borde-only a --color-text) armonizada al anillo acento estándar. Verificado: barrido confirma 0 peladas; build 0 errores.
  • [~] f2-states-components (L) — Parcial: el foco de todos los componentes ya lo cubre el baseline. Falta disabled/loading por-componente más allá de .btn.

Contraste WCAG ✅ — audit completo: 0 fallos en light Y dark (verificado por script)

  • f2-contrast-text (M) — muted/subtle/faint light → #5e6a78 (pasan AA sobre #fff, page #f5f7fa y muted #f0f3f7). El audit pilló que page/muted bg fallaban (4.43/4.28) — corregido.
  • f2-contrast-dark (M) — muted/subtle/faint dark → #9fadbe (pasan AA sobre surface, page y el hover #334155, que fallaba a 4.04/3.50).
  • f2-contrast-link (M) — auth-toggle-link, about-link, language-option activa → --color-accent-text; share-link ya iba a info (Fase 1). Iconos y estados hover/activos se dejan en accent vibrante (intencional).
  • f2-contrast-matrix (L) — Audit exhaustivo (resuelve light-dark() + alias): cada par text×bg + semántico en ambos modos. accent-text light → #cc3a16 (pasaba 4.45 sobre page → ahora ≥4.5). Resultado: 0 fallos.

Teclado, landmarks y semántica ✅ — la nav ya es operable por teclado

  • f2-nav-anchors (M) — Los 8 <div class=nav-item> → <button class=nav-item type=button> (teclado nativo Enter/Space, 0 cambio en el JS de click) + resets de botón en .nav-item + iconos aria-hidden.
  • f2-nav-aria-current (S) — aria-current="page" en el item activo (en setCurrentSection, navigation.js).
  • f2-landmarks-index (S) — .nav-menu → <nav aria-label="Primary">; .content-area → role="main" id="main". (<header> del topbar: opcional, omitido.)
  • f2-skip-link (S) — <a href="#main" class="skip-link"> como primer elemento del body + estilo en a11y.css (oculto hasta foco).
  • f2-lang (S) — lang="en" en <html> de index.html y login.html (fallback estático; languageSelector.js lo actualiza en runtime).
  • f2-landmarks-login (S) — .auth-container → role="main" (+ el h1 sr-only de Fase 1).
  • f2-landmarks-secondary (M) — role="main" en admin/profile/share/device-verify; nextcloud-* ya tenían lang.
  • [~] f2-roving-tabindex (L) — Diferido: navegación por flechas en grid/lista es JS no trivial (gestión de tabindex móvil).

Modales, live regions, formularios — núcleo de modales hecho

  • f2-modal-semantics (M) — role="dialog" + aria-modal="true" + aria-labelledby en input-modal y about-modal (id en su h2).
  • f2-modal-focus-trap (M) — Focus-trap (Tab cicla dentro) + guardado/restauración de foco al trigger en la clase Modal (modal.js). Verificado con node --check.
  • [~] f2-modal-escape (M) — input-modal ya tenía ESC + click-overlay (confirmado). Los otros overlays (share/groups/lightbox) son separados → follow-up.
  • f2-icon-button-names (M) — aria-label + aria-hidden en los botones de icono sin nombre (notif-bell, user-avatar, modal-close); el resto ya tenía label.
  • f2-theme-radio (S) — El radiogroup ya mantiene aria-checked (userMenu.js:104); arrow-roving = polish opcional.
  • f2-live-region (M) — Hecho: liveRegion.js crea una región aria-live global (polite + assertive, .sr-only, lazy); uiNotifications.js anuncia cada notificación (errores en assertive). El toast de errorBoundary ya tenía role="alert". node --check OK; bundle release verificado.
  • [~] f2-form-errors (M) — Diferido: aria-invalid/role=alert + foco al primer inválido en auth.js.
  • [~] f2-touch-targets (M) — Diferido: subir iconos a 44px es cambio visual de layout — mejor con la app delante.
  • [~] f2-lang-picker-keyboard (M) — Diferido: combobox custom, JS no trivial.

Movimiento y preferencias del sistema ✅ — media queries CSS (aditivas, riesgo cero)

  • f2-reduced-motion (S) — Guard global prefers-reduced-motion en a11y.css (animaciones/transiciones a ~0, sin scroll suave). Era 0 ocurrencias → ahora honra la preferencia del SO en toda la app.
  • f2-prefers-contrast (M) — Tier prefers-contrast: more: bordes más fuertes, muted/subtle/faint → secondary, anillo de foco 3px.
  • f2-forced-colors (S) — forced-colors: active: anillo de foco usa Highlight del sistema.

Skeletons y breadcrumb (loading sin salto)

  • f2-skeleton-component (M) — skeleton.css creado: primitivo .skeleton reutilizable + .skeleton-line/-row/-tile (la row hereda las columnas de la lista). Honra reduced-motion.
  • f2-files-skeleton (M) — Cableado: filesView.js reemplaza el spinner de carga por skeleton cards (grid) / rows (list) según app.currentView (8 placeholders, role=status/aria-busy, honra reduced-motion). Nuevos .skeleton-card (tile 4:3 + 2 líneas) y .skeleton-icon en skeleton.css.
  • [~] f2-list-views-skeleton / f2-photos-skeleton / f2-search-skeleton (S) — Pendientes: mismo patrón ya disponible; falta cablearlo en photos/search/otras vistas (la de archivos ya sirve de plantilla).
  • [~] f2-breadcrumb-collapse (L) — Diferido: colapso con menú … requiere medir ancho en JS.
  • [~] f2-lightbox-dialog (M) — Diferido: focus-trap en photosLightbox.js (reutilizable del patrón de modal.js).
  • [~] f2-photo-tile-keyboard (M) — Diferido: tiles enfocables/operables (JS).

Superficies externas: estados — login cubierto

  • f2-ext-focus (M) — Cubierto por el baseline global :focus-visible de a11y.css (las 3 páginas cargan main.css); inputs ya tienen anillo border/box-shadow.
  • f2-login-submit-loading (M) — Login: .is-loading+aria-busy+disabled en submit, reset en finally (auth.js) + CSS de spinner en .auth-button. node --check OK.
  • f2-ext-aria (M) — autocomplete en login (username/current-password/email). register/admin-setup: follow-up.
  • [~] f2-share-loading / f2-device-loading (M) — Diferidos: mismo patrón, falta cablearlo en publicShare.js / device-verify.js.
  • [~] f2-error-icons (L) — Diferido: componente de banner con icono líder (CSS+HTML).
  • f2-consolidate-ctas (M) — Hecho (tras revisar capturas): el login tenía dos botones primarios gradiente compitiendo; el de magic-link pasa a secundario (.auth-button-secondary, tinte/ghost) → una sola acción primaria por pantalla.
  • f2-progressive-magic-link (M) — Hecho: el formulario de magic-link se colapsa tras un link discreto ("¿Sin contraseña? Recíbelo por correo", #magic-link-toggle con aria-expanded/aria-controls); al abrir, foco al email. Pantalla más tensa e intencional. node --check OK.

Pulido premium de superficies auth (tras capturas) ✅ — login + setup wizard

  • f2-auth-brand-oxi (S) — Wordmark con el lockup "Oxi" en acento (.brand-oxi, gradiente de marca con background-clip:text) en los 4 paneles — el tratamiento ownable del DESIGN-SYSTEM §3.
  • f2-auth-field-icons (M) — Iconos guía (user/mail/lock) en los 11 inputs vía CSS mask (data-URI sin color → token-safe; color por --color-text-muted, acento en :focus-within). 0 SVG inline en HTML, solo una clase modificadora.
  • f2-auth-pw-toggle (M) — Show/hide de contraseña (ojo) en los 5 campos password ([data-pw-toggle], aria-pressed/aria-label localizada, icono eye/eye-off por máscara). node --check OK.
  • f2-auth-pw-match (M) — Indicador de coincidencia en vivo bajo "Confirmar contraseña" (register + admin): check verde / cruz roja + texto (auth.passwordsMatch / auth.passwords_mismatch), aria-live="polite".
  • f2-auth-stepper-connector (S) — Stepper del setup → grid 3-col con línea-track conectora + halo del paso activo (antes los círculos flotaban sueltos).
  • f2-auth-input-definition (S) — Borde de input a --color-border-medium (definición), placeholder a --color-text-muted (legibilidad); foco con anillo acento ya existía.
  • f2-auth-divider-press (S) — Divisor "o" → uppercase/tracked/muted; press táctil del botón (translateY(1px)); glow del tile suavizado (spread negativo). 2 claves i18n nuevas (magicLinkToggle, passwordsMatch) en los 16 locales (es/en propias) → check-locales verde.
  • f2-auth-ambient (M) — Fondo atmosférico premium centralizado en --brand-ambient (spotlight con bg-surface + mesh de marca) + --brand-grain (ruido fractal desaturado, mix-blend overlay, token-safe). Aplicado a login/share/device (mata la duplicación 3×); grano por ahora solo en login. lightningcss minifica el data-URI sin fallar.
  • f2-auth-readonly (S) — Campo [readonly] (admin user) ya no se atenúa como placeholder: texto full-strength + fill "locked" (--color-bg-input-alt).

Pulido tras 2.ª ronda de capturas (app completa) ✅

  • f3-share-i18n (M) — El modal de Compartir salía en inglés (faltaban 17 claves share.*/actions.*; el JS ya usaba i18n.t con fallback). Añadidas a los 16 locales (es propio + inglés de fallback) → modal en español (Compartir:, Personas, Enlaces públicos, Puede ver, Sin caducidad, Aplicar, Añadir…). check-locales verde.
  • f1-nav-active-unify (S) — Iconos del sidebar: estado activo siempre en acento (antes el icono activo cambiaba de color por nth-child → cal-7/8/9…). Decisión del usuario: mantener colores en reposo (tipo Notion), unificar solo el activo.
  • [~] f1-nav-avatar (S) — Mantenido a propósito: el color del avatar es por-usuario (hash de userId → uv-color-N, patrón Slack/Google = identidad visual), no un bug. No se fuerza a acento.

Correcciones del dashboard de archivos (3.ª ronda de capturas) ✅

  • f-bug-date-1970 (S) — BUG REAL: la fecha "Modificado" salía 21/1/1970. Causa: resourceList.js hacía formatDateTime(new Date(dateVal)) — modified_at es Unix en segundos (~1.7e9), new Date() lo tomaba como ms → epoch+21d. Fix: pasar dateVal directo (el formatter ya convierte seg→ms). Verificado: OLD→1970, NEW→2025; node --check OK. Otros renders sin el antipatrón; profile.js usa ISO (no afectado).
  • f-grid-thumbnails (M) — Miniaturas de la rejilla: .file-icon era fijo 100×70 (imagen pequeña, letterbox) → ahora full-width con aspect-ratio: 4/3 + el .file-thumb (object-fit:cover) llena edge-to-edge (rejilla uniforme tipo Drive); iconos de no-imagen centrados sobre fondo placeholder.
  • f-grid-name-ellipsis (S) — El ellipsis estaba en .name-cell pero el texto va en un span (block) → no aplicaba. Movido al span (nowrap/overflow/ellipsis) + title en los nombres (folder+file) para tooltip al truncar.
  • f-grid-meta (S) — El label redundante "Imagen" bajo cada card (ya se ve por la miniatura) → reemplazado por el tamaño (size-cell) como metadato secundario. (Carpetas muestran "--" en rejilla — edge case menor.)
  • f-files-skeleton (M) — Skeletons cableados en la carga de archivos (ver f2-files-skeleton) → sin flash de lista vacía mientras cargan miniaturas/datos.
  • f-breadcrumb-home (S) — El home folder mostraba su nombre técnico "My Folder - admin" → ahora muestra la etiqueta amigable "Archivos" (reutiliza nav.files, ya en los 16 locales; el nombre real queda como title).

Card premium de la rejilla (4.ª ronda — "producto final") ✅

  • f-card-premium (M) — Redesign de la grid card: borde hairline 1px → acento al hover (antes 2px gris), hover signature (translateY(-3px) + sombra suave grande + miniatura scale(1.04) dentro del overflow:hidden), anillo interior en la miniatura (inset 0 0 0 1px), tipografía text-sm, transiciones con tokens de motion.
  • f-card-overlay-actions (M) — Estrella/kebab/checkbox/badge-shared ya no flotan en la esquina de la card → reposicionados sobre la miniatura con scrim frosted (--color-scrim-control nuevo + backdrop-filter): checkbox top-left, estrella+kebab top-right, badge-shared top-left bajo el checkbox.
  • f-dropzone-explicit (M) — El #dropzone (mostrado al arrastrar archivos vía ui.js) pasa de caja inline pequeña a overlay full-area sobre el área de contenido (position:fixed, offset del sidebar, borde punteado, backdrop frosted, icono acento grande, "Arrastra archivos aquí"). pointer-events:none en hijos evita el flicker de dragleave.
  • [~] f-grid-share-download-btns (S) — Diferido (opcional): botones dedicados de compartir/descargar al hover vía customActions — pero ese mecanismo es global (afecta lista + carpetas), necesita scoping grid-only/file-only. El kebab ya cubre la función (el usuario lo marcó opcional).
  • [—] f-grid-select — Ya existía: selección por checkbox al hover en rejilla.
  • f-bug-tooltip-orphan (S) — BUG REAL: el tooltip del email del avatar se quedaba pegado al salir el ratón. Causa: _mountAvatarVignettes re-renderiza la vignette con replaceChildren (p.ej. al actualizarse el storage), pero el popover de attachTooltip está portalado a document.body y nadie llamaba a su cleanup → quedaba huérfano y visible. Fix: WeakMap de cleanups + disposeVignette() que se llama sobre el hijo viejo antes de re-montar. node --check OK.

Pulido del menú de usuario (desplegable) ✅

  • f-menu-no-avatar-tooltip (S) — Opción noTooltip en la vignette → el avatar propio del topbar ya no muestra el tooltip del email (redundante con el header del menú + solapaba la campana). Mata la causa del bug y el "tell" de redundancia.
  • f-menu-appearance-icon (S) — Icono líder de "Apariencia" fa-moon → fa-adjust (medio-círculo de tema/contraste; FA5+FA6) — neutro, ya no implica modo oscuro.
  • f-menu-row-focus (S) — :focus-visible en las filas del menú (mismo tinte que hover) → navegación por teclado clara.
  • [—] f-menu-segmented-aria — Ya existía: los 3 botones del segmented (sol/escritorio/luna) ya tenían aria-label i18n (light/auto/dark).
  • [~] f-menu-role-chip (S) — Diferido: mover el badge "Admin" junto al nombre en el header requiere reestructurar la vignette (generada por JS). Bajo valor / más riesgo.
  • [~] f-menu-shortcut-hints (S) — Diferido: mostrar atajos (⌘,) sin que existan los atajos reales sería una afordancia falsa; primero habría que cablear los shortcuts.

Pulido premium de la list view (tras captura) ✅

  • f-list-thumb-ring (S) — Miniaturas de lista a 40×40 + anillo interior (inset 0 0 0 1px) → consistente con la rejilla premium (no sangran sobre la fila).
  • f-list-row-accent (S) — Barra-acento izquierda en hover/selected (inset 3px 0 0 var(--color-accent), sin layout shift) + transición → énfasis "esta fila".
  • f-list-hover-actions (S) — Kebab revelado al hover (@media (hover:hover); en táctil sigue visible; visible en :focus-within para teclado) → filas más limpias.

Última milla del login (tras captura) ✅

  • f-login-bg-stronger (S) — El fondo se veía blanco/amateur → --brand-ambient reforzado (glow 0.13→0.20 + 4 blobs cálidos que llenan el lienzo, no solo esquinas) + grano a 0.6 → canvas dimensional, ya no plano.

  • f-login-autofocus (S) — Autofocus en el campo usuario al mostrar el panel (los paneles arrancan ocultos → focus por JS, no atributo) — cursor listo al cargar y al volver desde registro.

  • f-login-capslock (S) — Aviso "Bloq Mayús activado" bajo los campos de contraseña (login/register/admin) vía getModifierState('CapsLock') — evita el login fallido. Clave auth.capsLock en los 16 locales.

  • [—] f-login-sso — Ya existía: botón SSO condicional (configureOidcLoginUI cuando hay OIDC).

  • [~] f-login-remember (S) — Diferido: "Recordarme" necesita soporte backend (no hay param remember/persistent en /api/auth/login; sería un checkbox falso). Requiere TTL de refresh-token configurable.

  • [—] f-login-forgot — N/A: no hay self-service password-reset (solo admin_reset_password). El magic-link ES la vía de recuperación sin contraseña — un "¿olvidaste?" apuntaría a nada.

  • f-list-sortable-headers (M) — Hecho (opción A, sort plano tipo Drive): las cabeceras Nombre/Tipo/Tamaño/Modificado son clicables → flat sort asc/desc (sin swimlanes), con flecha en la columna activa que invierte al re-clicar. Nuevo path en filesView (_sortField que sobreescribe el order_by del group-by y fuerza _groupBy=''; setSortField + getter currentSort); el header se sincroniza desde _loadPage (header click / toolbar group-by / dirección). aria-label localizado con dirección (sort.asc/desc en los 16 locales). Verificado: state machine (click size→asc, re-click→desc, group type→limpia flat), node --check, build 0 errores.

    • Nota: el sort plano usa order_by server-side (el agrupado en swimlanes del toolbar sigue disponible aparte). El botón de dirección del toolbar ahora también re-sincroniza la flecha vía _loadPage.

Panel de notificaciones (tras captura) ✅

  • f-notif-i18n (L) — El idioma mezclado del panel (notificaciones en inglés en app española): 52 showNotification('inglés') hardcoded en 8 archivos → convertidas a i18n.t vía workflow multi-agente (1 agente/archivo, en paralelo) + manejo de interpolación (${var}→{{param}}). 67 claves notif.* nuevas traducidas al español y añadidas a los 16 locales (es propio + inglés de fallback). Verificado: node --check los 8, 0 hardcoded restantes, todas las claves referenciadas cubiertas, check-locales verde, build 0 errores.
  • f-notif-scrollbar (S) — Scrollbar del panel: del global naranja grueso a uno fino gris (6px) específico (.notif-panel-body).
  • [~] f-notif-relative-time / f-notif-fav-icon / f-notif-read-state (S) — Pendientes (menores): hora relativa ("hace 5 min" con formatRelativeTime, requiere guardar timestamp real), color del icono de favorito (verde→dorado), y estado leído/no-leído.

Bug del visor de imágenes (tras capturas) ✅

  • f-bug-viewer-overflow (S) — BUG REAL: las imágenes anchas se desbordaban del visor (scroll horizontal + barra naranja gruesa) → parecía que "se repetían" al scrollear. Causa: .inline-viewer-image es flex item y el min-width: auto por defecto de flexbox ignora el max-width: 100% (no encoge). Fix: min-width: 0; min-height: 0 → la imagen ajusta entera, sin scroll. + scrollbar fino/sutil en el visor (para el caso de zoom; reemplaza el global acento grueso). Build 0 errores.

Rendimiento (foco en shell)

  • [~] f2-icon-foit (L) — Diferido: eliminar el FOIT de iconos del shell requiere inlinear iconos críticos o un build — relacionado con el grupo de build/perf pendiente.

FASE 3 — Pulido (de "sólido" a "premium")

Empty states con CTA + ilustración — CSS compartido mejora TODAS

  • f3-empty-component (M) — .empty-state mejorado (content.css): icono accent (era gris apagado), primer <p> como título (lg/semibold/heading), desc muted con medida, spacing de CTA. Como todas las vistas comparten .empty-state/.empty-state-icon, mejora las 7 a la vez sin tocar su JS.
  • f3-empty-files (M) — CTA "Upload files" cableado en showEmptyList (ui.js) → abre el file picker. node --check OK.
  • [~] f3-empty-photos / f3-empty-search / f3-empty-trash / f3-empty-rest — Visualmente mejoradas por el CSS compartido; el CTA propio de cada una (subir fotos / limpiar búsqueda / etc.) queda de follow-up (JS por vista). Ilustraciones SVG bespoke → Fase 4.

Fotos y lightbox (nivel Google Photos)

  • f3-photo-placeholder (M) — decoding="async" + fondo placeholder muted en .photo-tile + fade-in al cargar (_fadeInTiles chequea img.complete, listeners once) → se acaba el "pop-in". node --check OK.
  • f3-lightbox-preload (M) — _preloadNeighbors() precarga el thumbnail prev/next en cada _show → navegación instantánea.
  • f3-lightbox-spinner (S) — Ya estaba: spinner girando (.photos-loading i con spin) + fallback "Failed to load" en error (verificado).
  • [~] f3-justified-grid (L) — Diferido: grid justificado necesita aspect-ratios + algoritmo de packing (feature grande).
  • [~] f3-lightbox-zoom (L) — Diferido: zoom/pan/doble-tap (gestos, JS no trivial).
  • [~] f3-lightbox-swipe (L) — Diferido: swipe táctil con rebote (gestos).

Refinamiento visual y movimiento — wins CSS aditivos hechos

  • f3-listrow-hover (S) — Transición en .file-item (bg/border) rutada por --motion-fast+--ease-standard (curva de desaceleración) → las filas ya no "snapean". El grid mantiene su transición de transform.
  • f3-text-balance (S) — text-wrap: balance en headings + pretty en prosa (empty-state, about, auth) en typography.css.
  • f3-tabular-nums (M) — font-variant-numeric: tabular-nums en storage/badge/stat-value vía una regla central (size-cell y música ya lo tenían).
  • [~] f3-prose-measure (M) — Parcial: text-wrap: pretty aplicado; la medida (max-width) ya la acotan los contenedores estrechos + .empty-state p (65ch explícito → Fase 1 via .prose).
  • [~] f3-filetype-glyphs / f3-dragdrop / f3-search-results / f3-toast-exit / f3-overlay-travel / f3-spinner-primitive / f3-shadow-adoption / f3-hover-active-press / f3-dark-elevation / f3-display-clamp — Diferidos: todos cambian valores visuales existentes de forma amplia (sombras, easing, glifos, elevación) → necesitan verse renderizados antes. El barrido de motion (rutar transiciones por --ease-*) y el de sombras (→ tokens --shadow-*) son los de mayor impacto pendiente.

Login/share atmosféricos + marca — primera impresión CSS hecha

  • f3-login-bg (L) — Fondo del login: gris plano → gradiente atmosférico (dos radiales de brand-glow sutil sobre --color-bg-page). En dark, el glow cálido sobre el navy se ve premium.
  • f3-share-bg (M) — Mismo gradiente en share y device-verify (antes bg-hover plano) → primera impresión coherente.
  • f3-card-elevation-ext (M) — Las 3 tarjetas externas (auth-panel/share-card/dag-card) unificadas a box-shadow: var(--shadow-xl) + border-radius: var(--radius-3xl).
  • f3-share-trust-footer (M) — Footer estático "🔒 Secured by OxiCloud" en la página de share. Expiry/password badge dinámicos → follow-up JS.
  • f3-logo-motion (S) — El hover del logo ya respeta reduced-motion (vía el guard global de a11y.css).
  • [~] f3-share-attribution (M+S) — Diferido: "compartido por" requiere exponerlo en la API (backend Rust) + render.
  • [~] f3-share-preview (L) — Diferido: preview inline de archivo (JS + thumbnail).
  • [~] f3-device-code-segments (M) — Diferido: input OTP segmentado (JS no trivial).
  • [~] f3-share-empty-error (M) — Diferido: mejorar empty/error de la galería (JS/CSS).
  • [~] f3-brand-voice / f3-brand-voice-i18n (M) — Diferidos: definir voz/tagline + reescribir "Acerca de" + propagar a 16 locales (contenido, decisión de marca).

Marca: favicon / PWA / OG — wiring hecho (faltan rásters que no puedo generar)

  • f3-pwa-manifest (M) — manifest.webmanifest + iconos PNG maskable 192/512 rasterizados (vía qlmanage de macOS) → PWA instalable en todas las plataformas. Mantiene los SVG (any) + theme-color light/dark.
  • f3-favicon-set (M) — apple-touch-icon ahora apunta a PNG 180×180 rasterizado (iOS ya muestra el icono al "añadir a inicio"; antes ignoraba el SVG). + SVG + favicon.ico + mask-icon en index/login/share.
  • f3-og-image (M) — og-image.png 1200×630 rasterizado (recortado del thumbnail cuadrado de qlmanage); og:image/twitter:image apuntan al PNG en index/login/share → previews reales al compartir (las redes ignoraban el SVG).
  • ✅ RESUELTO (antes el gap nº1 del audit): los 3 PNG se generaron con qlmanage -t -s N (Quick Look rasteriza SVG en macOS) + sips para recortar el OG. Dimensiones exactas verificadas (180²/192²/512² + 1200×630), copiados a static/logo/, refs actualizadas. iOS/redes/PWA ya no rotos.

Responsive: pulido — CSS + a11y del drawer hecho

  • f3-mobile-touch (M) — @media (max-width:768px): ≥44px en toggle/search/notif/avatar, nav-items y min-height:48px en filas de lista (a11y.css).
  • f3-drawer-focus-trap (M) — Toggle con aria-controls+aria-expanded (sincronizado en open/close); foco al primer nav-item al abrir y restaurado al toggle al cerrar; trap de Tab dentro del drawer abierto (navigation.js). node --check OK. inert de fondo omitido (el toggle vive en el fondo) — el overlay + trap lo cubren.
  • f3-drawer-motion (S) — Transición del sidebar → var(--motion-slow) var(--ease-emphasized) (curva de desaceleración, value-preserving 0.3s).
  • f3-scrollbar-gutter (S) — En móvil scrollbar-gutter: auto (sin gutter fantasma con scrollbars de overlay).
  • [~] f3-rtl-breakpoints (M) — Diferido: verificación de paridad RTL en cada breakpoint → necesita render.
  • [~] f3-list-secondary-line (M) — Diferido: línea secundaria (tamaño·fecha) en la fila móvil → restructura que conviene ver renderizada.

Rendimiento (assets)

  • f3-srcset-photos (M) — Tiles con srcset (icon 150w / preview 400w / large 800w) + sizes → sirve la resolución justa (icon en móvil denso, large en retina). CLS ya lo cubre aspect-ratio:1. node --check OK.
  • f3-sw-strategy (L) — Navigation preload añadido (enable + uso de preloadResponse) + bump de caché v27. El resto ya existía (precache versionado, skipWaiting, clients.claim, network-first HTML, stale-while-revalidate assets).
  • [~] f3-thumb-webp-avif (L) — Diferido: WebP lossy (el que ahorra) necesita el crate webp/libwebp (dep nueva); el image-webp presente solo hace decode/lossless (no ayuda en fotos). AVIF necesita ravif. Decisión de dependencia.
  • [~] f3-lcp-preload (M) — Diferido: en Photos/share la URL del LCP es dinámica (se conoce tras fetch JS) → no se puede <link rel=preload> estático sin lógica extra.
  • [~] f3-dead-css (L) — Diferido: barrido de CSS muerto es arriesgado sin coverage tooling (clases generadas por JS → falsos positivos).

FASE 4 — Clase mundial + guardarraíles (alcanzar y SOSTENER el 10)

Interacciones de producto premium

  • f4-command-palette (L) — Command palette global (Cmd/Ctrl+K) (commandPalette.js + css): navegación a las 8 secciones + acciones (upload/profile/about/logout/admin), filtro por substring, ↑/↓/Enter/Esc, focus restaurado. Ejecuta clicando los controles reales → no se desincroniza. node --check OK.
  • f4-detail-polish (S) — ::selection con tinte de marca + caret-color acento (scrollbar ya era custom).
  • [~] f4-shortcut-grammar / f4-shortcut-help (M/S) — Cmd+K ya es un atajo global; el sistema completo + overlay ? quedan de follow-up.
  • [~] f4-view-transitions (M) — Diferido (doable): envolver los cambios de sección en document.startViewTransition() (progresivo) — conviene verlo.
  • [~] f4-undo-destructive / f4-optimistic-everywhere / f4-dnd-finesse / f4-bulk-progress / f4-delight / f4-lightbox-filmstrip / f4-lightbox-shared-element / f4-empty-illustrations / f4-btn-signature / f4-high-dpi — Diferidos: features grandes (undo/optimista/bulk), gestos/transiciones complejas (filmstrip/shared-element/dnd), o pulido que conviene ver renderizado.

Internacionalización de verdad (16 locales) — Intl (sin deps)

  • f4-i18n-format (M) — formatFileSize → Intl.NumberFormat (separadores locale); formatDateTime/formatDateShort → pasan el locale de la app (antes el del navegador) en formatters.js.
  • f4-i18n-relative-time (S) — formatRelativeTime() con Intl.RelativeTimeFormat (CLDR, localizado) + consolidados los 2 timeAgo duplicados de admin/profile a usarlo (manteniendo el caso "never"). node --check OK.
  • f4-i18n-completeness-ci (S) — scripts/check-locales.mjs: claves faltantes/sobrantes + integridad de {placeholders}. Wireado en just frontend-check. nl.json corregido → la suite pasa ✓ (los 6 mismatches igualados a en per-clave). Hallazgo aparte: en.json mezcla {{}} y {} single en algunas claves (actions.upload.complete, groups.member_count_other) → bug latente de interpolación, consistente en los 16 → tarea i18n aparte.
  • [~] f4-i18n-plural (L) — Diferido: Intl.PluralRules sin valor sin las formas plurales traducidas por locale (datos i18n).
  • [~] f4-i18n-collation (M) — Diferido: no se encontró sort de nombres cliente (probablemente server-side).
  • [~] f4-i18n-bidi (M) — Diferido: unicode-bidi: isolate/dir="auto" en nombres de usuario (CSS/JS contenido, follow-up).
  • [~] f4-i18n-rtl-logical (XL) — Diferido: barrido físico→lógico (margin-left→margin-inline-start…) en toda la CSS.

Offline, errores, onboarding, voz

  • f4-error-boundary (M) — errorBoundary.js: window.error + unhandledrejection → toast throttled (.error-toast) + log a consola. Dependency-free, cargado primero. node --check OK.
  • f4-print (M) — @media print en a11y.css: oculta chrome (sidebar/topbar/actions/cmdk) y limpia sombras para imprimir el contenido.
  • [~] f4-offline-shell / f4-offline-queue (L/XL) — Diferidos: banner online/offline + cola durable de mutaciones (feature grande, termina el stub de background-sync del sw).
  • [~] f4-error-quota / f4-onboarding / f4-microcopy (M/L) — Diferidos: estados de quota, primer-uso, y sistema de microcopy (contenido/feature).
  • f4-live-region-async (M) — Hecho (ver f2-live-region): región aria-live global que anuncia las notificaciones asíncronas.
  • [~] f4-pwa-install (M) — Diferido (doable): prompt beforeinstallprompt + botón de instalar (tiene componente visual → mejor decidir ubicación con la app delante).
  • [~] f4-sr-flow-pass (L) — Diferido: test manual con lector de pantalla (no automatizable aquí).

Design system documentado (la fuente de verdad)

  • f4-token-docs (L) — scripts/gen-token-docs.mjs → docs/TOKENS.md (430 tokens, 20 grupos, auto-generado).
  • f4-a11y-docs (S) — Sección de accesibilidad codificada en docs/DESIGN-SYSTEM.md §2.
  • f4-brand-guide (M) — Mini-guía de marca (mark/wordmark/accent/clear-space/do-don'ts/maskable/OG) en DESIGN-SYSTEM.md §3.
  • [~] f4-perf-docs (M) — Parcial: los guardarraíles documentados; falta la arquitectura de build/perf (depende de formalizar el static-dist).
  • [~] f4-gallery-page / f4-ds-site / f4-gallery-vr-axe (XL/L/M) — Diferidos: styleguide vivo + snapshot/axe → necesitan tooling/build.
  • f4-brand-drift-ci (M) — Hecho: check-brand-drift.mjs bloquea cambios silenciosos del logo (hash sha256 de logo-plain.svg) y del gradiente (--color-logo-gradient) contra un baseline bloqueado; en just frontend-check. Pasa ✓.
  • [~] f4-token-parity (M) — Diferido: contrato de paridad con assets de diseño exportados (necesita los assets de diseño como fuente).
  • [~] f4-variable-font (XL) — Diferido: evaluar/adoptar fuente variable self-hosted (decisión + asset).
  • [~] f4-opentype (S) — Diferido: features OpenType (limitado en system stack; útil con fuente variable).

Guardarraíles de CI — los sin-deps entregados y PASANDO

  • f4-contrast-ci (L) — scripts/check-contrast.mjs: resuelve light-dark()+alias y falla si cualquier par texto/fondo o semántico baja de 4.5:1 en light o dark. Pasa ✓.
  • f4-heading-order-ci (M) — scripts/check-headings.mjs: exige ≥1 h1 y sin saltos de nivel por página. Pasa ✓.
  • f4-dead-token-sweep (M) — scripts/check-dead-tokens.mjs: reporta tokens definidos sin var() (informativo, excluye prefijos dinámicos). Encontró 96 candidatos a podar.
  • f4-ci-single-gate (M) — Recipe just frontend-check que corre los 4 scripts (contrast/headings/locales/dead-tokens). Falta añadir stylelint/biome/tsc cuando haya node_modules.
  • [~] f4-stylelint-scales / f4-lint-focus / f4-lint-breakpoints (L/M) — Pendiente: reglas escribibles, pero activarlas rompe CI hasta migrar los 184 px off-grid (f1-snap-fractional) — hacerlo después de esa migración.
  • [~] f4-axe-ci / f4-keyboard-ci / f4-visual-regression / f4-dark-parity / f4-crossbrowser / f4-lighthouse-ci — Diferidos: necesitan Playwright/axe/Lighthouse (deps + browsers, sin node_modules aquí). Decisión de tooling.
  • f4-brand-drift-ci (M) — Hecho (consolidado arriba): check-brand-drift.mjs en just frontend-check.

Nota: este grupo se consolidó arriba (sección «Design system documentado»). El bloque duplicado de planificación original se eliminó para no inflar el conteo de pendientes — f4-token-docs/a11y-docs/brand-guide están hechos; el resto (gallery-page/ds-site/gallery-vr-axe/token-parity/variable-font/opentype/perf-docs) está diferido con su razón en la sección de arriba.


Camino crítico (el orden que de verdad importa)

f0-stylelint-typo ─┐ (sin esto, nada se lintea)
f0-space/radius/type/motion/elevation/zindex ─┬─► f1-mig-* (migración) ─► f2-focus/contrast/nav ─► f4-stylelint/axe/contrast-ci
f0-oklch-ramps ────► f0-semantic-* ────────────┴─► f1-demote-*/fileicon-* (paleta)
f0-brand-mark ─────► f1-logo-* ───────────────────► f1-about-real-mark
f0-perf-baseline ──► f1-critical-css ─────────────► f4-lighthouse-ci

Nota de constraint (CLAUDE.md) — actualizada 2026-06: el proyecto SÍ tiene build pipeline (build.rs: bundling + content-hash + minify vía crates Rust lightningcss/oxc, sin npm). Lo que de verdad sigue necesitando devDependencies npm / navegadores es solo el tooling de test de UI: Playwright (keyboard/visual-regression/cross-browser), axe-core (a11y automatizada) y Lighthouse-CI (presupuestos de perf). WebP lossy necesita el crate webp. El resto de "diferidos por build" en realidad están bloqueados por: render-review (critical-CSS), complejidad/ROI (code-split), o el servido de CSS crudo en dev (@custom-media). CLAUDE.md prohíbe "npm deps no listadas" → el tooling de test necesita aprobación explícita; el riesgo está acotado a devDependencies (no toca el runtime ni el binario).

Resumen por área (de la auditoría)

Área Nota actual Tareas
Tokens / fundamentos 3.0 29
Tipografía 3.0 43
Color / paleta 4.0 32
Accesibilidad 3.0 35
Layout / responsive 4.5 34
Componentes / movimiento 4.5 39
Auth / primera impresión 4.5 41
Vistas núcleo 4.0 41
Marca / cohesión 3.5 25
Rendimiento 6.5 15
Tooling / CI / docs 5.5 36
Completitud (lo que faltaba) — 32
Total 4.8 402