Action key under the tabs, plus the brand mark (#12)
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: #12
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
This commit was merged in pull request #12.
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 172" role="img" aria-label="mangaBookmark">
|
||||||
|
<title>mangaBookmark</title>
|
||||||
|
<g fill="#100f0e" stroke="#f2ece5" stroke-width="5" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<path fill="none" d="M28 36H4v114h192V36h-24"></path>
|
||||||
|
<path fill="none" d="M28 23H17v127h166V23h-11"></path>
|
||||||
|
<g id="mb-half">
|
||||||
|
<path d="M28 7 88 55v97L28 138z"></path>
|
||||||
|
<g fill="#f2ece5" stroke="none">
|
||||||
|
<path d="M37 25 55 39v41L37 66z"></path>
|
||||||
|
<path d="M60 42 79 57v42L60 84z"></path>
|
||||||
|
<path d="M37 75 79 108v13L37 88z"></path>
|
||||||
|
<path d="M37 98 79 129v11L37 131z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<use href="#mb-half" transform="matrix(-1 0 0 1 200 0)"></use>
|
||||||
|
<g stroke="#e0452c">
|
||||||
|
<path d="M100 4l9 5v11l-9 5-9-5V9z"></path>
|
||||||
|
<path d="M94 24h12v24H94z"></path>
|
||||||
|
<path d="M70 47h60v14H70z"></path>
|
||||||
|
<path d="M91 61h18v87l-9 20-9-20z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 973 B |
+88
-18
@@ -81,6 +81,11 @@
|
|||||||
--danger-ink: #150808;
|
--danger-ink: #150808;
|
||||||
--danger-soft: #e2aaa1;
|
--danger-soft: #e2aaa1;
|
||||||
--brass: #b8912f; /* favourite — a second, cooler metal */
|
--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 */
|
--trash: #977671; /* remove, resting — icons need 3:1, not 4.5:1 */
|
||||||
|
|
||||||
/* Desktop cell borders for the two coloured action states. */
|
/* Desktop cell borders for the two coloured action states. */
|
||||||
@@ -98,7 +103,7 @@
|
|||||||
--measure: 760px;
|
--measure: 760px;
|
||||||
/* Cover width + row gap: the disclosure panels indent past the cover on
|
/* Cover width + row gap: the disclosure panels indent past the cover on
|
||||||
desktop, so both live here rather than as magic numbers. */
|
desktop, so both live here rather than as magic numbers. */
|
||||||
--cover-w: 86px;
|
--cover-w: 93px;
|
||||||
--row-gap: 14px;
|
--row-gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,6 +136,9 @@
|
|||||||
--danger-ink: #fff;
|
--danger-ink: #fff;
|
||||||
--danger-soft: #7c2c22;
|
--danger-soft: #7c2c22;
|
||||||
--brass: #8a681c;
|
--brass: #8a681c;
|
||||||
|
--slate: #3f6689;
|
||||||
|
--moss: #3d6c46;
|
||||||
|
--clay: #7c5533;
|
||||||
--trash: #8c6558;
|
--trash: #8c6558;
|
||||||
|
|
||||||
--play-hot-line: #f0cfc6;
|
--play-hot-line: #f0cfc6;
|
||||||
@@ -189,14 +197,21 @@ button { cursor: pointer; }
|
|||||||
/* ---- brand + chrome ---- */
|
/* ---- brand + chrome ---- */
|
||||||
.brand {
|
.brand {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
font: 400 26px/1 var(--font-display);
|
font: 400 26px/1 var(--font-display);
|
||||||
color: var(--paper);
|
color: var(--paper);
|
||||||
}
|
}
|
||||||
.brand em { color: var(--ember); font-style: italic; }
|
.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 {
|
.topbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
padding: 22px 20px 14px;
|
padding: 22px 20px 14px;
|
||||||
@@ -285,6 +300,35 @@ button { cursor: pointer; }
|
|||||||
border: 1px solid currentColor;
|
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 ---- */
|
/* ---- continue reading ---- */
|
||||||
.recent {
|
.recent {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -311,7 +355,7 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.recent-strip::-webkit-scrollbar { display: none; }
|
.recent-strip::-webkit-scrollbar { display: none; }
|
||||||
.recent-card {
|
.recent-card {
|
||||||
width: 92px;
|
width: var(--cover-w);
|
||||||
flex: none;
|
flex: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -319,8 +363,8 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.recent-cover {
|
.recent-cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 92px;
|
width: var(--cover-w);
|
||||||
height: 123px;
|
height: calc(var(--cover-w) * 4 / 3);
|
||||||
background: var(--hatch);
|
background: var(--hatch);
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -328,7 +372,7 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.recent-cover img { width: 100%; height: 100%; object-fit: cover; }
|
.recent-cover img { width: 100%; height: 100%; object-fit: cover; }
|
||||||
.recent-title {
|
.recent-title {
|
||||||
font: 400 15px/1.2 var(--font-display);
|
font: 400 16px/1.25 var(--font-display);
|
||||||
color: var(--paper-dim);
|
color: var(--paper-dim);
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 2;
|
||||||
@@ -336,7 +380,7 @@ button { cursor: pointer; }
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.recent-chapter {
|
.recent-chapter {
|
||||||
font: 500 10px/1 var(--font-mono);
|
font: 500 11px/1 var(--font-mono);
|
||||||
letter-spacing: .1em;
|
letter-spacing: .1em;
|
||||||
color: var(--mute-2);
|
color: var(--mute-2);
|
||||||
}
|
}
|
||||||
@@ -374,7 +418,7 @@ button { cursor: pointer; }
|
|||||||
}
|
}
|
||||||
.card.is-dim { background: var(--dim); }
|
.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 {
|
.cover {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -387,7 +431,7 @@ button { cursor: pointer; }
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.cover img { width: 100%; height: 100%; object-fit: cover; }
|
.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; }
|
.is-dim .cover { background: var(--hatch-dim); filter: grayscale(1); opacity: .85; }
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
@@ -395,6 +439,7 @@ button { cursor: pointer; }
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -403,7 +448,7 @@ button { cursor: pointer; }
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font: 400 19px/1.2 var(--font-display);
|
font: 400 21px/1.2 var(--font-display);
|
||||||
color: var(--paper-dim);
|
color: var(--paper-dim);
|
||||||
}
|
}
|
||||||
/* Heat: crimson title over an ember hairline sized to the text, not the row. */
|
/* 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 .title { font-style: italic; color: var(--mute); }
|
||||||
.is-dim .fav-mark { color: var(--mute-2); }
|
.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 {
|
.meta {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -424,7 +471,7 @@ button { cursor: pointer; }
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font: 500 10px/1 var(--font-mono);
|
font: 500 11px/1 var(--font-mono);
|
||||||
letter-spacing: .12em;
|
letter-spacing: .12em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
color: var(--mute);
|
color: var(--mute);
|
||||||
@@ -434,7 +481,7 @@ button { cursor: pointer; }
|
|||||||
.site-demonic { color: var(--demonic); }
|
.site-demonic { color: var(--demonic); }
|
||||||
.new-chapter { color: var(--ember); }
|
.new-chapter { color: var(--ember); }
|
||||||
.state { display: flex; align-items: center; gap: 4px; color: var(--mute); }
|
.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 .meta { color: var(--mute-2); }
|
||||||
.is-dim .site-asura, .is-dim .site-demonic { color: var(--mute); filter: grayscale(.6); }
|
.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 > *:last-child { border-right: none; }
|
||||||
.actions svg { width: 17px; height: 17px; }
|
.actions svg { width: 17px; height: 17px; }
|
||||||
.actions > *:hover { color: var(--paper); }
|
.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); }
|
.actions .play { color: var(--paper); }
|
||||||
.is-new .actions .play { color: var(--ember); }
|
.is-new .actions .play { color: var(--ember); }
|
||||||
.actions .play:hover { background: var(--hover); }
|
.actions .play:hover { background: var(--hover); }
|
||||||
@@ -696,9 +749,10 @@ button { cursor: pointer; }
|
|||||||
|
|
||||||
/* ---- desktop: same measure, actions fold up beside the row ---- */
|
/* ---- desktop: same measure, actions fold up beside the row ---- */
|
||||||
@media (min-width: 720px) {
|
@media (min-width: 720px) {
|
||||||
:root { --cover-w: 74px; --row-gap: 20px; }
|
:root { --cover-w: 80px; --row-gap: 20px; }
|
||||||
.topbar { padding: 26px 32px 18px; }
|
.topbar { padding: 26px 32px 18px; }
|
||||||
.brand { font-size: 30px; }
|
.brand { font-size: 30px; }
|
||||||
|
.brand .mark { width: 35px; height: 30px; }
|
||||||
|
|
||||||
.chrome {
|
.chrome {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -711,13 +765,25 @@ button { cursor: pointer; }
|
|||||||
.tabs a { padding: 10px 0 14px; font-size: 18px; }
|
.tabs a { padding: 10px 0 14px; font-size: 18px; }
|
||||||
.searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; }
|
.searchbar { order: 2; flex: 1; margin: 0; border-bottom: none; }
|
||||||
.recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; }
|
.recent h2, .recent-strip { padding-left: 32px; padding-right: 32px; }
|
||||||
.recent-card, .recent-cover { width: 100px; }
|
.keyrow {
|
||||||
.recent-cover { height: 133px; }
|
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; }
|
.card { padding: 18px 32px; }
|
||||||
.row { flex-wrap: nowrap; align-items: center; }
|
.row { flex-wrap: nowrap; align-items: center; }
|
||||||
.cover .monogram { font-size: 26px; }
|
.cover .monogram { font-size: 28px; }
|
||||||
.title { font-size: 21px; }
|
.title { font-size: 22px; }
|
||||||
|
|
||||||
.actions { flex: none; gap: 4px; border-top: none; }
|
.actions { flex: none; gap: 4px; border-top: none; }
|
||||||
.actions > * {
|
.actions > * {
|
||||||
@@ -732,6 +798,10 @@ button { cursor: pointer; }
|
|||||||
.actions > *:not(.lifecycle) + .lifecycle { margin-left: 10px; box-shadow: none; }
|
.actions > *:not(.lifecycle) + .lifecycle { margin-left: 10px; box-shadow: none; }
|
||||||
.is-new .actions .play { border-color: var(--play-hot-line); }
|
.is-new .actions .play { border-color: var(--play-hot-line); }
|
||||||
.actions .on { border-color: var(--fav-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. */
|
/* Panels line up with the body text, i.e. past the cover and its gap. */
|
||||||
.chapter-form, .confirm-row, .error-inline {
|
.chapter-form, .confirm-row, .error-inline {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>mangaBookmark</title>
|
<title>mangaBookmark</title>
|
||||||
|
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
{{/* Body text before meta lines: DM Sans is the biggest face and the one
|
{{/* Body text before meta lines: DM Sans is the biggest face and the one
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
{{template "icons" .}}
|
{{template "icons" .}}
|
||||||
<div class="sheet">
|
<div class="sheet">
|
||||||
<header class="topbar">
|
<header class="topbar">
|
||||||
<h1 class="brand">manga<em>Bookmark</em></h1>
|
<h1 class="brand">{{template "mark" .}}<span>manga<em>Bookmark</em></span></h1>
|
||||||
<form method="post" action="/logout">
|
<form method="post" action="/logout">
|
||||||
<button type="submit" class="ghost">Log out</button>
|
<button type="submit" class="ghost">Log out</button>
|
||||||
</form>
|
</form>
|
||||||
@@ -61,6 +62,8 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{template "keyrow" .}}
|
||||||
|
|
||||||
{{template "recent" .}}
|
{{template "recent" .}}
|
||||||
|
|
||||||
<main id="list" class="list">
|
<main id="list" class="list">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{{/* The two regions that live outside the swapped #list: the "Continue
|
{{/* The regions that live outside the swapped #list: the "Continue reading"
|
||||||
reading" strip and the Updated badge. Both are rendered inline by app.html
|
strip, the Updated badge and the action key. All are rendered inline by
|
||||||
and again, out of band, on every /ui/ response — a mutation must not leave
|
app.html and again, out of band, on every /ui/ response — a mutation must
|
||||||
them describing the library as it was before the tap.
|
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. */}}
|
swap always has an element with the right id to replace. */}}
|
||||||
|
|
||||||
{{define "recent"}}
|
{{define "recent"}}
|
||||||
@@ -27,4 +27,52 @@
|
|||||||
</section>
|
</section>
|
||||||
{{end}}
|
{{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"}}
|
||||||
|
<div class="keyrow" id="keyrow" aria-label="Action key"{{if .OOB}} hx-swap-oob="true"{{end}}>
|
||||||
|
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-play"/></svg><span>Read</span></span>
|
||||||
|
<span class="pair brass"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-star"/></svg><span>Fav</span></span>
|
||||||
|
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-pencil"/></svg><span>Chapter</span></span>
|
||||||
|
{{if or (eq .Tab "archived") (eq .Tab "finished")}}
|
||||||
|
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-undo"/></svg><span>Restore</span></span>
|
||||||
|
{{else}}
|
||||||
|
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-box"/></svg><span>Archive</span></span>
|
||||||
|
{{end}}
|
||||||
|
{{if ne .Tab "finished"}}
|
||||||
|
<span class="pair"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-check"/></svg><span>Done</span></span>
|
||||||
|
{{end}}
|
||||||
|
<span class="pair trash"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-trash"/></svg><span>Delete</span></span>
|
||||||
|
</div>
|
||||||
|
{{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"}}
|
||||||
|
<svg class="mark" viewBox="0 0 200 172" aria-hidden="true">
|
||||||
|
<g fill="var(--ink)" stroke="currentColor" stroke-width="5" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<path fill="none" d="M28 36H4v114h192V36h-24"></path>
|
||||||
|
<path fill="none" d="M28 23H17v127h166V23h-11"></path>
|
||||||
|
<g id="mb-half">
|
||||||
|
<path d="M28 7 88 55v97L28 138z"></path>
|
||||||
|
<g fill="currentColor" stroke="none">
|
||||||
|
<path d="M37 25 55 39v41L37 66z"></path>
|
||||||
|
<path d="M60 42 79 57v42L60 84z"></path>
|
||||||
|
<path d="M37 75 79 108v13L37 88z"></path>
|
||||||
|
<path d="M37 98 79 129v11L37 131z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<use href="#mb-half" transform="matrix(-1 0 0 1 200 0)"></use>
|
||||||
|
<g stroke="var(--ember)">
|
||||||
|
<path d="M100 4l9 5v11l-9 5-9-5V9z"></path>
|
||||||
|
<path d="M94 24h12v24H94z"></path>
|
||||||
|
<path d="M70 47h60v14H70z"></path>
|
||||||
|
<path d="M91 61h18v87l-9 20-9-20z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
{{define "newcount"}}<span class="count" id="new-count"{{if .OOB}} hx-swap-oob="true"{{end}}{{if not .NewCount}} hidden{{end}}>{{.NewCount}}</span>{{end}}
|
{{define "newcount"}}<span class="count" id="new-count"{{if .OOB}} hx-swap-oob="true"{{end}}{{if not .NewCount}} hidden{{end}}>{{.NewCount}}</span>{{end}}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>mangaBookmark</title>
|
<title>mangaBookmark</title>
|
||||||
|
<link rel="icon" href="/static/logo.svg" type="image/svg+xml">
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
<link rel="preload" href="/static/fonts/instrument-serif-400-latin.woff2" as="font" type="font/woff2" crossorigin>
|
||||||
</head>
|
</head>
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
<main class="login-card">
|
<main class="login-card">
|
||||||
<div>
|
<div>
|
||||||
<span class="eyebrow">Private library</span>
|
<span class="eyebrow">Private library</span>
|
||||||
<h1>manga<em>Bookmark</em></h1>
|
<h1 class="brand">{{template "mark" .}}<span>manga<em>Bookmark</em></span></h1>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/login">
|
<form method="post" action="/login">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+6
-4
@@ -248,12 +248,14 @@ func currentTab(r *http.Request) string {
|
|||||||
return u.Query().Get("tab")
|
return u.Query().Get("tab")
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeChromeOOB appends the two regions that live outside #list — the recent
|
// writeChromeOOB appends the regions that live outside #list — the recent
|
||||||
// strip and the Updated badge — as out-of-band swaps, so a mutation cannot
|
// strip, the Updated badge and the action key — as out-of-band swaps, so a
|
||||||
// leave them describing the library as it was before the tap.
|
// 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) {
|
func (h *webHandler) writeChromeOOB(w http.ResponseWriter, view listView) {
|
||||||
view.OOB = true
|
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 {
|
if err := h.tmpl.ExecuteTemplate(w, name, view); err != nil {
|
||||||
// The card is already written; stale chrome beats a torn response.
|
// The card is already written; stale chrome beats a torn response.
|
||||||
log.Printf("render %s oob: %v", name, err)
|
log.Printf("render %s oob: %v", name, err)
|
||||||
|
|||||||
+1
-1
@@ -189,7 +189,7 @@ func TestBookmarksAPIStillBearerOnly(t *testing.T) {
|
|||||||
|
|
||||||
func TestStaticAssetsServed(t *testing.T) {
|
func TestStaticAssetsServed(t *testing.T) {
|
||||||
srv, _ := newWebTestServer(t, webConfig())
|
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()
|
rr := httptest.NewRecorder()
|
||||||
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
|
srv.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, path, nil))
|
||||||
if rr.Code != http.StatusOK {
|
if rr.Code != http.StatusOK {
|
||||||
|
|||||||
@@ -1383,7 +1383,31 @@
|
|||||||
<div id="backdrop"></div>
|
<div id="backdrop"></div>
|
||||||
<aside id="panel" role="dialog" aria-label="Manga bookmarks">
|
<aside id="panel" role="dialog" aria-label="Manga bookmarks">
|
||||||
<header>
|
<header>
|
||||||
<span>manga<em>Bookmark</em></span>
|
<span class="brand">
|
||||||
|
<svg class="mark" viewBox="0 0 200 172" aria-hidden="true">
|
||||||
|
<g fill="var(--ink)" stroke="currentColor" stroke-width="6.5" stroke-linejoin="round" stroke-linecap="round">
|
||||||
|
<path fill="none" d="M28 36H4v114h192V36h-24"></path>
|
||||||
|
<path fill="none" d="M28 23H17v127h166V23h-11"></path>
|
||||||
|
<g id="mb-half">
|
||||||
|
<path d="M28 7 88 55v97L28 138z"></path>
|
||||||
|
<g fill="currentColor" stroke="none">
|
||||||
|
<path d="M37 25 55 39v41L37 66z"></path>
|
||||||
|
<path d="M60 42 79 57v42L60 84z"></path>
|
||||||
|
<path d="M37 75 79 108v13L37 88z"></path>
|
||||||
|
<path d="M37 98 79 129v11L37 131z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<use href="#mb-half" transform="matrix(-1 0 0 1 200 0)"></use>
|
||||||
|
<g stroke="var(--ember)">
|
||||||
|
<path d="M100 4l9 5v11l-9 5-9-5V9z"></path>
|
||||||
|
<path d="M94 24h12v24H94z"></path>
|
||||||
|
<path d="M70 47h60v14H70z"></path>
|
||||||
|
<path d="M91 61h18v87l-9 20-9-20z"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<span>manga<em>Bookmark</em></span>
|
||||||
|
</span>
|
||||||
<button id="closeBtn" aria-label="Close">close</button>
|
<button id="closeBtn" aria-label="Close">close</button>
|
||||||
</header>
|
</header>
|
||||||
<div id="nav">
|
<div id="nav">
|
||||||
@@ -1458,11 +1482,15 @@
|
|||||||
}
|
}
|
||||||
#panel.open { transform: translateX(0); }
|
#panel.open { transform: translateX(0); }
|
||||||
header {
|
header {
|
||||||
display: flex; align-items: baseline; justify-content: space-between;
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
padding: 16px; border-bottom: 1px solid var(--rule);
|
padding: 16px; border-bottom: 1px solid var(--rule);
|
||||||
font: 400 22px/1 var(--font-display); color: var(--paper);
|
font: 400 22px/1 var(--font-display); color: var(--paper);
|
||||||
}
|
}
|
||||||
header em { color: var(--ember); font-style: italic; }
|
header em { color: var(--ember); font-style: italic; }
|
||||||
|
.brand { display: flex; align-items: center; gap: 9px; }
|
||||||
|
/* 5px stroke on a 200-unit grid thins out at this size: the markup carries
|
||||||
|
6.5 instead. overflow visible keeps the topmost blade pip from clipping. */
|
||||||
|
.mark { width: 26px; height: 22px; flex: none; overflow: visible; }
|
||||||
#closeBtn {
|
#closeBtn {
|
||||||
background: none; border: none; color: var(--mute); cursor: pointer;
|
background: none; border: none; color: var(--mute); cursor: pointer;
|
||||||
font: 500 10px var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
|
font: 500 10px var(--font-mono); letter-spacing: .12em; text-transform: uppercase;
|
||||||
|
|||||||
Reference in New Issue
Block a user