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:232—LiveBadge(lime/presence) for an active keyconversations-page.tsx:152—LiveBadge(lime) for active conversationusers-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.tsxis a hand-rolled overlay: no focus trap, no Esc-to-close, no autofocus, background remains tabbable. Replace with RadixAlertDialog(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 anaria-liveregion (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 globaloutline-ring/50is 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
- 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 newstatus-badge.tsx, and the list pages. - P1 batch (toasts, empty states, date formatting, row hover, Radix AlertDialog + a11y).
- 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 lintandtsc -bclean.