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 />
|
<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,54 +950,51 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 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)]">
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
bind:value={newQty}
|
|
||||||
type="number"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</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"
|
|
||||||
aria-label={m.add()}
|
|
||||||
>
|
|
||||||
<Plus size={18} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 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"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
bind:value={newQty}
|
||||||
|
type="number"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
</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"
|
||||||
|
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>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<!-- Double-tap edit overlay -->
|
<!-- Double-tap edit overlay -->
|
||||||
{#if overlayItem}
|
{#if overlayItem}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -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,27 +362,30 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 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
|
|
||||||
bind:this={titleInput}
|
|
||||||
bind:value={newTitle}
|
|
||||||
onkeydown={handleKeydown}
|
|
||||||
placeholder={m.tasks_new_task_placeholder()}
|
|
||||||
class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-secondary focus:outline-none"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={handleAdd}
|
|
||||||
disabled={!newTitle.trim()}
|
|
||||||
aria-label={m.add()}
|
|
||||||
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-slate-100 dark:text-slate-900 transition-opacity"
|
|
||||||
>
|
|
||||||
<Plus size={18} strokeWidth={2} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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
|
||||||
|
bind:this={titleInput}
|
||||||
|
bind:value={newTitle}
|
||||||
|
onkeydown={handleKeydown}
|
||||||
|
placeholder={m.tasks_new_task_placeholder()}
|
||||||
|
class="min-w-0 flex-1 bg-transparent text-sm text-text-primary placeholder:text-text-secondary focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={handleAdd}
|
||||||
|
disabled={!newTitle.trim()}
|
||||||
|
aria-label={m.add()}
|
||||||
|
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-slate-100 dark:text-slate-900 transition-opacity"
|
||||||
|
>
|
||||||
|
<Plus size={18} strokeWidth={2} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|||||||
Reference in New Issue
Block a user