From 42b93dc155b06fa58b98bd5b6ec330bdfe0485b0 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Fri, 31 Jul 2026 03:18:54 +0700 Subject: [PATCH 1/2] feat(web): permanent action key under the tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card action strip is icon-only, so nothing on screen said what the six glyphs do. Add a keyrow line under the tabs that names each one. The key follows the tab, not the row: archived and finished swap Archive for Restore, and finished drops Done — same conditions card.html already uses for the buttons themselves. It therefore rides along in writeChromeOOB, or a tab switch would leave the previous bucket's key. Also from the Claude Design pass: per-action accent colours on hover and press (slate archive, moss finished, clay chapter) so a press says which lane it belongs to, a slightly larger cover and title, and the favourite mark pinned to the right edge of the measure instead of trailing the title, which drifted whenever a new-chapter title shrink-wrapped. Co-Authored-By: Claude Opus 5 --- backend/static/style.css | 96 ++++++++++++++++++++++++++++------- backend/templates/app.html | 2 + backend/templates/chrome.html | 31 +++++++++-- backend/web.go | 10 ++-- 4 files changed, 113 insertions(+), 26 deletions(-) diff --git a/backend/static/style.css b/backend/static/style.css index ec5b736..7ccbda7 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -81,6 +81,11 @@ --danger-ink: #150808; --danger-soft: #e2aaa1; --brass: #b8912f; /* favourite — a second, cooler metal */ + /* One accent per action, so a press says which lane it belongs to. All three + are held at the same weight as --brass: muted, no ember competition. */ + --slate: #7fa0c0; /* archive */ + --moss: #7fae86; /* finished */ + --clay: #b5906f; /* set chapter */ --trash: #977671; /* remove, resting — icons need 3:1, not 4.5:1 */ /* Desktop cell borders for the two coloured action states. */ @@ -98,7 +103,7 @@ --measure: 760px; /* Cover width + row gap: the disclosure panels indent past the cover on desktop, so both live here rather than as magic numbers. */ - --cover-w: 86px; + --cover-w: 93px; --row-gap: 14px; } @@ -131,6 +136,9 @@ --danger-ink: #fff; --danger-soft: #7c2c22; --brass: #8a681c; + --slate: #3f6689; + --moss: #3d6c46; + --clay: #7c5533; --trash: #8c6558; --play-hot-line: #f0cfc6; @@ -285,6 +293,35 @@ button { cursor: pointer; } border: 1px solid currentColor; } +/* ---- action key: one permanent line under the tabs, so the icon strip below + never has to be guessed at. Lean on a phone (28px, edge to edge), a step + bigger on desktop where there is room to read it. ---- */ +.keyrow { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + padding: 9px 18px 10px; + border-bottom: 1px solid var(--rule); +} +/* On a phone each key stacks: icon over word, so the word gets the full cell + width and can stay the long form. */ +.keyrow .pair { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + color: var(--mute); +} +.keyrow .pair svg { width: 14px; height: 14px; flex: none; } +.keyrow .pair span { + font: 500 10px/1 var(--font-mono); + letter-spacing: .04em; + text-transform: uppercase; +} +.keyrow .pair.brass svg { color: var(--brass); } +.keyrow .pair.trash svg { color: var(--trash); } + /* ---- continue reading ---- */ .recent { display: flex; @@ -311,7 +348,7 @@ button { cursor: pointer; } } .recent-strip::-webkit-scrollbar { display: none; } .recent-card { - width: 92px; + width: var(--cover-w); flex: none; display: flex; flex-direction: column; @@ -319,8 +356,8 @@ button { cursor: pointer; } } .recent-cover { position: relative; - width: 92px; - height: 123px; + width: var(--cover-w); + height: calc(var(--cover-w) * 4 / 3); background: var(--hatch); display: grid; place-items: center; @@ -328,7 +365,7 @@ button { cursor: pointer; } } .recent-cover img { width: 100%; height: 100%; object-fit: cover; } .recent-title { - font: 400 15px/1.2 var(--font-display); + font: 400 16px/1.25 var(--font-display); color: var(--paper-dim); display: -webkit-box; -webkit-line-clamp: 2; @@ -336,7 +373,7 @@ button { cursor: pointer; } overflow: hidden; } .recent-chapter { - font: 500 10px/1 var(--font-mono); + font: 500 11px/1 var(--font-mono); letter-spacing: .1em; color: var(--mute-2); } @@ -374,7 +411,7 @@ button { cursor: pointer; } } .card.is-dim { background: var(--dim); } -.row { display: flex; flex-wrap: wrap; gap: var(--row-gap); } +.row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--row-gap); } .cover { position: relative; @@ -387,7 +424,7 @@ button { cursor: pointer; } overflow: hidden; } .cover img { width: 100%; height: 100%; object-fit: cover; } -.cover .monogram { font-size: 30px; } +.cover .monogram { font-size: 32px; } .is-dim .cover { background: var(--hatch-dim); filter: grayscale(1); opacity: .85; } .body { @@ -395,6 +432,7 @@ button { cursor: pointer; } min-width: 0; display: flex; flex-direction: column; + justify-content: center; gap: 8px; } @@ -403,7 +441,7 @@ button { cursor: pointer; } margin: 0; flex: 1; min-width: 0; - font: 400 19px/1.2 var(--font-display); + font: 400 21px/1.2 var(--font-display); color: var(--paper-dim); } /* Heat: crimson title over an ember hairline sized to the text, not the row. */ @@ -416,7 +454,9 @@ button { cursor: pointer; } } .is-dim .title { font-style: italic; color: var(--mute); } .is-dim .fav-mark { color: var(--mute-2); } -.fav-mark { flex: none; width: 13px; height: 13px; margin-top: 5px; color: var(--brass); } +/* The mark always sits at the right edge of the measure, not after the last + word — a new-chapter title shrink-wraps, so without this it drifts. */ +.fav-mark { flex: none; margin-left: auto; width: 14px; height: 14px; margin-top: 5px; color: var(--brass); } .meta { margin: 0; @@ -424,7 +464,7 @@ button { cursor: pointer; } align-items: center; gap: 9px; flex-wrap: wrap; - font: 500 10px/1 var(--font-mono); + font: 500 11px/1 var(--font-mono); letter-spacing: .12em; text-transform: uppercase; color: var(--mute); @@ -434,7 +474,7 @@ button { cursor: pointer; } .site-demonic { color: var(--demonic); } .new-chapter { color: var(--ember); } .state { display: flex; align-items: center; gap: 4px; color: var(--mute); } -.state svg { width: 9px; height: 9px; } +.state svg { width: 10px; height: 10px; } .is-dim .meta { color: var(--mute-2); } .is-dim .site-asura, .is-dim .site-demonic { color: var(--mute); filter: grayscale(.6); } @@ -459,6 +499,12 @@ button { cursor: pointer; } .actions > *:last-child { border-right: none; } .actions svg { width: 17px; height: 17px; } .actions > *:hover { color: var(--paper); } +/* Per-action accent on hover and press: gold favourite, slate archive, moss + finished, clay chapter. Remove keeps --danger, play keeps paper/ember. */ +.actions .fav:hover, .actions .fav:active, .actions .fav:focus-visible { color: var(--brass); } +.actions .pencil:hover, .actions .pencil:active, .actions .pencil:focus-visible { color: var(--clay); } +.actions .box:hover, .actions .box:active, .actions .box:focus-visible { color: var(--slate); } +.actions .finish:hover, .actions .finish:active, .actions .finish:focus-visible { color: var(--moss); } .actions .play { color: var(--paper); } .is-new .actions .play { color: var(--ember); } .actions .play:hover { background: var(--hover); } @@ -696,7 +742,7 @@ button { cursor: pointer; } /* ---- desktop: same measure, actions fold up beside the row ---- */ @media (min-width: 720px) { - :root { --cover-w: 74px; --row-gap: 20px; } + :root { --cover-w: 80px; --row-gap: 20px; } .topbar { padding: 26px 32px 18px; } .brand { font-size: 30px; } @@ -711,13 +757,25 @@ button { cursor: pointer; } .tabs a { padding: 10px 0 14px; font-size: 18px; } .searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; } .recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; } - .recent-card, .recent-cover { width: 100px; } - .recent-cover { height: 133px; } + .keyrow { + align-items: center; + justify-content: flex-start; + gap: 26px; + height: 36px; + padding: 0 32px; + } + .keyrow .pair { flex-direction: row; gap: 7px; } + .keyrow .pair svg { width: 14px; height: 14px; } + .keyrow .pair span { font-size: 11px; letter-spacing: .1em; } + .keyrow .full { display: inline; } + .recent-card, .recent-cover { width: var(--cover-w); } + .recent-cover { height: calc(var(--cover-w) * 4 / 3); } + .recent-title { font-size: 17px; } .card { padding: 18px 32px; } .row { flex-wrap: nowrap; align-items: center; } - .cover .monogram { font-size: 26px; } - .title { font-size: 21px; } + .cover .monogram { font-size: 28px; } + .title { font-size: 22px; } .actions { flex: none; gap: 4px; border-top: none; } .actions > * { @@ -732,6 +790,10 @@ button { cursor: pointer; } .actions > *:not(.lifecycle) + .lifecycle { margin-left: 10px; box-shadow: none; } .is-new .actions .play { border-color: var(--play-hot-line); } .actions .on { border-color: var(--fav-line); } + /* The cell border follows the icon on hover, so the accent reads as a state + rather than a stray colour. */ + .actions .fav:hover, .actions .pencil:hover, + .actions .box:hover, .actions .finish:hover { border-color: currentColor; } /* Panels line up with the body text, i.e. past the cover and its gap. */ .chapter-form, .confirm-row, .error-inline { diff --git a/backend/templates/app.html b/backend/templates/app.html index 538d541..8699b35 100644 --- a/backend/templates/app.html +++ b/backend/templates/app.html @@ -61,6 +61,8 @@ + {{template "keyrow" .}} + {{template "recent" .}}
diff --git a/backend/templates/chrome.html b/backend/templates/chrome.html index 83f54db..cc9d898 100644 --- a/backend/templates/chrome.html +++ b/backend/templates/chrome.html @@ -1,9 +1,9 @@ -{{/* The two regions that live outside the swapped #list: the "Continue - reading" strip and the Updated badge. Both are rendered inline by app.html - and again, out of band, on every /ui/ response — a mutation must not leave - them describing the library as it was before the tap. +{{/* The regions that live outside the swapped #list: the "Continue reading" + strip, the Updated badge and the action key. All are rendered inline by + app.html and again, out of band, on every /ui/ response — a mutation must + not leave them describing the library as it was before the tap. - Both always render, hidden when they have nothing to say, so an out-of-band + All always render, hidden when they have nothing to say, so an out-of-band swap always has an element with the right id to replace. */}} {{define "recent"}} @@ -27,4 +27,25 @@ {{end}} +{{/* The action key. The icon strip on a card is unlabelled, so one permanent + line under the tabs names every glyph. It follows the tab rather than the + row: the archived and finished buckets swap Archive for Restore, and a + finished series has no Done to offer. */}} +{{define "keyrow"}} +
+ Read + Fav + Chapter + {{if or (eq .Tab "archived") (eq .Tab "finished")}} + Restore + {{else}} + Archive + {{end}} + {{if ne .Tab "finished"}} + Done + {{end}} + Delete +
+{{end}} + {{define "newcount"}}{{.NewCount}}{{end}} diff --git a/backend/web.go b/backend/web.go index b3368db..e30a4f1 100644 --- a/backend/web.go +++ b/backend/web.go @@ -248,12 +248,14 @@ func currentTab(r *http.Request) string { return u.Query().Get("tab") } -// writeChromeOOB appends the two regions that live outside #list — the recent -// strip and the Updated badge — as out-of-band swaps, so a mutation cannot -// leave them describing the library as it was before the tap. +// writeChromeOOB appends the regions that live outside #list — the recent +// strip, the Updated badge and the action key — as out-of-band swaps, so a +// mutation cannot leave them describing the library as it was before the tap. +// The key is in here because it is tab-shaped too: archived and finished swap +// Archive for Restore. func (h *webHandler) writeChromeOOB(w http.ResponseWriter, view listView) { view.OOB = true - for _, name := range []string{"recent", "newcount"} { + for _, name := range []string{"recent", "newcount", "keyrow"} { if err := h.tmpl.ExecuteTemplate(w, name, view); err != nil { // The card is already written; stale chrome beats a torn response. log.Printf("render %s oob: %v", name, err) -- 2.52.0 From 51839762e71fa0cfd18c8b45b1930096f9ca6cef Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Fri, 31 Jul 2026 17:12:53 +0700 Subject: [PATCH 2/2] feat(ui): brand mark logo in the web UI, panel, and favicon The mark is the same drawing in three places with three different colour sources: /static/logo.svg carries a fixed palette because a favicon has no page to inherit from, the {{define "mark"}} block takes --ink/--ember so it flips with the theme, and the userscript copy lives inside the shadow root next to the tokens it uses. Co-Authored-By: Claude Opus 5 --- backend/static/logo.svg | 23 ++++++++++++++++++++++ backend/static/style.css | 10 +++++++++- backend/templates/app.html | 3 ++- backend/templates/chrome.html | 27 ++++++++++++++++++++++++++ backend/templates/login.html | 3 ++- backend/web_test.go | 2 +- userscript/manga-bookmark.user.js | 32 +++++++++++++++++++++++++++++-- 7 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 backend/static/logo.svg diff --git a/backend/static/logo.svg b/backend/static/logo.svg new file mode 100644 index 0000000..8571fba --- /dev/null +++ b/backend/static/logo.svg @@ -0,0 +1,23 @@ + + mangaBookmark + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/static/style.css b/backend/static/style.css index 7ccbda7..c1e8ce7 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -197,14 +197,21 @@ button { cursor: pointer; } /* ---- brand + chrome ---- */ .brand { margin: 0; + display: flex; + align-items: center; + gap: 10px; font: 400 26px/1 var(--font-display); color: var(--paper); } .brand em { color: var(--ember); font-style: italic; } +.brand .mark { width: 30px; height: 26px; flex: none; overflow: visible; } +/* The line art is drawn at a 5px stroke on a 200-unit grid; at brand size that + thins out, so it is nudged up rather than scaled down blindly. */ +.brand .mark g { stroke-width: 6.5; } .topbar { display: flex; - align-items: baseline; + align-items: center; justify-content: space-between; gap: 16px; padding: 22px 20px 14px; @@ -745,6 +752,7 @@ button { cursor: pointer; } :root { --cover-w: 80px; --row-gap: 20px; } .topbar { padding: 26px 32px 18px; } .brand { font-size: 30px; } + .brand .mark { width: 35px; height: 30px; } .chrome { flex-direction: row; diff --git a/backend/templates/app.html b/backend/templates/app.html index 8699b35..10d5342 100644 --- a/backend/templates/app.html +++ b/backend/templates/app.html @@ -6,6 +6,7 @@ mangaBookmark + {{/* Body text before meta lines: DM Sans is the biggest face and the one @@ -18,7 +19,7 @@ {{template "icons" .}}
-

mangaBookmark

+

{{template "mark" .}}mangaBookmark

diff --git a/backend/templates/chrome.html b/backend/templates/chrome.html index cc9d898..7537c9a 100644 --- a/backend/templates/chrome.html +++ b/backend/templates/chrome.html @@ -48,4 +48,31 @@
{{end}} +{{/* The brand mark, inline so it takes the page's ink and ember rather than the + fixed palette of /static/logo.svg (which the favicon needs). */}} +{{define "mark"}} + +{{end}} + {{define "newcount"}}{{.NewCount}}{{end}} diff --git a/backend/templates/login.html b/backend/templates/login.html index c6cc228..1542737 100644 --- a/backend/templates/login.html +++ b/backend/templates/login.html @@ -6,6 +6,7 @@ mangaBookmark + @@ -13,7 +14,7 @@
Private library -

mangaBookmark

+

{{template "mark" .}}mangaBookmark

diff --git a/backend/web_test.go b/backend/web_test.go index a37bd0f..44f90fa 100644 --- a/backend/web_test.go +++ b/backend/web_test.go @@ -189,7 +189,7 @@ func TestBookmarksAPIStillBearerOnly(t *testing.T) { func TestStaticAssetsServed(t *testing.T) { srv, _ := newWebTestServer(t, webConfig()) - for _, path := range []string{"/static/style.css", "/static/htmx.min.js", "/static/filter.js"} { + for _, path := range []string{"/static/style.css", "/static/htmx.min.js", "/static/filter.js", "/static/logo.svg"} { rr := httptest.NewRecorder() srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil)) if rr.Code != http.StatusOK { diff --git a/userscript/manga-bookmark.user.js b/userscript/manga-bookmark.user.js index 1c36ebf..9392de1 100644 --- a/userscript/manga-bookmark.user.js +++ b/userscript/manga-bookmark.user.js @@ -1383,7 +1383,31 @@