// Port of web/kit/Tooltips.tsx. // // NOTE: the TSX builds tooltips on Floating.tsx (floating-ui-style measured // positioning with flip/shift, a Portal, a FloatingContext, and hover/focus // open-delay timers). The neutral runtime has none of that, so this port keeps // the API + Tailwind + arrow markup and instead reveals the bubble purely in CSS // via the Tailwind `group` marker (group-hover / group-focus-within). Placement // is approximated with static absolute utility classes (no flip/shift and no // measured coordinates), the numeric offset collapses to the fixed ~8px m*-2 gap // (the TSX default), and the open delay is dropped. package webui import "kjol/vdom" const tooltipCls = "bg-neutral-800 text-white text-sm px-2.5 py-1.5 rounded-default shadow-lg max-w-80 relative" // tooltipArrowCls returns the arrow classes for a tooltip on the given base side // (the arrow points back toward the trigger). Ported verbatim from arrowCls. func tooltipArrowCls(base string) string { const common = "absolute w-0 h-0" switch base { case "top": return common + " -bottom-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-t-[6px] border-t-neutral-800" case "bottom": return common + " -top-[6px] left-1/2 -translate-x-1/2 border-l-[6px] border-l-transparent border-r-[6px] border-r-transparent border-b-[6px] border-b-neutral-800" case "left": return common + " -right-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-l-[6px] border-l-neutral-800" case "right": return common + " -left-[6px] top-1/2 -translate-y-1/2 border-t-[6px] border-t-transparent border-b-[6px] border-b-transparent border-r-[6px] border-r-neutral-800" default: return common } } // tooltipPlacementCls positions the bubble relative to the trigger wrapper. func tooltipPlacementCls(placement string) string { switch placement { case "bottom": return "top-full left-1/2 -translate-x-1/2 mt-2" case "left": return "right-full top-1/2 -translate-y-1/2 mr-2" case "right": return "left-full top-1/2 -translate-y-1/2 ml-2" default: // "top" return "bottom-full left-1/2 -translate-x-1/2 mb-2" } } // tooltipRevealCls returns the CSS reveal classes for the given trigger mode. func tooltipRevealCls(trigger string) string { if trigger == "focus" { return "invisible opacity-0 transition-opacity group-focus-within:visible group-focus-within:opacity-100" } return "invisible opacity-0 transition-opacity group-hover:visible group-hover:opacity-100" } // TooltipProps configures Tooltip. Trigger is "hover" (default) or "focus"; // Placement is "top" (default), "bottom", "left" or "right". type TooltipProps struct { Content *vdom.VNode Trigger string Placement string Class string } // Tooltip wraps children (the trigger) and shows Content in a floating bubble on // hover (or keyboard focus when Trigger is "focus"). func Tooltip(p TooltipProps, children ...*vdom.VNode) *vdom.VNode { placement := pick(p.Placement, "top") bubble := vdom.El("div", vdom.Attr("role", "tooltip"), vdom.Attr("data-floating-content", "true"), vdom.Attr("class", cx("absolute z-[110]", tooltipPlacementCls(placement), tooltipCls, tooltipRevealCls(p.Trigger))), ) if p.Content != nil { bubble.Children = append(bubble.Children, p.Content) } bubble.Children = append(bubble.Children, vdom.El("div", vdom.Attr("class", tooltipArrowCls(placement)))) // The wrapper carries `group` + `tabindex` so both hover and keyboard-focus // reveal work in CSS. inline-block mirrors the TSX trigger's display. mods := []vdom.Mod{ vdom.Attr("class", cx("group relative inline-block", p.Class)), vdom.Attr("tabindex", "0"), } mods = kids(mods, children) mods = append(mods, bubble) return vdom.El("span", mods...) } // HoverTooltip is Tooltip with hover reveal (the TSX default variant). func HoverTooltip(content *vdom.VNode, placement, class string, children ...*vdom.VNode) *vdom.VNode { return Tooltip(TooltipProps{Content: content, Trigger: "hover", Placement: placement, Class: class}, children...) } // FocusTooltip is Tooltip revealed on keyboard focus. func FocusTooltip(content *vdom.VNode, placement, class string, children ...*vdom.VNode) *vdom.VNode { return Tooltip(TooltipProps{Content: content, Trigger: "focus", Placement: placement, Class: class}, children...) }