Replaces the checkbox/hover-delete model with an explicit gesture+overlay
flow on /lists/[id].
Row anatomy (new)
[drag-handle] [name as button] [qty stepper − N +]
- No checkbox in normal mode.
- Checked items render with strikethrough + muted text.
- Drag handle always visible on mobile; revealed on hover on desktop
(md:opacity-0 md:group-hover:opacity-100). The handle owns the drag
(pointerdown → dragEnabled=true), so row swipes + taps keep working.
Swipe gestures (symmetric toggle, no delete)
unchecked + swipe RIGHT → mark checked (green success reveal)
checked + swipe LEFT → mark unchecked (neutral reveal)
opposite direction for each state is a no-op (snaps back).
SWIPE_COMMIT_THRESHOLD=120 (was 200 for delete); REVEAL_WIDTH=96.
Symmetry means undo is the inverse swipe — no undoQueue on toggles.
Double-tap overlay (name + delete + confirm)
Short tap: no-op. Two taps within DOUBLE_TAP_WINDOW_MS (300 ms) open a
full-screen dialog (bottom sheet on mobile) with:
- X close button (top-right)
- name input
- Delete button (red) → scheduleUndoable → UndoToast
- Confirm button (primary) → updateItem({ name })
Click outside / Escape / X all dismiss without saving.
Tests
items.test.ts
D-03 rewritten: double-tap opens overlay, value matches, X closes.
D-04 rewritten: overlay Delete removes the row; toast locator scoped
out of itemRow so it doesn't shadow the assertion.
realtime.test.ts
R-E-02 updated: Ana renames via overlay → Borja sees new name over
Realtime UPDATE. (The check/uncheck path is now gestural only and
chromium touch emulation is too flaky to drive it in E2E; Vitest
integration R-02 still covers the UPDATE-row-payload invariant.)
mobile-swipe-delete.test.ts stays describe.skip — will be renamed and
rewritten for the new semantics once WebKit install lands in CI.
i18n
list_qty_decrease, list_qty_increase, list_reorder_handle,
list_edit_item, list_confirm, list_close — en/es.
Verification
just test-e2e → 40 passed, 2 skipped (same as pre-change).
Type-check clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove the sticky bottom create input from /lists. The masthead now hosts
a primary "New list" button next to the Trash icon, styled identically to
the "New note" button on /notes. Click → createList with an empty name →
goto /lists/[id], where a prominent editable title input (above To buy /
Checked) autosaves on blur / input with a 500 ms debounce.
Changes
apps/web/src/routes/(app)/lists/+page.svelte
- remove `newListName`, `nameInput`, `handleKeydown`
- remove the absolute-positioned sticky bottom block
- handleCreate now creates with name = '' and navigates to the detail
- actions snippet gains the primary New list button; Trash button
demoted to ghost icon
apps/web/src/routes/(app)/lists/[id]/+page.svelte
- new listName / scheduleNameSave / flushNameSave state (mirrors the
/notes editor pattern)
- big editable title <input> at top of content, autosaves on blur
via renameList
- contextual header h1 trimmed to a secondary caption-size preview
on mobile (so the user still sees the list name at the top)
- flushNameSave on component destroy so unsaved renames commit
apps/web/messages/{en,es}.json: lists_new_list = "New list"/"Nueva lista"
Tests
apps/web/tests/e2e/lists.test.ts: createList helper now clicks the
button, fills the title input, blurs, waits for autosave, and goes
back to /lists to present the same "list-is-on-the-overview" state
that C-11 / C-12 expect. C-07 picks up the reload check.
apps/web/tests/e2e/session.test.ts: S-03 mirrors the new flow for its
fresh-list setup.
Verification
just test-e2e → 40 passed, 2 skipped (swipe-delete .skip remains).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The unit field had no edit path under the redesign (only name + qty
remain in the row; the double-tap overlay is name-only). Users can encode
the unit inside the name when it matters — e.g. "Milk 1L", "Eggs 1 doz".
Changes
supabase/migrations/011_drop_shopping_items_unit.sql — DROP COLUMN
supabase/seed.sql — unit values folded into names where relevant
supabase/tests/002_item_frequency_trigger.sql — remove unit from INSERT
packages/types — remove unit from ShoppingItem Row/Insert/Update
apps/web/src/lib/stores/lists.ts — addItem/updateItem signatures
apps/web/src/routes/(app)/lists/[id]/+page.svelte — purge newUnit /
editUnit state + inputs + display
apps/web/src/routes/(app)/lists/[id]/session/+page.svelte — simplify
quantity-only render
apps/web/messages/{en,es}.json — remove list_unit_label
Verification
just test-db → 34 pgTAP green
just test-integration → 140 + 2 skipped
just test-unit → 11 green
just test-e2e → 39 + 2 skipped
The generated search tsvector on shopping_items.search already indexes
only `name`, so full-text search coverage is unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fase 2b closes the critical-path product differentiator. Two sessions on the
same list see each other's changes in real time, mutations survive network
drops and sync when reconnected, and a dedicated full-screen shopping view
optimises the in-store experience.
2b.1 Realtime
- `$lib/stores/realtimeSync.ts` wraps `postgres_changes` with an `applyItemEvent`
reducer (INSERT/UPDATE/DELETE → next items[]) and a `subscribeToList` helper
that awaits the SUBSCRIBED handshake before returning
- `/lists/[id]` subscribes in onMount, unsubscribes in onDestroy. Uses a
`pendingTempIds` set to deduplicate own-mutation echoes against optimistic
rows (matches by name+created_by+sort_order)
- Client-generated UUIDs for new inserts make the path idempotent: if the
server response is lost and we retry, the second POST hits PK-conflict
which the queue treats as success
- CHECKED section div now carries role="listitem" so Playwright locators
follow the item across sections
2b.2 Offline queue
- `$lib/sync/queue.ts` — SyncQueue class backed by IndexedDB (idb), FIFO flush,
MAX_ATTEMPTS=5 retry budget, PK-conflict-as-success short-circuit
- `$lib/sync/index.ts` — app-wide singleton, window.online listener flushes
automatically, hydrateSyncState() at page load restores pendingOpsCount
- `$lib/stores/syncStatus.ts` — derived store (offline | syncing | synced)
tracking navigator.onLine + queue depth
- SyncBanner component renders the offline/syncing indicator in the detail
and session views
- handleAdd enqueues on failure instead of reverting, so an offline mutation
keeps its optimistic row and syncs on reconnect
2b.3 Modo Compra
- `/lists/[id]/session/+page.svelte` — full-screen overlay (fixed inset-0
z-50) that covers the sidebar while keeping the normal auth routing.
56×56 toggles, flip animation shuffling items between TO BUY / CHECKED,
Finish Shopping confirmation modal → completeList → goto('/lists')
- Link from `/lists/[id]` header (data-testid=start-session) with the
new `list_start_session` message
Testing infra
- apps/web gets its own Vitest config (jsdom + fake-indexeddb/auto) and a
new `pnpm test:unit` script. `just test-all` now chains pgTAP → integration
→ unit → e2e so a single command is the gate.
- packages/test-utils/tests/sync-queue.test.ts (placeholder scaffold) removed —
replaced by `apps/web/src/lib/sync/queue.test.ts` co-located with the module
Totals: 103 tests green
16 pgTAP
59 Vitest integration (in packages/test-utils)
6 Vitest unit (in apps/web — the SyncQueue)
22 Playwright E2E (5 auth + 4 lists + 6 items + 2 realtime + 2 offline + 3 session)
2 skipped (realtime-presence — upstream bug, unchanged)
Documented in plan/fase-2b and CLAUDE.md.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove all 1px border separators (sidebar border-r, page header border-b,
section border-t, member list divide-y, invite card border). Separation is
now achieved via background color shifts and whitespace per the "No-Line" rule.
- Fix sidebar: bg-surface-raised vs bg-background creates tonal separation.
- Correct dark background to #020617 (slate-950) per spec.
- Add surface-container-low, text-primary/secondary/muted, destructive tokens.
- Fix CSS variable color space: all tokens are RGB triplets; switch Tailwind
config and app.css from hsl() to rgb() to prevent yellow/warm color bleed.
- Replace emoji nav icons with Lucide icons at 16px / 1.5 stroke-width.
- Apply Masthead typography (uppercase Label-MD + Title-LG) to all page headers
and section labels across lists, tasks, notes, search, settings, manage.
- Destructive action (sign out) uses bg-destructive per spec.
- Add `just serve` command: builds and runs the prod Node server at :3000
against the dev Docker stack for production build testing.
- Add nav_collective i18n key (en + es).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>