SGA Dental Partners·Growth · Internal Tools

SGA Tasks UX / UI audit

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.

Date: 2026-05-26 Scope: 22 surfaces under /tasks-app + adjacent inbox / mentions / activity Prepared by: SGA Dental Partners Growth Team

Executive summary

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.

11Critical
19High
17Medium
8Low
Four cross-cutting patterns account for most of the "doesn't feel baked" perception:
  1. Dead or half-wired controls shipped to production (TopBar bell, ProjectDetail bulk-select with no bar, double-navigating sprint inbox rows).
  2. Destructive actions without undo or preview (project archive, unpin, status → terminal, Gantt drag-reschedule).
  3. Same job, different affordance on every surface (status flip, "+ New project", tab style, calendar vs. gantt interaction density).
  4. Hover-only or pointer-only for primary actions (table action rail, Board drag, Gantt drag — none have keyboard or focus-within equivalents).

Top 10 — fix these to move the perception of polish the most

Ranked by leverage (visibility × frequency × severity), not by effort. Shipping the first five would change Dakota's read of the app in a sprint.

  1. All three audit agents flagged it independently. Hardcoded "3 unread" aria-label, no onClick, no popover, but always renders a red dot. The single most visible "fake control" in the app.
    Critical
  2. AreasList cards link to /tasks-app/teams/<key>; the left-rail Areas section links to /tasks-app/areas/<key>. Same affordance, two different destinations. Pure routing bug.
    Critical
  3. 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.
    Critical
  4. Currently <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.
    Critical
  5. Tasks get 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.
    High
  6. Four different tab treatments (underline on Home, pill on MyWork, pill on TeamSpace, underline on Standup). Three different "+ New project" affordances. Two different status-change interaction models (inline cell vs. drawer select).
    High
  7. Both views use 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.
    High
  8. TaskTable's row actions live behind opacity-0 group-hover:opacity-100. Sighted keyboard users have no idea what's focused. One-line CSS fix (group-focus-within:).
    High
  9. Gantt has drag-to-reschedule. Calendar of the same tasks doesn't. Day cells are inert and chips are 10px text — the view reads as a placeholder, not a tool.
    High
  10. Currently a dashed-border one-liner on TasksApp, "Empty" in 10px gray on BoardView columns, a silent collapse when MyDay buckets clear, an identical "Plan a sprint" card on SprintsList that looks like a populated card. Empty states need their own primitive.
    High

Critical — broken or blocking affordances · 11 findings

IDSurfaceFindingEffortSuggested fixEvidence
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

High — visibly unfinished · 19 findings

IDSurfaceFindingEffortSuggested fixEvidence
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

Medium — polish, consistency, less-load-bearing a11y · 17 findings

IDSurfaceFindingEffortSuggested fixEvidence
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

Low — nice-to-have polish · 8 findings

IDSurfaceFindingEffortSuggested fixEvidence
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

Recommended sequencing

If shipped in roughly this order, each step removes the strongest source of the "not baked" feeling without depending on the next.

SprintGoalIDs
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
Prepared by SGA Dental Partners Growth Team · Confidential · 2026-05-26