feat(dashboard): extend suggestions to TOC right margin + 1s shift delay

- Shift+click now works on #observablehq-toc links, KPI boxes, and [id] elements
- _inferWidgetName detects TOC context and labels suggestions accordingly
- Click handler adds inToc branch alongside existing inSidebar
- _updateMode: 1-second setTimeout before activating highlight mode
  so normal Shift+typing doesn't flicker the UI; clears immediately on
  Shift release or window blur

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-18 01:42:11 +01:00
parent e7565ce789
commit d9b9a0eaec

View File

@@ -144,14 +144,17 @@ function _ensureStyles() {
.impr-mode-shift,
.impr-mode-shift * { cursor: copy !important; }
/* Highlight "widget" elements so the user sees what can be annotated */
/* Highlight annotatable elements in main content, left nav, and right TOC */
.impr-mode-shift #observablehq-main figure,
.impr-mode-shift #observablehq-main h2,
.impr-mode-shift #observablehq-main h3,
.impr-mode-shift #observablehq-main h4,
.impr-mode-shift #observablehq-main [data-widget-name],
.impr-mode-shift #observablehq-sidebar a,
.impr-mode-shift #observablehq-sidebar summary {
.impr-mode-shift #observablehq-sidebar summary,
.impr-mode-shift #observablehq-toc a,
.impr-mode-shift #observablehq-toc .kpi-infobox,
.impr-mode-shift #observablehq-toc [id] {
outline: 1px dashed rgba(99, 102, 241, 0.45);
background: rgba(99, 102, 241, 0.055) !important;
border-radius: 4px;
@@ -163,7 +166,19 @@ function _ensureStyles() {
/* ── Widget name inference ─────────────────────────────────────────────── */
function _inferWidgetName(target) {
// 0. Sidebar navigation: nav link text or section heading
// 0a. Right-margin TOC: link text, KPI box title, or nearest labelled container
if (target.closest("#observablehq-toc")) {
const link = target.closest("a");
if (link) return (link.textContent.trim() || "TOC link") + " (TOC)";
const kpiTitle = target.closest(".kpi-infobox")
?.querySelector(".kpi-infobox-title");
if (kpiTitle) return kpiTitle.textContent.trim() + " (sidebar widget)";
const labelled = target.closest("[id]");
if (labelled) return labelled.id.replace(/-/g, " ") + " (TOC)";
return "Right margin";
}
// 0b. Left sidebar navigation: nav link text or section heading
if (target.closest("#observablehq-sidebar")) {
const link = target.closest("a");
if (link) return link.textContent.trim() || "Nav link";
@@ -232,12 +247,20 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain
_initialized = true;
_ensureStyles();
// Track modifier state via keydown, keyup, AND mousemove so mode
// stays in sync even when focus changes between elements.
// Track modifier state. Highlighting is delayed 1 s so normal Shift+typing
// doesn't trigger the visual mode change; releasing Shift cancels immediately.
let _shiftTimer = null;
function _updateMode(e) {
if (e.shiftKey) {
document.body.classList.add("impr-mode-shift");
if (!_shiftTimer && !document.body.classList.contains("impr-mode-shift")) {
_shiftTimer = setTimeout(() => {
document.body.classList.add("impr-mode-shift");
_shiftTimer = null;
}, 1000);
}
} else {
clearTimeout(_shiftTimer);
_shiftTimer = null;
document.body.classList.remove("impr-mode-shift");
}
}
@@ -245,23 +268,27 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain
window.addEventListener("keyup", _updateMode);
window.addEventListener("mousemove", _updateMode);
// Clear on blur in case Shift is held when the window loses focus
window.addEventListener("blur", () => document.body.classList.remove("impr-mode-shift"));
window.addEventListener("blur", () => {
clearTimeout(_shiftTimer);
_shiftTimer = null;
document.body.classList.remove("impr-mode-shift");
});
document.addEventListener("click", (e) => {
if (!e.shiftKey) return;
const inSidebar = !!e.target.closest("#observablehq-sidebar");
// Block shift-clicks on form controls; allow sidebar links (preventDefault stops navigation)
if (!inSidebar && e.target.matches("input, textarea, select, a, button")) return;
if (inSidebar && e.target.matches("input, textarea, select")) return;
const inToc = !!e.target.closest("#observablehq-toc");
// Block shift-clicks on form controls; allow nav/toc links (preventDefault stops navigation)
if (!inSidebar && !inToc && e.target.matches("input, textarea, select, a, button")) return;
if ((inSidebar || inToc) && e.target.matches("input, textarea, select")) return;
e.preventDefault();
const widgetName = _inferWidgetName(e.target);
const pageName = inSidebar
? "Navigation"
: (document.title
? document.title.replace(" Custodian State Hub", "").trim()
: (location.pathname.replace(/^\//, "") || "Overview"));
const currentPage = document.title
? document.title.replace(" Custodian State Hub", "").trim()
: (location.pathname.replace(/^\//, "") || "Overview");
const pageName = inSidebar ? "Navigation" : currentPage;
// Remove any open modal
document.getElementById("_impr-root")?.remove();