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

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.

Reviewed-on: #11
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #11.
This commit is contained in:
2026-07-30 22:34:23 +07:00
committed by sulthan
parent ac3ee9b298
commit f3b55fd883
15 changed files with 787 additions and 173 deletions
+45
View File
@@ -1,8 +1,10 @@
package main
import (
"compress/gzip"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
@@ -196,3 +198,46 @@ func TestPutOmittedStatusPreservesArchivedAndAppliesProgress(t *testing.T) {
t.Fatalf("stored last_chapter_num = %v, want 12", got.LastChapterNum)
}
}
func TestGzipCompressesTextNotFonts(t *testing.T) {
srv, _ := newWebTestServer(t, webConfig())
cases := []struct {
path string
want bool
}{
{"/static/style.css", true},
{"/static/filter.js", true},
{"/static/htmx.min.js", true},
{"/static/fonts/dm-sans-var-latin.woff2", false},
}
for _, tc := range cases {
req := httptest.NewRequest(http.MethodGet, tc.path, nil)
req.Header.Set("Accept-Encoding", "gzip")
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Fatalf("GET %s = %d, want 200", tc.path, rr.Code)
}
got := rr.Header().Get("Content-Encoding") == "gzip"
if got != tc.want {
t.Errorf("GET %s Content-Encoding gzip = %v, want %v", tc.path, got, tc.want)
}
if got {
zr, err := gzip.NewReader(rr.Body)
if err != nil {
t.Fatalf("GET %s: body is not gzip: %v", tc.path, err)
}
if _, err := io.ReadAll(zr); err != nil {
t.Fatalf("GET %s: gzip body did not decode: %v", tc.path, err)
}
}
}
// A client that does not ask still gets plain bytes.
rr := httptest.NewRecorder()
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/static/style.css", nil))
if enc := rr.Header().Get("Content-Encoding"); enc != "" {
t.Errorf("Content-Encoding without Accept-Encoding = %q, want empty", enc)
}
}