feat: add parse endpoint, refactor recurring tasks, and improve web task completion
Extract CreateRecurringTask into engine package for reuse by both CLI and API. Add POST /tasks/parse endpoint for CLI-style input parsing. Remove FK constraint on change_log to preserve history after task deletion. Update web frontend to filter completed tasks from view and add mock mode support for development. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,681 @@
|
||||
# Opal Web PWA — Architecture Design
|
||||
|
||||
**Target environment:** Internal product. Android Chrome and desktop
|
||||
Chrome/Firefox. Browser versions are controlled. iOS is not in scope.
|
||||
|
||||
**CSS philosophy:** Prefer modern HTML/CSS over JavaScript wherever possible.
|
||||
Use `dvh` units, `has()`, container queries, `flex-wrap`, `popover`,
|
||||
`@starting-style`, and other modern features freely — no legacy browser
|
||||
concerns.
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
The redesign transforms opal-web from a multi-page form-based app into a
|
||||
single-screen CLI-passthrough interface. The frontend becomes a thin shell over
|
||||
the opal command language, with all parsing handled server-side.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph "Frontend (SvelteKit Static PWA)"
|
||||
Layout["+layout.svelte"]
|
||||
Home["+page.svelte — Single Screen"]
|
||||
Settings["/settings"]
|
||||
|
||||
Home --> Header
|
||||
Home --> TaskList
|
||||
Home --> InputBar
|
||||
|
||||
Header --> ReportPicker["ReportPicker dropdown"]
|
||||
Header --> GearIcon["⚙ → /settings"]
|
||||
|
||||
InputBar --> PropertyPills
|
||||
InputBar --> TextInput["CLI text input"]
|
||||
|
||||
TaskList --> TaskItem["TaskItem (swipe + checkbox)"]
|
||||
end
|
||||
|
||||
subgraph "Backend (Go + chi)"
|
||||
API["REST API"]
|
||||
Parser["Modifier Parser"]
|
||||
Reports["Report Engine"]
|
||||
DB["SQLite"]
|
||||
|
||||
API --> Parser
|
||||
API --> Reports
|
||||
Parser --> DB
|
||||
Reports --> DB
|
||||
end
|
||||
|
||||
TextInput -- "POST /tasks/parse\n{input: raw string}" --> API
|
||||
TaskList -- "GET /tasks?report=pending" --> API
|
||||
TaskItem -- "POST /tasks/:uuid/complete" --> API
|
||||
```
|
||||
|
||||
### Page Structure
|
||||
|
||||
```
|
||||
/ ........................ Single-screen task view (auth-gated)
|
||||
/settings ................ Settings page (gear icon)
|
||||
/auth/login .............. OAuth login
|
||||
/auth/callback ........... OAuth callback
|
||||
```
|
||||
|
||||
All other routes (`/tasks/new`, `/tasks/[uuid]`, `/projects`, `/tags`) are
|
||||
**removed**. The bottom navigation bar is removed. The input bar replaces it.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component Design
|
||||
|
||||
### 2.1 Root Layout (`+layout.svelte`)
|
||||
|
||||
**Responsibility:** App shell — auth gating, global styles, routing.
|
||||
|
||||
**Changes from current:**
|
||||
- Remove `BottomNav` component entirely
|
||||
- No conditional nav rendering — the layout is just a slot
|
||||
|
||||
### 2.2 Home Page (`+page.svelte`)
|
||||
|
||||
**Responsibility:** The single-screen orchestrator. Owns the three vertical
|
||||
zones: header, task list, input bar.
|
||||
|
||||
**Interface (reactive state):**
|
||||
```typescript
|
||||
// Active report selection
|
||||
let activeReport: string = 'pending'
|
||||
|
||||
// Task data
|
||||
let tasks: Task[] = []
|
||||
let loading: boolean = true
|
||||
|
||||
// Input bar
|
||||
let inputValue: string = ''
|
||||
let inputFocused: boolean = false
|
||||
let inputError: string | null = null
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
- On mount: redirect to `/auth/login` if not authenticated, otherwise load
|
||||
tasks for default report (`pending`)
|
||||
- On report change: reload tasks with `GET /tasks?report={name}`
|
||||
- On input submit: `POST /tasks/parse` with raw string. On success, the
|
||||
response contains the created task — insert it into the list at the top
|
||||
(rather than re-fetching the entire list). The next report load will sort it
|
||||
into the correct position
|
||||
- On task complete (swipe or checkbox): `POST /tasks/:uuid/complete`, remove
|
||||
from list with animation
|
||||
|
||||
### 2.3 Header
|
||||
|
||||
**Responsibility:** Shows current report name, report picker trigger, settings
|
||||
link.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface HeaderProps {
|
||||
activeReport: string
|
||||
onReportChange: (report: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Layout:** `[Report Name ▾]` left-aligned, `[⚙]` right-aligned. Tapping the
|
||||
report name or chevron opens the `ReportPicker`.
|
||||
|
||||
### 2.4 ReportPicker
|
||||
|
||||
**Responsibility:** Dropdown overlay listing all available reports.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface ReportPickerProps {
|
||||
activeReport: string
|
||||
open: boolean
|
||||
onSelect: (report: string) => void
|
||||
onClose: () => void
|
||||
}
|
||||
```
|
||||
|
||||
**Reports list:**
|
||||
|
||||
| Group | Reports |
|
||||
|--------------|----------------------------------------------|
|
||||
| Common | Pending, Next, Active, Ready |
|
||||
| Time-based | Overdue, Waiting, Newest, Oldest |
|
||||
| Recurring | Recurring, Template |
|
||||
| Archive | Completed |
|
||||
|
||||
The dropdown is grouped with subtle dividers to manage the 11-item density on
|
||||
mobile. The active report has a visual indicator (checkmark or highlight).
|
||||
|
||||
**Implementation:** Use the native Popover API (`popover` attribute +
|
||||
`popovertarget`). This gives us light-dismiss (tap outside to close), top-layer
|
||||
rendering, and `::backdrop` styling for free — no JS for open/close state or
|
||||
click-outside detection. Entry/exit animations via `@starting-style` +
|
||||
`transition-behavior: allow-discrete`.
|
||||
|
||||
**Dismiss:** Tapping outside (popover light-dismiss), selecting a report, or
|
||||
pressing Escape.
|
||||
|
||||
### 2.5 TaskList
|
||||
|
||||
**Responsibility:** Scrollable list of tasks between header and input bar.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface TaskListProps {
|
||||
tasks: Task[]
|
||||
loading: boolean
|
||||
activeReport: string
|
||||
onComplete: (uuid: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Empty states:**
|
||||
- Loading: skeleton or spinner
|
||||
- No tasks at all (first use): "Type a task below to get started"
|
||||
- No tasks for report: "No {report} tasks" (e.g., "No overdue tasks")
|
||||
|
||||
**Scroll region:** Uses `flex: 1` with `overflow-y: auto` between the
|
||||
fixed-position header and input bar. Must account for keyboard open state.
|
||||
|
||||
### 2.6 TaskItem
|
||||
|
||||
**Responsibility:** Single task row with completion interactions.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface TaskItemProps {
|
||||
task: Task
|
||||
onComplete: (uuid: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Visual layout:**
|
||||
```
|
||||
[○] Buy groceries due:Feb 15
|
||||
+errand project:Home pri:H
|
||||
```
|
||||
|
||||
- Left: completion circle/checkbox
|
||||
- Center: description on first line, metadata pills on second line (only if
|
||||
metadata exists)
|
||||
- Metadata: project badge, priority indicator, due date, tags — all inline with
|
||||
subtle color coding
|
||||
|
||||
**Completion interactions:**
|
||||
1. **Checkbox tap:** Calls `onComplete` immediately
|
||||
2. **Swipe right:** See section 2.7
|
||||
|
||||
**Priority colors:**
|
||||
- High (3): red/warm
|
||||
- Medium (2): amber
|
||||
- Low (0): muted gray
|
||||
- Default (1): no indicator
|
||||
|
||||
**Due date colors:**
|
||||
- Overdue: red
|
||||
- Due today: amber
|
||||
- Due within 7 days: default
|
||||
- Future/none: muted
|
||||
|
||||
### 2.7 SwipeAction
|
||||
|
||||
**Responsibility:** Wraps TaskItem to provide swipe-to-complete gesture.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface SwipeActionProps {
|
||||
onSwipe: () => void
|
||||
threshold?: number // pixels, default 100
|
||||
children: Snippet // TaskItem content
|
||||
}
|
||||
```
|
||||
|
||||
**Gesture model (addresses Design Note on swipe/scroll conflict):**
|
||||
|
||||
1. Touch starts — record `startX`, `startY`
|
||||
2. On move, compute `deltaX` and `deltaY`
|
||||
3. **Lock-in rule:** If `|deltaX| > 10px` AND `|deltaX| > |deltaY| * 2`
|
||||
(angle < ~27°), lock to horizontal swipe and call `preventDefault()` on the
|
||||
touch event to suppress scrolling
|
||||
4. If vertical movement dominates first, do nothing (allow scroll)
|
||||
5. During swipe: translate the task row, reveal green background with checkmark
|
||||
icon underneath
|
||||
6. **Threshold:** If `deltaX > 100px` on release → trigger completion
|
||||
7. **Cancel:** If released before threshold → CSS transition back to
|
||||
`translate(0)` (set `transition: transform 0.2s ease` on release, remove it
|
||||
on touch start)
|
||||
8. **Completion animation:** On threshold met, add a `completing` class that
|
||||
triggers a CSS transition (`opacity: 0; height: 0; margin: 0; padding: 0;
|
||||
overflow: hidden; transition: all 0.3s ease`). The `transitionend` event
|
||||
fires the API call and removes the DOM element
|
||||
9. The swipe tracking (touch start/move/end, deltaX/deltaY, translateX) requires
|
||||
JS. But all animations (snap-back, completion fade-out) are CSS transitions
|
||||
|
||||
### 2.8 InputBar
|
||||
|
||||
**Responsibility:** Fixed-to-bottom CLI input with property pills.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface InputBarProps {
|
||||
value: string
|
||||
error: string | null
|
||||
loading: boolean
|
||||
onSubmit: (input: string) => void
|
||||
onInput: (value: string) => void
|
||||
onFocusChange: (focused: boolean) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Layout (bottom-up):**
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ [Due] [Pri] [Project] [Tag] ... │ ← Pills (visible when focused)
|
||||
├─────────────────────────────────────┤
|
||||
│ [ Add a task... ] [→] │ ← Input + submit button
|
||||
├─────────────────────────────────────┤
|
||||
│ (error message if any) │ ← Inline error
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Keyboard handling (addresses Design Note):**
|
||||
- The entire page layout uses `height: 100dvh` (`dvh` = dynamic viewport
|
||||
height, which accounts for the virtual keyboard on Android Chrome/Firefox)
|
||||
- The layout is a CSS grid or flexbox column: `header | task-list (flex:1) |
|
||||
input-bar`. When the keyboard opens, `100dvh` shrinks, the flex container
|
||||
shrinks, and the input bar stays at the bottom — no JS needed
|
||||
- This is a controlled-browser internal product (Android Chrome / desktop
|
||||
Firefox+Chrome); `dvh` is fully supported
|
||||
|
||||
**Error display:**
|
||||
- Errors appear as a small red text line between the input and the pills
|
||||
- Input does NOT clear on error — user can fix and retry
|
||||
- Error clears on next input change
|
||||
|
||||
### 2.9 PropertyPills
|
||||
|
||||
**Responsibility:** Horizontal row of tappable shortcut pills.
|
||||
|
||||
**Interface:**
|
||||
```typescript
|
||||
interface PropertyPillsProps {
|
||||
visible: boolean
|
||||
onInsert: (text: string) => void
|
||||
}
|
||||
```
|
||||
|
||||
**Pills (ordered per requirements):**
|
||||
|
||||
| Label | Inserts |
|
||||
|------------|--------------|
|
||||
| Due | `due:` |
|
||||
| Pri | `priority:` |
|
||||
| Project | `project:` |
|
||||
| Tag | `+` |
|
||||
| Recur | `recur:` |
|
||||
| Scheduled | `scheduled:` |
|
||||
| Wait | `wait:` |
|
||||
| Until | `until:` |
|
||||
|
||||
**Behavior:**
|
||||
- Tapping a pill calls `onInsert(text)` — the parent `InputBar` handles cursor
|
||||
positioning via `selectionStart`/`selectionEnd` on the input element
|
||||
- Pills wrap over multiple lines using `display: flex; flex-wrap: wrap; gap`
|
||||
- Appears with a brief slide-up animation when input focuses
|
||||
- Disappears when input blurs (with short delay to allow pill tap to register
|
||||
before blur fires)
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Model
|
||||
|
||||
No changes to the task entity. The existing `Task` type from
|
||||
`src/lib/api/types.js` is sufficient.
|
||||
|
||||
**New derived type for API responses:**
|
||||
```typescript
|
||||
// Report metadata returned alongside task list
|
||||
interface ReportResponse {
|
||||
report: string // Report name
|
||||
tasks: Task[] // Filtered/sorted tasks
|
||||
count: number // Total count
|
||||
}
|
||||
|
||||
// Parse endpoint response
|
||||
interface ParseResponse {
|
||||
task: Task // The created task
|
||||
}
|
||||
|
||||
// Parse endpoint error
|
||||
interface ParseError {
|
||||
error: string // Human-readable error message
|
||||
field?: string // Which modifier failed, if applicable
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. API Design
|
||||
|
||||
### 4.1 New Endpoint: Parse and Create Task
|
||||
|
||||
Resolves **Q6** from requirements.
|
||||
|
||||
```
|
||||
POST /tasks/parse
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Request:
|
||||
{
|
||||
"input": "Buy groceries due:tomorrow +errand priority:H"
|
||||
}
|
||||
|
||||
Success Response (201 Created):
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"task": { ...Task object... }
|
||||
}
|
||||
}
|
||||
|
||||
Error Response (400 Bad Request):
|
||||
{
|
||||
"success": false,
|
||||
"error": "invalid date value for 'due': neverday"
|
||||
}
|
||||
```
|
||||
|
||||
**Backend implementation:** The handler inlines the trivial arg classification
|
||||
from `cmd/add.go:parseAddArgs()` (split on whitespace, args with `+`/`-` prefix
|
||||
or containing `:` are modifiers, the rest is description). The real parsing is
|
||||
done by `engine.ParseModifier()` and `engine.CreateTaskWithModifier()`, which
|
||||
are already in the engine package.
|
||||
|
||||
Handler steps:
|
||||
1. Split `input` string on whitespace
|
||||
2. Classify args: modifier if starts with `+`/`-` or contains `:`, otherwise
|
||||
description word
|
||||
3. Call `engine.ParseModifier(modifierArgs)`
|
||||
4. Call `engine.CreateTaskWithModifier(description, modifier)`
|
||||
5. Handle recurrence if `recur` is set (same logic as `cmd/add.go:addRecurringTask`)
|
||||
6. Return the created task or a parse error
|
||||
|
||||
### 4.2 Updated Endpoint: Report-Based Task Listing
|
||||
|
||||
Resolves **Q7** from requirements.
|
||||
|
||||
```
|
||||
GET /tasks?report=pending
|
||||
Authorization: Bearer <token>
|
||||
|
||||
Response (200 OK):
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"report": "pending",
|
||||
"tasks": [ ...Task objects... ],
|
||||
"count": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Query parameters:**
|
||||
- `report` — Report name (default: `pending`). One of: `pending`, `next`,
|
||||
`active`, `ready`, `overdue`, `completed`, `waiting`, `recurring`,
|
||||
`template`, `newest`, `oldest`
|
||||
- Existing filter params (`project`, `tag`, etc.) are merged with the report's
|
||||
base filter, allowing further narrowing
|
||||
|
||||
**Backend implementation:** The report engine already exists in
|
||||
`internal/engine/report.go`. The handler:
|
||||
1. Looks up the report by name (map in `report.go` already has all reports)
|
||||
2. Applies the report's base filter + any additional query params
|
||||
3. Executes the query
|
||||
4. Applies post-filters, sort, and limit functions
|
||||
5. Returns tasks in the report's sort order
|
||||
|
||||
**Mapping of frontend report names to backend:**
|
||||
|
||||
| Frontend | Backend report name |
|
||||
|-------------|-------------------|
|
||||
| Pending | `list` |
|
||||
| Next | `next` |
|
||||
| Active | `active` |
|
||||
| Ready | `ready` |
|
||||
| Overdue | `overdue` |
|
||||
| Completed | `completed` |
|
||||
| Waiting | `waiting` |
|
||||
| Recurring | `recurring` |
|
||||
| Template | `template` |
|
||||
| Newest | `newest` |
|
||||
| Oldest | `oldest` |
|
||||
|
||||
### 4.3 Existing Endpoints (No Changes)
|
||||
|
||||
These are already sufficient:
|
||||
- `POST /tasks/{uuid}/complete` — Used by swipe and checkbox
|
||||
- `GET /health` — Health check
|
||||
- All auth endpoints — Unchanged
|
||||
|
||||
### 4.4 Error Response Format
|
||||
|
||||
All endpoints already use the standard envelope:
|
||||
```typescript
|
||||
{ success: boolean, data?: any, error?: string }
|
||||
```
|
||||
|
||||
No change needed. The parse endpoint follows the same pattern, with `error`
|
||||
containing a human-readable message suitable for display below the input bar.
|
||||
|
||||
---
|
||||
|
||||
## 5. Technical Decisions
|
||||
|
||||
### ADR-1: Single-screen layout replaces multi-page routing
|
||||
|
||||
**Context:** The current app has bottom nav with routes for tasks, projects,
|
||||
tags, and settings. Requirements specify a single-screen design.
|
||||
|
||||
**Decision:** Remove bottom nav and all routes except `/`, `/settings`,
|
||||
`/auth/*`. The home page owns the full screen layout.
|
||||
|
||||
**Alternatives:** Keep routes but hide nav (rejected — adds routing complexity
|
||||
for no benefit since there's only one functional view).
|
||||
|
||||
**Consequences:** Simpler mental model. Projects/tags pages (stubs) are removed.
|
||||
Task detail route is removed (out of scope per requirements). `tasks/new` is
|
||||
removed (replaced by input bar).
|
||||
|
||||
### ADR-2: Server-side parsing via new `/tasks/parse` endpoint
|
||||
|
||||
**Context:** The frontend needs to accept raw CLI syntax. Parsing is complex
|
||||
(dates, recurrence, relative expressions).
|
||||
|
||||
**Decision:** New `POST /tasks/parse` endpoint. Frontend sends the raw string;
|
||||
backend splits and parses using existing `parseAddArgs()` + `ParseModifier()`.
|
||||
|
||||
**Alternatives:**
|
||||
- Client-side parsing (rejected — would duplicate 500+ lines of Go date/modifier
|
||||
logic in JS, creating a maintenance burden)
|
||||
- Sending pre-split args array (rejected — adds no value; splitting on
|
||||
whitespace is trivial)
|
||||
|
||||
**Consequences:** Clean separation. Frontend is truly a thin shell. Parsing
|
||||
logic stays in one place. Trade-off: requires network round-trip for every add,
|
||||
but this matches the "CLI-passthrough" philosophy.
|
||||
|
||||
### ADR-3: Report-based queries via `?report=` query parameter
|
||||
|
||||
**Context:** The frontend needs to show 11 different task views. Reports involve
|
||||
complex server-side logic (urgency calculation, date filtering, sorting).
|
||||
|
||||
**Decision:** Extend the existing `GET /tasks` endpoint with a `report` query
|
||||
parameter. When present, it uses the report engine instead of raw filter queries.
|
||||
|
||||
**Alternatives:**
|
||||
- Dedicated endpoints per report (`GET /reports/next`) — rejected: adds 11
|
||||
routes with boilerplate
|
||||
- Client-side filtering/sorting — rejected: urgency calculation is complex and
|
||||
should stay server-side
|
||||
|
||||
**Consequences:** Single endpoint serves both raw filter queries (backward
|
||||
compatible) and report-based queries. The report engine already exists and is
|
||||
well-tested.
|
||||
|
||||
### ADR-4: Swipe gesture with angle-based lock-in
|
||||
|
||||
**Context:** Horizontal swipe on a vertical scroll list has ambiguity. Must not
|
||||
accidentally complete tasks during normal scrolling.
|
||||
|
||||
**Decision:** Use angle detection. If initial movement angle is < ~27° from
|
||||
horizontal (|deltaX| > |deltaY| * 2) after 10px of horizontal movement, lock
|
||||
into swipe mode. Otherwise, allow scroll. Threshold of 100px to trigger
|
||||
completion.
|
||||
|
||||
**Alternatives:**
|
||||
- Time-based delay (rejected — feels sluggish)
|
||||
- Long-press then swipe (rejected — adds friction)
|
||||
- Only allow checkbox completion (rejected — swipe is a MUST requirement)
|
||||
|
||||
**Consequences:** Natural gesture feel. Small risk of mis-detection near the
|
||||
boundary angle, mitigated by the 100px release threshold (mid-swipe cancel is
|
||||
always safe).
|
||||
|
||||
### ADR-5: Pure CSS keyboard-aware layout with `dvh` units
|
||||
|
||||
**Context:** Mobile keyboards push fixed elements off-screen. The input bar must
|
||||
stay visible above the keyboard.
|
||||
|
||||
**Decision:** Use `height: 100dvh` on the app shell with a flexbox column
|
||||
layout (`header | task-list flex:1 | input-bar`). The `dvh` (dynamic viewport
|
||||
height) unit automatically accounts for the virtual keyboard — when the keyboard
|
||||
opens, `100dvh` shrinks, the flex container reflows, and the input bar stays
|
||||
anchored at the bottom. No JavaScript needed.
|
||||
|
||||
**Alternatives:**
|
||||
- `visualViewport` API with JS resize listener (rejected — unnecessary
|
||||
complexity; `dvh` solves this in CSS)
|
||||
- `position: fixed; bottom: 0` (rejected — doesn't reflow the scroll area)
|
||||
- `interactive-widget=resizes-content` meta tag (viable fallback, but `dvh`
|
||||
alone is sufficient on target browsers)
|
||||
|
||||
**Consequences:** Zero JS for keyboard handling. Depends on `dvh` support, which
|
||||
is available in all target browsers (Chrome 108+, Firefox 108+). Not an issue
|
||||
since this is an internal product with controlled browser versions.
|
||||
|
||||
### ADR-6: Blur-delay for property pill interaction
|
||||
|
||||
**Context:** Tapping a pill above the input causes the input to blur (since the
|
||||
pill is a separate element), which would hide the pills before the tap registers.
|
||||
|
||||
**Decision:** Add a ~150ms delay before hiding pills on blur. If a pill tap
|
||||
occurs during that window, cancel the hide and re-focus the input.
|
||||
|
||||
**Alternatives:**
|
||||
- Use `mousedown`/`touchstart` with `preventDefault` on pills to prevent blur
|
||||
(simpler, but may have side effects on mobile)
|
||||
- Keep pills always visible (rejected — requirement F-23 says pills disappear on
|
||||
blur)
|
||||
|
||||
**Consequences:** Slight visual delay before pills disappear, but the
|
||||
interaction is reliable. The `preventDefault` approach could be used as a
|
||||
refinement if the delay feels wrong.
|
||||
|
||||
---
|
||||
|
||||
## 6. File & Module Structure
|
||||
|
||||
### New / Modified Files
|
||||
|
||||
```
|
||||
src/
|
||||
├── lib/
|
||||
│ ├── api/
|
||||
│ │ └── endpoints.js # MODIFY: add tasks.parse(), update tasks.list()
|
||||
│ ├── components/
|
||||
│ │ ├── Header.svelte # NEW: report name + picker trigger + gear
|
||||
│ │ ├── ReportPicker.svelte # NEW: dropdown with grouped reports
|
||||
│ │ ├── InputBar.svelte # NEW: CLI input + submit + error display
|
||||
│ │ ├── PropertyPills.svelte # NEW: horizontal pill row
|
||||
│ │ ├── SwipeAction.svelte # NEW: swipe gesture wrapper
|
||||
│ │ ├── TaskList.svelte # MODIFY: empty states, remove loading card
|
||||
│ │ ├── TaskItem.svelte # MODIFY: layout tweaks, remove click nav
|
||||
│ │ └── ui/ # KEEP: existing UI primitives
|
||||
│ ├── stores/
|
||||
│ │ └── tasks.js # MODIFY: add loadReport(), parseAndCreate()
|
||||
│ └── mock/
|
||||
│ └── tasks.js # KEEP: existing mock data
|
||||
├── routes/
|
||||
│ ├── +layout.svelte # MODIFY: remove BottomNav
|
||||
│ ├── +page.svelte # REWRITE: single-screen orchestrator
|
||||
│ ├── settings/+page.svelte # KEEP: unchanged
|
||||
│ ├── auth/
|
||||
│ │ ├── login/+page.svelte # KEEP: unchanged
|
||||
│ │ └── callback/+page.svelte # KEEP: unchanged
|
||||
│ ├── tasks/ # DELETE: entire directory
|
||||
│ ├── projects/+page.svelte # DELETE
|
||||
│ └── tags/+page.svelte # DELETE
|
||||
└── app.css # MODIFY: add swipe/pill/input-bar styles
|
||||
```
|
||||
|
||||
### Deleted Files
|
||||
|
||||
- `src/routes/tasks/new/+page.svelte` — replaced by InputBar
|
||||
- `src/routes/tasks/[uuid]/+page.svelte` — out of scope
|
||||
- `src/routes/projects/+page.svelte` — out of scope
|
||||
- `src/routes/tags/+page.svelte` — out of scope
|
||||
- `src/lib/components/BottomNav.svelte` — replaced by InputBar
|
||||
|
||||
### Backend Changes
|
||||
|
||||
```
|
||||
opal-task/
|
||||
└── internal/
|
||||
└── api/
|
||||
├── server.go # MODIFY: add POST /tasks/parse route
|
||||
├── handlers.go # MODIFY: add HandleParseTask(),
|
||||
│ # update HandleListTasks() for ?report=
|
||||
└── (no new files needed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Integration Points
|
||||
|
||||
### Frontend → Backend API
|
||||
|
||||
| Action | Endpoint | When |
|
||||
|---------------------|-------------------------------|----------------------------|
|
||||
| Load tasks | `GET /tasks?report={name}` | On mount, report change |
|
||||
| Create task | `POST /tasks/parse` | Input bar submit |
|
||||
| Complete task | `POST /tasks/{uuid}/complete` | Swipe or checkbox |
|
||||
| Auth (existing) | `GET /auth/login`, etc. | Login flow (unchanged) |
|
||||
| Sync (existing) | `POST /sync/*` | Background sync (unchanged)|
|
||||
|
||||
### Mock Mode
|
||||
|
||||
Mock mode (`VITE_MOCK_MODE=true`) continues to work:
|
||||
- `tasks.loadReport(name)` returns mock data filtered client-side by status
|
||||
(good enough for development)
|
||||
- `tasks.parseAndCreate(input)` does a naive client-side split (description =
|
||||
non-modifier words, ignore modifiers) and adds to mock array
|
||||
- No backend needed for development
|
||||
|
||||
### PWA / Service Worker
|
||||
|
||||
No changes to PWA config. The new endpoint (`/tasks/parse`) is covered by the
|
||||
existing Workbox runtime cache pattern (`/api/*` with NetworkFirst strategy).
|
||||
|
||||
---
|
||||
|
||||
## 8. Deferred Items
|
||||
|
||||
Items explicitly not included in this design, to be addressed in future
|
||||
iterations:
|
||||
|
||||
| Req ID | Description | Priority | Notes |
|
||||
|--------|-------------|----------|-------|
|
||||
| F-39 | Pull-to-refresh to reload task list | COULD | Low priority; report switching already reloads. Can be added later with `overscroll-behavior` + a small JS handler. |
|
||||
Reference in New Issue
Block a user