# Milestone: Local-first Multiplayer Kanban
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 the north-star integration test for slhx + hemplate + slhx-sync.
---
## 1. Template: `board.heml`
```html
{+ self.title +}
{+ column.title +}
{+ card.title +}
{+ card.assignee +}
```
Notes on keyed scopes:
- `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
---
## 2. What hemplate exports
hemplate does **not** interpret `data-slhx-*`. It records raw facts:
```rust
Node {
id: NodeId(12),
element: "article",
attrs: [
("class", "card"),
("data-slhx-slot", "card"),
("data-slhx-handle", "drag_card"),
("data-card-id", "{card.id}"),
("draggable", "true"),
],
scope: ScopeId(For { binding: "card", key_expr: "card.id" }),
}
FormSurface {
handle_attr: Some("create_card"),
controls: [
Control { name: "title", kind: Text, required: true },
Control { name: "column", kind: Select, required: true },
],
}
```
slhx reads this from hemplate Surface facts and generates scoped typed resources:
```rust
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 manual ids. The generated module owns the names.
---
## 3. App State
```rust
#[slhx::app]
pub struct BoardApp {
pub board: Atom,
pub drag: Atom