From d14d6cd5ab70ef752e4066939228f276f55eb371 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Mon, 13 Apr 2026 19:09:33 +0200 Subject: [PATCH] =?UTF-8?q?feat(fase-5.8):=20/lists=20big-button=20create?= =?UTF-8?q?=20=E2=80=94=20match=20/notes=20pattern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- apps/web/messages/en.json | 2 +- apps/web/messages/es.json | 2 +- apps/web/src/routes/(app)/lists/+page.svelte | 62 ++++++------------- .../src/routes/(app)/lists/[id]/+page.svelte | 53 ++++++++++++++-- apps/web/tests/e2e/lists.test.ts | 21 +++++-- apps/web/tests/e2e/session.test.ts | 24 +++---- 6 files changed, 96 insertions(+), 68 deletions(-) diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index 1a563ea..a2ffdd4 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -75,7 +75,7 @@ "masthead_search_label": "Omni-Search", "masthead_search_title": "Find Anything.", "lists_title": "Shopping Lists", - "lists_new_list": "Create", + "lists_new_list": "New list", "lists_no_lists": "No lists yet", "lists_create_first": "Create your first shopping list", "lists_trash": "Trash", diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json index 6e5ed4c..51d79ec 100644 --- a/apps/web/messages/es.json +++ b/apps/web/messages/es.json @@ -75,7 +75,7 @@ "masthead_search_label": "Buscar", "masthead_search_title": "Encuéntralo todo.", "lists_title": "Listas de la compra", - "lists_new_list": "Crear", + "lists_new_list": "Nueva lista", "lists_no_lists": "Sin listas", "lists_create_first": "Crea tu primera lista de la compra", "lists_trash": "Papelera", diff --git a/apps/web/src/routes/(app)/lists/+page.svelte b/apps/web/src/routes/(app)/lists/+page.svelte index 0ab824a..dab8f30 100644 --- a/apps/web/src/routes/(app)/lists/+page.svelte +++ b/apps/web/src/routes/(app)/lists/+page.svelte @@ -22,9 +22,7 @@ // ── State ────────────────────────────────────────────────────────────────── - let newListName = $state(''); let creating = $state(false); - let nameInput: HTMLInputElement | undefined = $state(); let showTrash = $state(false); let trashedLists = $state([]); @@ -57,19 +55,15 @@ } // ── Create ───────────────────────────────────────────────────────────────── + // Matches the /notes flow: click the masthead button → create with an empty + // name → navigate to /lists/[id] where the user renames inline. async function handleCreate() { - const name = newListName.trim(); - if (!name || !$currentCollective || !$currentUser) return; + if (!$currentCollective || !$currentUser || creating) return; creating = true; - newListName = ''; - await createList($currentCollective.id, name, $currentUser.id); + const created = await createList($currentCollective.id, '', $currentUser.id); creating = false; - nameInput?.focus(); - } - - function handleKeydown(e: KeyboardEvent) { - if (e.key === 'Enter') handleCreate(); + if (created) await goto(`/lists/${created.id}`); } // ── Actions ──────────────────────────────────────────────────────────────── @@ -129,19 +123,27 @@ {#snippet actions()} + {/snippet} -
+
{#if showTrash}
@@ -389,32 +391,4 @@ {/if}
- -
-
- - -
- -
diff --git a/apps/web/src/routes/(app)/lists/[id]/+page.svelte b/apps/web/src/routes/(app)/lists/[id]/+page.svelte index 6cea251..7230fa6 100644 --- a/apps/web/src/routes/(app)/lists/[id]/+page.svelte +++ b/apps/web/src/routes/(app)/lists/[id]/+page.svelte @@ -13,6 +13,7 @@ checkItem, deleteItem, reorderItems, + renameList, fetchSuggestions } from '$lib/stores/lists'; import { @@ -46,6 +47,11 @@ let items = $state([]); let loading = $state(true); + // Inline rename of the list (replaces the old /lists sticky input flow). + let listName = $state(''); + let nameSaveTimer: ReturnType | null = null; + const NAME_SAVE_DEBOUNCE_MS = 500; + // Add-item form let newName = $state(''); let newQty = $state(null); @@ -103,6 +109,7 @@ } list = listRes.data as ShoppingList; + listName = list.name ?? ''; items = itemsRes; loading = false; @@ -133,6 +140,10 @@ onDestroy(async () => { await realtimeSub?.unsubscribe(); + if (nameSaveTimer) { + clearTimeout(nameSaveTimer); + await flushNameSave(); + } }); // ── Suggestions ──────────────────────────────────────────────────────────── @@ -154,6 +165,24 @@ nameInput?.focus(); } + // ── Inline rename ────────────────────────────────────────────────────────── + + function scheduleNameSave() { + if (nameSaveTimer) clearTimeout(nameSaveTimer); + nameSaveTimer = setTimeout(() => { + nameSaveTimer = null; + void flushNameSave(); + }, NAME_SAVE_DEBOUNCE_MS); + } + + async function flushNameSave() { + if (!list) return; + const next = listName.trim(); + if (next === (list.name ?? '')) return; + list = { ...list, name: next }; + await renameList(list.id, next); + } + // ── Add item ─────────────────────────────────────────────────────────────── async function handleAdd() { @@ -398,11 +427,9 @@
-

- {m.list_status_active()} -

-

- {list?.name} + +

+ {list?.name || m.list_name_placeholder()}

@@ -445,6 +472,22 @@
+ +
+

+ {m.list_status_active()} +

+ +
+ {#if uncheckedItems.length === 0 && checkedItems.length === 0}

{m.list_items_empty()}

diff --git a/apps/web/tests/e2e/lists.test.ts b/apps/web/tests/e2e/lists.test.ts index fda9a61..6baecf0 100644 --- a/apps/web/tests/e2e/lists.test.ts +++ b/apps/web/tests/e2e/lists.test.ts @@ -37,14 +37,25 @@ function cardActionsButton(page: Page, name: string) { async function gotoListsClean(page: Page) { await page.goto('/lists'); - await expect(page.getByPlaceholder(PLACEHOLDER_NEW_LIST)).toBeVisible({ timeout: 15_000 }); + // The masthead "New list" button is the anchor now (replaces the sticky input). + await expect( + page.getByRole('button', { name: /new list|nueva lista/i }) + ).toBeVisible({ timeout: 15_000 }); } async function createList(page: Page, name: string) { - const input = page.getByPlaceholder(PLACEHOLDER_NEW_LIST); - await input.fill(name); - await input.press('Enter'); - await expect(listHeading(page, name)).toBeVisible({ timeout: 5_000 }); + await page.getByRole('button', { name: /new list|nueva lista/i }).click(); + // Lands on /lists/[id]; the editable title input carries the placeholder. + await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 }); + const titleInput = page.getByPlaceholder(PLACEHOLDER_NEW_LIST); + await expect(titleInput).toBeVisible({ timeout: 10_000 }); + await titleInput.fill(name); + // Blur + wait for the 500 ms autosave debounce + buffer. + await titleInput.blur(); + await page.waitForTimeout(900); + // Navigate back to /lists to give callers a consistent starting state. + await page.goto('/lists'); + await expect(listHeading(page, name)).toBeVisible({ timeout: 10_000 }); } test.describe('Shopping lists — member (Borja)', () => { diff --git a/apps/web/tests/e2e/session.test.ts b/apps/web/tests/e2e/session.test.ts index 5c8b1ed..90f802d 100644 --- a/apps/web/tests/e2e/session.test.ts +++ b/apps/web/tests/e2e/session.test.ts @@ -59,20 +59,20 @@ test.describe('Shopping session — full-screen mode', () => { page }) => { // Need a fresh list so completing it doesn't affect shared seed state. - // Create via the lists overview. + // Use the new big-button create flow: masthead "New list" → lands on + // /lists/[id] → fill inline title → autosave → we're already on the + // detail view, no need to navigate back. await page.goto('/lists'); - const listInput = page.getByPlaceholder(/weekly shop|compra semanal/i); - await expect(listInput).toBeVisible({ timeout: 15_000 }); + await page + .getByRole('button', { name: /new list|nueva lista/i }) + .click(); + await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 10_000 }); const listName = `S-03-list-${Date.now()}`; - await listInput.fill(listName); - await listInput.press('Enter'); - // Wait for the new list's heading so we can click into it - const heading = page.getByRole('heading', { name: new RegExp(`^${listName}$`) }); - await expect(heading).toBeVisible({ timeout: 5_000 }); - - // Navigate via the heading (linked to /lists/[id]) - await heading.click(); - await expect(page).toHaveURL(/\/lists\/[0-9a-f-]+$/, { timeout: 5_000 }); + const titleInput = page.getByPlaceholder(/weekly shop|compra semanal/i); + await expect(titleInput).toBeVisible({ timeout: 10_000 }); + await titleInput.fill(listName); + await titleInput.blur(); + await page.waitForTimeout(900); // autosave debounce // Open session mode await page.getByTestId('start-session').click();