@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/CLAUDE.md ADDED
@@ -0,0 +1,185 @@
1
+ # ScreenBook β€” authoring contract
2
+
3
+ > This file ships with ScreenBook and is scaffolded into consumer projects by `init` (it is the
4
+ > contract from EN-22/EN-32). You are authoring **screens, data, themes and flows** for a
5
+ > ScreenBook prototype project. You never need to read or edit anything under `engine/` β€”
6
+ > everything you can do is defined here. Engine developers: see `engine/ENGINE-BRIEF.md` instead.
7
+
8
+ ## What this is
9
+
10
+ ScreenBook is a static, no-build prototype studio. A project is just folders of standalone HTML
11
+ screens plus JSON files. The shell (`engine/index.html`) turns them into a navigable,
12
+ device-framed gallery with data scenarios, themes, dark/RTL toggles, guided flows, review
13
+ comments and a self-test.
14
+
15
+ ## Project layout (convention over configuration)
16
+
17
+ ```
18
+ app.config.json ← name, features order, devices, themes, languages
19
+ themes/default.json ← design tokens (light + dark), flattened to CSS vars
20
+ features/<feature-id>/
21
+ feature.json ← { id, title, icon, description, order }
22
+ manifest.json ← DERIVED index β€” never edit; CLI/MCP regenerates it
23
+ flows.json ← named walkthroughs: { list: [{id, title, icon, steps: […]}] }
24
+ data/seed.json ← mock data for the feature
25
+ data/scenarios.json ← named overlays over the seed (empty states, mid-trip, …)
26
+ screens/
27
+ shared.css / shared.js ← optional feature-wide look/helpers (screens opt in per file)
28
+ 010-home.html ← one file = one screen; numeric prefix = sidebar order fallback
29
+ review/comments.json ← Review-Mode pins from humans (read them, fix, resolve)
30
+ engine/ ← the studio itself β€” NEVER edit
31
+ ```
32
+
33
+ ## The screen contract (the whole truth in one file)
34
+
35
+ ```html
36
+ <!DOCTYPE html>
37
+ <html lang="en">
38
+ <head>
39
+ <meta charset="UTF-8">
40
+ <meta name="viewport" content="width=device-width, initial-scale=1">
41
+ <title>Home</title>
42
+ <script type="application/json" id="screen-meta">
43
+ {
44
+ "title": "Trips home",
45
+ "description": "Entry point β€” active trip hero, drafts, memories.",
46
+ "order": 10,
47
+ "device": "ios",
48
+ "status": "draft",
49
+ "tags": ["core"]
50
+ }
51
+ </script>
52
+ <script src="../../../engine/bridge.js"></script>
53
+ </head>
54
+ <body>
55
+ <button data-go="020-wizard">Plan a trip</button>
56
+ <script>
57
+ Engine.ready(function (data) { /* seed βŠ• active scenario, dates resolved */ });
58
+ </script>
59
+ </body>
60
+ </html>
61
+ ```
62
+
63
+ Rules:
64
+ - The meta block is **required** (`title` at minimum) β€” the self-test fails without it.
65
+ `device`: `ios` (393Γ—852) Β· `android` (412Γ—915) Β· `web` (1280Γ—760) Β· `none` (bare).
66
+ Optional: `devices` (variant array, e.g. `["ios","android"]` β€” one file rendered in each
67
+ listed frame; self-test covers all of them), `status` (draft/wip/ready/review), `tags`,
68
+ `group` (sidebar section), `statusBar` (`light`|`dark` β€” overrides status-bar/home-bar
69
+ ink on full-bleed art), `updated` (stamped automatically on save; manual override kept).
70
+ - Exactly one bridge include: `<script src="../../../engine/bridge.js"></script>`.
71
+ - Screens must render sensibly with **empty** `Engine.data` and `Engine.state`.
72
+ - Static screens with zero JS are perfectly legal.
73
+ - Sheets/modals live **inside** the screen that opens them (hidden markup until triggered).
74
+ - Markup duplication across screens is normal β€” there is no templating system. Shared look
75
+ goes in `screens/shared.css` + `shared.js`, included explicitly per screen.
76
+ - Screens may pull any library via CDN (`<script src="https://…">`) β€” iframe isolation makes
77
+ that safe. Keep it inside the screen.
78
+ - Never draw device hardware (status bar, notch, home bar) β€” the shell's frame owns it.
79
+ Pad with `var(--safe-top)` / `var(--safe-bottom)` instead.
80
+
81
+ ## Bridge API (everything a screen can use)
82
+
83
+ | API | Behavior |
84
+ |-----|----------|
85
+ | `Engine.ready(cb)` | `cb(data)` after theme + data are injected. Late registration still fires. |
86
+ | `Engine.data` | Frozen merged mock data: seed βŠ• active scenario, `{{todayΒ±N}}` resolved. |
87
+ | `Engine.state` | Shared KV across screens: `get(k, fallback)` / `set(k, v)` / `subscribe(k, cb)` / `keys()`. Survives screen switches; shell has a reset button. |
88
+ | `Engine.go(id)` / `data-go="id"` | Navigate to a sibling screen. Standalone fallback: plain `<id>.html` link. |
89
+ | `Engine.toast(msg)` | Transient toast inside the frame. |
90
+ | `Engine.env` | `{ mode, dir, lang, device, scenario, theme, embedded }`. |
91
+ | `Engine.meta` | This screen's parsed meta block. |
92
+ | CSS vars | Active theme flattened to `--color-*`, `--font-*`, `--radius-*`, `--space-*` on `:root`, plus `--safe-top` / `--safe-bottom`. The bridge also sets `data-mode="light|dark"`, `data-device`, `dir`, `lang` on `<html>`. |
93
+
94
+ Dark mode: style with tokens and they flip automatically; for bespoke dark rules use
95
+ `html[data-mode="dark"] .thing { … }`. RTL: layout flips via `dir` β€” use logical CSS
96
+ (`margin-inline-start`, `inset-inline-end`) instead of left/right when possible.
97
+
98
+ Opened standalone (no shell), a screen self-loads the default theme + seed and accepts
99
+ `?scenario=…&mode=dark&dir=rtl&lang=ar&theme=…` query params.
100
+
101
+ ## Data scenarios (`data/scenarios.json`)
102
+
103
+ ```json
104
+ { "list": [
105
+ { "id": "default", "icon": "πŸ§ͺ", "title": "Full demo", "description": "Baseline seed." },
106
+ { "id": "new-user", "icon": "πŸ†•", "title": "Brand-new user",
107
+ "remove": ["trips"], "patch": { "user": { "isNew": true } } } ] }
108
+ ```
109
+
110
+ `patch` deep-merges over the seed (arrays replace), `remove` deletes dot-paths first.
111
+ Date tokens `{{today}}`, `{{today+3}}`, `{{today-2}}` resolve to real ISO dates at injection β€”
112
+ "mid-trip today" is literally today. Always keep a `default` entry.
113
+
114
+ ## Themes (`themes/<id>.json`)
115
+
116
+ ```json
117
+ { "id": "default", "title": "Warm",
118
+ "modes": {
119
+ "light": { "color": { "accent": "#BF5B24", "bg": "#F7F2EA", "ink": "#241C13", "ink-2": "#6E6152" },
120
+ "font": { "sans": "-apple-system, sans-serif" }, "radius": { "card": "20px" } },
121
+ "dark": { "color": { "accent": "#E1793B", "bg": "#171108", "ink": "#F4EBDD" } } } }
122
+ ```
123
+
124
+ Flattening: `color.ink-2` β†’ `--color-ink-2` (nested paths join with `-`). `dark` deep-merges
125
+ over `light`, so only list what changes. Style screens **with tokens**, not hard-coded colors,
126
+ and the whole project can be re-themed from JSON β€” humans do exactly that in the shell's
127
+ 🎨 **Theme Lab** (live token editing; Save writes `themes/<id>.json` under `engine serve`),
128
+ so a theme file may change without an agent touching it. Re-read before editing.
129
+
130
+ ## Strings (`i18n.json` + `Engine.t`) β€” optional per feature
131
+
132
+ Key-first: `{ "slide1.title": { "en": "…", "ar": "…" } }`. Screens call
133
+ `Engine.t('slide1.title')` (placeholders: `Engine.t('greet', {name})`). Fallback: active
134
+ language β†’ `defaultLanguage` β†’ the key. Language switches reload the screen, so ready-time
135
+ binding is always right. Missing translations are validate WARNINGS, never blockers.
136
+ Features without i18n.json simply own their text inline β€” also fine.
137
+
138
+ ## Flows (`flows.json`)
139
+
140
+ ```json
141
+ { "list": [ { "id": "create-trip", "title": "Create a trip", "icon": "✨",
142
+ "description": "From empty home to a planned trip.",
143
+ "steps": [
144
+ { "screen": "010-home", "note": "Start here β€” empty state invites planning." },
145
+ { "screen": "020-wizard", "note": "Seven questions.", "scenario": "new-user" } ] } ] }
146
+ ```
147
+
148
+ The shell plays these as guided stakeholder walkthroughs (prev/next + note captions).
149
+ A step may pin a `scenario` (and `mode`). Steps must reference existing screens.
150
+
151
+ ## Embedded apps (app mode)
152
+
153
+ A feature with `"type": "app"` embeds a WHOLE single-page app: `src` (project-relative entry
154
+ HTML), `device`, `chrome: "self"|"engine"` (self = the app draws its own status bars), and
155
+ optionally `driver: "driver.js"` β€” a project-owned script injected into the app that calls
156
+ `AppBridge.register({screens, go, current?, set?, scenarios?})` so sealed apps stay untouched.
157
+ Cooperative apps include `engine/app-bridge.js` themselves. Register via CLI
158
+ (`screenbook app add <src>`) or MCP (`add_app`). Full contract: the skill's
159
+ `references/app-mode.md`. Validation boots the app and visits every reported screen.
160
+
161
+ ## Review comments
162
+
163
+ Humans pin comments on screens in Review Mode; they land in `review/comments.json` with full
164
+ context (screen, scenario, theme, mode, dir, CSS path of the clicked element). Workflow:
165
+ read them (`get_review_comments`), fix the screens, then `resolve_comment` β€” don't delete.
166
+
167
+ ## Doing the work
168
+
169
+ Prefer the MCP tools (`add_screen`, `update_screen`, `set_data`, `validate`, `screenshot`, …) β€”
170
+ they validate the contract, stamp `updated`, regenerate manifests and write atomically.
171
+ Editing screen files directly is fine too; afterwards run:
172
+
173
+ ```
174
+ node engine/cli/screenbook.js manifest # rescan β†’ manifest.json
175
+ node engine/cli/screenbook.js validate # static checks + headless render of every screen
176
+ ```
177
+
178
+ While `node engine/cli/screenbook.js serve` runs (http://127.0.0.1:4600), every save
179
+ live-reloads the browser and manifests regenerate automatically β€” no cache-busting, ever.
180
+
181
+ **Definition of done: `validate` is green** β€” zero static errors, every screen renders in
182
+ every theme Γ— light/dark Γ— ltr/rtl with zero console errors. Check review comments too.
183
+
184
+ Hard rules: never edit `manifest.json` (derived) Β· never edit `engine/` Β· never rename a
185
+ screen that other screens `data-go` to (or fix the references + flows in the same change).
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abdullah Alhaider
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # ⧉ ScreenBook
2
+
3
+ **A static, no-build prototype studio β€” built for AI agents to author, humans to review.**
4
+
5
+ Drop folders of plain HTML screens plus small JSON files into a project and get a navigable,
6
+ device-framed design gallery: iPhone / Android / browser frames, mock-data scenarios, design-token
7
+ themes, dark/light and LTR/RTL toggles, guided demo flows, pinned review comments, and a
8
+ machine-verified self-test β€” plus a CLI and an MCP server so agents can build screens
9
+ **without ever holding the engine's implementation in context**.
10
+
11
+ ![The ScreenBook studio β€” Qahwa demo project](docs/shots/hero-studio.png)
12
+
13
+ ## Why this exists
14
+
15
+ Prototyping with AI agents has a failure mode: the prototype starts as "just an HTML file,"
16
+ grows into a hand-rolled single-page app, and after a month you have a 20k-line runtime where
17
+ **an agent can't touch one screen without reading the whole world** β€” and neither can you.
18
+
19
+ ScreenBook inverts every cause of that rot:
20
+
21
+ | Instead of… | ScreenBook does… |
22
+ |---|---|
23
+ | One growing SPA runtime | **One file = one screen.** Full standalone HTML documents; each opens alone in a browser. |
24
+ | Metadata trapped in code | **Metadata as data.** A JSON block inside each screen; indexes are derived, never hand-kept. |
25
+ | App code and viewer code tangled | **A hard engine/app boundary.** Screens know one include (`bridge.js`) and a tiny API. |
26
+ | Hard-coded demo data | **Seed + scenario overlays** with `{{todayΒ±N}}` date tokens β€” "mid-trip today" is literally today. |
27
+ | CSS forks per restyle | **Design tokens as JSON themes**, flattened to CSS variables, dark mode as a delta. |
28
+ | "Trust me, it works" | **A self-test gate**: every screen Γ— theme Γ— light/dark Γ— LTR/RTL must render with zero console errors. |
29
+
30
+ | | | |
31
+ |---|---|---|
32
+ | ![Web dashboard frame](docs/shots/web-dashboard.png) | ![Android frame, dark](docs/shots/android-dark.png) | ![Two-theme re-skin](docs/shots/theming-noir.png) |
33
+ | Browser-chrome frame, scenario-driven data | Android frame, dark mode | Same screens, different theme JSON |
34
+
35
+ ## Setup
36
+
37
+ Requirements: **Node β‰₯ 18**. For headless validation/screenshots: Google Chrome (or see below).
38
+
39
+ Full guides live in **[docs/](docs/README.md)** β€” getting started & prerequisites, the
40
+ studio tour, the authoring guide, CLI/MCP references, app mode, troubleshooting.
41
+
42
+ ```bash
43
+ # 1. Put the engine into your project (this repo IS the engine β€” folder name matters):
44
+ git clone https://github.com/cs4alhaider/screenbook my-prototypes/engine
45
+ # (npm: @cs4alhaider/screenbook β€” after first publish)
46
+
47
+ # 2. Scaffold the project around it:
48
+ cd my-prototypes
49
+ node engine/cli/screenbook.js init .
50
+
51
+ # 3. Run the studio:
52
+ node engine/cli/screenbook.js serve .
53
+ # β†’ http://127.0.0.1:4600 Β· live reload Β· manifests auto-regenerate Β· no cache, ever
54
+ ```
55
+
56
+ `init` creates `app.config.json`, a starter theme, a `welcome` feature with one screen β€” and
57
+ drops **`CLAUDE.md`** (the authoring contract) plus a **packaged agent skill** into the project,
58
+ so any AI agent that opens it immediately knows the rules.
59
+
60
+ For headless `validate` / `screenshot`, install the one dev dependency once:
61
+
62
+ ```bash
63
+ cd engine && npm i # playwright-core β€” drives your installed Chrome
64
+ # no Chrome? use: npm i -D playwright && npx playwright install chromium
65
+ ```
66
+
67
+ Try the shipped examples first:
68
+
69
+ ```bash
70
+ node engine/cli/screenbook.js serve engine/examples/qahwa # β˜• coffee app: iOS + Android + web
71
+ node engine/cli/screenbook.js serve engine/examples/tokens-lab # 🎨 two themes, live token lab
72
+ ```
73
+
74
+ ## How it helps β€” a five-minute story
75
+
76
+ **1. A screen is one honest file.** No framework, no build, one conventional include:
77
+
78
+ ```html
79
+ <!DOCTYPE html>
80
+ <html lang="en">
81
+ <head>
82
+ <script type="application/json" id="screen-meta">
83
+ { "title": "Cart", "description": "Lines, totals, checkout.",
84
+ "order": 30, "device": "ios", "status": "wip" }
85
+ </script>
86
+ <script src="../../../engine/bridge.js"></script>
87
+ </head>
88
+ <body>
89
+ <button data-go="040-checkout">Checkout</button>
90
+ <script>
91
+ Engine.ready(function (data) { /* seed βŠ• active scenario, dates resolved */ });
92
+ </script>
93
+ </body>
94
+ </html>
95
+ ```
96
+
97
+ Save it under `features/shop/screens/030-cart.html` β€” it's already in the sidebar, framed as an
98
+ iPhone, styled by your tokens (`var(--color-accent)`), live-reloading on every save.
99
+
100
+ **2. Colors are a panel, not a hunt.** The 🎨 **Theme Lab** shows every token the active theme
101
+ defines and edits them **live against the hosted screens** β€” color pickers for hex values,
102
+ plain fields for fonts/radii/spacing, a theme switcher, Export, and one-click **Save** straight
103
+ back to `themes/<id>.json` while `serve` runs:
104
+
105
+ ![Theme Lab β€” live token editing](docs/shots/theme-lab.png)
106
+
107
+ **3. States are data, not branches.** Add a scenario and the 🎭 dropdown demos your empty state:
108
+
109
+ ```json
110
+ { "id": "first-visit", "icon": "πŸ†•", "title": "First visit",
111
+ "remove": ["order"], "patch": { "user": { "isNew": true } } }
112
+ ```
113
+
114
+ **4. Demos tell themselves.** A `flows.json` walkthrough gives stakeholders a guided,
115
+ captioned story β€” each step can pin its own scenario, and every state is a shareable URL:
116
+ `…#shop/030-cart?scenario=first-visit&mode=dark&dir=rtl`.
117
+
118
+ **5. Review closes the loop.** Reviewers pin comments directly on screens; each pin captures
119
+ screen, scenario, theme, mode, direction and the exact element clicked, and syncs to
120
+ `review/comments.json` β€” where agents read and resolve them.
121
+
122
+ **6. The machine decides "done".**
123
+
124
+ ```bash
125
+ node engine/cli/screenbook.js validate .
126
+ # βœ“ static contract checks
127
+ # βœ“ 28/28 render checks green across 7 screens (Γ— themes Γ— dark/light Γ— ltr/rtl, zero console errors)
128
+ ```
129
+
130
+ ## The agent workflow (the actual point)
131
+
132
+ Register the MCP server with your agent β€” for Claude Code:
133
+
134
+ ```bash
135
+ claude mcp add screenbook -- npx -y @cs4alhaider/screenbook mcp --root .
136
+ # or from a checkout: claude mcp add screenbook -- node engine/cli/screenbook.js mcp --root .
137
+ ```
138
+
139
+ Then ask for work in plain language:
140
+
141
+ > *"Add a checkout screen to the shop feature: summary card, payment method list from the seed,
142
+ > a pay button that toasts. Match the existing screens. Validate and show me a screenshot."*
143
+
144
+ The agent never opens `engine/` β€” it calls `list_screens` β†’ `get_screen` (steals an existing
145
+ screen's idioms) β†’ `add_screen` (which **lints the contract, stamps the date, regenerates the
146
+ manifest, writes atomically**) β†’ `validate` (full headless render matrix) β†’ `screenshot`
147
+ (a PNG inside the real device frame). Human taste, machine discipline.
148
+
149
+ 18 tools: `list_features Β· list_screens Β· get_screen Β· add_screen Β· update_screen Β·
150
+ remove_screen Β· add_feature Β· get_data Β· set_data Β· get_theme Β· set_theme Β· get_flows Β·
151
+ set_flows Β· get_review_comments Β· resolve_comment Β· validate Β· screenshot Β· add_app`.
152
+
153
+ ## CLI
154
+
155
+ | Command | Does |
156
+ |---|---|
157
+ | `init [dir] [--name X]` | Scaffold a project + authoring contract + agent skill |
158
+ | `serve [dir] [--port]` | Studio server: live reload, auto-manifests, review sync, `/engine/*` mounted from this package, no-cache always |
159
+ | `manifest [dir]` | Rescan screens β†’ regenerate derived `manifest.json` per feature |
160
+ | `validate [dir] [--static] [--json]` | Contract checks + headless render of every screen Γ— theme Γ— mode Γ— direction |
161
+ | `screenshot [dir] --screen f/s [--mode light,dark] [--dir ltr,rtl] [--scenario id] [--theme id]` | Device-framed PNGs (comma lists = matrix) |
162
+
163
+ ## What's in this repo
164
+
165
+ ```
166
+ bridge.js the ONLY file screens include (dual-mode: shell-driven / standalone)
167
+ index.html + shell/ the studio: frames, dropdowns, flows, review mode, selftest
168
+ core/ shared project API (scan, manifests, atomic writes, validate, serve, browser)
169
+ cli/ Β· mcp/ two thin faces over the same core
170
+ CLAUDE.md the authoring contract agents follow (scaffolded into projects)
171
+ skill/ packaged agent skill + reference docs (scaffolded into projects)
172
+ examples/ qahwa (β˜• 3-device demo app) Β· tokens-lab (🎨 multi-theme lab)
173
+ test/ 42-test suite: unit + serve E2E + CLI E2E β†’ npm test
174
+ TESTING.md the full test map and gaps
175
+ ```
176
+
177
+ RTL and Arabic are first-class (`dir` flipping, per-screen language dictionaries):
178
+
179
+ <img src="docs/shots/rtl-arabic.png" width="320" alt="RTL Arabic screen in dark mode">
180
+
181
+ ## App mode β€” embed entire existing apps
182
+
183
+ Already have a single-page prototype? Don't rewrite it β€” **embed it**:
184
+
185
+ ```bash
186
+ node engine/cli/screenbook.js app add legacy/index.html --device ios --chrome self
187
+ ```
188
+
189
+ The app's views appear in the sidebar as screens, deep links work, dark/RTL/scenario toggles
190
+ drive it live. Cooperative apps include `engine/app-bridge.js` and register themselves
191
+ (`examples/spa-demo`); sealed apps get a small project-owned **driver** injected at runtime β€”
192
+ **zero changes to the app's own files**. `validate` boots each app and visits every screen it
193
+ reports, still demanding zero console errors.
194
+
195
+ ![A sealed legacy app running inside the studio](docs/shots/app-mode.png)
196
+
197
+ ## Status
198
+
199
+ **v1 β€” complete and machine-verified** (studio, 4 frames, scenarios, flows, Theme Lab,
200
+ review mode, selftest, CLI, MCP, agent onboarding, example projects, 44-test suite).
201
+ **v2 β€” in progress**: app mode βœ“ shipped (above) Β· theme write-back βœ“ (Theme Lab Save) Β·
202
+ i18n strings βœ“ (`Engine.t` + key-first per-feature `i18n.json`, fallback chain, validate
203
+ warnings on gaps) Β· npm packaging next.
204
+
205
+ Born inside [Rahhal](https://github.com/cs4alhaider)'s product lab, where the predecessor
206
+ prototypes grew to ~22k lines and taught every lesson this engine inverts.
207
+
208
+ License: MIT.
package/TESTING.md ADDED
@@ -0,0 +1,54 @@
1
+ # ScreenBook β€” testing map
2
+
3
+ > The "don't forget" file: every test layer, what it covers, how to run it, and where the
4
+ > gaps are. Update this whenever a layer is added or a gap closes. Definition of done for
5
+ > any engine change: **`npm test` green AND `validate` green on all four projects below.**
6
+
7
+ ## How to run everything
8
+
9
+ ```bash
10
+ cd engine
11
+ npm i # once β€” playwright-core (drives your installed Chrome)
12
+ npm test # unit + E2E suites (node:test, ~58 tests, ~60s incl. browser)
13
+
14
+ # full browser matrix on every project that ships in this repo:
15
+ node cli/screenbook.js validate .. # Rahhal (repo root)
16
+ node cli/screenbook.js validate examples/qahwa
17
+ node cli/screenbook.js validate examples/tokens-lab
18
+ ```
19
+
20
+ ## Layers
21
+
22
+ | Layer | File(s) | Covers |
23
+ |-------|---------|--------|
24
+ | **EngineKit unit** | `test/kit.test.js` | The REAL `bridge.js` loaded in a Node VM exactly as the shell loads it: deepMerge (arrays replace), scenario overlay (remove→patch order, seed immutability), `{{today±N}}` resolution incl. embedded strings, token flattening, dark-over-light theme merge, fallback safety. |
25
+ | **Meta contract unit** | `test/meta.test.js` | Meta block parse errors, `withMeta` round-trip, `updated` stamping incl. the manual-override rule, every `lintScreen` violation, `data-go` extraction, filename-prefix order fallback. |
26
+ | **Atomic fs unit** | `test/fsx.test.js` | Atomic write (no temp leftovers, parent dirs), change-detection writes, JSON fallback/error paths. |
27
+ | **Project API unit** | `test/project.test.js` | Scan/sort, manifest regen stability (no churn), screen CRUD guards (lint reject, dup, missing feature), data/theme/flows shape validation + config registration, review comment filters/resolve, `staticValidate` catching dangling `data-go`, missing/stale manifests, broken flow refs. |
28
+ | **Serve E2E** | `test/serve.test.js` | Real HTTP on an ephemeral port: health, `/` redirect, `/engine/*` package mount, path-traversal refusal, no-cache headers, review GET/POST β†’ disk, **Theme-Lab write-back** (`POST /__sb/theme/:id` β†’ file, bad-shape/bad-id 400s), and the **live-reload pipeline**: raw file write β†’ SSE event β†’ debounced manifest auto-regen. |
29
+ | **CLI E2E** | `test/cli.test.js` | Spawns the real binary per command: `init` (full scaffold incl. CLAUDE.md + skill, `--force` guard), `manifest` (regenerated β†’ unchanged), `validate --static --json` (green β†’ red on a broken screen, exit codes), `serve` (parses printed URL, fetches studio/health/screen), `validate` **full browser** on the init-scaffolded project, `screenshot` (PNG on disk), error paths (no project β†’ exit 2, bad `--screen` spec β†’ exit 2). |
30
+ | **Browser matrix** | shell `?selftest=1` via `validate` | Every screen Γ— theme Γ— light/dark Γ— ltr/rtl: bridge handshake, `Engine.ready` fires, meta present, **zero console errors** (screen AND shell). This is the machine-verified fixed point gate. |
31
+ | **App-mode gate** | same `?selftest=1` pass | Every `type:"app"` feature: app must register (hello) within 20s, report β‰₯1 screen, and **every reported screen is visited once** β€” zero console errors (driver failures surface as `driver <fn>:` rows; adapters may declare narrow `ignoreErrors` for third-party noise, e.g. MapLibre teardown). |
32
+ | **Shell interaction E2E** | `test/shell.test.js` | Real Chrome driving the studio against a purpose-built fixture: boot + data binding, live dark/RTL env, `data-go` β†’ deep link, scenario overlay (`remove`) on reload, flow deep links + stepping with pinned scenarios, `Engine.state` across screens + reset chip, **review pinning β†’ disk β†’ resolve**, Theme-Lab live edit reaching the screen, **app mode** (embedded cooperative app: screen list, navigation, env follow). |
33
+ | **MCP session E2E** | `test/mcp.test.js` | Spawns the real stdio server against an init-scaffolded project: initialize (protocol echo + instructions), 18-tool surface, contract enforcement (lint reject, dup reject), full CRUD incl. `add_app`, static validate through the tool, JSON-RPC error shapes (-32601/-32602). |
34
+
35
+ ## Projects that double as fixtures
36
+
37
+ | Project | Exercises |
38
+ |---------|-----------|
39
+ | repo root (Rahhal) | iOS frame, AR/RTL/i18n screens, scenarios with `remove`, flows, `statusBar` override, shared.css/js β€” **plus both certified prototypes embedded as driven apps** (`features/rahhal-ios` 31 screens Β· `features/rahhal-web` 11 screens, drivers injected, `docs/mockups/` byte-untouched) |
40
+ | `examples/qahwa` | iOS + **Android** + **web** frames in one project, in-screen sheet (EN-24), `Engine.state` cart, feature groups, device filter |
41
+ | `examples/tokens-lab` | **Multi-theme** (Theme β–Ύ + theme axis in the matrix), **bare `none` frame**, live token re-skin, MutationObserver pattern |
42
+ | `examples/spa-demo` | **Cooperative app mode** β€” a one-page notes app including `app-bridge.js` itself; engine chrome on an app |
43
+ | `screenbook init` output | The scaffold itself (validated headlessly inside `test/cli.test.js` on every run); `app add` covered too |
44
+
45
+ ## Known gaps (candidates for the next hardening pass)
46
+
47
+ - ~~Shell interaction suite~~ β†’ **promoted to `test/shell.test.js`** (2026-07-18).
48
+ - ~~MCP server E2E~~ β†’ **promoted to `test/mcp.test.js`** (2026-07-18).
49
+ - Keyboard navigation (↑↓/jk, `/`, flow arrows) not yet asserted in the shell suite.
50
+ - RTL screenshot diffing / visual regression (screenshots exist; no baseline comparison).
51
+ - Windows: paths are POSIX-tested only; `fs.watch` recursive + rename semantics unverified.
52
+ - `file://` standalone mode has no automated check (manual only; fallback tokens by design).
53
+ - Driven-app injection path in the shell suite uses the cooperative mini-app only; the two
54
+ Rahhal drivers are covered by root `validate` (64/64), not by `npm test`.
package/app-bridge.js ADDED
@@ -0,0 +1,131 @@
1
+ /*
2
+ * ScreenBook app-bridge β€” app mode (EN-20). Runs INSIDE an embedded
3
+ * single-page app and speaks the sba:* postMessage protocol with the shell.
4
+ *
5
+ * Two ways it gets into the app page:
6
+ * cooperative β€” the SPA includes this file itself and calls
7
+ * AppBridge.register(adapter) from its own code
8
+ * driven β€” the shell injects this file, then injects a project-owned
9
+ * driver script that calls register(); the app's own files
10
+ * stay byte-untouched (how the certified prototypes plug in)
11
+ *
12
+ * adapter = {
13
+ * title?: string
14
+ * screens(): [{ id, title, description?, group? }] (required)
15
+ * go(id): navigate the app to a screen (required)
16
+ * current?(): current screen id
17
+ * set?(env): { mode?, dir?, lang?, scenario?, theme?, vars? } applied live
18
+ * scenarios?(): [{ id, icon?, title, description? }] (maps the app's own 🎭)
19
+ * }
20
+ * Call AppBridge.navigated(id) whenever the app navigates on its own.
21
+ */
22
+ (function () {
23
+ 'use strict';
24
+
25
+ var shell = window.parent !== window ? window.parent : null;
26
+ var adapter = null;
27
+ var ignoreErrors = [];
28
+
29
+ function send(type, payload) {
30
+ if (!shell) return;
31
+ var msg = { type: type };
32
+ if (payload) Object.keys(payload).forEach(function (k) { msg[k] = payload[k]; });
33
+ try { shell.postMessage(msg, '*'); } catch (e) { /* shell gone */ }
34
+ }
35
+
36
+ function reportError(message) {
37
+ var text = String(message);
38
+ /* adapters may declare known-benign noise from a sealed app's own libraries
39
+ (adapter.ignoreErrors: [RegExp|string]) β€” declared, visible, reviewable */
40
+ for (var i = 0; i < ignoreErrors.length; i++) {
41
+ if (ignoreErrors[i].test(text)) return;
42
+ }
43
+ send('sba:console-error', { message: text });
44
+ }
45
+
46
+ function guarded(fn, label) {
47
+ try { return fn(); }
48
+ catch (err) {
49
+ reportError('driver ' + label + ': ' + (err && err.message ? err.message : err));
50
+ return undefined;
51
+ }
52
+ }
53
+
54
+ function normalizeScreens(list) {
55
+ return (list || []).map(function (s) {
56
+ if (typeof s === 'string') return { id: s, title: s };
57
+ return { id: s.id, title: s.title || s.id, description: s.description, group: s.group };
58
+ }).filter(function (s) { return s.id; });
59
+ }
60
+
61
+ function pushScreens() {
62
+ if (!adapter) return;
63
+ send('sba:screens', { screens: normalizeScreens(guarded(function () { return adapter.screens(); }, 'screens()')) });
64
+ }
65
+
66
+ window.AppBridge = {
67
+ register: function (a) {
68
+ adapter = a || {};
69
+ ignoreErrors = (adapter.ignoreErrors || []).map(function (p) {
70
+ return p instanceof RegExp ? p : new RegExp(String(p));
71
+ });
72
+ var capabilities = ['go'];
73
+ if (adapter.set) capabilities.push('env');
74
+ if (adapter.scenarios) capabilities.push('scenarios');
75
+ var hello = { title: adapter.title || document.title || 'App', capabilities: capabilities };
76
+ if (adapter.scenarios) {
77
+ hello.scenarios = guarded(function () { return adapter.scenarios(); }, 'scenarios()') || [];
78
+ }
79
+ send('sba:hello', hello);
80
+ pushScreens();
81
+ var cur = adapter.current && guarded(function () { return adapter.current(); }, 'current()');
82
+ if (cur) send('sba:current', { id: cur });
83
+ },
84
+ navigated: function (id) { send('sba:current', { id: id }); },
85
+ refreshScreens: pushScreens
86
+ };
87
+
88
+ window.addEventListener('message', function (e) {
89
+ var msg = e.data;
90
+ if (!msg || typeof msg.type !== 'string' || !adapter) return;
91
+ switch (msg.type) {
92
+ case 'sba:go':
93
+ guarded(function () { adapter.go(msg.id); }, 'go(' + msg.id + ')');
94
+ send('sba:current', { id: msg.id });
95
+ break;
96
+ case 'sba:env':
97
+ if (adapter.set) {
98
+ guarded(function () {
99
+ adapter.set({ mode: msg.mode, dir: msg.dir, lang: msg.lang, scenario: msg.scenario, theme: msg.theme, vars: msg.vars });
100
+ }, 'set(env)');
101
+ }
102
+ break;
103
+ case 'sba:ping':
104
+ send('sba:pong', {});
105
+ break;
106
+ }
107
+ });
108
+
109
+ /* console/error forwarding so `validate` can hold apps to zero errors */
110
+ if (shell) {
111
+ window.addEventListener('error', function (e) {
112
+ if (e.target && e.target !== window && e.target.tagName) {
113
+ reportError('Resource failed: <' + e.target.tagName.toLowerCase() + '> ' + (e.target.src || e.target.href || ''));
114
+ } else {
115
+ reportError((e.message || 'Script error') + (e.filename ? ' (' + e.filename.split('/').pop() + ':' + e.lineno + ')' : ''));
116
+ }
117
+ }, true);
118
+ window.addEventListener('unhandledrejection', function (e) {
119
+ reportError('Unhandled rejection: ' + (e.reason && e.reason.message ? e.reason.message : e.reason));
120
+ });
121
+ var origConsoleError = console.error;
122
+ console.error = function () {
123
+ reportError(Array.prototype.map.call(arguments, function (a) {
124
+ if (typeof a === 'string') return a;
125
+ if (a && a.stack) return String(a.stack);
126
+ try { return JSON.stringify(a); } catch (e) { return String(a); }
127
+ }).join(' '));
128
+ return origConsoleError.apply(console, arguments);
129
+ };
130
+ }
131
+ })();