// ABBR // // 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 };