Authoring guide
The whole product rests on one idea: everything is a plain file an intern could read. One screen = one HTML document. Data, scenarios, themes, flows, strings = small JSON files.
A feature
features/trip-planning/
├── feature.json { "id", "title", "icon", "description", "order" }
├── manifest.json DERIVED — never edit; the CLI/MCP regenerates it
├── flows.json guided walkthroughs (optional)
├── i18n.json strings for Engine.t (optional)
├── data/
│ ├── seed.json the feature's mock data
│ └── scenarios.json named overlays over the seed
└── screens/
├── shared.css optional feature-wide look — screens opt in per file
├── shared.js optional feature-wide helpers
└── 010-home.html one file = one screen
A screen
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Home</title>
<script type="application/json" id="screen-meta">
{
"title": "Home",
"description": "Entry point.",
"order": 10,
"device": "ios",
"status": "wip",
"tags": ["core"]
}
</script>
<script src="../../../screenbook/bridge.js"></script>
<link rel="stylesheet" href="shared.css">
</head>
<body>
<button data-go="020-detail">Open detail</button>
<script>
Engine.ready(function (data) { /* bind seed ⊕ scenario */ });
</script>
</body>
</html>
Rules that keep everything sane:
- The meta block is required (
titleat minimum).device:ios393×852 ·android412×915 ·web1280×760 ·none. Optional:devices: ["ios","android"](one screen, several frames — switcher chips appear over the stage and the self-test covers every listed device),status,tags,group(sidebar section),statusBar: "light"|"dark",updated(auto-stamped on CLI/MCP saves). - One bridge include, exactly that relative path. Style with theme tokens
(
var(--color-accent),--font-*,--radius-*,--space-*) and pad withvar(--safe-top)/var(--safe-bottom). - Screens must render sensibly with empty data — guard your bindings.
- Sheets/modals are hidden markup inside the screen that opens them.
- Duplicated markup between screens is fine — there is no templating system, on purpose.
Shared look goes to
shared.css/shared.js, included explicitly. - Numeric filename prefixes (
010-,020-) order the sidebar and leave room to insert. - CDN libraries are allowed (iframes isolate them). Zero-JS screens are allowed.
- Every screen opens standalone too (no shell): the bridge self-loads the theme + seed and
honors
?scenario=…&mode=dark&dir=rtl&lang=ar.
The bridge API (inside screens)
Engine.ready(cb) | cb(data) when tokens + data are in. |
Engine.data | frozen seed ⊕ scenario, {{today±N}} dates resolved |
Engine.state | cross-screen KV: get/set/subscribe/keys — survives navigation, reset from the shell |
Engine.go(id) / data-go | navigate — within the feature, or across with feature/screen (e.g. data-go="order/010-menu"). Literal targets are scanned into the manifest's links at regen and become the feature map's edges, so prefer literals over computed ids |
Engine.t(key, {vars}) | strings from i18n.json (see below) |
Engine.toast(msg) | transient toast |
Engine.env | { mode, dir, lang, device, scenario, theme } |
Mock data + scenarios
seed.json is the demo-complete state. scenarios.json layers named states over it:
{
"list": [
{ "id": "default", "icon": "🧪", "title": "Full demo" }, // baseline — the seed, untouched
{
"id": "new-user",
"icon": "🆕",
"title": "Brand-new user",
"remove": ["trips", "user.points"], // 1. delete these dot-paths from the seed…
"patch": { "user": { "isNew": true } } // 2. …then deep-merge this over what's left
}
]
}
remove deletes dot-paths, then patch deep-merges (arrays replace whole). Date tokens —
{{today}}, {{today+7}}, {{today-2}} — resolve to real dates at injection, so
"mid-trip today" demos truthfully forever. Model lifecycle states: new → mid → done → edge.
Themes
themes/<id>.json, flattened to CSS variables (color.ink-2 → --color-ink-2), dark as a
delta over light. Edit visually in the 🎨 Theme Lab and Save. Register extra themes in
app.config.json's themes array; the first is the default.
Strings (optional)
i18n.json, key-first — every key shows all its languages side by side:
{
"home.title": { "en": "Trips", "ar": "الرحلات" },
"home.greet": { "en": "{name}, welcome back", "ar": "يا {name}، أهلاً بعودتك" } // {name} fills from Engine.t vars
}
Engine.t('home.greet', {name}) — fallback: active language → defaultLanguage → the key.
Language switches reload the screen. Gaps show up as validate warnings, never blockers.
Flows
{
"list": [
{
"id": "pitch",
"title": "The pitch",
"icon": "✨",
"steps": [
{ "screen": "010-home", "note": "Start empty.", "scenario": "new-user" }, // a step may pin a scenario
{ "screen": "020-detail", "note": "One tap deeper." } // …the note is presenter narration
]
}
]
}
Write notes as presenter narration; 3–7 steps; end on the money shot.
After editing by hand
screenbook manifest # if you added/renamed screens
screenbook validate # the done gate
(Under serve, manifests regenerate automatically on save. MCP writes do both for you.)