'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}
)
}