initial port of the UI kit
This commit is contained in:
101
go/webui/tooltips.go
Normal file
101
go/webui/tooltips.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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...)
|
||||
}
|
||||
Reference in New Issue
Block a user