Rebuild both UIs on the Cinder design #10

Merged
sulthan merged 6 commits from ui-revamp-cinder into main 2026-07-30 09:47:09 +07:00
Owner

Implements the Cinder design (Claude Design doc cfa39183) across both UI surfaces, self-hosts the fonts it depends on, and writes down the two documents that keep the result maintainable.

What changed

Web UI — rebuilt on the design's visual language: editorial serif, containerless sheets divided by ash hairlines, one 760px measure, no radii and no shadows. The organising rule is that heat is typographic: only a series with an unread chapter is crimson (title on an ember underline, cover foot rule, Ch N out, play icon), and favourites get brass rather than borrowing the accent. Both states hang off two classes on the <article> (is-new, is-dim), so sub-elements inherit the state instead of re-deriving it.

The six-cell action strip is flex-basis: 100% inside the row, which is what lets one piece of markup be a full-width strip with 46px thumb targets on a phone and a group of 40px squares beside the row on a desktop — no duplicate template branches. Icons moved to a sprite (templates/icons.html); htmx-swapped cards reference the page's symbols, so a row no longer carries a screenful of inline SVG.

Userscript panel — repainted in the same tokens. The panel and the web UI are the same product on the same phone, and the old purple-on-charcoal panel read as a different application once the web UI moved. Structure, ids and classes are untouched, and the edge tab keeps its geometry, touch-action and #hit sizing.

Fonts are self-hosted — five latin-subset woff2 files (~120 KB) embedded via the existing //go:embed static. Loading them from Google would lose the design's character exactly where it is used most: Bromite users routinely block Google's font domains, and the backend is reachable over a LAN with no internet route. staticHandler registers the .woff2 MIME type, which Go's table lacks and the scratch image has no /etc/mime.types for.

Docs — docs/design-system.md records the rules a stylesheet cannot state (what the ember is reserved for, why light mode is a re-tuning rather than an inversion, which details are load-bearing) so a future agent does not re-derive them from the CSS. REDEPLOY.md covers the operation actually performed every time, which DEPLOY.md reduced to two lines.

Commits

Each is one logical change and builds on its own:

4d69e54 listView.NewCount — data for the Updated badge, no markup
e940b96 Web UI rebuilt on Cinder (CSS, templates, sprite, filter.js)
686fcc1 Userscript panel repainted in the same tokens
ce7e93d Self-hosted webfonts + .woff2 MIME registration
a547cc9 docs/design-system.md
b20ecf1 REDEPLOY.md

Verification

  • go test ./... — 187 pass. Userscript logic tests — 14 pass.
  • Screenshotted at 390px and 1180px, in dark and light, across All / Archived / Finished / empty / chapter-form / confirm-row.
  • Fonts: a run with fonts.googleapis.com and fonts.gstatic.com blocked still reports all five faces loaded; served as 200 font/woff2.
  • The three REDEPLOY.md backup/restore commands were run, not assumed. :ro on the source volume fails (unable to open database file — WAL needs to create -shm), and a restored file lands root-owned while the container runs as uid 65532, so reads succeed and writes fail. Both are documented with the reason.

Note for the reviewer

The panel restyle is token-level only: the design doc covers the web UI, so the panel's structure has no reference to follow and was deliberately left alone.

Deploying this needs a rebuild — templates, CSS and fonts are //go:embeded, so pulling alone changes nothing.

Implements the **Cinder** design (Claude Design doc `cfa39183`) across both UI surfaces, self-hosts the fonts it depends on, and writes down the two documents that keep the result maintainable. ## What changed **Web UI** — rebuilt on the design's visual language: editorial serif, containerless sheets divided by ash hairlines, one 760px measure, no radii and no shadows. The organising rule is that *heat is typographic*: only a series with an unread chapter is crimson (title on an ember underline, cover foot rule, `Ch N out`, play icon), and favourites get brass rather than borrowing the accent. Both states hang off two classes on the `<article>` (`is-new`, `is-dim`), so sub-elements inherit the state instead of re-deriving it. The six-cell action strip is `flex-basis: 100%` inside the row, which is what lets one piece of markup be a full-width strip with 46px thumb targets on a phone and a group of 40px squares beside the row on a desktop — no duplicate template branches. Icons moved to a sprite (`templates/icons.html`); htmx-swapped cards reference the page's symbols, so a row no longer carries a screenful of inline SVG. **Userscript panel** — repainted in the same tokens. The panel and the web UI are the same product on the same phone, and the old purple-on-charcoal panel read as a different application once the web UI moved. Structure, ids and classes are untouched, and the edge tab keeps its geometry, `touch-action` and `#hit` sizing. **Fonts are self-hosted** — five latin-subset woff2 files (~120 KB) embedded via the existing `//go:embed static`. Loading them from Google would lose the design's character exactly where it is used most: Bromite users routinely block Google's font domains, and the backend is reachable over a LAN with no internet route. `staticHandler` registers the `.woff2` MIME type, which Go's table lacks and the scratch image has no `/etc/mime.types` for. **Docs** — `docs/design-system.md` records the rules a stylesheet cannot state (what the ember is reserved for, why light mode is a re-tuning rather than an inversion, which details are load-bearing) so a future agent does not re-derive them from the CSS. `REDEPLOY.md` covers the operation actually performed every time, which `DEPLOY.md` reduced to two lines. ## Commits Each is one logical change and builds on its own: | | | |---|---| | `4d69e54` | `listView.NewCount` — data for the Updated badge, no markup | | `e940b96` | Web UI rebuilt on Cinder (CSS, templates, sprite, `filter.js`) | | `686fcc1` | Userscript panel repainted in the same tokens | | `ce7e93d` | Self-hosted webfonts + `.woff2` MIME registration | | `a547cc9` | `docs/design-system.md` | | `b20ecf1` | `REDEPLOY.md` | ## Verification - `go test ./...` — 187 pass. Userscript logic tests — 14 pass. - Screenshotted at 390px and 1180px, in dark and light, across All / Archived / Finished / empty / chapter-form / confirm-row. - Fonts: a run with `fonts.googleapis.com` and `fonts.gstatic.com` blocked still reports all five faces `loaded`; served as `200 font/woff2`. - The three `REDEPLOY.md` backup/restore commands were run, not assumed. `:ro` on the source volume fails (`unable to open database file` — WAL needs to create `-shm`), and a restored file lands root-owned while the container runs as uid 65532, so reads succeed and writes fail. Both are documented with the reason. ## Note for the reviewer The panel restyle is token-level only: the design doc covers the web UI, so the panel's *structure* has no reference to follow and was deliberately left alone. Deploying this needs a rebuild — templates, CSS and fonts are `//go:embed`ed, so pulling alone changes nothing.
sulthan added 6 commits 2026-07-30 09:43:53 +07:00
The tab strip needs to say how many series have an unread chapter without
the user opening the tab to find out, so listView carries the number
alongside the filtered items.

The count is taken over the whole reading set rather than the rendered
items, so it means the same thing on every tab instead of collapsing to
len(Items) on Updated and 0 everywhere else. Archived and finished are
excluded, matching what the Updated tab itself shows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the card-and-chip visual language with the Cinder direction from
the design doc: editorial serif, containerless sheets divided by ash
hairlines, one 760px measure, no radii and no shadows.

The organising rule is that heat is typographic. Only a series with an
unread chapter is allowed to be crimson — title, cover foot rule, "Ch N
out", play icon — so the one thing worth acting on is the one thing that
draws the eye. Favourites get brass instead of borrowing the accent, which
is what made the old three-hue palette read as noise. Both states hang off
two classes on the article (is-new, is-dim), so a new sub-element inherits
the state rather than re-deriving it.

The action strip is flex-basis 100% inside the row, which is what lets the
same markup be a full-width six-cell strip under the row on a phone (46px
targets) and a group of 40px squares beside it on a desktop, with no
duplicate template branches. Icons move to a sprite in icons.html: cards
are swapped in by htmx and reference the page's symbols, so a row no
longer carries a screenful of inline SVG paths.

[hidden] gets a display:none override because every disclosure panel is
now a flex container and display beats the attribute. filter.js grows an
.open class alongside hidden so the strip can show which cell the open
panel belongs to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The panel and the web UI are the same product on the same phone, and the
old purple-on-charcoal panel read as a different application once the web
UI moved to Cinder. Same tokens, same rule that heat is typographic:
.item.hot mirrors the web UI's is-new, .item.dim mirrors is-dim, chips and
buttons become mono small-caps with hairline borders instead of pills, and
the loading spinner becomes the same burning hairline rather than a
rotating ring.

Tokens are declared on :host so the panel has one palette block to edit,
but the fonts stay system serif and mono — the web UI's webfonts cannot
follow, because an @import inside the shadow root is at the mercy of the
host site's CSP.

Structure, ids and classes are untouched: this is a restyle, and in
particular the edge tab keeps its geometry, touch-action and #hit sizing
(see docs/superpowers/specs/2026-07-28-edge-tab-hitbox-design.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design depends on three specific families, and loading them from
fonts.googleapis.com means the UI silently loses its character exactly
where it is used most: Bromite is a de-googled browser whose users
routinely block Google's font domains, and the backend is reachable over a
LAN with no internet route. In both cases the page fell back to Georgia
and a system sans.

Five latin-subset woff2 files, ~120 KB total, in a binary already 27 MB —
the cost is noise, and //go:embed static picks them up with no build
change (backend/Dockerfile already copies static/). Latin only because the
UI is English; woff2 only because every browser that can run this app
supports it, so there is no second format to carry. DM Sans ships as one
variable file, which covers the whole 400-700 range the design uses for
the price of a single request.

staticHandler registers the .woff2 MIME type: Go's built-in table has no
entry for it and the scratch image has no /etc/mime.types, so the fonts
would otherwise be served as application/octet-stream. The two faces
above the fold are preloaded, since they are now discovered a stylesheet
late rather than from a preconnect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The design lives in a Claude Design doc that an agent working in this repo
cannot see, so every future UI change would otherwise be a re-derivation
of the same rules from the CSS — and the rules that matter here are the
ones a stylesheet cannot state: that heat is reserved for an unread
chapter, that favourites get brass instead of borrowing it, that light mode
is a re-tuning rather than an inversion.

Records the token table for both colour branches, the three type roles,
the component anatomy of a row, the load-bearing details that look like
noise until they break something (the [hidden] override, the action
strip's flex-basis, the icon sprite, rebuilding the binary to see template
changes), and how to add a font weight.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
DEPLOY.md covers standing the stack up from nothing and ends with a two-line
"Updating" section, which is the operation actually performed every time and
the one where the irreversible mistakes live. Redeploying has a required
order — back up before pulling, because a backup taken after a bad deploy is
a backup of the damage — and three traps that are invisible until they cost
data:

- The store runs in WAL mode, so copying bookmarks.db alone while the
  container is up can silently drop the newest bookmarks. VACUUM INTO folds
  the WAL in; the cold-copy fallback has to take the sidecar files.
- That backup needs the source volume mounted read-write, which looks wrong.
  Opening a WAL database creates the -shm file, so :ro fails outright.
- A restored file lands root-owned while the container runs as uid 65532.
  Reads succeed and writes do not, so the restore looks like it worked.

Backups go to ../mangabm-backups/, a sibling of the checkout rather than a
directory inside it, so no git operation or careless rm in the project dir
can take the backups along with it. Names carry a UTC timestamp so they sort
chronologically as plain text and cannot collide across a DST shift.

Also documents that a rebuild is mandatory for any UI change now that the
templates, CSS and fonts are //go:embed-ed, and ends at the phone smoke test:
no curl can tell you the panel works on the device.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan merged commit ac3ee9b298 into main 2026-07-30 09:47:09 +07:00
sulthan deleted branch ui-revamp-cinder 2026-07-31 01:01:49 +07:00
Sign in to join this conversation.