From 46f4b0c25d6f697787a2dbdec4721446afc62535 Mon Sep 17 00:00:00 2001 From: tegwick Date: Tue, 17 Mar 2026 23:56:34 +0100 Subject: [PATCH] feat(dashboard): shift+click trigger + Improvements section in Todo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit improvement-modal.js: - Replace contextmenu handler with click+shiftKey check — browser context menu is no longer intercepted - Add keydown/keyup/blur listeners: holding Shift applies .impr-shift-mode to , switching cursor to crosshair across the entire page as a visual affordance - Update hint text to "Ctrl + Enter to submit · Escape to cancel" todo.md: - New "Improvements" section shows open dashboard-improvement TD items with a "review →" link to the UI Feedback page - KPI sidebar row added for open improvement count (indigo when > 0) Co-Authored-By: Claude Sonnet 4.6 --- dashboard/src/components/improvement-modal.js | 28 +++++++--- dashboard/src/todo.md | 52 ++++++++++++++++--- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/dashboard/src/components/improvement-modal.js b/dashboard/src/components/improvement-modal.js index 591e45e..8b58c44 100644 --- a/dashboard/src/components/improvement-modal.js +++ b/dashboard/src/components/improvement-modal.js @@ -1,5 +1,5 @@ /** - * improvement-modal — right-click any dashboard widget to suggest an improvement. + * improvement-modal — Shift+click any dashboard widget to suggest an improvement. * * Usage (once per page, usually via _footer.md): * import {initImprovementModal} from "./components/improvement-modal.js"; @@ -10,6 +10,10 @@ * * Otherwise the component walks the DOM to infer the nearest section heading. * Submissions are stored as technical-debt items with debt_type="dashboard-improvement". + * + * Interaction: + * - Hold Shift → cursor changes to crosshair across the entire page + * - Shift+click any element (except form controls) → opens suggestion modal */ const _STYLE_ID = "improvement-modal-styles"; @@ -136,8 +140,9 @@ function _ensureStyles() { @keyframes _im-tin { from { opacity:0; transform:translateX(-50%) translateY(6px) } to { opacity:1; transform:translateX(-50%) translateY(0) } } @keyframes _im-tout { from { opacity:1 } to { opacity:0 } } -/* ── Right-click hint cursor on interactive elements ────────────────────── */ -.impr-hint-cursor { cursor: context-menu; } +/* ── Shift-held cursor override (applied to ) ─────────────────────── */ +.impr-shift-mode, +.impr-shift-mode * { cursor: crosshair !important; } `; document.head.append(s); } @@ -204,8 +209,19 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain _initialized = true; _ensureStyles(); - document.addEventListener("contextmenu", (e) => { - // Don't intercept native right-clicks on form controls or links + // Shift-held cursor indicator + document.addEventListener("keydown", (e) => { + if (e.key === "Shift") document.body.classList.add("impr-shift-mode"); + }); + document.addEventListener("keyup", (e) => { + if (e.key === "Shift") document.body.classList.remove("impr-shift-mode"); + }); + // Remove shift-mode if window loses focus while Shift is held + window.addEventListener("blur", () => document.body.classList.remove("impr-shift-mode")); + + document.addEventListener("click", (e) => { + if (!e.shiftKey) return; + // Don't intercept shift-clicks on form controls or links if (e.target.matches("input, textarea, select, a, button")) return; e.preventDefault(); @@ -255,7 +271,7 @@ export function initImprovementModal({ apiBase = "http://127.0.0.1:8000", domain const hint = Object.assign(document.createElement("div"), { className: "impr-hint", - textContent: "Ctrl + Enter to submit", + textContent: "Ctrl + Enter to submit · Escape to cancel", }); body.append(ctxLabel, chip, sugLabel, textarea, hint); diff --git a/dashboard/src/todo.md b/dashboard/src/todo.md index b02e13c..ee2ea88 100644 --- a/dashboard/src/todo.md +++ b/dashboard/src/todo.md @@ -11,14 +11,15 @@ const THIS_REPO = "the-custodian"; // Live poll: tasks + workstreams + topics + contributions const todoState = (async function*() { while (true) { - let tasks = [], contribs = [], wsMap = {}, ok = false; + let tasks = [], contribs = [], improvements = [], wsMap = {}, ok = false; try { - const [rt, rw, rto, rr, rc] = await Promise.all([ + const [rt, rw, rto, rr, rc, ri] = await Promise.all([ fetch(`${API}/tasks/?limit=500`), fetch(`${API}/workstreams/`), fetch(`${API}/topics/`), fetch(`${API}/repos/`), fetch(`${API}/contributions/`), + fetch(`${API}/technical-debt/?debt_type=dashboard-improvement`), ]); ok = rt.ok && rw.ok && rto.ok && rr.ok && rc.ok; if (ok) { @@ -37,19 +38,21 @@ const todoState = (async function*() { domain: wsMap[t.workstream_id]?.domain ?? "unknown", })); contribs = contribList; + improvements = ri.ok ? (await ri.json()).filter(t => t.debt_type === "dashboard-improvement" && t.status === "open") : []; } } catch {} - yield {tasks, contribs, ok, ts: new Date()}; + yield {tasks, contribs, improvements, ok, ts: new Date()}; await new Promise(res => setTimeout(res, POLL)); } })(); ``` ```js -const tasks = todoState.tasks ?? []; -const contribs = todoState.contribs ?? []; -const _ok = todoState.ok ?? false; -const _ts = todoState.ts; +const tasks = todoState.tasks ?? []; +const contribs = todoState.contribs ?? []; +const improvements = todoState.improvements ?? []; +const _ok = todoState.ok ?? false; +const _ts = todoState.ts; ``` ```js @@ -102,6 +105,12 @@ const _kpiBox = html`
${thirdParty.length}
+
+ improvements (open) +
+
${improvements.length}
+
+
`; // ── Live indicator ──────────────────────────────────────────────────────────── @@ -198,6 +207,31 @@ if (thirdParty.length === 0) { } ``` +--- + +## Improvements + +Dashboard suggestions submitted via Shift+click. Review and action on the +[UI Feedback](/ui-feedback) page; open items shown here for visibility. + +```js +if (improvements.length === 0) { + display(html`

No open improvement suggestions. Shift+click any widget to submit one.

`); +} else { + display(html`
${improvements.map(t => html` +
+
+ improvement + ${t.location ?? ""} + review → +
+
${t.title.replace(/^UI:\s*/, "")}
+ ${t.description ? html`
${t.description.slice(0, 200)}${t.description.length > 200 ? " …" : ""}
` : ""} +
+ `)}
`); +} +``` +