zebflow 0.11.14

Reactive web automation platform — pipelines, templates, and real-time collaboration in a single binary.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
# RWE — Reactive Web Engine Specification

> **Design principle:** A developer who knows React should be able to open an RWE file and feel at home.
> No new mental model. No custom directives. No magic strings. Just TSX, imports, and standard hooks.

- from {bla, bla, bla} import "rwe" means this bla, bla, bla need to be added into the compiled

---

## Legend

| Symbol | Meaning |
|--------|---------|
| ✅ | Implemented and working |
| ⚠️ | Partially implemented — needs fix/completion |
| 🔨 | To be built now |
| 🔮 | Planned for later milestone |
| ❌ | Omitted / out of scope |

---

## 0. Complete Format Reference

### Backend (Rust)

```rust
// --- COMPILE ONCE AT STARTUP ---
insert_compiled_page(
    "home",                                               // page key
    "page.home",                                          // template id
    include_str!("demo/templates/pages/home.tsx"),        // markup source
    ReactiveWebOptions {
        processors: vec!["tailwind".to_string()],
        components: ComponentOptions {
            registry: {
                let mut m = BTreeMap::new();
                m.insert("MyComp".to_string(), include_str!("components/my-comp.tsx").to_string());
                m
            },
            strict: true,
        },
        allow_list: ResourceAllowList {
            scripts: vec!["https://cdn.example.com/lib.js".to_string()],
            ..Default::default()
        },
        load_scripts: vec!["https://cdn.example.com/lib.js".to_string()],
        ..Default::default()
    },
)?;

// --- RENDER PER REQUEST ---
async fn route_home(State(state): State<DemoAppState>) -> Html<String> {
    render_page(&state, "home", "/", json!({ "user": "mala", "count": 3 }))
        .map(Html)
        .map_err(internal_error)
}
```

### Frontend (`home.tsx`)

```tsx
import { useState, useEffect, useRef, useMemo, usePageState, useNavigate, Link } from "rwe";

export const page = {
  head: { title: "My Page" },
  navigation: "history",
};

export default function Page(input) {
  const [localVal, setLocalVal] = useState(input.count ?? 0);
  const [name, setName] = useState(input.user ?? "");
  const ref = useRef(null);
  const shared = usePageState({ count: 0 });
  const navigate = useNavigate();

  useEffect(() => {
    shared.setPageState({ count: localVal });
  }, [localVal]);

  const doubled = useMemo(() => localVal * 2, [localVal]);

  return (
    <div class="p-4 bg-zinc-900 text-white">
      <h1>Hello, {name}</h1>
      <p>Local: {localVal}</p>
      <p>Doubled: {doubled}</p>
      <p>Shared count: {shared.count}</p>
      <button onClick={() => setLocalVal(localVal + 1)}>+1</button>
      <button onClick={() => navigate("/other")}>go</button>
      <Link href="/about">About</Link>
      <div ref={ref}>tracked element</div>
    </div>
  );
}
```

---

## 1. Technical Architecture

### 1.1 The Two Worlds

RWE operates in two distinct worlds that must stay coherent:

```
┌─────────────────────────────────────────────────────────────┐
│  SERVER (Rust + embedded deno_core)                          │
│                                                              │
│  Request → compile() → render_ssr() → HTML string           │
│                                                              │
│  Embedded V8 via deno_core (singleton JsRuntime thread)     │
│  zeb_ssr_init.js loaded ONCE — installs all globals      │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│  CLIENT (Browser)                                            │
│                                                              │
│  HTML lands → <script type=module> hydrates #__rwe_root     │
│                                                              │
│  build_client_module() bakes runtime globals inline         │
│  No extra round-trip. No CDN dependency at runtime.         │
└─────────────────────────────────────────────────────────────┘
```

---

### 1.2 Compile Pipeline

```
Source .tsx file (entry page)
        │
        ▼
┌──────────────────┐
│   OXC Parser     │  Parse TSX into AST (Rust, fast)
│  (Rust, oxc)     │  Panics → saved to /tmp/rwe-parse-failed.tsx
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ ensure_default_  │  Must have `export default function Page()`
│    export()      │  Fails hard if missing
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ security::       │  Scan for forbidden patterns
│   analyze()      │  (eval, dangerous DOM access, etc.)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ collect_imports  │  Gather all import sources from OXC AST
│ validate_        │  Check against allowlist:
│   allowlist()    │    RWE internals, project aliases, and reviewed sources
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ rewrite_imports  │  @/ → absolute filesystem path
│ (alias rewrite)  │  Collects ImportEdge list (source + resolved)
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ bundle_for_      │  Recursively inline ALL local component files
│   client()       │  into ONE self-contained module.
│                  │
│                  │  Per-component text pipeline (each inlined file):
│                  │
│                  │  1. strip_local_imports()        ← runs on ORIGINAL content
│                  │       Remove `from "rwe"` and filesystem imports.
│                  │       Uses OXC AST byte spans — handles multi-line imports.
│                  │       Must run first — import paths must be visible
│                  │       (masking them would prevent correct filtering).
│                  │
│                  │  2. mask_string_literals()
│                  │       Replace all string/template literal contents
│                  │       with opaque placeholders __RWE_MASK_0__ etc.
│                  │       Compresses multiline template literals to one line.
│                  │       (e.g. `import x from y` inside a template is safe)
│                  │
│                  │  3. localize_exports()
│                  │       `export default function X` → `function X`
│                  │       `export default class X` → `class X`
│                  │       `export default Select;` → `Select;` (bare re-export)
│                  │       `export type/interface` → stripped (with multi-line
│                  │         brace-depth tracking for multi-line type defs)
│                  │       Safe: multiline template content is now masked.
│                  │
│                  │  4. prefix_module_locals()
│                  │       Auto-prefix UPPER_SNAKE_CASE module-scope constants
│                  │       with __c{n}_ per component — no collision in flat bundle
│                  │       (e.g. VARIANT_CLASSES → __c0_VARIANT_CLASSES)
│                  │       Safe: string contents are masked, so COLORS inside
│                  │       a string literal is never wrongly prefixed.
│                  │
│                  │  5. unmask_string_literals()
│                  │       Restore all __RWE_MASK_N__ → original content
│                  │
│                  │  Result: zero filesystem imports, zero "rwe" imports,
│                  │  no naming collisions, user string content untouched
└────────┬─────────┘
         │
         ▼
┌──────────────────┐
│ JSX_PRELUDE      │  Prepend `/** @jsx h */`
│  inject          │
└────────┬─────────┘
         │
         ▼
   CompiledTemplate
   ├── server_module_source  (fully bundled, for embedded deno_core SSR)
   ├── client_module_source  (fully bundled, for browser hydration)
   └── imports, diagnostics, hydrate_mode

BOTH server and client get the same fully-inlined bundle.
At render time there are NO filesystem imports and NO "rwe" imports.
Everything is compiled. Runtime globals handle the rest.
```

#### String Masking — Why It Matters

All text transforms (strip imports, localize exports, prefix constants) operate on raw
text, not AST. Without masking, a string like:

```ts
const SNIPPET = `import Button from "@/components/ui/button"`;
const SQL = "import xx from yy; select * from users";
```

would have its content incorrectly stripped or mutated by the line-based transforms.

**Masking contract:**
- Masks `"..."`, `'...'`, and `` `...` `` (template literals, including nested `${...}`)
- Placeholders: `"__RWE_MASK_0__"`, `'__RWE_MASK_1__'`, `` `__RWE_MASK_2__` ``
  (quotes preserved so surrounding syntax stays valid)
- Restored verbatim after all transforms complete
- Implementation status: ✅ implemented (`compiler.rs` — `mask_string_literals` / `unmask_string_literals`)

---

### 1.3 Render Pipeline

```
CompiledTemplate + vars (JSON)
        │
        ├──► render_ssr(server_module_source, vars)
        │         │
        │         ▼
        │    Embedded deno_core (singleton JsRuntime on dedicated thread)
        │    ├── zeb_ssr_init.js loaded ONCE at startup — installs globals:
        │    │     h, Fragment, React, createElement,
        │    │     useState, useEffect, useLayoutEffect, useInsertionEffect,
        │    │     useRef, useMemo, useCallback, useContext, useReducer, useId,
        │    │     useImperativeHandle, forwardRef, memo, createContext,
        │    │     usePageState, useNavigate, Link, cx
        │    │     __rweRenderToString, __rweWrapWithPageState
        │    ├── transpile_tsx() — OXC strips TS/JSX → plain JS (h() calls)
        │    ├── strip_rwe_imports() — remove any remaining "rwe" import lines
        │    ├── inject_page_globals() — expose default export on globalThis
        │    │     globalThis.__rwe_page = <default export name>
        │    │     globalThis.__rwe_page_config = page config (if defined)
        │    ├── globalThis.ctx = vars (inject render vars before module load)
        │    ├── load_side_es_module() — load fully-bundled module (no ext. files)
        │    │     (side module, not main — allows multiple pages per runtime)
        │    └── execute render script → op_rwe_store_result(html+config JSON)
        │
        ├──► transpile_client_cached(client_module_source)
        │         │
        │         ▼
        │    OXC transpiles TSX → JS via deno_core transpile_client()
        │    Result cached by source hash (in-memory, 256 cap LRU-ish)
        │
        └──► build_client_module(transpiled_js)
                  │
                  ▼
             Inline preamble injected (NO extra HTTP requests):
             ├── import Zeb React + hooks from embedded zeb/react assets
             ├── globalThis.h, Fragment, React, cx
             ├── globalThis.useState, useEffect, useRef, useMemo
             ├── globalThis.usePageState = __rweUsePageState
             ├── globalThis.useNavigate (calls window.rweNavigate)
             ├── globalThis.Link (intercepts click → window.rweNavigate)
             ├── window.rweNavigate — inline SPA router (installed once per page load)
             │     fetch → DOMParser → full DOM patch:
             │       #__rwe_root innerHTML swap, #__rwe_payload swap
             │       <style data-rwe-tw> swap (per-page Tailwind CSS)
             │       <link rel="stylesheet"> sync (per-page extra sheets)
             │       body.className swap, html.lang swap, document.title swap
             │       old nav scripts removed, new module scripts executed
             │     history.pushState, popstate handler, rwe:nav event
             │     Progress bar (#__rwe_nav_bar, --rwe-nav-color CSS variable)
             ├── await import(<bundle>) for each zeb/* library the page imports
             ├── markup-mounted libraries (zeb/potoru) the page does NOT import,
             │     but whose `data-zeb-lib` placeholder is in the server HTML or
             │     can be drawn by the client code: a MutationObserver that
             │     import()s the bundle the first time a placeholder is in the
             │     DOM (not awaited; never on a page without one; a project
             │     library list must include it)
             ├── base64-encode page module → data: URL import
             └── hydrate(<Page>, #__rwe_root)
```

---

### 1.4 Temp Dir & Asset Materialization

```
Binary (embedded via build.rs)
        │
        ▼
materialize_platform_template_root()
        │
        ├── Debug mode: always re-extract + delete stale files ✅
        ├── Release mode: skip if .materialized sentinel exists
        └── rewrite_platform_template_alias_imports() on all files
              @/ → absolute temp dir path in every .tsx/.ts file
```

---

### 1.5 Embedded JS Runtime

```
static JS_CHANNEL: LazyLock<UnboundedSender<JsRequest>>
```

- **Embedded deno_core** — V8 runs in-process, no external `deno` binary needed
- **Single dedicated thread** — `JsRuntime` is `!Send`, lives on `rwe-js-runtime` thread
- **Singleton runtime** — `zeb_ssr_init.js` loaded once at startup; globals persist
- **Side modules** — `load_side_es_module()` used (not main) so runtime can render multiple pages
- **Custom op** — `op_rwe_store_result(json)` delivers rendered HTML from JS→Rust via thread-local slot
- **Custom module loader** — `RweModuleLoader` resolves file:// URLs, transpiles TSX/TS on-the-fly via OXC
- Restart server = fresh runtime (important after template changes in dev)
- Client transpile: OXC in-process (Rust, no JS runtime needed)

**Runtime files:**
| File | Purpose | Used by |
|------|---------|---------|
| `runtime/zeb_ssr_init.js` | Self-contained SSR globals + renderToString | Embedded deno_core (current) |
| `runtime/zeb_react.js` | Shared first-party reconciler, hooks, and renderToString | Browser + embedded V8 |
| `runtime/zeb_react.mjs` | ESM export adapter | Browser |

---

## 2. Developer Experience Rules

### 2.1 React-Compatible Syntax

| Rule | Detail | Status |
|------|--------|--------|
| JSX in `.tsx` files | Standard JSX, PascalCase components | ✅ |
| Functional components with props | `function Page(props) { return <div/> }` | ✅ |
| `export default` page component | Required — compile fails without it | ✅ |
| Fragments `<>...</>` | Standard fragment syntax | ✅ |
| Conditional rendering `{x && <Y/>}` | Standard JSX patterns | ✅ |
| List rendering `.map((x) => <Item/>)` | Standard JSX patterns | ✅ |
| Event handlers `onClick`, `onInput`, etc. | Standard JSX events | ✅ |
| `onChange` on `<input>` / `<textarea>` | Every input event, as React (checkbox, radio, file, `<select>`: native `change`) | ✅ |
| `autoFocus` | Focused once when the element mounts after load (in place or in a portal), as React; server markup keeps the native attribute and hydration leaves focus alone | ✅ |
| Zeb React runtime | Developer imports the public API from `zeb` | ✅ |

---

### 2.2 The `"rwe"` Module — Hooks & Utilities

Everything a developer needs comes from a single import: `import { ... } from "rwe"`.

This works in **every file** — pages, components, layouts, behaviors. No exceptions.

| Export | Description | Status |
|--------|-------------|--------|
| `useState` | Local component state | ✅ |
| `useEffect` | Side effects after render | ✅ |
| `useLayoutEffect` | Synchronous layout effects (SSR no-op) | ✅ |
| `useRef` | Mutable ref to DOM element | ✅ |
| `useMemo` | Memoized computed value | ✅ |
| `useCallback` | Memoized callback function | ✅ |
| `useContext` | Consume a React-style context | ✅ |
| `useReducer` | Reducer-based state management | ✅ |
| `useSyncExternalStore` | External subscriptions with SSR/hydration snapshots; import from `zeb/react` | ✅ |
| `ErrorBoundary` | Zeb-specific fallback, retry and reset keys, including SSR; import from `zeb/react` | ✅ |
| `useId` | Stable unique ID for SSR/client matching | ✅ |
| `useImperativeHandle` | Customize ref handle (SSR no-op) | ✅ |
| `createContext` | Create a React-style context | ✅ |
| `forwardRef` | Forward refs through components | ✅ |
| `memo` | Memoize component (identity passthrough in SSR) | ✅ |
| `usePageState` | Shared state across all components on the same page | ✅ |
| `useNavigate` | SPA navigation hook — `const nav = useNavigate(); nav("/path")` | ✅ |
| `Link` | Router-aware anchor — `<Link href="/path">Go</Link>` | ✅ |
| `cx` | Class name utility — `cx("base", condition && "extra", className)` | ✅ |

**How `"rwe"` imports work — compile-time signal:**

`import { cx, useState } from "rwe"` is a **signal to the compiler**, not a real module import.

- At **compile time**: `bundle_for_client()` calls `strip_local_imports()` which strips all `from "rwe"` lines from every inlined component. They never reach the runtime.
- At **runtime**: all exported symbols are already installed as `globalThis.*` by the runtime — `zeb_ssr_init.js` on the server, `build_client_module()` preamble on the client.
- **Type definitions**: `rwe.d.ts` + `tsconfig.json` path mapping — planned, enables IDE autocomplete. Not required for runtime to work.

This pattern works in **every file** — pages, components, layouts. No exceptions.

---

### 2.3 Navigation

| Rule | Detail | Status |
|------|--------|--------|
| SPA navigation via `useNavigate()` | `const nav = useNavigate(); nav("/projects/x")` | ✅ |
| `<Link href="...">` component | Intercepts click → `window.rweNavigate(href)` | ✅ |
| Inline SPA router in every page | `window.rweNavigate` baked into `build_client_module()` — single source of truth, no extra files | ✅ |
| `history.pushState` URL updates | Browser URL bar reflects current page without reload | ✅ |
| Back/forward browser buttons | `popstate` handler re-runs `window.rweNavigate` | ✅ |
| Per-page Tailwind CSS swap | `<style data-rwe-tw>` removed/re-added on every navigation — selector matches server injection | ✅ |
| `<link rel="stylesheet">` sync | Per-page extra sheets (db-suite, devicons) added/removed on navigation | ✅ |
| `body.className` swap | Dark/light layout class updated on every navigation | ✅ |
| `html.lang` swap | Language attribute updated on navigation | ✅ |
| Page title update on navigation | `document.title` set from fetched page | ✅ |
| `rwe:nav` event | Dispatched after swap completes — for analytics, behavior re-init | ✅ |
| Progress bar | Thin top bar, `#__rwe_nav_bar`, color via `--rwe-nav-color` CSS var | ✅ |
| `rwe_router.js` | ❌ Removed — was a legacy shell-specific router, fully replaced by inline system | ❌ deleted |
| Direct `window.location.href` | ❌ Never — raw DOM access forbidden in TSX/TS source | ❌ omit |
| `window.rweNavigate` in user code | ❌ Internal only — use `useNavigate()` or `<Link>` | ❌ omit |

---

### 2.4 File Modularity

| Rule | Detail | Status |
|------|--------|--------|
| `import X from "@/components/ui/button"` | `@/` resolves to template root, works in all files | ✅ |
| `import X from "@/pages/home/components/chrome-header"` | Nested imports cascade correctly | ✅ |
| No relative `../` imports | `../../foo` is forbidden — use `@/` always | ✅ enforced |
| `import X from "npm:somelib"` | Direct package ingestion is not a project dependency contract | 🔮 future |
| `import X from "jsr:somelib"` | Direct registry ingestion is not a project dependency contract | 🔮 future |
| Component dependency graph / cache | Hash-based cache per source file | 🔮 M1 |
| Cycle detection in imports | Detect and error on circular deps | 🔮 M1 |

---

### 2.5 Import Allowlist (Sandbox)

Developers can only import from these sources. Everything else is blocked at compile time.

| Source | Example | Status |
|--------|---------|--------|
| `"rwe"` | `import { useState } from "rwe"` | ✅ |
| `"npm:*"` | Reserved for RWE internals; project package ingestion is unsupported | 🔮 future |
| `"node:*"` | `import { Buffer } from "node:buffer"` | ✅ |
| `"jsr:*"` | Reserved for future reviewed registry ingestion | 🔮 future |
| `"@/*"` | `import X from "@/components/ui/x"` | ✅ |
| Custom allowlist prefixes | Configured per project security policy | ✅ |
| Arbitrary HTTPS URLs | `import x from "https://evil.com/x"` ❌ blocked | ✅ blocked |
| Relative paths `./` `../` | ❌ blocked — use `@/` | ✅ blocked |
| fetch() domain allowlist | Control which domains page JS can call | ✅ |

---

### 2.6 Tailwind

| Rule | Detail | Status |
|------|--------|--------|
| Standard Tailwind class syntax | `className="flex items-center gap-2 text-sm"` | ✅ |
| Dynamic classes via `tw-variants` attribute | Declare dynamic class strings for compiler to pick up | ✅ |
| Per-page CSS compilation | Each page compiles only its own classes | ✅ |
| Custom CSS in `styles/main.css` | Global styles via `zf-*` class convention | ✅ |
| Tailwind plugins | Extended utilities (e.g. scrollbar) | 🔮 |

**`tw-variants` usage:**
```tsx
<div
  tw-variants="bg-red-500 bg-green-500 bg-yellow-500"
  className={`bg-${status}-500`}
/>
```
Tells the compiler to include those classes even though they're assembled dynamically.

---

### 2.7 Server / Client Contract

| Rule | Detail | Status |
|------|--------|--------|
| `export const page = {}` | Page config (head, body, navigation mode) | ✅ |
| `export default function Page(props)` | Props come from server render vars | ✅ |
| SSR-first — all pages render on server | First response is full HTML, SEO-friendly | ✅ |
| Hydration payload via `#__rwe_payload` | JSON injected into page for client hydration — the render state **without the request**: no `headers`, cookies, or trigger envelope `headers` / `cookies` / `auth` (`core/page_state.rs`; SSR still reads them) | ✅ |
| `server` / `client` script namespace | Explicit server data vs client state split | 🔮 M2 |
| `expose` list for hydration payload | Control what server data reaches the client | 🔮 M2 |

---

### 2.8 Hydration

| Rule | Detail | Status |
|------|--------|--------|
| Full page hydration (default) | Entire `#__rwe_root` hydrated on load | ✅ |
| `hydrate="off"` — pure SSR block | No JS cost for static regions | 🔮 M4 |
| `hydrate="visible"` — lazy on scroll | Mount only when element enters viewport | 🔮 M4 |
| `hydrate="interaction"` — on first touch | Mount on first click/keypress | 🔮 M4 |
| `hydrate="idle"` — on browser idle | Mount during idle callback | 🔮 M4 |
| No extra `<script>` tags needed | Runtime baked into single inline module | ✅ |

---

### 2.9 Page Config

Defined by exporting from the page file:

```ts
export const page = {
  head: {
    title: ctx?.seo?.title ?? "",           // JS expression — resolved at module eval time
    description: ctx?.seo?.description ?? "",
  },
  html: {
    lang: "en",
  },
  body: {
    className: "h-screen bg-slate-950 text-white font-sans",
  },
  render: "ssr",        // "ssr" | "ssg" (future) | "client" (future)
  navigation: "history" // "history" (SPA) | "document" (full reload)
};
```

`globalThis.ctx` is injected by the RWE engine **before** the module loads, so `export const page` expressions can reference `ctx` directly as standard JavaScript. The `build_document_shell()` function in `render.rs` reads the already-resolved config and generates the full `<!DOCTYPE html>` wrapper with `<meta charset>`, viewport, title, description, body class, and lang attribute.

| Config | Status |
|--------|--------|
| `render: "ssr"` | ✅ default |
| `render: "ssg"` | 🔮 M5 |
| `render: "client"` | 🔮 M5 |
| `navigation: "history"` | ✅ default (inline SPA router in every page) |
| `navigation: "document"` | ✅ fallback available |

---

### 2.10 JavaScript Sandbox

| Rule | Detail | Status |
|------|--------|--------|
| Embedded deno_core V8 | SSR runs in embedded V8, no external process — restricted by default | ✅ |
| Per-render timeout | Configurable `deno_timeout_ms` | ✅ |
| No arbitrary FS access in templates | Deno permissions restrict file system | ✅ |
| No arbitrary net access in templates | fetch() to unlisted domains blocked at compile time | ✅ |
| `eval()` blocked | Security scanner rejects eval | ✅ |
| Direct DOM manipulation blocked | `document.querySelector` etc. forbidden in TSX | ✅ enforced by convention |

---

## 3. Folder Convention

```
templates/
  pages/           ← Entry points (one per route)
  components/
    ui/            ← Reusable UI components (design system)
    layout/        ← Shell/layout wrappers (wrap pages)
    behavior/      ← Pure TS behavior files (no JSX, DOM wiring)
  styles/
    main.css       ← Global CSS, zf-* custom classes
```

### Rules

| Rule | Detail |
|------|--------|
| Pages are entry points | One `pages/*.tsx` per route. Defines the page root. |
| UI components are stateless-first | Prefer pure render — no behavior wiring |
| Layout components wrap pages | Platform studio: `pages/project-studio/components/shell.tsx` wraps project-studio routes |
| Behavior files are pure `.ts` | No JSX, no render(). Wire DOM events, export init functions. |
| Shared reactive state | Goes in layout (entry page context) via `usePageState` |
| Components that need hooks | Can live anywhere — `"rwe"` import is a compile-time signal, stripped during bundling |
| Module-scope constants are auto-scoped | The bundler auto-prefixes `UPPER_SNAKE_CASE` constants per component (`__c0_VARIANT_CLASSES`, `__c1_VARIANT_CLASSES`) using word-boundary replacement. Developers write clean names — no manual prefixing needed. |

---

## 4. Build History

| # | Task | Status |
|---|------|--------|
| 1 | Fix `"rwe"` import in ALL files — shim via `prepare_template_root()` | ✅ Done |
| 2 | `useNavigate()` hook — SSR no-op, client uses `rwe_router.js` | ✅ Done |
| 3 | `<Link>` component — SSR `<a>`, client intercepts click via history API | ✅ Done |
| 4 | `fetch()` domain allowlist in `security::analyze()` | ✅ Done |
| 5 | Inline SPA router in `build_client_module()` — `history.pushState`, fetch, DOM swap, `popstate` | ✅ Done |
| 6 | CSS swap on navigation — `data-rwe-page-css` attribute + `[data-rwe-page-css]` swap in router | ✅ Done |
| 7 | Progress bar — `#__rwe_nav_bar`, `--rwe-nav-color` CSS variable, `rwe:nav-start` event | ✅ Done |
| 8 | Showcase demo: `/` (all hooks), `/blog` (search+filter), `/todo` (full app) at port 8787 | ✅ Done |

---

## 5. Navigation Loading Indicator

The inline SPA router ships a thin progress bar by default. Three tiers of customisation — no API changes needed between tiers.

### Tier 1 — CSS only (zero JS)

```css
/* hide entirely */
#__rwe_nav_bar { display: none; }

/* recolor */
:root { --rwe-nav-color: #6366f1; }

/* thicker */
#__rwe_nav_bar { height: 4px !important; }
```

### Tier 2 — `window.rweNavProgress` hook

Define this **before** the RWE module script executes. The default bar is never created; your callbacks are called instead.

```js
window.rweNavProgress = {
  start() { /* show spinner, skeleton, overlay */ },
  done()  { /* hide */  },
  fail()  { /* hide + optional error state */ },
};
```

### Tier 3 — Events (passive, no takeover)

Both events fire regardless of which tier is active.

| Event | When fired | `detail` |
|-------|-----------|---------|
| `rwe:nav-start` | Immediately before `fetch()` | `{ url }` |
| `rwe:nav` | After DOM swap + re-hydration | `{ url }` |

---

## 6. Non-Goals

| Item | Reason |
|------|--------|
| Full React compatibility layer | Zeb React implements the RWE function-component contract; concurrent rendering, Suspense and React package compatibility are outside this contract |
| Plugin API | Too early — core model not stable yet |
| Static site export (`build --ssg`) | Later milestone — don't confuse with SSR runtime |
| Class components | Functional only |
| `npm:react` or `npm:react-dom` imports | Forbidden — use `"rwe"` |