A single, prioritized read of every surface in the task-management app where the experience reads as half-finished, inconsistent, or inaccessible. Built off a three-agent parallel pass — visual design, accessibility, and Nielsen-style interaction heuristics — synthesized into the list below.
The app's primitives are solid — inline editing, optimistic mutations, the Command Palette, the new 6-tab drawer — but the spine is held together by hover-only affordances, dead controls shipped to production (the notifications bell is the most visible offender), and view-to-view inconsistency in how the same action is performed. The single highest-leverage move is killing the false-positive controls (dead bell, archive without preview, status-flip with no undo) before adding any new feature.
Ranked by leverage (visibility × frequency × severity), not by effort. Shipping the first five would change Dakota's read of the app in a sprint.
onClick, no popover, but always renders a red dot. The single most visible "fake control" in the app./tasks-app/teams/<key>; the left-rail Areas section links to /tasks-app/areas/<key>. Same affordance, two different destinations. Pure routing bug.BOARD_TRANSITIONS lets you drag a card into cancelled, but BOARD_COLUMNS doesn't render a cancelled column — so the card disappears with no visual destination.<aside> with no role="dialog", no aria-modal, no focus trap (Tab escapes behind the scrim), and only a fade animation. The codebase already has useFocusTrap — it's applied to NewProjectModal but not here.showUndoToast on archive/delete. The pattern stops there. Status-to-completed is one click in the drawer with no recovery; Gantt drag persists silently; pin-unpin removes a project from the rail with no path back without scrolling.PointerSensor only. There is no keyboard path to change a task's status from the board or its due date from the Gantt — the views are read-only for keyboard users.opacity-0 group-hover:opacity-100. Sighted keyboard users have no idea what's focused. One-line CSS fix (group-focus-within:).| ID | Surface | Finding | Effort | Suggested fix | Evidence |
|---|---|---|---|---|---|
| C-01 | TopBar · Notifications | Bell is a dead button — hardcoded aria-label="Notifications (3 unread)", no onClick, no popover, but the red dot is always visible. Three agents flagged this independently. |
M | Wire to useNotifications, add popover with grouped unread + "mark all read"; hide dot when count = 0. |
components/layout/TopBar.tsx:91–101 |
| C-02 | AreasList ↔ left rail | Area cards link to /tasks-app/teams/<key>, but the left rail's Areas entries link to /tasks-app/areas/<key>. The same area opens two different pages depending on entry point. |
XS | Pick one route (AreaSpace is the canonical destination) and update AreasList to match. | pages/AreasList.tsx:101 vs components/layout/TasksAppShell.tsx:278–284 |
| C-03 | BoardView | BOARD_TRANSITIONS permits dragging into cancelled, but BOARD_COLUMNS omits it — cards dragged there disappear from the board with no visible destination. |
XS | Either render a Cancelled column or strip cancelled out of the allowed transitions in the board context. |
components/tasks/BoardView.tsx:45–66 |
| C-04 | TaskDrawer | Drawer is <aside> with no role="dialog", no aria-modal, no focus trap, no aria-labelledby. Tab escapes behind the scrim into the table. |
S | Apply the existing useFocusTrap hook (already used by NewProjectModal), add modal ARIA attributes, label by the task title. |
components/tasks/TaskDrawer.tsx:269–275; compare components/projects/NewProjectModal.tsx:62 |
| C-05 | BoardView / GanttView | Both views use only PointerSensor for drag-and-drop. No KeyboardSensor. There is no keyboard alternative to change status from the board or due date from the gantt — both views are read-only for keyboard users (WCAG 2.1.1). |
M | Add KeyboardSensor from @dnd-kit; bind Enter/Space + arrows on focused card to move between columns / shift dates. |
components/tasks/BoardView.tsx:140, :256–258 · components/tasks/GanttView.tsx:65–84 |
| C-06 | TaskTable inline editors | Status / priority / size / due popover triggers are 20×20px (h-5 w-5) — below WCAG 2.2 AA 24×24 target minimum. Inline editing is the table's primary affordance and it fails the spec on touch. |
XS | Bump trigger size to h-6 w-6 minimum and increase tap-padding around inline editors. |
components/tasks/TaskTable.tsx:1019–1028, 1046–1059, 1080–1089 |
| C-07 | TaskNew · validation | Required fields silently disable Continue with no required, no aria-required, no aria-invalid, no error message via aria-describedby. Error landing div has no role="alert". Submission state changes don't announce. |
S | Add required + aria-required on title/description, surface validation messages inline with aria-describedby, wrap submit error in role="alert". |
pages/TaskNew.tsx:215–216, 686–696, 910–914 |
| C-08 | InboxList · sprint rows | Sprint notifications double-navigate: the wrapper is a <Link> AND the onClick runs window.location.href = …, racing the SPA route swap with a full reload. |
XS | Drop the window.location assignment, let the <Link> handle routing. |
components/home/InboxList.tsx:84–98, 204–220 |
| C-09 | TaskTable · inline rename | Double-click renames the task title — there is no keyboard equivalent (no F2, no Enter-on-focused-button, no aria hint). Keyboard users literally cannot rename inline. | S | Make rename an Enter or F2 on the focused title button; surface the gesture in a tooltip. | components/tasks/TaskTable.tsx:1263–1281 |
| C-10 | TaskTable / BoardView / GanttView / CalendarView | Status dots, "—" placeholders, "+N more", "Empty" microcopy all use text-gray-300/text-gray-400 on white — 1.5–1.6:1 contrast. Fails WCAG 1.4.3 (4.5:1 normal text). |
XS | Bump muted text to text-gray-500 minimum (4.6:1). Replace 10px text-2xs with 12px text-xs wherever readability matters. |
components/tasks/TaskTable.tsx:110, 870, 912, 1004 · BoardView.tsx:237 · CalendarView.tsx:152–153 |
| C-11 | TaskDrawer / TaskTable mutations | Optimistic status / priority / size / assignee / due changes fire with no aria-live announcement, no error toast on failure, no rollback UI. Screen-reader users hear no confirmation. Sighted users get no recovery if the mutation 4xx's. |
M | Add a single role="status" aria-live="polite" region per drawer/table; toast on mutation error with retry. |
components/tasks/TaskDrawer.tsx:388–405 + every popover onPick |
| ID | Surface | Finding | Effort | Suggested fix | Evidence |
|---|---|---|---|---|---|
| H-01 | Cross-surface · tabs | Four different tab treatments live in the app: underline on Home, solid pill on MyWork, ghost pill on TeamSpace, underline on Standup, pill on AreaSpace. No canonical Tab primitive. | M | Extract a single <Tabs> primitive (underline-style preferred for read-heavy surfaces) and adopt everywhere. |
pages/TasksHome.tsx:158–162 · MyWork.tsx:314–336 · StandupFeed.tsx:104–127 · AreaSpace.tsx:127–147 |
| H-02 | BoardView · keyboard | Drag is the only path to change status from the board. No keyboard equivalent. (Companion to C-05 for the visual UX side: cards have onClick opening the drawer but no keyboard hint for status moves.) |
M | Add KeyboardSensor + a small "press Space to grab" hint on focused cards. |
components/tasks/BoardView.tsx:140, 256–258, 282–291 |
| H-03 | TaskTable · hover action rail | Row actions (Add subtask / Copy link / Archive / Delete) live behind opacity-0 group-hover:opacity-100 — invisible to focus-within. Keyboard users can't see what's focused. |
XS | Add group-focus-within:opacity-100 alongside the hover variant. |
components/tasks/TaskTable.tsx:1120–1178 |
| H-04 | CalendarView | Calendar is read-only — no drag-to-reschedule, no in-grid create, no day-click to add. Gantt has drag-to-reschedule. Same tasks, opposite interaction models. | L | Bring drag-to-day from Gantt's mechanism into CalendarView; add a "+ add task on this day" affordance. | components/tasks/CalendarView.tsx:140–160 |
| H-05 | Empty states (cross-surface) | No empty-state primitive. TasksApp shows a dashed-border one-liner; BoardView columns say "Empty" in 10px gray; MyDayBuckets renders nothing when clear; SprintsList "Plan a sprint" CTA looks identical to a populated card; RequestCenter empty-category cards are dead ends. | M | Build <EmptyState icon copy cta /> primitive, apply across surfaces with surface-appropriate copy and a primary CTA each time. |
pages/TasksApp.tsx:639–650 · BoardView.tsx:236–239 · SprintsList.tsx:252–269 · RequestCenter.tsx:194–197 |
| H-06 | CalendarView · type size | Task chips inside day cells are text-2xs (~10px). Only ~3 fit before "+N more" hides the rest. Cell height is min-h-[6.5rem] — chips are nearly unreadable. |
S | Bump chip type to 12px minimum; raise cell min-height to fit 5–6 chips; collapse via "+N" only beyond that. | components/tasks/CalendarView.tsx:121, 140–150 |
| H-07 | GanttView · readability | Bar labels are text-2xs white on colored bars at 24px row height — unreadable on short bars, no hover card with full task summary (only a title attr). |
S | Raise row to 32px; add HoverCard with title + assignee + due + status. | components/tasks/GanttView.tsx:23, 445–457 |
| H-08 | GanttView · color tokens | Bar colors are hardcoded hex (#2BA188, #6366F1, …) instead of the Tailwind sga-* tokens used elsewhere. Brand changes won't propagate. |
XS | Map domain → token color via the same lookup BoardView and TaskTable use. | components/tasks/GanttView.tsx:27–38 |
| H-09 | TaskDrawer · slide animation | Drawer uses animate-fade-in only — no slide-from-right transform. Pattern set by Linear/Asana is a transform-based slide. |
XS | Replace fade with translate-x-full → translate-x-0 at 200ms ease-out (and honor prefers-reduced-motion). |
components/tasks/TaskDrawer.tsx:269–275 |
| H-10 | TaskDrawer · tab nav | Tab bar uses overflow-x-auto at 640px drawer width with no scroll affordance or fade. Footer hint advertises "⌘1-5 tabs" but six tabs exist and TAB_ORDER is hardcoded to five (Comments is unreachable by shortcut). |
XS | Fit all tabs in width (icon-only on narrow), update TAB_ORDER to include comments, update footer hint. |
components/tasks/TaskDrawer.tsx:154–157, 486–534, 564–566 |
| H-11 | TaskDrawer · Activity tab | Activity items render as flat bordered cards with timestamps — no actor avatars, no verb coloring, no before/after diff for field changes. Reads as a log dump, not a story. | M | Avatar + verb-colored leading bar + before/after chips on fields_updated; use <time datetime> for the timestamp. |
components/tasks/TaskDrawer.tsx:983–1011 |
| H-12 | Cross-surface · "+ New project" | Same job, three shapes: drawer-inline ProjectPickerWithCreate, ProjectsList modal NewProjectModal, deep-link /tasks-app/new?domain=manual. No canonical creator. |
M | Pick one (the inline form pattern), use it everywhere; the modal is for advanced fields only. | components/tasks/TaskDrawer.tsx:2130–2262 · pages/ProjectsList.tsx:247–254 · SprintsList.tsx:206–208 |
| H-13 | TaskDrawer · status select | Status <select> repeats the current value as the first option without marking it "current" — the picker reads "In Progress / → In Progress / → Draft Ready / …" with visual duplication. Sibling priority picker marks the current pick correctly. |
XS | Mark current option with a checkmark prefix or remove it from the list of transitions. | components/tasks/TaskDrawer.tsx:388–405 vs :613 |
| H-14 | ProjectDetail · archive | Archive uses raw window.confirm("Archive ...? Tasks remain attached.") with no preview of open-task count, no undo. Destructive on a project that could carry hundreds of open tasks. |
S | Replace native confirm with an in-app modal showing task counts + radio for "leave attached / move to another project / archive all"; add UndoToast. | pages/ProjectDetail.tsx:163–169 |
| H-15 | ProjectDetail · bulk select | Bulk-select state (selectedIds, toggleSelect) is wired into TaskTable but no BulkBar ever renders. Selecting rows highlights them with nowhere to go. |
S | Render the same BulkBar that TasksApp uses, or drop the selection state. | pages/ProjectDetail.tsx:82, 269 vs pages/TasksApp.tsx:1733 |
| H-16 | GanttView · drag error path | Drag-to-reschedule fires fieldsMut.mutateAsync with no try/catch, no error toast, no undo. BoardView wraps the equivalent in a toast. |
XS | Match BoardView's pattern: wrap in try/catch, surface a toast on failure, attach UndoToast on success. | components/tasks/GanttView.tsx:69–84 vs BoardView.tsx:170–183 |
| H-17 | TasksAppShell · rail | Pin/unpin star fires togglePin with no confirmation and no undo. A project drops out of the rail instantly — losing the user's place if they're currently navigated to it. |
XS | Attach UndoToast to togglePin; or, if the user is on the project right now, keep the row pinned-with-strikethrough until they navigate away. |
components/layout/TasksAppShell.tsx:553–561 |
| H-18 | TaskTable · inline editor popover | Popovers use raw absolute positioning with no portal. At the bottom rows of a long table the popover clips below the viewport — no flip-up behavior. | S | Adopt floating-ui (Popper) for auto-flip + overflow handling; portal to body. |
components/tasks/TaskTable.tsx:814–822, 873–887 |
| H-19 | TasksHome · loading | Whole dashboard loading state is a single tiny centered "Loading your day…" spinner — no skeleton matching KPI strip + buckets + inbox shape. | S | Skeleton blocks matching KPI strip, MyDay buckets, Inbox so layout settles before data lands. | pages/TasksHome.tsx:123–127 |
| ID | Surface | Finding | Effort | Suggested fix | Evidence |
|---|---|---|---|---|---|
| M-01 | Whole app · motion | No prefers-reduced-motion handling. animate-fade-in, animate-spin, drag transforms, board hover all fire unconditionally. |
XS | Add a global media-query rule that disables non-essential transforms and animations; honor it from the Tailwind animation utilities. | styles/globals.css (zero matches for prefers-reduced-motion) |
| M-02 | TaskDrawer · backdrop | Click on the outer scrim closes the drawer via onClick on a non-interactive <div>. Errant clicks dismiss unsaved inline-edit state with no confirmation. |
XS | Treat backdrop clicks as a "request close" — check for dirty inline-edit state and prompt before closing. | components/tasks/TaskDrawer.tsx:270 |
| M-03 | TaskDrawer · footer hint | Keyboard-shortcut hint uses a raw glyph string ("⌘1-5 tabs · J/K subtasks · ⌘↵ comment · Esc close") with no <kbd> styling. |
XS | Render keys inside <kbd> chips with a subtle background and border. |
components/tasks/TaskDrawer.tsx:564–566 |
| M-04 | TaskTable · domain chips | Domain chips forced to a neutral gray here, but BoardView / CalendarView / GanttView still color domain. Inconsistent within the same task surface set. | XS | Adopt one canonical domain rendering. Either neutral everywhere or color everywhere. | components/tasks/TaskTable.tsx:95–107 vs CalendarView.tsx:146,174 · GanttView.tsx:27–38 |
| M-05 | BoardView · count pills | Column header counts use a flat gray pill in every column — Submitted, In Progress, Approved all look numerically identical with no semantic weight. | XS | Tint count pill by column accent (gray submitted, sga in_progress, emerald approved). | components/tasks/BoardView.tsx:228–230 |
| M-06 | CalendarView · header | Prev/Next arrows with no month-name click-to-jump, no week/day toggle, no in-grid create. Feels like a v0 month view. | M | Add month/year picker on title click; week/day view toggle; click-day to start a new task. | components/tasks/CalendarView.tsx:67–101, 118–160 |
| M-07 | CommandPalette · ranking | Uses substring includes; no fuzzy ranking, no recent items, no keyboard shortcuts shown next to quick actions, button-inside-listbox ARIA violation. |
M | Recents section, fuzzy ranker (cmdk style), inline shortcut hints, fix the role="option"/role="button" nesting. |
components/CommandPalette.tsx:134–227, 290–294, 333–348 |
| M-08 | TasksApp · color palette | Seven domain tiles use seven different accent colors creating chip-rainbow visual noise on the rail — color should be reserved for status/severity per the team's own contract. | XS | Single dominant accent for active tile, neutral for inactive. | pages/TasksApp.tsx:165–175, 396–411 |
| M-09 | TasksDashboard · charts | Four charts use four hardcoded color palettes (#10b981, #6366f1, #f43f5e) instead of sga-* tokens. Empty state is "No active or planned sprints" inside a dashed border with no CTA. |
S | Map series colors to tokens; empty state with "Plan a sprint" CTA. | pages/TasksDashboard.tsx:79–80, 113–119, 150–152, 164–171 |
| M-10 | TaskNew · labels | Field labels are <label> with no htmlFor; inputs have no matching id. Click-to-focus from label is broken even though aria-label gives SR users a name. |
XS | Wire id + htmlFor through the Field primitive. |
pages/TaskNew.tsx:946–961 |
| M-11 | TaskNew · Granola CTA glyph | "Pull from Granola meeting" secondary CTA uses literal emoji (📥) instead of a Lucide icon — violates the icon system the codebase otherwise follows. | XS | Lucide Mic / Download / Sparkles. |
pages/TaskNew.tsx:321 · pages/MyWork.tsx:360 |
| M-12 | TaskNew · Back-while-submitting | Step-3 Back button stays enabled during submit-in-flight; only the submit button is disabled. Clicking Back loses step-3 review with no way to cancel the in-flight create. | XS | Disable Back while createMut.isPending; or convert the navigation to an explicit "cancel and edit" with rollback. |
pages/TaskNew.tsx:394–402, 414–427 |
| M-13 | ProjectsList · scope flip | Scope flips from "all" to "mine" only after role.ready lands — visible content jump on first paint. |
XS | Render a small skeleton list until role.ready, then mount. |
pages/ProjectsList.tsx:77–87 |
| M-14 | SprintsList | "Plan a sprint" empty-state CTA uses an oversized dashed-border row that looks identical to a populated SprintCard. | XS | Make the empty CTA visually distinct: illustration, lighter background, smaller card. | pages/SprintsList.tsx:252–269 vs 298–353 |
| M-15 | SprintDetail · capacity bar | Two stacked colored layers (bg-emerald-500 on bg-sga-500/60) with no legend — the "done vs in-flight" encoding is non-obvious. |
XS | Legend chip pair beside the bar with matching colors. | pages/SprintDetail.tsx:225–266 |
| M-16 | InboxList · filter chips | Eight filter chips (All / @me / Comments / Assigned / Due / Watched / Sprints + Unread toggle) plus per-tone item icons turn the inbox into a multicolored grid. Unread toggle is a button with aria-current instead of aria-pressed. |
S | Reduce to 3–4 primary chips, neutralize item-tone variation, switch unread toggle to aria-pressed. |
components/home/InboxList.tsx:26–38, 147–150, 274–296 |
| M-17 | ProjectDetail · Activity | ProjectDetail Activity is a flat bordered list (no avatar, no icon, no time grouping) while TaskDrawer Activity does roll-ups and humanized actions. Two activity surfaces, two polish levels. | S | Extract a shared Activity primitive with bucketing, avatars, action coloring; adopt in both surfaces. | pages/ProjectDetail.tsx:1061–1080 vs components/tasks/TaskDrawer.tsx:973–1011 |
| ID | Surface | Finding | Effort | Suggested fix | Evidence |
|---|---|---|---|---|---|
| L-01 | TasksHome · Refresh button | Bare in-page "Refresh" button floats above the tabs with no surrounding chrome — orphan control. | XS | Move into PageHeader right slot or replace with auto-refresh indicator. | pages/TasksHome.tsx:131–142 |
| L-02 | RequestCenter · empty categories | "No forms published in this category yet" sits on otherwise-empty cards with no CTA. | XS | Add a faint "Propose a form for this category" link as a secondary CTA. | pages/RequestCenter.tsx:194–197 |
| L-03 | MentionsSection | 39-line component: heading + capped list of 8 TaskListRow items. No "view all," no time grouping, no avatars. Undershoots InboxList's polish next to it on Home. | S | Time-bucketed groups, mentioner avatar, "view all mentions" link to a filtered MyWork view. | components/home/MentionsSection.tsx:1–39 |
| L-04 | StandupFeed · empty state | Generic dashed-border one-liner with no first-post template, no link to "what a good standup looks like." | XS | Tutorial empty state: example post mockup + link to standup norms. | pages/StandupFeed.tsx:244–249 |
| L-05 | TasksAppShell · skip-link | No skip-link to bypass the rail and reach <main>. Rail collapse buttons use aria-expanded but not aria-controls. |
XS | Add a visually-hidden "Skip to main content" link; wire aria-controls on each rail collapse. |
components/layout/TasksAppShell.tsx:426–744 |
| L-06 | TaskDrawer · subtask focus ring | Subtask link uses focus:bg-sga-50 focus:text-sga-700 focus:ring-1 — light blue ring on light blue background, ~2.8:1 contrast. |
XS | Switch focus ring to darker accent (ring-sga-500) for 3:1 minimum. |
components/tasks/TaskDrawer.tsx:1151–1156 |
| L-07 | CommandPalette · footer | Shortcut hints at 10px text-gray-500 on bg-gray-50 = 4.43:1, just under AA 4.5:1. |
XS | Use 12px minimum, text-gray-600. |
components/CommandPalette.tsx:329–332, 370–387 |
| L-08 | TasksAppShell · Focus toggle label | Toggle uses aria-pressed (good) but visible label "Focus mode" never changes — SR users hear the unchanging label and pressed state without context about what pressed means. |
XS | Swap visible label between "Focus mode" / "Full workspace" or add a visually-hidden state label. | components/layout/TasksAppShell.tsx:713–731 |
If shipped in roughly this order, each step removes the strongest source of the "not baked" feeling without depending on the next.
| Sprint | Goal | IDs |
|---|---|---|
| Sprint 1 — Kill the fakes | Remove every dead or half-wired control. Fix the routing collisions. Tighten the destructive paths. | C-01 · C-02 · C-03 · C-08 · H-14 · H-15 · H-17 |
| Sprint 2 — Drawer + table parity | Drawer becomes a real dialog. Table inline editors meet a11y minimums. Status mutations announce and recover. | C-04 · C-06 · C-09 · C-10 · C-11 · H-03 · H-09 · H-10 · H-13 · H-18 |
| Sprint 3 — Board + Gantt + Calendar | Keyboard parity. Calendar promoted to editor. Empty-state primitive everywhere. | C-05 · H-02 · H-04 · H-05 · H-06 · H-07 · H-08 · H-16 |
| Sprint 4 — Consistency pass | One Tabs primitive. One "+ New project" creator. One Activity primitive. Skeletons + reduced-motion. | C-07 · H-01 · H-11 · H-12 · H-19 · M-01 · M-04 · M-17 |