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:
+89
-47
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user