From 6a9ecf9d46ac35bf50f8518b1ff51d01e409fdae Mon Sep 17 00:00:00 2001 From: Falkan Date: Wed, 18 Mar 2026 19:33:14 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20custom=20tooltip=20(no=20wiggle=20requir?= =?UTF-8?q?ed),=20add=20=E2=93=98=20indicator=20for=20described=20units?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace native title-based tooltip with custom positioned div Native title only renders on mousemove; custom div fires on mouseenter after 500ms delay as intended - Tooltip styled dark with fade-in; Dan Mode gets yellow-on-black with magenta glow - Units with descriptions show a small superscript ⓘ on the result label --- data/units.json | 4 +- src/app.css | 27 +++++++++++ src/components/ConversionResult.svelte | 14 +++++- src/lib/actions/tooltip.ts | 65 +++++++++++++++++++++----- 4 files changed, 96 insertions(+), 14 deletions(-) diff --git a/data/units.json b/data/units.json index 3eb4160..19907d3 100644 --- a/data/units.json +++ b/data/units.json @@ -62,7 +62,9 @@ "labelPlural": "Metric fucktons", "symbol": "mfcktn", "group": "dickloads", - "toBase": 170.2399999998 + "toBase": 170.2399999998, + "description": "Trivia: although the name is \"metric fuckton,\" it is actually equal to two long tons!", + "hidden": false }, { "id": "assload", diff --git a/src/app.css b/src/app.css index 21760fc..4f901e0 100644 --- a/src/app.css +++ b/src/app.css @@ -448,3 +448,30 @@ line-height: 1; border-radius: var(--pico-border-radius); } + +/* ── Custom tooltip ────────────────────────────────────────────────────────── */ +.hu-tooltip { + position: fixed; + z-index: 9999; + max-width: 280px; + padding: 0.4rem 0.6rem; + border-radius: 4px; + font-size: 0.8rem; + line-height: 1.4; + background: #1a1a2e; + color: #f0f0f0; + box-shadow: 0 2px 8px rgba(0,0,0,0.35); + pointer-events: none; + opacity: 0; + transition: opacity 0.15s ease; + white-space: normal; +} +.hu-tooltip--visible { + opacity: 1; +} +[data-theme=dan] .hu-tooltip { + background: #000; + color: #ffff00; + border: 1px solid #ff00ff; + box-shadow: 0 0 8px #ff00ff; +} diff --git a/src/components/ConversionResult.svelte b/src/components/ConversionResult.svelte index c3a677f..98fe5c9 100644 --- a/src/components/ConversionResult.svelte +++ b/src/components/ConversionResult.svelte @@ -73,8 +73,10 @@ N/A {/if} - - {unit.labelPlural} ({unit.symbol}) + + + {unit.labelPlural} ({unit.symbol}){#if description}{/if} + diff --git a/src/lib/actions/tooltip.ts b/src/lib/actions/tooltip.ts index 2fb00d1..9344be4 100644 --- a/src/lib/actions/tooltip.ts +++ b/src/lib/actions/tooltip.ts @@ -1,15 +1,56 @@ /** - * Svelte action: show a native `title` tooltip after a 500ms hover delay. + * Svelte action: show a custom tooltip after a 500ms hover delay. + * Uses a positioned div instead of the native `title` attribute — + * native tooltips only appear on mousemove, not on mouseenter, which + * causes a "hover then wiggle" requirement that feels broken. + * * If text is empty, does nothing. */ + +const TOOLTIP_CLASS = 'hu-tooltip'; + +// Shared tooltip element — one per page, repositioned as needed. +let tooltipEl: HTMLDivElement | null = null; + +function getTooltipEl(): HTMLDivElement { + if (!tooltipEl) { + tooltipEl = document.createElement('div'); + tooltipEl.className = TOOLTIP_CLASS; + document.body.appendChild(tooltipEl); + } + return tooltipEl; +} + export function tooltip(node: HTMLElement, text: string) { let timer: ReturnType | null = null; + let currentText = text; - function enter() { - if (!text) return; - timer = setTimeout(() => { - node.title = text; - }, 500); + function show(e: MouseEvent) { + if (!currentText) return; + const el = getTooltipEl(); + el.textContent = currentText; + + // Position near cursor, clamped to viewport + const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); + const y = e.clientY + 20; + el.style.left = `${x}px`; + el.style.top = `${y}px`; + el.classList.add('hu-tooltip--visible'); + } + + function enter(e: MouseEvent) { + if (!currentText) return; + timer = setTimeout(() => show(e), 500); + } + + function move(e: MouseEvent) { + // Keep tooltip near cursor while hovering + if (!tooltipEl?.classList.contains('hu-tooltip--visible')) return; + const el = getTooltipEl(); + const x = Math.min(e.clientX + 12, window.innerWidth - el.offsetWidth - 8); + const y = e.clientY + 20; + el.style.left = `${x}px`; + el.style.top = `${y}px`; } function leave() { @@ -17,24 +58,26 @@ export function tooltip(node: HTMLElement, text: string) { clearTimeout(timer); timer = null; } - node.removeAttribute('title'); + tooltipEl?.classList.remove('hu-tooltip--visible'); } node.addEventListener('mouseenter', enter); + node.addEventListener('mousemove', move); node.addEventListener('mouseleave', leave); return { update(newText: string) { - text = newText; - // If title is currently set, update it immediately - if (node.title) { - node.title = newText || ''; + currentText = newText; + if (tooltipEl?.classList.contains('hu-tooltip--visible')) { + tooltipEl.textContent = newText; } }, destroy() { if (timer !== null) clearTimeout(timer); node.removeEventListener('mouseenter', enter); + node.removeEventListener('mousemove', move); node.removeEventListener('mouseleave', leave); + tooltipEl?.classList.remove('hu-tooltip--visible'); } }; }