Files
workspace/docs/2026-05-08-task-ux-evolution-design.md
Roberto 310410350f docs: add Task UX Evolution design spec
Validated design for task list refactor: shadcn Table view with shared
pagination, right-side detail Sheet with attachments, redesigned
quick-capture create/edit dialog, project detail page reusing the same
list view.
2026-05-08 12:26:28 +02:00

15 KiB
Raw Blame History

Task UX Evolution — Design

Date: 2026-05-08 Scope: adiuvAI desktop app (renderer + main process) Status: Approved by user, ready for implementation plan

Goal

Evolve the task management UX:

  1. Replace the list-of-cards view with a paginated shadcn Table, keep the card grid as an alternative view, share pagination across both.
  2. Replace TaskDetailDialog with a right-side Sheet (sticky header + scrolling body + sticky composer), add attachments support.
  3. Redesign the create dialog as a quick-capture form with pill-style property controls. Edit dialog reuses the same shell.
  4. Apply the same task list view inside the project detail page, scoped to that project.

Single spec, single implementation plan. All four subsystems ship together.

Non-goals

  • AI-driven estimate generation (column added now, populated by a future agent).
  • Comment attachments (composer has no attach icon).
  • Per-column header sorting in the table (existing Order by Select stays).
  • Reporter / Tags fields (image reference includes them but spec excludes).

1. Architecture & shared state

A new TaskListView component owns the task list rendering for both the Tasks page and the Project detail page. It encapsulates the toolbar, the table or grid body, and the pager. The page consuming it passes a task array plus an optional hideProjectColumn flag.

Persisted state (localStorage):

  • tasksViewMode: 'list' | 'grid' (already exists, kept).
  • tasksPageSize: 10 | 25 | 50 | 100 — default 25.

Page index is component-local and resets per route entry. It also resets when the search, status filter, or Order by changes.

Pagination scope: Tasks page and Project page each maintain their own page state. Toggling list ↔ grid within a page preserves the current page.

Why client-side slicing: task list is already fully loaded via trpc.tasks.list. No backend pagination required at this scale.

2. Database schema

Two changes to src/main/db/schema.ts:

// tasks: add column
estimate: integer('estimate'),  // minutes, nullable

// new table
export const taskAttachments = sqliteTable('task_attachments', {
  id: text('id').primaryKey().$defaultFn(() => randomUUID()),
  taskId: text('task_id').notNull(),
  filename: text('filename').notNull(),
  mimeType: text('mime_type'),
  sizeBytes: integer('size_bytes').notNull(),
  storedPath: text('stored_path').notNull(),  // relative to userData/attachments
  createdAt: integer('created_at').notNull(),
});

Migration generated with drizzle-kit generate. Per project convention, no foreign key constraint — cascade is handled in the tasks.delete tRPC procedure (delete attachment files + rows before deleting the task).

3. Attachments — file storage and IPC

Storage path: app.getPath('userData') / attachments / <taskId> / <uuid>-<sanitizedFilename>.

Sanitization: strip path separators, control characters, leading dots; cap filename at 200 chars.

Limits:

  • Soft cap 50 MB per file. Larger files trigger a warning toast and are not uploaded.
  • No per-task total cap.

New tRPC sub-router taskAttachments (in src/main/router/index.ts):

Procedure Input Behavior
list { taskId } Returns attachment rows for the task.
pick {} Main: dialog.showOpenDialog({ properties: ['openFile', 'multiSelections'] }). Returns Array<{ path, name, size }>.
create { taskId, sourcePath, filename, sizeBytes, mimeType? } Main: fs.mkdir(userData/attachments/<taskId>, recursive), copy file with new uuid name, insert row.
delete { id } Look up row, fs.unlink(storedPath), delete row.
open { id } shell.openPath(absoluteStoredPath).

Helper module src/main/attachments/storage.ts: path resolution, sanitize, copy, delete. Keeps tRPC procedures thin.

Tasks router updates:

  • tasks.update accepts estimate?: number | null.
  • tasks.delete enumerates taskAttachments for the task and deletes files + rows before deleting the task row.

4. Table view

Component: TaskTable using shadcn Table / TableHeader / TableBody / TableRow / TableCell.

Container styling (translucent card over gradient bg):

bg-card/65  backdrop-blur-xl  border border-border/50  rounded-lg  shadow

The --card token is used (not a hard-coded color), so dark mode works.

Columns:

  1. Task — title, single line, truncate with tooltip.
  2. ProjectClient Project breadcrumb. Client text muted, project text foreground. Hidden when hideProjectColumn is set. Click navigates to the project page.
  3. Priority — existing <PriorityBadge> component (arrow icon + colored text, no pill).
  4. DueformatDueDate(t.dueDate, prefs). Overdue: red text. None: muted .
  5. Assignee<AssigneeStack>: overlapping avatars (max 2 visible), +N chip if more, tooltip listing all. None: muted .

Row interaction:

  • Click row → opens TaskDetailSheet.
  • Right-click / context menu (kept from current TaskRow behavior): Edit, Delete, Change status → submenu (To Do / In Progress / Done with checkmark on current).

Sorting: existing Order by Select in the toolbar remains the only sort control. No per-column header sort.

Empty state: existing <Empty> component spans all columns when the filtered list is empty.

5. Pagination

Component: TaskPager rendered in its own translucent card box below the list/grid (same style tokens as the table card, separate box).

Layout:

┌──────────────────────────────────────────────────────────────────┐
│  Showing 125 of 312 tasks    Rows per page: [25 ▾]              │
│                                   1 2 3 4 5 … 13               │
└──────────────────────────────────────────────────────────────────┘

Behavior:

  • Page-number window: always include first, last, current. Up to 7 buttons total. Ellipsis when the gap is greater than 1.
  • ResizeObserver on the pager → reduce visible buttons on narrow widths (7 → 5 → 3 → just prev/next).
  • Page-size change resets pageIndex to 0.
  • If filters trim the total below pageIndex * pageSize, snap pageIndex to the last valid page.
  • Pager renders for both list and grid views, identically.

6. Detail sheet

Replaces: TaskDetailDialog.tsx → new TaskDetailSheet.tsx using shadcn Sheet, right side, width ~480 px.

Three fixed regions:

┌─────────────────────────────────┐  ← STICKY HEADER
│  Acme  Communications          │     breadcrumb (small, muted)
│  Draft Q2 investor update email │     title (18 px, semibold)
│  [↑ High priority] [● In progress]    chip row
│                          [⋯]    │     overflow menu (Edit, Delete)
├─────────────────────────────────┤
│                                 │  ← SCROLLING BODY
│  ┌─ Properties card ──────────┐ │
│  │ Assignee  | Due            │ │
│  │ Estimate  | Created        │ │
│  │ Files: [chip] [chip] [+Add]│ │
│  └────────────────────────────┘ │
│                                 │
│  Description                    │
│  <body text>                    │
│                                 │
│  ── separator ──                │
│                                 │
│  Comments · 4                   │
│  <comment list, no inner scroll>│
│                                 │
├─────────────────────────────────┤  ← STICKY COMPOSER
│  [👤]  ┌──────────────┐  [↑]   │
│        │ Write comment│         │
│        └──────────────┘         │
└─────────────────────────────────┘

Header chips:

  • Priority: existing <PriorityBadge> (arrow + colored text).
  • Status: pill using existing STATUS_CONFIG colors.
  • Both are clickable → popover to change.

Properties card (translucent inner box, 2-column grid):

  • Assignee, Due, Estimate, Created — each row is small uppercase label + value.
  • Estimate shows muted until the AI agent ships.
  • Files row spans both columns: horizontal chip strip. Each chip: 📎 filename · sizeKB ×. + Add is a dashed pill that triggers taskAttachments.pick.
    • Click chip filename → taskAttachments.open.
    • Click × → confirm + taskAttachments.delete.

Comments: no inner ScrollArea — they scroll with the body.

Composer (sticky bottom): reuses the home / AI input wrapper styling:

rounded-2xl  bg-background/70  backdrop-blur-xl
border border-border/50  shadow-lg  ring-1 ring-border/20
focus-within:shadow-xl  focus-within:border-ring/50

Internally reuses the existing ChatInputBox component via a new 'comment' variant (auto-grow textarea + ArrowUp send button, draft persistence, ⌘ + Enter submit). No attach icon in the composer.

Edit / Delete: moved into the header overflow menu. The previous footer action bar is removed.

7. Create / Edit dialog

Components:

  • TaskFormDialog — shared shell, props: mode: 'create' | 'edit', initial values, onSubmit.
  • NewTaskDialog and EditTaskDialog become thin wrappers (different default values + mutation).

Layout:

┌─ New task ─────────────────── ⌘+Enter to create ─┐
│                                                  │
│  What needs to be done?           ← 22 px input  │
│  Add a description…               ← textarea    │
│                                                  │
│  PROPERTIES                                      │
│  [📁 Project: Acme  Communications]             │
│  [↑ Priority: High]  [● Status: To Do]           │
│  [📅 Due: Apr 30, 2026]                          │
│  [+ Add assignees]   ← dashed (empty)            │
│                                                  │
│ ────────────────────────────────────────────────│
│  [📎] *                  [Cancel] [Create task]  │
└──────────────────────────────────────────────────┘

* 📎 icon-pill is only visible in Edit mode. Attachments are blocked in Create mode (need a taskId); the user saves first, then attaches via the detail sheet or via Edit.

Pill states:

  • Set: bg-card/70, solid border, label + value visible.
  • Empty: dashed border, muted text.

Pill click → field-specific shadcn Popover:

  • Project — Select w/ inline create flow (existing logic preserved: project + client + sub-client).
  • Priority — three-option select (high / medium / low).
  • Status — three-option select.
  • Due — Calendar + optional hour/minute selectors.
  • Assignees — existing Popover (known-assignees list + add-new).

Keyboard: ⌘ / Ctrl + Enter submits. Title autoFocus.

8. Project page integration

File: src/renderer/routes/projects.$projectId.tsx.

The existing tasks tab content is replaced by <TaskListView projectId={...} hideProjectColumn />. The toolbar (status tabs, search, Order by, view toggle, New task button) is identical to the Tasks page. Pagination state is local to this page (separate from the Tasks page state). Clicking a row opens the same TaskDetailSheet.

No changes to other project tabs (overview, notes, timeline).

9. Files

New files:

src/renderer/components/tasks/
  TaskListView.tsx       — shared toolbar + table/grid + pager
  TaskTable.tsx          — shadcn Table renderer
  TaskTableRow.tsx       — single row + context menu
  TaskPager.tsx          — pagination card box
  TaskDetailSheet.tsx    — right-side Sheet replacing TaskDetailDialog
  TaskFormDialog.tsx     — shared shell for create/edit
  TaskAttachmentChip.tsx — file chip
  AssigneeStack.tsx      — overlapping avatars + overflow chip
  StatusBadge.tsx        — status pill (existing STATUS_CONFIG colors)

src/main/
  attachments/storage.ts — path resolution, sanitize, copy, delete helpers

Modified files:

src/renderer/routes/tasks.tsx                       — render <TaskListView>
src/renderer/routes/projects.$projectId.tsx        — tasks tab uses <TaskListView hideProjectColumn>
src/renderer/components/tasks/NewTaskDialog.tsx    — wrapper around TaskFormDialog (mode='create')
src/renderer/components/tasks/EditTaskDialog.tsx   — wrapper around TaskFormDialog (mode='edit')
src/main/db/schema.ts                               — tasks.estimate, taskAttachments
src/main/db/migrations/                             — new generated migration
src/main/router/index.ts                            — taskAttachments router; tasks.update accepts estimate; tasks.delete cascades attachments

Deleted files:

src/renderer/components/tasks/TaskDetailDialog.tsx  — replaced by TaskDetailSheet
src/renderer/components/tasks/TaskRow.tsx           — replaced by TaskTableRow (TaskCard kept for grid)

10. i18n keys

New keys in the tasks.* namespace, added to all five language files (en, it, es, fr, de):

tasks.colTask, tasks.colProject, tasks.colPriority, tasks.colDue, tasks.colAssignee
tasks.rowsPerPage, tasks.showingNofM (plural-aware), tasks.noAssignees
tasks.estimate, tasks.attachments, tasks.addFile, tasks.removeFile, tasks.fileTooLarge
tasks.changeStatus, tasks.properties
tasks.confirmDeleteAttachment

11. Out-of-scope follow-ups

  • AI agent that generates estimate for a task.
  • Comment attachments.
  • Per-column sort in the table.
  • Backend pagination (only needed if task counts grow much larger).

12. Implementation order (suggested)

A natural shipping order; the implementation plan can refine:

  1. DB migration (estimate column + taskAttachments table).
  2. Main-process attachments storage module + tRPC sub-router.
  3. TaskDetailSheet with attachment UI (deletes the old dialog).
  4. TaskFormDialog shared shell; rewire NewTaskDialog / EditTaskDialog.
  5. TaskListView, TaskTable, TaskPager, AssigneeStack, StatusBadge. Wire into Tasks page.
  6. Wire TaskListView into project detail page with hideProjectColumn.
  7. i18n keys for all five languages.