alacris

package module
v0.11.1 Latest Latest
Warning

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

Go to latest
Published: Aug 19, 2026 License: MIT Imports: 23 Imported by: 0

README

alacris-go

Build alacris web components from Go and templ.

Typed wrappers generated from your define() calls · the runtime served from Go, no npm · server-driven props with no HTML on the wire

CI Docs Go Reference license

Documentation → · Go API →

@ui.Board(ui.BoardProps{Items: items}).
    ID("board").
    On(ui.BoardEventAdd, "add-card")
live.On(srv, "add-card", func(c *live.Ctx, d ui.BoardAddDetail) error {
    list.Add(d.Text, d.Column)
    c.Session.Element("board").Set("items", list.Items())   // one property write
    return nil
})

A prop is a signal. A server change is one DOM property write and one node update: no HTML over the wire, and focus, scroll position and typed text stay put.

Those names are the example app's: examples/todo generates wrappers into package ui and aliases the design system as m3. In a new project, generate app wrappers to ./internal/components and import github.com/bmartel/alacris-go/ui as ui.

Install

go get github.com/bmartel/alacris-go

The alacris runtime is vendored into the module and served from Go, so a Go project needs no npm. Alacris UI (sixty-eight Material Design 3 components) is vendored the same way. Config.UI turns it on; the typed wrappers live in github.com/bmartel/alacris-go/ui.

What the server can and cannot render

A component's shadow content is produced by its setup() function when the element connects, in the browser. There is no server-side rendering of component internals and no declarative shadow DOM. What Go renders is the element itself:

<user-card name="Ada" age="36" tags="[&quot;math&quot;,&quot;code&quot;]">
  <h3 slot="title">Ada Lovelace</h3>   <!-- light DOM: in the first paint -->
</user-card>

Slot content is real HTML in the document, so it is in the first paint and in a crawler's view of the page. Everything inside the shadow root appears once the module loads. alacris.Pending is the stylesheet that keeps that transition from flashing.

Every prop crosses as an attribute, objects and arrays included. alacris coerces an attribute using the type of the prop's default, and JSON.parse is what it uses for object and array defaults. A fully-formed component needs no post-load property assignment, and the page is complete before any JavaScript has run.

Layers

Each works on its own.

alacris render elements, serve the runtime. Stateless, ordinary request/response.
gen + alacris-go generate typed Go wrappers from your define() calls.
live push prop changes from the server, receive component events.
app the same live handler, in an OS webview. Nested module, -tags desktop.
1. Rendering

Element implements templ.Component, so it drops straight into a .templ file and picks up children from the enclosing block:

@alacris.E("user-card").Prop("name", "Ada").Prop("tags", []string{"math", "code"}) {
    <h3 slot="title">Ada Lovelace</h3>
}

Serve the runtime and emit the script tags:

mux.Handle("/_alacris/", alacris.RuntimeHandler())
<head>
    @ui.Pending()
    @alacris.Scripts(alacris.Config{
        UI:      true,                 // Material Design 3 catalog + theme
        Modules: []string{"/web/components.js"},
        Version: build.Revision,   // makes each release a distinct URL
    })
</head>

Scripts writes the import map (alacris, alacris/store, alacris/context, alacris/signal) and your module entry points. Set Config.UI to also load Alacris UI (@alacris/core points at the same bytes as alacris, so the page has one reactive graph). It belongs in <head>: an import map has to precede the first module import it applies to.

2. Generating typed wrappers

Write components in JavaScript, because setup() runs in the browser:

/**
 * A person, at a glance.
 *
 * @prop {string[]} tags   the labels shown under the name
 * @fires greet {name: string} - the user said hello
 * @slot  title - replaces the heading
 * @cssprop [--card-bg=#fff] - the background
 */
define('user-card', {
  props: { name: 'anon', age: 0, tags: [] },
  setup({ name, age, tags }, host) { /* ... */ },
});
alacris-go generate ./web/components -o ./internal/components

You get a UserCardProps struct, a UserCard function returning an *alacris.Element, typed event details, slot name constants, and the theming contract:

@components.UserCard(components.UserCardProps{Name: "Ada", Age: 36}).
    Apply(components.UserCardVars, map[string]string{"--card-bg": "#ffe9a8"}) {
    <span slot={ components.UserCardSlotTitle }>Ada Lovelace</span>
}

Because the generator knows each prop's default, a value equal to it is left off the element. Smaller HTML, and only this layer can know to do it.

The scanner reads the define() call itself, so there is one source of truth and no second file to keep in step. It is a scanner, not a JavaScript engine: a props object it cannot read as literal data is an error, never a guess. When a component builds its props at runtime, write it into a manifest by hand and generate from that. generate accepts either.

alacris-go generate <path>... -o <dir>   write Go wrappers
alacris-go check    <path>... -o <dir>   fail if they are out of date (for CI)
alacris-go manifest <path>...            write what it found, as JSON

Wire it next to the templ step:

//go:generate go run github.com/bmartel/alacris-go/cmd/alacris-go generate ./web -o ./internal/components -strip ala-
//go:generate go run github.com/a-h/templ/cmd/templ@latest generate
JSDoc tags

define() says what a prop is called and how it is coerced. These say the rest.

Tag What it does
@prop {type} name description overrides the inferred Go type, documents the field
@fires name {a: string} - description generates an event constant and a typed detail struct
@slot name - description generates a slot name constant
@cssprop [--x=default] - description generates the theming contract
@goname Name overrides the derived Go identifier
@goimport alias path imports a package for a go: type

Types: string, number (float64), integer, boolean, object, any, T[], Array<T>, Record<string, T>, and go:YourType for a Go type you declared by hand.

3. Server-driven reactivity
srv := live.New()
defer srv.Close()

mux := http.NewServeMux()
live.Mount(mux, alacris.DefaultBase, srv)   // runtime + client + endpoints

Per page render, mint a session and put it in the page:

// Reads or sets the cookie that authorises this browser, so call it before
// writing anything to w.
sess := srv.NewSession(w, r)
sess.OnOpen(func(s *live.Session) { push(s) })   // also runs after a reconnect

cfg := alacris.Config{Live: true, Page: sess.ID(), /* ... */}

Patches, one property write per change, coalesced into one frame:

sess.Batch(func() {
    sess.Element("board").Set("items", list.Items())
})

Actions: a component's CustomEvents forwarded to named server actions:

@ui.Board(props).ID("board").On(ui.BoardEventAdd, "add-card")
live.On(srv, "add-card", func(c *live.Ctx, d ui.BoardAddDetail) error { ... })

One delegated listener per event type covers every element, present and future; alacris events are composed and bubbling, so it works across shadow boundaries.

The transport is SSE down and an ordinary POST up. No WebSocket, no extra dependency. Handle.SetHTML is there for the cases props express badly, but props are the better tool nearly every time.

This layer costs a stateful server and session affinity behind a load balancer. The first two layers do not depend on it.

Prop encoding

The Go type decides the encoding, and it has to agree with the type of the prop's default in define(). Generated wrappers guarantee that.

Go attribute matching default
string, fmt.Stringer, time.Time text a string
bool "true" / "false" a boolean
integers, floats a number a number
slices, maps, structs JSON an object or array

Three rules exist because of sharp edges in the runtime:

  • Booleans are always written out, never signalled by presence. Removing an attribute runs coerce(null, default), which returns false even when the declared default is true.
  • A boolean prop whose default is true generates a *bool field. Go's zero value is false, so a plain field could never mean "leave it alone" and the component's default would be unreachable.
  • Integers past 253−1 are an error, not a rounding. The value is coerced with +v on the other side. Send large identifiers as strings.

A prop patch sent by live uses the JavaScript prop name (maxCount), because it writes the DOM property. A server-rendered prop uses the same name and this library kebab-cases it into the attribute (max-count) exactly the way define.js does, quirks included.

Security

  • The live capability is an HttpOnly cookie, set by NewSession with SameSite=Lax, a path scoped to the live endpoints, and Secure over TLS. It never appears in a page, a URL or a log, and script cannot read it. What the page carries is a page id, which is an identifier and not a secret: without the cookie it reaches nothing. Serve pages that set the cookie no-store.
  • Action payloads are input. A well-behaved component emits what it says it emits; a console can emit anything. Binding is strict, the body is size-capped, and cross-origin posts are refused. Validate what you decode.
  • Interpolated prop values are attribute values, never markup. There is nothing to escape and no way to forget.
  • Style and Var refuse anything that could escape a declaration, rather than silently substituting a placeholder.
  • Under a Trusted Types CSP, allow both policies: Content-Security-Policy: trusted-types alacris alacris-live; Config.Nonce (or templ.WithNonce) puts a nonce on every script tag.

The example

go run ./examples/todo
# http://localhost:8080

go run -tags desktop ./examples/todo -desktop

A live board the server owns. Components in JavaScript, wrappers generated from them, card state in Go, every change arriving as one prop write. Open it in two windows. Move a card in one and it slides columns in the other; whatever you were typing does not. Neither tab polls.

go run ./examples/todo -demo

-demo is a collaborator that moves cards on a timer, so a one-window recording is enough to film the same trick.

Each lane has its own each(), so the columns are real stacks. A card that stays in a lane keeps its node when the list is rewritten; a card that changes lane is created in the destination. That is the cost of the layout, and the identity tests reorder inside a lane so they still catch an each placed inside a conditional.

Vendored runtime

assets/ holds alacris RuntimeVersion, published to npm, byte-pinned by assets_test.go. Refresh it with:

go run ./internal/vendorjs             # fetch and write
go run ./internal/vendorjs -check      # verify, change nothing

A vendored copy of someone else's build goes stale silently. The failing test is how you find out.

Building with AI agents

AGENTS.md is a drop-in file that teaches coding agents the conventions here: the encoding rules, that ui/ is generated, that each must not sit inside a conditional, that a reconnecting page needs OnOpen. Put it in your project root:

curl -o AGENTS.md https://bmartel.github.io/alacris-go/AGENTS.md

There is also an llms.txt map of the documentation for agents that fetch docs on demand.

Documentation

Full documentation, with every Go example rendered by the library itself, is at bmartel.github.io/alacris-go.

Every example on the site is extracted from internal/docsgen/examples.go with go/ast and then executed. The HTML shown beside the Go is what that Go actually rendered, and go test ./... fails if the two stop matching.

Development

go test ./...
cd app && go test ./...
go test -race ./...
go generate ./examples/todo/...

go run ./internal/docsgen        # re-render the documentation examples
cd docs && npm install && npm run dev

Browser tests cover the claims go test cannot reach: that a server-driven update moves rows instead of rebuilding them, that focus and a half-typed draft survive it, and that every open tab stays in step:

cd e2e && npm install && npx playwright install chromium
npx playwright test

They run against examples/todo unmodified.

License

MIT. The vendored alacris runtime is MIT too; see assets/LICENSE.alacris.

Documentation

Overview

Package alacris renders alacris web components from Go and templ.

alacris (https://github.com/bmartel/alacris) is a ~6 kB web component library built on signals and fine-grained DOM updates. This package is the server half: it renders the tags, gets the props across the boundary correctly, and serves the runtime.

What the server can and cannot render

A component's shadow content is produced by its setup() function when the element connects, in the browser. There is no server-side rendering of component internals and no declarative shadow DOM support. What Go renders is the element itself:

<user-card name="Ada" age="36">
  <h3 slot="title">Ada Lovelace</h3>   <!-- light DOM, visible immediately -->
</user-card>

Slot content is real HTML in the document, so it is in the first paint and in the crawler's view of the page. Everything inside the shadow root appears once the module loads. See Pending for the stylesheet that keeps that transition from flashing.

Props

Every prop crosses as an attribute, including objects and arrays: alacris coerces an attribute using the type of the prop's default, and JSON.parse is what it uses for object and array defaults. That means a fully-formed component needs no post-load property assignment, and the page works with JavaScript still in flight.

The encoding rules are in EncodeProp. Two of them exist because of sharp edges in the runtime:

  • Booleans are always written out as "true" or "false" rather than by presence. Removing an attribute runs coerce(null, default), which returns false even when the declared default is true.
  • Integers beyond JavaScript's safe range are an error, not a rounding. Send large identifiers as strings.

Layers

The package divides into three, each usable on its own, plus an optional desktop host in a nested module:

alacris  render elements and serve the runtime. Stateless.
gen      generate typed Go wrappers from your define() calls.
live     push prop changes from the server and receive component events.
app      the same live handler in an OS webview (github.com/bmartel/alacris-go/app).

The first two are ordinary request/response rendering. The third makes the server authoritative over component state: because a prop is a signal, "the server changed something" compiles to a single property write and a single DOM node update, with no HTML on the wire and no morphing.

Alacris UI ships in the ui subpackage: sixty-eight Material Design 3 components, enabled with Config.UI. A zero Config.Theme still applies the Material default scheme.

Content Security Policy

alacris registers a Trusted Types policy named by TrustedTypesPolicy for template parsing, and contains no eval. The live client registers a second, named alacris-live, which it uses only for Handle.SetHTML. Under a trusted-types directive, allow the ones you use:

Content-Security-Policy: trusted-types alacris alacris-live; require-trusted-types-for 'script'

Config.Nonce (or templ.WithNonce on the context) puts a nonce on every script tag this package emits.

Index

Examples

Constants

View Source
const (
	AssetCore    = "alacris.js"
	AssetCoreDev = "alacris.dev.js"
	AssetStore   = "store.js"
	AssetContext = "context.js"
	AssetSignal  = "signal.js"
	AssetLive    = "live.js"
	AssetUI      = "ui/index.js"
	AssetUITheme = "ui/theme/index.js"
)

Asset file names served by RuntimeHandler.

View Source
const DefaultBase = "/_alacris/"

DefaultBase is where the runtime is expected to be mounted.

View Source
const MaxSafeInteger = 1<<53 - 1

MaxSafeInteger is JavaScript's Number.MAX_SAFE_INTEGER. Integers beyond this magnitude cannot survive the trip through alacris' numeric coercion (`+v`), which produces a float64. Encoding one is an error rather than a silent rounding: send large identifiers as strings instead.

View Source
const RuntimeVersion = "0.11.3"

RuntimeVersion is the version of the alacris npm package vendored in assets/. Regenerate with `go generate ./...` after bumping it.

View Source
const TrustedTypesPolicy = "alacris"

TrustedTypesPolicy is the Trusted Types policy name alacris registers for template parsing. Under a trusted-types CSP directive it has to be allowed.

View Source
const UIVersion = "0.4.0"

UIVersion is the version of the @alacris/ui package vendored in assets/ui/.

Variables

View Source
var ErrNonFiniteFloat = errors.New("alacris: NaN and Inf cannot be encoded as a prop")

ErrNonFiniteFloat is returned for NaN and ±Inf, which have no attribute form.

View Source
var ErrUnsafeInteger = errors.New("alacris: integer exceeds JavaScript's safe integer range; encode it as a string")

ErrUnsafeInteger is returned when an integer prop cannot be represented exactly as a JavaScript number.

Functions

func AcceptsGzip added in v0.3.0

func AcceptsGzip(r *http.Request) bool

AcceptsGzip reports whether the request says gzip is an acceptable content coding. A quality of zero is a refusal, not an acceptance.

func Assets

func Assets() fs.FS

Assets returns the vendored alacris runtime as a filesystem, for projects that would rather copy the files into their own asset pipeline than serve them from Go.

func AttrName

func AttrName(prop string) string

AttrName converts a component's JavaScript prop name to the attribute name alacris observes for it.

It mirrors define.js exactly:

const kebab = s => s.replace(/[A-Z]/g, c => '-' + c.toLowerCase());

including its sharp edges — a leading capital produces a leading dash, and an acronym expands letter by letter ("URL" becomes "-u-r-l"). Reproducing the quirks matters more than being tasteful: the attribute has to match what the element is listening for.

func EncodeAttr

func EncodeAttr(v any) (s string, bare bool, ok bool, err error)

EncodeAttr renders v as an ordinary HTML attribute value, following HTML rules rather than alacris' — a true bool becomes a bare attribute and a false one disappears. bare is true when the attribute should be written without a value.

func EncodeProp

func EncodeProp(v any) (s string, ok bool, err error)

EncodeProp renders v as the attribute text for an alacris prop.

The encoding is chosen from v's Go type, and must line up with the type of the prop's default in the component's define() call, because that default is what drives coercion on the other side (define.js, coerce):

Go string, fmt.Stringer, time.Time  ->  text        (default: a string)
Go bool                             ->  "true"/"false" (default: a boolean)
Go integers and floats              ->  a number    (default: a number)
everything else                     ->  JSON        (default: an object or array)

ok is false when v is nil or a nil pointer, meaning the attribute should be left off entirely so the component keeps its declared default.

func IsZero

func IsZero(v any) bool

IsZero reports whether v is its type's zero value. Generated wrappers use it to decide whether a prop of a named or struct type was set at all, where there is no operator they can rely on.

func MatchETag added in v0.3.0

func MatchETag(ifNoneMatch, etag string) bool

MatchETag reports whether an If-None-Match header matches etag. Unlike a substring check it honours the header's real grammar: a comma-separated list, an optional W/ weakness prefix, and "*" matching anything.

func RuntimeHandler

func RuntimeHandler() http.Handler

RuntimeHandler serves the vendored alacris runtime.

It resolves requests by file name only, so it behaves the same at any mount point and does not need http.StripPrefix:

mux.Handle("/_alacris/", alacris.RuntimeHandler())

func Scripts

func Scripts(cfg Config) templ.Component

Scripts is shorthand for cfg.Scripts().

func SetCacheHeaders

func SetCacheHeaders(h http.Header, r *http.Request, etag string)

SetCacheHeaders applies the caching policy for a served asset.

An asset URL with no version in it must be revalidated: it is the same URL before and after a deploy, so a long max-age means a fixed bug stays broken in every browser that already has the old copy. Revalidation costs one conditional request and almost always answers 304.

Config.Version puts a ?v= on the URLs it emits, and that is what makes an asset safe to cache for a year: a new version is a new URL.

func ValidAttrName

func ValidAttrName(name string) error

ValidAttrName reports whether name can be written into an HTML start tag.

An attribute name is structure, not content: it goes into the tag verbatim, because there is no escaping that would leave it meaning the same thing. So this is an allowlist rather than a list of characters to avoid — a name that is not recognisably a name is refused, and the caller finds out at render rather than the browser finding out at parse.

A leading '-' is permitted: AttrName produces one for a prop whose name begins with a capital, matching define.js, and the element really is listening for that attribute.

func ValidElementName added in v0.1.2

func ValidElementName(name string) error

ValidElementName reports whether name can be written as a tag.

Used for the wrapper a slot's content is placed in. Raw-text elements are refused outright: a slot wrapper has no business being one, and content that is escaped everywhere else would not be escaped inside it.

func ValidTagName

func ValidTagName(name string) error

ValidTagName reports whether name is a valid custom element name — the same rule customElements.define enforces, so a typo fails at render time on the server rather than silently rendering an unknown element in the browser.

Types

type Config

type Config struct {
	// Base is the URL prefix RuntimeHandler is mounted at. Defaults to
	// DefaultBase.
	Base string

	// Dev swaps in the unminified build, which carries readable errors.
	Dev bool

	// Modules are your own ES module entry points, loaded after the import
	// map. This is where the file that calls define() for your components
	// goes.
	Modules []string

	// Imports adds or overrides import map entries, for pulling in component
	// packages of your own by bare specifier.
	Imports map[string]string

	// Live loads the live client and connects it. Page must be the id of a
	// session created by the live package for this page render.
	//
	// The page id is not a secret and does not need protecting: it says which
	// of a browser's pages is talking, and it is useless without the cookie
	// the live package sets, which script cannot read and which never appears
	// in a page or a URL.
	Live bool
	Page string

	// Endpoint is where the live client connects. Defaults to Base + "live".
	Endpoint string

	// NoRecover turns off the live client's automatic recovery. By default,
	// when the patch stream fails permanently — the server restarted, the
	// session expired — the client probes the endpoint until the server
	// answers, reattaches if the session survived, and reloads the page once
	// for a fresh session if it did not. Set NoRecover when the application
	// listens for 'alacris:live' window events and owns that decision itself.
	NoRecover bool

	// Nonce is the CSP nonce for the emitted script tags. When empty, the
	// nonce carried on the context by templ.WithNonce is used.
	Nonce string

	// Version is appended to every asset URL as ?v=, which makes each release
	// a distinct URL and lets the handler cache it for a year instead of
	// revalidating it. Any string that changes with a deploy works: a release
	// tag, a build id, a commit.
	Version string

	// UI loads Alacris UI — every Material Design 3 component, the token
	// system, and applyTheme. The typed wrappers live in the ui subpackage.
	//
	// A zero Theme still applies the default Material scheme (seed #e8ad18,
	// Google Sans Flex, light/dark from the OS). Set Theme to re-skin the
	// page; every component follows because they consume system tokens.
	UI    bool
	Theme Theme
}

Config describes the script tags a page needs.

func (Config) ImportMap

func (c Config) ImportMap() map[string]string

ImportMap returns the import map this configuration produces, so the same specifiers can be reused by a bundler or a test.

func (Config) Scripts

func (c Config) Scripts() templ.Component

Scripts renders the import map, your module entry points, and, when configured, the live client.

It belongs in <head>, before any other module script: an import map has to precede the first module import it applies to.

Example

Scripts emits the import map and module tags a page needs; with Version set, every asset URL is release-specific and cacheable for a year.

package main

import (
	"context"
	"os"

	alacris "github.com/bmartel/alacris-go"
)

func main() {
	cfg := alacris.Config{
		Version: "1.0.0",
		Modules: []string{"/static/app.js"},
	}
	_ = cfg.Scripts().Render(context.Background(), os.Stdout)
}
Output:
<script type="importmap">{"imports":{"alacris":"/_alacris/alacris.js?v=1.0.0","alacris/context":"/_alacris/context.js?v=1.0.0","alacris/signal":"/_alacris/signal.js?v=1.0.0","alacris/store":"/_alacris/store.js?v=1.0.0"}}</script><script type="module" src="/static/app.js"></script>

type Element

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

Element is a server-rendered alacris custom element: a tag, its props, its ordinary HTML attributes, and its light-DOM children.

It implements templ.Component, so it can be used directly in a .templ file:

@alacris.E("user-card").Prop("name", "Ada").Prop("tags", []string{"a", "b"}) {
    <h3 slot="title">Ada Lovelace</h3>
}

What the server can render is the tag, its attributes and its children. A component's shadow content is built by setup() in the browser and never appears in the HTML — see the package documentation.

Builder methods collect errors instead of panicking; Render reports them.

func E

func E(tag string) *Element

E starts an element with the given custom element tag name. An invalid tag name is recorded as an error and surfaces when the element is rendered.

Example

Every prop crosses as an attribute, objects and arrays included, so the element is complete before any JavaScript has run.

package main

import (
	"context"
	"os"

	alacris "github.com/bmartel/alacris-go"
)

func main() {
	card := alacris.E("user-card").
		Prop("name", "Ada").
		Prop("tags", []string{"math", "code"}).
		ID("ada")
	_ = card.Render(context.Background(), os.Stdout)
}
Output:
<user-card name="Ada" tags="[&#34;math&#34;,&#34;code&#34;]" id="ada"></user-card>

func (*Element) Apply

func (e *Element) Apply(vars VarSet, values map[string]string) *Element

Apply sets custom properties from a component's theming contract, rejecting anything the contract does not declare. It is Vars with the safety net:

@ui.Chip(props).Apply(ui.ChipVars, map[string]string{"--chip-bg": "#ffe9a8"})

func (*Element) Attr

func (e *Element) Attr(name string, v any) *Element

Attr sets an ordinary HTML attribute, using HTML's rules rather than alacris' — a true bool renders bare and a false one is omitted.

"class" appends to the element's class list. "style" is rejected in favour of Style and Var, which sanitize their input.

func (*Element) Attrs

func (e *Element) Attrs(attrs templ.Attributes) *Element

Attrs spreads a templ attribute map onto the element. Keys are applied in sorted order.

func (*Element) Children

func (e *Element) Children(c templ.Component) *Element

Children sets the element's light-DOM children explicitly, instead of taking them from the templ block that encloses the element.

func (*Element) Class

func (e *Element) Class(names ...string) *Element

Class appends CSS class names. Empty names are ignored.

func (*Element) ClassIf

func (e *Element) ClassIf(cond bool, names ...string) *Element

ClassIf appends the names only when cond holds.

func (*Element) Err

func (e *Element) Err() error

Err reports the first problem recorded while building the element, if any.

func (*Element) ID

func (e *Element) ID(id string) *Element

ID sets the element's id. The live package addresses elements by id, so any element the server intends to patch needs one.

func (*Element) On

func (e *Element) On(event, action string) *Element

On forwards a CustomEvent the component emits to a named server action, handled by the live package. It renders as a data-ala-on attribute; without the live client script on the page it does nothing.

func (*Element) Prop

func (e *Element) Prop(name string, v any) *Element

Prop sets a component prop, converting name to the attribute alacris observes for it and encoding the value for that prop's declared type. A nil value leaves the attribute off so the component keeps its own default.

Prop values are encoded from their Go type (see EncodeProp), so the Go type has to agree with the type of the prop's default in define(). Generated wrappers guarantee that; hand-written calls are on their own.

func (*Element) PropRaw

func (e *Element) PropRaw(name, value string) *Element

PropRaw sets a prop to an already-encoded attribute value, skipping the encoding rules. The caller is responsible for matching the prop's declared type — a prop whose default is an object needs valid JSON here, and alacris silently falls back to the default when JSON.parse throws.

func (*Element) Props

func (e *Element) Props(props map[string]any) *Element

Props sets several props at once. Keys are sorted so output is stable.

func (*Element) Render

func (e *Element) Render(ctx context.Context, w io.Writer) error

Render writes the element. Props come first, then class and style, then any remaining attributes, so output is stable and diffable.

func (*Element) Slot

func (e *Element) Slot(name string, c templ.Component) *Element

Slot renders c as light-DOM children inside a <div slot="name">, filling the component's named slot.

The wrapper element is real and participates in layout. When that matters, use SlotAs to pick the tag, or put the slot attribute on your own markup — generated packages export the slot names as constants for exactly that.

func (*Element) SlotAs

func (e *Element) SlotAs(tag, name string, c templ.Component) *Element

SlotAs is Slot with a caller-chosen wrapper tag.

func (*Element) Style

func (e *Element) Style(property, value string) *Element

Style adds one declaration to the element's style attribute. Both the property and the value are checked; anything that could escape the declaration is an error rather than a silent substitution.

func (*Element) Tag

func (e *Element) Tag() string

Tag returns the element's tag name.

func (*Element) Text

func (e *Element) Text(s string) *Element

Text sets a plain text child, escaped.

func (*Element) Var

func (e *Element) Var(name, value string) *Element

Var sets a CSS custom property on the element, which is how a themed alacris component is configured from outside its shadow root. A leading "--" is optional.

This is the supported way to push a runtime value into a component's styling; rebuilding a stylesheet per state change is the thing to avoid.

func (*Element) Vars

func (e *Element) Vars(vars map[string]string) *Element

Vars sets several custom properties at once, in sorted key order.

type Pending

type Pending struct {
	// Tags to hide. Usually every alacris tag the page renders.
	Tags []string

	// Style is the declaration applied while undefined.
	// Defaults to visibility: hidden.
	Style map[string]string

	// Nonce for the emitted <style> element. Falls back to the context nonce.
	Nonce string
}

Pending renders the stylesheet that hides custom elements until they are defined.

It is worth having. An alacris component's shadow content is built by setup() in the browser, so between first paint and the module loading, the element is present but empty. Without this the page paints, then reflows.

The elements still occupy no space until they are defined; give them a reserved size in your own CSS if layout stability matters.

func (Pending) Render

func (p Pending) Render(ctx context.Context, w io.Writer) error

Render writes the stylesheet.

type Theme added in v0.4.0

type Theme struct {
	// Seed is the single colour a whole scheme is grown from. Ignored for a
	// role that Colors names explicitly.
	Seed string

	// Colors names key palettes directly. Any subset is fine; omitted roles
	// are derived from Seed (or the Material default).
	Colors ThemeColors

	// Typography is a preset name ("google-sans-flex", "google-sans",
	// "roboto", "system") or a CSS font family. Empty keeps Google Sans Flex.
	Typography string

	// Radius multiplies the Material shape scale. nil keeps 1; 0 is square;
	// 2 is extra round. A pointer so 0 is distinct from "unset".
	Radius *float64

	// Motion multiplies durations. nil keeps 1; 0 is instant.
	Motion *float64

	// Density is 0, -1 or -2. nil keeps 0.
	Density *int

	// Scheme pins light or dark, or "auto" (the default) to follow the OS.
	Scheme string

	// LoadFonts is whether applyTheme injects the theme's typeface stylesheet.
	// nil means yes. Set to false when faces are self-hosted or already on
	// the page.
	LoadFonts *bool

	// Overrides are raw token writes, applied last. Keys are token names
	// without the `--ui-` prefix (`color-primary`, `radius-md`).
	Overrides ThemeOverrides
}

Theme is the Go form of @alacris/ui's applyTheme config. A zero Theme with Config.UI still applies Material Design 3 defaults: seed #e8ad18, Google Sans Flex, density 0, and light/dark following the OS.

Re-theming is one stylesheet write. Every component consumes system tokens, so a new seed (or an explicit primary) re-skins the page without touching a component.

type ThemeColors added in v0.4.0

type ThemeColors struct {
	Primary, Secondary, Tertiary  string
	Neutral, NeutralVariant       string
	Error, Success, Warning, Info string
}

ThemeColors is the explicit-palette form of Theme.Seed. Empty fields are derived.

type ThemeOverrides added in v0.4.0

type ThemeOverrides struct {
	Common map[string]string
	Light  map[string]string
	Dark   map[string]string
}

ThemeOverrides are last-write token maps, matching createTheme's `overrides: { common, light, dark }`.

type VarSet

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

VarSet is the Go side of a component's theming contract: the custom properties declared by alacris' vars(prefix, defaults).

Names are derived exactly as style.js derives them, so a Go constant and the component's own stylesheet cannot drift apart:

vars('btn', { bg: '#111' })  ->  --btn-bg

func NewVarSet

func NewVarSet(prefix string, keys ...string) VarSet

NewVarSet declares a contract the way alacris' vars() does: a prefix plus camelCase keys. NewVarSet("btn", "bg", "borderRadius") is the contract for vars('btn', { bg: ..., borderRadius: ... }), giving --btn-bg and --btn-border-radius.

Both the short key and the full property name work as keys in Apply.

func Vars

func Vars(names ...string) VarSet

Vars declares a contract from full custom property names, which is what generated packages use: a component's @cssprop tags name the properties outright rather than a prefix and keys.

func (VarSet) Apply

func (v VarSet) Apply(e *Element, values map[string]string) *Element

Apply sets the given values on an element. A key outside the contract is recorded as an error on the element rather than written out, so a rename in the component surfaces here instead of silently theming nothing.

func (VarSet) Name

func (v VarSet) Name(key string) string

Name returns the full custom property name for a key, or "" when the key is not part of the contract.

func (VarSet) Names

func (v VarSet) Names() []string

Names lists every declared property, which is the documented surface a consumer can override.

Directories

Path Synopsis
app module
cmd
alacris-go command
Command alacris-go generates typed Go wrappers for alacris web components and hosts a desktop app around the same live handler.
Command alacris-go generates typed Go wrappers for alacris web components and hosts a desktop app around the same live handler.
examples
todo command
Command todo is the alacris-go example — a live board.
Command todo is the alacris-go example — a live board.
todo/model
Package model is the example's server-side state.
Package model is the example's server-side state.
todo/ui
Package ui renders the project's alacris components from Go.
Package ui renders the project's alacris components from Go.
Package gen turns alacris component definitions into typed Go wrappers.
Package gen turns alacris component definitions into typed Go wrappers.
internal
appmeta
Package appmeta is the desktop app metadata and bundler used by `alacris-go app`.
Package appmeta is the desktop app metadata and bundler used by `alacris-go app`.
docsgen command
Command docsgen renders the documentation site's Go examples.
Command docsgen renders the documentation site's Go examples.
genui command
Command genui generates the ui package from the vendored @alacris/ui sources.
Command genui generates the ui package from the vendored @alacris/ui sources.
vendorjs command
Command vendorjs refreshes the vendored alacris runtime and @alacris/ui sources in assets/ from the npm registry.
Command vendorjs refreshes the vendored alacris runtime and @alacris/ui sources in assets/ from the npm registry.
Package live makes the server authoritative over component state.
Package live makes the server authoritative over component state.
livetest
Package livetest makes live action handlers unit-testable.
Package livetest makes live action handlers unit-testable.
Package ui is the typed Go surface of Alacris UI — sixty-eight Material Design 3 components, a three-tier token system, and a theme engine.
Package ui is the typed Go surface of Alacris UI — sixty-eight Material Design 3 components, a three-tier token system, and a theme engine.

Jump to

Keyboard shortcuts

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