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();