@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/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. Install one inside engine/:\n` +
25
- ` npm i -D playwright-core (uses your installed Chrome)\n` +
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}/engine/index.html?selftest=1`, { waitUntil: 'domcontentloaded' });
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}/engine/index.html?solo=1#${encodeURIComponent(c.feature)}/${encodeURIComponent(c.screen)}${q ? `?${q}` : ''}`;
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 root = path.resolve(dir || process.cwd());
15
- if (await exists(path.join(root, 'app.config.json'))) return root;
16
- /* convenience: running from inside engine/ of a project */
17
- const parent = path.dirname(root);
18
- if (await exists(path.join(parent, 'app.config.json'))) return parent;
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 = path.join(root, src);
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(path.join(root, fMeta.src)))) errors.push(`${id}: app src not found: ${fMeta.src}`);
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: '/engine/index.html' });
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.1.0', root: path.basename(root) }),
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
- /* /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);
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);
@@ -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="../../../engine/bridge.js"></script>
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
- 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
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
- 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`.
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
 
@@ -24,40 +24,44 @@ npm i -D playwright && npx playwright install chromium
24
24
 
25
25
  ## Install
26
26
 
27
- ScreenBook lives INSIDE your project as a folder named `engine/`:
27
+ One command, once, globally:
28
28
 
29
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/)
30
+ npm install -g @cs4alhaider/screenbook
35
31
  ```
36
32
 
37
- The folder name matters: screens reference `../../../engine/bridge.js` by convention.
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
- cd my-prototypes
43
- node engine/cli/screenbook.js init . --name "My product"
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
- You now have:
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
- 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)
50
+ .screenbook/
51
+ brief.md ← WRITE YOUR REQUIREMENTS HEREagents 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
- ## Serving without the CLI
87
+ ## Offline & prerequisites for the checks
84
88
 
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.
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.
@@ -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
- claude mcp add screenbook -- node engine/cli/screenbook.js mcp --root .
18
+ node <path-to-package>/cli/screenbook.js mcp --root . # rarely needed
19
19
  ```
20
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
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
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <link rel="stylesheet" href="shared.css">
20
20
  </head>
21
21
  <body>
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <link rel="stylesheet" href="shared.css">
20
20
  </head>
21
21
  <body>
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <link rel="stylesheet" href="shared.css">
20
20
  </head>
21
21
  <body>
@@ -16,7 +16,7 @@
16
16
  "updated": "2026-07-18"
17
17
  }
18
18
  </script>
19
- <script src="../../../engine/bridge.js"></script>
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="../../../engine/bridge.js"></script>
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 */
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <link rel="stylesheet" href="shared.css">
20
20
  </head>
21
21
  <body>
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <link rel="stylesheet" href="shared.css">
20
20
  </head>
21
21
  <body>
@@ -62,7 +62,7 @@
62
62
  </div>
63
63
 
64
64
  <!-- the ONE line that makes this a cooperative ScreenBook app -->
65
- <script src="../engine/app-bridge.js"></script>
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.' },
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <style>
20
20
  * { box-sizing: border-box; }
21
21
  body {
@@ -15,7 +15,7 @@
15
15
  "updated": "2026-07-18"
16
16
  }
17
17
  </script>
18
- <script src="../../../engine/bridge.js"></script>
18
+ <script src="../../../screenbook/bridge.js"></script>
19
19
  <style>
20
20
  * { box-sizing: border-box; }
21
21
  body {
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.1.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="../../../engine/bridge.js"></script> include. Use CSS vars from the active theme ' +
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.1.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
- "devDependencies": {
42
+ "dependencies": {
42
43
  "playwright-core": "^1.45.0"
43
44
  },
44
45
  "scripts": {
@@ -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('engine/app-bridge.js');
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>ScreenBook self-test</h2><span class="st-summary" id="st-summary">0 / ${all.length}</span></div>
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">screens × themes × light/dark × ltr/rtl · zero console errors · meta present</span>
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
- inset: 0;
874
+ right: 18px;
875
+ bottom: 18px;
874
876
  z-index: 400;
875
- background: color-mix(in srgb, var(--sb-bg) 88%, transparent);
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: min(760px, 100%);
883
- max-height: 100%;
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-rows { flex: 1; overflow-y: auto; padding: 10px 12px 14px; min-height: 120px; }
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
- (`../../../engine/bridge.js`). It works opened alone AND inside the shell.
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: claude mcp add screenbook -- node engine/cli/screenbook.js mcp --root .
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="../../../engine/bridge.js"></script>
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">