Files
the-custodian/state-hub/dashboard/src/components/help-tip.js
tegwick 531f278f73 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>
2026-02-27 08:11:09 +01:00

168 lines
4.6 KiB
JavaScript

// <help-tip label="Full Name" description="One sentence." doc="/path">ABBR</help-tip>
//
// A custom element that shows a floating card on hover/focus.
// Attributes:
// label — bold title line in the card
// description — body text
// doc — optional URL; adds a "Learn more →" link
//
// The card is appended to document.body (position:fixed) so it escapes
// any overflow:hidden or clipping ancestors (e.g. the TOC sidebar).
const _STYLE_ID = "helptip-global-style";
if (!document.getElementById(_STYLE_ID)) {
const s = document.createElement("style");
s.id = _STYLE_ID;
s.textContent = `
help-tip {
cursor: help;
display: inline;
}
.helptip-card {
position: fixed;
z-index: 9999;
background: var(--theme-background, #fff);
border: 1px solid var(--theme-foreground-faint, #ddd);
border-radius: 9px;
padding: 0.6rem 0.85rem;
max-width: 270px;
min-width: 130px;
box-shadow: 0 6px 22px rgba(0,0,0,0.13), 0 1px 4px rgba(0,0,0,0.07);
font-size: 0.78rem;
line-height: 1.5;
color: var(--theme-foreground, #333);
opacity: 0;
transition: opacity 0.12s ease;
pointer-events: auto;
}
.helptip-card.helptip-visible { opacity: 1; }
.helptip-card-label {
font-weight: 700;
font-size: 0.8rem;
margin-bottom: 0.3rem;
color: var(--theme-foreground, #222);
}
.helptip-card-desc {
color: var(--theme-foreground-muted, #555);
}
.helptip-card-link {
display: inline-block;
margin-top: 0.45rem;
font-size: 0.72rem;
color: var(--theme-foreground-focus, #3b82f6);
text-decoration: none;
}
.helptip-card-link:hover { text-decoration: underline; }
`;
document.head.appendChild(s);
}
class HelpTip extends HTMLElement {
#card = null;
#showTimer = null;
#hideTimer = null;
connectedCallback() {
this.addEventListener("mouseenter", this.#onEnter);
this.addEventListener("mouseleave", this.#onLeave);
this.addEventListener("focusin", this.#onEnter);
this.addEventListener("focusout", this.#onLeave);
}
disconnectedCallback() {
clearTimeout(this.#showTimer);
clearTimeout(this.#hideTimer);
this.#clearCard();
this.removeEventListener("mouseenter", this.#onEnter);
this.removeEventListener("mouseleave", this.#onLeave);
this.removeEventListener("focusin", this.#onEnter);
this.removeEventListener("focusout", this.#onLeave);
}
#onEnter = () => {
clearTimeout(this.#hideTimer);
this.#showTimer = setTimeout(() => this.#showCard(), 80);
};
#onLeave = () => {
clearTimeout(this.#showTimer);
this.#hideTimer = setTimeout(() => this.#clearCard(), 200);
};
#showCard() {
if (this.#card) return;
const label = this.getAttribute("label") || "";
const desc = this.getAttribute("description") || "";
const doc = this.getAttribute("doc") || "";
const card = document.createElement("div");
card.className = "helptip-card";
if (label) {
const h = document.createElement("div");
h.className = "helptip-card-label";
h.textContent = label;
card.appendChild(h);
}
if (desc) {
const d = document.createElement("div");
d.className = "helptip-card-desc";
d.textContent = desc;
card.appendChild(d);
}
if (doc) {
const a = document.createElement("a");
a.className = "helptip-card-link";
a.textContent = "Learn more →";
a.href = doc;
card.appendChild(a);
}
// Keep card alive while mouse is over it
card.addEventListener("mouseenter", () => clearTimeout(this.#hideTimer));
card.addEventListener("mouseleave", this.#onLeave);
document.body.appendChild(card);
this.#card = card;
this.#position(card);
requestAnimationFrame(() => card.classList.add("helptip-visible"));
}
#position(card) {
const rect = this.getBoundingClientRect();
const cw = card.offsetWidth || 230;
const ch = card.offsetHeight || 80;
const gap = 8;
const vw = window.innerWidth;
const vh = window.innerHeight;
// Horizontal: align to left of trigger, clamp inside viewport
let left = rect.left;
if (left + cw + gap > vw) left = vw - cw - gap;
if (left < gap) left = gap;
// Vertical: prefer above; fall back to below
const top = (rect.top - ch - gap >= 0)
? rect.top - ch - gap
: Math.min(rect.bottom + gap, vh - ch - gap);
card.style.left = `${left}px`;
card.style.top = `${top}px`;
}
#clearCard() {
if (this.#card) {
this.#card.remove();
this.#card = null;
}
}
}
if (!customElements.get("help-tip")) {
customElements.define("help-tip", HelpTip);
}
export { HelpTip };