widget

package
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 8, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package widget provides the framework's overlay-UI primitive.

A widget is a self-mounting UI surface that runs on top of any page — regardless of whether that page is built with core-ui components or is a plain HTML document. Widgets are distinct from components:

  • Components are part of a server-rendered page tree.
  • Widgets are siblings that float above the page (fixed corners, modal overlays, toast stacks). They mount themselves via a bootstrap script tag the framework generates.

Architectural goals:

  1. Hosts (kiln, future feature packages) describe widgets declaratively via WidgetDef. They never write DOM/CSS by hand.

  2. The runtime (core-ui/runtime + core-ui/html/Overlay) owns mounting, stacking, focus management, SSE wiring, and RPC dispatch. Hosts only fill slots and declare server-side signals + RPC handlers.

  3. Theming flows through core-ui/style. A widget never embeds hex colors or magic spacing; it references theme tokens ({colors.primary}, {spacing.lg}). Override the theme to reskin every widget at once.

Anatomy of a widget — code skeleton:

def := widget.New("kiln-panel").
    Mount(widget.BottomRight).
    Slot("header", headerComponent).
    Slot("body",   bodyComponent).
    Signal("page", pageSignalSrc).
    SSE("/.kiln/events", "world_edit", "page").
    RPC("POST", "/kiln/tool/reset_session", resetHandler)

widget.Mount(app, def)

The package is intentionally small in interface. Built-in surfaces (FloatingPanel, Modal, Toast, Drawer, Popover) live in core-ui/widget/preset and are constructed with the same WidgetDef builder — no new primitive per surface.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

func Mount(r *router.Router, def *Definition)

Mount registers the widget's HTTP routes on r and adds it to the process-global registry the framework runtime auto-discovers. Hosts don't embed a per-widget script tag — they embed ONE shared runtime tag (see RuntimeTag) and the runtime mounts every registered widget.

Routes mounted on r:

GET  <StylePath>         widget styles (theme-resolved CSS)
GET  <StatePath>         JSON snapshot of all signals (initial render)
GET  /core-ui/widget/<name>/chrome   rendered chrome HTML (lazy)
*    <RPC.Path>          for each RPC, the host's handler

Default paths are filled in on def if unset so the caller can read them after Mount returns. Mount is idempotent on def.Name.

func MountBuilder

func MountBuilder(r *router.Router, b *Builder)

MountBuilder builds the widget from b and mounts it on r — sugar over the two-step

def := b.Build()
widget.Mount(r, &def)

which every caller otherwise repeats. Use Builder.Hidden() in the chain for widgets (modals, drawers) that start closed.

func MountRuntime

func MountRuntime(r *router.Router)

MountRuntime registers the framework runtime endpoints on r:

GET /__gofastr/runtime.js                 the default bundled runtime
                                          (single-payload IIFE every
                                          page ships)
GET /__gofastr/runtime/<name>.js          one split runtime module
                                          (loaded on demand via the
                                          optional module-loader path)
GET /__gofastr/widgets                    JSON list of registered widgets

Call this once per host (kiln serve, examples/site, etc.). The runtime IIFE is idempotent, so re-mounting on rebuilds is safe.

func RenderChrome

func RenderChrome(d *Definition) string

RenderChrome returns the rendered chrome HTML for a single widget, using its registered Skeleton or the framework's defaultSkeleton. Exported so SSR hosts can inline chrome without instantiating `server` themselves.

func RuntimeModuleHash

func RuntimeModuleHash(name string) string

RuntimeModuleHash returns the content-addressed hash for a split runtime module. Used by client-side preload tags + by the loader to construct `?v=<hash>` URLs. Empty string if the module isn't embedded.

func RuntimeModuleManifestScript

func RuntimeModuleManifestScript() string

RuntimeModuleManifestScript emits an inert JSON manifest mapping every split runtime module to its content-addressed hash. Returns "" when no modules are embedded.

Both RuntimeTag (kiln + manual hosts) and framework/uihost embed this script. Pages without the manifest fall through to un-versioned module URLs and then collide with the immutable cache headers — see TestRuntimeTagEmbedsModuleManifest for the regression that motivated this.

func RuntimeTag

func RuntimeTag() string

RuntimeTag returns the markup a host page embeds to load the framework runtime + auto-mount every registered widget. The URL includes a content-hash query param so a new server build invalidates any cached runtime in the browser without manual hard-reload.

Also emits an inert <script type="application/json" id="gofastr-runtime-modules"> manifest mapping each split runtime module (popover, toasts, widgets, …) to its content-addressed hash. The client-side module loader reads this manifest to build cache-busted `?v=<hash>` URLs. Without it, kiln-style hosts that don't go through framework/uihost would fall through to un-versioned URLs and end up poisoning the browser cache (server returns `Cache-Control: ...immutable` unconditionally).

Implemented as a func, not a const, because the hashes are computed lazily from the embedded JS bytes.

func ServeRuntimeModule

func ServeRuntimeModule(w http.ResponseWriter, r *http.Request)

ServeRuntimeModule is the exported handler for /__gofastr/runtime/<name>.js. Hosts that mount routes via uihost get it through framework/uihost; kiln and standalone hosts can wire it themselves alongside MountRuntime.

func ServeWidgetList

func ServeWidgetList(w http.ResponseWriter, r *http.Request)

ServeWidgetList returns the JSON list of registered widgets — the payload the framework runtime fetches at /__gofastr/widgets to discover and mount what's been registered with widget.Mount. Exported so hosts that already own the /__gofastr/widgets route (e.g. framework/uihost, which serves an empty stub for widget-free apps) can delegate to the registry without double-registering the HTTP route.

Types

type BootstrapMode

type BootstrapMode string

BootstrapMode selects how the widget injects itself onto a page.

  • AutoScript: framework emits a <script> tag and the runtime mounts it on every page that includes the tag. Used by kiln's panel.
  • Embedded: the widget is rendered as part of an existing page tree (no script tag). Useful when the page already runs core-ui.
const (
	AutoScript BootstrapMode = "auto-script"
	Embedded   BootstrapMode = "embedded"
)

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder fluently composes a Definition. Use widget.New(name) to start.

func New

func New(name string) *Builder

New starts a builder for a widget Definition with sensible defaults.

func (*Builder) Backdrop

func (b *Builder) Backdrop() *Builder

Backdrop forces a backdrop regardless of position.

func (*Builder) Bootstrap

func (b *Builder) Bootstrap(m BootstrapMode) *Builder

func (*Builder) Build

func (b *Builder) Build() Definition

Build returns the assembled Definition.

func (b *Builder) DeepLink(key, value string) *Builder

DeepLink wires this widget to a query-string pair. When the URL includes `?key=value`, the SSR layer opens the widget at first paint and the runtime mirrors open/close as pushState updates — so refresh, share, and the browser back button all work.

Pair with DeepLinkParam to pass extra data into the widget's signals.

Intended for modal/drawer presets; not for toasts or dropdowns.

func (*Builder) DeepLinkParam

func (b *Builder) DeepLinkParam(key string) *Builder

DeepLinkParam registers a query-string key whose value is mirrored into a same-named signal whenever this widget opens via deep link. Call once per param. Example:

preset.Modal("user-edit").
    Hidden().
    DeepLink("modal", "user-edit").
    DeepLinkParam("user_id").
    Signal("user_id", widget.SignalFunc(func() (any, error) { ... })).

Visiting `/users?modal=user-edit&user_id=42` opens the modal with signal "user_id" pre-seeded to "42".

func (*Builder) Definition

func (b *Builder) Definition() *Definition

Definition returns a pointer to the in-progress Definition so preset builders (drawer / modal / toast) can tweak fields the fluent API doesn't expose setters for. Callers building widgets by hand should still finish with .Build().

func (*Builder) DescribedBy

func (b *Builder) DescribedBy(id string) *Builder

DescribedBy sets aria-describedby on the widget root. The id MUST match an element in the rendered slot HTML.

func (*Builder) DragDismiss

func (b *Builder) DragDismiss() *Builder

DragDismiss enables drag-to-dismiss for bottom-edge widgets. See Definition.DragDismiss for the full contract.

func (*Builder) Hidden

func (b *Builder) Hidden() *Builder

Hidden marks the widget as registered-but-not-auto-mounted. Open it from a button with data-fui-open="<name>".

func (*Builder) LabelledBy

func (b *Builder) LabelledBy(id string) *Builder

LabelledBy sets aria-labelledby on the widget root. The id MUST match an element in the rendered slot HTML.

func (*Builder) Mount

func (b *Builder) Mount(p Position) *Builder

func (*Builder) Pages

func (b *Builder) Pages(paths ...string) *Builder

Pages scopes the widget to exact path matches. The widget is hidden from the catalog + SSR-inlining on every page whose path isn't in the list. Stack with PagesPrefix / PagesMatch to combine rules.

func (*Builder) PagesMatch

func (b *Builder) PagesMatch(fn func(path string) bool) *Builder

PagesMatch scopes the widget to paths accepted by the supplied matcher. Use for non-trivial rules (e.g. regex, glob, denylist).

func (*Builder) PagesPrefix

func (b *Builder) PagesPrefix(prefixes ...string) *Builder

PagesPrefix scopes the widget to paths that start with any of the given prefixes. Useful for section-wide modals (e.g. every /customers/* page).

func (*Builder) RPC

func (b *Builder) RPC(method, path string, h http.Handler) *Builder

RPC registers a server-side handler the widget can invoke from the client. method=="" defaults to "POST".

func (*Builder) RPCWithSignal

func (b *Builder) RPCWithSignal(method, path string, h http.Handler, signal string) *Builder

RPCWithSignal is RPC + ResponseSignal: on success the handler's JSON response body is pushed into the named signal. Useful for "fetch and render" flows where a button click updates a region.

func (*Builder) Role

func (b *Builder) Role(r string) *Builder

Role sets the ARIA role on the widget root (e.g. "dialog", "alertdialog", "menu"). Pair with LabelledBy / DescribedBy for a complete a11y label.

func (*Builder) SSE

func (b *Builder) SSE(path, event, signal string) *Builder

SSE binds an SSE event to a signal. When the event fires on the stream at path, the event's payload becomes the named signal's new value.

func (*Builder) SSERefetch

func (b *Builder) SSERefetch(path, event, signal string) *Builder

SSERefetch binds an SSE event to a signal whose value is rendered server-side. The runtime re-fetches /state on each event and applies the named signal's fresh value, rather than using the event's payload. Use for HTML/derived signals where the SSE event is just a trigger.

func (*Builder) SSEReload

func (b *Builder) SSEReload(path, event string, matchPairs ...string) *Builder

SSEReload triggers a full page reload on the SSE event. Use for events that change what's rendered at the current URL (e.g. kiln's add_page / delete_page / add_route — the page itself is now different). Pass matchPairs as alternating key/value strings to filter on payload fields (e.g. SSEReload(path, "world_edit", "op", "add_page")).

func (*Builder) Signal

func (b *Builder) Signal(name string, src SignalSource) *Builder

Signal registers a named server-side signal source. The runtime pushes new values to client DOM nodes bound to it.

func (*Builder) Skeleton

func (b *Builder) Skeleton(fn func(slots map[string]render.HTML) render.HTML) *Builder

Skeleton overrides the default chrome wrapper.

func (*Builder) Slot

func (b *Builder) Slot(name string, c component.Component) *Builder

type Definition

type Definition struct {
	Name      string
	Position  Position
	Bootstrap BootstrapMode
	Slots     []Slot
	Signals   map[string]SignalSource
	SSE       []SSEBinding
	RPCs      []RPCEndpoint

	// Skeleton is the host's chrome wrapper. If nil, the framework
	// uses a sensible default for the chosen Position (FloatingPanel
	// for corners, Modal for Center, etc.). Most hosts leave this
	// nil and use a preset.
	Skeleton func(slots map[string]render.HTML) render.HTML

	// ExtraCSS is host-supplied CSS appended after the framework's
	// chrome rules in the per-widget stylesheet (/<StylePath>). Use
	// for content styling (slot innards, host-specific class names)
	// that doesn't fit in the page theme. Generate it through
	// core-ui/style.NewStyleSheet for token consistency, or pass an
	// already-resolved CSS string.
	ExtraCSS func() string

	// Modal flags
	Backdrop            bool // dim the page behind the widget
	CloseOnEscape       bool // ESC closes the widget
	CloseOnClickOutside bool

	// Role is the ARIA role applied to the widget root element.
	// Defaults to "dialog" for backdrop'd widgets and is left empty
	// for plain panels / floating surfaces. Use "alertdialog" for
	// widgets that demand the user's immediate attention.
	Role string

	// LabelledBy is the id of an element (typically a heading) inside
	// the slot HTML that names this widget for screen readers. Becomes
	// aria-labelledby on the widget root. The host is responsible for
	// putting a matching id on the element.
	LabelledBy string

	// DescribedBy is the id of an element inside the slot HTML that
	// provides supplementary description for the widget. Becomes
	// aria-describedby on the widget root.
	DescribedBy string

	// Hidden=true means the widget is registered but NOT auto-mounted
	// on page load. A button with data-fui-open="<name>" calls
	// __gofastr.openWidget(name) to mount it on demand. Use for
	// modals + drawers that should appear in response to user action.
	Hidden bool

	// DeepLinkKey is the URL query parameter that controls open state
	// for this widget — e.g. "modal". When the request URL contains
	// `?<DeepLinkKey>=<DeepLinkValue>`, the SSR layer renders the
	// widget open at first paint AND the runtime mirrors open/close to
	// pushState so refresh/share/back-button all stay consistent.
	//
	// Empty (the default) disables deep-linking — the widget remains
	// purely click-driven via data-fui-open.
	//
	// Only meaningful for Hidden widgets (modal / drawer). Toasts and
	// dropdowns intentionally do NOT support deep links.
	DeepLinkKey string
	// DeepLinkValue is the literal value of DeepLinkKey that opens
	// THIS widget. Multiple widgets can share the same DeepLinkKey
	// ("modal") as long as their DeepLinkValue is distinct
	// ("user-edit", "confirm-delete").
	DeepLinkValue string
	// DeepLinkParams lists additional query parameters whose values
	// should be mirrored into named signals when the widget opens via
	// deep link. e.g. ["user_id"] with URL `?modal=user-edit&user_id=42`
	// seeds signal "user_id"="42" before the slot renders.
	DeepLinkParams []string

	// Routes scopes the widget to specific request paths. When non-
	// empty, the SSR layer and the runtime catalog only expose this
	// widget on pages whose path is accepted by at least one matcher.
	// Empty (the default) means "available on every page" — the
	// behaviour before per-page scoping shipped.
	//
	// Constructed via the Builder methods .Pages, .PagesPrefix,
	// .PagesMatch (or manually for advanced cases).
	Routes []RouteMatcher

	// DragDismiss enables pointer-driven drag-to-dismiss for bottom-edge
	// widgets. The chrome renders a visible drag-handle bar at the top
	// of the panel; the runtime listens for pointerdown/move/up on the
	// handle (and on the panel itself) and closes the widget when the
	// user drags past a distance + velocity threshold. Snaps back to
	// the resting position when released earlier.
	//
	// Only meaningful for Bottom (and bottom-edge) positions today;
	// silently no-op elsewhere.
	DragDismiss bool

	// Asset path overrides. Default routes are derived from Name.
	BootstrapPath string // default: /core-ui/widget/<name>/bootstrap.js
	StylePath     string // default: /core-ui/widget/<name>/style.css
	StatePath     string // default: /core-ui/widget/<name>/state
}

Definition is the full description of a widget. It is built via the New(name) builder and consumed by Mount(app, def).

func AllForSSR

func AllForSSR() []*Definition

AllForSSR returns a snapshot of every registered widget. Exported so SSR hosts (framework/uihost) can walk the registry and inline chrome HTML on the page response. The returned slice is a copy of the live registry; callers may iterate freely.

func AvailableOn

func AvailableOn(path string) []*Definition

AvailableOn returns the subset of registered widgets visible on the given request path. The SSR host + per-request catalog handler use this to keep page-scoped widgets out of unrelated pages.

func (*Definition) IsAvailableOn

func (d *Definition) IsAvailableOn(path string) bool

IsAvailableOn returns true when the widget is registered for the given request path. Widgets with no Routes (the default) are available everywhere; widgets with one or more matchers are available on paths accepted by at least one matcher.

type Position

type Position string

Position is where the widget's root anchors itself when mounted. Modal/Center implies a backdrop and focus trap; Corner positions float without a backdrop.

const (
	BottomRight  Position = "bottom-right"
	BottomCenter Position = "bottom-center" // toast / banner stack mid-edge
	BottomLeft   Position = "bottom-left"
	TopRight     Position = "top-right"
	TopCenter    Position = "top-center" // toast / banner stack mid-edge
	TopLeft      Position = "top-left"
	Center       Position = "center"    // modal — backdrop + focus trap
	Top          Position = "top"       // banner across the top
	Bottom       Position = "bottom"    // banner across the bottom
	Edge         Position = "edge-left" // drawer-style edge mount
	EdgeRight    Position = "edge-right"
)

type RPCEndpoint

type RPCEndpoint struct {
	Method         string // "POST" by default
	Path           string
	Handler        http.Handler
	ResponseSignal string
}

RPCEndpoint is a server-side HTTP handler the widget can invoke from the client (typically via a button click or form submit). The runtime POSTs to Path; on success it can push the response body into a signal named ResponseSignal (optional).

type RouteMatcher

type RouteMatcher func(path string) bool

RouteMatcher decides whether a widget is available on a given request path. A Definition with one or more matchers is filtered out of catalogs + SSR-inlining for paths that no matcher accepts. A Definition with NO matchers is available on every path (the historical default).

type SSEBinding

type SSEBinding struct {
	Path   string `json:"path"`   // e.g. "/.kiln/events"
	Event  string `json:"event"`  // e.g. "world_edit"
	Signal string `json:"signal"` // e.g. "page"

	// Refetch=true tells the runtime to re-fetch /state and apply the
	// signal's fresh value instead of using the SSE event's payload.
	// Use when the signal source is server-rendered (HTML, derived
	// state) and the SSE event is just a "something changed" trigger.
	Refetch bool `json:"refetch,omitempty"`

	// Reload=true tells the runtime to do a full page reload on this
	// event. Useful for events that change which page is rendered at
	// the current URL (kiln add_page / delete_page / add_route /
	// delete_route). Signal is ignored when Reload is set.
	Reload bool `json:"reload,omitempty"`

	// Match optionally filters the binding to events whose JSON
	// payload contains all the listed key=value pairs. Useful when
	// the SSE channel multiplexes by event type (e.g. "world_edit")
	// and the host wants to react only to specific ops.
	Match map[string]string `json:"match,omitempty"`
}

SSEBinding maps a server-sent event kind to a signal name. When the named SSE event arrives on the bus mounted at Path, the runtime pushes the event payload into the named signal — every DOM node bound to that signal updates.

type SignalFunc

type SignalFunc func() (any, error)

SignalFunc is a func adapter for SignalSource.

func (SignalFunc) Read

func (f SignalFunc) Read() (any, error)

type SignalSource

type SignalSource interface {
	Read() (any, error)
}

SignalSource produces JSON-serializable values that flow to the browser as named signals. The runtime polls (or receives via SSE) and pushes new values into [data-fui-signal="<name>"] html.

type Slot

type Slot struct {
	Name      string
	Component component.Component
}

Slot is a host-supplied content region in the widget chrome. The runtime renders the widget skeleton and embeds the slot's component at the matching named placeholder.

Directories

Path Synopsis
Package preset bundles the most common widget surfaces as opinionated builders on top of widget.Definition.
Package preset bundles the most common widget surfaces as opinionated builders on top of widget.Definition.
Package theme provides the framework's default page theme — the visual identity for any app built via core-ui (or its consumers like kiln).
Package theme provides the framework's default page theme — the visual identity for any app built via core-ui (or its consumers like kiln).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL