Files
tapir/docs/ux-review/UX-REVIEW.md
T
mathiasandClaude Opus 4.8 ae239c02c2 docs(ux): Stage-0 reader UX review with screenshots
Reviewer pass against running `tapir serve` UI, seeded with 11
representative summaries (5 channels, rich+sparse, local+fallback,
varied action states). Captured via Playwright on koala k3s across
desktop/mobile and light/dark color schemes.

Two blocking findings: dark mode is unreadable (summary text near-black
on a dark canvas — `color-scheme: light dark` declared but `--fg`
hardcoded and no body background), and the list is a 6-column table that
does not reflow on mobile. Plus a sub-4.5:1 muted color, an unstyled
"admin table" surface, and small correctness nits (stray `· — ·` meta
join, cramped fallback badge). HTMX action toggles verified working.

Includes UX-REVIEW.md (severity-tagged findings + Top-5 sleek list) and
16 screenshots. No code changed — drives the next UI iteration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 00:06:54 +02:00

71 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tapir Stage-0 Reader — UX / Visual Review
_Reviewer pass against the running `tapir serve` UI (HTMX + Templ, `internal/web`), seeded
with 11 representative summaries across 5 channels (rich + sparse, local + fallback, varied
dates, action states). Captured via Playwright on the koala k3s cluster — desktop 1280, mobile
375, light + dark color schemes. Screenshots referenced inline live beside this file._
## Overall impression
The UI is **functionally complete and correct, but visually it reads as a database admin table,
not a reader** — browser-default links, a bordered `<table>`, raw native form controls, no
spacing rhythm, no cards, no design language. Two findings are blocking for a "sleek modern
reader": **dark mode is effectively unreadable** (the summary text disappears), and **the list
does not reflow on mobile** (a 6-column table crushed into 375px). HTMX interactions work well —
action toggles swap cleanly with correct `aria-pressed`, no full reload.
Verdict: **closer to unstyled-default HTML than to sleek.** The bones (semantics, HTMX, a11y
attributes, graceful fallbacks) are good; the surface needs a real design pass.
## Severity legend
🔴 Critical (broken/unusable) · 🟠 High (hurts core UX) · 🟡 Medium (noticeable) · 🔵 Low (polish)
## Findings (prioritized)
| # | Sev | Area | Problem | Why it hurts | Concrete fix | Evidence |
|---|-----|------|---------|--------------|--------------|----------|
| 1 | 🔴 | Theme / contrast | `:root { color-scheme: light dark }` is declared but `--fg` is hardcoded `#1a1a1a`, body sets no `background-color`. In a dark-OS browser the canvas paints dark and the near-black text stays → **the entire summary, title, highlights and takeaways become invisible**; only links, buttons and the badge survive. | The one thing the app exists to show — the summary — is unreadable for every dark-mode user. | Add `@media (prefers-color-scheme: dark)` overriding `--fg`, `--muted`, `--line`, and set an explicit `body { background-color }` for both schemes. Or drop the custom palette and use CSS system colors (`Canvas`/`CanvasText`). | `16-dark-detail.png`, `15-dark-list.png` |
| 2 | 🟠 | Mobile / layout | The list is a fixed 6-column `<table>` that does not reflow; at 375px columns are crushed and titles wrap to 45 lines, Provider/Actions are squeezed to the edge. | Mobile is a primary reading context; the list is the entry point and it looks broken. | Below ~640px, switch to a stacked **card-per-row** layout (`@media`: hide `thead`, make `tr` a block card with title on top, `channel · date` muted below, action state + fallback as chips). | `12-mobile-list.png` |
| 3 | 🟠 | Visual design | No design language: browser-default underlined-blue links, plain bordered table, native date/text inputs, default buttons, uniform spacing. Reads as phpMyAdmin, not a product. | "Useful to me" (Stage-0 gate) is undermined if the reader feels like raw tooling — it won't get daily use. | Introduce a minimal design system: 4/8px spacing scale, a card list (see #2/#7), restrained accent palette, styled links (no default underline, accent on hover), real buttons, subtle dividers/shadows. | `01-list-populated.png` |
| 4 | 🟡 | Contrast (WCAG) | `--muted:#777` on white = **4.48:1**, just under the 4.5:1 AA threshold for normal text. Used for meta line, table headers, dates, em-dashes. | Borderline-illegible meta; fails AA audits. | Darken to `#595959` (≈7:1) or `#636363` (≈4.7:1). Apply in both schemes. | `01`, `05-detail-rich.png` |
| 5 | 🟡 | Detail / meta | When a summary has no joined `videos` row, the meta line renders a dangling `· — · local (...)` with a stray leading middot. | Looks like a rendering bug on the exact edge case the app handles gracefully elsewhere. | Build meta as a slice of non-empty parts (`channel`, `date`, `provider`) and `strings.Join(parts, " · ")`; omit empties. | `08-detail-no-video.png` |
| 6 | 🟡 | Fallback badge | Amber `#f0ad4e` "fallback" pill sits in an **unlabeled empty column**, has `padding: 0 .4rem` (zero vertical padding, cramped), and the word "fallback" is unexplained jargon. | The signal (which summaries used the backup model) is present but unpolished and unclear. | Give vertical padding, an `aria-label`/`title` ("summarized with fallback model"), and either label the column or move the badge inline next to the provider. Consider a quieter dot + tooltip. | `01`, `05`, `07-detail-actions-active.png` |
| 7 | 🟡 | List / information | The list leads with a `Provider` column (`local`/`cloud` — an implementation detail) and a `Channel` column that actually shows the video **provider** field, not a real channel name (store limitation, `SummaryRow.Channel = videos.provider`). | A reader cares about channel + topic, not the inference backend. Columns spend scarce width on low-value data. | Surface the real channel name when the subscriptions table lands; demote provider to a small inline indicator; lead with title + channel + date. | `01` |
| 8 | 🟡 | Links | List titles and "watch on source" use default browser link styling; in dark mode visited links turn purple. | Inconsistent, dated look; purple-on-dark clashes with the accent. | Style `a` to the accent, remove default underline (underline on hover/focus), set `:visited` explicitly. | `05`, `16` |
| 9 | 🔵 | Empty state | Empty list is just _"No summaries."_ italic in a table cell — and this is the **real first-run landing** (the DB had 0 summaries before seeding). | First impression is a blank table, no orientation or next step. | Friendly empty state: short heading + one line ("Summaries appear here as your subscriptions are processed") outside the table chrome. | `02-list-empty.png` |
| 10 | 🔵 | Loading feedback | Filter submit (`hx-get``#summary-list`) has no `hx-indicator`; on a slow store the swap will feel dead. | Perceived unresponsiveness. | Add an `hx-indicator` spinner / disabled state on the filter button. | (not reproduced — swap instant locally) |
| 11 | 🔵 | Forms | Native date inputs render `mm/dd/yyyy` (locale-dependent, visually inconsistent with the rest). | Minor inconsistency; US format on a Swedish user's machine may surprise. | Style inputs to match the design system; keep native pickers but constrain width/typography. | `01` |
| 12 | 🔵 | Detail / readability | Prose spans the full `60rem` main width → ~110+ chars/line on desktop, above the 5075 ideal. | Long measure hurts reading comfort — the core task. | Cap prose blocks at ~`38rem`; keep the wider column for the list. | `05` |
| 13 | 🔵 | Detail / hierarchy | Summary / Highlights / Takeaways are same-weight `<h2>` + lists with no visual separation. | Flat hierarchy; hard to scan the three distinct blocks. | Separate the three into visually distinct blocks (cards or accent rules), give Highlights/Takeaways a lighter list style. | `05` |
| 14 | 🔵 | Actions | watched↔skipped mutual exclusion isn't visually communicated (three independent-looking buttons). | User can't tell watched and skipped are exclusive until they click. | Render watched/skipped as a segmented control; keep "saved" separate as a toggle. | `07` |
## HTMX interaction — PASS
Verified by scripted clicks (`09-action-before.png``10-action-after.png``11-action-multi.png`):
- Clicking an action POSTs to `/v/{id}/action` and swaps **only** `#action-buttons` (outerHTML) — no full-page reload.
- After set: button gains `class="action active"`, `aria-pressed="true"`, and a `✓` prefix (confirmed in logs).
- Toggle-off works: re-click clears it (`class="action"`).
- Multi-active (watched + saved) renders correctly.
Active state is legible (accent fill, white text, checkmark). The mechanics are solid — the work is purely visual.
## Top 5 changes to make it sleek (for the next workers)
1. **Fix dark mode (🔴 #1).** Define a dark palette under `@media (prefers-color-scheme: dark)` and set an explicit `body` background for both schemes. Nothing else matters if half the users can't read the text. Cheapest highest-impact change.
2. **Kill the table — ship a responsive card list (🟠 #2, #3).** One card per summary: title (prominent) · `channel · date` (muted) · action chips · fallback indicator. Solves the mobile-reflow break and the "admin table" look in one move.
3. **Adopt a minimal design system (🟠 #3).** Spacing scale (4/8px), restrained accent palette, styled links (accent, underline-on-hover), real buttons, subtle dividers/shadows, fixed type scale. Move decisively off browser defaults.
4. **Redesign the detail page as a reader (🟡 #12, #13, #5).** Constrain prose to ~38rem, large title + quiet single-line meta (fix the `· — ·` join), summary as a lead block, Highlights/Takeaways as visually distinct sections. This is the page users will spend time on.
5. **Sweep the small correctness + a11y nits (🟡 #4, #6, 🔵 #9, #10).** Muted color to ≥4.5:1; fallback badge vertical padding + `aria-label`; friendly empty state; `hx-indicator` on filter. Low effort, removes the "rough edges" feel.
## Reproduction / appendix
- **Seed:** `/tmp/tapir_seed.sql` — 11 summaries, video ids prefixed `5eed` for trivial cleanup
(`DELETE FROM summary_actions WHERE video_id LIKE '5eed%'; DELETE FROM summaries WHERE
video_id::text LIKE '5eed%'; DELETE FROM videos WHERE id::text LIKE '5eed%';`). Inserted under
the configured `TAPIR_USER_ID`; no throwaway user.
- **Serve:** `TAPIR_HTTP_ADDR=:8099 op run --env-file ~/.config/tapir/tapir.env -- ./bin/tapir serve`. `/healthz``ok`.
- **Screenshots** (this directory): `01``08` desktop states · `09``11` HTMX action sequence ·
`12``14` mobile (375px) · `15``16` dark mode.