refactor!: rename slhx to hemx

Rename the tracked product identity, crate/package names, Rust paths/macros, generated artifacts, runtime files, public attributes, examples, docs, requirements, and tests from slhx to hemx without compatibility shims.

Verified with cargo run -p hemx-xtask -- test, cargo test -p hemx-derive --test compile_fail, cargo test -p hemx-js, cargo test -p hemx-axum, cargo test -p hemx-v0-examples, cargo check --workspace, redgate list, redgate refs, redgate health --strict, git diff --check, and git grep/ls-files legacy-name audits.

req: misc/001

req: codegen/001

req: component/004

req: runtime/001
This commit is contained in:
slhx agent
2026-06-05 06:52:37 +02:00
parent d4e865ef92
commit c33500440e
69 changed files with 1415 additions and 1415 deletions
+35 -35
View File
@@ -4,18 +4,18 @@ A board with drag-and-drop cards, 60fps pointer-follow, optimistic updates,
offline queue, conflict reconciliation, live presence, and SSR-first rendering —
all without React/Vue/VDOM, in a single typed Rust codebase.
This is an explicitly advanced/low-level north-star boundary sketch for slhx + hemplate + slhx-sync, not the beginner-facing authoring path. Raw sync/effect/wire vocabulary below is excluded from beginner-facing examples by design.
This is an explicitly advanced/low-level north-star boundary sketch for hemx + hemplate + hemx-sync, not the beginner-facing authoring path. Raw sync/effect/wire vocabulary below is excluded from beginner-facing examples by design.
---
## 1. Template: `board.heml`
```html
<section data-slhx-root="board" data-slhx-slot="board" data-slhx-atom="board">
<section data-hemx-root="board" data-hemx-slot="board" data-hemx-atom="board">
<header>
<h1>{+ self.title +}</h1>
<form data-slhx-handle="create_card" data-slhx-form="create_card">
<form data-hemx-handle="create_card" data-hemx-form="create_card">
<input name="title" type="text" required>
<select name="column">
<template h-for="column in &self.columns" h-key="column.id">
@@ -26,14 +26,14 @@ This is an explicitly advanced/low-level north-star boundary sketch for slhx + h
</form>
</header>
<div class="columns" data-slhx-slot="columns">
<div class="columns" data-hemx-slot="columns">
<template h-for="column in &self.columns" h-key="column.id">
<section class="column" data-slhx-slot="column" +data-column-id="column.id">
<section class="column" data-hemx-slot="column" +data-column-id="column.id">
<h2>{+ column.title +}</h2>
<div class="cards" +data-column-id="column.id">
<template h-for="card in &column.cards" h-key="card.id">
<article class="card" data-slhx-slot="card" data-slhx-handle="drag_card" +data-card-id="card.id" draggable="true">
<article class="card" data-hemx-slot="card" data-hemx-handle="drag_card" +data-card-id="card.id" draggable="true">
<strong>{+ card.title +}</strong>
<small>{+ card.assignee +}</small>
</article>
@@ -43,7 +43,7 @@ This is an explicitly advanced/low-level north-star boundary sketch for slhx + h
</template>
</div>
<aside data-slhx-slot="presence">
<aside data-hemx-slot="presence">
<template h-for="user in &self.online_users" h-key="user.id">
<span>{+ user.name +}</span>
</template>
@@ -53,16 +53,16 @@ This is an explicitly advanced/low-level north-star boundary sketch for slhx + h
Notes on keyed scopes:
- `h-for="column in &self.columns" h-key="column.id"`**required** for slhx-addressable nodes inside
- `h-for="column in &self.columns" h-key="column.id"`**required** for hemx-addressable nodes inside
- `h-for="card in &column.cards" h-key="card.id"`**required**
- A slot inside a keyed loop is addressed as a generated keyed resource, never by selector strings or positional DOM targeting
- Without `h-key`, slhx rejects the build — no runtime selector fallback
- Without `h-key`, hemx rejects the build — no runtime selector fallback
---
## 2. What hemplate exports
hemplate does **not** interpret `data-slhx-*`. It records raw facts:
hemplate does **not** interpret `data-hemx-*`. It records raw facts:
```rust
Node {
@@ -70,8 +70,8 @@ Node {
element: "article",
attrs: [
("class", "card"),
("data-slhx-slot", "card"),
("data-slhx-handle", "drag_card"),
("data-hemx-slot", "card"),
("data-hemx-handle", "drag_card"),
("data-card-id", "{card.id}"),
("draggable", "true"),
],
@@ -87,7 +87,7 @@ FormSurface {
}
```
slhx reads this from hemplate Surface facts and generates scoped typed resources:
hemx reads this from hemplate Surface facts and generates scoped typed resources:
```rust
use ui::board::{forms, targets};
@@ -104,7 +104,7 @@ No string desync. No manual ids. The generated module owns the names.
## 3. App State
```rust
#[slhx::app]
#[hemx::app]
pub struct BoardApp {
pub board: Atom<BoardState>,
pub drag: Atom<Option<DragState>>,
@@ -119,13 +119,13 @@ The same struct runs on server (SSR) and in WASM (client-local effects).
## 4. Normal Form: Server-first
```rust
#[derive(SlhxForm)]
#[derive(HemxForm)]
pub struct CreateCardForm {
pub title: String,
pub column: ColumnId,
}
#[slhx::handler]
#[hemx::handler]
pub fn create_card(
form: Form<CreateCardForm>,
app: &mut BoardApp,
@@ -137,7 +137,7 @@ pub fn create_card(
(
targets::card.append(card.id, &CardView::from(card)),
forms::create_card.clear("title"),
// slhx-sync: queue atomic board state diff for sync
// hemx-sync: queue atomic board state diff for sync
SyncEffect::send_patch(atoms::board, Patch::insert_card(form.column, card)),
)
}
@@ -150,7 +150,7 @@ HTML submits as usual. Server returns a typed update batch. Browser applies DOM
## 5. Drag: 60fps client-local WASM
```rust
#[slhx::handler(client)]
#[hemx::handler(client)]
pub fn drag_card(
event: DragEvent,
app: &mut BoardApp,
@@ -163,7 +163,7 @@ pub fn drag_card(
}));
// Client-local extension APIs stay typed by generated resources;
// names below are illustrative until slhx-sync lands.
// names below are illustrative until hemx-sync lands.
Effect::batch((
Effect::class_keyed(slots::CARD, event.card_id, "dragging", true),
Effect::transform_keyed(
@@ -182,7 +182,7 @@ Zero round-trip. Zero custom JS. Pure Rust → typed updates → DOM.
## 6. Drop: optimistic update + sync
```rust
#[slhx::handler(client)]
#[hemx::handler(client)]
pub fn drop_card(
event: DropEvent,
app: &mut BoardApp,
@@ -194,7 +194,7 @@ pub fn drop_card(
app.drag.set(None);
// Client-local extension APIs stay typed by generated resources;
// names below are illustrative until slhx-sync lands.
// names below are illustrative until hemx-sync lands.
Effect::batch((
Effect::move_keyed(
slots::CARD,
@@ -204,7 +204,7 @@ pub fn drop_card(
InsertBefore(event.before_card),
),
Effect::class_keyed(slots::CARD, event.card_id, "dragging", false),
// slhx-sync: queue patch, send when online
// hemx-sync: queue patch, send when online
SyncEffect::send_patch(atoms::BOARD, patch),
))
}
@@ -212,14 +212,14 @@ pub fn drop_card(
A pure htmx+SSR app cannot model this: 60fps pointer → local transient drag → optimistic update → offline queue → reconciliation. You'd need custom JS or a parallel React/Vue layer.
slhx models it in one type graph.
hemx models it in one type graph.
---
## 7. Server reconciliation
```rust
#[slhx_sync::handler]
#[hemx_sync::handler]
pub fn apply_board_patch(
patch: BoardPatch,
app: &mut BoardApp,
@@ -253,7 +253,7 @@ Server-authoritative on conflict. No Redux sagas. No React Query cache fades.
## 8. Presence
```rust
#[slhx_sync::presence]
#[hemx_sync::presence]
pub fn user_joined(user: UserPresence) -> impl IntoEffect {
targets::presence_user.append(user.id, &PresenceBadge::from(user))
}
@@ -272,22 +272,22 @@ The runtime does not know "presence". It executes generated DOM updates.
## 9. What app authors write; what the browser receives
Initial SSR stays an ordinary rendered template with symbolic slhx attributes at
Initial SSR stays an ordinary rendered template with symbolic hemx attributes at
the authoring boundary:
```html
<section data-slhx-root="board" data-slhx-slot="board" data-slhx-atom="board">
<section data-hemx-root="board" data-hemx-slot="board" data-hemx-atom="board">
...
<article data-slhx-slot="card" data-slhx-handle="select_card" +data-card-id="card.id">
<article data-hemx-slot="card" data-hemx-handle="select_card" +data-card-id="card.id">
{+ card.title +}
</article>
...
</section>
<!-- the app shell loads /slhx.js and any bootstrap state -->
<!-- the app shell loads /hemx.js and any bootstrap state -->
```
The compiler lowers those symbols to compact runtime metadata, but that metadata
is not an app-authoring contract. Runtime attachment: `/slhx.js` installs
is not an app-authoring contract. Runtime attachment: `/hemx.js` installs
delegated root listeners for forms, clicks, and pointer/drag events. App authors
keep composing generated resources; they do not attach per-node listeners, copy
numeric ids, or write selector glue.
@@ -298,18 +298,18 @@ No framework download. No VDOM. No hydration. No game loop.
## 10. Why this is not a React/Vue/htmx app
| Concern | React/Vue | htmx+SSR | slhx |
| Concern | React/Vue | htmx+SSR | hemx |
|---|---|---|---|
| SSR | RSC/Vue SSR | native | native (hemplate) |
| 60fps drag | 100ms re-render + React-DnD | custom JS | WASM handler, typed update |
| Optimistic update | useOptimistic | impossible | `board.update``SyncEffect::send_patch` |
| Offline support | Service Worker + custom | impossible | patch queue in `slhx-sync` |
| Offline support | Service Worker + custom | impossible | patch queue in `hemx-sync` |
| Conflict resolution | manual / Yjs CRDT | impossible | server-authoritative patch |
| Presence | WebSocket + custom state | SSE possible | `Effect::broadcast` over channel |
| Keyed DOM | React key | not a concern | `KeyedSlot<K, T>` compile-time |
| Forms | React Hook Form | HTML native, but no validation bridge | `Form<T>` derived from `.heml` surface |
| Routing | React Router / Vue Router | HTML links, but no state routing | `Effect::navigate` with scroll/title |
| Total JS shipped | ~300KB+ | ~20KB htmx + custom | ~3KB slhx.js interpreter |
| Total JS shipped | ~300KB+ | ~20KB htmx + custom | ~3KB hemx.js interpreter |
---
@@ -317,7 +317,7 @@ No framework download. No VDOM. No hydration. No game loop.
```text
A local-first multiplayer board where all high-frequency UI runs as Rust/WASM effects,
all durable state syncs through slhx-sync,
all durable state syncs through hemx-sync,
all HTML is hemplate-rendered,
and the browser runtime only executes typed postcard DOM ops.
```
@@ -338,6 +338,6 @@ But:
```text
Rust owns types.
hemplate owns structure.
slhx owns interaction.
hemx owns interaction.
browser executes ops.
```
+5 -5
View File
@@ -1,5 +1,5 @@
[package]
name = "slhx-kanban-example"
name = "hemx-kanban-example"
version.workspace = true
edition.workspace = true
publish = false
@@ -11,13 +11,13 @@ path = "src/lib.rs"
axum = "0.7"
futures-util = "0.3"
hemplate = { path = "../../../hemplate/hemplate" }
slhx = { path = "../../slhx" }
slhx-axum = { path = "../../slhx-axum" }
hemx = { path = "../../hemx" }
hemx-axum = { path = "../../hemx-axum" }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
[dev-dependencies]
scraper = "0.23"
slhx-test = { path = "../../slhx-test" }
hemx-test = { path = "../../hemx-test" }
[build-dependencies]
slhx-build = { path = "../../slhx-build" }
hemx-build = { path = "../../hemx-build" }
+2 -2
View File
@@ -1,8 +1,8 @@
# slhx Kanban browser example
# hemx Kanban browser example
Run:
cargo run -p slhx-kanban-example
cargo run -p hemx-kanban-example
Open <http://127.0.0.1:3001>.
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
slhx_build::app().run().unwrap();
hemx_build::app().run().unwrap();
}
+6 -6
View File
@@ -1,4 +1,4 @@
#[slhx::surface]
#[hemx::surface]
pub mod ui {}
#[cfg(test)]
@@ -6,8 +6,8 @@ mod tests {
use super::ui::{board, board_card};
use hemplate::Hemplate;
use scraper::{Html, Selector};
use slhx::IntoEffect;
use slhx_test::inspect;
use hemx::IntoEffect;
use hemx_test::inspect;
#[derive(Hemplate)]
#[hemplate = "partials"]
@@ -17,7 +17,7 @@ mod tests {
#[allow(dead_code)]
#[derive(Clone, Debug)]
#[slhx::form("create_card")]
#[hemx::form("create_card")]
struct CreateCard {
title: String,
column: String,
@@ -56,8 +56,8 @@ mod tests {
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn kanban_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn create_card(_form: slhx::Form<CreateCard>) -> impl IntoEffect {
#[hemx::handler]
fn create_card(_form: hemx::Form<CreateCard>) -> impl IntoEffect {
board::notice.text("queued")
}
+10 -10
View File
@@ -4,14 +4,14 @@ use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{Html, IntoEffect};
use slhx_axum::{
use hemx::{Html, IntoEffect};
use hemx_axum::{
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
InteractionRequest, PageRequest,
};
use slhx_kanban_example::ui::board::{self as board};
use slhx_kanban_example::ui::board_card as card_board;
use slhx_kanban_example::ui::{self, board as board_ui};
use hemx_kanban_example::ui::board::{self as board};
use hemx_kanban_example::ui::board_card as card_board;
use hemx_kanban_example::ui::{self, board as board_ui};
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::net::SocketAddr;
@@ -118,12 +118,12 @@ async fn main() {
let app = Router::new()
.route("/", get(home).post(interact))
.route("/events", get(events))
.route("/slhx.js", get(runtime))
.route("/hemx.js", get(runtime))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3001));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("slhx Kanban example: http://{addr}");
println!("hemx Kanban example: http://{addr}");
axum::serve(listener, app).await.unwrap();
}
@@ -132,7 +132,7 @@ async fn home(State(state): State<Arc<AppState>>, request: PageRequest) -> impl
let board = state.board.lock().unwrap().clone();
request
.page_html(page_html(&board), shell)
.title("slhx Kanban")
.title("hemx Kanban")
.fingerprint(ui::BUILD_FINGERPRINT)
}
@@ -288,7 +288,7 @@ fn page_html(board: &BoardState) -> Html {
fn shell(body: Html) -> Html {
// req: html_safety/001 req: html_safety/002 req: axum_integration/001
slhx::render(&AppShell { body })
hemx::render(&AppShell { body })
}
fn render_options() -> Html {
@@ -333,7 +333,7 @@ fn render_card(card: &Card) -> BoardCard {
mod tests {
use super::*;
use scraper::{Html, Selector};
use slhx_test::{
use hemx_test::{
class_child_selector, disabled_button_selector, element_class_selector,
escaped_markup_selector, form_selector, keyed_selector, root_element_selector,
select_options_selector, small_text_selector, strong_text_selector,
+2 -2
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>slhx Kanban</title>
<script src="/slhx.js" defer></script>
<title>hemx Kanban</title>
<script src="/hemx.js" defer></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; }
form { display: flex; gap: .5rem; flex-wrap: wrap; margin: 1rem 0; }
+6 -6
View File
@@ -1,15 +1,15 @@
<section data-slhx-root="kanban" data-slhx-sse="/events">
<section data-hemx-root="kanban" data-hemx-sse="/events">
<header>
<h1>slhx Kanban</h1>
<form data-slhx-handle="create_card" data-slhx-form="create_card" data-slhx-disable-while-pending>
<h1>hemx Kanban</h1>
<form data-hemx-handle="create_card" data-hemx-form="create_card" data-hemx-disable-while-pending>
<input name="title" type="text" required="required" placeholder="Card title">
<select name="column" required="required">{+= self.options =+}</select>
<button type="submit">Add card</button>
</form>
<p data-slhx-slot="notice">Ready</p>
<p data-hemx-slot="notice">Ready</p>
</header>
<div data-slhx-slot="board">{+= self.board =+}</div>
<aside data-slhx-slot="presence">Waiting for presence…</aside>
<div data-hemx-slot="board">{+= self.board =+}</div>
<aside data-hemx-slot="presence">Waiting for presence…</aside>
</section>
@@ -1,10 +1,10 @@
<article class="card" +data-key="self.id">
<strong>{+ self.title +}</strong>
<menu>
<button h-if="self.left_disabled" type="button" data-slhx-handle="move_left" +data-card-id="self.id" disabled="disabled">←</button>
<button h-else type="button" data-slhx-handle="move_left" +data-card-id="self.id">←</button>
<button h-if="self.right_disabled" type="button" data-slhx-handle="move_right" +data-card-id="self.id" disabled="disabled">→</button>
<button h-else type="button" data-slhx-handle="move_right" +data-card-id="self.id">→</button>
<button type="button" data-slhx-handle="delete_card" +data-card-id="self.id">Delete</button>
<button h-if="self.left_disabled" type="button" data-hemx-handle="move_left" +data-card-id="self.id" disabled="disabled">←</button>
<button h-else type="button" data-hemx-handle="move_left" +data-card-id="self.id">←</button>
<button h-if="self.right_disabled" type="button" data-hemx-handle="move_right" +data-card-id="self.id" disabled="disabled">→</button>
<button h-else type="button" data-hemx-handle="move_right" +data-card-id="self.id">→</button>
<button type="button" data-hemx-handle="delete_card" +data-card-id="self.id">Delete</button>
</menu>
</article>
+5 -5
View File
@@ -1,5 +1,5 @@
[package]
name = "slhx-techdemo"
name = "hemx-techdemo"
version.workspace = true
edition.workspace = true
publish = false
@@ -11,14 +11,14 @@ path = "src/lib.rs"
axum = "0.7"
futures-util = "0.3"
hemplate = { path = "../../../hemplate/hemplate" }
slhx = { path = "../../slhx" }
slhx-axum = { path = "../../slhx-axum" }
hemx = { path = "../../hemx" }
hemx-axum = { path = "../../hemx-axum" }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
[dev-dependencies]
scraper = "0.23"
slhx-test = { path = "../../slhx-test" }
hemx-test = { path = "../../hemx-test" }
thirtyfour = "0.35"
[build-dependencies]
slhx-build = { path = "../../slhx-build" }
hemx-build = { path = "../../hemx-build" }
+9 -9
View File
@@ -1,12 +1,12 @@
# slhx full techdemo
# hemx full techdemo
Run:
cargo run -p slhx-techdemo
cargo run -p hemx-techdemo
Open <http://127.0.0.1:3002>.
This is a polished Linear-style product demo for planning typed work across lanes. It is tailored to showcase slhx strengths:
This is a polished Linear-style product demo for planning typed work across lanes. It is tailored to showcase hemx strengths:
- modern SSR-first UI
- generated target objects from `.heml`
@@ -17,12 +17,12 @@ This is a polished Linear-style product demo for planning typed work across lane
- root-scoped runtime application without selector lookups
- page-enhancer navigation with native link fallback
- SSE server push into a generated slot
- drag-and-drop lane moves persisted by typed server handlers through the slhx runtime
- an explicit advanced opaque canvas island fed by a generated event helper, without teaching slhx core about the widget
- no user-authored browser JavaScript in slhx-managed UI; the island JavaScript is a leaf-widget escape hatch
- drag-and-drop lane moves persisted by typed server handlers through the hemx runtime
- an explicit advanced opaque canvas island fed by a generated event helper, without teaching hemx core about the widget
- no user-authored browser JavaScript in hemx-managed UI; the island JavaScript is a leaf-widget escape hatch
Verification:
cargo test -p slhx-techdemo --test e2e
cargo test -p slhx-techdemo --test browser_e2e
mutest -p slhx-techdemo -f examples/techdemo/src/main.rs -F 'registry' -j 2 --timeout 90 -- --test e2e
cargo test -p hemx-techdemo --test e2e
cargo test -p hemx-techdemo --test browser_e2e
mutest -p hemx-techdemo -f examples/techdemo/src/main.rs -F 'registry' -j 2 --timeout 90 -- --test e2e
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
slhx_build::app().run().unwrap();
hemx_build::app().run().unwrap();
}
+6 -6
View File
@@ -1,4 +1,4 @@
#[slhx::surface]
#[hemx::surface]
pub mod ui {}
#[cfg(test)]
@@ -7,8 +7,8 @@ mod tests {
use super::ui::issue_card::advance_work;
use super::ui::issue_lane::events as lane_events;
use hemplate::Hemplate;
use slhx::IntoEffect;
use slhx_test::inspect;
use hemx::IntoEffect;
use hemx_test::inspect;
#[derive(Hemplate)]
#[hemplate = "partials"]
@@ -18,7 +18,7 @@ mod tests {
#[allow(dead_code)]
#[derive(Clone, Debug)]
#[slhx::form("launch_work")]
#[hemx::form("launch_work")]
struct LaunchWork {
title: String,
lane: String,
@@ -43,8 +43,8 @@ mod tests {
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn techdemo_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn launch_work(_form: slhx::Form<LaunchWork>) -> impl IntoEffect {
#[hemx::handler]
fn launch_work(_form: hemx::Form<LaunchWork>) -> impl IntoEffect {
notice.text("queued")
}
+16 -16
View File
@@ -5,14 +5,14 @@ use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{CssClass, CssClasses, EventName, Html, IntoEffect};
use slhx_axum::{
use hemx::{CssClass, CssClasses, EventName, Html, IntoEffect};
use hemx_axum::{
interactions, runtime_js, sse, DispatchRegistry, DispatchRejection, EffectResponse,
InteractionRequest, PageRequest,
};
use slhx_techdemo::ui;
use slhx_techdemo::ui::control_center::{self as control, classes};
use slhx_techdemo::ui::{issue_card as card_control, issue_lane as lane_control};
use hemx_techdemo::ui;
use hemx_techdemo::ui::control_center::{self as control, classes};
use hemx_techdemo::ui::{issue_card as card_control, issue_lane as lane_control};
use std::collections::{BTreeMap, VecDeque};
use std::convert::Infallible;
use std::net::SocketAddr;
@@ -24,7 +24,7 @@ const LANES: [(&str, &str, &str); 3] = [
("runtime", "Runtime", "typed updates → DOM"),
("product", "Product", "Native UX, zero app JS"),
];
const ISLAND_ORBIT: EventName = EventName::new("slhx:island-orbit");
const ISLAND_ORBIT: EventName = EventName::new("hemx:island-orbit");
#[derive(Clone)]
struct WorkItem {
@@ -226,18 +226,18 @@ async fn main() {
.route("/architecture", get(architecture))
.route("/events", get(events))
.route("/favicon.ico", get(favicon))
.route("/slhx.js", get(runtime))
.route("/hemx.js", get(runtime))
.route("/app.css", get(app_css))
.route("/control_center.css", get(control_center_css))
.route("/island.js", get(island_js))
.with_state(state);
let addr = std::env::var("SLHX_TECHDEMO_ADDR")
let addr = std::env::var("HEMX_TECHDEMO_ADDR")
.ok()
.and_then(|addr| addr.parse().ok())
.unwrap_or_else(|| SocketAddr::from(([127, 0, 0, 1], 3002)));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("slhx techdemo: http://{addr}");
println!("hemx techdemo: http://{addr}");
axum::serve(listener, app).await.unwrap();
}
@@ -246,7 +246,7 @@ async fn home(State(state): State<Arc<Shared>>, request: PageRequest) -> impl In
let demo = state.demo.lock().unwrap().clone();
request
.page_html(page_html(&demo), shell)
.title("slhx Techdemo")
.title("hemx Techdemo")
.fingerprint(ui::BUILD_FINGERPRINT)
}
@@ -261,7 +261,7 @@ async fn architecture(request: PageRequest) -> impl IntoResponse {
});
request
.page_html(body, shell)
.title("slhx Architecture")
.title("hemx Architecture")
.fingerprint(ui::BUILD_FINGERPRINT)
}
@@ -659,11 +659,11 @@ mod tests {
.select(&selector("title"))
.next()
.map(|title| title.text().collect::<String>()),
Some("slhx Techdemo".to_owned())
Some("hemx Techdemo".to_owned())
);
assert_eq!(
document
.select(&selector("script[src=\"/slhx.js\"]"))
.select(&selector("script[src=\"/hemx.js\"]"))
.count(),
1
);
@@ -687,7 +687,7 @@ mod tests {
);
assert_eq!(
document
.select(&selector("main[data-slhx-root=\"techdemo\"]"))
.select(&selector("main[data-hemx-root=\"techdemo\"]"))
.count(),
1
);
@@ -706,7 +706,7 @@ mod tests {
let document = Html::parse_fragment(html.as_str());
assert_eq!(
document
.select(&selector("[data-slhx-root=\"techdemo\"]"))
.select(&selector("[data-hemx-root=\"techdemo\"]"))
.count(),
1
);
@@ -853,7 +853,7 @@ mod tests {
assert_eq!(items.len(), 3);
assert_eq!(
items[1].text().collect::<String>(),
"Fetched HTML with X-SLHX-Partial"
"Fetched HTML with X-HEMX-Partial"
);
}
+1 -1
View File
@@ -7,7 +7,7 @@ main { width:min(1180px, calc(100vw - 32px)); margin:0 auto; padding:38px 0 56px
.hero-shell { display:grid; grid-template-columns:1.35fr .85fr; gap:22px; align-items:stretch; }
.hero-copy, .hero-panel, .command-card, .board-card, .glass-card, .topology { border:1px solid var(--line); background:linear-gradient(145deg, rgba(255,255,255,.14), rgba(255,255,255,.055)); box-shadow:0 24px 90px rgba(0,0,0,.36), inset 0 1px 0 rgba(255,255,255,.12); backdrop-filter: blur(22px) saturate(145%); border-radius:28px; }
.hero-copy { padding:34px; overflow:hidden; position:relative; }
.hero-copy::after { content:"slhx"; position:absolute; right:-18px; bottom:8px; font-size:86px; font-weight:900; color:rgba(255,255,255,.045); }
.hero-copy::after { content:"hemx"; position:absolute; right:-18px; bottom:8px; font-size:86px; font-weight:900; color:rgba(255,255,255,.045); }
.eyebrow { color:var(--cyan); text-transform:uppercase; letter-spacing:.2em; font-weight:800; font-size:12px; }
h1 { font-size:clamp(42px, 7vw, 84px); line-height:.88; letter-spacing:-.075em; margin:12px 0 18px; max-width:900px; text-wrap:balance; overflow-wrap:anywhere; }
h2 { margin:0 0 16px; letter-spacing:-.035em; }
+2 -2
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>slhx Techdemo</title>
<script src="/slhx.js" defer></script>
<title>hemx Techdemo</title>
<script src="/hemx.js" defer></script>
<script src="/island.js" defer></script>
<link rel="stylesheet" href="/app.css">
<link rel="stylesheet" href="/control_center.css">
+16 -16
View File
@@ -1,22 +1,22 @@
<main data-slhx-root="techdemo" data-slhx-sse="/events">
<main data-hemx-root="techdemo" data-hemx-sse="/events">
<section class="hero-shell">
<div class="hero-copy">
<p class="eyebrow">slhx Control Plane</p>
<p class="eyebrow">hemx Control Plane</p>
<h1>A Linear-class work system without a frontend framework.</h1>
<p class="lede">Create, inspect, advance, and stream work through native HTML, generated typed resources, and compact update batches. The UI feels app-grade; the model stays server-owned and boring.</p>
</div>
<div class="hero-panel" data-slhx-slot="hero_metrics">{+= self.hero =+}</div>
<div class="hero-panel" data-hemx-slot="hero_metrics">{+= self.hero =+}</div>
</section>
<nav class="topology" data-slhx-slot="nav">
<a href="/" data-slhx-nav="">Live system</a>
<a href="/architecture" data-slhx-nav="">Architecture</a>
<nav class="topology" data-hemx-slot="nav">
<a href="/" data-hemx-nav="">Live system</a>
<a href="/architecture" data-hemx-nav="">Architecture</a>
</nav>
<section class="workspace">
<aside class="command-card">
<h2>Create an issue</h2>
<form id="launch-work" method="post" data-slhx-handle="launch_work" data-slhx-form="launch_work" data-slhx-disable-while-pending>
<form id="launch-work" method="post" data-hemx-handle="launch_work" data-hemx-form="launch_work" data-hemx-disable-while-pending>
<label>Title <input name="title" required="required" value="Ship typed updates"></label>
<label>Lane
<select name="lane" required="required">
@@ -26,13 +26,13 @@
</select>
</label>
<label>Impact <input name="impact" type="number" min="1" max="9" value="7"></label>
<button class="primary-action" type="button" form="launch-work" data-slhx-handle="launch_work">Create issue</button>
<button class="primary-action" type="button" form="launch-work" data-hemx-handle="launch_work">Create issue</button>
</form>
<div class="quick-actions">
<button type="button" data-slhx-handle="simulate_push">Simulate server push</button>
<button type="button" data-slhx-handle="reset_demo">Reset demo</button>
<button type="button" data-hemx-handle="simulate_push">Simulate server push</button>
<button type="button" data-hemx-handle="reset_demo">Reset demo</button>
</div>
<p data-slhx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
<p data-hemx-slot="notice" class="notice">Every control posts through a generated handle and receives typed updates.</p>
</aside>
<section class="board-card">
@@ -40,27 +40,27 @@
<span>Generated slots + keyed cards</span>
<strong>generated resources, no selectors</strong>
</div>
<div data-slhx-slot="board">{+= self.board =+}</div>
<div data-hemx-slot="board">{+= self.board =+}</div>
</section>
</section>
<section class="insight-grid">
<article class="glass-card">
<h2>Update inspector</h2>
<div data-slhx-slot="inspector" class="inspector-slot">{+= self.inspector =+}</div>
<div data-hemx-slot="inspector" class="inspector-slot">{+= self.inspector =+}</div>
</article>
<article class="glass-card island-card" data-slhx-island="orbit" +data-island-snapshot="self.island_snapshot">
<article class="glass-card island-card" data-hemx-island="orbit" +data-island-snapshot="self.island_snapshot">
<h2>Opaque island bridge</h2>
<canvas width="360" height="180" aria-label="Animated island orbit"></canvas>
<p data-island-readout="">Waiting for Rust snapshot…</p>
</article>
<article class="glass-card">
<h2>Activity stream</h2>
<div data-slhx-slot="activity">{+= self.activity =+}</div>
<div data-hemx-slot="activity">{+= self.activity =+}</div>
</article>
<article class="glass-card glow">
<h2>Server push</h2>
<div data-slhx-slot="live_feed" class="live-feed">Waiting for SSE heartbeat…</div>
<div data-hemx-slot="live_feed" class="live-feed">Waiting for SSE heartbeat…</div>
</article>
</section>
+6 -6
View File
@@ -1,4 +1,4 @@
// Opaque leaf-widget island. slhx talks to it only with native CustomEvent payloads.
// Opaque leaf-widget island. hemx talks to it only with native CustomEvent payloads.
// req: interop/001 req: examples/001
(() => {
const roots = new WeakMap();
@@ -20,7 +20,7 @@
function rootOf(node) {
for (let el = node; el; el = el.parentElement) {
if (el.hasAttribute && el.hasAttribute("data-slhx-root")) return el;
if (el.hasAttribute && el.hasAttribute("data-hemx-root")) return el;
}
return document.documentElement;
}
@@ -66,7 +66,7 @@
ctx.fillStyle = "#fff";
ctx.font = "700 16px system-ui, sans-serif";
ctx.fillText("slhx island", 18, 30);
ctx.fillText("hemx island", 18, 30);
ctx.font = "12px system-ui, sans-serif";
ctx.fillStyle = "rgba(255,255,255,.74)";
ctx.fillText(`cards ${state.snapshot.cards} · impact ${state.snapshot.impact} · boost ${state.snapshot.power}`, 18, 50);
@@ -85,11 +85,11 @@
state.snapshot = parseSnapshot(event.detail);
island.setAttribute("data-island-snapshot", event.detail);
};
root.addEventListener("slhx:island-orbit", update);
root.addEventListener("hemx:island-orbit", update);
function tick() {
if (!document.contains(island)) {
root.removeEventListener("slhx:island-orbit", update);
root.removeEventListener("hemx:island-orbit", update);
return;
}
state.frame += 1;
@@ -101,7 +101,7 @@
function scan() {
forEachElement(document, (el) => {
if (el.hasAttribute("data-slhx-island")) boot(el);
if (el.hasAttribute("data-hemx-island")) boot(el);
});
}
@@ -1,5 +1,5 @@
<ol class="activity">
<li>Clicked a real anchor</li>
<li>Fetched HTML with X-SLHX-Partial</li>
<li>Fetched HTML with X-HEMX-Partial</li>
<li>Preserved native fallback semantics</li>
</ol>
@@ -1,5 +1,5 @@
<div class="lanes">
<section class="lane"><h3>hemplate</h3><p>Owns syntax and Surface facts.</p></section>
<section class="lane"><h3>slhx-build</h3><p>Generates resources and lowering tables.</p></section>
<section class="lane"><h3>hemx-build</h3><p>Generates resources and lowering tables.</p></section>
<section class="lane"><h3>runtime</h3><p>Executes compact typed DOM ops.</p></section>
</div>
@@ -2,8 +2,8 @@
<header><strong>{+ self.title +}</strong><span class="pill">{+ self.stage +}</span></header>
<div class="impact"><i +style="self.impact_style"></i></div>
<div class="card-actions">
<button type="button" data-slhx-handle="spotlight_work" +data-work-id="self.id" name="work_id" +value="self.id">Inspect</button>
<button type="button" data-slhx-handle="advance_work" +data-work-id="self.id" name="work_id" +value="self.id">Advance</button>
<button type="button" data-slhx-handle="delete_work" +data-work-id="self.id" name="work_id" +value="self.id">Delete</button>
<button type="button" data-hemx-handle="spotlight_work" +data-work-id="self.id" name="work_id" +value="self.id">Inspect</button>
<button type="button" data-hemx-handle="advance_work" +data-work-id="self.id" name="work_id" +value="self.id">Advance</button>
<button type="button" data-hemx-handle="delete_work" +data-work-id="self.id" name="work_id" +value="self.id">Delete</button>
</div>
</article>
@@ -1,4 +1,4 @@
<section +class="self.class" +data-lane="self.lane_id" data-slhx-handle="move_to_lane" data-slhx-on="drop">
<section +class="self.class" +data-lane="self.lane_id" data-hemx-handle="move_to_lane" data-hemx-on="drop">
<h3>{+ self.title +}</h3>
<p>{+ self.description +}</p>
<template h-for="card in &self.cards">
+6 -6
View File
@@ -1,5 +1,5 @@
use slhx_techdemo::ui::control_center::{self as control, launch_work, simulate_push};
use slhx_test::{
use hemx_techdemo::ui::control_center::{self as control, launch_work, simulate_push};
use hemx_test::{
any_root_selector, document_body_selector, handle_button_selector, handle_selector,
island_probe_script, island_readout_selector, keyed_selector, nav_link_selector,
scoped_island_readout_selector, target_selector,
@@ -36,8 +36,8 @@ impl Drop for ChildProcess {
#[tokio::test]
async fn browser_drives_typed_product_end_to_end() -> WebDriverResult<()> {
// req: examples/001 req: dx/008 req: form/002 req: page_swap/002 req: push/003
let mut app = Command::new(env!("CARGO_BIN_EXE_slhx-techdemo"));
app.env("SLHX_TECHDEMO_ADDR", APP_ADDR);
let mut app = Command::new(env!("CARGO_BIN_EXE_hemx-techdemo"));
app.env("HEMX_TECHDEMO_ADDR", APP_ADDR);
let _app = ChildProcess::spawn(app);
wait_for_tcp(APP_ADDR);
@@ -153,7 +153,7 @@ async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> {
loop {
let loaded = driver
.execute(
"return !!window.slhx && window.slhx.roots().length > 0",
"return !!window.hemx && window.hemx.roots().length > 0",
Vec::new(),
)
.await?
@@ -164,7 +164,7 @@ async fn wait_for_runtime(driver: &WebDriver) -> WebDriverResult<()> {
return Ok(());
}
if Instant::now() >= deadline {
panic!("timed out waiting for slhx runtime");
panic!("timed out waiting for hemx runtime");
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
+16 -16
View File
@@ -1,9 +1,9 @@
use scraper::{Html, Selector};
use slhx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
use slhx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
use slhx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
use slhx_techdemo::ui::BUILD_FINGERPRINT;
use slhx_test::{
use hemx_techdemo::ui::control_center::{self as control, launch_work, reset_demo, simulate_push};
use hemx_techdemo::ui::issue_card::{advance_work, delete_work, spotlight_work};
use hemx_techdemo::ui::issue_lane::move_to_lane as move_to_lane_handle;
use hemx_techdemo::ui::BUILD_FINGERPRINT;
use hemx_test::{
class_descendant_selector, class_selector, handle_form_body, inspect_wire,
island_attribute_name, island_event_name, island_selector, island_snapshot_marker,
root_selector, sse_endpoint_marker, strong_text_selector, unknown_handle_form_body,
@@ -22,12 +22,12 @@ struct Server {
impl Server {
fn start() -> Self {
let bin = env!("CARGO_BIN_EXE_slhx-techdemo");
let bin = env!("CARGO_BIN_EXE_hemx-techdemo");
let child = Command::new(bin)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("start slhx-techdemo");
.expect("start hemx-techdemo");
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
@@ -36,7 +36,7 @@ impl Server {
}
std::thread::sleep(Duration::from_millis(25));
}
panic!("slhx-techdemo did not listen on {ADDR}");
panic!("hemx-techdemo did not listen on {ADDR}");
}
}
@@ -73,7 +73,7 @@ fn product_is_e2e_working_over_http() {
let favicon = get("/favicon.ico");
assert_eq!(favicon.status, 204);
let runtime = get("/slhx.js");
let runtime = get("/hemx.js");
assert_eq!(runtime.status, 200);
assert!(runtime.header("content-type").contains("javascript"));
@@ -85,12 +85,12 @@ fn product_is_e2e_working_over_http() {
assert!(island.text().contains("MutationObserver"));
assert!(island.text().contains("removeEventListener"));
let architecture = request("GET", "/architecture", &[("X-SLHX-Partial", "1")], "");
let architecture = request("GET", "/architecture", &[("X-HEMX-Partial", "1")], "");
assert_eq!(architecture.status, 200);
assert!(architecture.header("x-slhx-partial").contains("true"));
assert!(architecture.header("x-hemx-partial").contains("true"));
let architecture_doc = Html::parse_document(architecture.text());
assert_text(&architecture_doc, "Page swap");
assert_text(&architecture_doc, "slhx-build");
assert_text(&architecture_doc, "hemx-build");
let launch = post(
"/",
@@ -277,19 +277,19 @@ fn product_is_e2e_working_over_http() {
let sse = get("/events?once=1");
assert_eq!(sse.status, 200);
assert!(sse.header("content-type").contains("text/event-stream"));
assert!(sse.text().contains("event: slhx"));
assert!(sse.text().contains("event: hemx"));
assert!(sse.text().contains("data: "));
let unknown = post("/", &unknown_handle_form_body(999999));
assert_eq!(unknown.status, 404);
assert!(unknown.text().contains("unknown slhx handle id 999999"));
assert!(unknown.text().contains("unknown hemx handle id 999999"));
}
fn assert_effect_response(response: &Response) {
assert_eq!(response.status, 200);
assert!(response.header("content-type").contains("application/slhx"));
assert!(response.header("content-type").contains("application/hemx"));
assert_eq!(
response.header("x-slhx-fingerprint"),
response.header("x-hemx-fingerprint"),
BUILD_FINGERPRINT.0.to_string()
);
assert!(!response.effects().is_empty());
+5 -5
View File
@@ -1,5 +1,5 @@
[package]
name = "slhx-v0-examples"
name = "hemx-v0-examples"
version.workspace = true
edition.workspace = true
publish = false
@@ -11,13 +11,13 @@ path = "src/lib.rs"
axum = "0.7"
futures-util = "0.3"
hemplate = { path = "../../../hemplate/hemplate" }
slhx = { path = "../../slhx" }
slhx-axum = { path = "../../slhx-axum" }
hemx = { path = "../../hemx" }
hemx-axum = { path = "../../hemx-axum" }
tokio = { version = "1", features = ["macros", "net", "rt-multi-thread", "time"] }
[dev-dependencies]
scraper = "0.23"
slhx-test = { path = "../../slhx-test" }
hemx-test = { path = "../../hemx-test" }
[build-dependencies]
slhx-build = { path = "../../slhx-build" }
hemx-build = { path = "../../hemx-build" }
+2 -2
View File
@@ -1,9 +1,9 @@
# slhx v0 browser examples
# hemx v0 browser examples
Run the examples server:
```sh
cargo run -p slhx-v0-examples
cargo run -p hemx-v0-examples
```
Open <http://127.0.0.1:3000>.
+1 -1
View File
@@ -1,3 +1,3 @@
fn main() {
slhx_build::app().run().unwrap();
hemx_build::app().run().unwrap();
}
+14 -14
View File
@@ -1,12 +1,12 @@
#[slhx::surface]
#[hemx::surface]
pub mod ui {}
#[cfg(test)]
mod tests {
use super::ui::{auth, counter, notifications, page_swap, todos, wizard};
use hemplate::Hemplate;
use slhx::{push, IntoEffect};
use slhx_test::inspect;
use hemx::{push, IntoEffect};
use hemx_test::inspect;
#[derive(Clone, Debug)]
struct Todo {
@@ -15,19 +15,19 @@ mod tests {
}
#[derive(Clone, Debug)]
#[slhx::form("new_todo")]
#[hemx::form("new_todo")]
struct TodoInput {
title: String,
}
#[derive(Clone, Debug)]
#[slhx::form("wizard_input")]
#[hemx::form("wizard_input")]
struct WizardInput {
step: u8,
}
#[derive(Clone, Debug)]
#[slhx::form("credentials")]
#[hemx::form("credentials")]
struct Credentials {
email: String,
password: String,
@@ -40,8 +40,8 @@ mod tests {
title: String,
}
impl slhx::KeyedPartial for TodoRow {
fn slhx_key(&self) -> String {
impl hemx::KeyedPartial for TodoRow {
fn hemx_key(&self) -> String {
self.id.to_string()
}
}
@@ -67,8 +67,8 @@ mod tests {
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn add_todo(_form: slhx::Form<TodoInput>) -> impl IntoEffect {
#[hemx::handler]
fn add_todo(_form: hemx::Form<TodoInput>) -> impl IntoEffect {
todos::todo_list.set("queued")
}
@@ -101,8 +101,8 @@ mod tests {
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn wizard_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn next_step(_form: slhx::Form<WizardInput>) -> impl IntoEffect {
#[hemx::handler]
fn next_step(_form: hemx::Form<WizardInput>) -> impl IntoEffect {
wizard::wizard_step.set("queued")
}
@@ -146,8 +146,8 @@ mod tests {
// req: examples/001 req: form/001 req: form/004 req: form/006 req: derive_handler/003
#[test]
fn auth_form_handler_is_checked_against_hemplate_form() {
#[slhx::handler]
fn login(_form: slhx::Form<Credentials>) -> impl IntoEffect {
#[hemx::handler]
fn login(_form: hemx::Form<Credentials>) -> impl IntoEffect {
auth::login_status.set("queued")
}
+32 -32
View File
@@ -4,12 +4,12 @@ use axum::routing::get;
use axum::Router;
use futures_util::{stream, StreamExt};
use hemplate::Hemplate;
use slhx::{Html, IntoEffect};
use slhx_axum::{
use hemx::{Html, IntoEffect};
use hemx_axum::{
interactions, runtime_js, sse, EffectResponse, Form, InteractionRequest, PageRequest, Registry,
};
use slhx_v0_examples::ui;
use slhx_v0_examples::ui::{auth, counter, notifications, page_swap, todo_row, todos, wizard};
use hemx_v0_examples::ui;
use hemx_v0_examples::ui::{auth, counter, notifications, page_swap, todo_row, todos, wizard};
use std::collections::BTreeMap;
use std::convert::Infallible;
use std::net::SocketAddr;
@@ -31,28 +31,28 @@ struct TodoRecord {
title: String,
}
#[slhx::form("new_todo")]
#[hemx::form("new_todo")]
struct NewTodo {
title: String,
}
#[slhx::form("rename_todo")]
#[hemx::form("rename_todo")]
struct RenameTodo {
id: u64,
title: String,
}
#[slhx::form("delete_todo")]
#[hemx::form("delete_todo")]
struct DeleteTodo {
id: u64,
}
#[slhx::form("wizard_input")]
#[hemx::form("wizard_input")]
struct WizardInput {
step: String,
}
#[slhx::form("credentials")]
#[hemx::form("credentials")]
struct Credentials {
email: String,
password: String,
@@ -82,8 +82,8 @@ struct TodoRow {
title: String,
}
impl slhx::KeyedPartial for TodoRow {
fn slhx_key(&self) -> String {
impl hemx::KeyedPartial for TodoRow {
fn hemx_key(&self) -> String {
self.id.to_string()
}
}
@@ -129,12 +129,12 @@ async fn main() {
.route("/", get(home).post(interact))
.route("/docs", get(docs).post(interact))
.route("/events", get(events))
.route("/slhx.js", get(runtime))
.route("/hemx.js", get(runtime))
.with_state(state);
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
println!("slhx v0 examples: http://{addr}");
println!("hemx v0 examples: http://{addr}");
axum::serve(listener, app).await.unwrap();
}
@@ -142,7 +142,7 @@ async fn main() {
async fn home(request: PageRequest) -> impl IntoResponse {
request
.page_html(all_examples(), shell)
.title("slhx v0 examples")
.title("hemx v0 examples")
.fingerprint(ui::BUILD_FINGERPRINT)
}
@@ -188,7 +188,7 @@ async fn events(Query(params): Query<BTreeMap<String, String>>) -> impl IntoResp
sse(batches)
}
#[slhx::app(
#[hemx::app(
counter_handlers,
todo_handlers,
todo_row_handlers,
@@ -216,11 +216,11 @@ fn shell(body: Html) -> Html {
ui::render(&AppShell { body })
}
#[slhx::component("counter")]
#[hemx::component("counter")]
mod counter_handlers {
use super::*;
#[slhx::handler]
#[hemx::handler]
async fn increment(State(state): State<Arc<ExampleState>>) -> impl IntoEffect {
// req: examples/001
let mut counter = state.counter.lock().unwrap();
@@ -229,11 +229,11 @@ mod counter_handlers {
}
}
#[slhx::component("todos")]
#[hemx::component("todos")]
mod todo_handlers {
use super::*;
#[slhx::handler]
#[hemx::handler]
async fn add_todo(
State(state): State<Arc<ExampleState>>,
Form(form): Form<NewTodo>,
@@ -261,7 +261,7 @@ mod todo_handlers {
))
}
#[slhx::handler]
#[hemx::handler]
async fn rename_todo(
State(state): State<Arc<ExampleState>>,
Form(form): Form<RenameTodo>,
@@ -269,7 +269,7 @@ mod todo_handlers {
rename_todo_effect(state, form)
}
#[slhx::handler]
#[hemx::handler]
async fn delete_todo(
State(state): State<Arc<ExampleState>>,
Form(form): Form<DeleteTodo>,
@@ -278,11 +278,11 @@ mod todo_handlers {
}
}
#[slhx::component("todo_row")]
#[hemx::component("todo_row")]
mod todo_row_handlers {
use super::*;
#[slhx::handler]
#[hemx::handler]
async fn rename_todo_row(
State(state): State<Arc<ExampleState>>,
Form(form): Form<RenameTodo>,
@@ -290,7 +290,7 @@ mod todo_row_handlers {
rename_todo_effect(state, form)
}
#[slhx::handler]
#[hemx::handler]
async fn delete_todo_row(
State(state): State<Arc<ExampleState>>,
Form(form): Form<DeleteTodo>,
@@ -299,11 +299,11 @@ mod todo_row_handlers {
}
}
#[slhx::component("wizard")]
#[hemx::component("wizard")]
mod wizard_handlers {
use super::*;
#[slhx::handler]
#[hemx::handler]
async fn next_step(
State(state): State<Arc<ExampleState>>,
Form(form): Form<WizardInput>,
@@ -316,11 +316,11 @@ mod wizard_handlers {
}
}
#[slhx::component("auth")]
#[hemx::component("auth")]
mod auth_handlers {
use super::*;
#[slhx::handler]
#[hemx::handler]
async fn login(
State(_state): State<Arc<ExampleState>>,
Form(credentials): Form<Credentials>,
@@ -401,7 +401,7 @@ fn render_docs_content(message: &'static str) -> Html {
mod tests {
use super::*;
use scraper::{Html, Selector};
use slhx_test::{
use hemx_test::{
article_selector, document_title_selector, escaped_markup_selector, heading_selector,
inspect_batch, keyed_items_selector, keyed_selector, list_item_selector,
page_nav_link_selector, prose_selector, root_element_selector, runtime_script_selector,
@@ -411,8 +411,8 @@ mod tests {
Selector::parse(value).expect("test selector parses")
}
fn form<I>(handle: slhx::Handle<I>, fields: &[(&str, &str)]) -> slhx_axum::InteractionForm {
slhx_axum::InteractionForm::for_handle(
fn form<I>(handle: hemx::Handle<I>, fields: &[(&str, &str)]) -> hemx_axum::InteractionForm {
hemx_axum::InteractionForm::for_handle(
handle,
fields
.iter()
@@ -430,7 +430,7 @@ mod tests {
.select(&selector(document_title_selector()))
.next()
.map(|title| title.text().collect::<String>()),
Some("slhx v0 examples".to_owned())
Some("hemx v0 examples".to_owned())
);
assert_eq!(
document
+3 -3
View File
@@ -3,8 +3,8 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>slhx v0 examples</title>
<script src="/slhx.js" defer></script>
<title>hemx v0 examples</title>
<script src="/hemx.js" defer></script>
<style>
body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 54rem; }
section, main { border: 1px solid #ddd; border-radius: .5rem; margin: 1rem 0; padding: 1rem; }
@@ -13,7 +13,7 @@
</style>
</head>
<body>
<h1>slhx v0 browser examples</h1>
<h1>hemx v0 browser examples</h1>
<p>Try the counter, todo form, wizard, login, page swap, and live SSE notifications.</p>
{+= self.body =+}
</body>
+3 -3
View File
@@ -1,8 +1,8 @@
<section data-slhx-root="auth">
<form data-slhx-handle="login" data-slhx-form="credentials">
<section data-hemx-root="auth">
<form data-hemx-handle="login" data-hemx-form="credentials">
<input name="email" type="email" autocomplete="username" required>
<input name="password" type="password" autocomplete="current-password" required>
<button type="submit">Sign in</button>
</form>
<p data-slhx-slot="login_status">Signed out</p>
<p data-hemx-slot="login_status">Signed out</p>
</section>
+3 -3
View File
@@ -1,4 +1,4 @@
<section data-slhx-root="counter">
<output data-slhx-slot="counter_value">0</output>
<button type="button" data-slhx-handle="increment">+</button>
<section data-hemx-root="counter">
<output data-hemx-slot="counter_value">0</output>
<button type="button" data-hemx-handle="increment">+</button>
</section>
+2 -2
View File
@@ -1,4 +1,4 @@
<section data-slhx-root="notifications" data-slhx-sse="/events">
<section data-hemx-root="notifications" data-hemx-sse="/events">
<h2>Notifications</h2>
<div data-slhx-slot="notifications">No notifications</div>
<div data-hemx-slot="notifications">No notifications</div>
</section>
+5 -5
View File
@@ -1,7 +1,7 @@
<main data-slhx-root="docs">
<nav data-slhx-slot="nav">
<a href="/docs" data-slhx-nav="">Docs</a>
<main data-hemx-root="docs">
<nav data-hemx-slot="nav">
<a href="/docs" data-hemx-nav="">Docs</a>
</nav>
<article data-slhx-slot="content">{+= self.content =+}</article>
<title data-slhx-slot="title">{+ self.title +}</title>
<article data-hemx-slot="content">{+= self.content =+}</article>
<title data-hemx-slot="title">{+ self.title +}</title>
</main>
+2 -2
View File
@@ -1,10 +1,10 @@
<li>
<span>{+ self.title +}</span>
<form data-slhx-handle="rename_todo_row">
<form data-hemx-handle="rename_todo_row">
<input type="hidden" name="id" +value="self.id">
<button type="submit" name="title" value="Renamed todo">Rename</button>
</form>
<form data-slhx-handle="delete_todo_row">
<form data-hemx-handle="delete_todo_row">
<button type="submit" name="id" +value="self.id">Delete</button>
</form>
</li>
+8 -8
View File
@@ -1,19 +1,19 @@
<section data-slhx-root="todos">
<form data-slhx-handle="add_todo" data-slhx-form="new_todo">
<section data-hemx-root="todos">
<form data-hemx-handle="add_todo" data-hemx-form="new_todo">
<input name="title" required="required">
<button type="submit">Add</button>
</form>
<p data-slhx-slot="summary">{+ self.summary +}</p>
<div data-slhx-slot="todo_list">
<ul data-slhx-slot="todo_row">
<p data-hemx-slot="summary">{+ self.summary +}</p>
<div data-hemx-slot="todo_list">
<ul data-hemx-slot="todo_row">
<template h-for="todo in &self.items" h-key="todo.id">
<li data-slhx-slot="todo_row" +data-key="todo.id">
<li data-hemx-slot="todo_row" +data-key="todo.id">
<span>{+ todo.title +}</span>
<form data-slhx-handle="rename_todo">
<form data-hemx-handle="rename_todo">
<input type="hidden" name="id" +value="todo.id">
<button type="submit" name="title" value="Renamed todo">Rename</button>
</form>
<form data-slhx-handle="delete_todo">
<form data-hemx-handle="delete_todo">
<button type="submit" name="id" +value="todo.id">Delete</button>
</form>
</li>
+3 -3
View File
@@ -1,7 +1,7 @@
<section data-slhx-root="wizard">
<form data-slhx-handle="next_step" data-slhx-form="wizard_input">
<section data-hemx-root="wizard">
<form data-hemx-handle="next_step" data-hemx-form="wizard_input">
<input type="hidden" name="step" value="1" required>
<div data-slhx-slot="wizard_step">Step 1</div>
<div data-hemx-slot="wizard_step">Step 1</div>
<button type="submit">Next</button>
</form>
</section>