diff --git a/backend/static/filter.js b/backend/static/filter.js index 33bc404..4e27c1e 100644 --- a/backend/static/filter.js +++ b/backend/static/filter.js @@ -101,9 +101,26 @@ function toggleChapterForm(key) { // cell share it. function toggleConfirmRow(key, kind) { var cls = { archive: ".box", finish: ".finish", remove: ".remove" }[kind]; - togglePanel(key, "confirm-" + kind + "-" + key, ".actions " + cls); + var row = togglePanel(key, "confirm-" + kind + "-" + key, ".actions " + cls); + // Focus the answer rather than trusting aria-live on a container that merely + // unhides: it makes the announcement deterministic, keeps tab order inside + // the confirm instead of running on into the next card, and means the row + // cannot be opened and scrolled past unnoticed. + if (row && !row.hidden) row.querySelector("button").focus(); } +// Esc closes whichever panel this card has open and hands focus back to the +// cell that owns it — otherwise the only way out is finding that exact cell +// again. +document.addEventListener("keydown", function (e) { + if (e.key !== "Escape" || !e.target.closest) return; + var card = e.target.closest(".card"); + var owner = card && card.querySelector(".actions .open"); + if (!owner) return; + closeCardPanels(card.id.replace(/^card-/, "")); + owner.focus(); +}); + // A failed favourite/chapter/delete request leaves the card in place (htmx // does not swap on a non-2xx response) but otherwise gives no sign anything // went wrong. Surface it inline instead of leaving the tap looking ignored. @@ -121,12 +138,23 @@ function toggleConfirmRow(key, kind) { slot.append(" ", a); } slot.hidden = false; - clearTimeout(slot._hideTimer); - slot._hideTimer = setTimeout(function () { - slot.hidden = true; - }, linkHref ? 30000 : 5000); + // No self-destruct timer: this reader gets interrupted mid-tap, and a + // notice that expires after 5s leaves a failed write with no trace at all + // — the star is back off and nothing says why. The notice stays until the + // next request from this card clears it (or a successful one swaps the + // whole card away). + slot.scrollIntoView({ + block: "nearest", + behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth", + }); } + document.body.addEventListener("htmx:beforeRequest", function (e) { + var card = e.detail.elt.closest(".card"); + var slot = card && card.querySelector(".error-inline"); + if (slot) slot.hidden = true; + }); + // The handlers answer a bad value with http.Error, i.e. a short plain-text // line — worth showing verbatim. Anything long or HTML-ish is an error page, // not a reason, so fall back to the generic copy. diff --git a/backend/static/style.css b/backend/static/style.css index e9028d9..ec5b736 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -163,7 +163,9 @@ button { cursor: pointer; } /* Every hideable thing here is a flex container, and display beats hidden. */ [hidden] { display: none !important; } -@keyframes barSlide { from { transform: translateX(-100%) } to { transform: translateX(320%) } } +/* Ends flush with the right edge of the card (30% wide × 233% travel), so the + card itself never needs overflow: hidden to contain it. */ +@keyframes barSlide { from { transform: translateX(-100%) } to { transform: translateX(233%) } } @keyframes sheetIn { from { opacity: 0; transform: translateY(-4px) } to { opacity: 1; transform: none } } /* The law: ember means "new chapter", and nothing else on a list screen — busy, error and destruction all stay off it, so a tired glance never @@ -258,7 +260,10 @@ button { cursor: pointer; } flex: none; display: flex; align-items: center; + justify-content: center; gap: 7px; + /* "All" is two characters in a 17px serif — 15px of target without this. */ + min-width: 44px; padding: 8px 0 12px; color: var(--mute); font: 400 17px var(--font-display); @@ -587,7 +592,6 @@ button { cursor: pointer; } /* Busy: a grey hairline slides across the top of the sheet — deliberately not ember, which on a list screen only ever means "new chapter". */ -.card.htmx-request { overflow: hidden; } .card.htmx-request .actions { pointer-events: none; opacity: .5; } .card.htmx-request::before { content: ""; @@ -718,8 +722,8 @@ button { cursor: pointer; } .actions { flex: none; gap: 4px; border-top: none; } .actions > * { flex: none; - width: 40px; - height: 40px; + width: 44px; + height: 44px; border: 1px solid var(--rule); } /* Cells are already gapped here, so the clusters separate by space rather diff --git a/backend/templates/card.html b/backend/templates/card.html index f67434e..5ef6a3d 100644 --- a/backend/templates/card.html +++ b/backend/templates/card.html @@ -7,7 +7,10 @@ diff --git a/backend/templates/login.html b/backend/templates/login.html index 0f6fdfe..c6cc228 100644 --- a/backend/templates/login.html +++ b/backend/templates/login.html @@ -21,7 +21,9 @@ -

{{.Error}}

+ {{/* The page reloads on a failed sign-in, so the message is present from + the start; role=alert is what gets it announced anyway. */}} +