Files
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

9.5 KiB
Raw Permalink Blame History

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.png10-action-after.png11-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. /healthzok.
  • Screenshots (this directory): 0108 desktop states · 0911 HTMX action sequence · 1214 mobile (375px) · 1516 dark mode.