From 4ce24a4145872cf0794bb72b9fde93937dad25e8 Mon Sep 17 00:00:00 2001 From: Oier Bravo Urtasun Date: Tue, 19 May 2026 02:29:17 +0200 Subject: [PATCH] feat(fase-18): CreateListModal replaces empty-create-then-rename flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `CreateListModal.svelte` is the new entry point for "New list" — opens on the masthead button click, prefills the input with `currentCollective.default_list_title` (Fase 18.3.1), shows an inline autocomplete dropdown via `fetchTitleSuggestions`, and renders a "Sugerencia: Compra #6" chip when (a) the typed value exactly matches a catalog-curated prefix case-insensitively and (b) `computeNextNumber` resolves a value (Fase 18.3.2). Submit is disabled while the trimmed value is empty or creation is in flight (Fase 18.3.3). On submit, if the catalog match still holds, the modal auto-suffixes "#N" before inserting and the lists page shows a transient toast confirming the final name (Fase 18.3.4). The prefill effect is gated by a `wasOpen` edge-tracker — a naive `$effect(() => { if (open) value = default })` re-runs whenever `$currentCollective` updates (e.g. a realtime UPDATE landing after the user typed something), which clobbered the typed value and froze the submit button as disabled. The edge guard limits the prefill to the false→true transition. `/lists/+page.svelte`: the masthead "New list" button now opens the modal instead of calling `createList(collective, '', user)` and goto-ing to the detail page for inline rename. The legacy flow was nice for quick creates but couldn't enforce a required title or surface the catalog. Existing per-list actions are unchanged. 15 new i18n strings (en + es) for the modal, the suggestion chip, the auto-numbered toast, and the manage-page subsection that ships in the next commit. Updated existing e2e specs: `lists.test.ts`'s `createList()` helper + the C-13 guest path now drive the modal instead of the legacy placeholder input; `session.test.ts` S-03 uses the modal for the fresh-list setup. New `tests/e2e/list-title-flow.test.ts` adds LTF-01 (default prefill end-to-end with a Supabase-driven setDefaultListTitle since the manage UI lands in the next commit) and LTF-03 (empty input keeps submit disabled, even with whitespace). Tests: 4 lists + 3 session + 2 new LTF specs all green (9/9). Co-Authored-By: Claude Opus 4.7 --- apps/web/messages/en.json | 21 +- apps/web/messages/es.json | 21 +- .../src/lib/components/CreateListModal.svelte | 283 ++++++++++++++++++ apps/web/src/routes/(app)/lists/+page.svelte | 53 +++- apps/web/tests/e2e/list-title-flow.test.ts | 116 +++++++ apps/web/tests/e2e/lists.test.ts | 47 +-- apps/web/tests/e2e/session.test.ts | 14 +- 7 files changed, 515 insertions(+), 40 deletions(-) create mode 100644 apps/web/src/lib/components/CreateListModal.svelte create mode 100644 apps/web/tests/e2e/list-title-flow.test.ts diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json index f9f11fb..a4516a1 100644 --- a/apps/web/messages/en.json +++ b/apps/web/messages/en.json @@ -342,5 +342,24 @@ "common_items_add_name_label": "Item name", "common_items_add_name_placeholder": "e.g. olive oil", "common_items_add_visibility_label": "Visibility", - "common_items_add_save": "Add" + "common_items_add_save": "Add", + "create_list_modal_title": "New shopping list", + "create_list_input_label": "List name", + "create_list_input_placeholder": "e.g. Weekly shop", + "create_list_submit": "Create", + "create_list_suggestion_chip": "Suggestion: {title}", + "create_list_auto_numbered_toast": "Created as “{title}”", + "manage_list_titles_section_title": "Suggested titles", + "manage_list_titles_blurb": "Pre-set a default title for new lists and curate suggestions admins want to surface.", + "manage_default_list_title_label": "Default new-list title", + "manage_default_list_title_placeholder": "e.g. Weekly shop", + "manage_default_list_title_clear": "Clear", + "manage_list_titles_empty": "No curated titles yet.", + "manage_list_titles_add_button": "Add title", + "manage_list_titles_add_modal_title": "Add suggested title", + "manage_list_titles_add_name_label": "Title", + "manage_list_titles_add_name_placeholder": "e.g. Compra", + "manage_list_titles_add_save": "Add", + "manage_list_titles_remove": "Remove", + "manage_list_titles_readonly": "Only admins can curate these titles." } diff --git a/apps/web/messages/es.json b/apps/web/messages/es.json index d279f27..1f71d3f 100644 --- a/apps/web/messages/es.json +++ b/apps/web/messages/es.json @@ -342,5 +342,24 @@ "common_items_add_name_label": "Nombre del item", "common_items_add_name_placeholder": "p. ej. aceite de oliva", "common_items_add_visibility_label": "Visibilidad", - "common_items_add_save": "Añadir" + "common_items_add_save": "Añadir", + "create_list_modal_title": "Nueva lista de la compra", + "create_list_input_label": "Nombre de la lista", + "create_list_input_placeholder": "p. ej. Compra semanal", + "create_list_submit": "Crear", + "create_list_suggestion_chip": "Sugerencia: {title}", + "create_list_auto_numbered_toast": "Creada como «{title}»", + "manage_list_titles_section_title": "Títulos sugeridos", + "manage_list_titles_blurb": "Preconfigura un título por defecto para las nuevas listas y mantén una lista de sugerencias.", + "manage_default_list_title_label": "Título por defecto", + "manage_default_list_title_placeholder": "p. ej. Compra semanal", + "manage_default_list_title_clear": "Borrar", + "manage_list_titles_empty": "No hay títulos sugeridos todavía.", + "manage_list_titles_add_button": "Añadir título", + "manage_list_titles_add_modal_title": "Añadir título sugerido", + "manage_list_titles_add_name_label": "Título", + "manage_list_titles_add_name_placeholder": "p. ej. Compra", + "manage_list_titles_add_save": "Añadir", + "manage_list_titles_remove": "Borrar", + "manage_list_titles_readonly": "Solo los admins pueden gestionar estos títulos." } diff --git a/apps/web/src/lib/components/CreateListModal.svelte b/apps/web/src/lib/components/CreateListModal.svelte new file mode 100644 index 0000000..adc86f9 --- /dev/null +++ b/apps/web/src/lib/components/CreateListModal.svelte @@ -0,0 +1,283 @@ + + +{#if open} + +{/if} diff --git a/apps/web/src/routes/(app)/lists/+page.svelte b/apps/web/src/routes/(app)/lists/+page.svelte index dab8f30..194edf8 100644 --- a/apps/web/src/routes/(app)/lists/+page.svelte +++ b/apps/web/src/routes/(app)/lists/+page.svelte @@ -19,10 +19,15 @@ import { ShoppingCart, Plus, MoreHorizontal, Trash2, Archive, RotateCcw } from 'lucide-svelte'; import * as m from '$lib/paraglide/messages'; import ScreenMasthead from '$lib/components/ScreenMasthead.svelte'; + import CreateListModal from '$lib/components/CreateListModal.svelte'; // ── State ────────────────────────────────────────────────────────────────── let creating = $state(false); + let createModalOpen = $state(false); + // Fase 18.3.4: when the modal auto-suffixes "#N", surface the final name in + // a transient toast so the user sees what was actually created. + let autoNumberedToast = $state(null); let showTrash = $state(false); let trashedLists = $state([]); @@ -55,15 +60,29 @@ } // ── Create ───────────────────────────────────────────────────────────────── - // Matches the /notes flow: click the masthead button → create with an empty - // name → navigate to /lists/[id] where the user renames inline. + // Fase 18: replaces the old "create with empty name + navigate to detail to + // rename inline" flow with an explicit modal. The modal prefills the input + // with `currentCollective.default_list_title`, suggests `#N` autocomplete + // for catalog-curated prefixes, and gates submit on non-empty trim. - async function handleCreate() { - if (!$currentCollective || !$currentUser || creating) return; - creating = true; - const created = await createList($currentCollective.id, '', $currentUser.id); - creating = false; - if (created) await goto(`/lists/${created.id}`); + function openCreateModal() { + // Open eagerly — the modal itself handles missing collective/user + // (the submit button is disabled until both are set). Letting the + // modal open on click avoids a UX trap where the button looks broken + // because the auth state hasn't propagated yet. + createModalOpen = true; + } + + function closeCreateModal() { + createModalOpen = false; + } + + async function handleCreated(id: string, finalName: string, wasAutoNumbered: boolean) { + if (wasAutoNumbered) { + autoNumberedToast = finalName; + setTimeout(() => (autoNumberedToast = null), 3000); + } + await goto(`/lists/${id}`); } // ── Actions ──────────────────────────────────────────────────────────────── @@ -132,8 +151,9 @@