# 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}
@for column in self.columns key column.id {
{column.title}
@for card in column.cards key card.id {
{card.title}
{card.assignee}
}
}
```
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
---
## 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.postcard` and generates typed constants:
```rust
pub const CARD: KeyedSlot = KeyedSlot::new(3);
pub const CREATE_CARD: Handle = Handle::new(0);
```
No string desync. No runtime mapping.
---
## 3. App State
```rust
#[slhx::app]
pub struct BoardApp {
pub board: Atom,
pub drag: Atom