@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.
- package/CLAUDE.md +185 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/TESTING.md +54 -0
- package/app-bridge.js +131 -0
- package/bridge.js +598 -0
- package/cli/screenbook.js +290 -0
- package/cli/templates.js +139 -0
- package/core/browser.js +108 -0
- package/core/fsx.js +71 -0
- package/core/meta.js +94 -0
- package/core/project.js +482 -0
- package/core/serve.js +224 -0
- package/docs/README.md +21 -0
- package/docs/app-mode-guide.md +79 -0
- package/docs/authoring-guide.md +129 -0
- package/docs/cli.md +78 -0
- package/docs/getting-started.md +88 -0
- package/docs/mcp-agents.md +60 -0
- package/docs/shots/android-dark.png +0 -0
- package/docs/shots/app-mode.png +0 -0
- package/docs/shots/hero-studio.png +0 -0
- package/docs/shots/rtl-arabic.png +0 -0
- package/docs/shots/theme-lab.png +0 -0
- package/docs/shots/theming-noir.png +0 -0
- package/docs/shots/web-dashboard.png +0 -0
- package/docs/studio-guide.md +64 -0
- package/docs/troubleshooting.md +55 -0
- package/examples/qahwa/app.config.json +9 -0
- package/examples/qahwa/features/order/data/scenarios.json +25 -0
- package/examples/qahwa/features/order/data/seed.json +21 -0
- package/examples/qahwa/features/order/feature.json +7 -0
- package/examples/qahwa/features/order/flows.json +16 -0
- package/examples/qahwa/features/order/manifest.json +78 -0
- package/examples/qahwa/features/order/screens/010-menu.html +121 -0
- package/examples/qahwa/features/order/screens/020-drink.html +120 -0
- package/examples/qahwa/features/order/screens/030-cart.html +99 -0
- package/examples/qahwa/features/order/screens/040-status.html +125 -0
- package/examples/qahwa/features/order/screens/050-status-android.html +95 -0
- package/examples/qahwa/features/order/screens/shared.css +146 -0
- package/examples/qahwa/features/roastery/data/scenarios.json +35 -0
- package/examples/qahwa/features/roastery/data/seed.json +17 -0
- package/examples/qahwa/features/roastery/feature.json +7 -0
- package/examples/qahwa/features/roastery/flows.json +14 -0
- package/examples/qahwa/features/roastery/manifest.json +32 -0
- package/examples/qahwa/features/roastery/screens/010-orders.html +103 -0
- package/examples/qahwa/features/roastery/screens/020-menu-editor.html +88 -0
- package/examples/qahwa/features/roastery/screens/shared.css +115 -0
- package/examples/qahwa/review/comments.json +3 -0
- package/examples/qahwa/themes/qahwa.json +43 -0
- package/examples/spa-demo/app/index.html +100 -0
- package/examples/spa-demo/app.config.json +9 -0
- package/examples/spa-demo/features/notes-app/feature.json +11 -0
- package/examples/spa-demo/review/comments.json +3 -0
- package/examples/spa-demo/themes/default.json +15 -0
- package/examples/tokens-lab/app.config.json +9 -0
- package/examples/tokens-lab/features/lab/data/scenarios.json +5 -0
- package/examples/tokens-lab/features/lab/data/seed.json +3 -0
- package/examples/tokens-lab/features/lab/feature.json +7 -0
- package/examples/tokens-lab/features/lab/flows.json +14 -0
- package/examples/tokens-lab/features/lab/manifest.json +33 -0
- package/examples/tokens-lab/features/lab/screens/010-swatches.html +112 -0
- package/examples/tokens-lab/features/lab/screens/020-typography.html +115 -0
- package/examples/tokens-lab/review/comments.json +3 -0
- package/examples/tokens-lab/themes/noir.json +32 -0
- package/examples/tokens-lab/themes/paper.json +32 -0
- package/index.html +50 -0
- package/mcp/server.js +366 -0
- package/package.json +57 -0
- package/shell/flowplayer.js +85 -0
- package/shell/frames.js +108 -0
- package/shell/main.js +397 -0
- package/shell/net.js +126 -0
- package/shell/review.js +213 -0
- package/shell/screenhost.js +380 -0
- package/shell/selftest.js +275 -0
- package/shell/shell.css +952 -0
- package/shell/sidebar.js +112 -0
- package/shell/store.js +119 -0
- package/shell/themelab.js +186 -0
- package/shell/toast.js +15 -0
- package/shell/toolbar.js +128 -0
- package/skill/SKILL.md +52 -0
- package/skill/references/app-mode.md +77 -0
- package/skill/references/bridge-api.md +55 -0
- package/skill/references/cli-mcp.md +53 -0
- package/skill/references/flows.md +44 -0
- package/skill/references/i18n.md +52 -0
- package/skill/references/scenarios.md +45 -0
- package/skill/references/screen-contract.md +83 -0
- 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
|
+

|
|
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.
|