A widget package — whether it’s a small in-app helpers file or a separately-versioned library — follows a consistent shape so consumers can mix it into their factory in two lines. This page is the contract.
One widget per .go file, named after the widget. Optional CSS and JS siblings carrying the same base name. A doc.go for the package doc and a factory.go for the factory type and asset registration:
myorg-widgets/
├── doc.go ← package doc with import path, license, footprint
├── factory.go ← MyOrgFactory + asset registration in init()
├── mywidget.go ← MyWidgetWidget + builder methods + Draw
├── mywidget.css ← optional sibling CSS
├── mywidget.js ← optional sibling JS
└── anotherwidget.go ← one widget per file
This is the layout every package under the framework root uses — see basic/, form/, or any of the optional packages (chart/, code/, richedit/).
Four responsibilities, all in one short file:
package myorg
import (
"embed"
"github.com/microbus-io/bespa/basic"
"github.com/microbus-io/bespa/widget"
)
// MyOrgFactory aggregates the widget constructors of this package.
type MyOrgFactory struct{}
// factory is a private composite the package uses internally to construct
// nested framework widgets.
var factory = struct {
widget.WidgetFactory
basic.BasicFactory
MyOrgFactory
}{}
// Convenience aliases — let the Draw methods in this package write
// Tag(…) and Text(…) instead of factory.Tag(…) and factory.Text(…).
// Not the public entry point; consumers compose MyOrgFactory into their
// own factory struct (see "How consumers compose" below).
var Any = factory.Any
var Many = factory.Many
var Text = factory.Text
var HTML = factory.HTML
var HTMLUnsafe = factory.HTMLUnsafe
var Bytes = factory.Bytes
var Tag = factory.Tag
type Widget = widget.Widget
type InputWidget = widget.InputWidget
type BytesWidget = widget.BytesWidget
// Bundle every static asset the package ships. Globbing by extension
// keeps .go source files (and READMEs, .txt, etc.) out of the binary.
// Add or remove extensions to match what the package contains — each
// pattern must match at least one file.
//go:embed *.css *.js
var bundle embed.FS
func init() {
widget.AssetRegistry.RegisterFS(bundle)
}
MyOrgFactory is the empty type whose methods are the constructors for your widgets. Consumers embed it.factory is the composite your widgets reach for internally — to call factory.Tag, factory.Text, etc.Tag, Text, Widget, etc. that point at the corresponding entries on the composite factory. They let your widgets’ Draw methods write Tag("div") instead of factory.Tag("div"). The full set every framework package keeps in sync is: Any, Many, Text, HTML, HTMLUnsafe, Bytes, Tag (functions) and Widget, InputWidget, BytesWidget (types). They are exported only because Go requires it for cross-file visibility within the package — they are not the intended consumer entry point, which is the factory composition shown below.init() call registers every CSS / JS asset the package owns. The framework’s asset registry handles bundling and delivery.From the calling side, your widgets fall in next to the framework ones:
import (
"github.com/microbus-io/bespa"
"github.com/myorg/myorg-widgets"
)
// Compose MyOrgFactory alongside the framework defaults:
var wf = struct {
bespa.DefaultFactory
myorg.MyOrgFactory
}{}
// Now wf.MyWidget(...) is available with full type-safety on the chain.
Because MyOrgFactory is at the shallowest depth in the composite struct, Go’s selector resolution picks its methods over any same-named method on a more deeply embedded factory. That’s how you can override a built-in widget by providing a same-named constructor.
If two embedded factories both define a method with the same name — say Button — Go’s selector resolution falls back to the shallowest match. When two libraries collide at the same depth, go build reports an ambiguous-selector error. Two ways out:
Button can publish its constructor as PrimaryButton, IconButton, or anything namespaced. Keep the struct type unambiguous (e.g. MyOrgButtonWidget) even when the factory method shares a name.For a published library, document the methods it exports. Two libraries can be composed without surprises as long as their surface-area docs are honest about what names they claim.
Semantic Versioning, the usual rules:
WidgetBase to take a new type parameter, changing the partial-redraw wire format, etc.), publish a corresponding major of your library.WithFoo methods that only exist on framework versions newer than your declared go.mod requirement.github.com/myorg/widgets/v2. Go’s module system enforces this; consumers can run v1 and v2 of your library side-by-side under different import paths if they really need to.v0.x) releases, expect breakage on every minor version and say so in the package doc. Most consumers will pin to an exact tag during this phase.If your package bundles anything weighty — a JS library, a font, large GeoJSON — say so in the package doc:
// Package mywidget embeds Foo.js (~800 KB, MIT). Importing this
// package adds ~1 MB to your binary. See ATTRIBUTIONS.md.
package mywidgetConsumers should know what they’re taking on. The pattern in this codebase is that anything over ~100 KB earns its own opt-in package rather than living in basic/.
Assets & CSS — the asset-registration API the init() calls into.
Material theming — how widget CSS should reference design tokens.
For a worked example, read the entirety of chart/ — it’s a single-widget package with a JS dependency, CSS, a dynamic-asset handler, and a clean factory composition.