diff --git a/backend/static/filter.js b/backend/static/filter.js index f4c9200..61a45ae 100644 --- a/backend/static/filter.js +++ b/backend/static/filter.js @@ -1,4 +1,24 @@ -// Title search and tab-state handling land in Task 7. +// Title search runs entirely in the browser: the full list is already in the +// DOM, so filtering it needs no request. +(function () { + function applyFilter() { + var box = document.getElementById("search"); + if (!box) return; + var needle = box.value.trim().toLowerCase(); + document.querySelectorAll(".card").forEach(function (card) { + var title = (card.dataset.title || "").toLowerCase(); + card.hidden = needle !== "" && title.indexOf(needle) === -1; + }); + } + + document.addEventListener("input", function (e) { + if (e.target && e.target.id === "search") applyFilter(); + }); + + // htmx replaces the list on a tab switch, so re-apply to the new cards. + document.body.addEventListener("htmx:afterSwap", applyFilter); +})(); + function setActiveTab(el) { el.parentElement.querySelectorAll("[role=tab]").forEach(function (t) { t.classList.toggle("active", t === el); diff --git a/backend/static/style.css b/backend/static/style.css index ff1dbdd..29f5d5e 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -1,2 +1,225 @@ -/* Styling lands in Task 7. */ -:root { color-scheme: dark light; } +/* Mobile first. Dark by default because manga reading happens at night; the + light branch follows the system preference. */ +:root { + color-scheme: dark light; + --bg: #14161a; + --surface: #1d2026; + --surface-2: #262a32; + --text: #e8eaed; + --muted: #9aa1ac; + --accent: #6aa9ff; + --danger: #ff6a6a; + --star: #ffc857; + --radius: 12px; +} + +@media (prefers-color-scheme: light) { + :root { + --bg: #f4f5f7; + --surface: #ffffff; + --surface-2: #eceef2; + --text: #1a1d22; + --muted: #5d646e; + } +} + +* { box-sizing: border-box; } + +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; +} + +/* 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; +} + +/* --- login --- */ + +.login-body { + display: grid; + place-items: center; + min-height: 100dvh; +} + +.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; } +.login-card label { display: block; margin-bottom: 6px; color: var(--muted); font-size: .875rem; } + +.login-card input { + 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; +} + +.login-card input:focus-visible { outline: 2px solid var(--accent); } +.login-card button { width: 100%; background: var(--accent); color: #0b1220; font-weight: 600; } +.error { margin: 0 0 12px; color: var(--danger); font-size: .875rem; } + +/* --- chrome --- */ + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 12px 0; +} + +.topbar h1 { margin: 0; font-size: 1.125rem; } +.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; +} + +.search:focus-visible { outline: 2px solid var(--accent); } + +.tabs { display: flex; gap: 8px; margin-bottom: 16px; } + +.tabs [role="tab"] { + flex: 1; + display: grid; + place-items: center; + background: var(--surface); + color: var(--muted); + text-decoration: none; +} + +.tabs [role="tab"].active { background: var(--accent); color: #0b1220; font-weight: 600; } + +/* --- continue reading --- */ + +.recent h2 { margin: 0 0 8px; font-size: .8125rem; text-transform: uppercase; 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; +} + +.recent-card img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 8px; } +.recent-title { display: block; margin-top: 6px; font-size: .8125rem; line-height: 1.25; + overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; } +.recent-chapter { display: block; color: var(--muted); font-size: .75rem; } + +/* --- list --- */ + +.list { display: grid; gap: 10px; } + +.card { + display: grid; + grid-template-columns: 72px 1fr; + gap: 12px; + padding: 10px; + background: var(--surface); + border-radius: var(--radius); +} + +.card .cover img { width: 72px; aspect-ratio: 3 / 4; object-fit: cover; border-radius: 8px; } +.card .body { min-width: 0; } +.card .title { margin: 0 0 4px; font-size: 1rem; line-height: 1.25; } + +.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; } +.new { padding: 2px 6px; background: var(--accent); color: #0b1220; border-radius: 6px; font-weight: 700; } + +.actions { display: flex; flex-wrap: wrap; gap: 8px; } + +.primary { + flex: 1 1 auto; + display: grid; + place-items: center; + padding: 0 14px; + background: var(--accent); + color: #0b1220; + font-weight: 600; + text-decoration: none; +} + +.icon { width: 44px; background: var(--surface-2); color: var(--text); font-size: 1.125rem; } +.icon.on { color: var(--star); } +.icon.danger { color: var(--danger); } + +.chapter-form { display: flex; gap: 8px; margin-top: 8px; } + +.chapter-form input { + flex: 1; + min-height: 44px; + padding: 0 12px; + background: var(--surface-2); + color: var(--text); + border: 1px solid transparent; + border-radius: var(--radius); + font: inherit; +} + +.chapter-form button { padding: 0 14px; background: var(--accent); color: #0b1220; font-weight: 600; } +.empty { padding: 32px 12px; text-align: center; color: var(--muted); } + +/* 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; } + +/* --- 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; } +} + +@media (min-width: 1300px) { + .list { grid-template-columns: repeat(3, 1fr); } +} + +@media (prefers-reduced-motion: reduce) { + * { animation: none !important; transition: none !important; } +}