@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
@@ -0,0 +1,77 @@
1
+ # App mode — embedding whole single-page apps
2
+
3
+ An app-type feature puts an ENTIRE existing SPA inside the ScreenBook shell: its views appear
4
+ in the sidebar as screens, deep links work, dark/RTL/scenario toggles drive it — without
5
+ rewriting the app as file-per-screen.
6
+
7
+ ## Registering
8
+
9
+ CLI (preferred): `screenbook app add <src> [--id x --title "X" --icon 🧩 --device ios --chrome self|engine --cooperative]`
10
+ or during scaffold: `screenbook init . --app path/to/index.html`. MCP: `add_app {src, …}`.
11
+
12
+ This writes an app-type feature:
13
+
14
+ ```json
15
+ { "id": "legacy-ios", "title": "Legacy app", "icon": "🧩", "order": 9,
16
+ "type": "app",
17
+ "src": "apps/legacy/index.html",
18
+ "device": "ios",
19
+ "chrome": "self",
20
+ "driver": "driver.js" }
21
+ ```
22
+
23
+ - `src` — project-root-relative path to the app's entry HTML (served same-origin).
24
+ - `device` — which bezel frames it (`ios`/`android`/`web`/`none`).
25
+ - `chrome` — `"self"` when the app draws its own status/home bars (engine shows bezel only);
26
+ `"engine"` for full engine chrome.
27
+ - `driver` — present for **driven** (sealed) apps; absent for **cooperative** apps.
28
+
29
+ ## Two integration modes, one protocol
30
+
31
+ **Cooperative** — the app includes the runtime itself and registers:
32
+
33
+ ```html
34
+ <script src="../engine/app-bridge.js"></script>
35
+ <script>
36
+ AppBridge.register({
37
+ title: 'Notes',
38
+ screens: () => [{ id: 'inbox', title: 'Inbox', description: '…', group: '…' }],
39
+ go: (id) => router.show(id),
40
+ current: () => router.current,
41
+ set: (env) => { /* { mode, dir, lang, scenario, theme, vars } */
42
+ document.body.classList.toggle('dark', env.mode === 'dark');
43
+ },
44
+ scenarios: () => [{ id: 'empty', title: 'Empty state' }] // optional: maps the app's own 🎭
45
+ });
46
+ </script>
47
+ ```
48
+
49
+ Call `AppBridge.navigated(id)` whenever the app navigates on its own (the shell follows).
50
+ An adapter may also declare `ignoreErrors: [/pattern/]` — known-benign console noise from a
51
+ sealed app's own libraries (e.g. a CDN map library's teardown race). Use narrowly and comment
52
+ why; everything else still fails validation.
53
+
54
+ **Driven** — the app's files stay byte-untouched. A `driver.js` in the FEATURE folder (not the
55
+ app!) is injected by the shell after the app loads, and makes the same `AppBridge.register`
56
+ call by reaching into the app's own globals (registries, router, toggles). `app add` scaffolds
57
+ a commented template. Drivers may poll briefly if the app boots async.
58
+
59
+ ## Behavior in the shell
60
+
61
+ - Sidebar lists the app-reported screens (title/description/group; no status pills or dates).
62
+ - Screen click / deep link `#feature/screen` → `sba:go` into the app; in-app navigation
63
+ reflects back into the URL.
64
+ - Mode/dir/lang/theme/scenario changes arrive live via `set(env)` — `env.vars` carries the
65
+ flattened theme tokens if the app wants to adopt them. No reloads.
66
+ - If the driver reports `scenarios()`, they populate the 🎭 dropdown for this feature.
67
+ - flows.json works over app screen ids too (steps play through the app).
68
+
69
+ ## Validation gate (what `validate` requires)
70
+
71
+ Static: `src` exists · declared driver file exists. Browser: the app must **register within
72
+ 20s, report ≥1 screen, and every reported screen is visited once — zero console errors
73
+ anywhere** (the runtime forwards the app's console.error/uncaught errors). A broken driver
74
+ fails loudly with `driver <fn>: <message>` rows.
75
+
76
+ Debug tips: veil says "App hasn't registered" → driver didn't run or threw before register
77
+ (check the browser console via `serve`); zero screens → `screens()` returned `[]`.
@@ -0,0 +1,55 @@
1
+ # Bridge API (`window.Engine`)
2
+
3
+ The bridge is the single include every screen carries. It is dual-mode: driven by the shell
4
+ over postMessage when embedded, self-booting when the screen is opened directly.
5
+
6
+ ## Lifecycle
7
+
8
+ ```js
9
+ Engine.ready(function (data) {
10
+ // called once theme vars + data are applied; `data` === Engine.data
11
+ });
12
+ ```
13
+
14
+ - Register any time — late callbacks fire immediately (async microtask).
15
+ - On scenario/data changes the shell reloads the iframe, so `ready` re-runs cleanly.
16
+ - Mode / direction / language / theme changes apply LIVE (no reload): CSS vars and the
17
+ `<html>` attributes just change. React to them with CSS, not JS, whenever possible.
18
+
19
+ ## Surface
20
+
21
+ | API | Detail |
22
+ |-----|--------|
23
+ | `Engine.data` | Frozen object: seed ⊕ scenario (`patch`/`remove`), `{{today±N}}` already resolved. Never mutate; copy what you need. |
24
+ | `Engine.state` | Cross-screen KV on localStorage: `get(key, fallback)` · `set(key, value)` (JSON-serializable) · `subscribe(key, cb) → unsubscribe` · `keys()`. Fires across iframes via storage events. Namespaced `sb:state:*`; the shell's ↺ button clears it. |
25
+ | `Engine.go(id)` | Same as clicking `data-go="id"`. Same-feature navigation only. |
26
+ | `Engine.toast(msg)` | Token-styled toast at the safe-area bottom, auto-dismisses. |
27
+ | `Engine.t(key, vars?)` | Strings from the feature's `i18n.json` (key-first): active language → `defaultLanguage` → the key itself; `{name}` placeholders interpolate from `vars`. Language switches reload the screen, so binding at ready-time is always correct. See `i18n.md`. |
28
+ | `Engine.env` | `{ mode: 'light'\|'dark', dir: 'ltr'\|'rtl', lang, device, scenario, theme, embedded }`. Read-only snapshot; attributes on `<html>` mirror it live. |
29
+ | `Engine.meta` | This screen's parsed meta block. |
30
+ | `Engine.kit` | The pure helpers (deepMerge, applyScenario, resolveDateTokens, themeVars) if you ever need them. |
31
+
32
+ ## Environment on `<html>`
33
+
34
+ The bridge sets: `data-mode="light|dark"` · `data-device` · `dir` · `lang` · all theme CSS
35
+ vars · `--safe-top` / `--safe-bottom`.
36
+
37
+ Idioms:
38
+
39
+ ```css
40
+ /* dark-specific rule when tokens aren't enough */
41
+ html[data-mode="dark"] .hero { filter: brightness(.9); }
42
+
43
+ /* RTL-safe spacing — prefer logical properties over left/right */
44
+ .row { padding-inline-start: 16px; margin-inline-end: 8px; }
45
+ ```
46
+
47
+ ```js
48
+ /* language-aware copy — screens own their strings (no engine i18n in v1) */
49
+ var copy = Engine.env.lang === 'ar' ? AR : EN;
50
+ ```
51
+
52
+ ## Errors
53
+
54
+ Uncaught errors, failed resource loads, unhandled rejections and `console.error` calls are
55
+ forwarded to the shell and FAIL the self-test. Keep the console clean — that's the contract.
@@ -0,0 +1,53 @@
1
+ # CLI + MCP
2
+
3
+ Both are thin faces over the same core (scan, parse meta, regenerate manifests, atomic
4
+ writes, stamp `updated`, validate). Prefer MCP tools when available — they validate the
5
+ contract and keep manifests fresh automatically.
6
+
7
+ ## CLI (`node engine/cli/screenbook.js …` or the `screenbook` bin)
8
+
9
+ | Command | Does |
10
+ |---------|------|
11
+ | `init [dir] [--name X] [--force] [--app <src>]` | Scaffold a project: app.config.json, starter theme + welcome feature, review/, CLAUDE.md, `.claude/skills/screenbook/`. `--app` also registers an existing SPA in that folder as an embedded app. |
12
+ | `app add <src> [dir] [--id --title --icon --device --chrome self\|engine --cooperative]` | Embed an existing single-page app as an app-type feature; scaffolds an injectable driver template unless `--cooperative`. |
13
+ | `serve [dir] [--port 4600]` | Static server with: `/engine/*` mounted from the package (projects don't need a local engine copy), live reload over SSE, automatic manifest regen on screen saves, `review/comments.json` disk sync, no-cache headers always. `/` redirects to the studio. |
14
+ | `manifest [dir]` | Rescan `features/*/screens/*.html` → regenerate each `manifest.json` (only writes on change). |
15
+ | `validate [dir] [--static] [--json]` | Static contract checks; then (unless `--static`) headless Chrome renders **every screen × theme × light/dark × ltr/rtl**, asserting Engine.ready fires, meta is present, zero console errors. Exit code 0 = green. |
16
+ | `screenshot [dir] --screen f/s [--mode light,dark] [--dir ltr,rtl] [--scenario id] [--theme id] [--lang xx] [--out dir]` | PNG(s) of the screen inside its real device frame (comma lists make a matrix). Default out: `review/screenshots/`. |
17
+
18
+ Browser commands need `playwright-core` (uses installed Chrome) or `playwright` installed
19
+ in `engine/`.
20
+
21
+ ## MCP server (`node engine/mcp/server.js --root <project>`, stdio)
22
+
23
+ Read: `list_features` · `list_screens {feature}` · `get_screen {feature, screen}` ·
24
+ `get_data {feature}` · `get_theme {id?}` · `get_flows {feature}` ·
25
+ `get_review_comments {feature?, unresolvedOnly?}`
26
+
27
+ Write: `add_screen {feature, screen, html}` · `update_screen {feature, screen, html}`
28
+ (both validate the contract, stamp `updated`, regen manifest; add fails on existing,
29
+ update fails on missing) · `remove_screen` · `add_feature {id, title, icon, description}` ·
30
+ `add_app {src, id?, device?, chrome?, driver?}` (embed an existing SPA — see app-mode.md) ·
31
+ `set_data {feature, seed?, scenarios?}` · `set_theme {id, theme}` · `set_flows {feature, flows}` ·
32
+ `resolve_comment {id, resolved?}`
33
+
34
+ Verify: `validate {mode: "static"|"full"}` · `screenshot {feature, screen, mode?, dir?,
35
+ scenario?, theme?, lang?, return_image?}`
36
+
37
+ Register with Claude Code:
38
+ ```
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 .
41
+ ```
42
+
43
+ ## Serve endpoints (for tooling)
44
+
45
+ `GET /__sb/health` → `{screenbook: true}` · `GET /__sb/events` → SSE change stream ·
46
+ `GET|POST /__sb/review/comments` → review pins. The shell auto-detects these — under any
47
+ other static server it falls back to localStorage for review comments.
48
+
49
+ ## Gates
50
+
51
+ 1. `validate` static errors → fix before anything else (broken meta, missing bridge, bad refs).
52
+ 2. `validate` full → the machine-verified fixed point. Screens with console errors are bugs.
53
+ 3. Unresolved review comments → the humans' queue for you; resolve them explicitly.
@@ -0,0 +1,44 @@
1
+ # Flows (`features/<f>/flows.json`)
2
+
3
+ Flows are named walkthrough sequences the shell plays for stakeholders — a guided story
4
+ with captions, not a test script.
5
+
6
+ ## Schema
7
+
8
+ ```json
9
+ { "list": [
10
+ { "id": "create-trip",
11
+ "title": "Create a trip",
12
+ "icon": "✨",
13
+ "description": "From empty home to a planned trip.",
14
+ "steps": [
15
+ { "screen": "010-home",
16
+ "note": "Start here — the empty state invites planning.",
17
+ "scenario": "new-user" },
18
+ { "screen": "020-wizard",
19
+ "note": "Seven questions shape the trip DNA." },
20
+ { "screen": "030-overview" } ] } ] }
21
+ ```
22
+
23
+ Step fields:
24
+ - `screen` (required) — a screen id in the same feature. Validation fails on dangling refs.
25
+ - `note` (optional) — caption shown in the player bar; write it as presenter narration.
26
+ - `scenario` (optional) — pins the 🎭 scenario from this step onward; the previous scenario
27
+ is restored when the flow exits.
28
+ - `mode` (optional) — pin `light`/`dark` for a step the same way.
29
+
30
+ ## Player behavior
31
+
32
+ - The ▶ dropdown in the shell lists flows of the current feature; picking one starts at
33
+ step 1 and shows a floating bar: ← / → , "2 / 5", the note, and ✕ to exit.
34
+ - Arrow keys step while a flow is active (direction-aware in RTL).
35
+ - Deep links carry the flow: `#feature/screen?flow=create-trip&step=1` — share a URL that
36
+ drops a stakeholder into the middle of the story.
37
+ - Manually clicking another screen in the sidebar exits the flow.
38
+
39
+ ## Writing good flows
40
+
41
+ - 3–7 steps; one idea per step; notes in the product's voice ("Votes decide, not WhatsApp").
42
+ - Lead with the scenario that makes the step's point (empty state → first slide, busy state
43
+ → dashboard step).
44
+ - End on the money shot (publish, confirmation, the AI acting).
@@ -0,0 +1,52 @@
1
+ # i18n strings (`features/<f>/i18n.json` + `Engine.t`)
2
+
3
+ Opt-in per feature. Without an i18n.json, screens simply own their text (the v1 way —
4
+ still perfectly legal for single-language features).
5
+
6
+ ## The strings file — key-first
7
+
8
+ ```json
9
+ {
10
+ "slide1.title": { "en": "One home for the whole trip", "ar": "بيت واحد للرحلة كلها" },
11
+ "slide1.next": { "en": "Continue", "ar": "متابعة" },
12
+ "greet": { "en": "{name}, your next story is waiting", "ar": "يا {name}، حكايتك القادمة بانتظارك" }
13
+ }
14
+ ```
15
+
16
+ Every key holds all its translations side by side — reviewers see the Arabic next to the
17
+ English, and gaps are visually obvious. Dot-namespace keys by screen or concern
18
+ (`slide1.*`, `common.*`).
19
+
20
+ ## Using it in screens
21
+
22
+ ```js
23
+ Engine.t('slide1.title') // active language, resolved at ready-time
24
+ Engine.t('greet', { name: 'Lina' }) // {var} placeholders interpolate
25
+ ```
26
+
27
+ A tidy pattern (see the Rahhal onboarding feature): put the FULL key in markup and fill in
28
+ one pass —
29
+
30
+ ```html
31
+ <h1 data-copy="slide1.title"></h1>
32
+ <script>
33
+ Engine.ready(function () {
34
+ document.querySelectorAll('[data-copy]').forEach(function (el) {
35
+ el.textContent = Engine.t(el.getAttribute('data-copy'));
36
+ });
37
+ });
38
+ </script>
39
+ ```
40
+
41
+ ## Semantics
42
+
43
+ - **Fallback chain**: requested language → project `defaultLanguage` → the key string itself.
44
+ Screens never crash on missing copy.
45
+ - **Language switches reload the screen** (same as scenario changes) — `Engine.t` binds at
46
+ ready-time; no re-render API needed. `dir` flips automatically for RTL languages.
47
+ - **Validation warns, never blocks**: `validate` reports keys missing a configured language,
48
+ languages not in `app.config.json`, and `Engine.t("key")` calls with no entry — as
49
+ warnings. Authoring stays unblocked; gaps stay visible.
50
+ - Standalone screens fetch `../i18n.json` themselves and honor `?lang=ar`.
51
+ - Read/write via MCP: `get_data` / `set_data {feature, i18n}` (full replacement, shape-checked).
52
+ - Embedded apps (app mode) own their i18n — the shell just forwards `env.lang` to the driver.
@@ -0,0 +1,45 @@
1
+ # Mock data + scenarios
2
+
3
+ Every feature owns its data: `data/seed.json` (the baseline) and `data/scenarios.json`
4
+ (named lifecycle states layered over it). The shell's 🎭 dropdown switches scenarios;
5
+ screens receive the merged result in `Engine.ready(data)`.
6
+
7
+ ## seed.json
8
+
9
+ Plain JSON, shaped however the feature needs. Model the *demo-complete* state — the one
10
+ that makes screens look alive.
11
+
12
+ ## scenarios.json
13
+
14
+ ```json
15
+ { "list": [
16
+ { "id": "default", "icon": "🧪", "title": "Full demo",
17
+ "description": "Baseline seed, untouched." },
18
+
19
+ { "id": "new-user", "icon": "🆕", "title": "Brand-new user",
20
+ "description": "Empty everything.",
21
+ "remove": ["trips", "user.firstName"],
22
+ "patch": { "user": { "isNew": true } } },
23
+
24
+ { "id": "mid-trip", "icon": "🟢", "title": "Mid-trip — Day 3",
25
+ "patch": { "trips": { "italy": {
26
+ "phase": "Traveling",
27
+ "startDate": "{{today-2}}", "endDate": "{{today+7}}" } } } } ] }
28
+ ```
29
+
30
+ Semantics, in order:
31
+ 1. `remove`: array of dot-paths deleted from a copy of the seed (`"trips.italy.hotel"`).
32
+ 2. `patch`: deep-merged over the result — objects merge recursively, **arrays and scalars
33
+ replace wholesale** (to change one array item, patch the whole array).
34
+ 3. Date tokens: any string may contain `{{today}}`, `{{today+3}}`, `{{today-2}}` —
35
+ resolved to real ISO `YYYY-MM-DD` dates at injection, so time-relative states are
36
+ always literally true when demoed.
37
+
38
+ ## Designing good scenarios
39
+
40
+ - Always keep `id: "default"` (no patch/remove) — the baseline the self-test uses.
41
+ - Model *lifecycle*, not variants: brand-new / mid-journey / done / edge (expired, offline,
42
+ over-budget). Each should visibly change at least one screen.
43
+ - Screens must survive every scenario: guard bindings (`d.user && d.user.firstName`) —
44
+ the empty-state scenario is where prototypes usually break.
45
+ - Flows can pin a scenario per step, so build scenarios with the demo story in mind.
@@ -0,0 +1,83 @@
1
+ # Screen contract
2
+
3
+ One screen = one standalone HTML file at `features/<feature>/screens/<id>.html`.
4
+ The id is the filename without `.html`; a numeric prefix (`010-home`) is the sidebar-order
5
+ fallback and keeps directory listings readable. Prefer ids like `010-home`, `020-wizard`.
6
+
7
+ ## Anatomy
8
+
9
+ ```html
10
+ <!DOCTYPE html>
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8">
14
+ <meta name="viewport" content="width=device-width, initial-scale=1">
15
+ <title>Cart</title>
16
+
17
+ <!-- 1. REQUIRED meta block — the file is the source of truth (manifest is derived) -->
18
+ <script type="application/json" id="screen-meta">
19
+ {
20
+ "title": "Cart",
21
+ "description": "Line items, totals, checkout entry.",
22
+ "order": 30,
23
+ "device": "ios",
24
+ "status": "wip",
25
+ "tags": ["commerce"],
26
+ "group": "Purchase",
27
+ "statusBar": "dark"
28
+ }
29
+ </script>
30
+
31
+ <!-- 2. REQUIRED bridge — the ONLY engine include, exact relative path -->
32
+ <script src="../../../engine/bridge.js"></script>
33
+
34
+ <!-- 3. Optional shared feature look/helpers (explicit opt-in per screen) -->
35
+ <link rel="stylesheet" href="shared.css">
36
+ <script src="shared.js" defer></script>
37
+
38
+ <style>/* screen-local styles, tokens only: var(--color-bg) etc. */</style>
39
+ </head>
40
+ <body>
41
+ <main>…plain HTML…</main>
42
+ <button data-go="040-checkout">Checkout</button>
43
+ <script>
44
+ Engine.ready(function (data) { /* bind data */ });
45
+ </script>
46
+ </body>
47
+ </html>
48
+ ```
49
+
50
+ ## Meta fields
51
+
52
+ | Field | Required | Notes |
53
+ |-------|----------|-------|
54
+ | `title` | ✅ | Sidebar row title. |
55
+ | `description` | recommended | One sentence; shows under the title. |
56
+ | `order` | recommended | Number; ties broken by filename. Missing → numeric filename prefix. |
57
+ | `device` | recommended | `ios` 393×852 · `android` 412×915 · `web` 1280×760 · `none` bare. Default `none`. |
58
+ | `devices` | optional | Variant array (e.g. `["ios","android"]`) — ONE file rendered in several frames; the stage shows switcher chips, the self-test renders every listed device, the Device filter matches any of them. `device` stays the primary/default. |
59
+ | `status` | optional | Free string; `draft` / `wip` / `ready` / `review` get colored pills. |
60
+ | `tags` | optional | Array of short strings, searchable. |
61
+ | `group` | optional | Sidebar section header (screens with the same group cluster). |
62
+ | `statusBar` | optional | `light` or `dark` — forces status-bar/home-bar ink over full-bleed art. |
63
+ | `updated` | auto | Stamped on every CLI/MCP save. Set manually only to override the stamp. |
64
+
65
+ ## Layout rules
66
+
67
+ - The device frame draws all hardware (status bar, island/punch-hole, home/gesture bar,
68
+ browser chrome). Screens NEVER draw hardware — instead pad:
69
+ `padding-top: calc(var(--safe-top) + …)` and `padding-bottom: calc(var(--safe-bottom) + …)`.
70
+ (`--safe-top`/`--safe-bottom` are `59px/34px` on ios, `42px/24px` on android, `0/0` elsewhere.)
71
+ - Sheets, modals, popovers live INSIDE the screen file as hidden markup; toggle them with
72
+ screen-local JS. They are not separate sidebar entries.
73
+ - Screens must render sensibly with empty `Engine.data` / `Engine.state` (guard your bindings).
74
+ - Zero-JS static screens are legal. CDN libraries are fine (iframe isolation).
75
+ - Duplicating markup between screens is expected and accepted — no partials/templating.
76
+ Shared look belongs in `shared.css` / `shared.js`, included explicitly.
77
+
78
+ ## Standalone mode
79
+
80
+ Every screen opens directly (no shell): the bridge self-loads `app.config.json`, the default
81
+ theme and `data/seed.json`, then honors query params `?scenario=…&mode=dark&dir=rtl&lang=ar&theme=…`.
82
+ `data-go` falls back to plain `<id>.html` links (query string preserved). If nothing is
83
+ fetchable, fallback tokens keep the screen legible — it should never look broken alone.
@@ -0,0 +1,56 @@
1
+ # Design tokens (`themes/<id>.json`)
2
+
3
+ Themes are JSON, flattened by the engine to CSS custom properties. Users restyle the whole
4
+ project without touching CSS files.
5
+
6
+ ## Schema
7
+
8
+ ```json
9
+ {
10
+ "id": "default",
11
+ "title": "Rahhal Warm",
12
+ "modes": {
13
+ "light": {
14
+ "color": { "accent": "#BF5B24", "bg": "#F7F2EA", "card": "#FFFFFF",
15
+ "ink": "#241C13", "ink-2": "#6E6152", "line": "#EAE1D2" },
16
+ "font": { "sans": "-apple-system, 'SF Pro Text', sans-serif",
17
+ "serif": "ui-serif, 'New York', Georgia, serif" },
18
+ "radius": { "card": "20px", "chip": "999px" },
19
+ "space": { "page": "16px" }
20
+ },
21
+ "dark": {
22
+ "color": { "accent": "#E1793B", "bg": "#171108", "card": "#231A10",
23
+ "ink": "#F4EBDD", "ink-2": "#BCAC96", "line": "#37291A" }
24
+ }
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Rules
30
+
31
+ - **Flattening**: nested paths join with `-` and get a `--` prefix:
32
+ `color.ink-2` → `--color-ink-2`, `radius.card` → `--radius-card`. Any group name works
33
+ (`elevation`, `duration`, …) — the four above are the convention screens rely on.
34
+ - **Dark = delta**: `modes.dark` deep-merges over `modes.light`; list only what changes.
35
+ - Values are raw CSS strings — keep units inside the value (`"20px"`, not `20`).
36
+ - Naming is DTCG-compatible (group/key), so tokens can later import/export to design tools.
37
+
38
+ ## Discipline for screens
39
+
40
+ - Color/typography/radius/spacing come from `var(--…)`. Hard-coded values are reserved for
41
+ one-off art (e.g. full-bleed brand gradients on onboarding slides).
42
+ - Always provide both modes for colors you add; test with the shell's dark toggle.
43
+ - The conventional core set every theme should define:
44
+ `color.accent/bg/card/ink/ink-2/line`, `font.sans` (+ `serif` if used),
45
+ `radius.card/chip`, `space.page`.
46
+ - Register new themes in `app.config.json` `themes` array (`set_theme` does this for you);
47
+ the first entry is the default.
48
+
49
+ ## Theme Lab (in the shell)
50
+
51
+ The 🎨 chip in the studio topbar opens the **Theme Lab**: every token of the active theme as
52
+ editable rows (color pickers for hex values), applied to the hosted screens **live**. Humans
53
+ use it to explore palettes; **Save** writes `themes/<id>.json` back to disk when `engine serve`
54
+ is running (Export downloads the JSON otherwise). If a reviewer hands you an exported theme
55
+ JSON or a saved change, treat the file on disk as the source of truth — read it with
56
+ `get_theme` before further edits.