Skip to content

Dashboard UI/UX Review

Context

A UI/UX review of apps/dashboard (the @tesark/dashboard admin console) — a Vite + React + shadcn/ui (Tailwind v4) SPA. The repo ships a detailed, MUST/SHOULD-graded spec at DESIGN_GUIDELINES.md, so this review measures the actual implementation against that spec and standard UX heuristics, then proposes a prioritized set of changes.

Overall: The foundation is strong — consistent page scaffold (PageHeader/PageContent), defined design tokens in src/index.css, a polished split-screen auth layout, copyable IDs, an onboarding checklist, and confirm-on-destroy. The gaps are mostly consistency drift (the same concept rendered differently per page), missing table affordances (pagination, skeletons, row interaction), and accessibility (hand-rolled modal, silent clipboard, focus management). Several items are direct violations of the project’s own guidelines.

This is a recommendations report only — no code changes have been made.


P0 — Guideline violations / correctness

1. Status badge semantics are inconsistent across pages (violates §6)

The spec: “The same word MUST always map to the same color.” Today “Active” renders three ways:

  • api-keys-page.tsx:232LiveBadge (lime/presence) for an active key
  • conversations-page.tsx:152LiveBadge (lime) for active conversation
  • users-page.tsx:114<Badge variant="secondary"> (gray) for active user

Lime/--tc-presence is reserved for live-right-now states only (§2, §6). A persistent “Active” key/user should be --tc-success green. Fix: Add a single StatusBadge component encoding the §6 table (success / presence / info / warning / danger) and replace the ad-hoc badges. Map: key Active → success; user Active → success; Revoked/Deleted → danger/neutral; Archived → neutral; only genuinely-live signals → presence lime. Files: new src/components/status-badge.tsx; update api-keys-page.tsx, users-page.tsx, conversations-page.tsx, tenants-page.tsx, system-profile-page.tsx.

2. Tables lack skeleton loading and pagination (violates §5)

data-table.tsx:207-215 renders the literal text “Loading…” (spec forbids this — wants 3 shimmering skeleton rows) and has no pagination at all (spec wants a cursor footer “Showing 1–20 of N” + Prev/Next). All rows render unbounded — a UX and perf problem for large tenants. Fix: In DataTable, add (a) skeleton rows while loading, (b) client-side pagination footer (page size ~20). True cursor pagination needs API/lib/api.ts changes (endpoints currently return full arrays), so do client-side now and note server-side as a follow-up.

3. API-key reveal is not a copy-first modal and has no copy button (violates §5, §9)

api-keys-page.tsx:176-183 shows the one-time plaintext key in an inline muted box with no copy button — the user must manually select the text. Spec §5: secret shown once in a copy-first modal; §9: every code/secret block needs a copy button. Fix: Show the new key in a modal (reuse Radix dialog) with a prominent Copy button, “shown once — store it now” warning, and the value in a mono block.

4. API-keys page doesn’t use the shared DataTable (consistency)

api-keys-page.tsx:200-256 is a bespoke <Table> — so it has no search, no sortable Created column, no skeletons, and a bare “No API keys yet” empty cell (spec §5 wants an actionable empty state). tenants and system-profile are likely similar. Fix: Migrate the API-keys (and tenants) list to DataTable for parity with users/conversations; give it an actionable empty state (issue-key CTA / curl).


P1 — High-value UX

5. No toasts for copy / create / revoke / save

Copy uses an inline text swap (copyable-id.tsx:34, code-block.tsx); create/revoke/save surface only an inline <p> error and no success confirmation (e.g. revoke succeeds silently). Spec §5/§8 expect a “Copied” toast and success toast on save. Fix: Add a toast system (e.g. sonner, standard with shadcn) and emit on copy success, key created/revoked, profile saved, and errors.

6. Empty-state inconsistency

users-page.tsx:140-154 is the gold standard (curl snippets + link). But conversations (:196-198) is a bare “No conversations yet” and api-keys is a bare cell. Spec §5: empty states MUST be actionable everywhere. Fix: Bring conversations + api-keys empty states up to the users-page bar (create-conversation curl / issue-key CTA).

7. Date format diverges from spec (§5)

display.ts:17-27 formatDateTime always outputs "Jun 10, 11:51" — no relative formatting, no year (ambiguous across years), and always shows a time. Spec: relative within 7 days (“2 h ago”, “Yesterday”, “Mon”), else “Jun 10, 2026”; full ISO already correctly in title. Fix: Implement relative/absolute logic; include year for older dates.

8. Row interaction missing (§5)

DataTable rows have no hover background and are not clickable; there’s no ⋯ overflow actions menu and no detail view. Spec §5 wants --tc-surface-muted row hover and clickable rows with a right-aligned overflow menu where a detail view exists. Fix (incremental): Add row hover now; add a user/conversation detail drawer (members, IDs, “mint test JWT”) as a follow-up.

9. Accessibility gaps (§11)

  • confirm-dialog.tsx is a hand-rolled overlay: no focus trap, no Esc-to-close, no autofocus, background remains tabbable. Replace with Radix AlertDialog (radix-ui is already a dependency) for correct focus management.
  • Clipboard failures are swallowed (copyable-id.tsx:19, code-block.tsx:21) and “Copied” isn’t announced to screen readers — add an aria-live region (or toast, per #5).
  • Verify visible focus rings (§11 wants 2px solid --tc-primary, 2px offset) on nav links, sort buttons, and copyable chips; the global outline-ring/50 is faint.

P2 — Polish / larger bets (flag for decision)

10. CopyableId details (§5)

truncateId (display.ts:29-32) is first8…last4; spec says first8…last6. No visible copy icon on the chip. Align truncation and add a small copy glyph.

11. Redundant tenant-switcher actions

tenant-switcher.tsx:82-93 has “Manage tenants” and “Add tenant” both linking to /tenants. Differentiate (Add → open create modal) or drop one.

12. “Select a tenant” boilerplate repeated on every page

Each tenant-scoped page repeats the same card (api-keys-page.tsx:134, users-page.tsx:163, conversations-page.tsx:166). Extract a RequireTenant wrapper.

13. Dark mode is defined but dead and off-brand

index.css:89-121 defines a full .dark palette, but there’s no theme toggle and the dark palette is grayscale (drops brand indigo/lime). Decision needed: either wire a brand-aligned dark theme + toggle, or delete the dead block.

14. Responsive / mobile

Fixed w-60 sidebar with no mobile drawer/hamburger (app-shell.tsx:39); wide multi-column tables with main set to overflow-x-hidden (app-shell.tsx:91) risk clipping on small screens. Add a collapsible sidebar and horizontally-scrollable (or card-collapsing) tables.

15. Sidebar IA nit

Nav label “System messages” routes to /system-profile (app-shell.tsx:30) — align label/route/icon naming to avoid confusion.


Suggested sequencing

  1. P0 batch (badges, DataTable skeleton+pagination, key-reveal modal, migrate api-keys/tenants to DataTable) — highest consistency/correctness payoff, mostly contained to data-table.tsx, a new status-badge.tsx, and the list pages.
  2. P1 batch (toasts, empty states, date formatting, row hover, Radix AlertDialog + a11y).
  3. P2 after a decision on dark mode (#13) and detail-drawer scope (#8).

Verification (when implemented)

  • pnpm --filter @tesark/dashboard dev, sign in, select a tenant.
  • Tables: confirm skeleton on load, pagination footer, search + sortable Created, and hover; shrink viewport to verify mobile behavior.
  • Badges: confirm “Active” is one consistent color across Users / API keys / Conversations; revoked/deleted/archived correct.
  • API keys: create a key → copy-first modal with working Copy + toast; revoke → confirm dialog (Esc/focus-trap works) + success toast.
  • Empty states: new tenant with no users/conversations/keys shows actionable CTAs.
  • A11y: keyboard-only pass through dialog and copy chips; visible focus rings; screen-reader announces “Copied”.
  • pnpm --filter @tesark/dashboard lint and tsc -b clean.