From bd6910a5dde90cb7cbcb6273a440ab8c115b9d56 Mon Sep 17 00:00:00 2001 From: Wesley Ray Date: Fri, 14 Aug 2026 08:45:12 -0400 Subject: [PATCH] Reading map: ring the newest highlight and the one quoted in the header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two standing markers, drawn as rings rather than brighter dots. The animated trace head is already a bright point and comes to rest on exactly the newest node at the end of every draw cycle — under prefers-reduced-motion it parks there permanently — so a second filled dot at that coordinate would read as a rendering fault. Outlines say "different kind of thing" without competing. The newest highlight is the LAST trace entry, never the max of `t`: the payload is pre-sorted, `t` is date-only, and many highlights share a day. The quoted one is looked up by text_hash, which /highlights/random now returns beside the quote. The two fetches race on every page view, so the hash is published both as a global and as a `garden:quote` event — one covers each order. About one highlight in fifteen has no node, because its source book was never indexed; markQuoted returns false and nothing is drawn. Rings are flat geometry parented to the rotating world, so they are billboarded each frame or they would vanish edge-on twice a revolution. Also adds the "Currently reading" rail widget. It says how long ago on purpose: six of the ten books on that shelf have not been opened in months, and without a relative date the thing looks broken rather than honest. Past 45 days it mutes itself. No cover — kobo.books.isbn is a sideloader's UUID for most of the shelf and storygraph_links has no cover URL. The TOC rail is now always rendered, since it carries the widget too; it was conditional on having two headings, which would have hidden the widget on the homepage. Hidden explicitly on wide notebook pages, where a third grid child would wrap under the sidebar. --- src/components/ReadingMap.astro | 11 ++- src/components/ReadingNow.astro | 117 ++++++++++++++++++++++++++++++++ src/env.d.ts | 18 +++++ src/layouts/Base.astro | 28 ++++++-- src/lib/reading-map.js | 70 +++++++++++++++++++ src/styles/reading-map.css | 13 ++++ 6 files changed, 249 insertions(+), 8 deletions(-) create mode 100644 src/components/ReadingNow.astro create mode 100644 src/env.d.ts diff --git a/src/components/ReadingMap.astro b/src/components/ReadingMap.astro index 0346c6d..4ea1710 100644 --- a/src/components/ReadingMap.astro +++ b/src/components/ReadingMap.astro @@ -57,12 +57,21 @@ const { try { const { mount } = await import('../lib/reading-map.js'); for (const el of containers) { - mount(el, { + const map = await mount(el, { src: el.dataset.src!, // Fail closed, like the garden's other widgets: a missing payload or a dead GPU // leaves a plain page rather than an empty bordered box. onError: (err: unknown) => console.warn('reading map unavailable:', err), }); + if (!map) continue; + + // Ring the node the header's quote came from. The two are independent fetches on + // every page view and either can land first, so handle both orders: read the hash if + // it is already there, and listen in case it is not. `garden:quote` is dispatched by + // the header script in Base.astro. + const ring = (hash?: string) => hash && map.markQuoted(hash); + ring(window.__gardenQuoteHash); + addEventListener('garden:quote', (e) => ring((e as CustomEvent).detail?.text_hash)); } } catch (err) { /* chunk failed to load: the figure stays hidden */ diff --git a/src/components/ReadingNow.astro b/src/components/ReadingNow.astro new file mode 100644 index 0000000..58ad106 --- /dev/null +++ b/src/components/ReadingNow.astro @@ -0,0 +1,117 @@ +--- +/** + * "Currently reading" — the book Wes most recently made progress in. + * + * import ReadingNow from '../components/ReadingNow.astro'; + * + * + * Data: api.c0smere.net/reading/current, which is kobo_archive on phlegethon by way of + * api.reading_current(). Date-only by design — the timestamp would publish when he is awake. + * + * ⚠️ **It says how long ago on purpose.** Six of the ten books on the currently-reading shelf + * have not been opened in months, so this widget will sit on one entry indefinitely the moment + * the Kobo goes down for a while. Without the relative date that reads as a broken widget + * rather than an honest one; past DORMANT_DAYS it also mutes itself and says "paused". + * + * No cover image: there is no source for one. `kobo.books.isbn` is a sideloader's UUID for most + * of the shelf and `kobo.storygraph_links` carries no cover URL. The remaining route is pulling + * it out of the epub at intake time, which is a library-rag change, not a garden one. + */ +--- + + + + + + diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..ad6b6c8 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,18 @@ +/// + +declare global { + interface Window { + /** + * `text_hash` of the reading highlight in the page header, set by Base.astro once + * api.c0smere.net/highlights/random answers. The reading map reads it to ring the node + * that passage came from. + * + * A global as well as a `garden:quote` event because the two fetches race: whichever + * lands second needs the other's result, and only one of the two mechanisms works in + * each order. + */ + __gardenQuoteHash?: string; + } +} + +export {}; diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index ae6b62b..b24e9b0 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -5,6 +5,7 @@ import { buildNavTree } from '../lib/nav.mjs'; import { COMMIT, BUILT_AT } from '../lib/build-info.mjs'; import NavTree from '../components/NavTree.astro'; import Toc from '../components/Toc.astro'; +import ReadingNow from '../components/ReadingNow.astro'; import { loadNotebooks, notebookUrl } from '../lib/notebooks.mjs'; interface Props { @@ -198,13 +199,13 @@ const pageUrl = new URL(Astro.url.pathname, Astro.site); - { - showToc && ( - - ) - } + {/* The rail exists whenever EITHER thing wants it. It used to be rendered only when + there was a table of contents, which would have left the reading widget with no home + on any page of fewer than two headings — including the homepage. */} +