Add fonts, autotable, autotable examples

This commit is contained in:
2026-07-13 13:01:26 -04:00
parent ea3d2a6d03
commit cf8342f8d4
71 changed files with 16084 additions and 1443 deletions

View File

@@ -2,16 +2,55 @@ package vdom
// DOM event-name constants for On / OnEvent.
const (
EVENT_CLICK = "click"
EVENT_DBLCLICK = "dblclick"
EVENT_INPUT = "input"
EVENT_CHANGE = "change"
EVENT_SUBMIT = "submit"
EVENT_KEYDOWN = "keydown"
EVENT_KEYUP = "keyup"
EVENT_FOCUS = "focus"
EVENT_BLUR = "blur"
EVENT_MOUSEDOWN = "mousedown"
EVENT_MOUSEUP = "mouseup"
EVENT_MOUSEMOVE = "mousemove"
EVENT_CLICK = "click"
EVENT_DBLCLICK = "dblclick"
EVENT_INPUT = "input"
EVENT_CHANGE = "change"
EVENT_SUBMIT = "submit"
EVENT_KEYDOWN = "keydown"
EVENT_KEYUP = "keyup"
EVENT_FOCUS = "focus"
EVENT_BLUR = "blur"
EVENT_MOUSEDOWN = "mousedown"
EVENT_MOUSEUP = "mouseup"
EVENT_MOUSEMOVE = "mousemove"
EVENT_MOUSEENTER = "mouseenter"
EVENT_MOUSELEAVE = "mouseleave"
EVENT_CONTEXTMENU = "contextmenu"
EVENT_SCROLL = "scroll"
EVENT_RESIZE = "resize"
EVENT_WHEEL = "wheel"
// focusin/focusout BUBBLE; focus/blur do not. Anything that needs to know a
// subtree gained focus (a tooltip staying open while a child is focused) must
// use these.
EVENT_FOCUSIN = "focusin"
EVENT_FOCUSOUT = "focusout"
// HTML5 drag-and-drop. The handler's Event carries the DataTransfer via
// SetData / GetData.
EVENT_DRAGSTART = "dragstart"
EVENT_DRAGOVER = "dragover"
EVENT_DRAGENTER = "dragenter"
EVENT_DRAGLEAVE = "dragleave"
EVENT_DROP = "drop"
EVENT_DRAGEND = "dragend"
)
// KeyboardEvent.key values, for Event.Key().
const (
KEY_ESCAPE = "Escape"
KEY_ENTER = "Enter"
KEY_SPACE = " "
KEY_TAB = "Tab"
KEY_BACKSPACE = "Backspace"
KEY_DELETE = "Delete"
KEY_ARROW_UP = "ArrowUp"
KEY_ARROW_DOWN = "ArrowDown"
KEY_ARROW_LEFT = "ArrowLeft"
KEY_ARROW_RIGHT = "ArrowRight"
KEY_HOME = "Home"
KEY_END = "End"
KEY_PAGE_UP = "PageUp"
KEY_PAGE_DOWN = "PageDown"
)

55
go/vdom/ref.go Normal file
View File

@@ -0,0 +1,55 @@
package vdom
// Ref is a handle on the real DOM node a VNode rendered to. It is the bridge the
// neutral component code uses to ask the browser for things it cannot know on its
// own — how big an element is, where it sits in the viewport — without importing
// anything platform-specific: the value inside is opaque (`any`), filled in by the
// wasm reconciler and always nil on the server. Read it through the wasmruntime
// host API (wasmruntime.Rect, SetStyle, Focus, …), which no-ops when it is nil.
//
// A component creates one ref per element it needs to measure and keeps it across
// renders (in a closure, next to its signals — NOT rebuilt inside the render
// function, or it would be a fresh empty ref every frame):
//
// panel := vdom.NewRef()
// return func() *VNode {
// return Div(WithRef(panel), Attr("class", "…"), …)
// }
//
// and later, after the DOM exists (see wasmruntime.AfterRender):
//
// r := wasmruntime.Rect(panel) // zero Rect if unmounted or on the server
type Ref struct{ node any }
// NewRef returns an unattached Ref.
func NewRef() *Ref { return &Ref{} }
// Node is the platform DOM handle (a js.Value in the browser), or nil if the ref
// is not currently attached to a mounted element. Component code should not need
// this — it is for the host API and the reconciler.
func (r *Ref) Node() any {
if r == nil {
return nil
}
return r.node
}
// Mounted reports whether the ref currently points at a live DOM node.
func (r *Ref) Mounted() bool { return r != nil && r.node != nil }
type refMod struct{ ref *Ref }
func (m refMod) apply(n *VNode) { n.Ref = m.ref }
// WithRef attaches a Ref to this element, so the DOM node it renders to can be
// measured and manipulated. Ignored on the server.
func WithRef(r *Ref) Mod { return refMod{r} }
// SetRefNode points a Ref at a platform DOM handle (or nil to detach). It exists
// for the reconciler and the host API, which live in another package; component
// code has no reason to call it.
func SetRefNode(r *Ref, node any) {
if r != nil {
r.node = node
}
}

View File

@@ -1,30 +1,81 @@
package vdom
// HTML tag helpers over El, for dot-import.
// HTML tag helpers over El.
//
// The set covers every tag kjol/webui actually renders, which is the bar that
// matters: a partial set is worse than none, because it forces a half-and-half style
// where `Div(...)` sits next to `El("thead", ...)` and the reader has to know which
// tags happen to be covered. If you reach for a tag that is not here, add it rather
// than falling back to El.
//
// El itself stays exported for the genuinely dynamic case — a tag chosen at runtime,
// as a component's `Tag` prop does.
// --- structure ---
func Div(m ...Mod) *VNode { return El("div", m...) }
func Span(m ...Mod) *VNode { return El("span", m...) }
func P(m ...Mod) *VNode { return El("p", m...) }
func Section(m ...Mod) *VNode { return El("section", m...) }
func Article(m ...Mod) *VNode { return El("article", m...) }
func Aside(m ...Mod) *VNode { return El("aside", m...) }
func Nav(m ...Mod) *VNode { return El("nav", m...) }
func Header(m ...Mod) *VNode { return El("header", m...) }
func Main(m ...Mod) *VNode { return El("main", m...) }
func Footer(m ...Mod) *VNode { return El("footer", m...) }
func H1(m ...Mod) *VNode { return El("h1", m...) }
func H2(m ...Mod) *VNode { return El("h2", m...) }
func H3(m ...Mod) *VNode { return El("h3", m...) }
func Hr(m ...Mod) *VNode { return El("hr", m...) }
func A(m ...Mod) *VNode { return El("a", m...) }
func Strong(m ...Mod) *VNode { return El("strong", m...) }
func Em(m ...Mod) *VNode { return El("em", m...) }
func Small(m ...Mod) *VNode { return El("small", m...) }
func Code(m ...Mod) *VNode { return El("code", m...) }
func Label(m ...Mod) *VNode { return El("label", m...) }
func Ul(m ...Mod) *VNode { return El("ul", m...) }
func Li(m ...Mod) *VNode { return El("li", m...) }
func Form(m ...Mod) *VNode { return El("form", m...) }
func Input(m ...Mod) *VNode { return El("input", m...) }
func Button(m ...Mod) *VNode { return El("button", m...) }
func Br(m ...Mod) *VNode { return El("br", m...) }
// --- headings ---
func H1(m ...Mod) *VNode { return El("h1", m...) }
func H2(m ...Mod) *VNode { return El("h2", m...) }
func H3(m ...Mod) *VNode { return El("h3", m...) }
func H4(m ...Mod) *VNode { return El("h4", m...) }
func H5(m ...Mod) *VNode { return El("h5", m...) }
func H6(m ...Mod) *VNode { return El("h6", m...) }
// --- inline ---
func A(m ...Mod) *VNode { return El("a", m...) }
func Strong(m ...Mod) *VNode { return El("strong", m...) }
func B(m ...Mod) *VNode { return El("b", m...) }
func Em(m ...Mod) *VNode { return El("em", m...) }
func I(m ...Mod) *VNode { return El("i", m...) }
func Small(m ...Mod) *VNode { return El("small", m...) }
func Code(m ...Mod) *VNode { return El("code", m...) }
func Pre(m ...Mod) *VNode { return El("pre", m...) }
// --- lists ---
func Ul(m ...Mod) *VNode { return El("ul", m...) }
func Ol(m ...Mod) *VNode { return El("ol", m...) }
func Li(m ...Mod) *VNode { return El("li", m...) }
// --- forms ---
func Form(m ...Mod) *VNode { return El("form", m...) }
func Fieldset(m ...Mod) *VNode { return El("fieldset", m...) }
func Legend(m ...Mod) *VNode { return El("legend", m...) }
func Label(m ...Mod) *VNode { return El("label", m...) }
func Input(m ...Mod) *VNode { return El("input", m...) }
func Textarea(m ...Mod) *VNode { return El("textarea", m...) }
func Select(m ...Mod) *VNode { return El("select", m...) }
func Option(m ...Mod) *VNode { return El("option", m...) }
func Button(m ...Mod) *VNode { return El("button", m...) }
// --- tables ---
func Table(m ...Mod) *VNode { return El("table", m...) }
func Thead(m ...Mod) *VNode { return El("thead", m...) }
func Tbody(m ...Mod) *VNode { return El("tbody", m...) }
func Tfoot(m ...Mod) *VNode { return El("tfoot", m...) }
func Tr(m ...Mod) *VNode { return El("tr", m...) }
func Th(m ...Mod) *VNode { return El("th", m...) }
func Td(m ...Mod) *VNode { return El("td", m...) }
func Img(m ...Mod) *VNode { return El("img", m...) }
func Caption(m ...Mod) *VNode { return El("caption", m...) }
// --- media and misc ---
func Img(m ...Mod) *VNode { return El("img", m...) }
func Canvas(m ...Mod) *VNode { return El("canvas", m...) }
func Dialog(m ...Mod) *VNode { return El("dialog", m...) }
// Svg and Path are the two SVG tags the icon layer needs. The reconciler creates
// them in the SVG namespace (see wasmruntime): an <svg> built as ordinary HTML is an
// inert unknown element that renders nothing.
func Svg(m ...Mod) *VNode { return El("svg", m...) }
func Path(m ...Mod) *VNode { return El("path", m...) }

View File

@@ -11,14 +11,24 @@ import (
)
// Event is a DOM event passed to handlers. The client provides a concrete
// implementation; on the server events are never invoked.
// implementation; on the server events are never invoked, and every accessor
// returns its zero value.
type Event interface {
PreventDefault()
Value() string // target.value (for inputs)
StopPropagation()
Value() string // target.value (for inputs)
Checked() bool // target.checked (for checkboxes / radios)
Key() string // KeyboardEvent.key — compare against the KEY_* constants
ClientX() int // pointer position, viewport coordinates
ClientY() int //
Target() any // the platform DOM handle of event.target; nil on the server
SetData(format, data string) // DataTransfer — no-ops off a drag event
GetData(format string) string
}
// VNode is a virtual DOM node. Tag == "" is a text node (content in Text). HTML,
// if set on an element, is raw innerHTML (children ignored).
// if set on an element, is raw innerHTML (children ignored). Tag == TagPortal
// renders its children into document.body instead of in place.
type VNode struct {
Tag string
Text string
@@ -28,6 +38,9 @@ type VNode struct {
Events map[string]func(Event)
Children []*VNode
// Ref, if set, receives the DOM node this VNode renders to (see WithRef).
Ref *Ref
// Runtime holds the wasm reconciler's per-node bookkeeping (DOM handle,
// listener wrappers). It's `any` so this package stays platform-neutral; it
// is nil on the server.
@@ -37,6 +50,13 @@ type VNode struct {
// Mod configures a VNode while it is built.
type Mod interface{ apply(*VNode) }
// TagPortal marks a node whose children are mounted into document.body rather
// than into its own parent. It is how a floating panel escapes an ancestor's
// `overflow: hidden` (menus scroll, modals clip) or `transform` (which would
// re-root `position: fixed` onto the modal instead of the viewport). In the tree
// it occupies a hidden, zero-size placeholder; the children live at body level.
const TagPortal = "#portal"
// El builds an element VNode.
func El(tag string, mods ...Mod) *VNode {
n := &VNode{Tag: tag, Attrs: map[string]string{}, Props: map[string]string{}, Events: map[string]func(Event){}}
@@ -46,6 +66,17 @@ func El(tag string, mods ...Mod) *VNode {
return n
}
// Portal renders its children into document.body. On the server it renders the
// placeholder only — floating content is closed during SSR, so there is nothing
// to emit, and the placeholder keeps the client's hydration walk aligned.
func Portal(children ...*VNode) *VNode {
n := El(TagPortal)
for _, c := range children {
c.apply(n)
}
return n
}
// Text builds a text VNode.
func Text(s string) *VNode { return &VNode{Text: s} }
@@ -106,6 +137,13 @@ func writeNode(b *strings.Builder, n *VNode) {
b.WriteString(html.EscapeString(n.Text))
return
}
if n.Tag == TagPortal {
// Placeholder only — the children belong to document.body, which SSR does
// not own. Emitting the same element the client creates keeps hydration's
// childNodes lined up.
b.WriteString(`<div data-portal="" style="display:none"></div>`)
return
}
b.WriteByte('<')
b.WriteString(n.Tag)
writeAttrs(b, n.Attrs)