@cs4alhaider/screenbook 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CLAUDE.md +185 -0
  2. package/LICENSE +21 -0
  3. package/README.md +208 -0
  4. package/TESTING.md +54 -0
  5. package/app-bridge.js +131 -0
  6. package/bridge.js +598 -0
  7. package/cli/screenbook.js +290 -0
  8. package/cli/templates.js +139 -0
  9. package/core/browser.js +108 -0
  10. package/core/fsx.js +71 -0
  11. package/core/meta.js +94 -0
  12. package/core/project.js +482 -0
  13. package/core/serve.js +224 -0
  14. package/docs/README.md +21 -0
  15. package/docs/app-mode-guide.md +79 -0
  16. package/docs/authoring-guide.md +129 -0
  17. package/docs/cli.md +78 -0
  18. package/docs/getting-started.md +88 -0
  19. package/docs/mcp-agents.md +60 -0
  20. package/docs/shots/android-dark.png +0 -0
  21. package/docs/shots/app-mode.png +0 -0
  22. package/docs/shots/hero-studio.png +0 -0
  23. package/docs/shots/rtl-arabic.png +0 -0
  24. package/docs/shots/theme-lab.png +0 -0
  25. package/docs/shots/theming-noir.png +0 -0
  26. package/docs/shots/web-dashboard.png +0 -0
  27. package/docs/studio-guide.md +64 -0
  28. package/docs/troubleshooting.md +55 -0
  29. package/examples/qahwa/app.config.json +9 -0
  30. package/examples/qahwa/features/order/data/scenarios.json +25 -0
  31. package/examples/qahwa/features/order/data/seed.json +21 -0
  32. package/examples/qahwa/features/order/feature.json +7 -0
  33. package/examples/qahwa/features/order/flows.json +16 -0
  34. package/examples/qahwa/features/order/manifest.json +78 -0
  35. package/examples/qahwa/features/order/screens/010-menu.html +121 -0
  36. package/examples/qahwa/features/order/screens/020-drink.html +120 -0
  37. package/examples/qahwa/features/order/screens/030-cart.html +99 -0
  38. package/examples/qahwa/features/order/screens/040-status.html +125 -0
  39. package/examples/qahwa/features/order/screens/050-status-android.html +95 -0
  40. package/examples/qahwa/features/order/screens/shared.css +146 -0
  41. package/examples/qahwa/features/roastery/data/scenarios.json +35 -0
  42. package/examples/qahwa/features/roastery/data/seed.json +17 -0
  43. package/examples/qahwa/features/roastery/feature.json +7 -0
  44. package/examples/qahwa/features/roastery/flows.json +14 -0
  45. package/examples/qahwa/features/roastery/manifest.json +32 -0
  46. package/examples/qahwa/features/roastery/screens/010-orders.html +103 -0
  47. package/examples/qahwa/features/roastery/screens/020-menu-editor.html +88 -0
  48. package/examples/qahwa/features/roastery/screens/shared.css +115 -0
  49. package/examples/qahwa/review/comments.json +3 -0
  50. package/examples/qahwa/themes/qahwa.json +43 -0
  51. package/examples/spa-demo/app/index.html +100 -0
  52. package/examples/spa-demo/app.config.json +9 -0
  53. package/examples/spa-demo/features/notes-app/feature.json +11 -0
  54. package/examples/spa-demo/review/comments.json +3 -0
  55. package/examples/spa-demo/themes/default.json +15 -0
  56. package/examples/tokens-lab/app.config.json +9 -0
  57. package/examples/tokens-lab/features/lab/data/scenarios.json +5 -0
  58. package/examples/tokens-lab/features/lab/data/seed.json +3 -0
  59. package/examples/tokens-lab/features/lab/feature.json +7 -0
  60. package/examples/tokens-lab/features/lab/flows.json +14 -0
  61. package/examples/tokens-lab/features/lab/manifest.json +33 -0
  62. package/examples/tokens-lab/features/lab/screens/010-swatches.html +112 -0
  63. package/examples/tokens-lab/features/lab/screens/020-typography.html +115 -0
  64. package/examples/tokens-lab/review/comments.json +3 -0
  65. package/examples/tokens-lab/themes/noir.json +32 -0
  66. package/examples/tokens-lab/themes/paper.json +32 -0
  67. package/index.html +50 -0
  68. package/mcp/server.js +366 -0
  69. package/package.json +57 -0
  70. package/shell/flowplayer.js +85 -0
  71. package/shell/frames.js +108 -0
  72. package/shell/main.js +397 -0
  73. package/shell/net.js +126 -0
  74. package/shell/review.js +213 -0
  75. package/shell/screenhost.js +380 -0
  76. package/shell/selftest.js +275 -0
  77. package/shell/shell.css +952 -0
  78. package/shell/sidebar.js +112 -0
  79. package/shell/store.js +119 -0
  80. package/shell/themelab.js +186 -0
  81. package/shell/toast.js +15 -0
  82. package/shell/toolbar.js +128 -0
  83. package/skill/SKILL.md +52 -0
  84. package/skill/references/app-mode.md +77 -0
  85. package/skill/references/bridge-api.md +55 -0
  86. package/skill/references/cli-mcp.md +53 -0
  87. package/skill/references/flows.md +44 -0
  88. package/skill/references/i18n.md +52 -0
  89. package/skill/references/scenarios.md +45 -0
  90. package/skill/references/screen-contract.md +83 -0
  91. package/skill/references/tokens.md +56 -0
package/core/serve.js ADDED
@@ -0,0 +1,224 @@
1
+ /* Static server for ScreenBook projects (EN-5, server-first).
2
+ - serves the project root, with /engine/* mounted from the engine package
3
+ (so examples and engine-less checkouts still work)
4
+ - no-cache headers everywhere — the ?v=N ritual dies here
5
+ - /__sb/health, /__sb/events (SSE live reload), /__sb/review/comments
6
+ - watches the project, auto-regenerates manifests, broadcasts changes */
7
+
8
+ import http from 'node:http';
9
+ import path from 'node:path';
10
+ import fs from 'node:fs';
11
+ import fsp from 'node:fs/promises';
12
+ import { readJSON, writeJSON } from './fsx.js';
13
+ import { regenManifest } from './project.js';
14
+
15
+ const MIME = {
16
+ '.html': 'text/html; charset=utf-8',
17
+ '.js': 'text/javascript; charset=utf-8',
18
+ '.mjs': 'text/javascript; charset=utf-8',
19
+ '.css': 'text/css; charset=utf-8',
20
+ '.json': 'application/json; charset=utf-8',
21
+ '.svg': 'image/svg+xml',
22
+ '.png': 'image/png',
23
+ '.jpg': 'image/jpeg',
24
+ '.jpeg': 'image/jpeg',
25
+ '.webp': 'image/webp',
26
+ '.gif': 'image/gif',
27
+ '.ico': 'image/x-icon',
28
+ '.woff2': 'font/woff2',
29
+ '.woff': 'font/woff',
30
+ '.txt': 'text/plain; charset=utf-8',
31
+ '.md': 'text/markdown; charset=utf-8',
32
+ '.map': 'application/json'
33
+ };
34
+
35
+ function send(res, status, body, headers = {}) {
36
+ res.writeHead(status, { 'Cache-Control': 'no-store', ...headers });
37
+ res.end(body);
38
+ }
39
+
40
+ async function serveFile(res, absPath) {
41
+ try {
42
+ const data = await fsp.readFile(absPath);
43
+ send(res, 200, data, { 'Content-Type': MIME[path.extname(absPath).toLowerCase()] || 'application/octet-stream' });
44
+ } catch (err) {
45
+ if (err.code === 'ENOENT' || err.code === 'EISDIR') send(res, 404, `Not found`);
46
+ else send(res, 500, `Read error: ${err.message}`);
47
+ }
48
+ }
49
+
50
+ /* Resolve a URL path inside a base dir; null on traversal attempts. */
51
+ function safeJoin(base, urlPath) {
52
+ const decoded = decodeURIComponent(urlPath).replace(/\0/g, '');
53
+ const abs = path.normalize(path.join(base, decoded));
54
+ return abs.startsWith(path.normalize(base + path.sep)) || abs === path.normalize(base) ? abs : null;
55
+ }
56
+
57
+ function readBody(req, limit = 5 * 1024 * 1024) {
58
+ return new Promise((resolve, reject) => {
59
+ let size = 0;
60
+ const chunks = [];
61
+ req.on('data', (c) => {
62
+ size += c.length;
63
+ if (size > limit) { reject(new Error('body too large')); req.destroy(); }
64
+ else chunks.push(c);
65
+ });
66
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
67
+ req.on('error', reject);
68
+ });
69
+ }
70
+
71
+ /* Map a changed file to a live-reload event, or null to ignore. */
72
+ function classify(rel) {
73
+ const p = rel.split(path.sep).join('/');
74
+ if (p.includes('node_modules/') || p.includes('.git/') || /\.tmp-/.test(p)) return null;
75
+ let m;
76
+ if ((m = /^features\/([^/]+)\/screens\/([^/]+)\.html$/.exec(p))) {
77
+ return { area: 'screens', feature: m[1], screen: m[2] };
78
+ }
79
+ if ((m = /^features\/([^/]+)\/screens\//.exec(p))) return { area: 'screens', feature: m[1] };
80
+ if ((m = /^features\/([^/]+)\/driver\.js$/.exec(p))) return { area: 'data', feature: m[1] };
81
+ if ((m = /^features\/([^/]+)\/data\//.exec(p))) return { area: 'data', feature: m[1] };
82
+ if ((m = /^features\/([^/]+)\/flows\.json$/.exec(p))) return { area: 'flows', feature: m[1] };
83
+ if ((m = /^features\/([^/]+)\/(feature|manifest)\.json$/.exec(p))) return { area: 'manifest', feature: m[1] };
84
+ if (/^themes\//.test(p)) return { area: 'theme' };
85
+ if (p === 'app.config.json') return { area: 'config' };
86
+ if (/^engine\//.test(p) && !/^engine\/(node_modules|examples)\//.test(p)) return { area: 'engine' };
87
+ return null;
88
+ }
89
+
90
+ export function startServer({ root, engineDir, port = 4600, quiet = false }) {
91
+ const clients = new Set();
92
+ const log = quiet ? () => {} : (...a) => console.log(...a);
93
+
94
+ const server = http.createServer(async (req, res) => {
95
+ const url = new URL(req.url, 'http://x');
96
+ const p = url.pathname;
97
+
98
+ try {
99
+ if (p === '/' || p === '/index.html') {
100
+ send(res, 302, '', { Location: '/engine/index.html' });
101
+ return;
102
+ }
103
+
104
+ if (p === '/__sb/health') {
105
+ send(res, 200, JSON.stringify({ screenbook: true, version: '0.1.0', root: path.basename(root) }),
106
+ { 'Content-Type': 'application/json' });
107
+ return;
108
+ }
109
+
110
+ if (p === '/__sb/events') {
111
+ res.writeHead(200, {
112
+ 'Content-Type': 'text/event-stream',
113
+ 'Cache-Control': 'no-store',
114
+ Connection: 'keep-alive'
115
+ });
116
+ res.write(':\n\n');
117
+ clients.add(res);
118
+ req.on('close', () => clients.delete(res));
119
+ return;
120
+ }
121
+
122
+ /* Theme Lab write-back: POST /__sb/theme/<id> → themes/<id>.json (atomic) */
123
+ if (p.startsWith('/__sb/theme/') && req.method === 'POST') {
124
+ const id = decodeURIComponent(p.slice('/__sb/theme/'.length));
125
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(id)) return send(res, 400, 'bad theme id');
126
+ const body = JSON.parse(await readBody(req) || '{}');
127
+ if (!body.theme?.modes?.light) return send(res, 400, 'theme must have modes.light');
128
+ await writeJSON(path.join(root, 'themes', `${id}.json`), body.theme);
129
+ log(` 🎨 theme saved: themes/${id}.json`);
130
+ send(res, 200, JSON.stringify({ saved: `themes/${id}.json` }), { 'Content-Type': 'application/json' });
131
+ return;
132
+ }
133
+
134
+ if (p === '/__sb/review/comments') {
135
+ const file = path.join(root, 'review', 'comments.json');
136
+ if (req.method === 'GET') {
137
+ const box = await readJSON(file, { comments: [] });
138
+ send(res, 200, JSON.stringify(box), { 'Content-Type': 'application/json' });
139
+ } else if (req.method === 'POST') {
140
+ const body = JSON.parse(await readBody(req) || '{}');
141
+ if (!Array.isArray(body.comments)) throw new Error('expected { comments: [...] }');
142
+ await writeJSON(file, { comments: body.comments });
143
+ send(res, 200, JSON.stringify({ saved: body.comments.length }), { 'Content-Type': 'application/json' });
144
+ } else {
145
+ send(res, 405, 'method not allowed');
146
+ }
147
+ return;
148
+ }
149
+
150
+ /* /engine/* always resolves inside the engine package — works even
151
+ when the project has no engine/ copy (examples, dev checkouts). */
152
+ if (p === '/engine' || p.startsWith('/engine/')) {
153
+ const sub = p === '/engine' ? '/index.html' : p.slice('/engine'.length);
154
+ const abs = safeJoin(engineDir, sub === '/' ? '/index.html' : sub);
155
+ if (!abs) return send(res, 403, 'forbidden');
156
+ await serveFile(res, abs);
157
+ return;
158
+ }
159
+
160
+ const abs = safeJoin(root, p);
161
+ if (!abs) return send(res, 403, 'forbidden');
162
+ await serveFile(res, abs);
163
+ } catch (err) {
164
+ send(res, 400, `Bad request: ${err.message}`);
165
+ }
166
+ });
167
+
168
+ /* ---------------- watch + broadcast ---------------- */
169
+
170
+ function broadcast(evt) {
171
+ const line = `data: ${JSON.stringify(evt)}\n\n`;
172
+ for (const c of clients) c.write(line);
173
+ }
174
+
175
+ const pendingRegen = new Map(); // feature → timer
176
+ const watchers = [];
177
+
178
+ function onFsEvent(base, rel) {
179
+ if (!rel) return;
180
+ const full = base === root ? rel : path.join('engine', rel);
181
+ const evt = classify(full);
182
+ if (!evt) return;
183
+ if (evt.area === 'screens' && evt.feature) {
184
+ clearTimeout(pendingRegen.get(evt.feature));
185
+ pendingRegen.set(evt.feature, setTimeout(async () => {
186
+ pendingRegen.delete(evt.feature);
187
+ try {
188
+ const r = await regenManifest(root, evt.feature);
189
+ if (r.written) log(` ↻ manifest regenerated: features/${evt.feature}/manifest.json`);
190
+ } catch { /* mid-write scan; the next event retries */ }
191
+ }, 250));
192
+ }
193
+ broadcast(evt);
194
+ }
195
+
196
+ try {
197
+ watchers.push(fs.watch(root, { recursive: true }, (_e, rel) => onFsEvent(root, rel)));
198
+ } catch { /* recursive watch unsupported — live reload off */ }
199
+ const engineInsideRoot = path.resolve(engineDir).startsWith(path.resolve(root) + path.sep);
200
+ if (!engineInsideRoot) {
201
+ try {
202
+ watchers.push(fs.watch(engineDir, { recursive: true }, (_e, rel) => onFsEvent(engineDir, rel)));
203
+ } catch { /* optional */ }
204
+ }
205
+
206
+ const heartbeat = setInterval(() => { for (const c of clients) c.write(':hb\n\n'); }, 25000);
207
+
208
+ return new Promise((resolve, reject) => {
209
+ server.on('error', reject);
210
+ server.listen(port, '127.0.0.1', () => {
211
+ const actual = server.address().port;
212
+ resolve({
213
+ port: actual,
214
+ url: `http://127.0.0.1:${actual}`,
215
+ close: () => {
216
+ clearInterval(heartbeat);
217
+ for (const w of watchers) w.close();
218
+ for (const c of clients) c.end();
219
+ return new Promise((r) => server.close(r));
220
+ }
221
+ });
222
+ });
223
+ });
224
+ }
package/docs/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # ScreenBook documentation
2
+
3
+ Start here. Each guide is short, self-contained, and ordered by how you'll actually meet
4
+ the tool.
5
+
6
+ | Guide | Read it when |
7
+ |-------|--------------|
8
+ | [Getting started](getting-started.md) | First contact — prerequisites, install, your first project in five minutes |
9
+ | [The studio](studio-guide.md) | A tour of every control in the shell: sidebar, dropdowns, flows, review, Theme Lab, self-test |
10
+ | [Authoring](authoring-guide.md) | You're building screens by hand — the full contract for screens, data, scenarios, themes, flows, strings |
11
+ | [CLI reference](cli.md) | Every command and flag |
12
+ | [AI agents & MCP](mcp-agents.md) | Hooking ScreenBook to Claude Code (or any MCP client) so agents author screens for you |
13
+ | [App mode](app-mode-guide.md) | Embedding an EXISTING single-page app instead of (or alongside) file-per-screen features |
14
+ | [Troubleshooting](troubleshooting.md) | Something's red, blank, or missing |
15
+
16
+ Two more references live at the repo root:
17
+
18
+ - **[CLAUDE.md](../CLAUDE.md)** — the authoring contract written for AI agents (scaffolded
19
+ into every project by `init`; humans are welcome to read it too).
20
+ - **[TESTING.md](../TESTING.md)** — the engine's own test map, if you're contributing to
21
+ ScreenBook itself.
@@ -0,0 +1,79 @@
1
+ # App mode — embedding an existing app
2
+
3
+ You have a working single-page prototype and zero desire to rewrite it screen-by-screen.
4
+ App mode puts the WHOLE app inside the studio: its views appear in the sidebar, deep links
5
+ work, dark/RTL/scenario controls drive it — and for sealed apps, **not one byte of the
6
+ app's files changes**.
7
+
8
+ ![A sealed app running inside the studio](shots/app-mode.png)
9
+
10
+ ## Register it
11
+
12
+ ```bash
13
+ node engine/cli/screenbook.js app add legacy/index.html --device ios --chrome self
14
+ ```
15
+
16
+ That writes an app-type feature:
17
+
18
+ ```json
19
+ { "id": "legacy", "title": "Legacy app", "type": "app",
20
+ "src": "legacy/index.html", "device": "ios", "chrome": "self",
21
+ "driver": "driver.js" }
22
+ ```
23
+
24
+ - `device` picks the bezel; `chrome: "self"` means the app draws its own status/home bars
25
+ so the engine shows bezel only (no doubling). Use `"engine"` for apps with no chrome.
26
+ - Same-origin is required for driven apps (the app is served from the project).
27
+
28
+ ## Two integration modes, one protocol
29
+
30
+ **Driven (sealed apps).** `app add` scaffolds `features/<id>/driver.js` — a small script
31
+ the shell injects into the app AFTER it loads. It teaches ScreenBook the app's internals:
32
+
33
+ ```js
34
+ AppBridge.register({
35
+ title: 'Legacy',
36
+ screens: () => Object.keys(App.screens).map(id => ({ id, title: App.screens[id].title })),
37
+ go: (id) => App.go(id),
38
+ current: () => App.state.screen,
39
+ set: (env) => { // called live on every toggle
40
+ App.state.dark = env.mode === 'dark';
41
+ App.render();
42
+ },
43
+ scenarios: () => SCENARIOS.list, // optional: surfaces the app's own 🎭
44
+ ignoreErrors: [/some-cdn-lib/] // optional, narrow, commented — known 3rd-party noise
45
+ });
46
+ ```
47
+
48
+ The driver lives in YOUR project, not in the app — the app stays certified-untouched.
49
+ Call `AppBridge.navigated(id)` when the app navigates itself (wrap its router once).
50
+
51
+ **Cooperative (apps you can edit).** Skip the driver (`--cooperative`); the app includes
52
+ one script and registers itself:
53
+
54
+ ```html
55
+ <script src="../engine/app-bridge.js"></script>
56
+ <script>AppBridge.register({ screens, go, current, set });</script>
57
+ ```
58
+
59
+ `examples/spa-demo` is a complete working cooperative app.
60
+
61
+ ## What the shell does with it
62
+
63
+ Sidebar lists the app-reported screens (with the app's own groups) · `#feature/screen`
64
+ deep links → `go(id)` · in-app navigation reflects back into the URL · mode/dir/lang/theme
65
+ tokens/scenario changes arrive live through `set(env)` · driver-reported scenarios populate
66
+ the 🎭 dropdown · `flows.json` steps work over app screen ids.
67
+
68
+ ## The gate
69
+
70
+ `validate` boots each app headlessly and requires: registration within 20s, at least one
71
+ reported screen, and **a clean visit to every reported screen — zero console errors**.
72
+ Driver bugs surface as `driver <fn>: <message>` rows. `ignoreErrors` patterns are for
73
+ narrow, documented third-party noise only — everything else still fails.
74
+
75
+ ## Reference drivers
76
+
77
+ This repo's root project embeds two real, sealed prototypes — `features/rahhal-ios/driver.js`
78
+ (screen registry, dark/RTL state, scenario player, router wrap) and
79
+ `features/rahhal-web/driver.js` (view/tab model) — read them when writing yours.
@@ -0,0 +1,129 @@
1
+ # Authoring guide
2
+
3
+ The whole product rests on one idea: **everything is a plain file an intern could read.**
4
+ One screen = one HTML document. Data, scenarios, themes, flows, strings = small JSON files.
5
+
6
+ ## A feature
7
+
8
+ ```
9
+ features/trip-planning/
10
+ ├── feature.json { "id", "title", "icon", "description", "order" }
11
+ ├── manifest.json DERIVED — never edit; the CLI/MCP regenerates it
12
+ ├── flows.json guided walkthroughs (optional)
13
+ ├── i18n.json strings for Engine.t (optional)
14
+ ├── data/
15
+ │ ├── seed.json the feature's mock data
16
+ │ └── scenarios.json named overlays over the seed
17
+ └── screens/
18
+ ├── shared.css optional feature-wide look — screens opt in per file
19
+ ├── shared.js optional feature-wide helpers
20
+ └── 010-home.html one file = one screen
21
+ ```
22
+
23
+ ## A screen
24
+
25
+ ```html
26
+ <!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1">
31
+ <title>Home</title>
32
+ <script type="application/json" id="screen-meta">
33
+ { "title": "Home", "description": "Entry point.", "order": 10,
34
+ "device": "ios", "status": "wip", "tags": ["core"] }
35
+ </script>
36
+ <script src="../../../engine/bridge.js"></script>
37
+ <link rel="stylesheet" href="shared.css">
38
+ </head>
39
+ <body>
40
+ <button data-go="020-detail">Open detail</button>
41
+ <script>
42
+ Engine.ready(function (data) { /* bind seed ⊕ scenario */ });
43
+ </script>
44
+ </body>
45
+ </html>
46
+ ```
47
+
48
+ Rules that keep everything sane:
49
+
50
+ - The **meta block is required** (`title` at minimum). `device`: `ios` 393×852 · `android`
51
+ 412×915 · `web` 1280×760 · `none`. Optional: `devices: ["ios","android"]` (one screen,
52
+ several frames — switcher chips appear over the stage and the self-test covers every
53
+ listed device), `status`, `tags`, `group` (sidebar section), `statusBar: "light"|"dark"`,
54
+ `updated` (auto-stamped on CLI/MCP saves).
55
+ - One bridge include, exactly that relative path. Style with theme tokens
56
+ (`var(--color-accent)`, `--font-*`, `--radius-*`, `--space-*`) and pad with
57
+ `var(--safe-top)` / `var(--safe-bottom)`.
58
+ - Screens must render sensibly with **empty data** — guard your bindings.
59
+ - Sheets/modals are hidden markup **inside** the screen that opens them.
60
+ - Duplicated markup between screens is fine — there is no templating system, on purpose.
61
+ Shared look goes to `shared.css`/`shared.js`, included explicitly.
62
+ - Numeric filename prefixes (`010-`, `020-`) order the sidebar and leave room to insert.
63
+ - CDN libraries are allowed (iframes isolate them). Zero-JS screens are allowed.
64
+ - Every screen opens standalone too (no shell): the bridge self-loads the theme + seed and
65
+ honors `?scenario=…&mode=dark&dir=rtl&lang=ar`.
66
+
67
+ ## The bridge API (inside screens)
68
+
69
+ | | |
70
+ |---|---|
71
+ | `Engine.ready(cb)` | `cb(data)` when tokens + data are in. |
72
+ | `Engine.data` | frozen seed ⊕ scenario, `{{today±N}}` dates resolved |
73
+ | `Engine.state` | cross-screen KV: `get/set/subscribe/keys` — survives navigation, reset from the shell |
74
+ | `Engine.go(id)` / `data-go` | navigate within the feature |
75
+ | `Engine.t(key, {vars})` | strings from `i18n.json` (see below) |
76
+ | `Engine.toast(msg)` | transient toast |
77
+ | `Engine.env` | `{ mode, dir, lang, device, scenario, theme }` |
78
+
79
+ ## Mock data + scenarios
80
+
81
+ `seed.json` is the demo-complete state. `scenarios.json` layers named states over it:
82
+
83
+ ```json
84
+ { "list": [
85
+ { "id": "default", "icon": "🧪", "title": "Full demo" },
86
+ { "id": "new-user", "icon": "🆕", "title": "Brand-new user",
87
+ "remove": ["trips", "user.points"], "patch": { "user": { "isNew": true } } } ] }
88
+ ```
89
+
90
+ `remove` deletes dot-paths, then `patch` deep-merges (arrays replace whole). Date tokens —
91
+ `{{today}}`, `{{today+7}}`, `{{today-2}}` — resolve to real dates at injection, so
92
+ "mid-trip today" demos truthfully forever. Model lifecycle states: new → mid → done → edge.
93
+
94
+ ## Themes
95
+
96
+ `themes/<id>.json`, flattened to CSS variables (`color.ink-2` → `--color-ink-2`), dark as a
97
+ delta over light. Edit visually in the 🎨 Theme Lab and Save. Register extra themes in
98
+ `app.config.json`'s `themes` array; the first is the default.
99
+
100
+ ## Strings (optional)
101
+
102
+ `i18n.json`, key-first — every key shows all its languages side by side:
103
+
104
+ ```json
105
+ { "home.title": { "en": "Trips", "ar": "الرحلات" },
106
+ "home.greet": { "en": "{name}, welcome back", "ar": "يا {name}، أهلاً بعودتك" } }
107
+ ```
108
+
109
+ `Engine.t('home.greet', {name})` — fallback: active language → `defaultLanguage` → the key.
110
+ Language switches reload the screen. Gaps show up as `validate` warnings, never blockers.
111
+
112
+ ## Flows
113
+
114
+ ```json
115
+ { "list": [ { "id": "pitch", "title": "The pitch", "icon": "✨",
116
+ "steps": [ { "screen": "010-home", "note": "Start empty.", "scenario": "new-user" },
117
+ { "screen": "020-detail", "note": "One tap deeper." } ] } ] }
118
+ ```
119
+
120
+ Write notes as presenter narration; 3–7 steps; end on the money shot.
121
+
122
+ ## After editing by hand
123
+
124
+ ```bash
125
+ node engine/cli/screenbook.js manifest . # if you added/renamed screens
126
+ node engine/cli/screenbook.js validate . # the done gate
127
+ ```
128
+
129
+ (Under `serve`, manifests regenerate automatically on save. MCP writes do both for you.)
package/docs/cli.md ADDED
@@ -0,0 +1,78 @@
1
+ # CLI reference
2
+
3
+ Run as `node engine/cli/screenbook.js <command>` (or `screenbook <command>` once the
4
+ package is installed with npm and on your PATH). Every command takes an optional project
5
+ directory (default: the current one; running from inside `engine/` resolves to its parent).
6
+
7
+ ## `init [dir] [--name X] [--force] [--app <src>] [--cooperative]`
8
+
9
+ Scaffold a consumer project: `app.config.json`, starter theme, a `welcome` feature with one
10
+ screen, `review/`, the agent authoring contract (`CLAUDE.md`) and the packaged skill
11
+ (`.claude/skills/screenbook/`). Refuses to overwrite an existing project without `--force`.
12
+ `--app path/to/index.html` additionally registers an existing SPA in that folder as an
13
+ embedded app (driver scaffolded unless `--cooperative`).
14
+
15
+ ## `serve [dir] [--port 4600]`
16
+
17
+ The dev server. Static hosting with:
18
+
19
+ - `/engine/*` mounted from the package folder — projects and the bundled examples work even
20
+ without a local `engine/` copy
21
+ - live reload over SSE — saving any project file refreshes the studio; screen edits reload
22
+ the current frame
23
+ - automatic `manifest.json` regeneration when screens change
24
+ - `review/comments.json` disk sync and 🎨 Theme-Lab **Save** (`POST /__sb/theme/<id>`)
25
+ - `Cache-Control: no-store` on everything — stale-cache rituals don't exist here
26
+ - `/` redirects to the studio
27
+
28
+ ## `manifest [dir]`
29
+
30
+ Rescan `features/*/screens/*.html` → regenerate each feature's derived `manifest.json`
31
+ (writes only on change; reports per-feature counts and any broken meta blocks).
32
+
33
+ ## `validate [dir] [--static] [--json]`
34
+
35
+ The done gate, two halves:
36
+
37
+ 1. **Static** — config/feature wiring, screen contract lint (meta block, bridge include,
38
+ ids), dangling `data-go` targets, manifest freshness, scenario/flow references, theme
39
+ shape, i18n gaps (warnings), app-feature `src`/driver existence.
40
+ 2. **Browser** (skipped with `--static`) — headless Chrome renders **every screen × theme ×
41
+ light/dark × LTR/RTL**, asserting `Engine.ready`, meta present, zero console errors;
42
+ embedded apps must register and survive a visit to every screen they report.
43
+
44
+ Exit code 0 = green. `--json` prints the full machine-readable report.
45
+
46
+ ## `screenshot [dir] --screen <feature>/<screen> [options]`
47
+
48
+ Device-framed PNGs via headless Chrome. Options: `--mode light,dark` · `--dir ltr,rtl` ·
49
+ `--scenario id` · `--theme id` · `--lang xx` · `--out dir` (default
50
+ `review/screenshots/`). Comma lists produce a matrix.
51
+
52
+ ## `app add <src> [dir] [--id --title --icon --device --chrome self|engine --cooperative]`
53
+
54
+ Embed an existing single-page app as an app-type feature. Scaffolds an injectable
55
+ `driver.js` template (fill in `screens()` / `go()` / `set()`) unless `--cooperative`, in
56
+ which case the app is expected to include `engine/app-bridge.js` itself. See
57
+ [App mode](app-mode-guide.md).
58
+
59
+ ## `mcp [--root dir]`
60
+
61
+ Start the stdio MCP server (18 authoring tools for AI agents). This is what makes the
62
+ npx registration line work:
63
+
64
+ ```bash
65
+ claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
66
+ ```
67
+
68
+ Nothing but JSON-RPC goes to stdout; server logs go to stderr. See
69
+ [AI agents & MCP](mcp-agents.md).
70
+
71
+ ## `help` · `version`
72
+
73
+ What you'd expect.
74
+
75
+ ## Exit codes
76
+
77
+ `0` success/green · `1` operation failed (validate red, bad input to an operation) ·
78
+ `2` usage errors (no project found, missing arguments, refused overwrite).
@@ -0,0 +1,88 @@
1
+ # Getting started
2
+
3
+ ## Prerequisites
4
+
5
+ | You need | For | Notes |
6
+ |----------|-----|-------|
7
+ | **Node.js ≥ 18** | the CLI, the dev server, the MCP server | the only hard requirement — check with `node --version` |
8
+ | **A modern browser** | the studio itself | Chrome, Safari, Edge, Firefox — the viewer is plain ES modules, no build |
9
+ | **Google Chrome installed** | `validate` and `screenshot` (headless checks) | used through `playwright-core`; see below if you don't have Chrome |
10
+ | **Git** | recommended | ScreenBook writes plain files — git is your undo and your review tool |
11
+
12
+ Nothing else. No bundler, no framework, no global installs. The viewer has **zero**
13
+ dependencies; the CLI has exactly one *dev* dependency (`playwright-core`) used only for
14
+ headless validation and screenshots.
15
+
16
+ One-time setup for the headless checks:
17
+
18
+ ```bash
19
+ cd engine
20
+ npm i # installs playwright-core (~3s, no browser download)
21
+ # no Chrome on this machine? use a bundled browser instead:
22
+ npm i -D playwright && npx playwright install chromium
23
+ ```
24
+
25
+ ## Install
26
+
27
+ ScreenBook lives INSIDE your project as a folder named `engine/`:
28
+
29
+ ```bash
30
+ # today (from the repo):
31
+ git clone https://github.com/cs4alhaider/screenbook my-prototypes/engine
32
+
33
+ # after npm publish:
34
+ # npm i @cs4alhaider/screenbook (then copy/link node_modules/@cs4alhaider/screenbook → engine/)
35
+ ```
36
+
37
+ The folder name matters: screens reference `../../../engine/bridge.js` by convention.
38
+
39
+ ## Your first project — five minutes
40
+
41
+ ```bash
42
+ cd my-prototypes
43
+ node engine/cli/screenbook.js init . --name "My product"
44
+ node engine/cli/screenbook.js serve .
45
+ # → open http://127.0.0.1:4600
46
+ ```
47
+
48
+ You now have:
49
+
50
+ ```
51
+ app.config.json project name · feature order · devices · themes · languages
52
+ themes/default.json design tokens (light + dark) — edit live in the 🎨 Theme Lab
53
+ features/welcome/ one starter feature with one screen
54
+ feature.json · flows.json · data/seed.json · data/scenarios.json
55
+ screens/010-hello.html ← copy this file to make your second screen
56
+ review/comments.json review pins land here
57
+ CLAUDE.md the authoring contract (for you AND your AI agents)
58
+ .claude/skills/screenbook/ packaged skill so Claude knows the rules instantly
59
+ ```
60
+
61
+ Edit `features/welcome/screens/010-hello.html`, save — the browser reloads itself.
62
+ Copy it to `020-something.html`, change the meta block's `title`/`order`, save — it's in
63
+ the sidebar. That's the whole loop.
64
+
65
+ Already have a single-page prototype? Embed it whole instead of rewriting:
66
+
67
+ ```bash
68
+ node engine/cli/screenbook.js init . --app path/to/index.html
69
+ ```
70
+
71
+ (see [App mode](app-mode-guide.md)).
72
+
73
+ ## The definition of done
74
+
75
+ ```bash
76
+ node engine/cli/screenbook.js validate .
77
+ ```
78
+
79
+ Green means: every screen renders in every theme × light/dark × LTR/RTL with **zero console
80
+ errors**, all metadata is present, every navigation target exists. Make it your habit —
81
+ it's the same gate AI agents are held to.
82
+
83
+ ## Serving without the CLI
84
+
85
+ Any static server works once `engine/` is copied into the project
86
+ (`python3 -m http.server`, nginx, GitHub Pages…). You lose the live extras — auto-reload,
87
+ manifest regeneration, review-comment disk sync, Theme-Lab save — but the studio, screens,
88
+ scenarios and flows all work. `/engine/index.html` is the entry point.
@@ -0,0 +1,60 @@
1
+ # AI agents & MCP
2
+
3
+ ScreenBook is built agents-first: the MCP server gives any MCP client (Claude Code, etc.)
4
+ a complete authoring surface, and the tool descriptions teach the contract — **agents never
5
+ need to read the engine's source**.
6
+
7
+ ## Hook it up (Claude Code)
8
+
9
+ The standard local-MCP form, via npx (no install needed):
10
+
11
+ ```bash
12
+ claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
13
+ ```
14
+
15
+ Working from a checkout instead (engine copied into the project)? Point at it directly:
16
+
17
+ ```bash
18
+ claude mcp add screenbook -- node engine/cli/screenbook.js mcp --root .
19
+ ```
20
+
21
+ `--root` points at the project (the folder with `app.config.json`). `init` has already
22
+ dropped `CLAUDE.md` and the `screenbook` skill into the project, so a fresh Claude session
23
+ knows the rules the moment it opens the folder.
24
+
25
+ ## What agents can do
26
+
27
+ Read: `list_features` · `list_screens` · `get_screen` · `get_data` (seed + scenarios +
28
+ i18n) · `get_theme` · `get_flows` · `get_review_comments`
29
+
30
+ Write (validated, atomic, manifest-refreshing, date-stamping):
31
+ `add_screen` · `update_screen` · `remove_screen` · `add_feature` · `add_app` ·
32
+ `set_data` · `set_theme` · `set_flows` · `resolve_comment`
33
+
34
+ Verify: `validate {mode: "static"|"full"}` · `screenshot {feature, screen, …}`
35
+
36
+ Guardrails built in: `add_screen`/`update_screen` lint the screen contract and reject
37
+ violations with actionable messages; `add` fails on existing screens and `update` on
38
+ missing ones (read-then-write); every write is atomic; `manifest.json` is regenerated for
39
+ you.
40
+
41
+ ## A working prompt
42
+
43
+ > Add a checkout screen to the shop feature: order summary card, payment method list from
44
+ > the seed, a pay button that toasts. Match the existing screens' style. Validate and give
45
+ > me a screenshot.
46
+
47
+ A well-behaved agent will `list_screens` → `get_screen` (borrow idioms) → `add_screen` →
48
+ `validate` → `screenshot`. Human taste, machine discipline.
49
+
50
+ ## The review loop
51
+
52
+ Humans pin comments in 📌 Review Mode; each pin carries screen, scenario, theme, mode,
53
+ direction and the CSS path of the clicked element. Agents read them
54
+ (`get_review_comments {unresolvedOnly: true}`), fix the screens, and `resolve_comment` —
55
+ a full design-review cycle without a meeting.
56
+
57
+ ## Serving + MCP together
58
+
59
+ Run `serve` while agents work: every MCP write triggers live reload, so you watch screens
60
+ appear and change in the studio in real time.