From 3c935ba7c3d189ac0b0c4e1a3fcf7e0c359479b4 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Fri, 31 Jul 2026 17:14:15 +0700 Subject: [PATCH] Action key under the tabs, plus the brand mark (#12) 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. This adds a permanent key line under the tabs naming each one. The key is tab-shaped, not row-shaped: archived and finished swap Archive for Restore, and finished drops Done — the same conditions card.html already uses for the buttons. It rides along in writeChromeOOB, otherwise an htmx tab switch would leave the previous bucket's key behind. Also from the Claude Design pass: • per-action accent on hover/press — slate archive, moss finished, clay chapter — so a press says which lane it belongs to; ember stays reserved for new-chapter heat • cover 74→80px desktop / 86→93px phone, title 19→21px, meta 10→11px • favourite mark pinned to the right edge of the measure instead of trailing the title, which drifted whenever a new-chapter title shrink-wrapped • recent strip sizes now derive from --cover-w instead of repeating magic numbers ### Brand mark The design project grew a logo, so it lands in three places with three different colour sources: • `backend/static/logo.svg` — fixed palette, because a favicon has no page to inherit from. Linked as `rel="icon"` from both the app and the login page. • `{{define "mark"}}` in chrome.html — takes `--ink`/`--ember`/currentColor, so the header mark flips with the light/dark theme. Used by app.html and login.html. • the userscript panel header — same drawing inside the shadow root, next to the tokens it uses. `.brand` becomes a flex row in both stylesheets and the topbar aligns centre rather than baseline. The 5px stroke on the 200-unit grid thins out at brand size, so it is nudged to 6.5 instead of scaled blindly. No mark on the edge tab: a 7px sliver has nowhere to put one. go test ./... passes (static-asset test now covers logo.svg), node --test on the userscript logic suite passes 14/14. Verified live at 412px and desktop: key renders, per-tab variant correct on /?tab=finished (play/star/pencil/undo/ trash, no Done), and the OOB swap fires on /ui/list. Reviewed-on: https://gitea.violetcrown.my.id/sulthan/mangaBookmark/pulls/12 Co-authored-by: Sulthan Zaki Co-committed-by: Sulthan Zaki --- backend/static/logo.svg | 23 +++++++ backend/static/style.css | 106 +++++++++++++++++++++++++----- backend/templates/app.html | 5 +- backend/templates/chrome.html | 58 ++++++++++++++-- backend/templates/login.html | 3 +- backend/web.go | 10 +-- backend/web_test.go | 2 +- userscript/manga-bookmark.user.js | 32 ++++++++- 8 files changed, 207 insertions(+), 32 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 ec5b736..c1e8ce7 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; @@ -189,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; @@ -285,6 +300,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 +355,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 +363,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 +372,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 +380,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 +418,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 +431,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 +439,7 @@ button { cursor: pointer; } min-width: 0; display: flex; flex-direction: column; + justify-content: center; gap: 8px; } @@ -403,7 +448,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 +461,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 +471,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 +481,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 +506,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,9 +749,10 @@ 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; } + .brand .mark { width: 35px; height: 30px; } .chrome { flex-direction: row; @@ -711,13 +765,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 +798,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..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

@@ -61,6 +62,8 @@
+ {{template "keyrow" .}} + {{template "recent" .}}
diff --git a/backend/templates/chrome.html b/backend/templates/chrome.html index 83f54db..7537c9a 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,52 @@ {{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}} + +{{/* 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.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) 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 @@