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:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user