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 />
<!-- 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
NAME_SAVE_DEBOUNCE_MS of inactivity. -->
<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="text-sm text-text-muted">{m.list_items_empty_hint()}</p>
</div>
{@render addItemForm()}
{:else}
<!-- Unchecked items (drag & droppable via left handle) -->
{#if uncheckedItems.length > 0}
@@ -823,6 +824,10 @@
</div>
{/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 -->
{#if checkedItems.length > 0}
<div class="px-4 md:px-8 mt-4">
@@ -945,29 +950,22 @@
</div>
{/if}
<!-- Sticky bottom: suggestions + add form (hidden during selection). -->
<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}
/>
</div>
<!-- Add input -->
<div class="flex items-center gap-2 px-4 pb-4 pt-1">
<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)]">
<!-- Inline add-item form: input row on top, frequency chips below.
Hidden in selection mode per UX decision 6. -->
{#snippet addItemForm()}
{#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
bind:this={nameInput}
bind:value={newName}
onkeydown={handleAddKeydown}
type="text"
placeholder={m.list_item_placeholder()}
class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-muted
outline-none"
class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-muted outline-none"
/>
<input
bind:value={newQty}
@@ -975,23 +973,27 @@
min="1"
placeholder={m.list_qty_label()}
onkeydown={handleAddKeydown}
class="w-12 bg-transparent text-sm text-text-secondary placeholder:text-text-muted
outline-none text-right"
class="w-12 bg-transparent text-sm text-text-secondary placeholder:text-text-muted outline-none text-right"
/>
</div>
<button
onclick={handleAdd}
disabled={!newName.trim()}
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"
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"
aria-label={m.add()}
>
<Plus size={18} strokeWidth={2} />
</button>
</div>
<!-- Suggestions — second line, right below the input. -->
<ItemSuggestions
{suggestions}
activePrefix={newName}
onselect={selectSuggestion}
/>
</div>
</div>
{/if}
{/snippet}
<!-- Double-tap edit overlay -->
{#if overlayItem}

View File

@@ -254,6 +254,7 @@
<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>
</div>
{@render addTaskForm()}
{:else}
<!-- Pending (drag-and-drop reorderable) -->
{#if pendingTasks.length > 0}
@@ -312,6 +313,9 @@
</section>
{/if}
<!-- Inline add-task form between pending and completed (option `a`). -->
{@render addTaskForm()}
<!-- Completed (sunk to bottom; not reorderable) -->
{#if completedTasks.length > 0}
<section class="mt-6">
@@ -358,8 +362,11 @@
{/if}
</div>
<!-- Sticky add-task input -->
<div class="border-t border-black/5 bg-surface-raised px-4 py-3 md:px-6 md:py-4 dark:border-white/5">
</div>
<!-- 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 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
@@ -381,4 +388,4 @@
</button>
</div>
</div>
</div>
{/snippet}