Recorded in plan/fase-5-mobile-ux.md (new §5.15 right next to the
existing 5.13 / 5.14 sections) and CLAUDE.md "Fase 5 — what has been
built" with the list.status → visual signal table.
Code change landed in 73bc51e.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
34 KiB
Fase 5 — Rediseño UX Mobile ("Monolith Editorial")
Estado: ✅ Completa en scope automatizable. 233 tests verdes (34 pgTAP + 140 integration + 15 unit + 44 Playwright), 2 skipped (Realtime presence upstream bug). Diferido: SEL mobile long-press tests (WebKit install), card presence avatars, max-w-2xl reading-width.
Duración estimada: 1–2 semanas
Objetivo: reescribir la capa de presentación para que el app sea mobile-first, siguiendo las mockups de Stitch en design/*/screen.png y la dirección "Monolith Editorial" de design/slate_collective/DESIGN.md.
Esta fase sigue TDD: primero los tests, al final la verificación. Toda la cobertura funcional actual (211 tests) debe seguir verde. Los nuevos tests cubren responsive breakpoints y el shell mobile.
0. Brecha entre el estado actual y la dirección de diseño
Lo que ya funciona a favor del plan:
- Modo Compra ya es full-screen overlay con z-50 (
/lists/[id]/session) — compatible mobile. - Sticky bottom input con glassmorphism básico en
/lists/[id]y/tasks/[id]. - Tokens de diseño en
app.css(RGB triplets +rgb(var(--token) / <alpha>)) compatibles con la paleta slate. - Swipe-to-delete touch en
/lists/[id]para items (parcial — sin zona roja animada ni toast de "undo").
Lo que hay que introducir/ajustar:
- El shell tiene una sidebar fija de 14rem que se muestra siempre. En mobile hay que ocultarla y mostrar top-bar con hamburger + bottom tab bar con glassmorphism.
- Ningún header tiene el tratamiento de masthead editorial (uppercase label + big title) que piden las mockups (
WORKSPACE / Shared Lists,CURRENT INVENTORY / Grocery List, etc.). - Las cards de listas en
/listsson genéricas; la mockup mobile las quiere con icono, meta ("12 items · Shared with Family") y avatares de presencia overlapping. - Swipe-to-delete no tiene zona roja con fade + toast con undo (ver §UX Patterns en
design/colectivo_design_reference.html). - Chips de frecuencia no son horizontalmente scrollables en mobile.
- Headers de sección (todas las pantallas) usan
text-xs uppercase tracking-widepero sin el pairing masthead. - Padding actual
px-6no escala: mobile pidepx-4, desktoppx-6.
Scope deliberadamente no incluido:
- Priority badges y secciones decorativas ("URGENT ACTION", "High Priority", "Bento Grid", "Backlog") de
tasks_mobile— no hay datos funcionales que los respalden; la mockup es de referencia visual, no contractual. - "SAVE" manual en notas (seguimos con autosave).
- Mic/voice search en
search_mobile(fuera de alcance). - Meta ficticia tipo "Dairy • Weekly" en ítems de la compra — el modelo no tiene categorías.
5.0 Tests primero — escribir antes de implementar
Playwright (apps/web/tests/e2e/):
mobile-shell.test.ts(4 tests):- M-01: en viewport 390×844 (iPhone 13), la sidebar NO es visible ✅
- M-02: bottom tab bar visible con 4 nav items y
aria-current="page"en la ruta activa ✅ - M-03: tap en el hamburger abre el drawer con collective switcher + settings + logout ✅
- M-04: en viewport 1280×800, sidebar visible y bottom bar hidden ✅
mobile-masthead.test.ts(4 tests): cada ruta renderizadata-testid="masthead-label"(uppercase) +data-testid="masthead-title"✅- [~]
mobile-swipe-delete.test.ts(2 tests.skip):- M-06/M-07 escritos pero en skip — Chromium touch emulation no dispara fiablemente la cadena pointer/touch que el componente consume. Re-activar con
playwright install webkiten CI. La lógica (zona roja + undo toast) vive en/lists/[id]/+page.sveltey está cubierta funcionalmente por D-04 (desktop) +undoQueue.test.ts.
- M-06/M-07 escritos pero en skip — Chromium touch emulation no dispara fiablemente la cadena pointer/touch que el componente consume. Re-activar con
Vitest (apps/web/src/lib/sync/):
undoQueue.test.ts(5 tests): scheduleUndoable → commit en TTL; undo() dispara restore y limpia; custom TTL; doble-undo no-op; fake-timers ✅
5.1 Shell responsive
apps/web/src/lib/components/layout/DesktopSidebar.svelte— extraída del layout;hidden md:flexcondata-testid="desktop-sidebar"✅apps/web/src/lib/components/layout/MobileTopBar.svelte— hamburger + collective emoji/name + avatar link a settings;md:hidden sticky top-0 z-30 backdrop-blur-xl✅apps/web/src/lib/components/layout/BottomTabBar.svelte— 4 nav items (iconos sólo, sin texto) con glassmorphism,pb-[env(safe-area-inset-bottom)],aria-current="page"en la ruta activa ✅apps/web/src/lib/components/layout/MobileDrawer.svelte— backdrop + collective switcher + manage collective + settings + logout; se cierra con click en backdrop o ESC ✅(app)/+layout.svelte— fix crítico: contenedor ahora esflex flex-col md:flex-row(antes era horizontal siempre, lo que hacía que MobileTopBar y main se renderizaran como columnas en paralelo ocultando el masthead fuera del viewport)
5.2 Masthead editorial + responsive padding
apps/web/src/lib/components/ScreenMasthead.svelte— propslabel,title, opcionalmeta/actionssnippets. Label 13px uppercase + title 26px mobile / 32px desktop.- Aplicado en
/lists(Workspace/Espacio),/tasks(Task Manager/Tareas),/notes(Repository/Repositorio),/search(Omni-Search/Buscar + title "Find Anything./Encuéntralo todo.") px-8→px-4 md:px-6en/listscontent + sticky input; mismo pase en/tasks,/notes,/searchmax-w-2xl mx-autoen pantallas de lectura — diferido; la mayoría del contenido es de listas cortas donde el ancho de pantalla no es molesto.
5.3 Rediseño de /lists mobile
- Grid responsivo: 2-col mobile, 3-col
md+(antes 2-col fijo) - Sticky input con offset para el bottom tab bar:
inset-x-0 mb-16 md:mb-0 md:left-56 - Card featured conservado + meta "ACTIVE LIST" label
- Diferido (polish): item count ("12 items") y avatares de presencia overlapping en cada card — requeriría cargar counts por lista vía RPC o DB view. No se prioriza.
5.4 Rediseño de /lists/[id] mobile
- Swipe-to-delete con zona roja:
SWIPE_MAX= 96px (ancho de la zona roja en reposo)SWIPE_THRESHOLD= 48px (latch open)SWIPE_COMMIT_THRESHOLD= 200px (full-swipe → delete)- Zona roja con
bg-red-500 text-white+ iconoTrash2yaria-label="Delete item"/"Eliminar producto" - Full-swipe dispara
handleDelete()que usascheduleUndoable()delundoQueue: eliminación optimista local + commit al DB tras 4s orestore()si el usuario pulsa Undo
- Qty stepper siempre visible en mobile — diferido; el actual funciona en touch (se muestra al tocar la fila).
- Top-bar contextual mejorado — el actual ya tiene back arrow + nombre truncado; menú 3-dots sin cambios.
5.5 Toast con "Undo" para deletes suaves
apps/web/src/lib/sync/undoQueue.ts—scheduleUndoable({ label, restore, commit, ttlMs? })devuelve id;undo(id)cancela timer y llama restore; TTL por defecto 4s → auto-commit ✅apps/web/src/lib/components/UndoToast.svelte— montado globalmente en(app)/+layout.svelte; renderiza el últimoundoQueue[$-1]centrado arriba del bottom bar; botón "Undo"/"Deshacer" ✅- Integrado en delete de item (
/lists/[id]) — fija snapshot + restaura enitemsarray si undo - Integrar en delete de task + send-to-trash de nota — diferido; los tests actuales no lo exigen y la implementación es trivial cuando se necesite.
5.6 Chips de frecuencia scrollables
ItemSuggestions.svelte—flex overflow-x-autoen mobile (shrink-0en chips);md:flex-wrap md:overflow-visibleen desktop. Scrollbar oculto vía[scrollbar-width:none] [&::-webkit-scrollbar]:hidden.- Chips siguen DESIGN.md §5:
rounded-full bg-slate-100 dark:bg-slate-800; activo invierte abg-slate-900 text-white✅
5.7 Adaptar /tasks, /notes, /search al shell + masthead
/tasks: masthead aplicado. Secciones pendientes/completadas con flip animation (desde Fase 3)./notes: masthead + contador pendiente de meta. Grid responsivo (1 col mobile → 2/3/4 col en md/lg/xl) desde Fase 3./search: masthead "Omni-Search / Find Anything.". Badges "{N} MATCHES / MATCH" junto a cada header de grupo ✅
5.8 Crear lista con botón "New list" (alineado con /notes)
Objetivo: sustituir la barra sticky de creación en /lists por el mismo patrón que usa /notes — un botón en el masthead que crea directamente el recurso con defaults y navega al detalle, donde el usuario renombra inline.
Motivación:
- La sticky bar compite con el bottom tab bar en mobile y con la presencia de ítems en la rejilla.
/notesya usa este flujo y funciona bien: crear inmediatamente + renombrar en el detalle. Consistencia entre módulos.- Un botón "big" cumple lo pedido sin ocupar viewport permanente.
Cambios propuestos:
/lists— eliminar el bloque<!-- Sticky bottom: create new list -->(div absolute bottom-0 ... backdrop-blur-[12px]) y el estado asociado (newListName,creating,nameInput,handleCreate,handleKeydown)./lists— añadir un botón primarioNew listen elactionssnippet del<ScreenMasthead>, idéntico al de/notes(fondobg-slate-900en light mode, iconoPlus):<button type="button" onclick={handleCreate} class="ml-1 inline-flex items-center gap-1 rounded-md bg-slate-900 px-3 py-1.5 text-sm font-medium text-white hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900" > <Plus size={14} strokeWidth={2} /> {m.lists_new_list()} </button>handleCreate(nueva versión) — crea la lista conname = ''(como hacecreateNote), sin mostrar input. Al resolver la promesa,goto(/lists/${created.id}).- Store
createListadmitename: ''(ya lo hace; confirmar que el backend acepta — la migración 005 tienename text NOT NULLpero sin CHECK length; una cadena vacía pasa). /lists/[id]— añadir un row de título editable al inicio del área de contenido (encima de las secciones "To buy" / "Checked"), siguiendo la pauta de/notes/[id]:<input bind:value={listName} oninput={scheduleNameSave} placeholder={m.list_name_placeholder()} class="mb-4 w-full bg-transparent text-2xl md:text-3xl font-semibold text-text-primary placeholder:text-text-secondary focus:outline-none" />- Autosave con debounce de 500ms (reutilizar la lógica que ya tiene el editor de notas).
- El nombre reemplaza el título del masthead/header actual cuando se rellena.
- i18n — añadir
lists_new_list= "New list" / "Nueva lista". El keylist_name_placeholderya existe. - Eliminar
lists_create_first(o dejar como empty-state hint) — ya no será un input placeholder.
Tests:
- Playwright
lists.test.ts— actualizarcreateList(page, name)helper:async function createList(page: Page, name: string) { await page.getByRole('button', { name: /new list|nueva lista/i }).click(); await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 }); const titleInput = page.getByPlaceholder(/weekly shop|compra semanal/i); await titleInput.fill(name); await titleInput.blur(); await page.waitForTimeout(800); // autosave debounce } - Los tests C-07 / C-11 / C-12 usan este helper → seguirán pasando sin cambios en sus aserciones, solo el flujo interno cambia.
- Nuevo test C-14: "empty name no persiste" — crear vía botón, no rellenar nada, volver a
/lists, confirmar que no aparece una tarjeta sin título (o bien filtrar en el store los registros conname = '').
Criterio de aceptación:
- No hay sticky bar en
/lists. - El botón "New list" del masthead crea la lista y lleva al detalle.
- El nombre se introduce en
/lists/[id]y se persiste vía autosave. - La suite completa sigue verde.
5.9 Responsive audit + fix de /lists/[id]
Objetivo: hacer que la pantalla de detalle de una lista funcione bien en viewports pequeños (iPhone 13 390×844), no solo en desktop.
5.9.a Auditoría — revisión previa antes de tocar código
No-código; producir una lista corta de puntos problemáticos con recomendación de fix. Buscar:
- Padding horizontal: ¿hay
px-8genérico? Debe serpx-4 md:px-8opx-4 md:px-6. - Header contextual: back arrow + nombre + menú 3-dots — ¿el nombre se trunca correctamente en pantallas estrechas?
- Fila de ítem:
- Botón delete con
hover:opacity-100osm:flex— ¿está visible en touch sin hover? - Stepper de qty — ¿ocupa demasiado ancho cuando el nombre es largo?
- Drag handle — ¿está oculto en mobile? ¿se activa con long-press?
- Swipe-to-delete funciona en touch (ya cubierto en 5.4)
- Botón delete con
- Formulario sticky de añadir ítem (
<div class="absolute bottom-0 left-56 right-0 ...">):left-56asume sidebar presente (desktop). En mobile donde no hay sidebar, debería serleft-0/inset-x-0.- Campos qty/unit al lado del nombre — ¿caben en 390px? Si no, colapsar en una segunda fila o esconderlos detrás de un toggle.
- Chips de frecuencia (
ItemSuggestions) ya son scrollables (5.6).
- Sección de tareas completadas — ¿tiene la misma estructura que pendientes o hay un chrome distinto?
- List-level actions menu (3-dots en header) — ¿el popover se abre encima del bottom tab bar en mobile?
- Modo Compra link — ¿suficientemente visible en mobile?
Entregable de 5.9.a: una sección con 3–6 bullets "problema → fix propuesto" antes de abrir 5.9.b.
5.9.b Implementación de los fixes
La implementación efectiva se hace en 5.10 (interacción rediseñada), que reescribe la fila de ítem y el formulario sticky. Los fixes de padding/width que no dependan del rediseño se pueden aplicar aquí como warm-up si sobra tiempo.
5.10 Rediseño de interacción en /lists/[id]
Objetivo: reemplazar el modelo "checkbox + botones en hover" por un flujo explícito basado en gestos y un overlay de edición. Los ítems pasan a ser button-rows clickables en vez de filas-con-checkbox.
5.10.0 Prerequisito — eliminar la columna unit del modelo
La decisión de no editar unit en el overlay (solo name) deja el campo sin ningún punto de edición en la UI. Para no arrastrar datos inalcanzables en la columna, la columna se elimina antes de tocar la fila.
Justificación del usuario: la unidad (si el caso de uso lo requiere) se escribirá dentro del propio nombre del ítem — por ejemplo "Milk 1L" o "Eggs 1 doz". El tsvector de búsqueda ya concatena solo name, así que los lexemas quedan indexados correctamente.
Cambios:
- Migración
supabase/migrations/011_drop_shopping_items_unit.sql:Nota: la columnaALTER TABLE public.shopping_items DROP COLUMN unit;search(GENERATED) ya refleja soloname, no hay que regenerarla. packages/types/src/database.ts+domain.ts— retirarunit: string | nulldel typeShoppingItem(Row / Insert / Update).apps/web/src/lib/stores/lists.ts:addItempierde el parámetrounit.updateItemretiraunitdel tipo del patch.
apps/web/src/routes/(app)/lists/[id]/+page.svelte:- Remover
newUnit,editUnity sus inputs. - Remover el render
{#if item.unit}debajo del nombre.
- Remover
apps/web/src/routes/(app)/lists/[id]/session/+page.svelte:- Cambiar
{item.quantity ?? ''}{item.unit ? ' ' + item.unit : ''}→{item.quantity ?? ''}.
- Cambiar
apps/web/messages/{en,es}.json— retirarlist_unit_label.supabase/seed.sql: retirar el valor deunitde cadaINSERT INTO shopping_items. (Opcional: concatenar el valor alname—'Milk 2 L'— para preservar legibilidad de los fixtures.)apps/web/tests/e2e/items.test.tsy cualquier otro test que referencie unit: limpiar.
Criterio de aceptación 5.10.0:
just db-resetaplica la migración sin error.just test-allsigue verde (los tests se han ajustado).- La columna
unitno existe eninformation_schema.columns.
5.10.a Fila de ítem (nueva anatomía)
┌─────────────────────────────────────────────────────────┐
│ ⋮⋮ Name of the item [− 2 +] │
└─────────────────────────────────────────────────────────┘
drag stepper
handle name (primary text) (always visible)
(left)
- Drag handle
GripVerticalsiempre visible en mobile, revelado en hover en desktop (md:opacity-0 md:group-hover:opacity-100). - Sin checkbox en modo normal.
- Nombre del ítem como
buttonaccesible (el elemento clickable de la fila). Si el usuario quiere una unidad, la incluye en el nombre ("Milk 1L"). Ver 5.10.0. - Stepper
− N +a la derecha, siempre visible, tanto mobile como desktop. - Estado checked: strikethrough +
text-text-secondary.
5.10.b Gestos en la fila
| Estado del ítem | Swipe → (right) | Swipe ← (left) |
|---|---|---|
| Unchecked | Mark as checked | — (no-op) |
| Checked | — (no-op) | Uncheck |
Racional: el swipe es una toggle "simétrico" (toda la fila se "voltea" de un lado a otro). Nunca borra. El delete vive solo en el overlay de doble-tap y en selection mode.
- El swipe right en unchecked revela una zona verde/success con icono
Check; al soltar, marca checked víacheckItem(id, userId, true). - El swipe left en checked revela una zona neutral (
bg-surface-raised) con iconoRotateCcw; al soltar, marca unchecked víacheckItem(id, userId, false). - Swipes en la dirección "equivocada" para el estado actual: snap-back sin ninguna acción (ni zona revelada ni commit).
- Ambas acciones son reversibles con el swipe contrario, sin undoQueue — la simetría hace que el undo sea la propia interacción opuesta.
- Se retira la lógica de
SWIPE_COMMIT_THRESHOLD=200(swipe-to-delete) introducida en 5.4. Se conservaSWIPE_THRESHOLD=48para el snap-to-open, y se añade unSWIPE_COMMIT_THRESHOLD_CHECK=120(más corto porque la acción es menos destructiva). - Los
scheduleUndoableque antes se disparaban desde swipe se mueven al overlay de doble-tap (botón Delete) y al bulk delete en selection mode. - Renombrar
mobile-swipe-delete.test.ts→mobile-swipe-toggle.test.tsy reescribir los casos (los actuales están.skipy ya no reflejan la semántica):- M-06: swipe-right en unchecked → checkbox transition + strikethrough aparece
- M-07: swipe-left en checked → vuelve a unchecked
- M-08: swipe-left en unchecked → no cambio (snap-back)
- M-09: swipe-right en checked → no cambio (snap-back)
Mantener
.skiphasta que se añada WebKit install a CI.
5.10.c Double-tap → overlay de edición
Doble tap corto (≤ 300 ms entre toques) en una fila (en modo normal) abre un overlay:
┌───────────────────────────┐
│ Edit item × │
│ │
│ [ milk ] │
│ │
│ [Delete] [Confirm] │
└───────────────────────────┘
- Overlay como
Dialog(o bottom sheet en mobile ≤ md;shadcn-svelteSheet / Dialog). Centrado en desktop. - Input de solo nombre prellenado con el valor actual.
- Botón Confirm: guarda el nuevo nombre (optimista + UPDATE) y cierra.
- Botón Delete: borra con
scheduleUndoable(mismo snapshot que swipe) y cierra. - Botón X en la esquina + tap fuera del overlay: cierra sin guardar.
- Doble-tap no se dispara en modo selección — allí un single tap alterna selección.
- No afecta qty/unit: qty siempre vía stepper; unit se edita desde otro lugar o queda como read-only en este plan (confirmar con el usuario si quiere unit también en el overlay).
5.10.d Drag handle → reorder
- El handle es el ÚNICO drag target (no la fila entera), para no competir con swipes y double-tap.
- Reutiliza
svelte-dnd-actionya presente; cambia eldragDisabled+ el target del dragzone (propiedaddragDisableden la fila, drag sólo habilitado al interactuar con el handle). - Mobile:
touch-action: noneen el handle para que el browser no interprete vertical-swipe-on-handle como scroll. - Solo aplica a items unchecked (los checked ya quedan al fondo con flip).
Criterio de aceptación 5.10:
- Single tap no hace nada en modo normal.
- Double tap abre el overlay y permite rename/delete.
- Swipe right unchecked → checked (verde brevemente, luego queda con strikethrough).
- Swipe right checked → delete con undo toast.
- Swipe left checked → unchecked.
- Drag handle reordena pendientes, handle sólo visible al tocar/hover.
- Qty stepper intacto, unit visible debajo del nombre si hay.
- Tests existentes D-03 (toggle) y D-04 (delete) ajustados a la nueva semántica sin perder cobertura.
5.11 Selection mode + bulk actions
Objetivo: permitir acciones masivas (delete / move / mark-all-checked) mediante un modo selección activado por long-press en mobile o botón toggle en desktop.
5.11.a Entrada y salida
- Mobile — entrada: long-press (≥ 500 ms) sobre cualquier fila entra en modo selección con esa fila preseleccionada.
- Mobile — continuación: una vez en modo selección, single-tap en cualquier fila alterna su selección.
- Desktop — entrada/salida: botón toggle
Select(iconoCheckSquare) junto al menú de 3-dots en el header de la lista. Cuando activo, el botón queda "pressed" (bg-slate-900 text-white) y cualquier click simple en una fila alterna selección. - Salida:
- Tap en "Cancel" del top-mode bar (mobile) o toggle del botón (desktop).
- Navegación fuera de la ruta.
- NO auto-salida al llegar a 0 seleccionados (evita frustración si el usuario está reorganizando).
5.11.b Chrome durante selection mode (mobile)
- Top bar contextual (reemplaza el header
back + title + 3-dots):Sticky top-0, fondo[ Cancel ] 3 selected [ ⋯ ]bg-slate-900 text-whitepara diferenciar visualmente del modo normal. - Bottom action bar (reemplaza el
BottomTabBar, sticky al mismobottomcon glassmorphism, misma altura 56 px):Iconos Lucide:[ Trash ] [ Move ] [ Check ]Trash2,FolderInput,CheckSquare. Cada botón con label sr-only + tooltip desktop. - Los elementos seleccionados muestran un checkbox prominente a la izquierda (reemplaza el drag handle mientras dure el modo; reorder deshabilitado en selección).
- El stepper de qty se oculta en selection mode para simplificar la fila.
5.11.c Chrome durante selection mode (desktop)
- El header de la lista (back + título + 3-dots) se transforma en una barra de selección inline:
← Back 3 selected · Trash · Move · Mark checked · Cancel - Los checkboxes aparecen en el lado izquierdo de cada fila, a la derecha del drag handle oculto.
- No existe bottom bar en desktop: todas las acciones caben en el header.
5.11.d Bulk actions
- Delete: borra las filas seleccionadas con un único
scheduleUndoable:label: "Deleted N items"restore: reinserta las filas (snapshot del array completo)commit: DELETE por IDs- Mantiene la misma UI de
UndoToast.
- Move to other list: abre un bottom sheet / dialog con:
- Lista de colectivos activos (
status = 'active', excluyendo la actual), cargadas vialoadListssi hace falta. - Botón "+ Create new list" al final; crea una lista vacía y mueve.
- Al confirmar:
UPDATE shopping_items SET list_id = $new WHERE id IN (...). Checked state se preserva. - Toast de confirmación "Moved N items to {list name}" (sin undo; el usuario puede volver a mover si quiere).
- Lista de colectivos activos (
- Mark selected as checked:
UPDATE shopping_items SET is_checked = true, checked_by = auth.uid(), checked_at = now() WHERE id IN (...) AND is_checked = false. Solo marca; no toggle. Si todos ya estaban checked, no-op silencioso.
5.11.e Store + backend helpers
apps/web/src/lib/stores/lists.ts:bulkDeleteItems(ids: string[])bulkMoveItems(ids: string[], newListId: string)bulkCheckItems(ids: string[], userId: string)- Cada uno hace una sola llamada Supabase con
.in('id', ids).
- No se necesita migración: todas las ops usan las mismas RLS existentes.
5.11.f Tests
- Vitest: unit tests de las tres funciones bulk en una suite nueva
rls-bulk-items.test.ts:- miembro puede bulk-delete / move / check sobre items de su colectivo
- guest no puede (RLS bloquea)
- cross-collective: Borja no puede mover items de Eva a una lista de Borja
- Playwright mobile:
mobile-selection.test.ts:- SEL-01 long-press → selection mode activo + fila preseleccionada
- SEL-02 multi-select vía single-tap
- SEL-03 bulk delete + undo toast
- SEL-04 mark selected as checked
- Playwright desktop:
selection.test.ts:- SEL-D-01 toggle del botón selection activa modo
- SEL-D-02 move-to-other-list con dialog de destino
Criterio de aceptación 5.11:
- Long-press entra en selection mode, single-tap alterna selección.
- Botón "Select" en desktop (toggle).
- Bulk delete con single undo toast.
- Bulk move con dialog que muestra listas activas + crear.
- Bulk mark-checked solo sobre los seleccionados.
5.12 Chrome de detail views (top bar oculto, sticky bars)
Objetivo: en las vistas de detalle (/lists/[id], /tasks/[id], /notes/[id], más /lists/[id]/session que ya lo hace) el MobileTopBar global desaparece y se reemplaza por una barra contextual local con back + title pegada al top. El BottomTabBar permanece siempre visible.
Cambios:
apps/web/src/routes/(app)/+layout.svelte— detectar si la ruta es una detail view y condicionalmente ocultar<MobileTopBar>:con un helper:{#if !isDetailRoute($page.url.pathname)} <MobileTopBar onMenu={() => (drawerOpen = true)} /> {/if}function isDetailRoute(path: string): boolean { return /^\/lists\/[0-9a-f-]+(\/session)?$/.test(path) || /^\/tasks\/[0-9a-f-]+$/.test(path) || /^\/notes\/[0-9a-f-]+$/.test(path); }- Para cada detail view, su header local (back arrow + título) debe ser
sticky top-0 z-30 bg-surface/80 backdrop-blur-xlpara igualar el estilo delMobileTopBarque sustituye./lists/[id]/+page.svelte— actualmente el header está dentro del scroll; mover a sticky./tasks/[id]/+page.svelte— igual./notes/[id]/+page.svelte— igual (ya tiene glassmorphism implícito por el color de fondo).
BottomTabBarsigue presente en todas las detail views salvo/lists/[id]/session(que ya se oculta explícitamente al ser un overlay full-screen con z-50).- El título contextual se trunca con ellipsis si es largo (
truncate). - Regla general: cualquier barra que una vista renderice debe ser sticky (o fixed si replace-nav), nunca inline scrolling.
Scope explícito de 5.12: solo las rutas de detalle de listas/tareas/notas. /collective/manage, /settings, /invitation/[token], /notes/archive, /notes/trash mantienen el chrome actual (fuera de este sprint).
Tests:
- Actualizar
mobile-shell.test.ts:- M-01: en una detail view (e.g.
/lists/[id]), elMobileTopBarNO está presente. - M-02: en la misma ruta, existe un header local con
back + titlesticky. - M-03: el
BottomTabBarsigue visible.
- M-01: en una detail view (e.g.
Criterio de aceptación 5.12:
- En mobile, navegar a
/lists/seed-id:MobileTopBar(hamburger + collective + avatar) no aparece.- Un header local "← Weekly shop" pegado al top, sticky al hacer scroll.
BottomTabBarvisible.
- En desktop, sin cambios (los detail headers actuales ya se ven arriba del main).
5.14 Formulario de alta inline al final de la lista
Cambio de UX: el formulario de "añadir ítem" (lists) y "añadir tarea" (tasks) dejan de estar pegados al borde inferior del viewport y pasan a vivir dentro del scroll, como el último elemento de la sección sin marcar (unchecked / pending). Las sugerencias de frecuencia bajan debajo del input en vez de encima.
Decisiones (respuestas del usuario a las preguntas 1–6):
- Posición del formulario → opción
a: al final de unchecked only, justo encima de la sección Checked. - Scroll → inline puro. Sin FAB, sin auto-scroll.
- Empty state → mensaje primero, formulario debajo (usuario puede añadir el primer ítem sin scrollear).
- Sugerencias: input arriba, chips debajo. Mobile: scroll horizontal. Desktop: wrap (comportamiento actual de
ItemSuggestions). - Aplica a
/lists/[id]y/tasks/[id]. - Selection mode: el formulario se oculta.
Cambios:
/lists/[id]/+page.svelte:- Eliminado el bloque
absolute bottom-0conbackdrop-blur. - Nuevo snippet
addItemFormrenderizado (a) dentro de la rama empty-state y (b) entre las secciones unchecked y checked. Sólo se renderiza cuando!selectionMode. ItemSuggestionsmovido dentro del snippet, debajo del input.- Padding inferior del scroll:
pb-32→pb-16 md:pb-6. data-testid="add-item-form"añadido como hook de tests futuros.
- Eliminado el bloque
/tasks/[id]/+page.svelte:- Mismo patrón con
addTaskForm(sin suggestions —/tasksno tieneitem_frequency). data-testid="add-task-form".- Reposicionamiento sin cambiar el comportamiento de Enter / botón
+.
- Mismo patrón con
Tests:
- Los tests existentes pasan sin cambios — los locators son
getByPlaceholder(/add item|añadir producto/i)/getByPlaceholder(/add task|añadir tarea/i)y funcionan estén donde estén los inputs. - Hooks
data-testidañadidos por si se quieren assertions explícitos de ubicación más adelante.
Criterio de aceptación:
- Form no flota; scrollea con la lista.
- Sugerencias debajo del input en ambos breakpoints.
- En lista vacía: mensaje + form debajo.
- Selection mode: form desaparece (el action bar toma su lugar).
5.15 Título limpio en detail views (sin eyebrow) + señal de estado
Cambio de UX: el "eyebrow" uppercase ("ACTIVE LIST" / "TASK MANAGER") que vivía sobre el título editable en /lists/[id] y /tasks/[id] se retira. El título sube para ocupar ese espacio.
Como la eyebrow anterior mezclaba "masthead label" + "status", el estado de la lista de la compra ahora se comunica de forma más directa:
list.status |
Señal visual |
|---|---|
active |
sin chrome — solo el título |
completed |
título con text-text-secondary + line-through |
archived |
pill muted (bg-slate-200 / slate-800 en dark) junto al título, data-testid="list-status-pill" |
/tasks/[id] no tiene concepto de estado en task_lists, así que ahí es simplemente el título sin eyebrow.
Archivos tocados: apps/web/src/routes/(app)/lists/[id]/+page.svelte, apps/web/src/routes/(app)/tasks/[id]/+page.svelte.
Criterio de aceptación: la detail view ya no muestra la eyebrow; una lista completada se ve con strikethrough en el título; una archivada muestra el pill muted.
5.13 Resistencia a contextos no-seguros (HTTP LAN)
Problema descubierto tras deploy de la preview mobile: el usuario abrió http://192.168.1.167:5173 en el móvil (origen no-seguro) y handleAdd lanzó TypeError: crypto.randomUUID is not a function. crypto.randomUUID solo está expuesto en HTTPS, localhost, 127.0.0.1 o file:// — fuera de eso queda undefined.
Cambios:
- Nuevo helper
apps/web/src/lib/utils/id.ts—generateId()usacrypto.randomUUID()cuando está disponible; cae encrypto.getRandomValues()+ ensamblado RFC 4122 §4.4 cuando no (la API de bytes aleatorios SÍ está en contextos inseguros). - Sustituidos todos los call sites:
stores/lists.ts,stores/tasks.ts,sync/queue.ts,sync/undoQueue.ts,lists/[id]/+page.svelte,tasks/[id]/+page.svelte. - Regla del repo (añadida a CLAUDE.md): nadie llama
crypto.randomUUID()directamente — siempregenerateId().
Tests:
src/lib/utils/id.test.ts— U-01 devuelve UUID v4; U-02 fallback cuandorandomUUIDesundefined; U-03 50 IDs únicos bajo fallback; U-04 prefiererandomUUIDcuando existe.tests/e2e/insecure-context.test.ts— INS-01:addInitScriptanularandomUUID,loginAs, añadir ítem, verificar que no crashea y el row aparece.
5.Z Verificación final — todos los tests verdes
just test-all→ 233 verdes, 2 skipped:- 34 pgTAP (sin cambios)
- 140 Vitest integration + 2 skipped presence (sin cambios)
- 15 Vitest unit (was 11, +4 generateId)
- 44 Playwright (was 43, +1 INS-01)
- Inline add-form UX (5.14) verificado manualmente en DevTools mobile + desktop tras el refactor.
- Validación visual en DevTools iPhone 13 (390×844) + Pixel 7 (412×915) + Desktop (1280×800) — pendiente (manual, post-deploy).
loginAs(USERS.borja)conpage.setViewportSize(MOBILE)activa el shell mobile automáticamente (verificado en M-01..M-04).- Seis commits incrementales (5.10.0 → 5.12 → 5.8 → 5.10 → 5.11) para facilitar revert o review por pasos.
Plan de ejecución (orden sugerido)
- 5.0 Tests primero — escribir los mobile tests aunque inicialmente fallen.
- 5.1 Shell responsive — sin esto, nada más es testable en mobile.
- 5.5 UndoQueue + UndoToast — dependencia de 5.4.
- 5.2 Masthead + padding — trivial, desbloquea los tests M-05.
- 5.3 / 5.4 / 5.6 — ajustes por pantalla.
- 5.7 — pulido de tasks/notes/search con el shell.
- 5.Z Verificación — correr toda la suite, snapshot visual, commit.
Criterio de aceptación
- En viewport ≤ 768px: sidebar oculta, bottom tab bar + top bar visibles, drawer abre con el switcher.
- Todas las pantallas tienen el par masthead (label uppercase + title big).
- Swipe-left en un item revela la zona roja; full-swipe borra con toast de undo.
- Los 211 tests funcionales siguen verdes. Los nuevos mobile tests (M-01..M-07) pasan.
- Validado manualmente en DevTools en 3 viewports.