diff --git a/backend/static/filter.js b/backend/static/filter.js index 0bf9d8a..64d9d49 100644 --- a/backend/static/filter.js +++ b/backend/static/filter.js @@ -4,17 +4,43 @@ function applyFilter() { var box = document.getElementById("search"); if (!box) return; - var needle = box.value.trim().toLowerCase(); - document.querySelectorAll(".card").forEach(function (card) { + var query = box.value.trim(); + var needle = query.toLowerCase(); + var cards = document.querySelectorAll(".card"); + var visible = 0; + cards.forEach(function (card) { var title = (card.dataset.title || "").toLowerCase(); card.hidden = needle !== "" && title.indexOf(needle) === -1; + if (!card.hidden) visible++; }); + + // The strip is never filtered, so leaving it up during a search puts + // non-matching covers above an empty list and the screen contradicts + // itself. + var recent = document.querySelector(".recent"); + if (recent) recent.hidden = needle !== ""; + + // An empty bucket already explains itself server-side; this only speaks + // when the filter is what emptied the screen. + var none = document.getElementById("no-match"); + if (none) { + none.hidden = !(needle !== "" && cards.length > 0 && visible === 0); + if (!none.hidden) none.querySelector(".no-match-q").textContent = query; + } } document.addEventListener("input", function (e) { if (e.target && e.target.id === "search") applyFilter(); }); + document.addEventListener("click", function (e) { + if (!e.target || !e.target.classList.contains("clear-search")) return; + var box = document.getElementById("search"); + box.value = ""; + applyFilter(); + box.focus(); + }); + // htmx replaces the list on a tab switch, so re-apply to the new cards. document.body.addEventListener("htmx:afterSwap", applyFilter); })(); diff --git a/backend/static/style.css b/backend/static/style.css index 9d2ebaa..ebf18dd 100644 --- a/backend/static/style.css +++ b/backend/static/style.css @@ -591,6 +591,17 @@ button { cursor: pointer; } } .empty strong { font: 400 20px var(--font-display); color: var(--paper); } .empty.hot strong { color: var(--ember); } +.empty .clear-search { + align-self: flex-start; + margin-top: 4px; + height: 44px; + padding: 0 16px; + border: 1px solid var(--field-line); + background: transparent; + color: var(--paper-dim); + font: 400 16px var(--font-display); +} +.empty .clear-search:hover { color: var(--paper); border-color: var(--paper); } .empty p { margin: 0; max-width: 44ch; diff --git a/backend/templates/list.html b/backend/templates/list.html index e6edf97..38ca406 100644 --- a/backend/templates/list.html +++ b/backend/templates/list.html @@ -1,6 +1,13 @@ {{define "list"}} {{if .Items}} {{range .Items}}{{template "card" .}}{{end}} + {{/* The client filter only hides cards, so without this the list area goes + blank on a query that matches nothing. filter.js fills in the query and + unhides it; it lives inside #list so a tab swap re-creates it. */}} +
Star a series to pin it here.