feat(api): streamline generated app authoring

Move the canonical examples toward generated component-root helpers, typed form decoding, async/state handler registration, and derive-driven app/component registry wiring. Tighten requirements and diagnostics for the server-first, selectorless authoring path.

Verified with cargo run -p slhx-xtask -- test, cargo check --workspace, redgate list, redgate refs, redgate health --strict, and git diff --check.

req: canonical/001

req: canonical/003

req: canonical/004

req: dx/002

req: derive_app/001

req: component/003

req: form/004

req: axum_integration/003
This commit is contained in:
slhx agent
2026-06-05 06:33:41 +02:00
parent eb6086616c
commit d4e865ef92
34 changed files with 4573 additions and 1156 deletions
+89 -47
View File
@@ -1,9 +1,11 @@
# slhx — Semantic, Laterally HX
slhx does not compete with React by becoming a better frontend framework.
slhx competes with React by making frontend frameworks unnecessary for most apps.
slhx makes server-rendered HTML feel like it grew just enough interactivity.
App authors change state in Rust, render hemplate partials, and return generated
UI intent; the runtime swaps generated slots without selectors, a VDOM, or a
client app state framework.
> **hemplate owns syntax. hemplate emits surface. slhx consumes surface. slhx owns semantics. JS executes bytecode.**
> **hemplate owns syntax. hemplate emits surface. slhx consumes surface. slhx owns semantics. JS applies effects.**
---
@@ -29,7 +31,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## pitch
### req: pitch/001
001 slhx is checked hypermedia for Rust. Write `.heml`, write `#[slhx::handler]`, return commands. The compiler checks every cross-file reference. The browser runtime only sees ids and effect bytes. No JS app code required. [north_star]
001 slhx is checked hypermedia for Rust. Write `.heml`, write Rust handlers, return generated UI commands. The compiler checks every cross-file reference. The browser runtime only sees lowered ids and effect bytes. No JS app code is required for ordinary server-first apps. [north_star]
### req: pitch/002
002 No CSS selectors. No hx-* strings. No virtual DOM. No client framework. No hidden global proxy magic. No component hydration. SSR state bootstrap via `data-slhx-st` is allowed, but the browser never reconstructs a component tree. [north_star]
@@ -38,33 +40,61 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
003 slhx replaces React/Vue not with a UI framework, but with a compiler contract: hemplate knows the surface, Rust knows the types, slhx knows the effects, the browser only executes commands. [north_star]
### req: pitch/004
004 The north-star feel: **Svelte at the call site, Rust at the boundary.** Short handler bodies, compile-checked HTML contracts, one language for server and client logic. [north_star]
004 The north-star feel: **boring server-rendered HTML with typed, selectorless partial swaps.** Short handler bodies, compile-checked HTML contracts, plain Rust state changes, hemplate-rendered partials, and a tiny runtime that applies effects. [north_star]
---
## canonical_authoring
### req: canonical/001
001 The canonical app shape is templates plus Rust, not a frontend folder: `.heml` files declare `data-slhx-root`, `data-slhx-slot`, `data-slhx-handle`, `data-slhx-form`, `h-key`, optional pending/page/island facts, and handlers return generated UI commands. Ordinary app code avoids selectors, numeric ids, raw effects, wire formats, manual registries/form parsing, raw `SafeHtml`, and raw render calls. Plain CSS owns appearance. [north_star]
### req: canonical/002
002 Typed partial swaps are the primary UX, not an advanced feature: handlers change domain state in Rust, convert domain values into view values, render hemplate partials through generated helpers, and place them into generated targets. The real primitive is generated target + rendered partial + swap kind. [north_star]
### req: canonical/003
003 Canonical keyed-row CRUD reads like ordinary Rust intent: create appends a rendered row partial, update/toggle replaces a keyed row partial, delete removes a keyed row, summary/text/form effects compose in tuples or arrays implementing `IntoEffect`, and no handler chooses a target with a CSS selector. [north_star]
### req: canonical/004
004 Generated helpers may compose only facts uniquely known from templates and checked Rust types: template, slot, optional key, form/control, class token, explicit island/event marker, and effect kind. If a handler parameter, key, form, target, raw route, or legacy target would require guessing, the user must say it explicitly and diagnostics must point to the Rust and hemplate spans. [north_star]
### req: canonical/005
005 Good generated helpers name UI intent without mixing domain work: `ui::content.replace(page)`, `ui::todos.append(todo)`, `ui::todo_row.replace(todo)`, `ui::todo_row.remove(todo_id)`, `ui::summary.set(text)`, `ui::notice.set("Saved")`, `ui::new_todo.clear()`, `ui::new_todo.focus("title")`, `ui::modal.replace(view)`, `ui::errors.set(errors)`, `ui::chart_path.set_attr("d", path)`, and `ui::game.emit(event)`. Helpers such as `refresh`, `save_and_update`, `sync_component`, and generic `rerender` are forbidden because they mix persistence, routing, rendering, target selection, or domain policy. [north_star]
### req: canonical/006
006 There is no separate beginner API and expert API. The simple generated shape is canonical and should scale: generated slots, partials, forms, class constants, islands/events, and page helpers are normal. Explicit primitives, raw targets, raw HTML, raw effects, manual registries/form parsing, low-level ids/opcodes, wire formats, and raw routes remain named escape hatches or internals around the same render/target/effect/transport model. [north_star]
### req: canonical/007
007 Opaque islands are explicit leaf adapters: templates declare `data-slhx-island` and optional generated handles/events; server code may emit snapshots/events such as `ui::game.emit(event)`, while island JS owns only high-frequency local behavior. Islands do not introduce a component runtime, client state graph, VDOM, or second UI model. [north_star]
### req: canonical/008
008 Offline/PWA support is opt-in adapter territory. Server-first slhx may fail interactions while offline; cached shells and local-sync queues live in crates such as `slhx-pwa` or `slhx-sync`, reuse generated slots/effects, queue explicit patches, and reconcile with server-canonical effects. Core slhx must not gain a mandatory client state graph, scheduler, CRDT, or local app runtime. [north_star]
---
## modes
### req: mode/001
001 slhx has two happy paths: Page Enhancer and Interaction Handler.
001 slhx has one core authoring loop: render a partial and place it into a generated target with a swap kind. HTTP handlers, page navigation, push streams, and island events are transport/adapters around that loop.
### req: mode/002
002 Page Enhancer mode replaces minimal HTMX page swapping. Authors use real anchors with `data-slhx-nav` or `data-slhx-boost`; no user-authored handler is required.
002 Page Enhancer mode is a specialized partial swap for navigation: it updates generated page/content/title/nav targets and adds history, scroll, shell, and fallback behavior. Authors use real anchors with `data-slhx-nav` or `data-slhx-boost`; no user-authored handler is required for ordinary navigation.
### req: mode/003
003 Interaction Handler mode handles forms, buttons, typed params, and targeted updates through `#[slhx::handler]`.
003 Interaction Handler mode handles forms, buttons, typed params, and generated partial/text/form/island effects through Rust handlers.
### req: mode/004
004 Beginner docs must teach Page Enhancer first, Interaction Handler second, Atoms third, client-local/WASM fourth, sync last.
004 Beginner docs must teach server-first typed partial swaps first, Page Enhancer as navigation around the same slot/effect model, explicit islands for leaf widgets, client-local/WASM only for local high-frequency behavior, and sync/offline last as opt-in adapters.
---
## dx
### req: dx/001
001 The common case must feel like writing a Svelte/Vue component: template, state, handlers, and targeted updates. Users should not need to understand Surface IR, ResourceId, EffectWriter, postcard, or runtime opcodes for basic apps. [north_star]
001 The common case must feel like writing server-rendered HTML plus tiny Rust handlers: template, state, hemplate partials, and generated UI swaps. Users should not need to understand Surface IR, ResourceId, EffectWriter, postcard, runtime opcodes, selector targeting, or manual registries for basic apps. [north_star]
### req: dx/002
002 The happy path is: write `.heml`, write `#[slhx::handler]`, return generated slot/atom commands. No manual ids, no manual registry, no manual serialization, no manual JavaScript. [north_star]
002 The happy path is: write `.heml`, write a Rust handler, return generated partial/text/form/page/island commands. No manual ids, no manual registry, no manual serialization, no CSS selector targets, no raw render calls, and no manual JavaScript for ordinary app UI. [north_star]
### req: dx/003
003 Public APIs are generated around the user's names. If the template declares `data-slhx-slot="todo_list"`, the user gets `slots::todo_list`, not `SlotId(12)`.
@@ -76,7 +106,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
005 Error messages must explain fixes in author language, not internal language. Say “add `h-key="todo.id"` to this `h-for`”, not “missing ScopeKey for ResourceRef”.
### req: dx/006
006 Generated target objects are the preferred authoring API: `targets::todo_list.put(&view)`, `targets::card.append(key, &view)`, `targets::count.text(42)`, `atoms::user.set(user)`. The public facade exposes `render(view)` for trusted hemplate-to-`SafeHtml` page and fragment composition; generated view modules expose lower-aware target methods plus compatibility functions `render(view)`, `put(slot, view)`, `append/prepend/replace(keyed_slot, key, view)`, `static_fragment(include_str!(...))` for prototype/static `.heml` fragments that need generated resource lowering as `SafeHtml`, and `lower(html)` for callers that need the lowered string. `render_html(view)` and `lower_html(html)` remain doc-hidden compatibility aliases and are not beginner-prelude exports. The beginner prelude should not expose lower-level slot render shortcuts that bypass generated lowering. These return `impl IntoEffect`, `SafeHtml`, or lowered HTML at the boundary. Raw `Effect` constructors, opcodes, and `EffectWriter` remain low-level. [north_star]
006 Generated object-like helpers are the preferred authoring API and are re-exported at the component root: `todos.append(todo)`, `todo_row.replace(todo)`, `todo_row.remove(todo_id)`, `summary.set(text)`, `new_todo.clear()`, `new_todo.focus("title")`, `page.replace(view)`, and `game.emit(event)`. These helpers hide hemplate rendering and resource lowering in the common path. Namespaced `targets`, `handles`, `forms`, and raw `advanced::slots` modules remain compatibility/organization and escape-hatch surfaces, not the ordinary call-site shape. The public facade may expose explicit `render(view)`, `target(name)`, `html(value)`, `lower(html)`, or raw effect constructors only as named escape hatches; they must not appear in beginner-prelude exports, canonical handler examples, or ordinary docs. [north_star]
### req: dx/007
007 Tuple composition of `IntoEffect` is the canonical batch syntax: `(a, b, c)` implements `IntoEffect` up to arity 12. `Effect::batch((...))` is available but not required for the happy path.
@@ -105,7 +135,7 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
## progressive_disclosure
### req: pd/001
001 A beginner can build CRUD with only: `.heml`, `#[slhx::handler]`, `Form<T>`, generated `slots::*` methods, and `impl IntoEffect`.
001 A beginner can build CRUD with only: `.heml`, Rust handlers, `Form<T>` or typed params, generated object-like UI helpers, and `impl IntoEffect`.
### req: pd/002
002 Atoms are not required for basic server-first apps. They appear only when client-local state, SSR bootstrapped state, or WASM handlers are used.
@@ -114,23 +144,23 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
003 Sync, transitions, resources/queries, islands, capabilities, and raw EffectWriter are advanced layers. They must not appear in starter examples.
### req: pd/004
004 Documentation must present three levels: server-first, client-local, hybrid-sync. Each level introduces only the new primitive it needs.
004 Documentation must present levels as adapters around the same core: server-first partial swaps; cached Page/PWA shell; explicit leaf islands or client-local handlers for high-frequency behavior; hybrid sync/offline queues last. Each level introduces only the new primitive it needs.
---
## page_swap
### req: page_swap/001
001 Minimal page swapping is a first-class slhx-axum happy path. Authors mark real anchors with `data-slhx-nav`; links keep valid `href` and work without JS. Missing or empty static `href` on a `data-slhx-nav` anchor is a build error.
001 Page swapping is a first-class specialization of partial swapping: render a page partial, place it into generated page targets, then apply history/title/scroll/shell behavior. Authors mark real anchors with `data-slhx-nav`; links keep valid `href` and work without JS. Missing or empty static `href` on a `data-slhx-nav` anchor is a build error.
### req: page_swap/002
002 A `data-slhx-nav` click fetches the target URL as a slhx partial request. The response updates the canonical content slot, optionally navigation and title, then applies a `Navigate` effect.
002 A `data-slhx-nav` click fetches the target URL as a slhx partial request. The response is conceptually `ui::content.replace(page)` plus optional generated nav/title targets and a `Navigate` effect; it must not introduce selector targeting or a second page-specific UI model.
### req: page_swap/003
003 Page swapping uses generated slots, not CSS selectors. The default content target is the slot named `content`, not `#content`.
003 Page swapping uses generated targets, not CSS selectors. The default content target is the generated slot named `content`, not `#content`; explicit page helpers such as `ui::content.page(req, view)` or `request.page_html(ui::content.render(view), shell)` are adapters around the same partial-swap primitive.
### req: page_swap/004
004 Minimal page swap must not require user-authored `#[slhx::handler]`. Explicit navigation handlers are available only when custom application logic is needed.
004 Ordinary page navigation must not require user-authored handlers. Explicit navigation handlers are available only when custom application logic is needed, and they still return generated target/page commands.
### req: page_swap/005
005 Browser back/forward is supported. On `popstate`, slhx fetches the URL as a partial request and applies the same page-swap update without pushing a new history entry.
@@ -149,10 +179,10 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
001 slhx replaces common HTMX use-cases through typed equivalents, not HTMX syntax.
### req: htmx/002
002 Easy equivalents must exist for: boosted links/forms, page swap, form submit, targeted replacement, append/prepend/remove, loading indicators, confirmation, debounce/throttle, polling, history navigation, multi-target updates, response events, SSE/push, and validation errors.
002 Easy equivalents must exist for: generated target replacement, append/prepend/remove, boosted links/forms, page swap, form submit, loading indicators, confirmation, debounce/throttle, polling, history navigation, multi-target updates, response events, SSE/push, validation errors, modals, toasts, table rows, SVG fragments, and form error regions.
### req: htmx/003
003 slhx core deliberately does not clone selector-based HTMX features: `hx-target` selectors, `hx-select`, `hx-include` selectors, `closest/find/this` target strings, or trigger mini-languages. Equivalent patterns use generated slots, typed params, forms, and explicit handlers.
003 slhx core deliberately does not clone selector-based HTMX features: `hx-target` selectors, `hx-select`, `hx-include` selectors, `closest/find/this` target strings, or trigger mini-languages. Equivalent patterns use generated targets, typed params, forms, explicit handlers, and page/push adapters around partial swaps.
---
@@ -166,8 +196,8 @@ slhx competes with React by making frontend frameworks unnecessary for most apps
### req: component/003
003 Generated APIs are component-namespaced by default:
`ui::todo_list::slots::todo_row`, `ui::todo_list::handles::create`, `ui::todo_list::forms::create`, and `ui::todo_list::COMPONENT` as a checked `ComponentRef`.
Global exports (`ui::slots::*`, `ui::handles::*`, `ui::components::*`) are opt-in only.
`ui::todo_list::todo_row`, `ui::todo_list::create`, `ui::todo_list::new_todo`, and `ui::todo_list::COMPONENT` as a checked `ComponentRef`.
Category modules such as `targets`, `handles`, and `forms` remain available for organization/compatibility, while raw slot constants live under `advanced::slots`; global exports (`ui::slots::*`, `ui::handles::*`, `ui::components::*`) are opt-in only.
### req: component/004
004 `#[slhx::surface]` bridges generated code into a user module. Users write `#[slhx::surface] mod ui {}` instead of `include!(concat!(env!("OUT_DIR"), ...))`. slhx-build emits `slhx.generated.rs` which the macro expands in place. No direct `$OUT_DIR` includes in user-authored source.
@@ -226,7 +256,7 @@ a `data-*` handle param is statically known or runtime-extracted.
001 `slhx_build` generates three artifacts from the generic Surface IR: (a) `slhx.generated.rs` containing ergonomic resource modules (`slots`, `targets`, `handles`, `forms`, `atoms`), (b) `slhx.syms` for proc-macro validation, (c) runtime id-lowering tables. `slhx_build` interprets tool-specific conventions (`data-slhx-*`, `h-for`, `h-key`, form controls) from the Surface. [north_star]
### req: codegen/002
002 Generated view modules expose ergonomic target objects and resource commands: `targets::list.put(value)`, `targets::row.append(key, value)`, `targets::row.replace(key, value)`, plus compatibility commands `put(slot, value)`, `append(keyed_slot, key, value)`, `prepend(keyed_slot, key, value)`, and `replace(keyed_slot, key, value)`. String-keyed target objects accept displayable domain ids without caller-side `.to_string()` noise. Commands return `impl IntoEffect` and preserve generated lowering.
002 Generated view modules expose ergonomic root-level target objects and commands that hide render/lower details: text slots provide `set(text)`, singleton partial slots provide `replace(view)`/`clear()`, keyed collection slots provide `append(view)`, `prepend(view)`, `replace(view)`, `remove(key_or_view)`, and forms provide `clear()`/`clear(field)`/`focus(field)`. String-keyed target objects accept displayable domain ids without caller-side `.to_string()` noise. Commands return `impl IntoEffect`, compose in plain Rust, preserve generated lowering, and fail to generate when the template lacks enough facts to infer the slot, key, form, or renderable view type.
### req: codegen/003
003 Generated module `handles` exports typed constants: `Handle<I>` where `I` is `Form<T>`, a param type, or `()`. Users rarely reference handles directly; they are consumed by `#[slhx::handler]` for validation.
@@ -245,7 +275,7 @@ a `data-*` handle param is statically known or runtime-extracted.
## public_api
### req: public_api/001
001 The generated API is the primary public authoring API. Most user code should return generated slot/atom/form/nav commands, not raw `Effect` constructors.
001 The generated API is the primary public authoring API. Most user code should return generated partial, text, keyed-row, form, page, nav, or island/event commands, not raw `Effect` constructors or raw render/lower calls.
### req: public_api/002
002 `Effect`, `EffectWriter`, `ResourceId`, `ResourceRef`, and raw opcodes are advanced APIs. They must not appear in beginner docs, generated examples, or common diagnostics.
@@ -256,6 +286,9 @@ a `data-*` handle param is statically known or runtime-extracted.
### req: public_api/004
004 If a common UI operation requires raw `EffectWriter`, the public API is considered incomplete.
### req: public_api/005
005 Beginner-facing page/template composition uses generated render or page helpers. Direct `SafeHtml` construction, raw `html(...)`, raw `target(...)`, raw route fragments, and explicit `ui::render(...)` calls are advanced escape hatches and must not appear in beginner examples or docs.
---
## effect_algebra
@@ -315,7 +348,7 @@ resources. Concrete runtime targets are addressed through `ResourceRef`
002 Slots inside a keyed loop receive a composite identity. hemplate records `key_expr` in the Surface; slhx implements keyed slot lookups.
### req: list/003
003 Effects on keyed slots: `replace_keyed(slot, key, value)`, `remove_keyed(slot, key)`, `append_keyed(slot, key, value)`, `prepend_keyed(slot, key, value)`. Mismatch between key type and slot key type is compile-time error.
003 Generated helpers for keyed slots are `append(view)`, `prepend(view)`, `replace(view)`, and `remove(key_or_view)` when the template and view type provide an unambiguous `h-key`. Compatibility functions such as `append(keyed_slot, key, view)` may exist as explicit low-level forms. Mismatch between key type and slot key type is a compile-time error, and missing/ambiguous keys are build errors with template spans.
---
@@ -346,7 +379,7 @@ what a valid business email is.
## form_effects
### req: form_effects/001
001 Generated form APIs provide common commands: `reset()`, `clear(field)`, `error(field, message)`, `focus(field)`, and `disable_while_pending()`.
001 Generated form APIs provide common commands: `clear()`, `clear(field)`, `reset()`, `error(field, message)`, `focus(field)`, and `disable_while_pending()`. `form.clear()` clears the canonical generated form without requiring callers to name raw control ids.
### req: form_effects/002
002 Form effects target generated form/control ids, not CSS selectors.
@@ -435,13 +468,13 @@ what a valid business email is.
## axum_integration
### req: axum/001
001 slhx-axum supports the common shell/partial pattern. Full-page requests are wrapped in a user-provided Shell; slhx/partial requests may return only the rendered component or an EffectBatch. The shell/partial helper has a `SafeHtml` path so already-rendered hemplate fragments can cross the page boundary without downgrading to unchecked strings.
001 slhx-axum supports the common full-shell/partial pattern as an adapter around generated partial swaps. Full-page requests are wrapped in a user-provided Shell; slhx partial requests may return a rendered partial for a generated target or an EffectBatch. Page helpers add shell/title/history/fallback behavior without changing the render/target/effect model.
### req: axum/002
002 Existing Axum routes remain normal Axum routes. slhx does not own routing. slhx-axum only mounts handler dispatch, runtime assets, and optional push endpoints.
### req: axum/003
003 Interactive fragments that would traditionally be implemented as `/demo/...` HTMX endpoints should be expressible as `#[slhx::handler]` functions returning generated slot commands. Integration registration should read as interactions over generated handles, not low-level registry wiring.
003 Interactive fragments that would traditionally be implemented as `/demo/...` HTMX endpoints should be expressible as Rust handlers returning generated target commands. Integration registration should read as interactions over generated handles and page/partial helpers, not low-level registry wiring.
### req: axum/004
004 Query-string demo endpoints may be migrated to typed handler params from `data-*` attributes or forms. `Query<T>` remains available in normal Axum routes but is not the slhx happy path.
@@ -476,10 +509,10 @@ what a valid business email is.
002 SSE/WebSocket connections are authenticated by the server framework before stream creation. slhx does not define auth semantics for streams.
### req: push/003
003 HTMX-style SSE swaps are represented as streamed effects targeting generated slots/atoms. No selector-based `sse-swap` semantics in core.
003 Push swaps are ordinary partial swaps carried over SSE/WebSocket: streamed effects target generated slots, atoms, or island events such as `ui::feed.prepend(event)` or `ui::game.emit(snapshot)`. No selector-based `sse-swap` semantics in core.
### req: push/004
004 Out-of-band updates are ordinary multi-target EffectBatches.
004 Out-of-band updates are ordinary multi-target partial swaps/effects, not a separate response model.
### req: push/005
005 Push is one-way server-to-client delivery of EffectBatch. It does not define client mutation, optimistic queues, reconciliation, or conflict handling.
@@ -520,19 +553,28 @@ what a valid business email is.
## interop
### req: interop/001
001 `Effect::event` is the single bridge between slhx and third-party JS. External widgets, charts, and maps listen via native `CustomEvent`. slhx core does not inspect or manage them. [north_star]
001 `Effect::event` and generated event helpers are the single slhx-to-widget bridge. External widgets, charts, games, maps, Alpine/Svelte islands, and Web Components listen via native `CustomEvent`; slhx core does not inspect their state, rendering internals, or framework lifecycle. [north_star]
### req: interop/002
002 Web Components and custom elements are valid opaque leaf nodes. slhx does not inspect shadow DOM. Escape hatches are leaves, never app foundations.
002 Web Components and custom elements are valid opaque leaf nodes. slhx does not inspect shadow DOM or mutate inside custom elements unless the author explicitly exposes slhx-owned slots/handles at the boundary. Escape hatches are leaves, never app foundations.
### req: interop/003
003 WASM islands (`#[slhx::island]`) compile handler code to WASM for client-local execution. The island is a leaf in the DOM; slhx core is unaware of WASM except via the same `EffectBatch` contract.
003 WASM islands and third-party framework islands are explicit leaf boundaries. slhx may replace the island root as a generated target, but it does not manage inside it; slhx owns the generated slot/island boundary, Alpine/Svelte/Web Components/hand-written widgets own the inside, and events cross the boundary. Commands flow widget-to-slhx through explicit generated handles or `slhx.send(...)`, and server-to-widget through generated event helpers such as `ui::chart.emit(snapshot)`.
### req: interop/004
004 Existing `hx-*` attributes are treated as ordinary raw attributes in the hemplate Surface without slhx semantics. An optional `slhx-htmx-migrate` tool may read Surface `hx-*` attrs and suggest equivalent `data-slhx-*` handlers/effects.
### req: interop/005
005 HTMX-style response triggers are represented by `Effect::event` or generated event helpers. Events are native `CustomEvent`s scoped to the slhx root.
005 HTMX-style response triggers and widget notifications are represented by `Effect::event` or generated event helpers. Events are native `CustomEvent`s scoped to the slhx root.
### req: interop/006
006 `data-slhx-preserve` is an explicit preserve boundary for rare leaf-widget cases where slhx updates around a subtree without destroying it. Preserve semantics must be simple: preserve the marked subtree identity, do not diff or hydrate inside it, and require authors to mark the boundary deliberately. Preserve must not become a default lifecycle model or a workaround for unclear ownership.
### req: interop/007
007 The runtime emits native lifecycle events such as `slhx:before-swap`, `slhx:after-swap`, `slhx:event`, `slhx:connect`, and `slhx:disconnect` so Alpine, Svelte, Web Components, and hand-written widgets can attach at DOM/event boundaries. slhx core must not add framework-specific adapters.
### req: interop/008
008 Interop must prevent selector hacks, manual JS reinitialization races, lost widget state after swaps, and double-owned state by making ownership explicit: slhx owns generated server DOM targets, the external widget owns explicit leaves, and events are the supported crossing point. Core must not add selector targeting, hydration compatibility, a client store, or a framework lifecycle to make interop easy.
---
@@ -594,7 +636,7 @@ what a valid business email is.
## test
### req: test/001
001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser: `slhx_test::run(handler, input)` returns an `EffectInspector` with `contains(op)`, `has_slot(slot)`, `has_atom(atom)`, etc.
001 `EffectWriter` implements a test backend so handlers can be unit-tested without a browser: `slhx_test::run(handler, input)`, `slhx_test::inspect_batch(dispatched_batch)`, and `slhx_test::inspect_wire(bytes)` return an `EffectInspector` with generated-resource assertions such as `has_target(generated_target)`, `updates_text(generated_target)`, `updates_html(generated_target)`, `target_html_containing(generated_target, text)`, `inserts_html_containing(generated_target, key, text)`, `replaces_keyed_html_containing(generated_target, key, text)`, `removes_key(generated_target, key)`, `pushes_to(url)`, `emits(name, payload)`, `emits_containing(name, text)`, `has_slot(slot)`, `has_atom(atom)`, etc. Canonical example tests should prefer generated-target assertions so tests use the same generated target objects as handlers instead of importing raw slot constants or matching raw effects/payloads. Browser/E2E selector helpers are allowed only as low-level test adapters for driving rendered HTML and must be generated or named around public authoring concepts such as handles, targets, forms, nav links, roots, islands, class tokens, or keys; they must not become app authoring APIs or teach selector targeting.
### req: test/002
002 `#[slhx::component]` may compile without generated Surface files for incremental module-local testing. `#[slhx::surface]` requires `slhx.generated.rs` in `$OUT_DIR` and fails with an actionable diagnostic when generation is missing, because it is the public generated API bridge.
@@ -635,7 +677,7 @@ what a valid business email is.
002 Atoms are not reactive by default. Updating an atom does not re-render anything until a handler returns an effect referencing it.
### req: state/003
003 The JS runtime maintains a client-side atom store keyed by `AtomId`. Runtime values are type-erased postcard bytes. Types are compile-time only. A deterministic `TypeHash` may be generated by `slhx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`.
003 The JS runtime may maintain a narrow atom value table keyed by `AtomId` only for explicit `Atom<T>` resources and SSR bootstrap. This is not an app state framework, component store, cache, or reactive graph. Runtime values are type-erased postcard bytes; types are compile-time only. A deterministic `TypeHash` may be generated by `slhx_build` for diagnostics, but the JS runtime does not depend on Rust `TypeId`.
### req: state/004
004 SSR roots may carry a `data-slhx-st` base64url-encoded postcard blob on the `data-slhx-root` element. Runtime decodes it into the client atom store. Atoms computed from server state are immediately available to client-side handlers without a round-trip.
@@ -823,31 +865,31 @@ Optional ergonomic macros may exist: `#[slhx::surface]`, `#[slhx::component]`,
003 Handler parameters are inferred from four sources: `Form<T>`, `data-*` attributes on the triggering node, route params supplied by integration crates, and explicit app/context parameters. Missing or incompatible params are compile-time errors with source spans. slhx does not implement selector-based `hx-include`; shared params are represented by forms, hidden inputs, scoped context, or explicit `data-*` attributes.
### req: derive_handler/004
004 The common handler signature forms are:
004 The common handler signature forms are plain Rust functions, synchronous or async:
```rust
fn my_handler() -> impl IntoEffect
fn my_handler(form: Form<CreateTodo>) -> impl IntoEffect
fn my_handler(app: &mut AppState) -> impl IntoEffect
fn my_handler(card_id: CardId, app: &mut AppState) -> impl IntoEffect
fn ping() -> impl IntoEffect
async fn add(app: State<App>, form: Form<NewTodo>) -> impl IntoEffect
async fn rename(app: State<App>, todo_id: TodoId, title: Title) -> impl IntoEffect
async fn delete(app: State<App>, todo_id: TodoId) -> impl IntoEffect
```
All forms support returning `impl IntoEffect` and compose through tuples.
`State<App>` is illustrative integration context; equivalent framework extractors or app references are adapter concerns. Form fields and `data-*` params parse through normal Rust `FromForm`/`FormValue`/`FromStr`-style traits, so domain newtypes remain user-authored. Handlers may return `impl IntoEffect` or `Result<impl IntoEffect, E>` for fallible database/domain work; `IntoEffect` values compose through tuples while `Result` paths preserve a typed error boundary for integrations to map to form errors, toasts, events, or HTTP responses.
---
## derive_app
### req: derive_app/001
001 `#[slhx::app]` marks an application/root state type and registry entry point. There may be one registry per app/root type, and multiple runtime instances may exist per process. slhx does not require a process-global singleton.
001 `#[slhx::app(...)]` marks an application/root registry entry point and composes generated component handler modules into one app registry from the app state. There may be one registry per app/root type, and multiple runtime instances may exist per process. slhx does not require a process-global singleton or handwritten chains of per-component registration calls in canonical app code.
---
## locality
### req: locality/001
001 slhx does not support selector targets such as `closest`, `find`, or `this` in core. The equivalent pattern is a named/generated slot on the local component or keyed row.
001 slhx does not support selector targets such as `closest`, `find`, or `this` in core. The equivalent pattern is a named generated target on the local component, keyed row, modal, toast, SVG fragment, form error region, or page content area.
### req: locality/002
002 Diagnostics should suggest adding `data-slhx-slot` to the local element when users attempt a self/row update pattern.
002 Diagnostics should suggest adding `data-slhx-slot` or `h-key` to the local element when users attempt a self/row update pattern, and should explain which generated target helper would become available.
---
@@ -911,7 +953,7 @@ All forms support returning `impl IntoEffect` and compose through tuples.
## target_policy
### req: target/001
001 slhx does not support response-side selector retargeting. Handlers choose targets by returning effects for generated resources.
001 slhx does not support response-side selector retargeting. Handlers choose targets by returning generated UI commands for generated resources.
### req: target/002
002 slhx does not implement response-side CSS fragment selection in core. Servers return explicit component fragments or EffectBatches.
002 slhx does not implement response-side CSS fragment selection in core. Servers return explicit hemplate partials for generated targets or EffectBatches containing generated target effects.