onTap(task)}>
```
This is separate from the swipe gesture — taps are point events, swipes require
movement past the 10px angle-lock threshold.
---
## 12. Page Integration
### 12.1 New Handler in `+page.svelte`
```javascript
async function handleStartStop(uuid) {
const task = tasks.find(t => t.uuid === uuid);
if (!task) return;
try {
if (task.start) {
await tasksStore.stopTask(uuid);
} else {
await tasksStore.startTask(uuid);
}
} catch (error) {
console.error('Failed to start/stop task:', error);
}
}
```
### 12.2 Store Methods
`tasksStore` needs `startTask` and `stopTask` methods that call the existing
API endpoints and update the local task array:
```javascript
async startTask(uuid) {
// Optimistic: set start to now
const now = Math.floor(Date.now() / 1000);
update(tasks => tasks.map(t =>
t.uuid === uuid ? { ...t, start: now } : t
));
try {
await tasks.start(uuid);
} catch (err) {
// Rollback
update(tasks => tasks.map(t =>
t.uuid === uuid ? { ...t, start: null } : t
));
throw err;
}
}
async stopTask(uuid) {
// Optimistic: clear start
let prevStart;
update(tasks => tasks.map(t => {
if (t.uuid === uuid) {
prevStart = t.start;
return { ...t, start: null };
}
return t;
}));
try {
await tasks.stop(uuid);
} catch (err) {
// Rollback
update(tasks => tasks.map(t =>
t.uuid === uuid ? { ...t, start: prevStart } : t
));
throw err;
}
}
```
### 12.3 Wiring
```svelte
selectedTask = task}
onStartStop={handleStartStop}
/>
```
`TaskList` passes `onStartStop` through to each `TaskItem`.
---
## 13. Interaction Flow
```mermaid
sequenceDiagram
participant User
participant SwipeAction
participant TaskItem
participant Page
participant API
User->>SwipeAction: swipe left on task row
SwipeAction->>SwipeAction: offsetX goes negative, left bg reveals
SwipeAction->>SwipeAction: release past -100px threshold
SwipeAction->>SwipeAction: animate to -100vw, then snap back to 0
SwipeAction->>TaskItem: onSwipeLeft()
TaskItem->>Page: onStartStop(uuid)
Page->>Page: optimistic: toggle task.start
TaskItem-->>User: active indicator appears/disappears
Page->>API: POST /tasks/:uuid/start (or /stop)
API-->>Page: success
Note over Page: on failure, rollback optimistic update
```
---
## 14. Technical Decision
### ADR-11: Extend SwipeAction vs. new component
**Context:** Left-swipe needs different behavior from right-swipe (toggle vs.
collapse, different colors/icons). Should we extend the existing SwipeAction or
create a separate component?
**Decision:** Extend `SwipeAction` to be bidirectional. Rename `onSwipe` to
`onSwipeRight`, add `onSwipeLeft`. All call sites are updated.
**Rationale:**
- Touch gesture handlers can't be cleanly nested — two swipe components on the
same element would fight over touch events
- The angle-lock, threshold, and snap-back logic is identical in both
directions; only the threshold action differs
- One component, one touch handler, two callbacks
**Trade-off:** SwipeAction becomes slightly more complex (dual backgrounds,
direction detection). This is manageable — the core gesture logic stays the same,
with a direction check at threshold.
---
## 15. Theme Additions
Each theme needs new tokens for the active state badge:
```css
/* Add to each theme block in app.css */
--color-active-bg: rgba(57, 208, 186, 0.15); /* obsidian: primary @ 15% */
--color-active-text: #39d0ba; /* obsidian: primary */
```
Paper and midnight themes follow the same pattern with their respective primary
colors (`#6366f1` for paper, `#8b5cf6` for midnight).
---
## 16. Updated File List
Combining bottom sheet + swipe-left changes:
```
src/lib/components/
├── BottomSheet.svelte # NEW — generic bottom sheet
├── TaskDetail.svelte # NEW — task detail content
├── SwipeAction.svelte # MODIFY — bidirectional swipe support
├── TaskItem.svelte # MODIFY — onTap, onStartStop, active indicator
└── TaskList.svelte # MODIFY — pass onTap + onStartStop through
src/lib/stores/
└── tasks.js # MODIFY — add startTask(), stopTask()
src/routes/
└── +page.svelte # MODIFY — selectedTask, handleStartStop, sheet
src/app.css # MODIFY — add --color-active-bg/text tokens
```
---
---
# Undo Toast — Design
**Implements:** [web-cli-parity.md](web-cli-parity.md) 1.4 (Uncomplete / Revert)
**New component:** `Toast.svelte`
**Modifies:** `+page.svelte`, `TaskDetail.svelte`
---
## 17. Overview
Two paths to undo a completion:
1. **Undo toast** — appears immediately after completing a task (swipe or
checkbox). Shows the task description and an "Undo" button. Auto-dismisses
after 5 seconds. This is the fast path for accidental completions.
2. **Uncomplete in detail view** — available on any completed task via the
Completed report. The "Uncomplete" action button in `TaskDetail` (already
designed in section 3.7) handles this path.
Both paths use the same API call: `PUT /tasks/:uuid` with
`{ status: 'P', end: null }`.
```
┌─────────────────────────────────┐
│ Task list │
│ │
│ ... │
│ │
├─────────────────────────────────┤
│ ┌─────────────────────────────┐ │
│ │ ✓ "Buy groceries" [Undo] │ │ ← toast
│ └─────────────────────────────┘ │
├─────────────────────────────────┤
│ InputBar │
└─────────────────────────────────┘
```
The toast sits above the InputBar, inside the scroll area's bottom margin.
---
## 18. Component: `Toast.svelte`
A generic, reusable toast/snackbar. Not specific to undo — can be used for
any brief feedback message with an optional action button.
### 18.1 Interface
```typescript
interface ToastProps {
message: string
action?: { label: string, handler: () => void }
duration?: number // auto-dismiss ms, default 5000. 0 = no auto-dismiss
onDismiss: () => void
}
```
### 18.2 Behavior
- Appears with a slide-up + fade-in animation from the bottom
- Auto-dismisses after `duration` ms (default 5s)
- Dismisses immediately if the action button is tapped (after calling handler)
- Dismisses on swipe-right (quick manual dismiss without action)
- Only one toast is visible at a time. A new toast replaces the current one
(the previous action is lost — same as Android snackbar)
### 18.3 Position
- Fixed above the InputBar: `bottom: calc(InputBar height + spacing)`
- Horizontally: full width with `var(--spacing-md)` margin on each side
- On desktop (>768px): capped at `--content-max-width` and centered
### 18.4 Layout
```svelte
{message}
{#if action}
{action.label}
{/if}
```
### 18.5 CSS
```css
.toast {
position: fixed;
bottom: calc(3.5rem + var(--spacing-md)); /* above InputBar */
left: var(--spacing-md);
right: var(--spacing-md);
max-width: var(--content-max-width);
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-md);
z-index: 40; /* below sheet (50) but above content */
}
.toast-message {
font-size: var(--font-size-sm);
color: var(--text-primary);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.toast-action {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--color-primary);
background: none;
border: none;
cursor: pointer;
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: 0.25rem;
white-space: nowrap;
min-width: unset;
min-height: 36px; /* touch target */
}
.toast-action:hover {
background-color: var(--bg-secondary);
}
```
---
## 19. Undo Toast Integration
### 19.1 State in `+page.svelte`
```typescript
/** @type {{ uuid: string, description: string } | null} */
let undoToast = null;
```
### 19.2 Completion Flow Change
When a task is completed, stash its info for the toast before removing it from
the list:
```javascript
async function handleComplete(uuid) {
const task = tasks.find(t => t.uuid === uuid);
if (!task) return;
try {
await tasksStore.complete(uuid);
// Show undo toast
undoToast = { uuid: task.uuid, description: task.description };
} catch (error) {
console.error('Failed to complete task:', error);
}
}
```
### 19.3 Undo Handler
```javascript
async function handleUndo() {
if (!undoToast) return;
const { uuid } = undoToast;
undoToast = null; // dismiss toast immediately
try {
await tasksStore.updateTask(uuid, { status: 'P', end: null });
// Reload current report to show the restored task
await loadReport(activeReport);
} catch (error) {
console.error('Failed to undo completion:', error);
}
}
```
### 19.4 Template
```svelte
{#if undoToast}
undoToast = null}
/>
{/if}
```
### 19.5 Edge Cases
- **Completing another task while toast is showing:** Replace the toast. The
previous undo opportunity is lost. This matches the standard snackbar pattern
— keeping a queue adds complexity for a rare scenario.
- **Completing a recurring task:** The toast shows "Completed" with undo. If
undo is tapped, the completion is reverted but the spawned next instance is
NOT automatically deleted. The user may end up with a duplicate. This is
acceptable — it matches the CLI behavior where `undo` after `done` on a
recurring task restores the completed instance but leaves the new one. The
detail view can be used to clean up.
- **Toast while bottom sheet is open:** The toast should appear below the sheet
scrim (z-index 40 < sheet 50). If the user completes from the detail view's
action button, close the sheet first, then show the toast.
---
# Delete Confirmation — Design
**Implements:** [web-cli-parity.md](web-cli-parity.md) 1.5 (Delete Confirmation)
**New component:** `ConfirmDialog.svelte`
**Modifies:** `+page.svelte`, `TaskDetail.svelte`
---
## 20. Overview
A confirmation dialog that appears before any delete action. Shows the task
description so the user can verify what they're about to delete. Uses the native
`` element, matching the `FilterModal` pattern.
```
┌──────────────────────────────┐
│ │
│ Delete task? │
│ │
│ "Buy groceries" │
│ │
│ This cannot be undone. │
│ │
│ [Cancel] [Delete] │
│ │
└──────────────────────────────┘
```
---
## 21. Component: `ConfirmDialog.svelte`
A generic confirmation dialog. Not specific to delete — can be reused for any
destructive action (bulk operations in Tier 2, etc.).
### 21.1 Interface
```typescript
interface ConfirmDialogProps {
title: string
message: string // task description or details to show
detail?: string // secondary text (e.g. "This cannot be undone.")
confirmLabel: string // button text, e.g. "Delete"
confirmVariant: 'danger' | 'primary' // button color
onConfirm: () => void
onCancel: () => void
}
```
Exposed method: `open()` — opens the dialog (matches FilterModal pattern).
### 21.2 Template
```svelte
{title}
"{message}"
{#if detail}
{detail}
{/if}
Cancel
{confirmLabel}
```
### 21.3 Behavior
- Opens via `dialogEl.showModal()` — blocks interaction, provides backdrop
- Closes on Cancel, Confirm, Escape, or backdrop click
- Confirm calls `onConfirm` then closes
- Cancel/Escape/backdrop calls `onCancel` then closes
- Auto-focuses the Cancel button on open (safe default — user must deliberately
choose Delete)
### 21.4 CSS
```css
.confirm-dialog {
border: none;
border-radius: var(--border-radius);
background: transparent;
padding: 0;
margin: auto;
max-width: min(360px, calc(100vw - 2rem));
width: 100%;
}
.confirm-dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.dialog-content {
background-color: var(--bg-primary);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: var(--shadow-lg);
padding: var(--spacing-lg);
}
.dialog-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-md) 0;
}
.dialog-message {
font-size: var(--font-size-base);
color: var(--text-primary);
margin: 0 0 var(--spacing-xs) 0;
word-break: break-word;
}
.dialog-detail {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin: 0 0 var(--spacing-lg) 0;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
.btn {
padding: var(--spacing-sm) var(--spacing-md);
border: none;
border-radius: var(--border-radius);
font-size: var(--font-size-sm);
font-family: inherit;
font-weight: 500;
cursor: pointer;
min-height: 40px;
min-width: unset;
}
.btn-ghost {
background: none;
color: var(--text-secondary);
}
.btn-ghost:hover {
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.btn-danger {
background-color: var(--color-danger);
color: white;
}
.btn-danger:hover {
opacity: 0.9;
}
```
---
## 22. Delete Flow Integration
### 22.1 State in `+page.svelte`
```typescript
/** @type {{ uuid: string, description: string } | null} */
let deleteTarget = null;
/** @type {ConfirmDialog} */
let confirmDialog;
```
### 22.2 Triggering Delete
Delete is triggered from the TaskDetail action buttons. The `onDelete` callback
sets the target and opens the dialog:
```javascript
function handleDeleteRequest(uuid) {
const task = tasks.find(t => t.uuid === uuid)
?? (selectedTask?.uuid === uuid ? selectedTask : null);
if (!task) return;
deleteTarget = { uuid: task.uuid, description: task.description };
confirmDialog.open();
}
```
### 22.3 Confirm/Cancel Handlers
```javascript
async function handleDeleteConfirm() {
if (!deleteTarget) return;
const { uuid } = deleteTarget;
deleteTarget = null;
// Close the bottom sheet if it's open
selectedTask = null;
try {
await tasksStore.deleteTask(uuid);
} catch (error) {
console.error('Failed to delete task:', error);
}
}
function handleDeleteCancel() {
deleteTarget = null;
// Bottom sheet stays open — user cancelled
}
```
### 22.4 Template
```svelte
```
### 22.5 Z-Index Layering
The dialog uses `` with `showModal()`, which renders to the top-layer
— it will appear above the bottom sheet scrim (z-index 50) and above the toast
(z-index 40) without any manual z-index management.
```
Top layer (browser-managed): ConfirmDialog ( showModal)
z-index 51: BottomSheet panel
z-index 50: BottomSheet scrim
z-index 40: Toast
Content: TaskList, Header, InputBar
```
---
## 23. Interaction Flows
### 23.1 Undo After Completion
```mermaid
sequenceDiagram
participant User
participant TaskItem
participant Page
participant Toast
participant API
User->>TaskItem: swipe right / checkbox
TaskItem->>Page: onComplete(uuid)
Page->>API: POST /tasks/:uuid/complete
Page->>Page: remove task from list
Page->>Toast: show "Completed 'Buy groceries'" + [Undo]
Toast-->>User: toast visible for 5s
alt User taps Undo
User->>Toast: tap [Undo]
Toast->>Page: handleUndo()
Page->>API: PUT /tasks/:uuid {status: 'P', end: null}
Page->>Page: reload report (task reappears)
Toast-->>User: toast dismissed
else Toast expires
Toast-->>Page: onDismiss()
Page->>Page: undoToast = null
end
```
### 23.2 Delete from Detail View
```mermaid
sequenceDiagram
participant User
participant Detail as TaskDetail
participant Page
participant Dialog as ConfirmDialog
participant API
User->>Detail: tap [Delete] button
Detail->>Page: onDelete(uuid)
Page->>Dialog: open()
Dialog-->>User: "Delete task? 'Buy groceries'"
alt User confirms
User->>Dialog: tap [Delete]
Dialog->>Page: onConfirm()
Page->>Page: close bottom sheet
Page->>API: DELETE /tasks/:uuid
Page->>Page: remove task from list
else User cancels
User->>Dialog: tap [Cancel] / Escape / backdrop
Dialog->>Page: onCancel()
Note over Page: bottom sheet stays open
end
```
---
## 24. Technical Decisions
### ADR-12: `` for confirmation, `` for toast
**Context:** Both components are overlaid UI. Should they use the same pattern?
**Decision:** `ConfirmDialog` uses `
` with `showModal()`. `Toast` uses
a positioned ``.
**Rationale:**
- The confirmation dialog IS a modal — it blocks interaction and requires a
deliberate choice. `` provides backdrop, focus trapping, and Escape
handling for free.
- The toast is NOT a modal — the user can ignore it and keep interacting with
the task list. A `` would be wrong here because it would block
interaction.
- Different semantics → different elements.
### ADR-13: Single toast, not a queue
**Context:** What happens if two tasks are completed quickly? Should toasts
queue?
**Decision:** Latest toast replaces the current one. No queue.
**Rationale:**
- Queuing toasts adds complexity (queue data structure, stacking layout or
sequential display, timing coordination)
- The scenario is rare — completing two tasks within 5 seconds
- When it does happen, the user most likely wants to undo the most recent one
- Matches Android snackbar behavior, which is the established pattern
**Trade-off:** The undo opportunity for the first completion is lost when the
second one fires. Acceptable — the Completed report + Uncomplete button is the
fallback path.
### ADR-14: Auto-focus Cancel on confirm dialog
**Context:** Which button should be focused when the dialog opens?
**Decision:** Auto-focus Cancel.
**Rationale:** Delete is destructive and irreversible. Focusing Cancel means an
accidental Enter keypress doesn't delete the task. The user must deliberately
tab to or click Delete.
---
## 25. Updated File List
Full list combining all Tier 1 designs:
```
src/lib/components/
├── BottomSheet.svelte # NEW — generic bottom sheet (section 2)
├── TaskDetail.svelte # NEW — task detail + inline edit (section 3)
├── Toast.svelte # NEW — generic toast/snackbar (section 18)
├── ConfirmDialog.svelte # NEW — generic confirm dialog (section 21)
├── SwipeAction.svelte # MODIFY — bidirectional (section 10)
├── TaskItem.svelte # MODIFY — onTap, onStartStop, active indicator
└── TaskList.svelte # MODIFY — pass new callbacks through
src/lib/stores/
└── tasks.js # MODIFY — add startTask(), stopTask()
src/routes/
└── +page.svelte # MODIFY — all Tier 1 state + handlers
src/app.css # MODIFY — add --color-active-bg/text tokens
```
**New components: 4** (`BottomSheet`, `TaskDetail`, `Toast`, `ConfirmDialog`)
**Modified components: 4** (`SwipeAction`, `TaskItem`, `TaskList`, `+page.svelte`)
**Modified stores: 1** (`tasks.js`)
**Modified styles: 1** (`app.css`)
No new API endpoints — all Tier 1 features use existing endpoints.
---
## 8. Resolved Questions
| # | Question | Resolution |
|---|----------|------------|
| Q1 | Should the sheet remember scroll position when the same task is re-opened? | **No.** Always scroll to top on open. |
| Q2 | Should editing a recurring instance offer "edit this instance" vs "edit template"? | **Yes.** Show a confirmation prompt: "Edit this instance" or "Edit template". See section 3.8. |
| Q3 | Should we show a loading state in the sheet while an edit is in flight? | **No.** Optimistic updates only. Show error on failure. |