Work the design critique down: chapter format, colour law, search, strip, a11y #11

Merged
sulthan merged 13 commits from improve-design-health into main 2026-07-30 22:34:24 +07:00
Owner

Two rounds of design-critique fixes on the web UI. Every visual change was verified at 390x844 and 1280x900 in both dark and light with screenshots; go test ./... is green throughout; no new dependencies.

Earlier commits on this branch

The two oldest commits predate this session and were never opened as their own PR, so they are under review here too:

  • Confirm-gate the lifecycle actions, cluster the action strip by consequence.
  • Fix the accessibility findings from the audit: contrast, focus, reduced motion.

The rest

Chapter format. The userscript and the poller both write "Chapter N", and the templates prefixed Ch again, so every real Asura row read Ch Chapter 250 — while a manual edit stored a bare 250, leaving two formats in one list. DisplayChapter/DisplayLatest on Bookmark now strip the lead-in and re-add exactly one Ch .

Zero-result search. The client filter only toggled card.hidden, so a query matching nothing left a blank list under a fully populated, unfiltered "Continue reading" strip. There is now a no-match state with a Clear-search button, and the strip goes down while a filter is active.

The colour law. --ember is documented as meaning "new chapter" and was spent on eight things, including setting "Nothing new." in the colour reserved for new chapters. Destruction moves to a new --danger token; text-input focus follows the searchbar idiom and turns --paper. Contrast, both themes: --danger on the page 4.82 / 6.65, the solid Remove button 4.94 / 7.30, the confirm question 9.00 / 7.98.

The remove confirm. Buttons 40px 8px apart became 46px 12px apart, and the question names the series and the loss instead of asking "Remove this?". It opens with Cancel focused, not Remove — the two reversible rows still open on their affirmative.

The recent strip. It was the head of the same updated_at DESC list rendered directly below it, on every tab, costing ~240px of the first phone screen. It is now scoped to series with a chapter waiting, and only on All. With nothing new anywhere it does not render — deliberate.

Stale chrome. The strip and the Updated badge describe the whole library but live outside the swapped #list, so archiving a series left it under "Continue reading" with the badge still counting it, and /?tab=all reached by htmx differed from the same URL reloaded. Both regions move into chrome.html and refresh out of band on every mutation and every tab switch.

Accessibility and touch. Esc closes any open panel and returns focus to the cell that owns it; opening a confirm moves focus into it; the inline error scrolls into view and no longer self-destructs after 5s; every tab and desktop action cell clears 44px; role="alert" on the login error; the card monogram is no longer announced; the busy bar is clipped by its own travel rather than by overflow: hidden on the card.

Chapter form label. The panel's only visible text named the published chapter while the field held your progress. The field gets a real label; "Latest known" moves below it.

gzip. Nothing was compressed. A stdlib middleware handles the four text types and leaves woff2 alone: style.css 21.8 -> 6.5 KB, htmx 50.9 -> 16.4, filter.js 7.6 -> 2.9.

Not addressed

The role="status" error slot is still mutated while hidden and then revealed, which is the non-announcing pattern the confirm rows were fixed for. Delete is still a silent vanish. Both are flagged in the critique snapshot under .impeccable/critique/.

Design health went 24/36 (66.7%) to 29/40 (72.5%) between snapshots; the two P1s that survived were found and fixed after that run.

🤖 Generated with Claude Code

Two rounds of design-critique fixes on the web UI. Every visual change was verified at 390x844 and 1280x900 in both dark and light with screenshots; `go test ./...` is green throughout; no new dependencies. ## Earlier commits on this branch The two oldest commits predate this session and were never opened as their own PR, so they are under review here too: - Confirm-gate the lifecycle actions, cluster the action strip by consequence. - Fix the accessibility findings from the audit: contrast, focus, reduced motion. ## The rest **Chapter format.** The userscript and the poller both write `"Chapter N"`, and the templates prefixed `Ch ` again, so every real Asura row read `Ch Chapter 250` — while a manual edit stored a bare `250`, leaving two formats in one list. `DisplayChapter`/`DisplayLatest` on `Bookmark` now strip the lead-in and re-add exactly one `Ch `. **Zero-result search.** The client filter only toggled `card.hidden`, so a query matching nothing left a blank list under a fully populated, unfiltered "Continue reading" strip. There is now a no-match state with a Clear-search button, and the strip goes down while a filter is active. **The colour law.** `--ember` is documented as meaning "new chapter" and was spent on eight things, including setting "Nothing new." in the colour reserved for new chapters. Destruction moves to a new `--danger` token; text-input focus follows the searchbar idiom and turns `--paper`. Contrast, both themes: `--danger` on the page 4.82 / 6.65, the solid Remove button 4.94 / 7.30, the confirm question 9.00 / 7.98. **The remove confirm.** Buttons 40px 8px apart became 46px 12px apart, and the question names the series and the loss instead of asking "Remove this?". It opens with **Cancel** focused, not Remove — the two reversible rows still open on their affirmative. **The recent strip.** It was the head of the same `updated_at DESC` list rendered directly below it, on every tab, costing ~240px of the first phone screen. It is now scoped to series with a chapter waiting, and only on All. With nothing new anywhere it does not render — deliberate. **Stale chrome.** The strip and the Updated badge describe the whole library but live outside the swapped `#list`, so archiving a series left it under "Continue reading" with the badge still counting it, and `/?tab=all` reached by htmx differed from the same URL reloaded. Both regions move into `chrome.html` and refresh out of band on every mutation and every tab switch. **Accessibility and touch.** Esc closes any open panel and returns focus to the cell that owns it; opening a confirm moves focus into it; the inline error scrolls into view and no longer self-destructs after 5s; every tab and desktop action cell clears 44px; `role="alert"` on the login error; the card monogram is no longer announced; the busy bar is clipped by its own travel rather than by `overflow: hidden` on the card. **Chapter form label.** The panel's only visible text named the published chapter while the field held your progress. The field gets a real label; "Latest known" moves below it. **gzip.** Nothing was compressed. A stdlib middleware handles the four text types and leaves woff2 alone: style.css 21.8 -> 6.5 KB, htmx 50.9 -> 16.4, filter.js 7.6 -> 2.9. ## Not addressed The `role="status"` error slot is still mutated while hidden and then revealed, which is the non-announcing pattern the confirm rows were fixed for. Delete is still a silent vanish. Both are flagged in the critique snapshot under `.impeccable/critique/`. Design health went 24/36 (66.7%) to 29/40 (72.5%) between snapshots; the two P1s that survived were found and fixed after that run. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sulthan added 13 commits 2026-07-30 22:32:58 +07:00
Fixes the 2026-07-30 impeccable critique of the web UI.

Archive and finish now open their own .confirm-row disclosure, the
pattern delete already used, so no single tap moves a series out of the
list. Restore stays instant — it is the reversal. The two reversible
confirms wear grey; remove keeps the ember wash.

The six action cells cluster by consequence: navigate (play) | organize
(favourite, chapter) | lifecycle (archive/restore, finish, remove). The
lifecycle cells sit on a recessed ground, separated by a hairline on a
phone and by a gap on desktop, so the 4th-slot icon swapping between
.box and .restore no longer breaks position memory.

htmx:responseError branches on status: 401 offers a login link, 400
surfaces the server's own reason, everything else keeps the retry copy.

Busy bar and inline error move to --mute, leaving --ember exclusive to
the new-chapter signal.

Also: aria-live on the confirm rows, max on the chapter input, cover
width and row gap as tokens instead of a hardcoded 94px indent, and
mobile covers 30% larger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contrast: --mute-2 and --trash both failed AA in both themes — every
10px mono label (recent chapter, section headings, chapter hint) sat
between 2.5:1 and 3.7:1, and the trash icon, the one irreversible
control, was the least visible thing in the strip. Retuned per theme:
labels now 4.8-5.2:1, trash 4.3-4.5:1 against a 3:1 requirement.

Focus: the search input clears its own outline and the underline lives
on the parent, so :focus-within on .searchbar carries the ring.

The inline error slot gets role=status, so a failed write is announced
rather than looking like an ignored tap.

The tab strip drops role=tablist/tab: these are real links that change
the URL, and the roles promised a tabpanel contract that was never
implemented, leaving aria-selected unset on every one. aria-current
carries the state instead.

prefers-reduced-motion now names pseudo-elements — `*` does not match
them, so the busy bar and the error dot, both ::before, kept animating
for the users who asked them not to.

Log out was a 54x15 target; an ::after pad makes it 78x45 without
moving the label.

Also: the two dark-tuned hex borders in the desktop block become
theme-aware tokens, and the preload swaps mono for DM Sans, the larger
face and the one the body text needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The userscript writes last_chapter/latest_chapter as "Chapter N" and the
poller writes the same label, so the templates' own "Ch " prefix doubled it on
every real row. A manual edit through the web UI stores a bare "250", so the
list carried two formats at once and stopped being scannable.

Move the formatting out of the templates into DisplayChapter/DisplayLatest on
Bookmark, which strip a leading "Chapter"/"Ch." lead-in and re-add exactly one
"Ch ", falling back to the numeric column when the label is empty.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The client filter only toggles card.hidden, so a query matching nothing left
the list area blank while the unfiltered "Continue reading" strip stayed
populated above it — the screen contradicted itself and read as a crash.

list.html now carries a hidden no-match block; applyFilter counts visible cards,
fills the query in with textContent, and hides .recent for the duration of a
filter. The Clear-search button is handled by a delegated click listener so it
survives an htmx tab swap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The stylesheet asserted twice that --ember only ever means "new chapter" while
spending it on eight things, including "Nothing new." — the heat signal firing
on its own negation.

Add a --danger token (a duller oxblood, tuned per theme) and move remove hover,
the open remove cell, the confirm wash and the login error onto it. Text-input
focus follows the .searchbar idiom and turns --paper, so a valid number field
no longer paints itself red. Drop .empty.hot. Ember now survives on the is-new
group, the Updated tab and the brand, which the law comment now says out loud.

Contrast, both themes: --danger on --ink 4.82 dark / 6.65 light, --danger-ink
on the solid button 4.94 / 7.30, --danger-soft on the wash 9.00 / 7.98.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The one irreversible path was defended with 40px buttons 8px apart and a
question — "Remove this?" — that never said what was leaving or what it cost.

Confirm buttons go to 46px with a 12px gap (all three rows), and the remove
question names the series and the loss. The row wraps, so a long title takes
the width it needs instead of overflowing; verified at 320 and 390 in both
themes with a 90-character title.

Button order is unchanged: the group is right-aligned with Cancel outermost, so
the destructive button already sits further from a right thumb's arc than
Cancel does, and all three rows keep the same shape.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The strip is the head of the same updated_at DESC reading list the All tab
renders, and app.html put it on every tab: on a small library it repeated the
list immediately below it while eating ~230px of the first phone screen, and on
Archived and Finished it showed reading series above the opposite bucket.

buildListView now fills Recent only for tab=all with more reading series than
the strip holds. Because the strip lives outside the htmx-swapped #list, an
in-page tab switch cannot re-render it, so applyFilter also decides its
visibility from the active tab; setActiveTab pokes it after a swap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Esc closes whichever panel a card has open and returns focus to the cell that
  owns it; opening a confirm row moves focus to its primary button, so the
  announcement is deterministic and tab order stays inside the confirm.
- The inline error scrolls itself into view and no longer self-destructs after
  5s: this reader gets interrupted mid-tap, and an expiring notice left a failed
  write with no trace at all. It clears on the next request from that card.
- Every tab clears 44px wide (All was 15.25); desktop action cells go 40 -> 44.
- login.html's error gets role="alert".
- The card monogram carries its own aria-hidden — Chromium exposes it through
  the cover link's, because that link is programmatically focusable.
- The busy bar ends its travel flush with the card edge, so .card.htmx-request
  no longer needs overflow: hidden and cannot clip a panel mid-request.

Verified at 390 and 1280 in both themes; detector over templates+static reports
only the two known Instrument Serif false positives.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Moving focus into an opened confirm row took the first button in DOM order,
which is the affirmative in all three rows — so the one irreversible action
opened with its destructive button pre-armed and a stray Enter would fire it.
That inverts the point of the gate the row exists to be.

The reversible rows still open on their affirmative; remove opens on Cancel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Continue reading" was the head of the same updated_at DESC list rendered
directly beneath it, so on a small library it said nothing new for ~240px of
the first phone screen. Scoping it to series with an unread chapter gives it
the one question the list does not already answer, since the list's order is
reading recency and its head *is* the old strip.

Consequence, accepted deliberately: with nothing new anywhere the strip does
not render at all, and the list starts at the top of the page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The "Continue reading" strip and the Updated badge describe the whole library
but live outside the #list htmx swaps, so nothing corrected them after a
mutation: archiving a series left it sitting in the strip under "Continue
reading", with the badge still counting it. A tab switch had the same problem
from the other side — the strip was server-gated to All but could not be
re-rendered client-side, so /?tab=all reached by htmx and the same URL reloaded
produced different DOM.

Both regions move into chrome.html and always render, hidden when they have
nothing to say, so an out-of-band swap always has an id to target. /ui/list and
every mutation append them; the tab the reader is on comes from HX-Current-URL.
filter.js keeps only the job the server cannot do — hiding the strip while a
filter is active.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing was compressed: the list page shipped ~123 KB of HTML, CSS and JS in
the clear on every cold load, which is the wrong bill for a phone on mobile
data. A stdlib middleware compresses the four content types this server
actually serves in bulk and leaves woff2 alone, since it is already compressed.

Measured on the live server: style.css 21.8 KB -> 6.5, htmx 50.9 -> 16.4,
filter.js 7.6 -> 2.9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The edit panel's only visible text was "Latest known: Ch 30" sitting directly
above a field pre-filled with your own progress — so on the one occasion the
form is worth opening, when those two numbers differ, the visible label named
the wrong one. Correcting a chapter number is this UI's stated primary job.

The field gets a real label ("Chapter you're on", tied by for/id, replacing the
screen-reader-only aria-label) and "Latest known" moves below it as context.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan force-pushed improve-design-health from a6d245c25e to 8fcc4f7e49 2026-07-30 22:32:58 +07:00 Compare
sulthan merged commit f3b55fd883 into main 2026-07-30 22:34:24 +07:00
sulthan deleted branch improve-design-health 2026-07-31 01:01:45 +07:00
Sign in to join this conversation.