This commit is contained in:
mark committed 2026-09-09 09:08:19 -04:00
1 parent 37ac9c2757
commit ce73c76126
6 files changed
+158 -38

No files matched your search

+54 -13
View File
@@ -13,6 +13,7 @@
// POST /api/bookmark set the bookmark, and return the slot
// GET /api/reference the navbar's chapter and verse picker
// GET /api/verse the dialog for one verse
// GET /api/toast one confirmation, by name
//
// Anything with a dot in its last segment is a file, not a route, and is
// served from the cache or the network without going near the router. Pages
@@ -436,8 +437,8 @@ async function servePage(path, code, call) {
if (!call.navigate && !call.restore) {
// The picker is in the navbar and every boosted link targets #content, so
// a swap never reaches it. It rides along out of band instead, the way a
// bookmark write carries the modal's status line: one response, both
// halves of the page. Sent even when it is null -- that is the hidden
// bookmark write carries its toast: one response, both halves of the
// page. Sent even when it is null -- that is the hidden
// state, and arriving at the book list has to put the picker away.
return fragment + '\n'
+ await view.render('reference.njk', { reference: reference, oob: true });
@@ -578,20 +579,60 @@ router.post('/api/bookmark', async (call) => {
await withStore('readwrite',
(s) => s.put({ ref: ref, href: href, savedAt: Date.now() }, BOOKMARK_KEY));
// Only on the way out of a write that committed. base.njk closes the
// dialog on this, and a dialog that closed itself on a failure would take
// the line explaining the failure with it.
return new Response(await bookmarkFragment('Bookmark set.'), {
// dialog on this, and there is nothing left in the dialog worth keeping
// open for: the confirmation rides out of band into the toast container,
// which is not inside it.
return new Response(await bookmarkFragment('bookmarked'), {
headers: {
'Content-Type': 'text/html; charset=utf-8',
'Cache-Control': 'no-store',
'HX-Trigger-After-Settle': 'bookmark-saved'
'HX-Trigger-After-Settle': 'verse-done'
}
});
} catch (err) {
return bookmarkFragment('Could not save: storage unavailable.');
// No trigger, so the dialog stays where it is. The toast says what
// happened whether or not it does, but a dialog that closed on a failed
// write would look exactly like one that closed on a successful one.
return bookmarkFragment('bookmark-refused');
}
});
// ------------------------------------------------------------------ toasts
// Everything the app says back to a reader who has just done something. Named
// rather than passed as text: the page asks for 'copied', not for a sentence,
// so the wording is here with the rest of the app's words and an attribute in
// a template never has to spell one out. bad is the ones that did not happen.
const TOASTS = {
'copied': { text: 'Link copied.' },
'copy-refused': { text: 'Copy the link above — this browser would not '
+ 'take it.', bad: true },
'bookmarked': { text: 'Bookmark set.' },
'bookmark-refused': { text: 'Could not save the bookmark: storage '
+ 'unavailable.', bad: true }
};
// oob is how it travels: on its own to the toast container, which is what the
// copy button asks for, or out of band on an answer that is already going
// somewhere else, which is what a bookmark write sends.
function toastFragment(say, oob) {
const toast = TOASTS[say];
return view.render('toast.njk', { text: toast.text, bad: toast.bad, oob: oob });
}
// One toast, by name.
//
// say=copied which one, from the table above.
//
// 204 for a name that is not in it, on the same reasoning api/verse 204s for a
// verse that is not there: htmx swaps nothing, so a typo appends nothing,
// rather than an empty toast sliding up to say nothing at all.
router.get('/api/toast', (call) => {
const say = call.url.searchParams.get('say');
if (!TOASTS[say]) return new Response('', { status: 204 });
return toastFragment(say, false);
});
// ----------------------------------------------------------------- library
// The background download of the whole corpus.
@@ -693,13 +734,13 @@ function getBookmark() {
.then((mark) => mark || DEFAULT_BOOKMARK);
}
async function bookmarkFragment(status) {
async function bookmarkFragment(say) {
let html = await view.render('bookmark.njk', { mark: await getBookmark() });
if (status) {
// Out of band, so one response updates both the navbar slot and the
// modal's status line without the modal having to ask separately. Same
// template the shell uses, so the two cannot drift apart.
html += '\n' + await view.render('status.njk', { text: status, oob: true });
if (say) {
// Out of band, so one response updates the navbar slot and raises the
// toast without the page having to ask for the second one. Same template
// and same table api/toast answers from, so the two cannot drift apart.
html += '\n' + await toastFragment(say, true);
}
return html;
}