@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,125 @@
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 โ€” Order status</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Order status",
10
+ "description": "Live pickup timeline โ€” flips with the โœ… order-ready scenario. Renders on iPhone AND Android (device variants).",
11
+ "order": 40,
12
+ "device": "ios",
13
+ "devices": ["ios", "android"],
14
+ "status": "ready",
15
+ "tags": ["live"],
16
+ "updated": "2026-07-18"
17
+ }
18
+ </script>
19
+ <script src="../../../engine/bridge.js"></script>
20
+ <link rel="stylesheet" href="shared.css">
21
+ <style>
22
+ .stepper { position: relative; padding-inline-start: 26px; }
23
+ .stepper::before {
24
+ content: ''; position: absolute;
25
+ inset-block: 10px; inset-inline-start: 8px;
26
+ width: 2px; background: var(--color-line);
27
+ }
28
+ .step { position: relative; padding: 12px 0; }
29
+ .step::before {
30
+ content: ''; position: absolute;
31
+ inset-inline-start: -24px; top: 16px;
32
+ width: 12px; height: 12px; border-radius: 50%;
33
+ background: var(--color-line);
34
+ outline: 3px solid var(--color-bg);
35
+ }
36
+ .step.done::before { background: var(--color-good); }
37
+ .step.now::before { background: var(--color-accent); box-shadow: 0 0 0 5px color-mix(in srgb, var(--color-accent) 22%, transparent); }
38
+ .step .t { font-weight: 700; font-size: 14.5px; }
39
+ .step .s { font-size: 12.5px; color: var(--color-ink-2); margin-top: 2px; }
40
+ .step.todo { opacity: .5; }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <div class="app">
45
+ <div class="appbar">
46
+ <button class="back" data-go="010-menu">โ†</button>
47
+ <h1 style="font-size:19px">Order status</h1>
48
+ <div class="spacer"></div>
49
+ </div>
50
+
51
+ <div id="none" hidden style="text-align:center;padding:60px 10px">
52
+ <div style="font-size:44px">๐ŸŒ™</div>
53
+ <div style="font-weight:700;margin-top:10px">No active order</div>
54
+ <div class="small muted" style="margin-top:4px">Your next cup is one tap away.</div>
55
+ <button class="btn" data-go="010-menu" style="margin-top:18px">Order something</button>
56
+ </div>
57
+
58
+ <div id="active" hidden>
59
+ <div class="card" style="text-align:center;padding:22px">
60
+ <div class="small muted">Order</div>
61
+ <div style="font-family:var(--font-serif);font-size:30px;font-weight:700" id="o-id"></div>
62
+ <span class="badge warn" id="o-badge" style="margin-top:8px"></span>
63
+ <div class="small muted" style="margin-top:10px" id="o-eta"></div>
64
+ </div>
65
+
66
+ <div class="card" style="margin-top:12px">
67
+ <div class="stepper" id="steps"></div>
68
+ </div>
69
+
70
+ <div class="card" style="margin-top:12px;padding:6px 14px">
71
+ <div id="o-items"></div>
72
+ </div>
73
+ </div>
74
+ </div>
75
+
76
+ <script>
77
+ var PHASES = [
78
+ { id: 'received', t: 'Received', s: 'The barista has your ticket' },
79
+ { id: 'brewing', t: 'Brewing', s: 'Dialing in, pulling shots' },
80
+ { id: 'ready', t: 'Ready', s: 'Pick it up at the counter' },
81
+ { id: 'picked-up', t: 'Picked up', s: 'Enjoy โ˜•' }
82
+ ];
83
+ Engine.ready(function (d) {
84
+ if (!d.order) {
85
+ document.getElementById('none').hidden = false;
86
+ return;
87
+ }
88
+ document.getElementById('active').hidden = false;
89
+ var phase = d.order.phase || 'received';
90
+ var phaseIx = PHASES.findIndex(function (p) { return p.id === phase; });
91
+ if (phaseIx < 0) phaseIx = 0;
92
+
93
+ document.getElementById('o-id').textContent = d.order.id;
94
+ var badge = document.getElementById('o-badge');
95
+ badge.textContent = PHASES[phaseIx].t.toUpperCase();
96
+ badge.className = 'badge ' + (phase === 'ready' ? 'good' : 'warn');
97
+ document.getElementById('o-eta').textContent =
98
+ phase === 'ready' ? 'Waiting for you โ€” placed ' + d.order.placedAt
99
+ : 'Ready by ' + d.order.readyBy + ' ยท placed ' + d.order.placedAt;
100
+
101
+ var steps = document.getElementById('steps');
102
+ PHASES.forEach(function (p, i) {
103
+ var el = document.createElement('div');
104
+ el.className = 'step ' + (i < phaseIx ? 'done' : i === phaseIx ? 'now' : 'todo');
105
+ el.innerHTML = '<div class="t">' + p.t + '</div><div class="s">' + p.s + '</div>';
106
+ steps.appendChild(el);
107
+ });
108
+
109
+ var itemsBox = document.getElementById('o-items');
110
+ (d.order.items || []).forEach(function (line) {
111
+ var m = (d.menu || []).find(function (x) { return x.id === line.menuId; });
112
+ if (!m) return;
113
+ var row = document.createElement('div');
114
+ row.className = 'row';
115
+ row.style.cursor = 'default';
116
+ row.innerHTML =
117
+ '<span class="emoji">' + m.emoji + '</span>' +
118
+ '<div class="grow"><div class="name">' + m.name + ' ร—' + line.qty + '</div></div>' +
119
+ '<div class="price">' + m.price * line.qty + ' SAR</div>';
120
+ itemsBox.appendChild(row);
121
+ });
122
+ });
123
+ </script>
124
+ </body>
125
+ </html>
@@ -0,0 +1,95 @@
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 โ€” Order status (Android)</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Status โ€” Android",
10
+ "description": "The same pickup moment in the Android frame (Material-ish idioms).",
11
+ "order": 50,
12
+ "device": "android",
13
+ "status": "draft",
14
+ "tags": ["android", "live"],
15
+ "group": "Other devices",
16
+ "updated": "2026-07-18"
17
+ }
18
+ </script>
19
+ <script src="../../../engine/bridge.js"></script>
20
+ <link rel="stylesheet" href="shared.css">
21
+ <style>
22
+ /* Material-ish accents for the Android take */
23
+ .m-card { border-radius: 24px; }
24
+ .m-fab {
25
+ position: fixed;
26
+ inset-inline-end: 18px;
27
+ bottom: calc(var(--safe-bottom) + 18px);
28
+ width: 56px; height: 56px;
29
+ border-radius: 18px;
30
+ border: 0;
31
+ background: var(--color-accent);
32
+ color: var(--color-accent-ink, #fff);
33
+ font-size: 22px;
34
+ cursor: pointer;
35
+ box-shadow: 0 6px 18px color-mix(in srgb, var(--color-accent) 45%, transparent);
36
+ }
37
+ .m-progress {
38
+ height: 6px; border-radius: 3px;
39
+ background: var(--color-line);
40
+ overflow: hidden;
41
+ }
42
+ .m-progress i { display: block; height: 100%; background: var(--color-accent); border-radius: 3px; }
43
+ </style>
44
+ </head>
45
+ <body>
46
+ <div class="app">
47
+ <div class="appbar" style="padding-top:14px">
48
+ <h1 style="font-size:22px;font-family:var(--font-sans);font-weight:800">Your order</h1>
49
+ <span class="chip-points">โ˜• Qahwa</span>
50
+ </div>
51
+
52
+ <div class="card m-card" id="card" hidden>
53
+ <div style="display:flex;align-items:center;justify-content:space-between">
54
+ <div>
55
+ <div class="small muted">Order</div>
56
+ <div style="font-size:26px;font-weight:800" id="o-id"></div>
57
+ </div>
58
+ <span class="badge warn" id="o-badge"></span>
59
+ </div>
60
+ <div style="margin-top:16px" class="m-progress"><i id="o-bar" style="width:40%"></i></div>
61
+ <div class="small muted" style="margin-top:8px" id="o-eta"></div>
62
+ </div>
63
+
64
+ <div id="none" hidden class="card m-card" style="text-align:center;padding:34px">
65
+ <div style="font-size:40px">๐ŸŒ™</div>
66
+ <div style="font-weight:800;margin-top:8px">No active order</div>
67
+ </div>
68
+ </div>
69
+
70
+ <button class="m-fab" id="fab" title="New order">+</button>
71
+
72
+ <script>
73
+ Engine.ready(function (d) {
74
+ var pct = { received: 15, brewing: 55, ready: 100, 'picked-up': 100 };
75
+ if (!d.order) {
76
+ document.getElementById('none').hidden = false;
77
+ } else {
78
+ var card = document.getElementById('card');
79
+ card.hidden = false;
80
+ var ready = d.order.phase === 'ready';
81
+ document.getElementById('o-id').textContent = d.order.id;
82
+ var badge = document.getElementById('o-badge');
83
+ badge.textContent = (d.order.phase || 'received').toUpperCase();
84
+ badge.className = 'badge ' + (ready ? 'good' : 'warn');
85
+ document.getElementById('o-bar').style.width = (pct[d.order.phase] || 15) + '%';
86
+ document.getElementById('o-eta').textContent =
87
+ ready ? 'Waiting at the counter' : 'Ready by ' + d.order.readyBy;
88
+ }
89
+ document.getElementById('fab').addEventListener('click', function () {
90
+ Engine.toast('Ordering lives on the iOS screens in this demo');
91
+ });
92
+ });
93
+ </script>
94
+ </body>
95
+ </html>
@@ -0,0 +1,146 @@
1
+ /* Qahwa ordering โ€” shared app chrome (EN-23 opt-in per screen). Tokens only. */
2
+
3
+ * { box-sizing: border-box; }
4
+ html, body { height: 100%; }
5
+ body {
6
+ margin: 0;
7
+ background: var(--color-bg);
8
+ color: var(--color-ink);
9
+ font-family: var(--font-sans);
10
+ -webkit-font-smoothing: antialiased;
11
+ }
12
+
13
+ .app {
14
+ min-height: 100vh;
15
+ display: flex;
16
+ flex-direction: column;
17
+ padding: calc(var(--safe-top) + 8px) var(--space-page) calc(var(--safe-bottom) + 16px);
18
+ }
19
+
20
+ .appbar {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: space-between;
24
+ gap: 10px;
25
+ padding: 8px 2px 14px;
26
+ }
27
+ .appbar .back {
28
+ border: 0; background: var(--color-card);
29
+ width: 34px; height: 34px; border-radius: 50%;
30
+ font-size: 15px; color: var(--color-ink);
31
+ cursor: pointer; border: 1px solid var(--color-line);
32
+ }
33
+ .appbar h1 {
34
+ margin: 0;
35
+ font-family: var(--font-serif);
36
+ font-size: 24px;
37
+ letter-spacing: -0.3px;
38
+ }
39
+ .appbar .spacer { width: 34px; }
40
+
41
+ .chip-points {
42
+ display: inline-flex; align-items: center; gap: 5px;
43
+ background: var(--color-card);
44
+ border: 1px solid var(--color-line);
45
+ border-radius: var(--radius-chip);
46
+ padding: 6px 11px;
47
+ font-size: 12px; font-weight: 700;
48
+ }
49
+
50
+ .card {
51
+ background: var(--color-card);
52
+ border: 1px solid var(--color-line);
53
+ border-radius: var(--radius-card);
54
+ padding: 16px;
55
+ }
56
+
57
+ .row {
58
+ display: flex; align-items: center; gap: 12px;
59
+ padding: 12px 4px;
60
+ border-bottom: 1px solid var(--color-line);
61
+ cursor: pointer;
62
+ }
63
+ .row:last-child { border-bottom: 0; }
64
+ .row .emoji {
65
+ width: 44px; height: 44px;
66
+ display: grid; place-items: center;
67
+ background: var(--color-bg);
68
+ border: 1px solid var(--color-line);
69
+ border-radius: 14px;
70
+ font-size: 22px;
71
+ flex: 0 0 auto;
72
+ }
73
+ .row .grow { flex: 1; min-width: 0; }
74
+ .row .name { font-weight: 700; font-size: 15px; }
75
+ .row .desc { font-size: 12.5px; color: var(--color-ink-2); margin-top: 2px; line-height: 1.4; }
76
+ .row .price { font-weight: 700; font-size: 14px; white-space: nowrap; }
77
+ .row.is-off { opacity: .45; cursor: default; }
78
+
79
+ .btn {
80
+ display: block; width: 100%;
81
+ border: 0; padding: 15px;
82
+ border-radius: 16px;
83
+ background: var(--color-accent);
84
+ color: var(--color-accent-ink, #fff);
85
+ font-family: inherit; font-size: 16px; font-weight: 700;
86
+ cursor: pointer;
87
+ }
88
+ .btn:active { transform: scale(.985); }
89
+ .btn.ghost {
90
+ background: var(--color-card);
91
+ color: var(--color-ink);
92
+ border: 1px solid var(--color-line);
93
+ }
94
+
95
+ .muted { color: var(--color-ink-2); }
96
+ .small { font-size: 12.5px; }
97
+
98
+ .badge {
99
+ display: inline-flex; align-items: center; gap: 6px;
100
+ border-radius: var(--radius-chip);
101
+ padding: 5px 10px;
102
+ font-size: 11.5px; font-weight: 700;
103
+ }
104
+ .badge.good { background: color-mix(in srgb, var(--color-good) 14%, transparent); color: var(--color-good); }
105
+ .badge.warn { background: color-mix(in srgb, var(--color-warn) 16%, transparent); color: var(--color-warn); }
106
+
107
+ /* bottom sheet (lives inside its screen, EN-24) */
108
+ .sheet-veil {
109
+ position: fixed; inset: 0;
110
+ background: rgba(20, 12, 5, 0.45);
111
+ opacity: 0; pointer-events: none;
112
+ transition: opacity .2s ease;
113
+ z-index: 40;
114
+ }
115
+ .sheet {
116
+ position: fixed; left: 0; right: 0; bottom: 0;
117
+ background: var(--color-card);
118
+ border-radius: 22px 22px 0 0;
119
+ padding: 14px var(--space-page) calc(var(--safe-bottom) + 16px);
120
+ transform: translateY(105%);
121
+ transition: transform .24s ease;
122
+ z-index: 41;
123
+ }
124
+ .sheet .grab {
125
+ width: 40px; height: 4px; border-radius: 2px;
126
+ background: var(--color-line);
127
+ margin: 0 auto 12px;
128
+ }
129
+ body.sheet-open .sheet-veil { opacity: 1; pointer-events: auto; }
130
+ body.sheet-open .sheet { transform: translateY(0); }
131
+
132
+ .seg { display: flex; gap: 8px; }
133
+ .seg button {
134
+ flex: 1; padding: 11px 6px;
135
+ border: 1px solid var(--color-line);
136
+ border-radius: 12px;
137
+ background: var(--color-bg);
138
+ color: var(--color-ink);
139
+ font-family: inherit; font-size: 13px; font-weight: 650;
140
+ cursor: pointer;
141
+ }
142
+ .seg button.on {
143
+ background: var(--color-accent);
144
+ border-color: var(--color-accent);
145
+ color: var(--color-accent-ink, #fff);
146
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "list": [
3
+ {
4
+ "id": "default",
5
+ "icon": "๐Ÿงช",
6
+ "title": "Steady morning",
7
+ "description": "A calm queue โ€” two ready, one brewing."
8
+ },
9
+ {
10
+ "id": "rush",
11
+ "icon": "๐Ÿ”ฅ",
12
+ "title": "Friday rush",
13
+ "description": "The queue when the mosque lets out.",
14
+ "patch": {
15
+ "queue": [
16
+ { "id": "Q-2493", "customer": "Walk-in", "items": "Espresso ร—4", "total": 48, "placed": "12:44", "status": "queued" },
17
+ { "id": "Q-2492", "customer": "Aisha", "items": "Saffron Latte ร—2", "total": 48, "placed": "12:43", "status": "queued" },
18
+ { "id": "Q-2491", "customer": "Careem rider", "items": "Cold Brew ร—3 ยท Flat White ร—2", "total": 86, "placed": "12:41", "status": "brewing" },
19
+ { "id": "Q-2490", "customer": "Omar", "items": "V60 ร—1", "total": 22, "placed": "12:40", "status": "brewing" },
20
+ { "id": "Q-2489", "customer": "Huda", "items": "Flat White ร—1", "total": 16, "placed": "12:38", "status": "ready" },
21
+ { "id": "Q-2488", "customer": "Yousef", "items": "Espresso ร—2", "total": 24, "placed": "12:35", "status": "ready" },
22
+ { "id": "Q-2487", "customer": "Reem", "items": "Cold Brew ร—1", "total": 18, "placed": "12:31", "status": "done" }
23
+ ]
24
+ }
25
+ },
26
+ {
27
+ "id": "closed",
28
+ "icon": "๐ŸŒ™",
29
+ "title": "After close",
30
+ "description": "Empty queue โ€” the end-of-day state.",
31
+ "remove": ["queue"],
32
+ "patch": { "queue": [] }
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "shop": { "name": "Qahwa Roastery", "branch": "As-Sahafa ยท Riyadh", "day": "{{today}}" },
3
+ "queue": [
4
+ { "id": "Q-2481", "customer": "Lina", "items": "Flat White ร—1 ยท V60 ร—1", "total": 38, "placed": "08:31", "status": "brewing" },
5
+ { "id": "Q-2480", "customer": "Mohannad", "items": "Cold Brew ร—2", "total": 36, "placed": "08:28", "status": "ready" },
6
+ { "id": "Q-2479", "customer": "Sara", "items": "Espresso ร—1", "total": 12, "placed": "08:26", "status": "ready" },
7
+ { "id": "Q-2478", "customer": "Faisal", "items": "V60 ร—1 ยท Espresso ร—2", "total": 46, "placed": "08:19", "status": "done" },
8
+ { "id": "Q-2477", "customer": "Noura", "items": "Flat White ร—2", "total": 32, "placed": "08:12", "status": "done" }
9
+ ],
10
+ "menu": [
11
+ { "id": "flat-white", "name": "Flat White", "price": 16, "emoji": "โ˜•", "available": true, "sold": 41 },
12
+ { "id": "v60", "name": "V60 Pour-over", "price": 22, "emoji": "๐Ÿซ–", "available": true, "sold": 18 },
13
+ { "id": "espresso", "name": "Espresso", "price": 12, "emoji": "๐Ÿฅƒ", "available": true, "sold": 33 },
14
+ { "id": "cold-brew", "name": "Cold Brew", "price": 18, "emoji": "๐ŸงŠ", "available": true, "sold": 24 },
15
+ { "id": "saffron-latte", "name": "Saffron Latte", "price": 24, "emoji": "๐ŸŒผ", "available": false, "sold": 0 }
16
+ ]
17
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "roastery",
3
+ "title": "Roastery desk",
4
+ "icon": "๐Ÿ–ฅ",
5
+ "description": "The staff side on the web โ€” live order queue and the menu editor.",
6
+ "order": 2
7
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "list": [
3
+ {
4
+ "id": "friday-rush",
5
+ "title": "Surviving the rush",
6
+ "icon": "๐Ÿ”ฅ",
7
+ "description": "The desk during peak โ€” queue triage, then menu tweaks.",
8
+ "steps": [
9
+ { "screen": "010-orders", "scenario": "rush", "note": "Friday 12:44 โ€” seven tickets. Queued/brewing/ready rows sort themselves." },
10
+ { "screen": "020-menu-editor", "note": "Saffron Latte is 86'd; flipping availability here hides it in the customer app." }
11
+ ]
12
+ }
13
+ ]
14
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "feature": "roastery",
3
+ "generated": "2026-07-18",
4
+ "screens": [
5
+ {
6
+ "id": "010-orders",
7
+ "file": "screens/010-orders.html",
8
+ "title": "Live orders",
9
+ "description": "The queue board โ€” KPIs on top, tickets sorted by state. Try the ๐Ÿ”ฅ rush scenario.",
10
+ "order": 10,
11
+ "device": "web",
12
+ "status": "ready",
13
+ "tags": [
14
+ "dashboard"
15
+ ],
16
+ "updated": "2026-07-18"
17
+ },
18
+ {
19
+ "id": "020-menu-editor",
20
+ "file": "screens/020-menu-editor.html",
21
+ "title": "Menu editor",
22
+ "description": "Prices + availability switches; unsaved-changes bar demonstrates in-screen state.",
23
+ "order": 20,
24
+ "device": "web",
25
+ "status": "ready",
26
+ "tags": [
27
+ "admin"
28
+ ],
29
+ "updated": "2026-07-18"
30
+ }
31
+ ]
32
+ }
@@ -0,0 +1,103 @@
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>Roastery โ€” Live orders</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Live orders",
10
+ "description": "The queue board โ€” KPIs on top, tickets sorted by state. Try the ๐Ÿ”ฅ rush scenario.",
11
+ "order": 10,
12
+ "device": "web",
13
+ "status": "ready",
14
+ "tags": ["dashboard"],
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="desk">
23
+ <header class="desk-top">
24
+ <div class="brand"><span class="mark">โ˜•</span> <span id="shop">Qahwa Roastery</span></div>
25
+ <nav>
26
+ <button class="on">Orders</button>
27
+ <button data-go="020-menu-editor">Menu</button>
28
+ </nav>
29
+ <div class="meta" id="daymeta"></div>
30
+ </header>
31
+
32
+ <main class="desk-body">
33
+ <div class="kpis">
34
+ <div class="kpi"><div class="v" id="k-open">0</div><div class="l">Open tickets</div></div>
35
+ <div class="kpi"><div class="v" id="k-ready">0</div><div class="l">Waiting at counter</div></div>
36
+ <div class="kpi"><div class="v" id="k-done">0</div><div class="l">Served today</div></div>
37
+ <div class="kpi"><div class="v" id="k-rev">0</div><div class="l">Revenue (SAR)</div></div>
38
+ </div>
39
+
40
+ <div class="panel">
41
+ <div class="panel-head">
42
+ <span>Queue</span>
43
+ <button class="btn-sm" id="mark-all">Mark all ready โ†’ done</button>
44
+ </div>
45
+ <table>
46
+ <thead>
47
+ <tr><th>Ticket</th><th>Customer</th><th>Items</th><th>Placed</th><th style="text-align:end">Total</th><th>Status</th></tr>
48
+ </thead>
49
+ <tbody id="rows"></tbody>
50
+ </table>
51
+ <div id="empty" hidden style="padding:44px;text-align:center;color:var(--color-ink-2)">
52
+ <div style="font-size:38px">๐ŸŒ™</div>
53
+ <div style="font-weight:700;margin-top:8px;color:var(--color-ink)">Queue's clear</div>
54
+ <div style="font-size:12.5px;margin-top:3px">Every ticket served. Time to cup tomorrow's roast.</div>
55
+ </div>
56
+ </div>
57
+ </main>
58
+ </div>
59
+
60
+ <script>
61
+ var RANK = { queued: 0, brewing: 1, ready: 2, done: 3 };
62
+ Engine.ready(function (d) {
63
+ var queue = (d.queue || []).slice().sort(function (a, b) {
64
+ return (RANK[a.status] ?? 9) - (RANK[b.status] ?? 9) || (b.placed > a.placed ? 1 : -1);
65
+ });
66
+ document.getElementById('shop').textContent = (d.shop && d.shop.name) || 'Roastery';
67
+ document.getElementById('daymeta').textContent =
68
+ (d.shop ? d.shop.branch + ' ยท ' : '') + (d.shop ? d.shop.day : '');
69
+
70
+ document.getElementById('k-open').textContent =
71
+ queue.filter(function (o) { return o.status === 'queued' || o.status === 'brewing'; }).length;
72
+ document.getElementById('k-ready').textContent =
73
+ queue.filter(function (o) { return o.status === 'ready'; }).length;
74
+ document.getElementById('k-done').textContent =
75
+ queue.filter(function (o) { return o.status === 'done'; }).length;
76
+ document.getElementById('k-rev').textContent =
77
+ queue.reduce(function (s, o) { return s + (o.total || 0); }, 0);
78
+
79
+ var tbody = document.getElementById('rows');
80
+ if (!queue.length) {
81
+ document.getElementById('empty').hidden = false;
82
+ document.querySelector('table').style.display = 'none';
83
+ }
84
+ queue.forEach(function (o) {
85
+ var tr = document.createElement('tr');
86
+ tr.innerHTML =
87
+ '<td class="num">' + o.id + '</td>' +
88
+ '<td>' + o.customer + '</td>' +
89
+ '<td class="muted">' + o.items + '</td>' +
90
+ '<td class="num">' + o.placed + '</td>' +
91
+ '<td class="num" style="text-align:end">' + o.total + '</td>' +
92
+ '<td><span class="st ' + o.status + '">' + o.status + '</span></td>';
93
+ tbody.appendChild(tr);
94
+ });
95
+
96
+ document.getElementById('mark-all').addEventListener('click', function () {
97
+ var n = queue.filter(function (o) { return o.status === 'ready'; }).length;
98
+ Engine.toast(n ? n + ' ticket' + (n > 1 ? 's' : '') + ' handed over โœ“' : 'Nothing waiting');
99
+ });
100
+ });
101
+ </script>
102
+ </body>
103
+ </html>
@@ -0,0 +1,88 @@
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>Roastery โ€” Menu editor</title>
7
+ <script type="application/json" id="screen-meta">
8
+ {
9
+ "title": "Menu editor",
10
+ "description": "Prices + availability switches; unsaved-changes bar demonstrates in-screen state.",
11
+ "order": 20,
12
+ "device": "web",
13
+ "status": "ready",
14
+ "tags": ["admin"],
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="desk">
23
+ <header class="desk-top">
24
+ <div class="brand"><span class="mark">โ˜•</span> <span id="shop">Qahwa Roastery</span></div>
25
+ <nav>
26
+ <button data-go="010-orders">Orders</button>
27
+ <button class="on">Menu</button>
28
+ </nav>
29
+ <div class="meta" id="daymeta"></div>
30
+ </header>
31
+
32
+ <main class="desk-body">
33
+ <div class="panel">
34
+ <div class="panel-head">
35
+ <span>Menu ยท <span id="count" class="muted"></span></span>
36
+ <span style="display:flex;gap:8px;align-items:center">
37
+ <span id="dirty" hidden class="st brewing">unsaved changes</span>
38
+ <button class="btn-sm primary" id="save">Save menu</button>
39
+ </span>
40
+ </div>
41
+ <table>
42
+ <thead>
43
+ <tr><th></th><th>Drink</th><th>Sold today</th><th>Price (SAR)</th><th>Available</th></tr>
44
+ </thead>
45
+ <tbody id="rows"></tbody>
46
+ </table>
47
+ </div>
48
+ <p class="meta" style="margin-top:12px;color:var(--color-ink-2);font-size:12.5px">
49
+ Flipping availability here is what hides a drink in the customer app โ€” same seed, two surfaces.
50
+ </p>
51
+ </main>
52
+ </div>
53
+
54
+ <script>
55
+ Engine.ready(function (d) {
56
+ var menu = (d.menu || []).map(function (m) { return Object.assign({}, m); });
57
+ document.getElementById('shop').textContent = (d.shop && d.shop.name) || 'Roastery';
58
+ document.getElementById('daymeta').textContent = d.shop ? d.shop.branch + ' ยท ' + d.shop.day : '';
59
+ document.getElementById('count').textContent = menu.length + ' drinks';
60
+
61
+ var dirtyBadge = document.getElementById('dirty');
62
+ function markDirty() { dirtyBadge.hidden = false; }
63
+
64
+ var tbody = document.getElementById('rows');
65
+ menu.forEach(function (m) {
66
+ var tr = document.createElement('tr');
67
+ var priceId = 'p-' + m.id;
68
+ tr.innerHTML =
69
+ '<td style="font-size:20px">' + m.emoji + '</td>' +
70
+ '<td style="font-weight:650">' + m.name + '</td>' +
71
+ '<td class="num">' + (m.sold || 0) + '</td>' +
72
+ '<td class="num"><input id="' + priceId + '" value="' + m.price + '" inputmode="numeric" ' +
73
+ 'style="width:64px;padding:6px 8px;border:1px solid var(--color-line);border-radius:8px;' +
74
+ 'background:var(--color-bg);color:var(--color-ink);font:inherit"></td>' +
75
+ '<td><label class="switch"><input type="checkbox" ' + (m.available ? 'checked' : '') + '><i></i></label></td>';
76
+ tr.querySelector('input[inputmode]').addEventListener('input', markDirty);
77
+ tr.querySelector('input[type=checkbox]').addEventListener('change', markDirty);
78
+ tbody.appendChild(tr);
79
+ });
80
+
81
+ document.getElementById('save').addEventListener('click', function () {
82
+ dirtyBadge.hidden = true;
83
+ Engine.toast('Menu saved โ€” the customer app sees it instantly โœ“');
84
+ });
85
+ });
86
+ </script>
87
+ </body>
88
+ </html>