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
7 changed files with 723 additions and 462 deletions
Showing only changes of commit e940b96eae - Show all commits
+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
+574 -365
View File
@@ -1,53 +1,81 @@
/* Mobile first. Dark by default because manga reading happens at night; the
light branch follows the system preference. */
/* Cinder — editorial serif, containerless sheets, ash hairlines.
Heat is purely typographic: a series with an unread chapter turns its title
crimson and gains an ember underline. Everything else stays cool. Dark is
the default because this is read at night on a phone; the light branch
re-tunes the same tokens onto paper. */
:root {
color-scheme: dark light;
--bg: #14161a;
--surface: #1d2026;
--surface-2: #262a32;
--text: #e8eaed;
--muted: #9aa1ac;
--accent: #6aa9ff;
--accent-ink: #0b1220;
--danger: #ff6a6a;
--danger-ink: #2a0a0a;
--star: #ffc857;
/* A newly-published chapter is its own signal, distinct from "tap to act"
(accent) or "favourited" (star) — a third hue keeps those three meanings
from collapsing into one overloaded blue. */
--new: #34d8a6;
--new-ink: #0b1220;
/* Per-site tint so the two source sites this app exists to unify are
still tellable apart at a glance, not just by reading the label text. */
--site-asura-bg: #3a2a20;
--site-asura-text: #ffb37a;
--site-demonic-bg: #2e2440;
--site-demonic-text: #c9a8ff;
--radius: 12px;
--font-display: "Instrument Serif", Georgia, "Times New Roman", serif;
--font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--font-body: "DM Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
--ink: #100f0e; /* page */
--ash: #161413; /* recessed panel (chapter form) */
--dim: #0d0c0b; /* archived / finished rows sink */
--rule: #221f1d; /* hairline between sheets */
--rule-soft: #1a1817; /* measure edges */
--field-line: #2c2926;
--hover: #1a1816;
--paper: #f2ece5; /* highest-contrast text, primary button */
--paper-hot: #f0d3cb; /* title of a series with a new chapter */
--paper-dim: #ddd5cb; /* resting title */
--mute: #8d857c; /* secondary text, idle icons */
--mute-2: #5a5450; /* labels, hints */
--faint: #3a3733; /* meta separators */
--faint-2: #57504b; /* cover monogram */
--ember: #e0452c; /* the only heat */
--ember-wash: #1a1211; /* ember-tinted surface */
--ember-ink: #150907; /* text on solid ember */
--ember-soft: #eda798; /* text on ember wash */
--brass: #b8912f; /* favourite — a second, cooler metal */
--trash: #6b5450; /* remove, resting */
--asura: #7d93a5;
--demonic: #a98a78;
/* Covers are often missing; the hatch keeps the slot honest instead of
faking artwork. */
--hatch: repeating-linear-gradient(135deg, #211d1b 0 5px, #191614 5px 10px);
--hatch-dim: repeating-linear-gradient(135deg, #1b1918 0 5px, #151313 5px 10px);
--measure: 760px;
}
/* Light theme darkens the four semantic hues (and flips their ink) rather
than reusing the dark-theme values verbatim: the originals were tuned to
sit on near-black surfaces and fall well under WCAG AA the moment the
surface goes to white — verified via computed contrast, not eyeballed. */
/* Light mode: same rules, cooler paper. Hues are re-tuned, not reused — the
dark ember is too bright to read as text on off-white. */
@media (prefers-color-scheme: light) {
:root {
--bg: #f4f5f7;
--surface: #ffffff;
--surface-2: #eceef2;
--text: #1a1d22;
--muted: #5d646e;
--accent: #1f5fd6;
--accent-ink: #ffffff;
--danger: #b42323;
--danger-ink: #ffffff;
--star: #7d5f00;
--new: #087a5c;
--new-ink: #ffffff;
--site-asura-bg: #fde8d8;
--site-asura-text: #8a4a1f;
--site-demonic-bg: #f0e6fa;
--site-demonic-text: #6b2fa8;
--ink: #f7f4ef;
--ash: #efeae3;
--dim: #f1ede7;
--rule: #e0dad2;
--rule-soft: #e8e3dc;
--field-line: #d4cdc4;
--hover: #efeae3;
--paper: #191715;
--paper-hot: #a33018;
--paper-dim: #191715;
--mute: #6b645d;
--mute-2: #857d75;
--faint: #c9c2ba;
--faint-2: #a8a098;
--ember: #c23a22;
--ember-wash: #fbeee9;
--ember-ink: #fff;
--ember-soft: #8d2c17;
--brass: #8a681c;
--trash: #a98276;
--asura: #4f6b80;
--demonic: #8a6a55;
--hatch: repeating-linear-gradient(135deg, #e6e0d8 0 5px, #efeae3 5px 10px);
--hatch-dim: repeating-linear-gradient(135deg, #ebe6de 0 5px, #f2eee8 5px 10px);
}
}
@@ -55,350 +83,531 @@
body {
margin: 0;
padding: 0 12px calc(24px + env(safe-area-inset-bottom));
background: var(--bg);
color: var(--text);
font: 16px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: var(--ink);
color: var(--paper-dim);
font: 400 15px/1.5 var(--font-body);
display: flex;
justify-content: center;
-webkit-text-size-adjust: 100%;
}
/* Every interactive element clears the 44px touch-target floor. */
button, .primary, [role="tab"] {
min-height: 44px;
border-radius: var(--radius);
border: 0;
font: inherit;
cursor: pointer;
}
a { color: inherit; text-decoration: none; }
button, input { font: inherit; }
button { cursor: pointer; }
/* --- login --- */
/* Every hideable thing here is a flex container, and display beats hidden. */
[hidden] { display: none !important; }
.login-body {
display: grid;
place-items: center;
min-height: 100dvh;
}
@keyframes barSlide { from { transform: translateX(-100%) } to { transform: translateX(320%) } }
@keyframes sheetIn { from { opacity: 0; transform: translateY(-4px) } to { opacity: 1; transform: none } }
@keyframes emberPulse { 0%, 100% { opacity: .5 } 50% { opacity: 1 } }
.login-card {
width: min(380px, 100%);
padding: 24px;
background: var(--surface);
border-radius: var(--radius);
}
.login-card h1 { margin: 0 0 20px; font-size: 1.25rem; font-weight: 700; letter-spacing: -.01em; }
.login-card label { display: block; margin-bottom: 6px; color: var(--muted); font-size: .875rem; }
.login-card input {
/* One measured column, edges drawn rather than boxed. */
.sheet {
width: 100%;
min-height: 44px;
padding: 0 12px;
margin-bottom: 12px;
background: var(--surface-2);
color: var(--text);
border: 1px solid transparent;
border-radius: var(--radius);
font: inherit;
max-width: var(--measure);
min-height: 100vh;
display: flex;
flex-direction: column;
border-left: 1px solid var(--rule-soft);
border-right: 1px solid var(--rule-soft);
padding-bottom: env(safe-area-inset-bottom);
}
.login-card input:focus-visible { outline: 2px solid var(--accent); }
.login-card button { width: 100%; background: var(--accent); color: var(--accent-ink); font-weight: 600; }
.error { margin: 0 0 12px; color: var(--danger); font-size: .875rem; }
/* --- chrome --- */
/* ---- brand + chrome ---- */
.brand {
margin: 0;
font: 400 26px/1 var(--font-display);
color: var(--paper);
}
.brand em { color: var(--ember); font-style: italic; }
.topbar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
padding: 22px 20px 14px;
}
.topbar form { margin: 0; }
.ghost {
padding: 0;
border: none;
border-bottom: 1px solid var(--field-line);
background: transparent;
color: var(--mute);
font: 500 11px var(--font-mono);
letter-spacing: .1em;
text-transform: uppercase;
}
.ghost:hover { color: var(--paper); border-bottom-color: var(--paper); }
.chrome { display: flex; flex-direction: column; }
.searchbar {
display: flex;
align-items: center;
gap: 10px;
height: 44px;
margin: 0 20px 14px;
border-bottom: 1px solid var(--rule);
color: var(--mute-2);
}
.searchbar svg { width: 15px; height: 15px; flex: none; }
.search {
flex: 1;
min-width: 0;
height: 44px;
border: none;
background: transparent;
color: var(--paper);
font: 400 15px var(--font-body);
outline: none;
}
.search::placeholder { color: var(--mute-2); }
/* Tabs are set in the display serif and underlined, not chipped. */
.tabs {
display: flex;
gap: 16px;
padding: 2px 20px 0;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
border-bottom: 1px solid var(--rule);
}
.tabs::-webkit-scrollbar { display: none; }
.tabs [role="tab"] {
flex: none;
display: flex;
align-items: center;
gap: 7px;
padding: 8px 0 12px;
color: var(--mute);
font: 400 17px var(--font-display);
white-space: nowrap;
}
.tabs [role="tab"]:hover { color: var(--paper-dim); }
.tabs [role="tab"].active {
color: var(--paper);
border-bottom: 2px solid var(--paper);
margin-bottom: -1px;
}
/* Updated is the one tab that carries heat. */
.tabs .tab-new { color: var(--ember); }
.tabs .tab-new.active { border-bottom-color: var(--ember); }
.count {
font: 600 10px var(--font-mono);
letter-spacing: 0;
padding: 2px 5px;
border: 1px solid currentColor;
}
/* ---- continue reading ---- */
.recent {
display: flex;
flex-direction: column;
gap: 10px;
padding: 16px 0 18px;
border-bottom: 1px solid var(--rule);
}
.recent h2 {
margin: 0;
padding: 0 20px;
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
color: var(--mute-2);
}
.recent-strip {
display: flex;
gap: 14px;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
padding: 0 20px 4px;
}
.recent-strip::-webkit-scrollbar { display: none; }
.recent-card {
width: 92px;
flex: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.recent-cover {
position: relative;
width: 92px;
height: 123px;
background: var(--hatch);
display: grid;
place-items: center;
overflow: hidden;
}
.recent-cover img { width: 100%; height: 100%; object-fit: cover; }
.recent-title {
font: 400 15px/1.2 var(--font-display);
color: var(--paper-dim);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.recent-chapter {
font: 500 10px/1 var(--font-mono);
letter-spacing: .1em;
color: var(--mute-2);
}
.recent-card.is-new .recent-title { color: var(--paper-hot); }
.recent-card.is-new .recent-chapter { color: var(--ember); }
/* The rule at the cover foot is the only cover ornament: ember for a new
chapter, brass for a favourite. */
.foot-rule {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 3px;
background: var(--ember);
}
.foot-rule.brass { background: var(--brass); }
.monogram {
font: 400 32px var(--font-display);
color: var(--faint-2);
}
/* ---- rows ---- */
.list { display: flex; flex-direction: column; }
.card {
position: relative;
display: flex;
flex-direction: column;
gap: 12px;
padding: 18px 20px;
border-bottom: 1px solid var(--rule);
animation: sheetIn .18s ease-out;
}
.card.is-dim { background: var(--dim); }
.row { display: flex; flex-wrap: wrap; gap: 14px; }
.cover {
position: relative;
width: 66px;
height: 88px;
flex: none;
background: var(--hatch);
display: grid;
place-items: center;
overflow: hidden;
}
.cover img { width: 100%; height: 100%; object-fit: cover; }
.cover .monogram { font-size: 24px; }
.is-dim .cover { background: var(--hatch-dim); filter: grayscale(1); opacity: .85; }
.body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.title-line { display: flex; gap: 8px; align-items: flex-start; }
.title {
margin: 0;
flex: 1;
min-width: 0;
font: 400 19px/1.2 var(--font-display);
color: var(--paper-dim);
}
/* Heat: crimson title over an ember hairline sized to the text, not the row. */
.is-new .title {
flex: 0 1 auto;
align-self: flex-start;
color: var(--paper-hot);
border-bottom: 1px solid var(--ember);
padding-bottom: 3px;
}
.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); }
.meta {
margin: 0;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
font: 500 10px/1 var(--font-mono);
letter-spacing: .12em;
text-transform: uppercase;
color: var(--mute);
}
.meta .sep { color: var(--faint); }
.site-asura { color: var(--asura); }
.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; }
.is-dim .meta { color: var(--mute-2); }
.is-dim .site-asura, .is-dim .site-demonic { color: var(--mute); filter: grayscale(.6); }
/* ---- action strip: full-width on a phone, hairline-divided cells ---- */
.actions {
flex: 1 0 100%;
display: flex;
border-top: 1px solid var(--rule);
}
.actions > * {
flex: 1 1 0;
min-width: 0;
height: 46px;
display: grid;
place-items: center;
padding: 0;
border: none;
border-right: 1px solid var(--rule);
background: transparent;
color: var(--mute);
}
.actions > *:last-child { border-right: none; }
.actions svg { width: 17px; height: 17px; }
.actions > *:hover { color: var(--paper); }
.actions .play { color: var(--paper); }
.is-new .actions .play { color: var(--ember); }
.actions .play:hover { background: var(--hover); }
.actions .on { color: var(--brass); }
.actions .restore { color: var(--paper); }
.actions .remove { color: var(--trash); }
.actions .remove:hover { color: var(--ember); }
.actions .open { background: var(--hover); color: var(--paper); }
.actions .remove.open { background: var(--ember-wash); color: var(--ember); }
.is-dim .actions > * { color: var(--mute-2); }
/* ---- disclosure panels ---- */
.chapter-form, .confirm-row, .error-inline { animation: sheetIn .18s ease-out; }
.chapter-form {
display: flex;
flex-direction: column;
gap: 10px;
margin: 0;
padding: 14px;
background: var(--ash);
}
.chapter-form .hint {
margin: 0;
font: 500 10px/1 var(--font-mono);
letter-spacing: .14em;
text-transform: uppercase;
color: var(--mute-2);
}
.chapter-form .field { display: flex; gap: 10px; }
.chapter-form input {
flex: 1;
min-width: 0;
height: 46px;
padding: 0 12px;
border: 1px solid var(--field-line);
background: var(--ink);
color: var(--paper);
font: 500 17px var(--font-mono);
outline: none;
}
.chapter-form input:focus { border-color: var(--ember); }
.chapter-form input::-webkit-outer-spin-button,
.chapter-form input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.chapter-form button {
height: 46px;
padding: 0 20px;
border: none;
background: var(--paper);
color: var(--ink);
font: 400 17px var(--font-display);
}
.confirm-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px 0;
padding: 14px;
background: var(--ember-wash);
}
.topbar h1 { margin: 0; font-size: 1.125rem; font-weight: 700; letter-spacing: -.01em; }
.ghost { padding: 0 12px; background: var(--surface-2); color: var(--muted); }
.search {
width: 100%;
min-height: 44px;
padding: 0 12px;
margin-bottom: 12px;
background: var(--surface);
color: var(--text);
border: 1px solid transparent;
border-radius: var(--radius);
font: inherit;
.confirm-row span { font: 400 17px var(--font-display); color: var(--ember-soft); }
.confirm-row div { display: flex; gap: 8px; }
.confirm-row button {
height: 40px;
padding: 0 14px;
border: 1px solid var(--field-line);
background: transparent;
color: var(--mute);
font: 500 12px var(--font-body);
}
.search:focus-visible { outline: 2px solid var(--accent); }
/* Five buttons no longer divide a phone's width legibly, so the row scrolls
sideways instead of squeezing. The scrollbar is hidden: on mobile there is
none anyway, and on desktop it would sit under the tab labels. */
.tabs {
display: flex; gap: 8px; margin-bottom: 16px;
overflow-x: auto; scrollbar-width: none; -webkit-overflow-scrolling: touch;
.confirm-row button:hover { color: var(--paper); }
.confirm-row .danger-solid {
border: none;
background: var(--ember);
color: var(--ember-ink);
font-weight: 600;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs [role="tab"] {
flex: 1 0 auto;
padding-inline: 14px;
white-space: nowrap;
display: grid;
place-items: center;
background: var(--surface);
color: var(--muted);
text-decoration: none;
}
.tabs [role="tab"].active { background: var(--accent); color: var(--accent-ink); font-weight: 600; }
/* --- continue reading --- */
.recent h2 { margin: 0 0 8px; font-size: .8125rem; font-weight: 600; text-transform: uppercase;
letter-spacing: .04em; color: var(--muted); }
.recent-strip {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 8px;
margin-bottom: 16px;
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
}
.recent-card {
flex: 0 0 110px;
scroll-snap-align: start;
display: block;
padding: 8px;
background: var(--surface);
border-radius: var(--radius);
color: var(--text);
text-decoration: none;
}
/* A reserved slot even without cover art, so the ribbon always has a
dedicated area to sit in instead of overlapping the title text. */
.recent-cover {
position: relative;
display: block;
aspect-ratio: 3 / 4;
background: var(--surface-2);
border-radius: 8px;
overflow: hidden;
}
.recent-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.recent-title { display: block; margin-top: 6px; font-size: .8125rem; font-weight: 600; line-height: 1.35;
overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
.recent-chapter { display: block; color: var(--muted); font-size: .75rem; font-variant-numeric: tabular-nums; }
/* --- list --- */
.list { display: grid; gap: 10px; }
.card {
display: grid;
grid-template-columns: 96px 1fr;
gap: 12px;
padding: 10px;
background: var(--surface);
border-radius: var(--radius);
}
.card .cover {
position: relative;
display: block;
width: 96px;
aspect-ratio: 3 / 4;
background: var(--surface-2);
border-radius: 8px;
overflow: hidden;
}
.card .cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No og:image was ever scraped for this series. A bare --surface-2 rectangle
read as a cover still loading; the title's initial says the slot is filled
and nothing more is coming. Muted, not accent — a missing cover is an
absence, not a signal, and it must never outrank the New ribbon it sits
under. The two cover contexts share this rule and differ only in size. */
.cover-blank {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: var(--muted);
font-size: 2rem;
font-weight: 700;
line-height: 1;
letter-spacing: -.02em;
opacity: .65;
user-select: none;
}
.recent-cover .cover-blank { font-size: 1.625rem; }
.card .body { min-width: 0; }
/* 17px, not the 16px body floor: the title is the one thing per card the
squint test must catch first, so it needs a size step of its own, not
just the browser's default h3 bold riding on an unchanged size. */
.card .title { margin: 0 0 4px; font-size: 1.0625rem; font-weight: 700; line-height: 1.35; }
.meta { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin: 0 0 10px;
font-size: .75rem; color: var(--muted); }
.site { padding: 2px 6px; background: var(--surface-2); border-radius: 6px; text-transform: uppercase;
letter-spacing: .03em; }
/* Per-site tint: the two source sites this app unifies are otherwise
identical chrome, so this is the one glanceable "which site" cue beyond
the label text itself. */
.site-asura { background: var(--site-asura-bg); color: var(--site-asura-text); }
.site-demonic { background: var(--site-demonic-bg); color: var(--site-demonic-text); }
/* Read progress is data, not metadata, so it takes the body text color
instead of the muted tone the label pills around it use. */
.chapter { color: var(--text); font-variant-numeric: tabular-nums; }
.new-chapter { color: var(--new); font-weight: 600; font-variant-numeric: tabular-nums; }
/* The signal the poller exists to surface: a corner ribbon on the cover
itself outranks any line of card metadata, so it reads on the squint
test even in a dense grid, not just on close inspection. Its own hue
(not accent) keeps "new chapter" distinct from "tap to act". */
.new-ribbon {
position: absolute;
top: 6px;
left: 6px;
padding: 2px 7px;
background: var(--new);
color: var(--new-ink);
font-size: .6875rem; /* 11px: the functional-text legibility floor */
font-weight: 700;
letter-spacing: .02em;
text-transform: uppercase;
border-radius: 6px;
box-shadow: 0 2px 6px rgba(0, 0, 0, .35);
}
.new-ribbon-sm { top: 4px; left: 4px; padding: 1px 5px; }
/* One row of equal-sized icon buttons: Continue no longer stretches to
fill leftover space (that was both the wrap bug at narrow widths and
what made it read as arbitrarily oversized). It carries the same 44px
footprint as the utility icons and is told apart by fill color alone.
Grid columns capped at 44px, not flex items fixed at 44px: on the
narrowest real Android widths four 44px targets plus gaps don't quite
fit beside a 96px cover, and minmax(0, 44px) lets the row compress a
couple of px instead of wrapping — imperceptible, and never wraps. */
.actions {
display: grid;
grid-template-columns: repeat(4, minmax(0, 44px));
gap: 8px;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--surface-2);
}
.primary {
display: grid;
place-items: center;
background: var(--accent);
color: var(--accent-ink);
font-size: 1.125rem;
text-decoration: none;
}
.icon { display: grid; place-items: center; padding: 0;
background: var(--surface-2); color: var(--text); font-size: 1.125rem; }
.icon.on { color: var(--star); }
.icon.danger { color: var(--danger); }
/* Hand-drawn inline SVG rather than the OS emoji font: ☆ ✎ 🗑 ▶ each render in
a different face at a different optical weight and colour (some fonts force
their own), which is the one place the card ignored its own type and colour
system. currentColor keeps them on .icon / .icon.on / .icon.danger /
.primary without a second palette. 20px, not the 1.125rem font-size the
glyphs used: a glyph inks about two thirds of its em box, so matching the
old optical size means an SVG a step larger than the font-size it replaces. */
.icon svg, .primary svg { width: 20px; height: 20px; display: block; }
/* Wraps, like .actions and .confirm-row: the hint claims its own full-width
row (flex-basis 100%) so input + Save keep the whole card width below it.
Without the wrap the three children fought for one unwrapped row and pushed
Save off-screen — ~97px of page overflow at 390px, 127px at 360px. */
.chapter-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.chapter-form input {
/* flex-basis 0 + min-width 0, not flex: 1: a number input's default
intrinsic width is ~20 characters, so on `flex: 1` (basis auto) it refused
to shrink and pushed Save onto a third row. A chapter number needs five
characters, and the field can have whatever is left over. */
flex: 1 1 0;
min-width: 0;
min-height: 44px;
padding: 0 12px;
background: var(--surface-2);
color: var(--text);
border: 1px solid transparent;
border-radius: var(--radius);
font: inherit;
font-variant-numeric: tabular-nums;
}
.chapter-form button { padding: 0 14px; background: var(--accent); color: var(--accent-ink); font-weight: 600; }
/* flex: 1 0 100% — own row above the field. The margin-bottom is 0 because the
parent's 8px gap already spaces it; keeping both double-spaced the hint. */
.hint { flex: 1 0 100%; margin: 0; font-size: .75rem; color: var(--muted); font-variant-numeric: tabular-nums; }
/* Delete confirm, themed in place of a native confirm() dialog: that OS
chrome renders outside the app's own dark surface, breaking the
night-reading brand constraint at the one moment attention matters most. */
.confirm-row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; }
/* Own full-width row, same as .chapter-form's hint. Sharing one row with both
buttons broke the prompt into "Remove / this?" while its own answer sat
whole beside it; nowrap alone only converted that into 26px of page
overflow, because a nowrap flex item will not shrink past its text. */
.confirm-row span { flex: 1 0 100%; font-size: .8125rem; white-space: nowrap; }
.confirm-row .danger-solid { padding: 0 14px; background: var(--danger); color: var(--danger-ink); font-weight: 600; }
.confirm-row button[type="button"] { padding: 0 14px; background: var(--surface-2); color: var(--text); }
.empty { padding: 32px 12px; text-align: center; color: var(--muted); }
/* --- mutation feedback --- */
/* htmx adds .htmx-request to the element named by hx-indicator while a
request is in flight; dimming the whole card is the visible "it's working"
signal the templates otherwise never give. */
.card.htmx-request { opacity: .6; }
.error-inline {
margin: 8px 0 0;
padding: 6px 10px;
background: var(--surface-2);
color: var(--danger);
border-radius: 8px;
font-size: .8125rem;
display: flex;
align-items: center;
gap: 9px;
margin: 0;
padding: 11px 13px;
background: var(--ember-wash);
border-left: 2px solid var(--ember);
font: 400 16px var(--font-display);
color: var(--ember-soft);
}
.error-inline::before {
content: "";
flex: none;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--ember);
animation: emberPulse 1.4s ease-in-out infinite;
}
.error-inline[hidden] { display: none; }
/* Cards hidden by the search filter. The .chapter-form { display: flex }
rule above has equal specificity to the UA [hidden] rule, so without this
override the form stays visible even when the hidden attribute is set. */
.card[hidden] { display: none; }
.chapter-form[hidden] { display: none; }
.confirm-row[hidden] { display: none; }
/* --- wide screens --- */
@media (min-width: 900px) {
body { max-width: 1100px; margin: 0 auto; padding-inline: 24px; }
.list { grid-template-columns: repeat(2, 1fr); }
.recent-card { flex-basis: 140px; }
/* Busy: the hairline at the top of the sheet burns across it. */
.card.htmx-request { overflow: hidden; }
.card.htmx-request .actions { pointer-events: none; opacity: .5; }
.card.htmx-request::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 30%;
height: 1px;
background: var(--ember);
animation: barSlide 1.15s linear infinite;
}
@media (min-width: 1300px) {
.list { grid-template-columns: repeat(3, 1fr); }
/* ---- empty ---- */
.empty {
display: flex;
flex-direction: column;
gap: 7px;
padding: 40px 20px 48px;
}
.empty strong { font: 400 20px var(--font-display); color: var(--paper); }
.empty.hot strong { color: var(--ember); }
.empty p {
margin: 0;
max-width: 44ch;
font: 400 14px/1.6 var(--font-body);
color: var(--mute);
text-wrap: pretty;
}
/* ---- login ---- */
.login-card {
width: 100%;
max-width: 420px;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 52px 28px 40px;
}
.login-card .eyebrow {
font: 500 10px/1 var(--font-mono);
letter-spacing: .2em;
text-transform: uppercase;
color: var(--mute-2);
}
.login-card h1 {
margin: 14px 0 0;
font: 400 44px/1 var(--font-display);
letter-spacing: -.01em;
color: var(--paper);
}
.login-card h1 em { color: var(--ember); font-style: italic; }
.login-card form { display: flex; flex-direction: column; gap: 18px; }
.login-card label {
font: 500 10px/1 var(--font-mono);
letter-spacing: .16em;
text-transform: uppercase;
color: var(--mute);
}
.login-card input {
width: 100%;
height: 54px;
margin-top: 9px;
padding: 0 2px;
border: none;
border-bottom: 1px solid var(--field-line);
background: transparent;
color: var(--paper);
font: 500 20px var(--font-mono);
letter-spacing: .16em;
outline: none;
}
.login-card input:focus { border-bottom-color: var(--ember); }
.login-card .error {
margin: 0;
min-height: 20px;
font: 400 13px/1.4 var(--font-body);
color: var(--ember);
}
.login-card button {
height: 54px;
border: 1px solid var(--paper);
background: var(--paper);
color: var(--ink);
font: 400 19px var(--font-display);
}
.login-card button:hover {
background: var(--ember);
border-color: var(--ember);
color: #fff;
}
/* ---- desktop: same measure, actions fold up beside the row ---- */
@media (min-width: 720px) {
.topbar { padding: 26px 32px 18px; }
.brand { font-size: 30px; }
.chrome {
flex-direction: row;
align-items: center;
gap: 26px;
padding: 0 32px;
border-bottom: 1px solid var(--rule);
}
.tabs { order: 1; flex: none; gap: 20px; padding: 0; border-bottom: none; }
.tabs [role="tab"] { 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; }
.card { padding: 18px 32px; }
.row { flex-wrap: nowrap; align-items: center; gap: 20px; }
.cover { width: 74px; height: 100px; }
.cover .monogram { font-size: 26px; }
.title { font-size: 21px; }
.actions { flex: none; gap: 4px; border-top: none; }
.actions > * {
flex: none;
width: 40px;
height: 40px;
border: 1px solid var(--rule);
}
.is-new .actions .play { border-color: #3a1d18; }
.actions .on { border-color: #332b14; }
.chapter-form, .confirm-row, .error-inline { margin-left: 94px; }
}
@media (prefers-reduced-motion: reduce) {
+24 -8
View File
@@ -6,20 +6,31 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<title>mangaBookmark</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
<script src="/static/htmx.min.js" defer></script>
<script src="/static/filter.js" defer></script>
</head>
<body>
{{template "icons" .}}
<div class="sheet">
<header class="topbar">
<h1>mangaBookmark</h1>
<h1 class="brand">manga<em>Bookmark</em></h1>
<form method="post" action="/logout">
<button type="submit" class="ghost">Log out</button>
</form>
</header>
<input id="search" class="search" type="search" placeholder="Search titles…"
autocomplete="off" aria-label="Search titles">
{{/* Search sits above the tabs on a phone and folds into the tab row on a
wider screen — one flex container, order swapped in CSS. */}}
<div class="chrome">
<div class="searchbar">
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-search"/></svg>
<input id="search" class="search" type="search" placeholder="Find a title"
autocomplete="off" aria-label="Search titles">
</div>
<nav class="tabs" role="tablist">
<a role="tab" href="/?tab=all" class="{{if eq .Tab "all"}}active{{end}}"
@@ -27,7 +38,8 @@
hx-push-url="/?tab=all" hx-on::after-request="setActiveTab(this)">All</a>
<a role="tab" href="/?tab=new" class="tab-new {{if eq .Tab "new"}}active{{end}}"
hx-get="/ui/list?tab=new" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated</a>
hx-push-url="/?tab=new" hx-on::after-request="setActiveTab(this)">Updated{{if .NewCount}}
<span class="count">{{.NewCount}}</span>{{end}}</a>
<a role="tab" href="/?tab=fav" class="{{if eq .Tab "fav"}}active{{end}}"
hx-get="/ui/list?tab=fav" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=fav" hx-on::after-request="setActiveTab(this)">Favourites</a>
@@ -38,20 +50,23 @@
hx-get="/ui/list?tab=finished" hx-target="#list" hx-swap="innerHTML"
hx-push-url="/?tab=finished" hx-on::after-request="setActiveTab(this)">Finished</a>
</nav>
</div>
{{if .Recent}}
<section class="recent">
<h2>Continue reading</h2>
<div class="recent-strip">
{{range .Recent}}
<a class="recent-card" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
<a class="recent-card {{if .HasNewChapter}}is-new{{end}}" href="{{.ContinueURL}}"
target="_blank" rel="noopener noreferrer">
<span class="recent-cover">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
{{if .HasNewChapter}}<span class="new-ribbon new-ribbon-sm">New</span>{{end}}
{{else}}<span class="monogram" aria-hidden="true">{{.Initial}}</span>{{end}}
{{if .HasNewChapter}}<span class="foot-rule"></span>
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
</span>
<span class="recent-title">{{.Title}}</span>
<span class="recent-chapter">Ch {{.LastChapter}}</span>
<span class="recent-chapter">Ch {{.LastChapter}}{{if .HasNewChapter}} · New{{end}}</span>
</a>
{{end}}
</div>
@@ -61,6 +76,7 @@
<main id="list" class="list">
{{template "list" .}}
</main>
</div>
</body>
</html>
{{end}}
+67 -64
View File
@@ -1,111 +1,114 @@
{{define "card"}}
<article class="card" id="card-{{.Key}}" data-title="{{.Title}}">
<a class="cover" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
{{else}}<span class="cover-blank" aria-hidden="true">{{.Initial}}</span>{{end}}
{{if .HasNewChapter}}<span class="new-ribbon">New</span>{{end}}
</a>
<div class="body">
<h3 class="title">{{.Title}}</h3>
<p class="meta">
<span class="site site-{{.Site}}">{{.Site}}</span>
<span class="chapter">Ch {{.LastChapter}}</span>
{{if .HasNewChapter}}<span class="new-chapter">Ch {{.LatestChapter}} out</span>{{end}}
</p>
{{/* One sheet per series. is-new turns the title crimson over an ember rule;
is-dim sinks archived and finished rows into italic grey. */}}
<article class="card{{if eq .Status "reading"}}{{if .HasNewChapter}} is-new{{end}}{{else}} is-dim{{end}}"
id="card-{{.Key}}" data-title="{{.Title}}">
<div class="row">
<a class="cover" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
tabindex="-1" aria-hidden="true">
{{if .Cover}}<img src="{{.Cover}}" alt="" loading="lazy">
{{else}}<span class="monogram">{{.Initial}}</span>{{end}}
{{if and (eq .Status "reading") .HasNewChapter}}<span class="foot-rule"></span>
{{else if .Favorite}}<span class="foot-rule brass"></span>{{end}}
</a>
<div class="body">
<div class="title-line">
<h3 class="title">{{.Title}}</h3>
{{if .Favorite}}
<svg class="fav-mark" viewBox="0 0 24 24" aria-label="Favourite" role="img"><use href="#i-star-on"/></svg>
{{end}}
</div>
<p class="meta">
<span class="site-{{.Site}}">{{.Site}}</span>
<span class="sep">/</span>
<span class="chapter">Ch {{.LastChapter}}</span>
{{if and (eq .Status "reading") .HasNewChapter}}
<span class="sep">/</span>
<span class="new-chapter">Ch {{.LatestChapter}} out</span>
{{end}}
{{if eq .Status "archived"}}
<span class="sep">/</span>
<span class="state">archived</span>
{{else if eq .Status "finished"}}
<span class="sep">/</span>
<span class="state"><svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-check"/></svg>finished</span>
{{end}}
</p>
</div>
<div class="actions">
<a class="primary" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
<a class="play" href="{{.ContinueURL}}" target="_blank" rel="noopener noreferrer"
title="Continue reading" aria-label="Continue reading">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false">
<path d="M8 4.8a1 1 0 0 1 1.52-.85l10 7.2a1 1 0 0 1 0 1.7l-10 7.2A1 1 0 0 1 8 19.2z"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-play"/></svg>
</a>
<button class="icon {{if .Favorite}}on{{end}}"
title="Favourite" aria-label="Toggle favourite"
<button class="{{if .Favorite}}on{{end}}"
title="{{if .Favorite}}Remove from favourites{{else}}Add to favourites{{end}}"
aria-label="Toggle favourite"
hx-post="/ui/bookmarks/{{.Key}}/favorite"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="{{if .Favorite}}currentColor{{else}}none{{end}}"
stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"
aria-hidden="true" focusable="false">
<path d="M12 3.7l2.63 5.33 5.87.86-4.25 4.14 1 5.85L12 17.31l-5.25 2.57 1-5.85L3.5 9.89l5.87-.86z"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-star{{if .Favorite}}-on{{end}}"/></svg>
</button>
<button class="icon" title="Set chapter" aria-label="Set chapter"
<button class="pencil" title="Set chapter" aria-label="Set chapter"
onclick="toggleChapterForm('{{.Key}}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 20h4L19.3 8.7a1.5 1.5 0 0 0 0-2.12l-1.88-1.88a1.5 1.5 0 0 0-2.12 0L4 16z"/>
<path d="M13.8 7.2l3 3"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-pencil"/></svg>
</button>
{{if eq .Status "finished"}}
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
<button class="restore" title="Restore to reading" aria-label="Restore to reading"
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-undo"/></svg>
</button>
{{else}}
{{if eq .Status "archived"}}
<button class="icon on" title="Restore to reading" aria-label="Restore to reading"
<button class="restore" title="Restore to reading" aria-label="Restore to reading"
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"reading"}'
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4 12a8 8 0 1 1 2.6 5.9M4 12V7M4 12h5"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-undo"/></svg>
</button>
{{else}}
<button class="icon" title="Archive" aria-label="Archive"
<button title="Archive" aria-label="Archive"
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"archived"}'
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M3.5 6.4h17v3.2h-17zM5 9.6h14V19H5zM10 13h4"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-box"/></svg>
</button>
{{end}}
<button class="icon" title="Mark finished" aria-label="Mark finished"
<button title="Mark finished" aria-label="Mark finished"
hx-post="/ui/bookmarks/{{.Key}}/status" hx-vals='{"status":"finished"}'
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4.5 12.5l5 5 10-11"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-check"/></svg>
</button>
{{end}}
<button class="icon danger" title="Remove" aria-label="Remove"
<button class="remove" title="Remove" aria-label="Remove"
onclick="toggleConfirmRow('{{.Key}}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M4.5 6.8h15M9.8 6.8V4.6h4.4v2.2M7.3 6.8l.9 12.6h7.6l.9-12.6"/>
</svg>
<svg viewBox="0 0 24 24" aria-hidden="true"><use href="#i-trash"/></svg>
</button>
</div>
<form class="chapter-form" id="chapter-form-{{.Key}}" hidden
hx-post="/ui/bookmarks/{{.Key}}/chapter"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
</div>
<form class="chapter-form" id="chapter-form-{{.Key}}" hidden
hx-post="/ui/bookmarks/{{.Key}}/chapter"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="input, button">
{{if .LatestChapter}}<p class="hint">Latest known: Ch {{.LatestChapter}}</p>{{end}}
<div class="field">
<input name="chapter" type="number" step="0.1" min="0"
value="{{.LastChapterNum}}" aria-label="Chapter number" required>
<button type="submit">Save</button>
</form>
<div class="confirm-row" id="confirm-row-{{.Key}}" hidden>
<span>Remove this?</span>
</div>
</form>
<div class="confirm-row" id="confirm-row-{{.Key}}" hidden>
<span>Remove this?</span>
<div>
<button class="danger-solid"
hx-delete="/ui/bookmarks/{{.Key}}"
hx-target="[id='card-{{.Key}}']" hx-swap="outerHTML"
hx-indicator="[id='card-{{.Key}}']" hx-disabled-elt="this">Remove</button>
<button type="button" onclick="toggleConfirmRow('{{.Key}}')">Cancel</button>
</div>
<p class="error-inline" hidden></p>
</div>
<p class="error-inline" hidden></p>
</article>
{{end}}
+15
View File
@@ -0,0 +1,15 @@
{{define "icons"}}
<!-- One sprite per page. Card fragments swapped in by htmx reference these
symbols by id, so the markup for a row stays a handful of <use> tags. -->
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
<symbol id="i-play" viewBox="0 0 24 24"><path d="M8 5l11 7-11 7z" fill="currentColor"/></symbol>
<symbol id="i-star" viewBox="0 0 24 24"><path d="M12 3.6l2.6 5.6 6 .8-4.4 4.2 1.1 6-5.3-2.9-5.3 2.9 1.1-6-4.4-4.2 6-.8z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
<symbol id="i-star-on" viewBox="0 0 24 24"><path d="M12 3.6l2.6 5.6 6 .8-4.4 4.2 1.1 6-5.3-2.9-5.3 2.9 1.1-6-4.4-4.2 6-.8z" fill="currentColor" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
<symbol id="i-pencil" viewBox="0 0 24 24"><path d="M4 20h4L19 9l-4-4L4 16z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
<symbol id="i-box" viewBox="0 0 24 24"><path d="M3 7h18v4H3zM5 11v9h14v-9M10 15h4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="M4 12.5l5.2 5.5L20 6.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9.5 7V4h5v3M6.5 7l1 13h9l1-13M10.5 11v6M13.5 11v6" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></symbol>
<symbol id="i-undo" viewBox="0 0 24 24"><path d="M4 9h9.5a5 5 0 010 10H8M4 9l4.2-4.2M4 9l4.2 4.2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></symbol>
<symbol id="i-search" viewBox="0 0 24 24"><circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="1.8"/><path d="M15.3 15.3L20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></symbol>
</svg>
{{end}}
+5 -7
View File
@@ -2,16 +2,14 @@
{{if .Items}}
{{range .Items}}{{template "card" .}}{{end}}
{{else if eq .Tab "fav"}}
<p class="empty">No favourites yet. Star a series to pin it here.</p>
<div class="empty"><strong>No favourites yet.</strong><p>Star a series to pin it here.</p></div>
{{else if eq .Tab "new"}}
<p class="empty">Nothing new. Every series is caught up to its latest chapter.</p>
<div class="empty hot"><strong>Nothing new.</strong><p>Every series is caught up to its latest chapter.</p></div>
{{else if eq .Tab "archived"}}
<p class="empty">Nothing archived. Shelve a series to park it here — it keeps getting checked for new chapters.</p>
<div class="empty"><strong>Nothing archived.</strong><p>Shelve a series to park it here — it keeps getting checked for new chapters.</p></div>
{{else if eq .Tab "finished"}}
<p class="empty">Nothing finished yet.</p>
<div class="empty"><strong>Nothing finished yet.</strong><p>Mark a series finished and it moves out of your reading list.</p></div>
{{else}}
<p class="empty">
Nothing here yet. Bookmarks appear once the userscript records a chapter.
</p>
<div class="empty"><strong>Nothing here yet.</strong><p>Bookmarks appear once the userscript records a chapter.</p></div>
{{end}}
{{end}}
+14 -6
View File
@@ -6,16 +6,24 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark light">
<title>mangaBookmark</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/style.css">
</head>
<body class="login-body">
<body>
<main class="login-card">
<h1>mangaBookmark</h1>
<div>
<span class="eyebrow">Private library</span>
<h1>manga<em>Bookmark</em></h1>
</div>
<form method="post" action="/login">
<label for="password">Password</label>
<input id="password" name="password" type="password"
autocomplete="current-password" autofocus required>
{{if .Error}}<p class="error">{{.Error}}</p>{{end}}
<div>
<label for="password">Password</label>
<input id="password" name="password" type="password"
autocomplete="current-password" autofocus required>
</div>
<p class="error">{{.Error}}</p>
<button type="submit">Sign in</button>
</form>
</main>