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:
+35
-35
@@ -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.
|
||||
```
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
fn main() {
|
||||
slhx_build::app().run().unwrap();
|
||||
hemx_build::app().run().unwrap();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
fn main() {
|
||||
slhx_build::app().run().unwrap();
|
||||
hemx_build::app().run().unwrap();
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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" }
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
fn main() {
|
||||
slhx_build::app().run().unwrap();
|
||||
hemx_build::app().run().unwrap();
|
||||
}
|
||||
|
||||
+14
-14
@@ -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
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user