dashboard: add toc-sidebar utility; move live indicator into TOC column

Extracts the TOC-injection pattern into a reusable component:

  src/components/toc-sidebar.js
    injectTocTop(id, element) — prepends an element to #observablehq-toc,
    removing any previous instance with the same id first so reactive cells
    can re-inject on each poll without accumulating duplicates.

decisions.md now uses injectTocTop to place a single widget (live
indicator + Decision Health KPI box) into the right-column sidebar,
removing the standalone live-indicator cell and the ad-hoc id/remove
pattern that was previously inlined.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 14:42:38 +01:00
parent 5b743196db
commit 902aafcfb1
2 changed files with 37 additions and 13 deletions

View File

@@ -0,0 +1,29 @@
/**
* toc-sidebar — inject a persistent widget into Observable Framework's
* right-column table-of-contents sidebar.
*
* Observable Framework renders a non-scrolling TOC aside (#observablehq-toc)
* in the right column. This helper lets you prepend a custom element to it,
* replacing any previously injected element with the same id on each call so
* reactive cells can refresh the widget without accumulating duplicates.
*
* Usage:
* import {injectTocTop} from "./components/toc-sidebar.js";
*
* const el = html`<div>…</div>`;
* injectTocTop("my-widget-id", el); // call again on each reactive update
*
* @param {string} id Stable id used to find and remove the previous
* instance. Must be unique per widget on the page.
* @param {HTMLElement} element Element to inject. Its id will be set to `id`.
* @returns {boolean} true if injected into the TOC sidebar;
* false if #observablehq-toc was not found.
*/
export function injectTocTop(id, element) {
document.getElementById(id)?.remove();
element.id = id;
const toc = document.querySelector("#observablehq-toc");
if (!toc) return false;
toc.prepend(element);
return true;
}

View File

@@ -103,7 +103,8 @@ function fmtDuration(ms) {
# Decisions
```js
import {withDocHelp} from "./components/doc-overlay.js";
import {withDocHelp} from "./components/doc-overlay.js";
import {injectTocTop} from "./components/toc-sidebar.js";
// ── KPI computation (uses full data, not filtered) ──────────────────────────
const _resolved5 = data
@@ -150,22 +151,16 @@ const _kpiBox = html`<div class="kpi-infobox">
withDocHelp(_kpiBox, "/docs/decisions-kpi");
// ── Inject into the TOC sidebar (right column, non-scrolling) ───────────────
_kpiBox.id = "decisions-kpi-box";
const _toc = document.querySelector("#observablehq-toc");
if (_toc) {
document.getElementById("decisions-kpi-box")?.remove();
_toc.prepend(_kpiBox);
} else display(html`<div style="float:right;width:215px;margin-left:1rem">${_kpiBox}</div>`);
```
```js
display(html`<div class="live-indicator">
// ── Build sidebar widget: live indicator + KPI box ──────────────────────────
const _liveEl = html`<div class="live-indicator">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`);
</div>`;
const _tocInjected = injectTocTop("decisions-sidebar", html`<div>${_liveEl}${_kpiBox}</div>`);
if (!_tocInjected) display(html`<div style="float:right;width:215px;margin-left:1rem">${_liveEl}${_kpiBox}</div>`);
```
## Resolution History