feat(dashboard): replace title tooltips with <help-tip> web component

New custom element (src/components/help-tip.js):
- Floating card appears on hover/focus, appended to document.body
  (position:fixed) so it escapes overflow:hidden in the TOC sidebar
- Attributes: label (bold), description (body), doc (optional
  "Learn more →" link)
- Mouse-over-card grace period so the link stays reachable
- Correct viewport clamping (horizontal + prefer-above/fallback-below)

workstreams.md:
- WHI metric abbreviations (DD/BR/SPR/PEP/CDDR) now use <help-tip>
  with full name, one-sentence description, and doc link
- Domain breakdown labels show domain-scoped stats (open count,
  blocked%, runnable%) and a doc link
- Cycle ⚠ icon upgraded to <help-tip> with explanation
- Removed dotted underline; cursor:help comes from the element CSS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:11:09 +01:00
parent 29a0368e6d
commit f8e76deeaa
2 changed files with 181 additions and 10 deletions

View File

@@ -134,6 +134,7 @@ const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unkno
```js
import {injectTocTop} from "./components/toc-sidebar.js";
import {withDocHelp} from "./components/doc-overlay.js";
import "./components/help-tip.js";
// ── Live indicator ────────────────────────────────────────────────────────────
const _liveEl = html`<div class="live-indicator">
@@ -158,11 +159,11 @@ function _warnLevel(name, val) {
function _warnColor(lv) { return lv === 2 ? "#dc2626" : lv === 1 ? "#d97706" : "var(--theme-foreground-muted, #666)"; }
const _whiMetrics = [
{name: "DD", val: _DD, fmt: v => v.toFixed(2), tip: "Dependency Density — average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
{name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", tip: "Blocked Ratio — share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
{name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", tip: "Single-Point Risk — share of open workstreams that are depended on by others but have no incoming dependencies themselves; losing one stalls everything downstream."},
{name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", tip: "Parallel Execution Potential — share of open workstreams with zero blocking dependencies that could start or continue immediately."},
{name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", tip: "Cross-Domain Dependency Ratio — share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
{name: "DD", val: _DD, fmt: v => v.toFixed(2), label: "Dependency Density", desc: "Average number of dependencies per open workstream; high values indicate a tightly coupled graph that is hard to parallelise."},
{name: "BR", val: _BR, fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio", desc: "Share of open workstreams currently in a blocked state; directly reduces the work that can proceed right now."},
{name: "SPR", val: _SPR, fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk", desc: "Share of workstreams depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
{name: "PEP", val: _PEP, fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workstreams with zero blocking dependencies that could start or continue immediately."},
{name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", label: "Cross-Domain Dependency Ratio", desc: "Share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
];
const _whiBox = html`<div class="kpi-infobox whi-box">
@@ -179,8 +180,8 @@ const _whiBox = html`<div class="kpi-infobox whi-box">
${_whiMetrics.map(m => {
const lv = _warnLevel(m.name, m.val);
return html`<div class="whi-metric-row">
<span class="whi-metric-name" style="color:${_warnColor(lv)}" title="${m.tip}">${m.name}</span>
<span class="whi-metric-val" style="color:${_warnColor(lv)}">${m.fmt(m.val)}</span>
<help-tip class="whi-metric-name" style="color:${_warnColor(lv)}" label="${m.label}" description="${m.desc}" doc="/docs/workstream-health-index">${m.name}</help-tip>
<span class="whi-metric-val" style="color:${_warnColor(lv)}">${m.fmt(m.val)}</span>
</div>`;
})}
</div>
@@ -189,9 +190,12 @@ const _whiBox = html`<div class="kpi-infobox whi-box">
<div class="whi-domain-header">by domain</div>
${_domainBreakdown.map(d => html`<div class="whi-domain-row">
<span class="whi-domain-dot" style="background:${_whiColor(d.whi)}"></span>
<span class="whi-domain-name">${d.domain}</span>
<help-tip class="whi-domain-name"
label="${d.domain.replaceAll('_', ' ')}"
description="Domain-scoped WHI (intra-domain edges only). Open: ${d.openCount} · Blocked: ${(d.br*100).toFixed(0)}% · Runnable: ${(d.pep*100).toFixed(0)}%"
doc="/docs/workstream-health-index">${d.domain}</help-tip>
<span class="whi-domain-score" style="color:${_whiColor(d.whi)}">${(d.whi*100).toFixed(0)}%</span>
${d.cpi === 1 ? html`<span style="color:#d97706;font-size:0.7rem" title="cycle detected">⚠</span>` : ""}
${d.cpi === 1 ? html`<help-tip style="color:#d97706;font-size:0.7rem" label="Dependency Cycle" description="A circular dependency exists within this domain — workstreams are waiting on each other and cannot all proceed." doc="/docs/workstream-health-index">⚠</help-tip>` : ""}
</div>`)}
</div>` : ""}
`}
@@ -323,7 +327,7 @@ if (wsWithDeps.length === 0) {
.whi-cycle-alert { background: #fef2f2; color: #dc2626; border-radius: 4px; padding: 0.2rem 0.45rem; font-size: 0.72rem; font-weight: 600; margin-bottom: 0.4rem; }
.whi-metrics { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; margin-bottom: 0.35rem; }
.whi-metric-row { display: flex; justify-content: space-between; padding: 0.16rem 0; }
.whi-metric-name { font-family: monospace; font-size: 0.72rem; text-decoration: underline dotted; text-underline-offset: 2px; cursor: help; }
.whi-metric-name { font-family: monospace; font-size: 0.72rem; }
.whi-metric-val { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.78rem; }
.whi-domains { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; }
.whi-domain-header { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-faint, #aaa); margin-bottom: 0.2rem; }