/** * One floating tooltip, shared by every page. * * Reference detail — what a card does, why an action is offered, what a limit means — is needed * occasionally and read once. Printing all of it costs the space that the board, the hand and the * action list actually need. So anything carrying `data-tip` shows its text on hover or focus and * nothing the rest of the time. * * Not the browser's native `title`: that waits about a second, cannot be styled, wraps badly at * this length, and never appears for keyboard users. */ let tip: HTMLElement | null = null; function ensure(): HTMLElement { if (tip) return tip; const el = document.createElement('div'); el.id = 'tip'; el.setAttribute('role', 'tooltip'); document.body.appendChild(el); tip = el; return el; } function place(el: HTMLElement, text: string): void { const t = ensure(); t.textContent = text; t.style.display = 'block'; // Measure, then keep it on screen: flip above when it would fall off the bottom, and pull back // from the right edge rather than letting the text run off it. const r = el.getBoundingClientRect(); const box = t.getBoundingClientRect(); const margin = 8; let left = r.left + r.width / 2 - box.width / 2; left = Math.max(margin, Math.min(left, window.innerWidth - box.width - margin)); let top = r.bottom + 6; if (top + box.height > window.innerHeight - margin) top = r.top - box.height - 6; t.style.left = `${Math.round(left + window.scrollX)}px`; t.style.top = `${Math.round(top + window.scrollY)}px`; } function hide(): void { if (tip) tip.style.display = 'none'; } /** * Start listening. Delegated from the document, so markup replaced later — the board redraws on * every action — needs no re-binding. */ export function installTooltips(): void { const find = (node: EventTarget | null): HTMLElement | null => { let el = node as HTMLElement | null; while (el && el !== document.body) { // SVG elements have no closest() in older engines, so walk manually. if (el.getAttribute && el.getAttribute('data-tip')) return el; el = (el.parentNode as HTMLElement) ?? null; } return null; }; document.addEventListener('mouseover', (e) => { const el = find(e.target); if (el) place(el, el.getAttribute('data-tip') ?? ''); else hide(); }); document.addEventListener('mouseout', (e) => { if (!find((e as MouseEvent).relatedTarget)) hide(); }); document.addEventListener('focusin', (e) => { const el = find(e.target); if (el) place(el, el.getAttribute('data-tip') ?? ''); }); document.addEventListener('focusout', hide); document.addEventListener('scroll', hide, true); // Escape closes it, which matters when one is pinned open by focus. document.addEventListener('keydown', (e) => { if ((e as KeyboardEvent).key === 'Escape') hide(); }); } /** Styling, kept here so every page that installs tooltips gets the same one. */ export const TOOLTIP_CSS = ` #tip{position:absolute;z-index:9999;display:none;max-width:340px; background:#0b0e12;color:#e6e9ee;border:1px solid #4d6fa8;border-radius:6px; padding:7px 10px;font:12px/1.45 ui-monospace,Menlo,Consolas,monospace; box-shadow:0 6px 22px rgba(0,0,0,.55);pointer-events:none;white-space:pre-wrap} [data-tip]{cursor:help} button[data-tip],a[data-tip]{cursor:pointer} `;