Rebuild both UIs on the Cinder design (#10)

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.

Reviewed-on: #10
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #10.
This commit is contained in:
2026-07-30 09:47:08 +07:00
committed by sulthan
parent 1dc5b2ab65
commit ac3ee9b298
16 changed files with 1455 additions and 526 deletions
+24 -12
View File
@@ -26,29 +26,41 @@ function setActiveTab(el) {
}
// The chapter-edit form and the delete confirm row are the two per-card
// disclosure panels; only one makes sense open at a time.
// disclosure panels; only one makes sense open at a time. The button that
// owns an open panel carries .open, which is how the strip shows which cell
// the panel belongs to.
function closeCardPanels(key) {
var form = document.getElementById("chapter-form-" + key);
var confirm = document.getElementById("confirm-row-" + key);
if (form) form.hidden = true;
if (confirm) confirm.hidden = true;
var card = document.getElementById("card-" + key);
if (card) {
card.querySelectorAll(".actions .open").forEach(function (b) {
b.classList.remove("open");
});
}
}
function togglePanel(key, panelId, buttonSelector) {
var panel = document.getElementById(panelId + key);
if (!panel) return null;
var opening = panel.hidden;
closeCardPanels(key);
panel.hidden = !opening;
var card = document.getElementById("card-" + key);
var button = card && card.querySelector(buttonSelector);
if (button) button.classList.toggle("open", opening);
return panel;
}
function toggleChapterForm(key) {
var form = document.getElementById("chapter-form-" + key);
if (!form) return;
var opening = form.hidden;
closeCardPanels(key);
form.hidden = !opening;
if (!form.hidden) form.querySelector("input").focus();
var form = togglePanel(key, "chapter-form-", ".actions .pencil");
if (form && !form.hidden) form.querySelector("input").focus();
}
function toggleConfirmRow(key) {
var row = document.getElementById("confirm-row-" + key);
if (!row) return;
var opening = row.hidden;
closeCardPanels(key);
row.hidden = !opening;
togglePanel(key, "confirm-row-", ".actions .remove");
}
// A failed favourite/chapter/delete request leaves the card in place (htmx
Binary file not shown.
Binary file not shown.
Binary file not shown.
+618 -367
View File
File diff suppressed because it is too large Load Diff