e940b96eae
Replaces the card-and-chip visual language with the Cinder direction from the design doc: editorial serif, containerless sheets divided by ash hairlines, one 760px measure, no radii and no shadows. The organising rule is that heat is typographic. Only a series with an unread chapter is allowed to be crimson — title, cover foot rule, "Ch N out", play icon — so the one thing worth acting on is the one thing that draws the eye. Favourites get brass instead of borrowing the accent, which is what made the old three-hue palette read as noise. Both states hang off two classes on the article (is-new, is-dim), so a new sub-element inherits the state rather than re-deriving it. The action strip is flex-basis 100% inside the row, which is what lets the same markup be a full-width six-cell strip under the row on a phone (46px targets) and a group of 40px squares beside it on a desktop, with no duplicate template branches. Icons move to a sprite in icons.html: cards are swapped in by htmx and reference the page's symbols, so a row no longer carries a screenful of inline SVG paths. [hidden] gets a display:none override because every disclosure panel is now a flex container and display beats the attribute. filter.js grows an .open class alongside hidden so the strip can show which cell the open panel belongs to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
89 lines
3.0 KiB
JavaScript
89 lines
3.0 KiB
JavaScript
// 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);
|
|
});
|
|
}
|
|
|
|
// The chapter-edit form and the delete confirm row are the two per-card
|
|
// 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 = togglePanel(key, "chapter-form-", ".actions .pencil");
|
|
if (form && !form.hidden) form.querySelector("input").focus();
|
|
}
|
|
|
|
function toggleConfirmRow(key) {
|
|
togglePanel(key, "confirm-row-", ".actions .remove");
|
|
}
|
|
|
|
// 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.
|
|
(function () {
|
|
function showError(elt, message) {
|
|
var card = elt.closest(".card");
|
|
var slot = card && card.querySelector(".error-inline");
|
|
if (!slot) return;
|
|
slot.textContent = message;
|
|
slot.hidden = false;
|
|
clearTimeout(slot._hideTimer);
|
|
slot._hideTimer = setTimeout(function () {
|
|
slot.hidden = true;
|
|
}, 5000);
|
|
}
|
|
|
|
document.body.addEventListener("htmx:responseError", function (e) {
|
|
showError(e.detail.elt, "Couldn't save — try again.");
|
|
});
|
|
document.body.addEventListener("htmx:sendError", function (e) {
|
|
showError(e.detail.elt, "No connection — try again.");
|
|
});
|
|
})();
|