@cs4alhaider/screenbook 0.1.0 → 0.2.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/AGENTS.md +147 -0
- package/CLAUDE.md +4 -184
- package/README.md +140 -162
- package/bridge.js +1 -1
- package/cli/screenbook.js +67 -28
- package/cli/templates.js +54 -1
- package/core/browser.js +22 -7
- package/core/project.js +28 -7
- package/core/serve.js +22 -7
- package/docs/authoring-guide.md +1 -1
- package/docs/cli.md +9 -5
- package/docs/getting-started.md +27 -23
- package/docs/mcp-agents.md +3 -3
- package/docs/shots/rtl-arabic.png +0 -0
- package/examples/qahwa/features/order/screens/010-menu.html +1 -1
- package/examples/qahwa/features/order/screens/020-drink.html +1 -1
- package/examples/qahwa/features/order/screens/030-cart.html +1 -1
- package/examples/qahwa/features/order/screens/040-status.html +1 -1
- package/examples/qahwa/features/order/screens/050-status-android.html +1 -1
- package/examples/qahwa/features/roastery/screens/010-orders.html +1 -1
- package/examples/qahwa/features/roastery/screens/020-menu-editor.html +1 -1
- package/examples/spa-demo/app/index.html +1 -1
- package/examples/tokens-lab/features/lab/screens/010-swatches.html +1 -1
- package/examples/tokens-lab/features/lab/screens/020-typography.html +1 -1
- package/mcp/server.js +2 -2
- package/package.json +3 -2
- package/shell/screenhost.js +1 -1
- package/shell/selftest.js +6 -2
- package/shell/shell.css +17 -9
- package/skill/SKILL.md +1 -1
- package/skill/references/cli-mcp.md +1 -1
- package/skill/references/screen-contract.md +1 -1
package/core/browser.js
CHANGED
|
@@ -6,12 +6,13 @@ import path from 'node:path';
|
|
|
6
6
|
import { startServer } from './serve.js';
|
|
7
7
|
import { mkdirp } from './fsx.js';
|
|
8
8
|
|
|
9
|
-
async function launch() {
|
|
9
|
+
async function launch({ allowDownload = true } = {}) {
|
|
10
10
|
let lastErr;
|
|
11
11
|
for (const [pkg, opts] of [
|
|
12
12
|
['playwright', { headless: true }],
|
|
13
13
|
['playwright-core', { headless: true, channel: 'chrome' }],
|
|
14
|
-
['playwright-core', { headless: true, channel: 'chromium' }]
|
|
14
|
+
['playwright-core', { headless: true, channel: 'chromium' }],
|
|
15
|
+
['playwright-core', { headless: true }]
|
|
15
16
|
]) {
|
|
16
17
|
try {
|
|
17
18
|
const mod = await import(pkg);
|
|
@@ -20,10 +21,24 @@ async function launch() {
|
|
|
20
21
|
lastErr = err;
|
|
21
22
|
}
|
|
22
23
|
}
|
|
24
|
+
|
|
25
|
+
/* No usable browser on this machine — download one automatically, once.
|
|
26
|
+
(Nobody should ever be told to install something by hand.) */
|
|
27
|
+
if (allowDownload) {
|
|
28
|
+
console.error('No browser found — downloading a headless Chromium once (~120MB)…');
|
|
29
|
+
try {
|
|
30
|
+
const { spawnSync } = await import('node:child_process');
|
|
31
|
+
const { createRequire } = await import('node:module');
|
|
32
|
+
const req = createRequire(import.meta.url);
|
|
33
|
+
const cli = req.resolve('playwright-core/cli.js');
|
|
34
|
+
const r = spawnSync(process.execPath, [cli, 'install', 'chromium'], { stdio: ['ignore', 'inherit', 'inherit'] });
|
|
35
|
+
if (r.status === 0) return launch({ allowDownload: false });
|
|
36
|
+
} catch { /* fall through to the error below */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
23
39
|
throw new Error(
|
|
24
|
-
`No headless browser available
|
|
25
|
-
`
|
|
26
|
-
` npm i -D playwright && npx playwright install chromium (self-contained)\n` +
|
|
40
|
+
`No headless browser available and the automatic download failed.\n` +
|
|
41
|
+
`Install Google Chrome, or run: npx playwright-core install chromium\n` +
|
|
27
42
|
`Underlying error: ${lastErr?.message}`
|
|
28
43
|
);
|
|
29
44
|
}
|
|
@@ -62,7 +77,7 @@ export async function runBrowserSelftest({ root, engineDir, timeout = 180000 })
|
|
|
62
77
|
shellErrors.push(text);
|
|
63
78
|
});
|
|
64
79
|
|
|
65
|
-
await page.goto(`${base}/
|
|
80
|
+
await page.goto(`${base}/screenbook/?selftest=1`, { waitUntil: 'domcontentloaded' });
|
|
66
81
|
await page.waitForFunction('window.__selftestDone === true', null, { timeout });
|
|
67
82
|
const result = await page.evaluate('window.__selftestResult');
|
|
68
83
|
result.shellErrors = shellErrors;
|
|
@@ -87,7 +102,7 @@ export async function screenshot({ root, engineDir, combos, outDir }) {
|
|
|
87
102
|
if (c.dir && c.dir !== 'ltr') params.set('dir', c.dir);
|
|
88
103
|
if (c.lang) params.set('lang', c.lang);
|
|
89
104
|
const q = params.toString();
|
|
90
|
-
const url = `${base}/
|
|
105
|
+
const url = `${base}/screenbook/?solo=1#${encodeURIComponent(c.feature)}/${encodeURIComponent(c.screen)}${q ? `?${q}` : ''}`;
|
|
91
106
|
|
|
92
107
|
await page.goto('about:blank');
|
|
93
108
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
package/core/project.js
CHANGED
|
@@ -10,15 +10,36 @@ const MANIFEST_FIELDS = ['title', 'description', 'order', 'device', 'devices', '
|
|
|
10
10
|
|
|
11
11
|
/* ---------------- roots + config ---------------- */
|
|
12
12
|
|
|
13
|
+
/*
|
|
14
|
+
* Find the ScreenBook root for a directory. The standard home is a
|
|
15
|
+
* `.screenbook/` folder inside the user's project (created by `init` — the
|
|
16
|
+
* self-contained source of truth they can commit or gitignore). A config at
|
|
17
|
+
* the directory itself also works (bundled examples, legacy layouts).
|
|
18
|
+
*/
|
|
13
19
|
export async function resolveRoot(dir) {
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
const base = path.resolve(dir || process.cwd());
|
|
21
|
+
const candidates = [
|
|
22
|
+
base,
|
|
23
|
+
path.join(base, '.screenbook'),
|
|
24
|
+
path.dirname(base), // running from inside engine/ or .screenbook/
|
|
25
|
+
path.join(path.dirname(base), '.screenbook')
|
|
26
|
+
];
|
|
27
|
+
for (const c of candidates) {
|
|
28
|
+
if (await exists(path.join(c, 'app.config.json'))) return c;
|
|
29
|
+
}
|
|
19
30
|
return null;
|
|
20
31
|
}
|
|
21
32
|
|
|
33
|
+
/* Disk location of an app-feature src. Apps usually live in the USER'S
|
|
34
|
+
project, outside .screenbook/ — those are recorded as "project/<path>"
|
|
35
|
+
and served via the /project mount. */
|
|
36
|
+
export function appSrcPath(root, src) {
|
|
37
|
+
if (src.startsWith('project/') && path.basename(root) === '.screenbook') {
|
|
38
|
+
return path.join(path.dirname(root), src.slice('project/'.length));
|
|
39
|
+
}
|
|
40
|
+
return path.join(root, src);
|
|
41
|
+
}
|
|
42
|
+
|
|
22
43
|
export async function readConfig(root) {
|
|
23
44
|
return readJSON(path.join(root, 'app.config.json'), {});
|
|
24
45
|
}
|
|
@@ -195,7 +216,7 @@ export async function addFeature(root, { id, title, icon, description, order })
|
|
|
195
216
|
*/
|
|
196
217
|
export async function addApp(root, { src, id, title, icon, description, device, chrome, driver, order }) {
|
|
197
218
|
if (!src) throw new Error('src required — project-relative path to the app\'s index.html');
|
|
198
|
-
const srcPath =
|
|
219
|
+
const srcPath = appSrcPath(root, src);
|
|
199
220
|
if (!(await exists(srcPath))) throw new Error(`app entry not found: ${src}`);
|
|
200
221
|
|
|
201
222
|
const appId = id || path.basename(path.dirname(srcPath)).toLowerCase().replace(/[^a-z0-9-]+/g, '-') || 'app';
|
|
@@ -386,7 +407,7 @@ export async function staticValidate(root) {
|
|
|
386
407
|
const fMeta = await readJSON(path.join(featureDir(root, id), 'feature.json'), null);
|
|
387
408
|
if (fMeta?.type === 'app') {
|
|
388
409
|
if (!fMeta.src) errors.push(`${id}: app feature has no "src"`);
|
|
389
|
-
else if (!(await exists(
|
|
410
|
+
else if (!(await exists(appSrcPath(root, fMeta.src)))) errors.push(`${id}: app src not found: ${fMeta.src}`);
|
|
390
411
|
if (fMeta.driver && !(await exists(path.join(featureDir(root, id), fMeta.driver)))) {
|
|
391
412
|
errors.push(`${id}: driver not found: features/${id}/${fMeta.driver}`);
|
|
392
413
|
}
|
package/core/serve.js
CHANGED
|
@@ -97,12 +97,12 @@ export function startServer({ root, engineDir, port = 4600, quiet = false }) {
|
|
|
97
97
|
|
|
98
98
|
try {
|
|
99
99
|
if (p === '/' || p === '/index.html') {
|
|
100
|
-
send(res, 302, '', { Location: '/
|
|
100
|
+
send(res, 302, '', { Location: '/screenbook/' });
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
if (p === '/__sb/health') {
|
|
105
|
-
send(res, 200, JSON.stringify({ screenbook: true, version: '0.
|
|
105
|
+
send(res, 200, JSON.stringify({ screenbook: true, version: '0.2.0', root: path.basename(root) }),
|
|
106
106
|
{ 'Content-Type': 'application/json' });
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
@@ -147,16 +147,31 @@ export function startServer({ root, engineDir, port = 4600, quiet = false }) {
|
|
|
147
147
|
return;
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
/*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
/* When the root is a .screenbook/ folder, the user's real project sits
|
|
151
|
+
one level up — mount it at /project/ so embedded apps (app mode) are
|
|
152
|
+
reachable same-origin without any copying. */
|
|
153
|
+
const parentMount = path.basename(root) === '.screenbook' ? path.dirname(root) : null;
|
|
154
|
+
if (parentMount && (p === '/project' || p.startsWith('/project/'))) {
|
|
155
|
+
const sub = p === '/project' ? '/index.html' : p.slice('/project'.length);
|
|
156
|
+
const abs = safeJoin(parentMount, sub === '/' ? '/index.html' : sub);
|
|
155
157
|
if (!abs) return send(res, 403, 'forbidden');
|
|
156
158
|
await serveFile(res, abs);
|
|
157
159
|
return;
|
|
158
160
|
}
|
|
159
161
|
|
|
162
|
+
/* The studio runtime is served from THIS installed package at
|
|
163
|
+
/screenbook/* — nothing is ever copied into user projects.
|
|
164
|
+
/engine/* remains as a compatibility alias for older screens. */
|
|
165
|
+
for (const prefix of ['/screenbook', '/engine']) {
|
|
166
|
+
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
167
|
+
const sub = p === prefix ? '/index.html' : p.slice(prefix.length);
|
|
168
|
+
const abs = safeJoin(engineDir, sub === '/' ? '/index.html' : sub);
|
|
169
|
+
if (!abs) return send(res, 403, 'forbidden');
|
|
170
|
+
await serveFile(res, abs);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
160
175
|
const abs = safeJoin(root, p);
|
|
161
176
|
if (!abs) return send(res, 403, 'forbidden');
|
|
162
177
|
await serveFile(res, abs);
|
package/docs/authoring-guide.md
CHANGED
|
@@ -33,7 +33,7 @@ features/trip-planning/
|
|
|
33
33
|
{ "title": "Home", "description": "Entry point.", "order": 10,
|
|
34
34
|
"device": "ios", "status": "wip", "tags": ["core"] }
|
|
35
35
|
</script>
|
|
36
|
-
<script src="../../../
|
|
36
|
+
<script src="../../../screenbook/bridge.js"></script>
|
|
37
37
|
<link rel="stylesheet" href="shared.css">
|
|
38
38
|
</head>
|
|
39
39
|
<body>
|
package/docs/cli.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# CLI reference
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Get the direct `screenbook` command with one global install:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @cs4alhaider/screenbook
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Alternatives that need no install: `npx -y @cs4alhaider/screenbook <command>`, or from a
|
|
10
|
+
checkout `node engine/cli/screenbook.js <command>`. Every command takes an optional project
|
|
5
11
|
directory (default: the current one; running from inside `engine/` resolves to its parent).
|
|
6
12
|
|
|
7
13
|
## `init [dir] [--name X] [--force] [--app <src>] [--cooperative]`
|
|
8
14
|
|
|
9
|
-
|
|
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`.
|
|
15
|
+
Create `.screenbook/` in the target project — brief.md (your requirements), app.config.json, starter theme, a `welcome` feature, review/, the agent authoring contract, plus the packaged skill at `.claude/skills/screenbook/`. The folder is the prototype's source of truth; commit or gitignore it. Refuses to overwrite an existing `.screenbook/` without `--force`.
|
|
12
16
|
`--app path/to/index.html` additionally registers an existing SPA in that folder as an
|
|
13
17
|
embedded app (driver scaffolded unless `--cooperative`).
|
|
14
18
|
|
package/docs/getting-started.md
CHANGED
|
@@ -24,40 +24,44 @@ npm i -D playwright && npx playwright install chromium
|
|
|
24
24
|
|
|
25
25
|
## Install
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
One command, once, globally:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
|
|
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/)
|
|
30
|
+
npm install -g @cs4alhaider/screenbook
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
|
|
33
|
+
That's the entire install — no cloning, nothing copied into your projects. (Prefer no global
|
|
34
|
+
installs? `npx -y @cs4alhaider/screenbook <cmd>` works everywhere `screenbook` appears below.)
|
|
38
35
|
|
|
39
36
|
## Your first project — five minutes
|
|
40
37
|
|
|
38
|
+
Go to **any project you're working on** — an iOS app, a web app, an empty folder — and:
|
|
39
|
+
|
|
41
40
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
node engine/cli/screenbook.js serve .
|
|
41
|
+
screenbook init --name "My product"
|
|
42
|
+
screenbook serve
|
|
45
43
|
# → open http://127.0.0.1:4600
|
|
46
44
|
```
|
|
47
45
|
|
|
48
|
-
|
|
46
|
+
`init` creates one folder, **`.screenbook/`** — the self-contained source of truth for your
|
|
47
|
+
prototype. Commit it or add it to `.gitignore`; your call.
|
|
49
48
|
|
|
50
49
|
```
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
50
|
+
.screenbook/
|
|
51
|
+
brief.md ← WRITE YOUR REQUIREMENTS HERE — agents read this first
|
|
52
|
+
app.config.json project name · feature order · devices · themes · languages
|
|
53
|
+
CLAUDE.md the authoring contract (for you AND your AI agents)
|
|
54
|
+
themes/default.json design tokens (light + dark) — edit live in the 🎨 Theme Lab
|
|
55
|
+
features/welcome/ one starter feature with one screen
|
|
56
|
+
feature.json · flows.json · data/seed.json · data/scenarios.json
|
|
57
|
+
screens/010-hello.html ← copy this file to make your second screen
|
|
58
|
+
review/comments.json review pins land here
|
|
58
59
|
.claude/skills/screenbook/ packaged skill so Claude knows the rules instantly
|
|
59
60
|
```
|
|
60
61
|
|
|
62
|
+
The studio runtime always comes from the installed package — `screenbook serve` mounts it at
|
|
63
|
+
`/screenbook/` automatically, and screens reference it as `../../../screenbook/bridge.js`.
|
|
64
|
+
|
|
61
65
|
Edit `features/welcome/screens/010-hello.html`, save — the browser reloads itself.
|
|
62
66
|
Copy it to `020-something.html`, change the meta block's `title`/`order`, save — it's in
|
|
63
67
|
the sidebar. That's the whole loop.
|
|
@@ -80,9 +84,9 @@ Green means: every screen renders in every theme × light/dark × LTR/RTL with *
|
|
|
80
84
|
errors**, all metadata is present, every navigation target exists. Make it your habit —
|
|
81
85
|
it's the same gate AI agents are held to.
|
|
82
86
|
|
|
83
|
-
##
|
|
87
|
+
## Offline & prerequisites for the checks
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
Everything runs on your machine — after the one npm install, ScreenBook needs no network at
|
|
90
|
+
all. The automated checks (`screenbook validate`, `screenshot`) drive your installed Google
|
|
91
|
+
Chrome; on a machine without Chrome they download a headless browser once (~120MB),
|
|
92
|
+
automatically. Nobody is ever asked to install anything by hand.
|
package/docs/mcp-agents.md
CHANGED
|
@@ -15,11 +15,11 @@ claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
|
|
|
15
15
|
Working from a checkout instead (engine copied into the project)? Point at it directly:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
node <path-to-package>/cli/screenbook.js mcp --root . # rarely needed
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
`--root` points at
|
|
22
|
-
dropped `
|
|
21
|
+
`--root` points at your project directory — the `.screenbook/` inside it is found automatically. `init` has already
|
|
22
|
+
dropped `AGENTS.md` (the universal contract) and the `screenbook` skill into the project, so a fresh Claude session
|
|
23
23
|
knows the rules the moment it opens the folder.
|
|
24
24
|
|
|
25
25
|
## What agents can do
|
|
Binary file
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"updated": "2026-07-18"
|
|
17
17
|
}
|
|
18
18
|
</script>
|
|
19
|
-
<script src="../../../
|
|
19
|
+
<script src="../../../screenbook/bridge.js"></script>
|
|
20
20
|
<link rel="stylesheet" href="shared.css">
|
|
21
21
|
<style>
|
|
22
22
|
.stepper { position: relative; padding-inline-start: 26px; }
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"updated": "2026-07-18"
|
|
17
17
|
}
|
|
18
18
|
</script>
|
|
19
|
-
<script src="../../../
|
|
19
|
+
<script src="../../../screenbook/bridge.js"></script>
|
|
20
20
|
<link rel="stylesheet" href="shared.css">
|
|
21
21
|
<style>
|
|
22
22
|
/* Material-ish accents for the Android take */
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
</div>
|
|
63
63
|
|
|
64
64
|
<!-- the ONE line that makes this a cooperative ScreenBook app -->
|
|
65
|
-
<script src="../
|
|
65
|
+
<script src="../screenbook/app-bridge.js"></script>
|
|
66
66
|
<script>
|
|
67
67
|
var VIEWS = [
|
|
68
68
|
{ id: 'inbox', title: 'Inbox', description: 'The notes list — tap one to open it.' },
|
package/mcp/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from '../core/project.js';
|
|
17
17
|
|
|
18
18
|
const ENGINE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
-
const VERSION = '0.
|
|
19
|
+
const VERSION = '0.2.0';
|
|
20
20
|
|
|
21
21
|
const rootFlagIx = process.argv.indexOf('--root');
|
|
22
22
|
const rootArg = rootFlagIx > -1 ? process.argv[rootFlagIx + 1] : undefined;
|
|
@@ -32,7 +32,7 @@ const SCREEN_CONTRACT =
|
|
|
32
32
|
'A screen is one full standalone HTML document: <!DOCTYPE html>, a required ' +
|
|
33
33
|
'<script type="application/json" id="screen-meta"> block ({title, description, order, ' +
|
|
34
34
|
'device: ios|android|web|none, status?, tags?, group?, statusBar?: light|dark, updated?}), and one ' +
|
|
35
|
-
'<script src="../../../
|
|
35
|
+
'<script src="../../../screenbook/bridge.js"></script> include. Use CSS vars from the active theme ' +
|
|
36
36
|
'(--color-*, --font-*, --radius-*, --space-*, --safe-top, --safe-bottom), bind mock data with ' +
|
|
37
37
|
'Engine.ready(cb → cb(data)), navigate with data-go="<screen-id>" attributes, share state via ' +
|
|
38
38
|
'Engine.state.get/set/subscribe, notify with Engine.toast(msg). Screens must render sensibly ' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cs4alhaider/screenbook",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -35,10 +35,11 @@
|
|
|
35
35
|
"skill/",
|
|
36
36
|
"examples/",
|
|
37
37
|
"docs/",
|
|
38
|
+
"AGENTS.md",
|
|
38
39
|
"CLAUDE.md",
|
|
39
40
|
"TESTING.md"
|
|
40
41
|
],
|
|
41
|
-
"
|
|
42
|
+
"dependencies": {
|
|
42
43
|
"playwright-core": "^1.45.0"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
package/shell/screenhost.js
CHANGED
|
@@ -30,7 +30,7 @@ export function injectAppRuntime(iframe, featureId, driverFile, onFail) {
|
|
|
30
30
|
}
|
|
31
31
|
const host = doc.head || doc.documentElement;
|
|
32
32
|
const runtime = doc.createElement('script');
|
|
33
|
-
runtime.src = url('
|
|
33
|
+
runtime.src = url('screenbook/app-bridge.js');
|
|
34
34
|
runtime.onload = () => {
|
|
35
35
|
const driver = doc.createElement('script');
|
|
36
36
|
driver.src = url(`features/${featureId}/${driverFile}`);
|
package/shell/selftest.js
CHANGED
|
@@ -200,11 +200,12 @@ export async function runSelftest() {
|
|
|
200
200
|
overlay.hidden = false;
|
|
201
201
|
overlay.innerHTML =
|
|
202
202
|
`<div class="st-card">
|
|
203
|
-
<div class="st-head"><h2>
|
|
203
|
+
<div class="st-head"><h2>Self-test</h2><span class="st-summary" id="st-summary">0 / ${all.length}</span></div>
|
|
204
204
|
<div class="st-progress"><i id="st-bar"></i></div>
|
|
205
|
+
<div class="st-now" id="st-now">warming up…</div>
|
|
205
206
|
<div class="st-rows" id="st-rows"></div>
|
|
206
207
|
<div class="st-foot">
|
|
207
|
-
<span class="st-note">
|
|
208
|
+
<span class="st-note">every screen × theme × mode × direction · zero console errors</span>
|
|
208
209
|
<button class="btn-ghost" onclick="location.search=''">Close</button>
|
|
209
210
|
</div>
|
|
210
211
|
</div>`;
|
|
@@ -217,11 +218,13 @@ export async function runSelftest() {
|
|
|
217
218
|
const results = [];
|
|
218
219
|
let done = 0;
|
|
219
220
|
|
|
221
|
+
const nowLine = document.getElementById('st-now');
|
|
220
222
|
function report(r) {
|
|
221
223
|
results.push(r);
|
|
222
224
|
done++;
|
|
223
225
|
summary.textContent = `${done}${done <= all.length ? ` / ${all.length}` : ' checks'}`;
|
|
224
226
|
bar.style.width = `${Math.min(100, (done / Math.max(1, all.length)) * 100)}%`;
|
|
227
|
+
nowLine.textContent = `${r.ok ? '✓' : '✕'} ${r.feature}/${r.screen} · ${r.theme}·${r.mode}·${r.dir}${r.device && r.device !== 'app' ? '·' + r.device : ''}`;
|
|
225
228
|
if (!r.ok) {
|
|
226
229
|
const row = document.createElement('div');
|
|
227
230
|
row.className = 'st-row is-fail';
|
|
@@ -245,6 +248,7 @@ export async function runSelftest() {
|
|
|
245
248
|
for (const f of project.features.values()) {
|
|
246
249
|
if (f.type !== 'app') continue;
|
|
247
250
|
summary.textContent = `app: ${f.id}…`;
|
|
251
|
+
nowLine.textContent = `booting embedded app "${f.id}" — visiting every screen it reports…`;
|
|
248
252
|
await runAppFeature(f, report);
|
|
249
253
|
}
|
|
250
254
|
|
package/shell/shell.css
CHANGED
|
@@ -868,19 +868,18 @@ body.solo .canvas { padding: 0; }
|
|
|
868
868
|
|
|
869
869
|
/* ---------------------------------------------------------------- selftest */
|
|
870
870
|
|
|
871
|
+
/* Self-test runs in a floating card — the studio stays fully visible. */
|
|
871
872
|
.selftest-overlay {
|
|
872
873
|
position: fixed;
|
|
873
|
-
|
|
874
|
+
right: 18px;
|
|
875
|
+
bottom: 18px;
|
|
874
876
|
z-index: 400;
|
|
875
|
-
|
|
876
|
-
backdrop-filter: blur(6px);
|
|
877
|
-
display: grid;
|
|
878
|
-
place-items: center;
|
|
879
|
-
padding: 30px;
|
|
877
|
+
max-width: min(520px, calc(100vw - 36px));
|
|
880
878
|
}
|
|
881
879
|
.st-card {
|
|
882
|
-
width:
|
|
883
|
-
max-
|
|
880
|
+
width: 480px;
|
|
881
|
+
max-width: 100%;
|
|
882
|
+
max-height: min(70vh, 640px);
|
|
884
883
|
display: flex;
|
|
885
884
|
flex-direction: column;
|
|
886
885
|
background: var(--sb-panel-2);
|
|
@@ -915,7 +914,16 @@ body.solo .canvas { padding: 0; }
|
|
|
915
914
|
border-radius: 2px;
|
|
916
915
|
transition: width .15s ease;
|
|
917
916
|
}
|
|
918
|
-
.st-
|
|
917
|
+
.st-now {
|
|
918
|
+
padding: 8px 18px 2px;
|
|
919
|
+
font-size: 11px;
|
|
920
|
+
font-family: var(--sb-mono);
|
|
921
|
+
color: var(--sb-ink-2);
|
|
922
|
+
white-space: nowrap;
|
|
923
|
+
overflow: hidden;
|
|
924
|
+
text-overflow: ellipsis;
|
|
925
|
+
}
|
|
926
|
+
.st-rows { flex: 1; overflow-y: auto; padding: 8px 12px 12px; min-height: 60px; }
|
|
919
927
|
.st-row {
|
|
920
928
|
display: flex;
|
|
921
929
|
align-items: baseline;
|
package/skill/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ read or modify `engine/` internals.
|
|
|
13
13
|
|
|
14
14
|
- One screen = one full HTML document in `features/<f>/screens/<id>.html`, with a required
|
|
15
15
|
`<script type="application/json" id="screen-meta">` block and one bridge include
|
|
16
|
-
(`../../../
|
|
16
|
+
(`../../../screenbook/bridge.js`). It works opened alone AND inside the shell.
|
|
17
17
|
- Mock data = `features/<f>/data/seed.json`; named **scenarios** overlay it
|
|
18
18
|
(`patch` deep-merge, `remove` dot-paths, `{{today±N}}` date tokens).
|
|
19
19
|
- Look = design tokens in `themes/<id>.json`, flattened to CSS vars (`--color-accent`,
|
|
@@ -37,7 +37,7 @@ scenario?, theme?, lang?, return_image?}`
|
|
|
37
37
|
Register with Claude Code:
|
|
38
38
|
```
|
|
39
39
|
claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
|
|
40
|
-
# or from a checkout:
|
|
40
|
+
# or from a checkout: node <path-to-package>/cli/screenbook.js mcp --root . # rarely needed
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
## Serve endpoints (for tooling)
|
|
@@ -29,7 +29,7 @@ fallback and keeps directory listings readable. Prefer ids like `010-home`, `020
|
|
|
29
29
|
</script>
|
|
30
30
|
|
|
31
31
|
<!-- 2. REQUIRED bridge — the ONLY engine include, exact relative path -->
|
|
32
|
-
<script src="../../../
|
|
32
|
+
<script src="../../../screenbook/bridge.js"></script>
|
|
33
33
|
|
|
34
34
|
<!-- 3. Optional shared feature look/helpers (explicit opt-in per screen) -->
|
|
35
35
|
<link rel="stylesheet" href="shared.css">
|