diff --git a/docs/ux-review/01-list-populated.png b/docs/ux-review/01-list-populated.png new file mode 100644 index 0000000..b595c2a Binary files /dev/null and b/docs/ux-review/01-list-populated.png differ diff --git a/docs/ux-review/02-list-empty.png b/docs/ux-review/02-list-empty.png new file mode 100644 index 0000000..0dd5a40 Binary files /dev/null and b/docs/ux-review/02-list-empty.png differ diff --git a/docs/ux-review/03-filter-channel.png b/docs/ux-review/03-filter-channel.png new file mode 100644 index 0000000..8a21940 Binary files /dev/null and b/docs/ux-review/03-filter-channel.png differ diff --git a/docs/ux-review/04-filter-date.png b/docs/ux-review/04-filter-date.png new file mode 100644 index 0000000..5a103d6 Binary files /dev/null and b/docs/ux-review/04-filter-date.png differ diff --git a/docs/ux-review/05-detail-rich.png b/docs/ux-review/05-detail-rich.png new file mode 100644 index 0000000..79be2ba Binary files /dev/null and b/docs/ux-review/05-detail-rich.png differ diff --git a/docs/ux-review/06-detail-sparse.png b/docs/ux-review/06-detail-sparse.png new file mode 100644 index 0000000..7a04d2f Binary files /dev/null and b/docs/ux-review/06-detail-sparse.png differ diff --git a/docs/ux-review/07-detail-actions-active.png b/docs/ux-review/07-detail-actions-active.png new file mode 100644 index 0000000..701014b Binary files /dev/null and b/docs/ux-review/07-detail-actions-active.png differ diff --git a/docs/ux-review/08-detail-no-video.png b/docs/ux-review/08-detail-no-video.png new file mode 100644 index 0000000..efd7ed9 Binary files /dev/null and b/docs/ux-review/08-detail-no-video.png differ diff --git a/docs/ux-review/09-action-before.png b/docs/ux-review/09-action-before.png new file mode 100644 index 0000000..4e8f592 Binary files /dev/null and b/docs/ux-review/09-action-before.png differ diff --git a/docs/ux-review/10-action-after.png b/docs/ux-review/10-action-after.png new file mode 100644 index 0000000..fc13010 Binary files /dev/null and b/docs/ux-review/10-action-after.png differ diff --git a/docs/ux-review/11-action-multi.png b/docs/ux-review/11-action-multi.png new file mode 100644 index 0000000..315b075 Binary files /dev/null and b/docs/ux-review/11-action-multi.png differ diff --git a/docs/ux-review/12-mobile-list.png b/docs/ux-review/12-mobile-list.png new file mode 100644 index 0000000..8b348f5 Binary files /dev/null and b/docs/ux-review/12-mobile-list.png differ diff --git a/docs/ux-review/13-mobile-detail.png b/docs/ux-review/13-mobile-detail.png new file mode 100644 index 0000000..7b93ffb Binary files /dev/null and b/docs/ux-review/13-mobile-detail.png differ diff --git a/docs/ux-review/14-mobile-detail-actions.png b/docs/ux-review/14-mobile-detail-actions.png new file mode 100644 index 0000000..10ff659 Binary files /dev/null and b/docs/ux-review/14-mobile-detail-actions.png differ diff --git a/docs/ux-review/15-dark-list.png b/docs/ux-review/15-dark-list.png new file mode 100644 index 0000000..71d6e00 Binary files /dev/null and b/docs/ux-review/15-dark-list.png differ diff --git a/docs/ux-review/16-dark-detail.png b/docs/ux-review/16-dark-detail.png new file mode 100644 index 0000000..76558c4 Binary files /dev/null and b/docs/ux-review/16-dark-detail.png differ diff --git a/docs/ux-review/UX-REVIEW.md b/docs/ux-review/UX-REVIEW.md new file mode 100644 index 0000000..6101954 --- /dev/null +++ b/docs/ux-review/UX-REVIEW.md @@ -0,0 +1,70 @@ +# 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 ``, 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 `
` 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 `

` + 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.