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>
56 KiB
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--radiusya definido (alias de--radius-2xl; resuelve losvar(--radius,12px)de share-public/device-verify). - f0-type (M) — Escala tipográfica en rem
--text-2xs…--text-6xlcon--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.5enbody;html { font-size: 100% }(honra zoom); clases.heading-page/-section/-card/-eyebrow+.proseen typography.css. - f0-zindex (S) — Capas semánticas
--z-*(base…max) con huecos para insertar. - f0-motion (S) —
--motion-*(instant…spinner) +--ease-*con 5 curvascubic-bezier(standard/emphasized de desaceleración por defecto) +--spin-duration. - f0-elevation (M) — Recetas
--shadow-xs…2xlcompuestas;--color-shadow-5xlmuerto eliminado y colisión dark arreglada (base 0.3 < md 0.34 < lg 0.38). - [~] f0-breakpoints (S) — Tokens
--bp-xs…xldefinidos como referencia para JS/docs. ⚠️ Diferido (razón real, no "dependencia"): lightningcss SÍ soporta@custom-media, peroweb/mod.rssirve el CSS crudo desdestatic/en debug (cargo run), sin preprocesar.@custom-mediano 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@mediade forma nativa. Quedan como referencia. - f0-density (S) — Tokens
--density-*(row/gap/control) + overridehtml[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 de100vhen 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-accentañ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 #b45309AA,border #f59e0b); el oro brillante#ffc107se retira. 7 variantes → alias. - f0-semantic-danger (L) — Unificado a
#ef4444/#dc2626;danger-text-altahora usa el rojo AA. - f0-semantic-info (L) — Unificado a azul (
text #1d4ed8AA + 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-ringañadidos. Migración de enlaces → Fase 2 (f2-contrast-link). - f0-on-accent (S) —
--color-on-accent: #ffffffañadido. Repunteo de consumidores de--color-danger-text→ Fase 1. - [~] f0-brand-mark (M) — Parcial:
--color-logo-gradientcanó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 enbase/a11y.css(@media prefers-contrast) que stylelint habría marcado; movidos avariables.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-texty 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 spincanónico, conectado temprano en main.css. - f0-delete-spin-dupes (S) — 6
@keyframes spinduplicados 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-onlyañ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-heightunitless 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-durationcambia el valor). - f1-icon-size-scale (M) — Diferido (separar
font-sizede icono vs texto no es automatizable sin ambigüedad). - f1-tracking-normalize (M) — Diferido (los
letter-spacingen 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,h3actualizado) → ordenh1→h2→h2. - f1-h-login (S) —
<h1 class="sr-only">OxiCloud</h1>añadido (usa la utilidad.sr-onlyde 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-primaryaliasado 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-textcomo fondo →--color-danger-bg). - f1-auth-foreground (S) — 3×
--color-danger-textforeground →--color-on-accenten auth.css (+ 2× en device-verify). - f1-share-logo (S) — Tile de marca canónico (
.brand-markcon el SVG real) añadido a share.html. - f1-device-logo (S) — Tile
.brand-markañadido a device-verify.html (SVG inline → no hizo falta icons.js). - f1-share-emoji-expired (S) — 🚫
🚫→ SVG alert-circle (color danger). - f1-share-emoji-file (M) — 📄
📄→ 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-cloudgenérico → marca SVG real en el modal "Acerca de" (+ gradiente unificado a--color-logo-gradient). - (nuevo) brandLogo.css — Componente
.brand-markcanó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-cellen móvil). - f1-list-fav-recent (M) — Cubierto por el reflow universal (mismo
.file-item). - f1-grid-density (M) —
--grid-card-mintokenizado (200px → 140px en móvil) + gap apretado. - f1-actions-bar-responsive (M) —
flex-wrap+height:autoen 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:rooten media query. - f1-vh-views (M) —
100vh → 100dvhen 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-mediano es nativo y dev sirve CSS crudo sin preprocesar → rompería las media queries encargo 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) copiastatic/→static-dist/, bundlea CSS/JS aapp.{hash}.css|js(content-hash FNV-1a), minifica con lightningcss (CSS) y oxc (JS), reescribeindex.htmly actualizasw.js. Sin npm/devDependencies — todo son crates Rust ya enCargo.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_htmlinyecta<link rel="preload" as="style">+<link rel="modulepreload">para los bundles hasheados, justo tras<meta charset>(charset queda primero). Eltheme-init.jsclásico es render-blocking y retrasa el descubrimiento de los bundles; los hints arrancan ambas descargas en paralelo, ahorrando un RTT serializado. Verificado: harnessrustcaislado sobre elindex.htmlreal → 1 stylesheet + hints presentes; build release regenerastatic-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 duranterewrite_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 enstatic-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.rses un concatenador del grafo completo (un IIFE, eliminaimport/export). Code-splitting de verdad necesita emisión de chunks + dedup de deps compartidas enbuild.rsy refactor de los entry points (photos/music) aimport()dinámico. admin/profile ya están fuera del bundle (páginas aparte, minificadas sueltas porminify_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 (reusainfer, 0 deps nuevas) detecta el formato real por magic bytes; los 3image/jpeghardcodeados 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 !importantdel languageSelector eliminados → el toggle de idioma recupera anillo de teclado. - f2-btn-focus (S) —
.btn:focus-visibleexplí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 spincanó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 acentobox-shadow), que para inputs debe mostrarse en:focus(ratón+teclado) — convertirlos a:focus-visibleserí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#f5f7fay 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+ iconosaria-hidden. - f2-nav-aria-current (S) —
aria-current="page"en el item activo (ensetCurrentSection, 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íanlang. - [~] 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-labelledbyen 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-hiddenen 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-liveglobal (polite + assertive,.sr-only, lazy); uiNotifications.js anuncia cada notificación (errores enassertive). El toast de errorBoundary ya teníarole="alert".node --checkOK; 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-motionen 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 usaHighlightdel sistema.
Skeletons y breadcrumb (loading sin salto)
- f2-skeleton-component (M) — skeleton.css creado: primitivo
.skeletonreutilizable +.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-iconen 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-visiblede 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 enfinally(auth.js) + CSS de spinner en.auth-button.node --checkOK. - f2-ext-aria (M) —
autocompleteen 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-toggleconaria-expanded/aria-controls); al abrir, foco al email. Pantalla más tensa e intencional.node --checkOK.
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 conbackground-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-labellocalizada, icono eye/eye-off por máscara).node --checkOK. - 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-localesverde. - f2-auth-ambient (M) — Fondo atmosférico premium centralizado en
--brand-ambient(spotlight conbg-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 usabai18n.tcon 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-localesverde. - 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_ates Unix en segundos (~1.7e9),new Date()lo tomaba como ms → epoch+21d. Fix: pasardateValdirecto (el formatter ya convierte seg→ms). Verificado: OLD→1970, NEW→2025;node --checkOK. Otros renders sin el antipatrón;profile.jsusa ISO (no afectado). - f-grid-thumbnails (M) — Miniaturas de la rejilla:
.file-iconera fijo 100×70 (imagen pequeña, letterbox) → ahora full-width conaspect-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-cellpero el texto va en unspan(block) → no aplicaba. Movido alspan(nowrap/overflow/ellipsis) +titleen 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 comotitle).
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 + miniaturascale(1.04)dentro deloverflow:hidden), anillo interior en la miniatura (inset 0 0 0 1px), tipografíatext-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-controlnuevo +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:noneen 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:
_mountAvatarVignettesre-renderiza la vignette conreplaceChildren(p.ej. al actualizarse el storage), pero el popover deattachTooltipestá portalado adocument.bodyy 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 --checkOK.
Pulido del menú de usuario (desplegable) ✅
- f-menu-no-avatar-tooltip (S) — Opción
noTooltipen 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-visibleen 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-labeli18n (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-withinpara teclado) → filas más limpias.
Última milla del login (tras captura) ✅
-
f-login-bg-stronger (S) — El fondo se veía blanco/amateur →
--brand-ambientreforzado (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. Claveauth.capsLocken los 16 locales. -
[—] f-login-sso — Ya existía: botón SSO condicional (
configureOidcLoginUIcuando 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(_sortFieldque sobreescribe elorder_bydel group-by y fuerza_groupBy='';setSortField+ gettercurrentSort); el header se sincroniza desde_loadPage(header click / toolbar group-by / dirección).aria-labellocalizado con dirección (sort.asc/descen 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_byserver-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.
- Nota: el sort plano usa
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 ai18n.tvía workflow multi-agente (1 agente/archivo, en paralelo) + manejo de interpolación (${var}→{{param}}). 67 clavesnotif.*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-localesverde, 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-imagees flex item y elmin-width: autopor defecto de flexbox ignora elmax-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-statemejorado (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 --checkOK. - [~] 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 (_fadeInTileschequeaimg.complete, listenersonce) → se acaba el "pop-in".node --checkOK. - 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 iconspin) + 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: balanceen headings +prettyen prosa (empty-state, about, auth) en typography.css. - f3-tabular-nums (M) —
font-variant-numeric: tabular-numsen 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: prettyaplicado; 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-hoverplano) → 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
qlmanagede macOS) → PWA instalable en todas las plataformas. Mantiene los SVG (any) + theme-color light/dark. - f3-favicon-set (M) —
apple-touch-iconahora 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.png1200×630 rasterizado (recortado del thumbnail cuadrado de qlmanage);og:image/twitter:imageapuntan 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) +sipspara recortar el OG. Dimensiones exactas verificadas (180²/192²/512² + 1200×630), copiados astatic/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 ymin-height:48pxen 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 --checkOK.inertde 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 cubreaspect-ratio:1.node --checkOK. - 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); elimage-webppresente solo hace decode/lossless (no ayuda en fotos). AVIF necesitaravif. 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 --checkOK. - f4-detail-polish (S) —
::selectioncon tinte de marca +caret-coloracento (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()conIntl.RelativeTimeFormat(CLDR, localizado) + consolidados los 2timeAgoduplicados de admin/profile a usarlo (manteniendo el caso "never").node --checkOK. - f4-i18n-completeness-ci (S) — scripts/check-locales.mjs: claves faltantes/sobrantes + integridad de
{placeholders}. Wireado enjust 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.PluralRulessin 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 --checkOK. - f4-print (M) —
@media printen 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ónaria-liveglobal 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; enjust 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
h1y 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-checkque corre los 4 scripts (contrast/headings/locales/dead-tokens). Falta añadir stylelint/biome/tsc cuando hayanode_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_modulesaquí). 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-guideestá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 cratewebp. 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 adevDependencies(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 |