@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
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,64 @@
1
+ # The studio, control by control
2
+
3
+ Open `http://127.0.0.1:4600` (or `/engine/index.html` under any server).
4
+
5
+ ![The studio](shots/hero-studio.png)
6
+
7
+ ## Top bar
8
+
9
+ | Control | What it does |
10
+ |---------|--------------|
11
+ | **โง‰ Feature โ–พ** | Switches the active feature (one folder under `features/` = one entry). Embedded apps appear here too. |
12
+ | **๐Ÿ“ฑ Device โ–พ** | Filters the sidebar by device (`ios` / `android` / `web` / `none`). A screen's own frame always comes from its meta. Hidden when the project declares one device. |
13
+ | **๐ŸŽญ Scenario โ–พ** | Applies a named data overlay from the feature's `scenarios.json` โ€” empty states, mid-journey, edge cases. The screen reloads with the merged data. Embedded apps can expose their own scenarios here. |
14
+ | **๐ŸŽจ Theme Lab** | Docked panel: every token of the active theme, editable live against the hosted screens โ€” color pickers, Export JSON, **Save** to `themes/<id>.json` (under `serve`), Revert, theme switcher when several themes exist. |
15
+ | **โ˜พ / โ˜€๏ธŽ** | Dark/light. Applies live โ€” tokens flip, no reload. The studio chrome follows. |
16
+ | **โ‡„ LTR/RTL** | Flips direction live (`dir` on the screen's `<html>`). |
17
+ | **๐ŸŒ lang โ–พ** | Sets `Engine.env.lang` and reloads the screen so `Engine.t` strings rebind. Picking an RTL language flips direction automatically. Hidden for single-language projects. |
18
+ | **โ–ถ Flow โ–พ** | Plays a walkthrough from the feature's `flows.json`: a floating bar with โ†/โ†’, step counter and the step's note. Steps can pin scenarios. Arrow keys work; โœ• exits. |
19
+ | **๐Ÿ“Œ Review** | Review Mode. Click anywhere on the screen to pin a comment โ€” it captures screen, scenario, theme, mode, direction and the exact element. The docked panel lists, resolves, exports. Synced to `review/comments.json` under `serve`; localStorage otherwise. |
20
+ | **โ†บ** | Resets the shared `Engine.state` store (the cross-screen KV) and reloads the screen. |
21
+
22
+ ## Sidebar
23
+
24
+ Search (`/` focuses it) ยท grouped screen rows (title, description, updated date, status
25
+ pill, tags) ยท the โ— live badge shows when the CLI server's extras are active ยท
26
+ the Self-test link runs the render matrix in your browser.
27
+
28
+ Keyboard: โ†‘/โ†“ or j/k moves between screens ยท โ†/โ†’ steps an active flow ยท Esc closes menus.
29
+
30
+ ## Deep links
31
+
32
+ Everything is a URL:
33
+
34
+ ```
35
+ #feature/screen?scenario=mid-trip&mode=dark&dir=rtl&lang=ar&theme=noir&flow=tour&step=2
36
+ ```
37
+
38
+ Share a link and a stakeholder lands in the exact state โ€” screen, data, theme, direction,
39
+ mid-flow. `?solo=1` renders just the framed screen (used by `screenshot`); `?selftest=1`
40
+ runs the matrix.
41
+
42
+ ## Device variants, groups, transitions
43
+
44
+ Screens declaring `"devices": ["ios","android"]` get **switcher chips** above the frame โ€”
45
+ one file, several frames, and the self-test covers each. Features declaring a `"group"` in
46
+ feature.json cluster under section headers in the Feature โ–พ dropdown. Screen switches play
47
+ a subtle 180ms entrance โ€” disabled automatically under `prefers-reduced-motion`, or
48
+ project-wide with `"transitions": false` in app.config.json.
49
+
50
+ ## Device frames
51
+
52
+ The engine draws the hardware โ€” iPhone (island, status bar, home bar), Android (punch-hole,
53
+ gesture bar), browser chrome, or a bare card. Screens pad with `var(--safe-top)` /
54
+ `var(--safe-bottom)` and never draw hardware themselves. Full-bleed screens can force the
55
+ status-bar ink with `"statusBar": "light" | "dark"` in their meta. Frames scale to fit the
56
+ window. Embedded apps that draw their own chrome declare `"chrome": "self"` and get bezel
57
+ only.
58
+
59
+ ## Self-test
60
+
61
+ `?selftest=1` โ€” every screen ร— theme ร— light/dark ร— LTR/RTL must reach `Engine.ready` with
62
+ its meta present and **zero console errors**; embedded apps must register and survive a
63
+ visit to every screen they report. The same matrix runs headlessly via
64
+ `screenbook validate` โ€” treat green as the definition of done.
@@ -0,0 +1,55 @@
1
+ # Troubleshooting
2
+
3
+ **The studio is blank / "could not load this project".**
4
+ The shell expects to live at `<project>/engine/` with `app.config.json` one level up.
5
+ Serve the PROJECT root (not `engine/`), then open `/engine/index.html`. Under
6
+ `screenbook serve` the `/engine/*` mount handles this automatically.
7
+
8
+ **My new screen isn't in the sidebar.**
9
+ The sidebar reads the derived `features/<f>/manifest.json`. Under `serve` it regenerates on
10
+ save; under any other server run `node engine/cli/screenbook.js manifest .`. Also check the
11
+ meta block parses (validate says exactly what's wrong).
12
+
13
+ **The screen shows "Loadingโ€ฆ" forever.**
14
+ The bridge never finished its handshake โ€” almost always a thrown error in the screen's own
15
+ script before `Engine.ready`. Open the screen standalone
16
+ (`/features/<f>/screens/<s>.html`) and read the console.
17
+
18
+ **"App hasn't registered" on an embedded app.**
19
+ Driven app: the driver didn't reach `AppBridge.register` โ€” check
20
+ `features/<id>/driver.js` against the app's real globals (open the app directly, poke in
21
+ the console). Cooperative app: the include path to `engine/app-bridge.js` is wrong, or
22
+ `register()` never runs. Cross-origin apps can't be driven โ€” serve them same-origin or make
23
+ them cooperative.
24
+
25
+ **`validate` fails with "No headless browser available".**
26
+ `cd engine && npm i` (uses your installed Chrome via playwright-core). No Chrome:
27
+ `npm i -D playwright && npx playwright install chromium`.
28
+
29
+ **`validate` is red on a screen that looks fine.**
30
+ The gate is zero console errors across every theme ร— mode ร— direction. The failure row
31
+ names the combo and the first error โ€” usually a missing guard on empty data
32
+ (`d.user.name` with no user in some scenario) or a 404'd asset. Screens must survive every
33
+ scenario, including the empty ones.
34
+
35
+ **i18n warnings ("missing ar", "has no entry").**
36
+ Warnings never block. They list exactly which keys lack which language, and which
37
+ `Engine.t("key")` calls have no entry in `i18n.json`. Fill them when ready.
38
+
39
+ **Review comments don't persist.**
40
+ Disk sync needs `screenbook serve` (the โ— live badge). Under other servers, pins live in
41
+ localStorage and can be exported as JSON from the panel.
42
+
43
+ **Theme Lab's Save fails.**
44
+ Save needs `serve` too (`POST /__sb/theme/<id>`). Use Export under static servers.
45
+
46
+ **Two status bars on an embedded app.**
47
+ The app draws its own chrome โ€” set `"chrome": "self"` in its feature.json.
48
+
49
+ **Stale content under python http.server.**
50
+ Python's server sends cache headers; hard-reload, or use `screenbook serve`
51
+ (no-store on everything โ€” this problem is why).
52
+
53
+ **Something engine-side looks wrong.**
54
+ `?selftest=1` in the browser for the live matrix; `TESTING.md` maps every layer
55
+ (`npm test` inside `engine/` runs the engine's own 61-test suite).
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "Qahwa",
3
+ "defaultFeature": "order",
4
+ "features": ["order", "roastery"],
5
+ "devices": ["ios", "android", "web"],
6
+ "themes": ["qahwa"],
7
+ "languages": ["en"],
8
+ "defaultLanguage": "en"
9
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "list": [
3
+ {
4
+ "id": "default",
5
+ "icon": "๐Ÿงช",
6
+ "title": "Regular morning",
7
+ "description": "Lina has points and an order brewing right now."
8
+ },
9
+ {
10
+ "id": "first-visit",
11
+ "icon": "๐Ÿ†•",
12
+ "title": "First visit",
13
+ "description": "No account history, no active order โ€” the empty-state path.",
14
+ "remove": ["order", "user.points"],
15
+ "patch": { "user": { "firstName": "Guest" } }
16
+ },
17
+ {
18
+ "id": "order-ready",
19
+ "icon": "โœ…",
20
+ "title": "Order ready",
21
+ "description": "The pickup moment โ€” status flips to ready.",
22
+ "patch": { "order": { "phase": "ready" } }
23
+ }
24
+ ]
25
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "cafe": { "name": "Qahwa", "branch": "As-Sahafa ยท Riyadh", "open": true },
3
+ "user": { "firstName": "Lina", "points": 340 },
4
+ "menu": [
5
+ { "id": "flat-white", "name": "Flat White", "ar": "ูู„ุงุช ูˆุงูŠุช", "price": 16, "emoji": "โ˜•", "desc": "Double ristretto, silky oat milk", "featured": true, "available": true },
6
+ { "id": "v60", "name": "V60 Pour-over", "ar": "ฺคูŠ ูฆู ", "price": 22, "emoji": "๐Ÿซ–", "desc": "Single-origin Ethiopia โ€” jasmine, peach", "available": true },
7
+ { "id": "espresso", "name": "Espresso", "ar": "ุฅุณุจุฑูŠุณูˆ", "price": 12, "emoji": "๐Ÿฅƒ", "desc": "House blend, cocoa finish", "available": true },
8
+ { "id": "cold-brew", "name": "Cold Brew", "ar": "ูƒูˆู„ุฏ ุจุฑูˆ", "price": 18, "emoji": "๐ŸงŠ", "desc": "18-hour steep, over ice", "available": true },
9
+ { "id": "saffron-latte", "name": "Saffron Latte", "ar": "ู„ุงุชูŠู‡ ุจุงู„ุฒุนูุฑุงู†", "price": 24, "emoji": "๐ŸŒผ", "desc": "Saudi twist โ€” saffron + cardamom", "available": false }
10
+ ],
11
+ "order": {
12
+ "id": "Q-2481",
13
+ "phase": "brewing",
14
+ "placedAt": "{{today}} 08:31",
15
+ "readyBy": "08:44",
16
+ "items": [
17
+ { "menuId": "flat-white", "qty": 1 },
18
+ { "menuId": "v60", "qty": 1 }
19
+ ]
20
+ }
21
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "order",
3
+ "title": "Ordering",
4
+ "icon": "โ˜•",
5
+ "description": "The customer app โ€” menu, drink customization, cart, live order status.",
6
+ "order": 1
7
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "list": [
3
+ {
4
+ "id": "order-a-cup",
5
+ "title": "Order a cup",
6
+ "icon": "โ˜•",
7
+ "description": "First visit โ†’ customized flat white โ†’ cart โ†’ pickup.",
8
+ "steps": [
9
+ { "screen": "010-menu", "scenario": "first-visit", "note": "First visit โ€” no active order banner, the menu sells itself." },
10
+ { "screen": "020-drink", "note": "Drink detail. The size/milk sheet lives inside this screen (tap Customize)." },
11
+ { "screen": "030-cart", "note": "Cart is shared Engine.state โ€” it survives screen switches and resets from the shell." },
12
+ { "screen": "040-status", "scenario": "order-ready", "note": "The pickup moment โ€” same screen, โœ… scenario flips the timeline." }
13
+ ]
14
+ }
15
+ ]
16
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "feature": "order",
3
+ "generated": "2026-07-18",
4
+ "screens": [
5
+ {
6
+ "id": "010-menu",
7
+ "file": "screens/010-menu.html",
8
+ "title": "Menu",
9
+ "description": "Home โ€” active-order banner, featured drink, the menu list.",
10
+ "order": 10,
11
+ "device": "ios",
12
+ "status": "ready",
13
+ "tags": [
14
+ "home"
15
+ ],
16
+ "updated": "2026-07-18"
17
+ },
18
+ {
19
+ "id": "020-drink",
20
+ "file": "screens/020-drink.html",
21
+ "title": "Drink detail",
22
+ "description": "Hero, tasting notes, and the size/milk customization sheet (in-file, EN-24).",
23
+ "order": 20,
24
+ "device": "ios",
25
+ "status": "ready",
26
+ "tags": [
27
+ "detail",
28
+ "sheet"
29
+ ],
30
+ "updated": "2026-07-18"
31
+ },
32
+ {
33
+ "id": "030-cart",
34
+ "file": "screens/030-cart.html",
35
+ "title": "Cart",
36
+ "description": "Lines from shared Engine.state + seed prices; totals; place order.",
37
+ "order": 30,
38
+ "device": "ios",
39
+ "status": "ready",
40
+ "tags": [
41
+ "commerce"
42
+ ],
43
+ "updated": "2026-07-18"
44
+ },
45
+ {
46
+ "id": "040-status",
47
+ "file": "screens/040-status.html",
48
+ "title": "Order status",
49
+ "description": "Live pickup timeline โ€” flips with the โœ… order-ready scenario. Renders on iPhone AND Android (device variants).",
50
+ "order": 40,
51
+ "device": "ios",
52
+ "devices": [
53
+ "ios",
54
+ "android"
55
+ ],
56
+ "status": "ready",
57
+ "tags": [
58
+ "live"
59
+ ],
60
+ "updated": "2026-07-18"
61
+ },
62
+ {
63
+ "id": "050-status-android",
64
+ "file": "screens/050-status-android.html",
65
+ "title": "Status โ€” Android",
66
+ "description": "The same pickup moment in the Android frame (Material-ish idioms).",
67
+ "order": 50,
68
+ "device": "android",
69
+ "status": "draft",
70
+ "tags": [
71
+ "android",
72
+ "live"
73
+ ],
74
+ "group": "Other devices",
75
+ "updated": "2026-07-18"
76
+ }
77
+ ]
78
+ }
@@ -0,0 +1,121 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Qahwa โ€” Menu</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Menu",
10
+ "description": "Home โ€” active-order banner, featured drink, the menu list.",
11
+ "order": 10,
12
+ "device": "ios",
13
+ "status": "ready",
14
+ "tags": ["home"],
15
+ "updated": "2026-07-18"
16
+ }
17
+ </script>
18
+ <script src="../../../engine/bridge.js"></script>
19
+ <link rel="stylesheet" href="shared.css">
20
+ </head>
21
+ <body>
22
+ <div class="app">
23
+ <div class="appbar">
24
+ <div>
25
+ <h1 id="greet">Qahwa</h1>
26
+ <div class="small muted" id="branch"></div>
27
+ </div>
28
+ <span class="chip-points" id="points" hidden>โœฆ <span></span></span>
29
+ </div>
30
+
31
+ <div class="card" id="order-banner" hidden style="display:flex;align-items:center;gap:12px;margin-bottom:14px;cursor:pointer">
32
+ <span style="font-size:26px" id="ob-emoji">โ˜•</span>
33
+ <div style="flex:1">
34
+ <div style="font-weight:700" id="ob-title"></div>
35
+ <div class="small muted" id="ob-sub"></div>
36
+ </div>
37
+ <span class="badge warn" id="ob-badge"></span>
38
+ </div>
39
+
40
+ <div class="card" id="featured" hidden style="margin-bottom:14px;cursor:pointer">
41
+ <div class="small muted" style="text-transform:uppercase;letter-spacing:.8px;font-weight:700;font-size:10.5px">Today's pick</div>
42
+ <div style="display:flex;align-items:center;gap:14px;margin-top:8px">
43
+ <span style="font-size:44px" id="f-emoji"></span>
44
+ <div style="flex:1">
45
+ <div style="font-family:var(--font-serif);font-size:21px;font-weight:700" id="f-name"></div>
46
+ <div class="small muted" id="f-desc"></div>
47
+ </div>
48
+ <div class="price" id="f-price" style="font-weight:800"></div>
49
+ </div>
50
+ </div>
51
+
52
+ <div class="card" style="padding:6px 14px">
53
+ <div id="menu-list"></div>
54
+ </div>
55
+ </div>
56
+
57
+ <script>
58
+ Engine.ready(function (d) {
59
+ var menu = d.menu || [];
60
+ document.getElementById('greet').textContent =
61
+ d.user && d.user.firstName !== 'Guest' ? 'Morning, ' + d.user.firstName : 'Qahwa';
62
+ document.getElementById('branch').textContent = d.cafe ? d.cafe.branch : '';
63
+
64
+ if (d.user && d.user.points) {
65
+ var p = document.getElementById('points');
66
+ p.hidden = false;
67
+ p.querySelector('span').textContent = d.user.points + ' pts';
68
+ }
69
+
70
+ if (d.order) {
71
+ var banner = document.getElementById('order-banner');
72
+ banner.hidden = false;
73
+ var ready = d.order.phase === 'ready';
74
+ document.getElementById('ob-title').textContent = 'Order ' + d.order.id;
75
+ document.getElementById('ob-sub').textContent = ready
76
+ ? 'Waiting at the counter' : 'Ready by ' + d.order.readyBy;
77
+ var badge = document.getElementById('ob-badge');
78
+ badge.textContent = ready ? 'READY' : 'BREWING';
79
+ badge.className = 'badge ' + (ready ? 'good' : 'warn');
80
+ banner.addEventListener('click', function () { Engine.go('040-status'); });
81
+ }
82
+
83
+ var feat = menu.find(function (m) { return m.featured && m.available; });
84
+ if (feat) {
85
+ var f = document.getElementById('featured');
86
+ f.hidden = false;
87
+ document.getElementById('f-emoji').textContent = feat.emoji;
88
+ document.getElementById('f-name').textContent = feat.name;
89
+ document.getElementById('f-desc').textContent = feat.desc;
90
+ document.getElementById('f-price').textContent = feat.price + ' SAR';
91
+ f.addEventListener('click', function () {
92
+ Engine.state.set('qahwa.selected', feat.id);
93
+ Engine.go('020-drink');
94
+ });
95
+ }
96
+
97
+ var list = document.getElementById('menu-list');
98
+ if (!menu.length) {
99
+ list.innerHTML = '<div class="small muted" style="padding:16px 0">The menu is being roasted โ€” check back soon.</div>';
100
+ }
101
+ menu.forEach(function (m) {
102
+ var row = document.createElement('div');
103
+ row.className = 'row' + (m.available ? '' : ' is-off');
104
+ row.innerHTML =
105
+ '<span class="emoji">' + m.emoji + '</span>' +
106
+ '<div class="grow"><div class="name">' + m.name +
107
+ ' <span class="muted" style="font-weight:500;font-size:12px">ยท ' + m.ar + '</span></div>' +
108
+ '<div class="desc">' + (m.available ? m.desc : 'Back tomorrow') + '</div></div>' +
109
+ '<div class="price">' + m.price + ' SAR</div>';
110
+ if (m.available) {
111
+ row.addEventListener('click', function () {
112
+ Engine.state.set('qahwa.selected', m.id);
113
+ Engine.go('020-drink');
114
+ });
115
+ }
116
+ list.appendChild(row);
117
+ });
118
+ });
119
+ </script>
120
+ </body>
121
+ </html>
@@ -0,0 +1,120 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Qahwa โ€” Drink</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Drink detail",
10
+ "description": "Hero, tasting notes, and the size/milk customization sheet (in-file, EN-24).",
11
+ "order": 20,
12
+ "device": "ios",
13
+ "status": "ready",
14
+ "tags": ["detail", "sheet"],
15
+ "updated": "2026-07-18"
16
+ }
17
+ </script>
18
+ <script src="../../../engine/bridge.js"></script>
19
+ <link rel="stylesheet" href="shared.css">
20
+ </head>
21
+ <body>
22
+ <div class="app">
23
+ <div class="appbar">
24
+ <button class="back" data-go="010-menu">โ†</button>
25
+ <h1 style="font-size:19px">Drink</h1>
26
+ <div class="spacer"></div>
27
+ </div>
28
+
29
+ <div style="text-align:center;padding:18px 0 6px">
30
+ <div style="font-size:96px;line-height:1" id="d-emoji">โ˜•</div>
31
+ <div style="font-family:var(--font-serif);font-size:28px;font-weight:700;margin-top:14px" id="d-name"></div>
32
+ <div class="muted" style="margin-top:6px;font-size:14px" id="d-desc"></div>
33
+ <div style="font-weight:800;font-size:20px;margin-top:12px" id="d-price"></div>
34
+ </div>
35
+
36
+ <div class="card" style="margin-top:14px">
37
+ <div style="display:flex;justify-content:space-between;font-size:13px">
38
+ <span class="muted">Roast</span><b>Medium โ€” Yemeni heritage</b>
39
+ </div>
40
+ <div style="display:flex;justify-content:space-between;font-size:13px;margin-top:10px">
41
+ <span class="muted">Notes</span><b id="d-notes">Cocoa ยท dates ยท orange peel</b>
42
+ </div>
43
+ </div>
44
+
45
+ <div style="flex:1"></div>
46
+ <button class="btn ghost" id="open-sheet" style="margin-bottom:10px">Customize</button>
47
+ <button class="btn" id="add">Add to order</button>
48
+ </div>
49
+
50
+ <!-- the sheet lives INSIDE this screen โ€” hidden until triggered (EN-24) -->
51
+ <div class="sheet-veil" id="veil"></div>
52
+ <div class="sheet">
53
+ <div class="grab"></div>
54
+ <div style="font-weight:800;font-size:16px;margin-bottom:12px">Make it yours</div>
55
+ <div class="small muted" style="margin-bottom:6px">Size</div>
56
+ <div class="seg" id="size-seg">
57
+ <button data-v="Short">Short</button>
58
+ <button data-v="Tall" class="on">Tall</button>
59
+ <button data-v="Grande">Grande</button>
60
+ </div>
61
+ <div class="small muted" style="margin:14px 0 6px">Milk</div>
62
+ <div class="seg" id="milk-seg">
63
+ <button data-v="Oat" class="on">Oat</button>
64
+ <button data-v="Whole">Whole</button>
65
+ <button data-v="None">None</button>
66
+ </div>
67
+ <button class="btn" id="sheet-done" style="margin-top:18px">Done</button>
68
+ </div>
69
+
70
+ <script>
71
+ Engine.ready(function (d) {
72
+ var id = Engine.state.get('qahwa.selected', 'flat-white');
73
+ var menu = d.menu || [];
74
+ var item = menu.find(function (m) { return m.id === id; }) || menu[0];
75
+ if (item) {
76
+ document.getElementById('d-emoji').textContent = item.emoji;
77
+ document.getElementById('d-name').textContent = item.name;
78
+ document.getElementById('d-desc').textContent = item.desc;
79
+ document.getElementById('d-price').textContent = item.price + ' SAR';
80
+ }
81
+
82
+ var choice = { size: 'Tall', milk: 'Oat' };
83
+ function wireSeg(segId, key) {
84
+ var seg = document.getElementById(segId);
85
+ seg.addEventListener('click', function (e) {
86
+ var btn = e.target.closest('button');
87
+ if (!btn) return;
88
+ seg.querySelectorAll('button').forEach(function (b) { b.classList.remove('on'); });
89
+ btn.classList.add('on');
90
+ choice[key] = btn.dataset.v;
91
+ });
92
+ }
93
+ wireSeg('size-seg', 'size');
94
+ wireSeg('milk-seg', 'milk');
95
+
96
+ var open = function () { document.body.classList.add('sheet-open'); };
97
+ var close = function () { document.body.classList.remove('sheet-open'); };
98
+ document.getElementById('open-sheet').addEventListener('click', open);
99
+ document.getElementById('veil').addEventListener('click', close);
100
+ document.getElementById('sheet-done').addEventListener('click', function () {
101
+ close();
102
+ Engine.toast(choice.size + ' ยท ' + choice.milk + ' milk');
103
+ });
104
+
105
+ document.getElementById('add').addEventListener('click', function () {
106
+ if (!item) return;
107
+ var cart = Engine.state.get('qahwa.cart', {});
108
+ var line = cart[item.id] || { qty: 0 };
109
+ line.qty += 1;
110
+ line.size = choice.size;
111
+ line.milk = choice.milk;
112
+ cart[item.id] = line;
113
+ Engine.state.set('qahwa.cart', cart);
114
+ Engine.toast('Added ' + item.name + ' โœ“');
115
+ setTimeout(function () { Engine.go('030-cart'); }, 550);
116
+ });
117
+ });
118
+ </script>
119
+ </body>
120
+ </html>
@@ -0,0 +1,99 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Qahwa โ€” Cart</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Cart",
10
+ "description": "Lines from shared Engine.state + seed prices; totals; place order.",
11
+ "order": 30,
12
+ "device": "ios",
13
+ "status": "ready",
14
+ "tags": ["commerce"],
15
+ "updated": "2026-07-18"
16
+ }
17
+ </script>
18
+ <script src="../../../engine/bridge.js"></script>
19
+ <link rel="stylesheet" href="shared.css">
20
+ </head>
21
+ <body>
22
+ <div class="app">
23
+ <div class="appbar">
24
+ <button class="back" data-go="010-menu">โ†</button>
25
+ <h1 style="font-size:19px">Your order</h1>
26
+ <div class="spacer"></div>
27
+ </div>
28
+
29
+ <div class="card" style="padding:6px 14px" id="lines-card">
30
+ <div id="lines"></div>
31
+ </div>
32
+
33
+ <div class="card" style="margin-top:12px" id="totals" hidden>
34
+ <div style="display:flex;justify-content:space-between;font-size:13.5px">
35
+ <span class="muted">Subtotal</span><b id="t-sub"></b>
36
+ </div>
37
+ <div style="display:flex;justify-content:space-between;font-size:13.5px;margin-top:8px">
38
+ <span class="muted">VAT 15%</span><b id="t-vat"></b>
39
+ </div>
40
+ <div style="display:flex;justify-content:space-between;font-size:16px;margin-top:12px;padding-top:12px;border-top:1px solid var(--color-line)">
41
+ <b>Total</b><b id="t-total"></b>
42
+ </div>
43
+ </div>
44
+
45
+ <div style="flex:1"></div>
46
+ <button class="btn" id="checkout" hidden>Place order</button>
47
+ </div>
48
+
49
+ <script>
50
+ Engine.ready(function (d) {
51
+ var menu = d.menu || [];
52
+ var cart = Engine.state.get('qahwa.cart', {});
53
+ var ids = Object.keys(cart);
54
+ var linesBox = document.getElementById('lines');
55
+
56
+ if (!ids.length) {
57
+ linesBox.innerHTML =
58
+ '<div style="text-align:center;padding:34px 10px">' +
59
+ '<div style="font-size:44px">๐Ÿ›๏ธ</div>' +
60
+ '<div style="font-weight:700;margin-top:10px">Nothing here yet</div>' +
61
+ '<div class="small muted" style="margin-top:4px">Pick something warm from the menu.</div>' +
62
+ '<button class="btn ghost" data-go="010-menu" style="margin-top:16px">Browse the menu</button></div>';
63
+ return;
64
+ }
65
+
66
+ var subtotal = 0;
67
+ ids.forEach(function (id) {
68
+ var item = menu.find(function (m) { return m.id === id; });
69
+ if (!item) return;
70
+ var line = cart[id];
71
+ subtotal += item.price * line.qty;
72
+ var row = document.createElement('div');
73
+ row.className = 'row';
74
+ row.style.cursor = 'default';
75
+ row.innerHTML =
76
+ '<span class="emoji">' + item.emoji + '</span>' +
77
+ '<div class="grow"><div class="name">' + item.name + ' ร—' + line.qty + '</div>' +
78
+ '<div class="desc">' + (line.size || 'Tall') + ' ยท ' + (line.milk || 'Oat') + ' milk</div></div>' +
79
+ '<div class="price">' + item.price * line.qty + ' SAR</div>';
80
+ linesBox.appendChild(row);
81
+ });
82
+
83
+ var vat = Math.round(subtotal * 0.15 * 100) / 100;
84
+ document.getElementById('totals').hidden = false;
85
+ document.getElementById('t-sub').textContent = subtotal + ' SAR';
86
+ document.getElementById('t-vat').textContent = vat + ' SAR';
87
+ document.getElementById('t-total').textContent = (subtotal + vat) + ' SAR';
88
+
89
+ var btn = document.getElementById('checkout');
90
+ btn.hidden = false;
91
+ btn.addEventListener('click', function () {
92
+ Engine.state.set('qahwa.cart', {});
93
+ Engine.toast('Order placed โ€” see you at the counter โœ“');
94
+ setTimeout(function () { Engine.go('040-status'); }, 600);
95
+ });
96
+ });
97
+ </script>
98
+ </body>
99
+ </html>