feat(dashboard): shift+click trigger + Improvements section in Todo

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 <body>, 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 <noreply@anthropic.com>
This commit is contained in:
2026-03-17 23:56:34 +01:00
parent b558610de6
commit 46f4b0c25d
2 changed files with 67 additions and 13 deletions

View File

@@ -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 <body>) ─────────────────────── */
.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);

View File

@@ -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`<div class="kpi-infobox">
<div class="kpi-row-value">${thirdParty.length}</div>
</div>
</div>
<div class="kpi-row">
<span class="kpi-row-label">improvements (open)</span>
<div class="kpi-row-right">
<div class="kpi-row-value" style="color:${improvements.length > 0 ? '#6366f1' : 'inherit'}">${improvements.length}</div>
</div>
</div>
</div>`;
// ── 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`<p class="dim">No open improvement suggestions. Shift+click any widget to submit one.</p>`);
} else {
display(html`<div class="task-list">${improvements.map(t => html`
<div class="task-item impr-item">
<div class="task-item-header">
<span class="task-badge" style="background:#ede9fe;color:#4c1d95">improvement</span>
<span class="task-context">${t.location ?? ""}</span>
<a class="impr-review-link" href="/ui-feedback">review →</a>
</div>
<div class="task-title">${t.title.replace(/^UI:\s*/, "")}</div>
${t.description ? html`<div class="impr-desc">${t.description.slice(0, 200)}${t.description.length > 200 ? " …" : ""}</div>` : ""}
</div>
`)}</div>`);
}
```
<style>
/* ── Live indicator ───────────────────────────────────────────────────────── */
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
@@ -231,4 +265,8 @@ if (thirdParty.length === 0) {
.task-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.15rem; }
.task-blocking-reason { font-size: 0.8rem; color: #b45309; background: #fef3c7; border-radius: 4px; padding: 0.2rem 0.5rem; margin-top: 0.25rem; }
.dim { color: gray; font-style: italic; }
.task-item.impr-item { border-left-color: #6366f1; }
.impr-desc { font-size: 0.8rem; color: var(--theme-foreground-muted); margin-top: 0.2rem; line-height: 1.45; }
.impr-review-link { margin-left: auto; font-size: 0.75rem; color: #6366f1; text-decoration: none; }
.impr-review-link:hover { text-decoration: underline; }
</style>