feat(build): generate page-boundary helper

Expose generated ui::page as the canonical page-boundary rendering helper and move the Workout golden path and docs off beginner-visible ui::render calls.

req: public_api/002

req: codegen/002

req: canonical_authoring/006
This commit is contained in:
slhx agent
2026-06-12 07:18:38 +02:00
parent 176259afd6
commit 4a8442c5f9
5 changed files with 18 additions and 8 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ For beginner and production-shaped app code, stay on this path. req: public_api/
## Mental model: render → slot/key → effect → runtime ## Mental model: render → slot/key → effect → runtime
- **Render:** hemplate renders Rust view structs into checked HTML. App code - **Render:** hemplate renders Rust view structs into checked HTML. App code
normally reaches rendering through generated helpers or `hemx::page(...)` at a normally reaches rendering through generated `ui::page(...)` helpers at a
server page boundary, not raw HTML construction. req: html_safety/002 req: view/001 server page boundary, not raw HTML construction. req: html_safety/002 req: view/001
- **Slot/key:** a generated slot names the target, and a generated keyed slot - **Slot/key:** a generated slot names the target, and a generated keyed slot
also carries the stable row key. A list target inside `h-for` must have a also carries the stable row key. A list target inside `h-for` must have a
+3 -3
View File
@@ -99,9 +99,9 @@ persistence or cross-version addressing contract. req: abi/005
For v1, document changes in three buckets: For v1, document changes in three buckets:
- **Beginner API:** generated helpers, `#[hemx::app]`, `#[hemx::component]`, - **Beginner API:** generated helpers, `#[hemx::app]`, `#[hemx::component]`,
`#[hemx::handler]`, `#[hemx::form]`, `hemx::page`, tuple `IntoEffect`, and `#[hemx::handler]`, `#[hemx::form]`, generated page-boundary rendering, tuple
`Result<impl IntoEffect, E>` mapping. Breaking changes require a major version `IntoEffect`, and `Result<impl IntoEffect, E>` mapping. Breaking changes
or an explicit migration note. require a major version or an explicit migration note.
- **Wire/runtime ABI:** EffectBatch schema, runtime ABI version, and fingerprint - **Wire/runtime ABI:** EffectBatch schema, runtime ABI version, and fingerprint
inputs. Incompatible changes must bump ABI versions and fail closed at runtime. inputs. Incompatible changes must bump ABI versions and fail closed at runtime.
- **Advanced escape hatches:** raw effects, manual registries, low-level ids, - **Advanced escape hatches:** raw effects, manual registries, low-level ids,
+2 -2
View File
@@ -15,8 +15,8 @@ changes:
- `#[hemx::surface]`, `#[hemx::app]`, `#[hemx::component]`, `#[hemx::handler]`, - `#[hemx::surface]`, `#[hemx::app]`, `#[hemx::component]`, `#[hemx::handler]`,
and `#[hemx::form]` and `#[hemx::form]`
- generated component helpers for slots, keyed partials, forms, handles, page - generated component helpers for slots, keyed partials, forms, handles, page
targets, class tokens, and events targets, page-boundary rendering, class tokens, and events
- `hemx::page(...)` for page-boundary rendering - `hemx::page(...)` only at explicit server shell boundaries
- tuple `IntoEffect` composition - tuple `IntoEffect` composition
- `Result<impl IntoEffect, E>` handlers with `IntoHandlerFailure` - `Result<impl IntoEffect, E>` handlers with `IntoHandlerFailure`
- generated form commands such as `clear`, `reset`, `error`, and `focus` - generated form commands such as `clear`, `reset`, `error`, and `focus`
+2 -2
View File
@@ -627,7 +627,7 @@ pub struct AppShell {
} }
pub fn page(runtime_src: &'static str, state: &WorkoutState) -> Html { pub fn page(runtime_src: &'static str, state: &WorkoutState) -> Html {
ui::render(&AppShell { ui::page(&AppShell {
runtime_src, runtime_src,
body: render(state), body: render(state),
}) })
@@ -683,7 +683,7 @@ pub fn view(state: &WorkoutState) -> Workout {
} }
pub fn render(state: &WorkoutState) -> Html { pub fn render(state: &WorkoutState) -> Html {
ui::render(&view(state)) ui::page(&view(state))
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
+10
View File
@@ -743,6 +743,9 @@ impl Resources {
"{pad} ::hemx::__private::html_trusted(lower(html))\n" "{pad} ::hemx::__private::html_trusted(lower(html))\n"
)); ));
out.push_str(&format!("{pad}}}\n\n")); out.push_str(&format!("{pad}}}\n\n"));
out.push_str(&format!(
"{pad}pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"
));
out.push_str(&format!("{pad}#[doc(hidden)]\n")); out.push_str(&format!("{pad}#[doc(hidden)]\n"));
out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n")); out.push_str(&format!("{pad}pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html {{ render(view) }}\n\n"));
out.push_str(&format!("{pad}#[doc(hidden)]\n")); out.push_str(&format!("{pad}#[doc(hidden)]\n"));
@@ -1733,6 +1736,7 @@ mod tests {
assert!( assert!(
generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") generated.contains("pub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")
); );
assert!(generated.contains("pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"));
assert!(generated.contains( assert!(generated.contains(
"#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html" "#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"
)); ));
@@ -1845,6 +1849,9 @@ mod tests {
assert!( assert!(
generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html") generated.contains("\npub fn render(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")
); );
assert!(
generated.contains("\npub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")
);
assert!(generated.contains("\n#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")); assert!(generated.contains("\n#[doc(hidden)]\npub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"));
assert!(generated.contains("\npub fn put<T>(slot: ::hemx::advanced::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect")); assert!(generated.contains("\npub fn put<T>(slot: ::hemx::advanced::Slot<T>, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect"));
assert!(generated.contains("\npub fn append<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect")); assert!(generated.contains("\npub fn append<K, T>(slot: ::hemx::advanced::KeyedSlot<K, T>, key: K, view: &impl ::hemplate::Hemplate) -> ::hemx::advanced::Effect"));
@@ -1858,6 +1865,9 @@ mod tests {
assert!(generated.contains(" pub const todos: SlotTarget<::std::string::String> = SlotTarget::new(super::advanced::slots::todos);")); assert!(generated.contains(" pub const todos: SlotTarget<::std::string::String> = SlotTarget::new(super::advanced::slots::todos);"));
assert!(generated.contains(" pub use self::targets::todos;")); assert!(generated.contains(" pub use self::targets::todos;"));
assert!(generated.contains(" #[doc(hidden)]\n pub fn lower_html")); assert!(generated.contains(" #[doc(hidden)]\n pub fn lower_html"));
assert!(
generated.contains(" pub fn page(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")
);
assert!(generated.contains(" #[doc(hidden)]\n pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html")); assert!(generated.contains(" #[doc(hidden)]\n pub fn render_html(view: &impl ::hemplate::Hemplate) -> ::hemx::Html"));
assert!( assert!(
generated.contains(" pub fn static_fragment(html: &'static str) -> ::hemx::Html") generated.contains(" pub fn static_fragment(html: &'static str) -> ::hemx::Html")