Adds a planned-phase doc for the next ask on top of the closed MVP2 cycle: weighted suggestion ordering + exclude items already on the active list. - Extends item_frequency (migration 026) with `weight integer` written via two admin-only SECURITY DEFINER RPCs (set_item_frequency_weight + purge_item_frequency). The client never writes weight directly — the existing all-deny policies on item_frequency stay in place. - fetchSuggestions reorders by `weight desc, use_count desc, last_used_at desc` and accepts an excludeNames arg to drop already-added items from the dropdown. - UI lives in /collective/manage as a 3-way segmented control per row (Hide / Normal / Boost mapping to -50 / 0 / 50). Admin writes; member read-only; guest hidden. Rebrand: MVP2 reopens from 6/6 ✅ to in-progress 6/7 ✅, with Fase 15 pending. Headers in fases 9–14 bump from "·N/6" + "fases 9 → 14" to "·N/7" + "fases 9 → 15". README status table + index updated, CLAUDE.md project status appends the Fase 15 pending paragraph. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10 KiB
Fase 9 — Polish + Dark Mode
MVP2 · 1/7. Primera fase del ciclo MVP2 (rama mvp2, fases 9 → 15).
Estado: ✅ Completada (2026-05-18). Ver docs/history/fase-9-polish-dark-mode.md para detalles + paleta definitiva. 41 tests nuevos: 8 user_theme pgTAP, 12 sync_conflicts pgTAP, 4 sync-conflicts integración, 3 SC unit, 8 theme unit, 3 theme E2E, 3 reading-width E2E. WebKit specs (2 nuevos) gated en RUN_WEBKIT=1.
Objetivo: estabilizar la experiencia visual con un tema oscuro de primera clase + cerrar cuatro pulidos que se quedaron fuera del MVP sin añadir deuda técnica nueva. No se tocan features de dominio — eso queda para Fase 10.
Presencia de avatares en modo-compra (polish #5 del brainstorm): CONFIRMADO PARKED. Auditoría upstream del 2026-04-23:
- Última versión estable:
supabase/realtime v2.86.3(2026-04-21). - Bug #1617 (
handle_out/3 is undefined+ Presence crashes) sigue abierto desde 2025-11-14; ninguna release entre v2.76.5 y v2.86.3 lo arregla. - Los 2 tests de presencia siguen
.skip. Revisitar cuando #1617 se cierre.
Se documenta la auditoría en docs/history/fase-9-polish-dark-mode.md al cierre para que la próxima persona que encuentre los tests saltados no repita la investigación.
9.1 Dark mode — feature flagship
Fichero principal: apps/web/src/app.css + apps/web/tailwind.config.cjs + nuevo toggle en /settings.
Decisiones de diseño:
- Tres modos:
light|dark|system. Default en usuarios nuevos:system. - Persistencia por usuario: nueva columna
users.theme text not null default 'system' check (theme in ('light','dark','system'))(migración 015). - Aplicación:
<html data-theme="...">gestionado en(app)/+layout.sveltea partir decurrentUser.theme+window.matchMedia('(prefers-color-scheme: dark)')cuandosystem. - Paleta: triplets RGB en
app.cssbajo:root[data-theme=light]y:root[data-theme=dark]— respetar la gotcha existente: tokens como triplets espaciados (51 65 85) y consumidos conrgb(var(--token) / <alpha>), nuncahsl(). - Transición: sin animación en el cambio (evita flash/jank); el
data-themese escribe síncronamente antes del primer paint enapp.html(inline script de ~5 líneas leyendolocalStorage.getItem('theme')para evitar FOUC en refresh).
Tareas:
- 9.1.1 Migración
supabase/migrations/015_user_theme.sql— añade columnausers.themecon default'system'+ check constraint. - 9.1.2 Regenerar
packages/types/src/database.tsvíajust db-types. - 9.1.3 Definir paleta oscura en
apps/web/src/app.css:- Tokens base:
--color-bg,--color-surface,--color-border,--color-text,--color-text-muted,--color-accent,--color-danger,--color-success. - Contraste mínimo WCAG AA (4.5:1 para texto normal, 3:1 para UI chrome).
- Revisión componente a componente en
$lib/components/— todo lo que hardcodea unslate-*específico pasa a token.
- Tokens base:
- 9.1.4 Script anti-FOUC en
apps/web/src/app.html— leelocalStorage.theme(fallback'system') ymatchMediaantes del primer render. - 9.1.5
ThemeToggle.svelteen$lib/components— tres opciones (light/dark/system); onChange escribe localStorage inmediatamente + dispara UPDATE ausers.themeen background. - 9.1.6 Añadir sección "Apariencia" en
/settings/+page.sveltecon el toggle. - 9.1.7 Reactividad:
(app)/+layout.svelteobserva$currentUser.themeyprefers-color-scheme(listenerchange) y actualizadata-themesin reload. - 9.1.8 Tests:
- Unit:
ThemeToggle.test.ts— las tres opciones escriben el valor correcto en localStorage +data-theme. - Integración:
theme.spec.ts(Vitest con@testing-library) — cambio deusers.themese refleja en el DOM sin reload. - Playwright:
tests/e2e/theme.test.ts— 3 tests: T-01 toggle persiste tras reload; T-02systemrespetaprefers-color-schemeemulado; T-03 refresh en mododarkno produce flash blanco (screenshot comparativo del primer frame).
- Unit:
Criterio de aceptación: los 3 modos funcionan sin reload, persisten entre sesiones, respetan prefers-color-scheme cuando system, sin FOUC en refresh. Paleta oscura pasa WCAG AA en /lists, /list/[id] (modo compra incluido), /tasks, /notes, /settings, /collective/manage, /onboarding, /invitation/[token], /logged-out.
9.2 Ancho de lectura en desktop — max-w-2xl
Fichero: apps/web/src/routes/(app)/+layout.svelte (o wrapper equivalente).
Tareas:
- 9.2.1 Aplicar
max-w-2xl mx-autoal contenedor principal de las vistas de contenido (/lists/[id],/tasks/[id],/notes/[id]). Breakpoint:md:en adelante; móvil sigue full-width. - 9.2.2 Verificar que el modo compra sigue ocupando todo el viewport en desktop (shopping session es excepción — inmersiva).
- 9.2.3 Manual check: resize 320px → 1920px sin regresiones de layout en las 5 rutas tocadas.
Criterio de aceptación: desktop ≥ md muestra contenido en columna central; móvil sin cambios; modo compra sigue full-width.
9.3 Wiring de sync_conflicts
Fichero: apps/web/src/lib/sync/queue.ts + nueva migración + panel debug en /settings.
Estado actual: la tabla está referenciada en el header del fichero (comentario de diseño) pero nunca se creó ni se escribe. Queda como "advisory only" sin nada que aconsejar.
Tareas:
- 9.3.1 Migración
supabase/migrations/016_sync_conflicts.sql— tabla:RLS:create table public.sync_conflicts ( id uuid primary key default gen_random_uuid(), user_id uuid not null references auth.users(id) on delete cascade, collective_id uuid references public.collectives(id) on delete cascade, entity_type text not null, -- 'shopping_item' | 'task' | 'note' | ... entity_id uuid not null, local_version jsonb not null, remote_version jsonb not null, resolution text not null, -- 'remote_won' | 'local_won' created_at timestamptz not null default now() );selectsólo porauth.uid() = user_id;insertidem. Sin update/delete (append-only). - 9.3.2 En
queue.ts, cuando el merge last-write-wins descarta una versión local, insertar fila ensync_conflictscon ambos payloads (best-effort; fallo de inserción no bloquea el sync). - 9.3.3 Panel debug en
/settings(sólo visible en dev o tras toggle secreto): tabla con los últimos 20 conflictos del usuario, botón "Exportar JSON". - 9.3.4 Tests pgTAP (
009_sync_conflicts_rls.sql): 4 asserts — insert propio OK, select propio OK, select ajeno bloqueado, update/delete bloqueado. - 9.3.5 Test integración: simular conflicto (dos clientes escriben al mismo item offline, mergear) → verifica fila en
sync_conflicts.
Criterio de aceptación: los conflictos reales (no los simulados en dev) se loguean sin afectar el flujo; panel debug visible con los últimos 20 del usuario actual.
9.4 Swipe-delete E2E en WebKit
Estado actual: 2 tests .skip en apps/web/tests/e2e/mobile.test.ts porque Chromium emula mal el touch; lógica de swipe ya funciona en iOS real (validada manualmente en Fase 5).
Tareas:
- 9.4.1 Añadir
npx playwright install webkitaljust playwright-install(Justfile) y al CI. - 9.4.2 Anotar los tests para correr sólo en WebKit (
test.describe.parallelcon project filter) — no reactivar en Chromium. - 9.4.3 Desactivar
.skipde los 2 tests afectados. - 9.4.4 Validar en local (
just test-e2e --project=webkit) y en CI.
Criterio de aceptación: 2 tests previamente saltados pasan en WebKit; no se añaden nuevos saltos.
9.Z Verificación + cierre
just test-all→ 259 + deltas estimados (~8 nuevos: 3 dark-mode Playwright + 1 integración + 1 unit + 2 swipe-delete desaltados + 1 sync_conflicts integración) = 267 verdes, 3 skip (2 presencia upstream + 1 rate-limit gated).just test-rate-limitsigue pasando.- Lighthouse (
just lighthouse) no cae más de 2 puntos en modo oscuro vs claro. - CLAUDE.md actualizado: total tests + línea "Fase 9 complete: dark mode + polish".
docs/history/fase-9-polish-dark-mode.mdescrito con:- Paleta oscura definitiva (hex + motivación).
- Auditoría upstream de
#1617(para que no se repita la investigación). - Nota de migración: los usuarios existentes obtienen
theme='system'por defecto.
Riesgos / notas
-
Paleta oscura es trabajo manual tedioso — riesgo de olvidar componentes. Mitigación: grep exhaustivo de
slate-|gray-|zinc-|white|blackenapps/web/src/**/*.svelteantes del PR; cada hit o migra a token o se justifica en comentario. -
FOUC en
/logged-out. Esa ruta vive fuera de(app)y no tiene acceso a$currentUser. El inline script deapp.htmlla cubre igualmente porque leelocalStoragedirectamente — verificar en el test T-03 con esta ruta también. -
users.themeañadida mientras hay sesiones activas en prod. El JWT cachea la fila deauth.users, nopublic.users; el UPDATE apublic.users.themese lee por query, no del token → no requiere re-login. Documentar de todas formas. -
sync_conflictstienecollective_idnullable. Se deja nullable porque algunos conflictos (p.ej. cambio deusers.theme— no aplica aquí pero como principio general) no son scoped a colectivo. Si siempre va a ser non-null en la práctica, se puede apretar más adelante; peor apretarlo demasiado pronto. -
Dark mode en PNG/SVG del onboarding/empty states. Si hay ilustraciones con fondo blanco fijo, se verán raras en oscuro. Revisar
apps/web/static/+ cualquier<img>inline durante 9.1.3.
Scope explícito fuera
- No se tocan features de dominio (leave/rename/dissolve collective, account deletion, trash restore, reset-from-detail, Accept-Language) — todo eso es Fase 10.
- No se añade presencia de avatares (bloqueado upstream; documentado arriba).
- No se reintroduce campo estructurado de cantidad/unidad en items — eso es trabajo de producto, no polish.
- No se cambia la tipografía ni el spacing base — sólo color y ancho de lectura.
- No se persigue un dark mode "perfecto" en screenshots de la página
/onboardingilustrada si hay arte estático: documentar deuda y seguir.