'use client' /** * Inline "?"" hint icon with a hover tooltip. * * Use this for any UI element whose meaning isn't obvious from its label — * stat boxes, tab names, jargon-y settings. Goal is the same as the (i) * dots in Linear / Hyperliquid / Coinglass: small, ignorable, one hover * away from a one-line plain-English explanation. * * Design rules: * * `text` MUST be one short sentence — never paste long docs in here. * If you need a paragraph, you're explaining the wrong thing. * * The icon is intentionally subtle (12 px circle, ink-4 colour). It * should disappear visually until the user actively scans for help. * * The tooltip uses CSS hover positioning — no portals. The only JS is * an on-reveal measurement that clamps the bubble inside the viewport * (a centred bubble on an icon near the screen edge used to hang * off-screen, worst on phones). Trade-off: it's still clipped by the * nearest `overflow:hidden` ancestor. Pages that need tooltips inside * a `` cell should add `overflow: visible` to that row/td. * * Usage: * Latest cycle * * */ import { useRef } from 'react' import type { CSSProperties } from 'react' type Placement = 'top' | 'bottom' | 'left' | 'right' interface Props { text: string placement?: Placement /** Tip max-width in px. Default 240 — fits ~2 short lines. */ width?: number /** Tweak the icon's vertical alignment for fussy contexts. */ iconStyle?: CSSProperties } export default function InfoTip({ text, placement = 'top', width = 240, iconStyle, }: Props) { const rootRef = useRef(null) // Clamp the bubble inside the viewport when it's revealed. The bubble is // CSS-centred on the icon, so near a screen edge it overflowed off-screen. // Measured at shift=0, then the needed horizontal offset is written to the // --tip-shift var consumed by the .infotip-top/-bottom transforms. function clampIntoViewport() { const bubble = rootRef.current?.querySelector('.infotip-bubble') if (!bubble) return bubble.style.setProperty('--tip-shift', '0px') const r = bubble.getBoundingClientRect() const vw = document.documentElement.clientWidth const margin = 8 let shift = 0 if (r.left < margin) shift = margin - r.left else if (r.right > vw - margin) shift = (vw - margin) - r.right if (shift !== 0) bubble.style.setProperty('--tip-shift', `${shift}px`) } return ( child for non-CSS-tooltip fallback. role="button" tabIndex={0} aria-label={text} style={{ ['--tip-w' as string]: `${width}px`, ...iconStyle }} onMouseEnter={clampIntoViewport} onFocus={clampIntoViewport} onTouchStart={clampIntoViewport} > {text} ) }