Confirmations pin to the viewport; open ballots mark their rows
Twice the feedback was the bug: the success banner lives at the top of the page and the user acts at the bottom of a 136-row list — so a working "pick edition" read as broken (it worked both times; the data proves it). showToast() pins transient confirmations to the viewport bottom (role=status, carries buttons, auto-dismisses), and pick- edition/edit-saved use it. Better: state stops being transient at all — any row whose edition ballot is open shows a persistent "ballot in Review" link where its button was, so the answer to "where did it go?" lives on the row forever. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
edb72514f4
commit
c94e8bc695
@@ -473,6 +473,21 @@ button.danger { color: var(--stop-ink); border-color: var(--stop); background: #
|
||||
border: var(--line); border-radius: var(--radius); background: #fff;
|
||||
}
|
||||
|
||||
/* -- toast: viewport-pinned transient confirmations --------------------- */
|
||||
#toast {
|
||||
position: fixed; bottom: 1.1rem; left: 50%; transform: translateX(-50%);
|
||||
z-index: 20; max-width: min(92vw, 34rem);
|
||||
background: var(--navy); color: #fff;
|
||||
border: var(--line); border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-raised);
|
||||
padding: .7rem 1rem; font-size: .9rem; line-height: 1.45;
|
||||
}
|
||||
#toast a { color: var(--gold); }
|
||||
#toast button {
|
||||
font-size: .8rem; padding: .25rem .7rem; margin-left: .5rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* -- responsive -------------------------------------------------------- */
|
||||
|
||||
/* The rail collapses to a top bar: brand + hamburger; the nav drops
|
||||
|
||||
@@ -13,6 +13,24 @@ function showBanner(html) {
|
||||
document.getElementById("banner").innerHTML = html;
|
||||
}
|
||||
|
||||
/* Transient confirmation pinned to the viewport: the #banner region sits
|
||||
* at the top of the page, which a user deep in a long list never sees.
|
||||
* The toast may carry buttons — wire them before the timer clears it. */
|
||||
function showToast(html, ms = 8000) {
|
||||
let el = document.getElementById("toast");
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "toast";
|
||||
el.setAttribute("role", "status");
|
||||
document.body.appendChild(el);
|
||||
}
|
||||
el.innerHTML = html;
|
||||
el.hidden = false;
|
||||
clearTimeout(showToast._timer);
|
||||
showToast._timer = setTimeout(() => { el.hidden = true; }, ms);
|
||||
return el;
|
||||
}
|
||||
|
||||
function errorBanner(detail) {
|
||||
// an HTTP status means the server answered — the problem is its data
|
||||
const httpish = /^\d{3}$/.test(String(detail));
|
||||
|
||||
@@ -89,6 +89,10 @@ function render() {
|
||||
title="one line, several boxes? make each photo its own copy">
|
||||
split into copies</button>`
|
||||
: ""}
|
||||
${c.version_status === "version_ambiguous"
|
||||
? `<a class="linkbtn ballot" href="/review#editions"
|
||||
title="this copy's edition ballot is open">ballot in Review</a>`
|
||||
: ""}
|
||||
${c.bgg_id && ["auto", "approved"].includes(c.status)
|
||||
&& ["version_unknown", "version_error", ""].includes(c.version_status || "")
|
||||
? `<button class="pickedition" data-title="${esc(c.title_raw)}"
|
||||
@@ -139,9 +143,8 @@ document.getElementById("catbody").addEventListener("click", async e => {
|
||||
row_ix: pe.dataset.rowix === "" ? null : Number(pe.dataset.rowix),
|
||||
});
|
||||
if (res) {
|
||||
showBanner(`<div class="banner">edition ballot ready for
|
||||
<b>${esc(pe.dataset.title)}</b> —
|
||||
<a href="/review#editions">pick it in Review</a></div>`);
|
||||
showToast(`edition ballot ready for <b>${esc(pe.dataset.title)}</b> —
|
||||
<a href="/review#editions">pick it in Review</a>`);
|
||||
GATE.reset(); refresh().catch(() => {});
|
||||
}
|
||||
return;
|
||||
@@ -214,14 +217,13 @@ document.getElementById("catbody").addEventListener("submit", async e => {
|
||||
EDITING = null; GATE.reset(); refresh().catch(() => {});
|
||||
// an edit re-queues any matched row: without a resolve run the line
|
||||
// sits "awaiting resolve" — say so, and offer the run right here
|
||||
showBanner(`<div class="banner">saved — a previously matched title
|
||||
re-matches on the next resolve run.
|
||||
<button id="resolvenow">run resolve now</button></div>`);
|
||||
const btn = document.getElementById("resolvenow");
|
||||
const toast = showToast(`saved — a previously matched title re-matches
|
||||
on the next resolve run. <button id="resolvenow">run resolve now</button>`);
|
||||
const btn = toast.querySelector("#resolvenow");
|
||||
if (btn) btn.addEventListener("click", async () => {
|
||||
const r = await apiPost("/api/run/resolve");
|
||||
if (r) showBanner(`<div class="banner">resolve running — progress on
|
||||
the <a href="/">Pipeline</a> page; this list updates itself</div>`);
|
||||
if (r) showToast(`resolve running — progress on the
|
||||
<a href="/">Pipeline</a> page; this list updates itself`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user