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`
```html
<section data-slhx-slot="board" data-slhx-atom="board">
<header>
<h1>{self.title}</h1>
<section data-slhx-root="board" data-slhx-slot="board" data-slhx-atom="board">
<header>
<h1>{+ self.title +}</h1>
<form data-slhx-handle="create_card">
<input name="title" type="text" required>
<select name="column">
@for column in self.columns key column.id {
<option value="{column.id}">{column.title}</option>
}
</select>
<button>Add card</button>
</form>
</header>
<form data-slhx-handle="create_card" data-slhx-form="create_card">
<input name="title" type="text" required>
<select name="column">
<template h-for="column in &self.columns" h-key="column.id">
<option +value="column.id">{+ column.title +}</option>
</template>
</select>
<button>Add card</button>
</form>
</header>
<div class="columns" data-slhx-slot="columns">
@for column in self.columns key column.id {
<section
class="column"
data-slhx-slot="column"
data-column-id="{column.id}"
>
<h2>{column.title}</h2>
<div class="columns" data-slhx-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">
<h2>{+ column.title +}</h2>
<div
class="cards"
data-slhx-dropzone="column"
data-column-id="{column.id}"
>
@for card in column.cards key card.id {
<article
class="card"
data-slhx-slot="card"
data-slhx-handle="drag_card"
data-card-id="{card.id}"
draggable="true"
>
<strong>{card.title}</strong>
<small>{card.assignee}</small>
</article>
}
</div>
</section>
}
</div>
<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">
<strong>{+ card.title +}</strong>
<small>{+ card.assignee +}</small>
</article>
</template>
</div>
</section>
</template>
</div>
<aside data-slhx-slot="presence">
@for user in self.online_users key user.id {
<span>{user.name}</span>
}
</aside>
<aside data-slhx-slot="presence">
<template h-for="user in &self.online_users" h-key="user.id">
<span>{+ user.name +}</span>
</template>
</aside>
</section>
```
Notes on keyed scopes:
- `@for column key column.id`**required** for slhx-addressable nodes inside
- `@for card key card.id`**required**
- A slot inside a keyed loop is addressed as `(SlotId, KeyValue)`, never `querySelector(".card:nth-child(3)")`
- Without `key`, slhx rejects the build — no runtime selector fallback
- `h-for="column in &self.columns" h-key="column.id"`**required** for slhx-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 `querySelector(".card:nth-child(3)")`
- 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
pub const CARD: KeyedSlot<CardId, CardView> = KeyedSlot::new(3);
pub const CREATE_CARD: Handle<CreateCardForm> = Handle::new(0);
use ui::board::{forms, slots};
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()));
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
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,
}));
// Client-local extension APIs stay typed by generated resources;
// names below are illustrative until slhx-sync lands.
Effect::batch((
Effect::class_keyed(slots::CARD, event.card_id, "dragging", true),
Effect::transform_keyed(
slots::CARD, event.card_id,
slots::CARD,
event.card_id,
Transform::translate(event.x, event.y),
),
))
@@ -200,10 +193,14 @@ 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.
Effect::batch((
Effect::move_keyed(
slots::CARD, event.card_id,
slots::COLUMN, event.to_column,
slots::CARD,
event.card_id,
slots::COLUMN,
event.to_column,
InsertBefore(event.before_card),
),
Effect::class_keyed(slots::CARD, event.card_id, "dragging", false),
@@ -236,14 +233,14 @@ pub fn apply_board_patch(
Effect::broadcast(
Channel::Board(app.board.id()),
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((
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
#[slhx_sync::presence]
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` |
| 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 | `KeydSlot<T, K>` compile-time |
| Forms | React Hook Form | HTML native, but no validation bridge | `Fork<T>` derived from `.heml` surface |
| 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 |