docs(kanban): show real hemplate composition syntax

Refresh the north-star kanban walkthrough to use current hemplate syntax plus lean generated render/slot/form helpers instead of stale sketch syntax and manual ids.

req: dx/006

req: component/003
This commit is contained in:
slhx agent
2026-06-01 23:27:34 +02:00
parent 60eed5f98f
commit 6e55d9a46f
+64 -67
View File
@@ -11,66 +11,52 @@ This is the north-star integration test for slhx + hemplate + slhx-sync.
## 1. Template: `board.heml` ## 1. Template: `board.heml`
```html ```html
<section data-slhx-slot="board" data-slhx-atom="board"> <section data-slhx-root="board" data-slhx-slot="board" data-slhx-atom="board">
<header> <header>
<h1>{self.title}</h1> <h1>{+ self.title +}</h1>
<form data-slhx-handle="create_card"> <form data-slhx-handle="create_card" data-slhx-form="create_card">
<input name="title" type="text" required> <input name="title" type="text" required>
<select name="column"> <select name="column">
@for column in self.columns key column.id { <template h-for="column in &self.columns" h-key="column.id">
<option value="{column.id}">{column.title}</option> <option +value="column.id">{+ column.title +}</option>
} </template>
</select> </select>
<button>Add card</button> <button>Add card</button>
</form> </form>
</header> </header>
<div class="columns" data-slhx-slot="columns"> <div class="columns" data-slhx-slot="columns">
@for column in self.columns key column.id { <template h-for="column in &self.columns" h-key="column.id">
<section <section class="column" data-slhx-slot="column" +data-column-id="column.id">
class="column" <h2>{+ column.title +}</h2>
data-slhx-slot="column"
data-column-id="{column.id}"
>
<h2>{column.title}</h2>
<div <div class="cards" +data-column-id="column.id">
class="cards" <template h-for="card in &column.cards" h-key="card.id">
data-slhx-dropzone="column" <article class="card" data-slhx-slot="card" data-slhx-handle="drag_card" +data-card-id="card.id" draggable="true">
data-column-id="{column.id}" <strong>{+ card.title +}</strong>
> <small>{+ card.assignee +}</small>
@for card in column.cards key card.id { </article>
<article </template>
class="card" </div>
data-slhx-slot="card" </section>
data-slhx-handle="drag_card" </template>
data-card-id="{card.id}" </div>
draggable="true"
>
<strong>{card.title}</strong>
<small>{card.assignee}</small>
</article>
}
</div>
</section>
}
</div>
<aside data-slhx-slot="presence"> <aside data-slhx-slot="presence">
@for user in self.online_users key user.id { <template h-for="user in &self.online_users" h-key="user.id">
<span>{user.name}</span> <span>{+ user.name +}</span>
} </template>
</aside> </aside>
</section> </section>
``` ```
Notes on keyed scopes: Notes on keyed scopes:
- `@for column key column.id`**required** for slhx-addressable nodes inside - `h-for="column in &self.columns" h-key="column.id"`**required** for slhx-addressable nodes inside
- `@for card key card.id`**required** - `h-for="card in &column.cards" h-key="card.id"`**required**
- A slot inside a keyed loop is addressed as `(SlotId, KeyValue)`, never `querySelector(".card:nth-child(3)")` - A slot inside a keyed loop is addressed as a generated keyed resource, never `querySelector(".card:nth-child(3)")`
- Without `key`, slhx rejects the build — no runtime selector fallback - Without `h-key`, slhx rejects the build — no runtime selector fallback
--- ---
@@ -101,14 +87,17 @@ FormSurface {
} }
``` ```
slhx reads this from `hemplate.surface.postcard` and generates typed constants: slhx reads this from hemplate Surface facts and generates scoped typed resources:
```rust ```rust
pub const CARD: KeyedSlot<CardId, CardView> = KeyedSlot::new(3); use ui::board::{forms, slots};
pub const CREATE_CARD: Handle<CreateCardForm> = Handle::new(0);
slots::card.replace(card.id, CardView::from(card));
forms::create_card.clear("title");
ui::render(&BoardView::from(board));
``` ```
No string desync. No runtime mapping. No string desync. No manual ids. The generated module owns the names.
--- ---
@@ -145,11 +134,12 @@ pub fn create_card(
app.board.update(|board| board.insert_card(form.column, card.clone())); app.board.update(|board| board.insert_card(form.column, card.clone()));
Effect::batch(( (
Effect::append_keyed(slots::CARD, card.id, CardView::from(card)), slots::card.append(card.id, &CardView::from(card)),
forms::create_card.clear("title"),
// slhx-sync: queue atomic board state diff for sync // slhx-sync: queue atomic board state diff for sync
SyncEffect::send_patch(atoms::BOARD, Patch::insert_card(form.column, card)), SyncEffect::send_patch(atoms::board, Patch::insert_card(form.column, card)),
)) )
} }
``` ```
@@ -172,10 +162,13 @@ pub fn drag_card(
pointer_y: event.y, pointer_y: event.y,
})); }));
// Client-local extension APIs stay typed by generated resources;
// names below are illustrative until slhx-sync lands.
Effect::batch(( Effect::batch((
Effect::class_keyed(slots::CARD, event.card_id, "dragging", true), Effect::class_keyed(slots::CARD, event.card_id, "dragging", true),
Effect::transform_keyed( Effect::transform_keyed(
slots::CARD, event.card_id, slots::CARD,
event.card_id,
Transform::translate(event.x, event.y), Transform::translate(event.x, event.y),
), ),
)) ))
@@ -200,10 +193,14 @@ pub fn drop_card(
app.drag.set(None); app.drag.set(None);
// Client-local extension APIs stay typed by generated resources;
// names below are illustrative until slhx-sync lands.
Effect::batch(( Effect::batch((
Effect::move_keyed( Effect::move_keyed(
slots::CARD, event.card_id, slots::CARD,
slots::COLUMN, event.to_column, event.card_id,
slots::COLUMN,
event.to_column,
InsertBefore(event.before_card), InsertBefore(event.before_card),
), ),
Effect::class_keyed(slots::CARD, event.card_id, "dragging", false), Effect::class_keyed(slots::CARD, event.card_id, "dragging", false),
@@ -236,14 +233,14 @@ pub fn apply_board_patch(
Effect::broadcast( Effect::broadcast(
Channel::Board(app.board.id()), Channel::Board(app.board.id()),
Effect::batch(changed_cards.into_iter().map(|c| Effect::batch(changed_cards.into_iter().map(|c|
Effect::replace_keyed(slots::CARD, c.id, CardView::from(c)) slots::card.replace(c.id, &CardView::from(c))
)), )),
), ),
)), )),
PatchResult::Conflict { canonical_board } => Effect::batch(( PatchResult::Conflict { canonical_board } => Effect::batch((
Effect::set(atoms::BOARD, canonical_board.clone()), Effect::set(atoms::BOARD, canonical_board.clone()),
Effect::render(slots::BOARD, BoardView::from(canonical_board)), slots::board.render(&BoardView::from(canonical_board)),
)), )),
} }
} }
@@ -258,7 +255,7 @@ Server-authoritative on conflict. No Redux sagas. No React Query cache fades.
```rust ```rust
#[slhx_sync::presence] #[slhx_sync::presence]
pub fn user_joined(user: UserPresence) -> impl IntoEffect { pub fn user_joined(user: UserPresence) -> impl IntoEffect {
Effect::append_keyed(slots::PRESENCE_USER, user.id, PresenceBadge::from(user)) slots::presence_user.append(user.id, &PresenceBadge::from(user))
} }
``` ```
@@ -315,8 +312,8 @@ No framework download. No VDOM. No hydration. No game loop.
| Offline support | Service Worker + custom | impossible | patch queue in `slhx-sync` | | Offline support | Service Worker + custom | impossible | patch queue in `slhx-sync` |
| Conflict resolution | manual / Yjs CRDT | impossible | server-authoritative patch | | Conflict resolution | manual / Yjs CRDT | impossible | server-authoritative patch |
| Presence | WebSocket + custom state | SSE possible | `Effect::broadcast` over channel | | Presence | WebSocket + custom state | SSE possible | `Effect::broadcast` over channel |
| Keyed DOM | React key | not a concern | `KeydSlot<T, K>` compile-time | | Keyed DOM | React key | not a concern | `KeyedSlot<K, T>` compile-time |
| Forms | React Hook Form | HTML native, but no validation bridge | `Fork<T>` derived from `.heml` surface | | 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 | | 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 slhx.js interpreter |