f3e8234b7082d28992b2c1b52905dfc96d02debe
12
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f3e8234b70 |
feat(fase-5.11): selection mode + bulk actions (delete / move / mark-checked)
Long-press on mobile or the new header toggle on desktop puts
/lists/[id] into multi-select mode. Rows render a checkbox on the left,
the stepper + drag handle go away, and swipes + double-tap are disabled.
Chrome during selection mode
Mobile: header turns into a dark slate-900 action bar with "N selected"
+ Cancel on the left, and a bottom action bar (Delete / Move / Check)
appears above the BottomTabBar. The sticky add-item form is hidden.
Desktop: same dark header with Cancel + inline action icons on the right.
Bulk actions
Delete — one scheduleUndoable with a batch restore (re-inserts all
snapshots) / batch commit (bulkDeleteItems). Single undo toast.
Move — opens a bottom sheet / dialog with the collective's other active
lists + a "Create new list" row that creates an empty list and moves
into it in a single gesture.
Mark checked — flips only the selected unchecked items
(bulkCheckItems with is_checked=false filter), no toggle.
Store helpers (apps/web/src/lib/stores/lists.ts)
bulkDeleteItems(ids) → DELETE .in('id', ids)
bulkMoveItems(ids, newListId) → UPDATE list_id .in('id', ids)
bulkCheckItems(ids, userId) → UPDATE is_checked=true where !is_checked
Entry / exit
Long-press 500 ms on a row enters with that row pre-selected. Movement
cancels the long-press intent so it never fights the swipe.
Single-tap toggles selection while active (instead of invoking the
no-op short-tap / dbltap-overlay path).
Cancel button exits + clears selection.
Tests
apps/web/tests/e2e/selection.test.ts (3 desktop tests)
SEL-01 toggle enters, row click toggles, Cancel exits
SEL-02 bulk delete → row gone + undo toast visible
SEL-03 mark checked → row gains strikethrough
Mobile long-press tests deferred until WebKit install lands (same
reason as the swipe-toggle M-series).
i18n additions
list_select, list_cancel_selection, list_selection_count({n}),
list_bulk_delete/move/mark_checked, list_undo_bulk_delete({n}),
list_move_title, list_move_create_new (en/es).
Verification
just test-e2e → 43 passed + 2 skipped (was 40 + 2).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
1cb408acc2 |
feat(fase-5.10): /lists/[id] row redesign — buttons + swipe-toggle + overlay
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>
|
||
|
|
d14d6cd5ab |
feat(fase-5.8): /lists big-button create — match /notes pattern
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>
|
||
|
|
d6cea5146f |
feat(fase-5.10.0): drop shopping_items.unit column
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>
|
||
|
|
2174d2c2c0 |
feat(fase-5): mobile UX shell + masthead + swipe-delete undo (224 tests green)
5.0 Tests primero
Playwright: mobile-shell.test.ts (4 — sidebar hidden on mobile, bottom tab
bar visible with aria-current, hamburger opens drawer, desktop shows
sidebar), mobile-masthead.test.ts (4 — every top-level route pairs
masthead-label + masthead-title), mobile-swipe-delete.test.ts (M-06/M-07
describe.skip — Chromium touch emulation flaky; needs `playwright install
webkit`)
Vitest: undoQueue.test.ts (5 — schedule+commit, undo+restore, TTL expiry,
double-undo no-op, custom ttlMs)
5.1 Responsive shell
DesktopSidebar.svelte (hidden md:flex)
MobileTopBar.svelte (md:hidden sticky backdrop-blur-xl)
BottomTabBar.svelte (md:hidden glassmorphism, safe-area-inset-bottom)
MobileDrawer.svelte (backdrop + collective switcher + settings + logout)
(app)/+layout.svelte — CRITICAL FIX: flex flex-col md:flex-row. Was
horizontal-only, which pushed masthead off-screen on mobile. UndoToast
mounted globally here.
5.2 Masthead + responsive padding
ScreenMasthead.svelte — label (13px uppercase) + title (26px/32px).
Applied to /lists, /tasks, /notes, /search. px-8 → px-4 md:px-6 everywhere.
messages/{en,es}.json: masthead_{lists,tasks,notes,search}_label +
masthead_search_title, undo, undo_deleted_item, list_item_delete_aria.
5.4 Red-zone swipe-delete on /lists/[id]
SWIPE_MAX=96 (red zone rest width), SWIPE_THRESHOLD=48 (latch-open),
SWIPE_COMMIT_THRESHOLD=200 (full-swipe commit). Red zone button exposes
aria-label="Delete item"/"Eliminar producto". handleDelete uses
scheduleUndoable — optimistic local removal, 4-s TTL, commit to Supabase
on timeout or restore from snapshot on Undo.
5.5 UndoQueue + UndoToast
src/lib/sync/undoQueue.ts — writable store + Map-backed actions with TTL.
__flushUndoQueueForTests + __pendingUndoCount for Vitest.
src/lib/components/UndoToast.svelte — renders latest undoable, offset
above bottom tab bar (bottom: calc(env(safe-area-inset-bottom) + 4.5rem)),
Undo button.
5.6 Scrollable frequency chips
ItemSuggestions.svelte — mobile: flex overflow-x-auto with
[scrollbar-width:none] [&::-webkit-scrollbar]:hidden; desktop: wraps.
Chips shrink-0 so they don't compress.
5.7 Shell polish on tasks/notes/search
Search group headers now carry "{n} MATCH / MATCHES" badges.
Grid in /lists goes 2-col → 3-col at md+.
Sticky create input offset for the bottom tab bar on mobile.
5.Z Verification
just test-all → 224 verdes, 4 skipped
34 pgTAP (unchanged)
140 Vitest integration + 2 skipped presence (unchanged)
11 Vitest unit (was 6, +5 undoQueue)
39 Playwright + 2 skipped swipe-touch (was 31, +8 new mobile tests)
Deferred (explicitly):
- Presence avatars + item counts on list cards (needs per-list count RPC)
- Integrate undoQueue in task delete + note trash (trivial when needed)
- WebKit install to unskip M-06/M-07 swipe gesture tests
- max-w-2xl reading-width constraint on notes/search
- Manual visual QA in DevTools iPhone/Pixel/Desktop viewports
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
2806e06db2 |
feat(fase-4): global search + RLS isolation audit (211 tests green)
4.0 Tests primero
Vitest: search.test.ts (10 — S-01..S-04), rls-audit.test.ts (42 —
3 intruders × 14 cross-collective ops proving zero leakage)
pgTAP: 007_search_tsvector.sql (9 — columns, GIN indexes, function
signature, generated-vector invariant)
Playwright: search.test.ts (2 — SR-01 happy-path, SR-02 filter toggle)
4.1 Búsqueda global
Migration 010_search_vectors.sql: STORED GENERATED tsvector columns
on shopping_items/tasks/notes + GIN indexes + search_result_type
enum + search_in_collective() SECURITY INVOKER function (RLS-aware,
trash-excluded per RN-08) + GRANT EXECUTE to anon/authenticated.
Uses `simple` config (no stemmer — bilingual en/es).
Store apps/web/src/lib/stores/search.ts — RPC wrapper
/search: debounced input (300ms, ≥2 chars), type filter chips with
multi-toggle, results grouped per type with data-testid hooks for
Playwright, deep-link per type (shopping_item → /lists/[id], task →
/tasks/[id], note → /notes/[id])
4.4 Security
rls-audit.test.ts replaces the curl-based manual audit with a
parametrised matrix — zero cross-collective reads/writes under
three different attacker roles
Realtime flake hardening
Add 250ms settle after SUBSCRIBED in realtime-helpers.ts — the Phoenix
socket reports SUBSCRIBED slightly before the backend finishes
propagating the filter to the WAL subscription, so mutations fired
immediately after subscribe could miss the filter under load.
4.Z Verification
just test-all → 211 verdes, 2 skipped
34 pgTAP (was 25, +9 search)
140 Vitest integration (was 88, +10 search +42 rls-audit; 2 skipped)
6 Vitest unit (unchanged)
31 Playwright (was 29, +2 search)
Deferred (prod-deploy scope):
PWA install/push (needs real iOS/Android hardware)
Kong rate-limit plugin config
JWT_SECRET / ANON_KEY rotation
Lighthouse PWA ≥ 90 manual validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
104eeba02e |
feat(fase-3): Tareas + Notas — TDD complete (148 tests green)
3.0 Tests primero
pgTAP: 005_tasks_rls.sql (5 — completion CHECK), 006_notes_trash_purge.sql (4 — purge SQL inline + pin/archive CHECK)
Vitest RLS: rls-tasks.test.ts (14), rls-notes.test.ts (15)
Playwright: tasks.test.ts (3), notes.test.ts (4)
3.1 Tareas
Migration 008_tasks.sql: task_lists + tasks + task_list_collective_id() helper
CHECK constraint: is_completed ⇔ completed_by ⇔ completed_at
RLS by role + cross-collective isolation
Store apps/web/src/lib/stores/tasks.ts (optimistic CRUD + reorder)
/tasks overview (grid + sticky create input + 5s polling)
/tasks/[id] detail with svelte-dnd-action reorder, flip animation,
inline edit (dblclick), "Completed by …" attribution via lazy-loaded
collective_members
3.2 Notas
Migration 009_notes.sql: notes + note_color enum (8) + pin/archive CHECK
+ 7d trash window in RLS + fn_notes_touch trigger + pg_cron purge job
Store apps/web/src/lib/stores/notes.ts (CRUD + pin/archive/trash/duplicate)
/notes board (pinned section testid notes-pinned + 30s polling)
/notes/[id] editor (title + textarea, 500ms debounced autosave,
color picker, pin/archive/duplicate/trash actions)
/notes/archive (restore + send to trash)
/notes/trash (restore + permanent delete)
3.Z Verification
just test-all → 148 verdes, 2 skipped:
25 pgTAP (was 16, +9)
88 Vitest integration (was 59, +29; 2 presence skipped)
6 Vitest unit (unchanged)
29 Playwright (was 22, +3 tasks +4 notes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
e7a961a66d |
feat(fase-2b): Realtime sync + offline queue + Modo Compra
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>
|
||
|
|
3af1276c15 |
feat: Fase 2a — shopping lists CRUD
DB: shopping_lists + shopping_items tables with full RLS, pg_cron jobs (archive completed lists after 7 days, purge trash after 7 days). item_frequency table with SECURITY DEFINER trigger that normalises and upserts on every shopping_items INSERT. Frontend: /lists overview (featured card + 2-col grid + trash toggle), /lists/[id] detail (qty stepper, inline edit, swipe-to-reveal-delete, drag & drop reorder via svelte-dnd-action, ItemSuggestions chips). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
ce1bcfb7a6 |
feat(design): apply Monolith Editorial design system to all Fase 1 screens
- 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> |
||
|
|
7d91705fc2 |
feat(fase-1): auth, collective management, and DB migrations
- SQL migrations: users, collectives, collective_members, collective_invitations,
full RLS policies, is_active_member/is_member/is_admin helpers,
accept_invitation SECURITY DEFINER function, admin auto-promote trigger,
avatars storage bucket
- Auth refactor: replace keycloak-js with Supabase OAuth (GoTrue OIDC proxy,
Option B). signInWithOAuth({ provider: 'keycloak' }) + PKCE flow
- GoTrue config: GOTRUE_EXTERNAL_KEYCLOAK_URL + GOTRUE_EXTERNAL_KEYCLOAK_REDIRECT_URI
added to docker-compose.dev.yml; Keycloak realm updated with GoTrue callback URI
- New routes: /auth/callback, /invitation/[token], /(app)/collective/manage
- Functional onboarding: create collective or join via invite link
- Full settings page: display name (debounced), avatar (initials/emoji/upload),
language switcher, Keycloak account console link
- Components: Avatar.svelte (initials/emoji/upload with fallback),
ImageCropper.svelte (cropperjs, 1:1 crop, lazy-loaded)
- i18n: added all Fase 1 message keys (en + es); renamed 'delete' → 'action_delete'
(JS reserved word); fixed plugin-m-function-matcher major-version URL format
- Database types: manually seeded packages/types/src/database.ts from migrations
- .env.development: committed public dev defaults for SvelteKit type checking
- CLAUDE.md: documented /etc/hosts requirement for keycloak hostname
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
||
|
|
973f9e413c |
Fase 0: scaffold monorepo, SvelteKit skeleton, and dev infrastructure
- Turborepo + pnpm workspaces with apps/web and packages/types - SvelteKit app: Tailwind, Paraglide i18n (en/es), keycloak-js, Supabase client, auth/collective stores, PWA service worker skeleton, all route placeholders - packages/types: domain types (User, Collective, ShoppingList, etc.) and database.ts placeholder for generated types - Justfile with dev, db-*, kc-*, build, backup, restore, deploy recipes - infra/docker-compose.dev.yml: Postgres 15, GoTrue, PostgREST, Realtime v2.83, Storage, Kong (port 8001), Studio, Keycloak 24 - infra/docker-compose.prod.yml, kong.yml, db-init scripts, backup/deploy scripts - keycloak/realm-export.json with colectivo realm and 5 dev test users - supabase/config.toml and seed.sql with sample collective and items - GitHub Actions: ci.yml (lint+typecheck+build) and deploy.yml (GHCR + SSH) - .env.example documenting all required variables - Fixed docker-compose issues: Studio image tag, Kong port conflict (8001), internal role passwords init script, Realtime METRICS_JWT_SECRET/APP_NAME Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> |