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>
9.5 KiB
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 4–5 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 50–75 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}/actionand 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)
- Fix dark mode (🔴 #1). Define a dark palette under
@media (prefers-color-scheme: dark)and set an explicitbodybackground for both schemes. Nothing else matters if half the users can't read the text. Cheapest highest-impact change. - 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. - 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.
- 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. - 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-indicatoron filter. Low effort, removes the "rough edges" feel.
Reproduction / appendix
- Seed:
/tmp/tapir_seed.sql— 11 summaries, video ids prefixed5eedfor 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 configuredTAPIR_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–08desktop states ·09–11HTMX action sequence ·12–14mobile (375px) ·15–16dark mode.