# AGENTS.md: building Go applications with alacris-go

You are working in a Go project that uses **alacris-go** to render
[alacris](https://github.com/bmartel/alacris) web components from Go and
[templ](https://templ.guide). Docs: https://bmartel.github.io/alacris-go/

Follow this file exactly unless the project's own conventions visibly differ.

## How it works

1. **A component's shadow content is rendered in the browser, never on the
   server.** `setup()` runs on `connectedCallback`. What Go renders is the
   element, its attributes, and its light-DOM slot children. There is no SSR of
   component internals and no declarative shadow DOM. Do not try to add one.
2. **Every prop crosses as an attribute, objects and arrays included.** alacris
   coerces using the type of the prop's default in `define()`, and object
   defaults are parsed with `JSON.parse`. So no post-load property assignment is
   ever needed, and the page is complete before JavaScript runs.
3. **A prop is a signal.** A server-side change is one property write, one
   binding, one DOM node. This is why the `live` layer has no HTML on the wire
   and nothing to diff.
4. **JavaScript owns component internals; Go owns composition and state.** Do
   not invent a Go DSL for templates or signals. Write `setup()` in JavaScript.

## Project layout

```
web/components.js          every define() call; one module, imported once
internal/components/       GENERATED app wrappers; never edit
  components_gen.go
  alacris_gen.go           Tags, package docs
views/*.templ              pages that use ui.* (design system) and app wrappers
main.go                    routes, live wiring, //go:generate lines
```

- **One `define()` per component**, all reachable from one module the page loads.
- **Generated packages are generated.** Editing `*_gen.go` is always wrong;
  change the component or the manifest and regenerate.
- **Do not generate app wrappers into a package named `ui`.** That import path
  is the Alacris UI design system (`github.com/bmartel/alacris-go/ui`). Put
  yours in `./internal/components` (or `./webui`). If the project already
  generates to `./ui`, alias the design system: `m3 "github.com/bmartel/alacris-go/ui"`.
- Generated files carry `// Code generated by alacris-go … DO NOT EDIT.`

## Setup

```go
mux.Handle("/_alacris/", alacris.RuntimeHandler())   // no StripPrefix needed
```

```go
import "github.com/bmartel/alacris-go/ui"
```

```templ
<head>
    @ui.Pending()
    @alacris.Pending{Tags: components.Tags}
    @alacris.Scripts(alacris.Config{
        UI:      true,                   // Material Design 3 components + theme
        Modules: []string{"/web/components.js"},
        Version: buildRevision,          // makes assets cacheable for a year
    })
</head>
```

`Config.UI` loads every Alacris UI component and applies Material defaults
(seed `#e8ad18`, Google Sans Flex, scheme from the OS). `Config.Theme` re-skins
the page. Omit `Modules` if the page only uses the design system.

`Scripts` MUST be in `<head>` before any other module script. An import map has
to precede the first import it applies to.

The alacris runtime and Alacris UI are vendored in the Go module. **Do not add
npm, a bundler, or a CDN link for them.**

Under a CSP, set `Config.Nonce`, or put the nonce on the context with
`templ.WithNonce` and leave the field empty, which is the usual way. Every tag
`Scripts` emits then carries it. Do not reach for `unsafe-inline`.

## Alacris UI

Sixty-eight Material Design 3 components ship with the module. Prefer them to
hand-rolled buttons, fields, dialogs, and lists. Compose what is missing
(dismissible chips next to autocomplete, a search field as filter chrome)
rather than wrapping a tag the catalog already has.

```templ
@ui.Button(ui.ButtonProps{Variant: "tonal"}) { Save }
@ui.TextField(ui.TextFieldProps{Label: "Name", Value: name})
```

Re-theme from Go:

```go
alacris.Config{
    UI: true,
    Theme: alacris.Theme{Seed: "#0b57d0", Scheme: "dark"},
}
```

A zero `Theme` with `UI: true` is the Material default. Do not edit files under
the module's `ui/` package; they are generated from the vendored sources.

## Writing a component

```js
import { define, html, css, computed } from 'alacris';

/**
 * 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: [] },
  styles: css`:host { display: block }`,
  setup({ name, age, tags }, host) {
    const grown = computed(() => age() >= 18);
    return html`
      <h3><slot name="title">${name}</slot></h3>
      <p>${() => (grown() ? 'adult' : 'minor')}</p>
      <button @click=${() => host.emit('greet', { name: name() })}>hi</button>`;
  },
});
```

**Always write the JSDoc block.** `define()` cannot express element types for
empty arrays, events, slots, or custom properties, and without the tags the
generator produces `[]any` and no event constants.

In the template: **a function is a live binding, a plain value is written once.**
`${count}` updates forever; `${count()}` is a snapshot. This is the single most
important rule in alacris.

## Generating

```go
//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
```

Wrappers first; the templates use them. In CI use `check`:

```bash
alacris-go check ./web -o ./internal/components -strip ala-
```

After changing any component, **run `go generate ./...` before claiming the task
is done.**

## Rendering

```templ
@components.UserCard(components.UserCardProps{Name: "Ada", Age: 36, Tags: []string{"math"}}).
    ID("ada") {
    <h3 slot={ components.UserCardSlotTitle }>Ada Lovelace</h3>
}
```

- Prefer generated wrappers to `alacris.E`. Reach for `E` only for a component
  that has no wrapper. Prefer Alacris UI (`ui.Button`, `ui.Dialog`, …) to
  writing a `define()` for something the design system already has.
- Use the generated `XSlotY` and `XEventY` constants; do not write the
  strings.
- `ID()` is required for anything the live layer will patch.
- `Attr` follows HTML rules (a false bool disappears). `Prop` follows alacris'
  (a bool is always written out). They are different on purpose.

## Prop encoding

| Go | Attribute | Required default in `define()` |
| --- | --- | --- |
| `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 |
| `nil` | *omitted* | anything |

Rules that are not negotiable:

- **Never rely on attribute presence for a boolean.** Removing an attribute runs
  `coerce(null, default)`, which returns `false` even when the default is
  `true`. The library always writes `"true"`/`"false"`; do not work around it.
- **A boolean prop whose default is `true` generates `*bool`.** That is correct;
  do not "simplify" it to `bool`.
- **Integers past 2^53-1 are an error.** Send large ids as strings.
- **The Go type must match the type of the prop's default.** A mismatch does not
  error. `JSON.parse` fails silently on the client and the default is kept.

## The live layer

Only when the server owns what the page shows. Do not add it for state
the browser can own.

```go
srv := live.New()
defer srv.Close()
live.Mount(mux, alacris.DefaultBase, srv)

// per page render
// NewSession needs w and r: it sets the cookie that authorises this browser.
// Call it before writing anything to w.
sess := srv.NewSession(w, r)
sess.OnOpen(func(s *live.Session) { pushEverything(s) })   // do not skip this
cfg := alacris.Config{UI: true, Live: true, Page: sess.ID(), ...}
w.Header().Set("Cache-Control", "no-store, private")       // the response carries a Set-Cookie
```

Down:

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

Up:

```templ
@components.Board(props).ID("board").On(components.BoardEventAdd, actionAdd)
```

```go
live.On(srv, actionAdd, func(c *live.Ctx, d components.BoardAddDetail) error {
    if _, err := list.Add(d.Text, d.Column); err != nil {
        return nil
    }
    c.Session.Element("board").Set("items", list.Items())
    return nil
})
```

- `Handle.Set` takes the **JavaScript** prop name (`maxCount`), because it writes
  the DOM property. Server-rendered props use the same name; only the wire
  differs.
- **The capability is an HttpOnly cookie, not `sess.ID()`.** The page id is not
  a secret and needs no protecting. Do not put a session id in a URL, a log or
  a template variable that ends up in one. There is no longer one to put.
- **Always register `OnOpen`** and push the full state there. A reconnecting
  `EventSource` missed everything sent while it was away.
- **Use `Session.Context()` for `SetHTML`, never the request's.** The request
  that rendered the page has finished by the time `OnOpen` runs, so its context
  is cancelled. Same for a push to another session from an action handler.
- **Validate the detail.** Strict binding checks the shape, not the values.
- Action names are strings on both sides. Put them in constants.
- A handler error is logged and answered 500; it never reaches the browser. If a
  user should see something, send a patch.

### Before this goes to production

- **A session per page render is a session per unauthenticated GET.** Anything
  that follows links (a crawler, a scanner, a load test) leaves one behind,
  each holding a buffer until its TTL. `Options.MaxSessions` (default 10,000)
  is a backstop, not a substitute for **rate limiting the handler that calls
  `NewSession`**. Create the session only where a page will actually use it.
- **Do not set `WriteTimeout` on the `http.Server`.** It cuts every live stream
  on a timer. Bound the read side instead: `ReadHeaderTimeout` is safe.
- **The stream is stateful.** It needs session affinity behind a load balancer,
  and a proxy that does not buffer. The handler sets `X-Accel-Buffering: no`
  for nginx; other proxies need their own equivalent.
- **Cross-origin is off unless you turn it on.** Only if the page and the live
  endpoint are on different origins: set `Options.AllowOrigin` to a
  function naming the origins you trust (never one that returns `true`) and
  `CookieSameSite` to `http.SameSiteNoneMode`, which browsers honour only with
  `Secure`. Same-origin deployments need none of this and should not have it.
- Behind a TLS-terminating proxy that does not set `X-Forwarded-Proto`, set
  `CookieSecure: live.SecureAlways`. `SecureNever` is for local development
  only.

## Desktop apps

Same live handler, in an OS webview. Nested module
`github.com/bmartel/alacris-go/app`, tagged `app/vX.Y.Z` in lockstep with the
root module. `go get github.com/bmartel/alacris-go/app@vX.Y.Z`, not `v0.0.0`.
Rebuild with `-tags desktop`.

```go
live.New(live.Options{CookieSecure: live.SecureNever})
app.Run(app.Options{
    Title: "Board", Width: 1100, Height: 800,
    Handler: mux, Menu: app.DefaultMenu(),
    Identifier: "com.example.board",
})
```

`Run` issues a host token so another local process cannot create a session
on the loopback port. The live cookie is still required. EventSource needs
HTTP, so there is no custom scheme.

Native APIs are Go, typically from `live.On`:

```go
live.On(srv, "export", func(c *live.Ctx, d struct{}) error {
    path, err := app.SaveFile(c.Context(), app.FileFilter{Name: "JSON", Ext: ".json"})
    if err != nil {
        return err
    }
    return os.WriteFile(path, payload, 0o644)
})
```

A window whose page runs to the top edge wants `Titlebar`:

```go
app.Run(app.Options{Titlebar: app.TitlebarInset, /* ... */})
```

`TitlebarInset` keeps the OS window buttons and lets the page draw behind
them — on macOS the traffic lights float over your header, and dragging and
the buttons stay native. Leave room for them: about 78px at the top left.
Windows and Linux have no transparent caption to draw behind, so they fall
back to `TitlebarHidden`, where the page draws its own bar and buttons and
`Window.BeginDrag` moves the window from a pointerdown in whatever region
should act like a title bar. `Undecorated` is the older spelling of
`TitlebarHidden`.

Also on `app`: `Notify`, `OpenURL`, `WriteClipboard`, `DataDir`,
`RegisterShortcut`, `Options.SingleInstance`, `Options.DeepLinkScheme`,
`Options.Tray`, `Updater.ApplyAndRelaunch`.

Do not add Wails bindings, a JS `invoke`, or a second interop. The live
protocol is the bridge. `alacris.app.json` is bundle metadata only; window
size stays in `app.Options`. See
https://bmartel.github.io/alacris-go/guides/desktop/

## Common mistakes (wrong → right)

| Wrong | Right | Why |
| --- | --- | --- |
| `each` inside a conditional template | `<ul ?hidden=...>` with `each` outside | The conditional rebuilds every row on every change |
| One `each` for a whole board | One `each` per lane, each outside a conditional | A card that changes lane is a new node; identity still holds inside a lane |
| `${todo().text}` in an `each` row | `${() => todo().text}` | The row signal must be read in a thunk |
| Editing `*_gen.go` | Change the component, regenerate | It is overwritten |
| Generating app wrappers into `./ui` | `./internal/components` | `github.com/bmartel/alacris-go/ui` is the design system |
| `alacris.E("user-card")` when a wrapper exists | `components.UserCard(...)` | No defaults, no types, no checked names |
| Hand-writing `max-count` | `Prop("maxCount", ...)` | The library kebab-cases it the way `define.js` does |
| `float64` for an id | `@prop {integer}` or a string | JavaScript numbers round past 2^53 |
| Treating `sess.ID()` as a secret | The cookie is the secret | The page id is an identifier; the capability is `HttpOnly` |
| Skipping `OnOpen` | Push full state there | A reconnect silently shows stale data |
| `SetHTML(r.Context(), ...)` in `OnOpen` | `SetHTML(s.Context(), ...)` | The request finished; its context is cancelled |
| `WriteTimeout` on the http.Server | Leave it unset | It cuts every live stream on a timer |
| `NewSession` on every GET | Only where a live page needs one | A crawler otherwise leaves a session per request |
| `AllowOrigin` returning `true` | Name the origins you trust | A credentialed endpoint that reflects any origin is an open door |
| `unsafe-inline` to make Scripts work | `Config.Nonce` or `templ.WithNonce` | Every emitted tag carries the nonce already |
| Adding npm for the runtime or Alacris UI | They are vendored | `RuntimeHandler` already serves them |
| A Wails/Fyne/JS `invoke` bridge | `live.On` plus `app.SaveFile` | The live protocol is the interop |
| `!important` in a component | Custom properties, `::part` | It is the one thing a consumer cannot override |

## Verifying your work

Before declaring a task done:

1. `go generate ./...` then `go build ./...`. Generated code is current.
2. `go test ./...`, and `go test -race ./...` if you touched the live layer.
3. `alacris-go check` passes.
4. Load a page: no console errors, elements upgrade, no flash of empty elements.
5. If you changed a list or the live layer: run the browser tests
   (`cd e2e && npx playwright test`). They assert that an update keeps focus and
   typed text and that the row nodes are the same nodes afterwards. If node
   identity fails, an `each` is inside a conditional.
6. Nothing untrusted flows into `SetHTML`, and no action handler trusts its
   detail without checking it.

## Reference

- Docs: https://bmartel.github.io/alacris-go/
- Go API: https://pkg.go.dev/github.com/bmartel/alacris-go
- Wire protocol: https://bmartel.github.io/alacris-go/reference/wire-protocol/
- The alacris runtime itself: https://bmartel.github.io/alacris/ (and its own
  `AGENTS.md` for writing component internals)
- Alacris UI: https://bmartel.github.io/alacris-go/guides/alacris-ui/
- Desktop: https://bmartel.github.io/alacris-go/guides/desktop/
