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.
15 KiB
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:
- Replace the list-of-cards view with a paginated shadcn Table, keep the card grid as an alternative view, share pagination across both.
- Replace
TaskDetailDialogwith a right-sideSheet(sticky header + scrolling body + sticky composer), add attachments support. - Redesign the create dialog as a quick-capture form with pill-style property controls. Edit dialog reuses the same shell.
- 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 bySelect 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— default25.
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.updateacceptsestimate?: number | null.tasks.deleteenumeratestaskAttachmentsfor 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:
- Task — title, single line, truncate with tooltip.
- Project —
Client › Projectbreadcrumb. Client text muted, project text foreground. Hidden whenhideProjectColumnis set. Click navigates to the project page. - Priority — existing
<PriorityBadge>component (arrow icon + colored text, no pill). - Due —
formatDueDate(t.dueDate, prefs). Overdue: red text. None: muted—. - Assignee —
<AssigneeStack>: overlapping avatars (max 2 visible),+Nchip if more, tooltip listing all. None: muted—.
Row interaction:
- Click row → opens
TaskDetailSheet. - Right-click / context menu (kept from current
TaskRowbehavior): 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 1–25 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.
ResizeObserveron the pager → reduce visible buttons on narrow widths (7 → 5 → 3 → just prev/next).- Page-size change resets
pageIndexto 0. - If filters trim the total below
pageIndex * pageSize, snappageIndexto 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_CONFIGcolors. - 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 ×.+ Addis a dashed pill that triggerstaskAttachments.pick.- Click chip filename →
taskAttachments.open. - Click × → confirm +
taskAttachments.delete.
- Click chip filename →
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.NewTaskDialogandEditTaskDialogbecome 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
estimatefor 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:
- DB migration (estimate column + taskAttachments table).
- Main-process attachments storage module + tRPC sub-router.
TaskDetailSheetwith attachment UI (deletes the old dialog).TaskFormDialogshared shell; rewireNewTaskDialog/EditTaskDialog.TaskListView,TaskTable,TaskPager,AssigneeStack,StatusBadge. Wire into Tasks page.- Wire
TaskListViewinto project detail page withhideProjectColumn. - i18n keys for all five languages.