Add fonts, autotable, autotable examples
This commit is contained in:
@@ -1,165 +1,168 @@
|
||||
// Port of web/kit/Popovers.tsx.
|
||||
// Port of web/uikit/Popovers.tsx, rebuilt on the Floating controller (floating.go).
|
||||
//
|
||||
// NOTE: the TSX builds these on Floating.tsx — a floating-ui-style layer with
|
||||
// getBoundingClientRect measurement, requestAnimationFrame reposition, a Portal
|
||||
// to document.body, a global single-open manager, outside-click/Escape handling,
|
||||
// and hover open/close timers, all threaded through a FloatingContext. None of
|
||||
// that has an equivalent in the neutral runtime (no document, portals, refs,
|
||||
// element measurement, or timers), so it is dropped. This port keeps the
|
||||
// component API + Tailwind + event wiring, drives click open/close with a
|
||||
// caller-supplied `open` bool + toggle callback, does hover reveal purely in CSS
|
||||
// (group-hover), and approximates placement with static absolute utility classes
|
||||
// instead of computed coordinates (so flip/shift and a numeric offset are gone —
|
||||
// the gap is a fixed ~8px via the m*-2 classes, matching the TSX default offset).
|
||||
|
||||
// The first Go port dropped everything that made a popover a popover: measured
|
||||
// placement (it used static absolute utilities off a `relative inline-block`
|
||||
// wrapper), the portal, the single-open manager, outside-click / Escape dismissal,
|
||||
// and — for HoverPopover — the hover bridge, which it faked with Tailwind's
|
||||
// `group-hover`. That fake only worked because the panel was a DOM descendant of the
|
||||
// hovered wrapper. The panel is now PORTALED to document.body, so it is not a
|
||||
// descendant of anything the cursor is over and CSS :hover cannot reach it: the
|
||||
// reveal MUST be the timer-based bridge in Floating (OpenOnHover + HoverDelay /
|
||||
// HoverCloseDelay), which holds the panel open while the cursor crosses the gap and
|
||||
// while it rests on the panel itself. That is what NewHoverPopover wires up.
|
||||
//
|
||||
// Shape of the port: the TSX's <Popover> element was only a carrier for a Solid
|
||||
// context, which Go has no equivalent of, so it becomes a CONTROLLER instead — the
|
||||
// state (open, refs, timers, resolved placement) has to outlive a render, so it
|
||||
// cannot be created inside one. There is no wrapper element left: the trigger sits
|
||||
// where you put it and the panel is portaled to the body, so nothing needs to
|
||||
// contain them both.
|
||||
//
|
||||
// pop := webui.NewPopover(webui.PopoverProps{Placement: webui.PlacementBottomEnd})
|
||||
// return func() *vdom.VNode {
|
||||
// return vdom.Div(
|
||||
// pop.Trigger(webui.PopoverTriggerProps{Class: "btn"}, vdom.Text("Filters")),
|
||||
// pop.Content(webui.PopoverContentProps{Class: "p-3 w-64"}, body()...),
|
||||
// )
|
||||
// }
|
||||
package webui
|
||||
|
||||
import "kjol/vdom"
|
||||
|
||||
const popoverCls = "bg-white rounded-default shadow-lg border border-neutral-200"
|
||||
|
||||
// popoverPlacementCls maps a floating placement to static absolute-position
|
||||
// utility classes relative to the Popover's `relative` wrapper.
|
||||
func popoverPlacementCls(placement string) string {
|
||||
switch placement {
|
||||
case "top", "top-start":
|
||||
return "bottom-full left-0 mb-2"
|
||||
case "top-end":
|
||||
return "bottom-full right-0 mb-2"
|
||||
case "bottom-end":
|
||||
return "top-full right-0 mt-2"
|
||||
case "left", "left-start":
|
||||
return "right-full top-0 mr-2"
|
||||
case "left-end":
|
||||
return "right-full bottom-0 mr-2"
|
||||
case "right", "right-start":
|
||||
return "left-full top-0 ml-2"
|
||||
case "right-end":
|
||||
return "left-full bottom-0 ml-2"
|
||||
default: // "bottom" / "bottom-start" and unknown
|
||||
return "top-full left-0 mt-2"
|
||||
}
|
||||
}
|
||||
// TSX defaults: offset ?? 8, hoverDelay ?? 0, hoverCloseDelay ?? 150.
|
||||
const popoverOffset = 8
|
||||
|
||||
// PopoverProps configures Popover. Placement defaults to "bottom-start".
|
||||
// PopoverProps configures NewPopover.
|
||||
type PopoverProps struct {
|
||||
// Placement defaults to "bottom-start"; the resolved placement may differ after
|
||||
// a flip (read it with Placement()).
|
||||
Placement string
|
||||
Class string
|
||||
|
||||
// Offset is the gap between trigger and panel in px; 0 means the TSX default, 8.
|
||||
Offset float64
|
||||
|
||||
// Standalone opts out of the single-open manager: this popover neither closes,
|
||||
// nor is closed by, other floatings. Set it for a popover nested inside another
|
||||
// floating (a popover opened from a menu item), which would otherwise close its
|
||||
// own parent the moment it opened.
|
||||
Standalone bool
|
||||
|
||||
// OnOpenChange is called whenever the panel opens or closes — for callers that
|
||||
// mirror the state (the popover owns it either way; there is no `Open` input,
|
||||
// because a controller that both owns state and takes it is a race).
|
||||
OnOpenChange func(bool)
|
||||
}
|
||||
|
||||
// Popover is the floating wrapper: a relative container that anchors an
|
||||
// absolutely-positioned PopoverContent to a PopoverTrigger. Compose a
|
||||
// PopoverTrigger and a PopoverContent as its children.
|
||||
func Popover(p PopoverProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
return vdom.El("div", kids([]vdom.Mod{vdom.Attr("class", cx("relative inline-block", p.Class))}, children)...)
|
||||
}
|
||||
|
||||
// PopoverTriggerProps configures PopoverTrigger. Open drives aria-expanded and
|
||||
// OnToggle fires on click (the TSX toggles open on click).
|
||||
type PopoverTriggerProps struct {
|
||||
Open bool
|
||||
OnToggle func()
|
||||
Class string
|
||||
Title string
|
||||
}
|
||||
|
||||
// PopoverTrigger is the click target that opens/closes the popover.
|
||||
func PopoverTrigger(p PopoverTriggerProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
expanded := "false"
|
||||
if p.Open {
|
||||
expanded = "true"
|
||||
}
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("type", "button"),
|
||||
vdom.Attr("class", p.Class),
|
||||
vdom.Attr("aria-expanded", expanded),
|
||||
vdom.Attr("aria-haspopup", "menu"),
|
||||
}
|
||||
if p.Title != "" {
|
||||
mods = append(mods, vdom.Attr("title", p.Title))
|
||||
}
|
||||
if p.OnToggle != nil {
|
||||
mods = append(mods, vdom.On(vdom.EVENT_CLICK, p.OnToggle))
|
||||
}
|
||||
return vdom.El("button", kids(mods, children)...)
|
||||
}
|
||||
|
||||
// PopoverContentProps configures PopoverContent. Placement defaults to
|
||||
// "bottom-start"; Open toggles visibility (nil when closed).
|
||||
type PopoverContentProps struct {
|
||||
Open bool
|
||||
Placement string
|
||||
Class string
|
||||
}
|
||||
|
||||
// PopoverContent is the floating panel. It renders nil when Open is false.
|
||||
func PopoverContent(p PopoverContentProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
if !p.Open {
|
||||
return nil
|
||||
}
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("role", "menu"),
|
||||
vdom.Attr("data-floating-content", "true"),
|
||||
vdom.Attr("class", cx("absolute z-[110]", popoverPlacementCls(p.Placement), popoverCls, p.Class)),
|
||||
}
|
||||
return vdom.El("div", kids(mods, children)...)
|
||||
}
|
||||
|
||||
// HoverPopoverProps configures HoverPopover. Placement defaults to
|
||||
// "bottom-start".
|
||||
// HoverPopoverProps configures NewHoverPopover. It is PopoverProps plus the two
|
||||
// hover timings (the TSX's HoverPopoverProps extends PopoverProps the same way).
|
||||
type HoverPopoverProps struct {
|
||||
Placement string
|
||||
Class string
|
||||
}
|
||||
|
||||
// HoverPopover is the hover-driven wrapper. It carries the Tailwind `group`
|
||||
// marker so HoverPopoverContent can reveal itself on hover purely in CSS.
|
||||
func HoverPopover(p HoverPopoverProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
return vdom.El("div", kids([]vdom.Mod{vdom.Attr("class", cx("group relative inline-block", p.Class))}, children)...)
|
||||
}
|
||||
|
||||
// HoverPopoverTriggerProps configures HoverPopoverTrigger. OnMouseEnter /
|
||||
// OnMouseLeave mirror the TSX hover wiring (optional; hover reveal itself is CSS).
|
||||
type HoverPopoverTriggerProps struct {
|
||||
OnMouseEnter func()
|
||||
OnMouseLeave func()
|
||||
Class string
|
||||
}
|
||||
|
||||
// HoverPopoverTrigger is the hover target.
|
||||
func HoverPopoverTrigger(p HoverPopoverTriggerProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
mods := []vdom.Mod{vdom.Attr("type", "button"), vdom.Attr("class", p.Class)}
|
||||
if p.OnMouseEnter != nil {
|
||||
mods = append(mods, vdom.On("mouseenter", p.OnMouseEnter))
|
||||
}
|
||||
if p.OnMouseLeave != nil {
|
||||
mods = append(mods, vdom.On("mouseleave", p.OnMouseLeave))
|
||||
}
|
||||
return vdom.El("button", kids(mods, children)...)
|
||||
}
|
||||
|
||||
// HoverPopoverContentProps configures HoverPopoverContent.
|
||||
type HoverPopoverContentProps struct {
|
||||
Placement string
|
||||
Class string
|
||||
OnMouseEnter func()
|
||||
OnMouseLeave func()
|
||||
Offset float64
|
||||
Standalone bool
|
||||
OnOpenChange func(bool)
|
||||
|
||||
// HoverDelay is how long the cursor must rest on the trigger before the panel
|
||||
// opens (default 0 — the TSX default). HoverCloseDelay is the grace period after
|
||||
// the cursor leaves the trigger OR the panel: the bridge across the gap between
|
||||
// them (default 150ms).
|
||||
HoverDelay int
|
||||
HoverCloseDelay int
|
||||
}
|
||||
|
||||
// HoverPopoverContent is the hover panel: hidden by default and revealed while
|
||||
// the surrounding HoverPopover (group) is hovered.
|
||||
func HoverPopoverContent(p HoverPopoverContentProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
const reveal = "invisible opacity-0 transition-opacity group-hover:visible group-hover:opacity-100"
|
||||
mods := []vdom.Mod{
|
||||
vdom.Attr("role", "menu"),
|
||||
vdom.Attr("data-floating-content", "true"),
|
||||
vdom.Attr("class", cx("absolute z-[110]", popoverPlacementCls(p.Placement), popoverCls, reveal, p.Class)),
|
||||
}
|
||||
if p.OnMouseEnter != nil {
|
||||
mods = append(mods, vdom.On("mouseenter", p.OnMouseEnter))
|
||||
}
|
||||
if p.OnMouseLeave != nil {
|
||||
mods = append(mods, vdom.On("mouseleave", p.OnMouseLeave))
|
||||
}
|
||||
return vdom.El("div", kids(mods, children)...)
|
||||
// Popover is a live popover — the controller that NewPopover and NewHoverPopover
|
||||
// both return. Build it once, alongside your signals, NOT inside a render function
|
||||
// (a Floating rebuilt every frame would lose its refs, its timers and its open
|
||||
// state, i.e. it would never open).
|
||||
type Popover struct{ f *Floating }
|
||||
|
||||
// NewPopover creates a click-to-open popover.
|
||||
func NewPopover(p PopoverProps) *Popover {
|
||||
return &Popover{f: NewFloating(FloatingOptions{
|
||||
Placement: pick(p.Placement, PlacementBottomStart),
|
||||
Offset: pickOffset(p.Offset, popoverOffset),
|
||||
Standalone: p.Standalone,
|
||||
OnOpenChange: p.OnOpenChange,
|
||||
})}
|
||||
}
|
||||
|
||||
// NewHoverPopover creates a hover-to-open popover.
|
||||
//
|
||||
// The hover bridge lives in Floating and is wired on BOTH the trigger and the panel
|
||||
// (Trigger/Panel install it when OpenOnHover is set), so the panel survives the
|
||||
// cursor's trip across the gap and stays up while the cursor is on it — the thing
|
||||
// the old CSS group-hover version could not do once the panel was portaled out of
|
||||
// the wrapper.
|
||||
func NewHoverPopover(p HoverPopoverProps) *Popover {
|
||||
return &Popover{f: NewFloating(FloatingOptions{
|
||||
Placement: pick(p.Placement, PlacementBottomStart),
|
||||
Offset: pickOffset(p.Offset, popoverOffset),
|
||||
Standalone: p.Standalone,
|
||||
OnOpenChange: p.OnOpenChange,
|
||||
OpenOnHover: true,
|
||||
HoverDelay: p.HoverDelay,
|
||||
HoverCloseDelay: p.HoverCloseDelay, // 0 → NewFloating's 150ms
|
||||
})}
|
||||
}
|
||||
|
||||
// Floating exposes the underlying controller (Reposition, Dispose, ...).
|
||||
func (p *Popover) Floating() *Floating { return p.f }
|
||||
|
||||
// IsOpen reports the current state; Placement is the placement AFTER any flip.
|
||||
func (p *Popover) IsOpen() bool { return p.f.IsOpen() }
|
||||
func (p *Popover) Placement() string { return p.f.Placement() }
|
||||
|
||||
// Show, Hide and Toggle drive the panel from outside (a keyboard shortcut, a
|
||||
// "close" button inside the panel, a route change).
|
||||
func (p *Popover) Show() { p.f.Show() }
|
||||
func (p *Popover) Hide() { p.f.Hide() }
|
||||
func (p *Popover) Toggle() { p.f.Toggle() }
|
||||
|
||||
// Dispose closes the panel and drops every listener and timer it owns.
|
||||
func (p *Popover) Dispose() { p.f.Dispose() }
|
||||
|
||||
// PopoverTriggerProps configures Trigger. The Open/OnToggle fields the old port had
|
||||
// are gone: the Popover owns its open state now, and the trigger it renders drives
|
||||
// it (click for NewPopover, hover/focus for NewHoverPopover).
|
||||
type PopoverTriggerProps struct {
|
||||
Class string
|
||||
Title string
|
||||
// OnClick runs in addition to the open/close (for a hover popover, whose trigger
|
||||
// is not a toggle, it is the only thing a click does).
|
||||
OnClick func()
|
||||
}
|
||||
|
||||
// Trigger renders the <button> the panel is anchored to and measured against.
|
||||
func (p *Popover) Trigger(t PopoverTriggerProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
return p.f.Trigger(FloatingTriggerProps{
|
||||
Class: t.Class,
|
||||
Title: t.Title,
|
||||
OnClick: t.OnClick,
|
||||
}, children...)
|
||||
}
|
||||
|
||||
// PopoverContentProps configures Content. The Open/Placement fields the old port had
|
||||
// are gone: open state and placement belong to the Popover.
|
||||
type PopoverContentProps struct {
|
||||
Class string
|
||||
}
|
||||
|
||||
// Content renders the floating panel, portaled to document.body and positioned by
|
||||
// measurement once it is in the DOM.
|
||||
//
|
||||
// Call it on EVERY render, in the same slot. It no longer returns nil when closed —
|
||||
// Floating.Panel renders an empty portal instead, so the panel's slot in the parent's
|
||||
// child list never disappears (the reconciler diffs children by index; a child that
|
||||
// vanishes shifts every sibling after it).
|
||||
func (p *Popover) Content(c PopoverContentProps, children ...*vdom.VNode) *vdom.VNode {
|
||||
return p.f.Panel(FloatingPanelProps{Class: cx(popoverCls, c.Class)}, children...)
|
||||
}
|
||||
|
||||
// pickOffset is pick for a float64 offset: 0 means "unset, use the default".
|
||||
func pickOffset(v, def float64) float64 {
|
||||
if v == 0 {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user