feat(lists+tasks): inline add-form at end of list, suggestions below input

UX change per direct user request:

  - Add-item / add-task form no longer sticks to the bottom of the viewport.
    It's now the last element of the unchecked/pending section, sitting
    just above the Checked/Completed section (option `a` of the review).
  - On /lists/[id] the frequency suggestion chips moved from ABOVE the
    input to BELOW it, forming a two-line block (input row on top, chips
    on the second line). Chips keep their responsive behavior — horizontal
    scroll on mobile, wrap on desktop (ItemSuggestions already handles
    that).
  - Empty-state message still shows when the list has no items; the form
    renders below it so you can add the first item without scrolling.
  - The form disappears while selection mode is active on /lists/[id]
    (selection chrome takes over the bottom).

Implementation
  /lists/[id]/+page.svelte
    - Removed the `absolute bottom-0` sticky form block.
    - New `addItemForm` Svelte snippet rendered at the end of unchecked
      items AND in the empty-state branch, gated by `!selectionMode`.
    - ItemSuggestions moved inside the snippet, below the input.
    - pb-32 scroll padding reduced to pb-16/md:pb-6 (no more sticky
      overlapping the last row).
  /tasks/[id]/+page.svelte
    - Same treatment with an `addTaskForm` snippet (no suggestions).

Tests
  All existing D- / T-UI- / selection tests pass unchanged: the locators
  are placeholder-based and the form is still findable regardless of
  placement. `data-testid="add-item-form"` / `"add-task-form"` added as
  future-proof hooks.

Verification
  just test-all → exit 0, 233 green, 2 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-14 03:05:07 +02:00
parent 53de447aa5
commit 3a11914fc5
2 changed files with 79 additions and 70 deletions

View File

@@ -670,7 +670,7 @@
<SyncBanner /> <SyncBanner />
<!-- Items --> <!-- Items -->
<div class="flex-1 overflow-y-auto pb-32"> <div class="flex-1 overflow-y-auto pb-16 md:pb-6">
<!-- Inline rename title — matches /notes editor pattern. Autosaves after <!-- Inline rename title — matches /notes editor pattern. Autosaves after
NAME_SAVE_DEBOUNCE_MS of inactivity. --> NAME_SAVE_DEBOUNCE_MS of inactivity. -->
<div class="px-4 pt-4 pb-2 md:px-8 md:pt-6 md:pb-4"> <div class="px-4 pt-4 pb-2 md:px-8 md:pt-6 md:pb-4">
@@ -692,6 +692,7 @@
<p class="mb-1 text-sm font-medium text-text-primary">{m.list_items_empty()}</p> <p class="mb-1 text-sm font-medium text-text-primary">{m.list_items_empty()}</p>
<p class="text-sm text-text-muted">{m.list_items_empty_hint()}</p> <p class="text-sm text-text-muted">{m.list_items_empty_hint()}</p>
</div> </div>
{@render addItemForm()}
{:else} {:else}
<!-- Unchecked items (drag & droppable via left handle) --> <!-- Unchecked items (drag & droppable via left handle) -->
{#if uncheckedItems.length > 0} {#if uncheckedItems.length > 0}
@@ -823,6 +824,10 @@
</div> </div>
{/if} {/if}
<!-- Inline add-item form. Always rendered between unchecked and checked
sections (option `a` from the UX review); hidden in selection mode. -->
{@render addItemForm()}
<!-- Checked items section — separate zone, symmetric swipe-to-uncheck --> <!-- Checked items section — separate zone, symmetric swipe-to-uncheck -->
{#if checkedItems.length > 0} {#if checkedItems.length > 0}
<div class="px-4 md:px-8 mt-4"> <div class="px-4 md:px-8 mt-4">
@@ -945,29 +950,22 @@
</div> </div>
{/if} {/if}
<!-- Sticky bottom: suggestions + add form (hidden during selection). --> </div>
<div
class="absolute inset-x-0 bottom-0 bg-background/80 backdrop-blur-[12px] mb-16 md:left-56 md:right-0 md:mb-0 {selectionMode ? 'hidden' : ''}"
>
<!-- Suggestions -->
<ItemSuggestions
{suggestions}
activePrefix={newName}
onselect={selectSuggestion}
/>
<!-- Add input --> <!-- Inline add-item form: input row on top, frequency chips below.
<div class="flex items-center gap-2 px-4 pb-4 pt-1"> Hidden in selection mode per UX decision 6. -->
<div class="flex flex-1 items-center gap-2 rounded-lg bg-surface px-4 py-2.5 {#snippet addItemForm()}
shadow-[0px_2px_8px_rgba(15,23,42,0.06)]"> {#if !selectionMode}
<div class="px-4 pt-2 pb-6 md:px-8" data-testid="add-item-form">
<div class="flex items-center gap-2">
<div class="flex flex-1 items-center gap-2 rounded-lg bg-surface px-4 py-2.5 shadow-[0px_2px_8px_rgba(15,23,42,0.06)]">
<input <input
bind:this={nameInput} bind:this={nameInput}
bind:value={newName} bind:value={newName}
onkeydown={handleAddKeydown} onkeydown={handleAddKeydown}
type="text" type="text"
placeholder={m.list_item_placeholder()} placeholder={m.list_item_placeholder()}
class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-muted class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-muted outline-none"
outline-none"
/> />
<input <input
bind:value={newQty} bind:value={newQty}
@@ -975,23 +973,27 @@
min="1" min="1"
placeholder={m.list_qty_label()} placeholder={m.list_qty_label()}
onkeydown={handleAddKeydown} onkeydown={handleAddKeydown}
class="w-12 bg-transparent text-sm text-text-secondary placeholder:text-text-muted class="w-12 bg-transparent text-sm text-text-secondary placeholder:text-text-muted outline-none text-right"
outline-none text-right"
/> />
</div> </div>
<button <button
onclick={handleAdd} onclick={handleAdd}
disabled={!newName.trim()} disabled={!newName.trim()}
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-slate-900 text-white hover:opacity-90 disabled:opacity-40 dark:bg-white dark:text-slate-900 transition-opacity"
bg-slate-900 text-white hover:opacity-90 disabled:opacity-40
dark:bg-white dark:text-slate-900 transition-opacity"
aria-label={m.add()} aria-label={m.add()}
> >
<Plus size={18} strokeWidth={2} /> <Plus size={18} strokeWidth={2} />
</button> </button>
</div> </div>
<!-- Suggestions — second line, right below the input. -->
<ItemSuggestions
{suggestions}
activePrefix={newName}
onselect={selectSuggestion}
/>
</div> </div>
</div> {/if}
{/snippet}
<!-- Double-tap edit overlay --> <!-- Double-tap edit overlay -->
{#if overlayItem} {#if overlayItem}

View File

@@ -254,6 +254,7 @@
<p class="text-sm font-medium text-text-primary">{m.tasks_empty()}</p> <p class="text-sm font-medium text-text-primary">{m.tasks_empty()}</p>
<p class="mt-1 text-sm text-text-secondary">{m.tasks_empty_hint()}</p> <p class="mt-1 text-sm text-text-secondary">{m.tasks_empty_hint()}</p>
</div> </div>
{@render addTaskForm()}
{:else} {:else}
<!-- Pending (drag-and-drop reorderable) --> <!-- Pending (drag-and-drop reorderable) -->
{#if pendingTasks.length > 0} {#if pendingTasks.length > 0}
@@ -312,6 +313,9 @@
</section> </section>
{/if} {/if}
<!-- Inline add-task form between pending and completed (option `a`). -->
{@render addTaskForm()}
<!-- Completed (sunk to bottom; not reorderable) --> <!-- Completed (sunk to bottom; not reorderable) -->
{#if completedTasks.length > 0} {#if completedTasks.length > 0}
<section class="mt-6"> <section class="mt-6">
@@ -358,8 +362,11 @@
{/if} {/if}
</div> </div>
<!-- Sticky add-task input --> </div>
<div class="border-t border-black/5 bg-surface-raised px-4 py-3 md:px-6 md:py-4 dark:border-white/5">
<!-- Inline add-task form: input row on top, no suggestions for tasks. -->
{#snippet addTaskForm()}
<div class="mt-3 pb-6" data-testid="add-task-form">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="flex flex-1 items-center gap-2 rounded-lg bg-surface px-3 py-2 shadow-[0px_2px_8px_rgba(15,23,42,0.06)]"> <div class="flex flex-1 items-center gap-2 rounded-lg bg-surface px-3 py-2 shadow-[0px_2px_8px_rgba(15,23,42,0.06)]">
<input <input
@@ -381,4 +388,4 @@
</button> </button>
</div> </div>
</div> </div>
</div> {/snippet}