Skip to content

Commit 34b5520

Browse files
committed
Add subtask and UI improvements spec
1 parent d5cbad1 commit 34b5520

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Subtask Management & UI Polish — Spec
2+
3+
## Overview
4+
5+
Four improvements to TaskMenu:
6+
1. **Inline "Add subtask"** from the task list (no detail view required)
7+
2. **Indent/Outdent** to convert tasks ↔ subtasks via context menu and keyboard
8+
3. **Hover highlight** — subtle row highlight on mouse hover
9+
4. **Completion animation** — satisfying visual feedback when checking off a task
10+
11+
---
12+
13+
## 1. Inline "Add Subtask"
14+
15+
### Behavior
16+
- In the **context menu** for any root-level task (i.e. tasks where `parent == nil`), add an "Add Subtask" action
17+
- Clicking it inserts a **temporary inline text field** directly below the task (indented to child level), with focus
18+
- Pressing Enter creates the subtask via `appState.addSubtask(title:parentId:)` and dismisses the field
19+
- Pressing Escape cancels and dismisses the field
20+
- If the parent was collapsed, auto-expand it first
21+
22+
### Implementation
23+
24+
**TaskListView.swift:**
25+
- Add `@State private var inlineSubtaskParentID: String?` to track which task is getting an inline subtask
26+
- In the context menu for task rows where `task.parent == nil`, add:
27+
```
28+
Button { inlineSubtaskParentID = task.id } label: {
29+
Label("Add Subtask", systemImage: "text.badge.plus")
30+
}
31+
```
32+
- When rendering the flattened task list, after a task whose `id == inlineSubtaskParentID`, insert an inline text field row at `indentLevel + 1`
33+
- Auto-expand: if `inlineSubtaskParentID` is set and the task is collapsed, call `appState.toggleCollapsed(taskID)` to expand it
34+
35+
**New: InlineSubtaskField view (can be a private struct in TaskListView.swift):**
36+
- Simple HStack: plus.circle icon + TextField
37+
- `@FocusState` auto-focused on appear
38+
- On submit: call `appState.addSubtask(title:parentId:)`, then keep the field open for rapid entry (clear text, stay focused)
39+
- On Escape (via `.onExitCommand`): set `inlineSubtaskParentID = nil`
40+
- On click outside / loss of focus: dismiss (set `inlineSubtaskParentID = nil`)
41+
- Indented to match child level: `paddingLeading = (parentIndentLevel + 1) * 20`
42+
43+
---
44+
45+
## 2. Indent / Outdent
46+
47+
### Behavior
48+
- **Indent** (Tab or context menu "Make Subtask"): Convert a root-level task into a subtask of the task directly above it in the list
49+
- Only available if the task above exists and is a root-level task (no nested subtask-of-subtask — Google Tasks only supports one level of nesting)
50+
- Uses the `moveTask` API with `parentId` set to the task above
51+
- **Outdent** (Shift+Tab or context menu "Move to Top Level"): Convert a subtask back to a root-level task
52+
- Uses the `moveTask` API with `parentId` omitted (moves to top level)
53+
- Position: place it after its former parent in the root list
54+
55+
### Implementation
56+
57+
**AppState.swift — new methods:**
58+
```swift
59+
func indentTask(_ task: TaskItem) async {
60+
// Find the task directly above in the root tasks list
61+
// Call api.moveTask(listId:taskId:parentId:) to make it a child
62+
// Update local tasks array
63+
}
64+
65+
func outdentTask(_ task: TaskItem) async {
66+
// Call api.moveTask(listId:taskId:previousId:) with no parent
67+
// previousId = the former parent's ID (to place it right after)
68+
// Update local tasks array
69+
}
70+
```
71+
72+
**Context menu additions (TaskListView.swift or TaskRowView.swift):**
73+
- For root-level tasks that have a sibling above them: "Make Subtask" (indent)
74+
- For subtasks (tasks with `parent != nil`): "Move to Top Level" (outdent)
75+
76+
**Keyboard shortcuts:** Deferred — context menu only for now.
77+
78+
### Constraints
79+
- Google Tasks only supports **one level** of nesting. Do NOT allow indenting a subtask further (making a sub-subtask)
80+
- Indent is only available for incomplete root-level tasks
81+
- Outdent is only available for subtasks
82+
83+
---
84+
85+
## 3. Hover Highlight
86+
87+
### Current State
88+
`TaskRowView` already has `@State private var isHovering` and applies a background:
89+
```swift
90+
.background(
91+
RoundedRectangle(cornerRadius: 6)
92+
.fill(isHovering ? Color.primary.opacity(0.05) : .clear)
93+
)
94+
```
95+
96+
### Changes
97+
- Increase opacity slightly: `0.05``0.06` (subtle but more visible)
98+
- Show context actions on hover: display a small trailing "..." button (or action icons) that appear on hover. This gives discoverability for the "Add Subtask" action without requiring right-click
99+
- The hover actions area appears at the trailing edge of the row
100+
- Contains: a `+` button (add subtask, only for root tasks) and optionally `` for more actions
101+
- Fades in/out with the hover state
102+
103+
### Implementation
104+
- In `TaskRowView`, add a trailing HStack that's only visible when `isHovering`
105+
- Pass new callbacks: `onAddSubtask: (() -> Void)?` (nil for subtasks)
106+
- The `+` button calls `onAddSubtask`
107+
- Use `.opacity(isHovering ? 1 : 0)` with animation for smooth fade
108+
109+
---
110+
111+
## 4. Completion Animation
112+
113+
### Behavior
114+
When a user checks off a task:
115+
1. The circle fills with a **green checkmark** with a brief scale-up bounce (already partially there with `.contentTransition(.symbolEffect(.replace))`)
116+
2. The title gets a **strikethrough that animates left-to-right** (or just appears)
117+
3. After a **short delay (~600ms)**, the row **slides out to the right and fades**, then moves to the completed section
118+
4. If the task has subtasks, they gray out together with the parent (already works)
119+
120+
### Implementation
121+
122+
**TaskRowView.swift:**
123+
- Add `@State private var justCompleted = false`
124+
- When `onToggle` is called and the task is going from incomplete → complete:
125+
- Set `justCompleted = true`
126+
- The checkmark circle gets a brief `.scaleEffect` pulse: scale to 1.2 then back to 1.0
127+
- The existing `contentTransition(.symbolEffect(.replace))` on the checkmark icon handles the symbol swap animation already — keep that
128+
129+
**TaskListView.swift:**
130+
- The transition is already defined:
131+
```swift
132+
.transition(.asymmetric(
133+
insertion: .move(edge: .top).combined(with: .opacity),
134+
removal: .move(edge: .trailing).combined(with: .opacity)
135+
))
136+
```
137+
This should already animate the row sliding right when it moves from active to completed section. Verify this works correctly with the current animation block. If it doesn't trigger, we may need to add a brief delay before the optimistic update in `toggleTask` to let the animation play.
138+
139+
---
140+
141+
## Files to Modify
142+
143+
| File | Changes |
144+
|------|---------|
145+
| `TaskRowView.swift` | Hover action buttons, completion animation (scale pulse), add `onAddSubtask` callback |
146+
| `TaskListView.swift` | Inline subtask field, context menu indent/outdent/add-subtask, keyboard shortcuts |
147+
| `AppState.swift` | `indentTask()`, `outdentTask()` methods |
148+
149+
## Files to Create
150+
151+
None — all changes fit in existing files.
152+
153+
---
154+
155+
## Testing Notes
156+
157+
- Test indent when there's only one root task (should be disabled — no task above to become parent of)
158+
- Test outdent places the task after its former parent
159+
- Test that indenting a subtask is not possible (one-level limit)
160+
- Test inline subtask field dismissal on Escape and focus loss
161+
- Test completion animation plays before the row moves to completed section
162+
- Test drag-and-drop still works with the new hover actions

0 commit comments

Comments
 (0)