# Task Detail Bottom Sheet — Design **Status:** Draft **Implements:** [web-cli-parity.md](web-cli-parity.md) 1.1 (Task Detail View) + 1.2 (Task Editing) **Depends on:** Existing `Task` type, `PUT /tasks/:uuid`, tag endpoints --- ## 1. Overview A bottom sheet that slides up when a user taps a task row. It shows every non-null field on the task and allows inline editing of mutable fields. This is the single biggest new surface in the web app — it becomes the hub for viewing, editing, starting/stopping, and deleting tasks. ``` ┌─────────────────────────────────┐ │ Header │ ReportPicker │ ⚙ │ ├─────────────────────────────────┤ │ │ │ Task list (partially visible, │ │ dimmed behind scrim) │ │ │ ├─────────────────────────────────┤ ← scrim starts here │ ┌─────────────────────────────┐ │ │ │ ─── drag handle ─── │ │ │ │ │ │ │ │ Buy groceries [edit]│ │ ← description (tappable to edit) │ │ │ │ │ │ Status Pending │ │ ← read-only │ │ Priority ● High │ │ ← tappable │ │ Project Home │ │ ← tappable │ │ Due Tomorrow │ │ ← tappable │ │ Tags errand, food │ │ ← tappable (add/remove) │ │ Created Feb 18 │ │ ← read-only │ │ Modified Feb 19 │ │ ← read-only │ │ Urgency 8.2 │ │ ← read-only │ │ │ │ │ │ [Start] [Delete] │ │ ← action buttons │ └─────────────────────────────┘ │ ├─────────────────────────────────┤ │ InputBar │ └─────────────────────────────────┘ ``` --- ## 2. Component: `BottomSheet.svelte` A generic, reusable bottom sheet. Owns the open/close lifecycle, scrim, drag-to-dismiss, and snap positioning. Content is a slot. ### 2.1 Interface ```typescript interface BottomSheetProps { open: boolean onClose: () => void } ``` Controlled via the `open` prop. No exposed methods needed. ### 2.2 Behavior **Open:** - Sheet translates from `translateY(100%)` to `translateY(0)` via CSS transition - Scrim fades in (`opacity: 0 → 0.5`) — same `rgba(0,0,0,0.5)` as FilterModal backdrop - Body scroll is locked (`overflow: hidden` on ``) while sheet is open - Focus is trapped inside the sheet (keyboard accessibility) **Close (four ways):** 1. **Tap scrim** — light-dismiss, same as FilterModal 2. **Drag down** — swipe the sheet down past a threshold (see 2.3) 3. **Close button** — explicit X in top-right (accessibility fallback) 4. **Escape key** — keyboard dismiss **Position:** - Sheet takes up to 85% of viewport height (`max-height: 85dvh`) - Content scrolls internally (`overflow-y: auto`) if it exceeds the sheet height - On desktop (>768px), sheet is capped at 480px width and centered horizontally ### 2.3 Drag-to-Dismiss Gesture Same angle-based detection model as `SwipeAction`, but vertical: 1. Touch starts on the drag handle or sheet header area 2. If `|deltaY| > 10px` AND `|deltaY| > |deltaX| * 2` → lock to vertical drag 3. During drag: `translateY(deltaY)` (only positive — can't drag up past 0) 4. On release: if `deltaY > 150px` or velocity > threshold → close with animation. Otherwise snap back to `translateY(0)` 5. CSS transition on release (`transition: transform 0.3s ease-out`) The drag handle is a visual affordance (small pill-shaped bar at the top), but the entire header region is the drag target. ### 2.4 Implementation Pattern Uses a `
` rather than `` (see ADR-7 below for rationale). ```svelte
``` ### 2.5 CSS ```css .sheet-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, 0.5); opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } .sheet-scrim.open { opacity: 1; pointer-events: auto; } .sheet { position: fixed; bottom: 0; left: 0; right: 0; max-height: 85dvh; background: var(--bg-primary); border-radius: 1rem 1rem 0 0; box-shadow: var(--shadow-lg); transform: translateY(100%); transition: transform 0.3s ease-out; display: flex; flex-direction: column; z-index: 51; } .sheet.open { transform: translateY(0); } .sheet-handle { display: flex; justify-content: center; padding: var(--spacing-sm) 0; cursor: grab; touch-action: none; } .handle-bar { width: 2.5rem; height: 0.25rem; background: var(--text-tertiary); border-radius: 9999px; } .sheet-content { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 0 var(--spacing-md) var(--spacing-lg); } /* Desktop constraint */ @media (min-width: 769px) { .sheet { max-width: 480px; left: 50%; transform: translateX(-50%) translateY(100%); } .sheet.open { transform: translateX(-50%) translateY(0); } } ``` --- ## 3. Component: `TaskDetail.svelte` The content rendered inside the `BottomSheet`. Owns the task field layout and inline editing logic. ### 3.1 Interface ```typescript interface TaskDetailProps { task: Task onUpdate: (uuid: string, updates: Partial) => Promise onStart: (uuid: string) => Promise onStop: (uuid: string) => Promise onDelete: (uuid: string) => void // opens confirmation, doesn't delete directly onComplete: (uuid: string) => void onClose: () => void } ``` ### 3.2 Field Layout Fields are displayed in a two-column key-value layout. Each row is a `field-row` with a label (left) and value (right). Editable fields show a subtle edit affordance on tap. **Field display order and editability:** | Field | Editable | Display | Shown when | |-------|----------|---------|------------| | Description | Yes | Large text at top, outside the key-value grid | Always | | Status | No | Badge: "Pending" / "Completed" / "Deleted" / "Recurring" | Always | | Priority | Yes | Colored dot + label. Tap → cycles through H/M/D/L | Always | | Project | Yes | Text. Tap → inline text input | Non-null, or "Add..." | | Due | Yes | Relative date + absolute in parentheses. Tap → date input | Non-null, or "Set..." | | Scheduled | Yes | Same format as due | Non-null, or "Set..." | | Wait | Yes | Same format | Non-null, or "Set..." | | Until | Yes | Same format | Non-null, or "Set..." | | Start (active since) | No | Relative time | Only if task is started | | Recurrence | Yes* | Duration label (e.g. "every 1 week") | Only on templates | | Parent | No | "Recurring instance" with parent link | Only on instances | | Tags | Yes | Pill list with + button to add, x on each to remove | Always (empty = just [+]) | | Created | No | Absolute date | Always | | Modified | No | Absolute date | Always | | End | No | Absolute date | Only if completed | | UUID | No | Truncated, tap to copy full UUID | Always | | Urgency | No | Numeric score | Always (> 0) | *Recurrence is editable only on template tasks (`status === 'R'`), not on instances. For all other editable fields on recurring instances, an "edit instance or template?" prompt appears first (see section 3.8). **Null-field rule:** Fields with null values are hidden entirely. No empty rows or placeholder text. Exception: editable fields that are commonly set show an "Add..." or "Set..." affordance when null (project, due, scheduled, wait, until). ### 3.3 Visual Layout ``` ─── handle ─── Buy groceries [✏] ───────────────────────────────────────────────── Status Pending Priority ● High [tap to change] Project Home [tap to edit] Due Tomorrow (Feb 20) [tap to edit] Tags [errand] [food] [+] ─── divider ─── Created Feb 18, 2026 Modified Feb 19, 2026 Urgency 8.2 UUID 8f3a1b2c... [tap to copy] ─── divider ─── [ ▶ Start ] [ ✓ Complete ] [ 🗑 Delete ] ``` Three visual sections separated by subtle dividers: 1. **Editable fields** — the fields users interact with 2. **Read-only metadata** — timestamps, urgency, UUID 3. **Actions** — start/stop, delete, complete/uncomplete ### 3.4 Inline Editing Model When a user taps an editable field value, the value transforms in-place into an input control. No modal, no separate edit screen. **Per-field edit controls:** | Field | Edit Control | Behavior | |-------|-------------|----------| | Description | Text input (auto-expanding) | Replace text, Enter to save, Escape to cancel | | Priority | Cycle on tap | No input — tapping cycles `H → M → default → L → H` | | Project | Text input | Inline text replaces the value. Enter to save. Empty = clear | | Due, Scheduled, Wait, Until | `` | Native date picker. Change event saves. Clear button to null | | Tags | Pill list + input | [x] on tag to remove, [+] opens a small text input for new tag | | Recurrence | Text input | Accepts duration format like `1w`, `2d`, `1mo`. Enter to save | **Edit lifecycle:** 1. User taps field value → value becomes an input (the label stays) 2. User modifies → on Enter or blur or selection → `onUpdate(uuid, { field: newValue })` is called 3. **Optimistic:** UI shows new value immediately. On API failure, revert and show a brief error toast 4. Only one field is editable at a time. Tapping another field saves the current one first **Date fields** use the native ``. This gives us the OS date picker on Android and a calendar dropdown on desktop — no library needed. The value is converted to/from Unix timestamp via the existing `toUnix`/`fromUnix` utils. ### 3.5 Description Editing The description sits at the top of the sheet as a prominent heading. An edit icon (pencil) sits beside it. Tapping the icon (or the description text) replaces it with a `