@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.
- package/CLAUDE.md +185 -0
- package/LICENSE +21 -0
- package/README.md +208 -0
- package/TESTING.md +54 -0
- package/app-bridge.js +131 -0
- package/bridge.js +598 -0
- package/cli/screenbook.js +290 -0
- package/cli/templates.js +139 -0
- package/core/browser.js +108 -0
- package/core/fsx.js +71 -0
- package/core/meta.js +94 -0
- package/core/project.js +482 -0
- package/core/serve.js +224 -0
- package/docs/README.md +21 -0
- package/docs/app-mode-guide.md +79 -0
- package/docs/authoring-guide.md +129 -0
- package/docs/cli.md +78 -0
- package/docs/getting-started.md +88 -0
- package/docs/mcp-agents.md +60 -0
- package/docs/shots/android-dark.png +0 -0
- package/docs/shots/app-mode.png +0 -0
- package/docs/shots/hero-studio.png +0 -0
- package/docs/shots/rtl-arabic.png +0 -0
- package/docs/shots/theme-lab.png +0 -0
- package/docs/shots/theming-noir.png +0 -0
- package/docs/shots/web-dashboard.png +0 -0
- package/docs/studio-guide.md +64 -0
- package/docs/troubleshooting.md +55 -0
- package/examples/qahwa/app.config.json +9 -0
- package/examples/qahwa/features/order/data/scenarios.json +25 -0
- package/examples/qahwa/features/order/data/seed.json +21 -0
- package/examples/qahwa/features/order/feature.json +7 -0
- package/examples/qahwa/features/order/flows.json +16 -0
- package/examples/qahwa/features/order/manifest.json +78 -0
- package/examples/qahwa/features/order/screens/010-menu.html +121 -0
- package/examples/qahwa/features/order/screens/020-drink.html +120 -0
- package/examples/qahwa/features/order/screens/030-cart.html +99 -0
- package/examples/qahwa/features/order/screens/040-status.html +125 -0
- package/examples/qahwa/features/order/screens/050-status-android.html +95 -0
- package/examples/qahwa/features/order/screens/shared.css +146 -0
- package/examples/qahwa/features/roastery/data/scenarios.json +35 -0
- package/examples/qahwa/features/roastery/data/seed.json +17 -0
- package/examples/qahwa/features/roastery/feature.json +7 -0
- package/examples/qahwa/features/roastery/flows.json +14 -0
- package/examples/qahwa/features/roastery/manifest.json +32 -0
- package/examples/qahwa/features/roastery/screens/010-orders.html +103 -0
- package/examples/qahwa/features/roastery/screens/020-menu-editor.html +88 -0
- package/examples/qahwa/features/roastery/screens/shared.css +115 -0
- package/examples/qahwa/review/comments.json +3 -0
- package/examples/qahwa/themes/qahwa.json +43 -0
- package/examples/spa-demo/app/index.html +100 -0
- package/examples/spa-demo/app.config.json +9 -0
- package/examples/spa-demo/features/notes-app/feature.json +11 -0
- package/examples/spa-demo/review/comments.json +3 -0
- package/examples/spa-demo/themes/default.json +15 -0
- package/examples/tokens-lab/app.config.json +9 -0
- package/examples/tokens-lab/features/lab/data/scenarios.json +5 -0
- package/examples/tokens-lab/features/lab/data/seed.json +3 -0
- package/examples/tokens-lab/features/lab/feature.json +7 -0
- package/examples/tokens-lab/features/lab/flows.json +14 -0
- package/examples/tokens-lab/features/lab/manifest.json +33 -0
- package/examples/tokens-lab/features/lab/screens/010-swatches.html +112 -0
- package/examples/tokens-lab/features/lab/screens/020-typography.html +115 -0
- package/examples/tokens-lab/review/comments.json +3 -0
- package/examples/tokens-lab/themes/noir.json +32 -0
- package/examples/tokens-lab/themes/paper.json +32 -0
- package/index.html +50 -0
- package/mcp/server.js +366 -0
- package/package.json +57 -0
- package/shell/flowplayer.js +85 -0
- package/shell/frames.js +108 -0
- package/shell/main.js +397 -0
- package/shell/net.js +126 -0
- package/shell/review.js +213 -0
- package/shell/screenhost.js +380 -0
- package/shell/selftest.js +275 -0
- package/shell/shell.css +952 -0
- package/shell/sidebar.js +112 -0
- package/shell/store.js +119 -0
- package/shell/themelab.js +186 -0
- package/shell/toast.js +15 -0
- package/shell/toolbar.js +128 -0
- package/skill/SKILL.md +52 -0
- package/skill/references/app-mode.md +77 -0
- package/skill/references/bridge-api.md +55 -0
- package/skill/references/cli-mcp.md +53 -0
- package/skill/references/flows.md +44 -0
- package/skill/references/i18n.md +52 -0
- package/skill/references/scenarios.md +45 -0
- package/skill/references/screen-contract.md +83 -0
- package/skill/references/tokens.md +56 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/* ?selftest=1 — generic smoke (EN-17): every screen × theme × mode × dir
|
|
2
|
+
must render with a valid meta block, reach Engine.ready, and log zero
|
|
3
|
+
console errors. Results land on window.__selftestResult for the headless
|
|
4
|
+
CLI harness (engine validate). */
|
|
5
|
+
|
|
6
|
+
import { store } from './store.js';
|
|
7
|
+
import { url } from './net.js';
|
|
8
|
+
import { injectAppRuntime } from './screenhost.js';
|
|
9
|
+
|
|
10
|
+
const Kit = window.EngineKit;
|
|
11
|
+
const overlay = document.getElementById('selftest-overlay');
|
|
12
|
+
const READY_TIMEOUT = 8000;
|
|
13
|
+
const SETTLE_MS = 120;
|
|
14
|
+
const CONCURRENCY = 4;
|
|
15
|
+
|
|
16
|
+
let farm = null;
|
|
17
|
+
|
|
18
|
+
function combos(project) {
|
|
19
|
+
const themes = [...project.themes.keys()];
|
|
20
|
+
const list = [];
|
|
21
|
+
for (const f of project.features.values()) {
|
|
22
|
+
if (f.type === 'app') continue; // apps run their own pass below
|
|
23
|
+
for (const s of f.manifest.screens || []) {
|
|
24
|
+
const devices = s.devices?.length ? s.devices : [s.device || 'none'];
|
|
25
|
+
for (const device of devices) {
|
|
26
|
+
for (const theme of themes) {
|
|
27
|
+
for (const mode of ['light', 'dark']) {
|
|
28
|
+
for (const dir of ['ltr', 'rtl']) {
|
|
29
|
+
list.push({ feature: f.id, screen: s.id, device, theme, mode, dir });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return list;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runCombo(project, combo) {
|
|
40
|
+
return new Promise((resolve) => {
|
|
41
|
+
const started = performance.now();
|
|
42
|
+
const errors = [];
|
|
43
|
+
let meta = null;
|
|
44
|
+
let settled = false;
|
|
45
|
+
|
|
46
|
+
const iframe = document.createElement('iframe');
|
|
47
|
+
const dims = { ios: [393, 852], android: [412, 915], web: [1280, 760], none: [900, 640] }[combo.device] || [900, 640];
|
|
48
|
+
iframe.style.cssText = `position:fixed;left:-99999px;top:0;width:${dims[0]}px;height:${dims[1]}px;border:0;`;
|
|
49
|
+
farm.append(iframe);
|
|
50
|
+
|
|
51
|
+
const feature = project.features.get(combo.feature);
|
|
52
|
+
const scenarioList = feature?.scenarios?.list || [];
|
|
53
|
+
const scenario = scenarioList.find((x) => x.id === 'default') || null;
|
|
54
|
+
const data = Kit.applyScenario(feature?.seed || {}, scenario);
|
|
55
|
+
const theme = project.themes.get(combo.theme) || Kit.FALLBACK_THEME;
|
|
56
|
+
|
|
57
|
+
function finish(ok, failReason) {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
window.removeEventListener('message', onMessage);
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
iframe.remove();
|
|
63
|
+
resolve({
|
|
64
|
+
...combo,
|
|
65
|
+
ok,
|
|
66
|
+
errors,
|
|
67
|
+
fail: failReason || (errors.length ? errors[0] : null),
|
|
68
|
+
ms: Math.round(performance.now() - started)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const timer = setTimeout(() => finish(false, 'timeout: Engine.ready never fired'), READY_TIMEOUT);
|
|
73
|
+
|
|
74
|
+
function onMessage(e) {
|
|
75
|
+
if (e.source !== iframe.contentWindow) return;
|
|
76
|
+
const msg = e.data;
|
|
77
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
78
|
+
if (msg.type === 'sb:hello') {
|
|
79
|
+
meta = msg.meta || null;
|
|
80
|
+
iframe.contentWindow.postMessage({
|
|
81
|
+
type: 'sb:init',
|
|
82
|
+
theme,
|
|
83
|
+
themeId: combo.theme,
|
|
84
|
+
mode: combo.mode,
|
|
85
|
+
dir: combo.dir,
|
|
86
|
+
lang: project.config.defaultLanguage || 'en',
|
|
87
|
+
device: combo.device,
|
|
88
|
+
scenario: 'default',
|
|
89
|
+
data,
|
|
90
|
+
review: { active: false, pins: [] }
|
|
91
|
+
}, '*');
|
|
92
|
+
} else if (msg.type === 'sb:console-error') {
|
|
93
|
+
errors.push(msg.message);
|
|
94
|
+
} else if (msg.type === 'sb:ready') {
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
if (!meta || typeof meta.title !== 'string' || !meta.title) {
|
|
97
|
+
finish(false, 'meta block missing or has no title');
|
|
98
|
+
} else if (errors.length) {
|
|
99
|
+
finish(false, errors[0]);
|
|
100
|
+
} else {
|
|
101
|
+
finish(true);
|
|
102
|
+
}
|
|
103
|
+
}, SETTLE_MS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
window.addEventListener('message', onMessage);
|
|
108
|
+
iframe.src = url(`features/${combo.feature}/screens/${encodeURIComponent(combo.screen)}.html`);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* App-type feature (EN-20 gate, per Abdullah's panel pick): boot the app,
|
|
113
|
+
require hello + a screens list, then VISIT EVERY SCREEN once — zero
|
|
114
|
+
console errors anywhere. */
|
|
115
|
+
function runAppFeature(f, report) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
const dims = { ios: [393, 852], android: [412, 915], web: [1280, 760], none: [1000, 700] }[f.meta.device || 'none'] || [1000, 700];
|
|
118
|
+
const iframe = document.createElement('iframe');
|
|
119
|
+
iframe.style.cssText = `position:fixed;left:-99999px;top:0;width:${dims[0]}px;height:${dims[1]}px;border:0;`;
|
|
120
|
+
farm.append(iframe);
|
|
121
|
+
|
|
122
|
+
const errors = [];
|
|
123
|
+
let screens = null;
|
|
124
|
+
let helloTimer = null;
|
|
125
|
+
let currentResolve = null;
|
|
126
|
+
let visiting = false;
|
|
127
|
+
|
|
128
|
+
function row(screen, ok, fail, ms) {
|
|
129
|
+
report({ feature: f.id, screen, theme: 'app', mode: '—', dir: '—', ok, fail, errors: [...errors], ms });
|
|
130
|
+
errors.length = 0;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finish() {
|
|
134
|
+
window.removeEventListener('message', onMessage);
|
|
135
|
+
clearTimeout(helloTimer);
|
|
136
|
+
iframe.remove();
|
|
137
|
+
resolve();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function onMessage(e) {
|
|
141
|
+
if (e.source !== iframe.contentWindow) return;
|
|
142
|
+
const msg = e.data;
|
|
143
|
+
if (!msg || typeof msg.type !== 'string') return;
|
|
144
|
+
if (msg.type === 'sba:console-error') errors.push(msg.message);
|
|
145
|
+
else if (msg.type === 'sba:screens') screens = msg.screens || [];
|
|
146
|
+
else if (msg.type === 'sba:current' && currentResolve) currentResolve(msg.id);
|
|
147
|
+
if (msg.type === 'sba:screens') visitAll();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function visitAll() {
|
|
151
|
+
if (visiting) return;
|
|
152
|
+
visiting = true;
|
|
153
|
+
clearTimeout(helloTimer);
|
|
154
|
+
if (!screens.length) {
|
|
155
|
+
row('(app)', false, 'app registered but reported zero screens', 0);
|
|
156
|
+
return finish();
|
|
157
|
+
}
|
|
158
|
+
row('(boot)', errors.length === 0, errors[0] || null, 0);
|
|
159
|
+
for (const s of screens) {
|
|
160
|
+
const started = performance.now();
|
|
161
|
+
iframe.contentWindow.postMessage({ type: 'sba:go', id: s.id }, '*');
|
|
162
|
+
await new Promise((r) => {
|
|
163
|
+
currentResolve = r;
|
|
164
|
+
setTimeout(r, 700);
|
|
165
|
+
});
|
|
166
|
+
currentResolve = null;
|
|
167
|
+
await new Promise((r) => setTimeout(r, SETTLE_MS));
|
|
168
|
+
row(s.id, errors.length === 0, errors[0] || null, Math.round(performance.now() - started));
|
|
169
|
+
}
|
|
170
|
+
finish();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
window.addEventListener('message', onMessage);
|
|
174
|
+
helloTimer = setTimeout(() => {
|
|
175
|
+
row('(boot)', false, 'app never registered (no sba:hello/screens within 20s)', 20000);
|
|
176
|
+
finish();
|
|
177
|
+
}, 20000);
|
|
178
|
+
|
|
179
|
+
iframe.addEventListener('load', () => {
|
|
180
|
+
if (f.meta.driver) {
|
|
181
|
+
injectAppRuntime(iframe, f.id, f.meta.driver, (failText) => {
|
|
182
|
+
errors.push(failText);
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
iframe.src = url(f.meta.src);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function esc(s) {
|
|
191
|
+
return String(s ?? '').replace(/[&<>"]/g, (c) => (
|
|
192
|
+
{ '&': '&', '<': '<', '>': '>', '"': '"' }[c]
|
|
193
|
+
));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function runSelftest() {
|
|
197
|
+
const project = store.project;
|
|
198
|
+
const all = combos(project);
|
|
199
|
+
|
|
200
|
+
overlay.hidden = false;
|
|
201
|
+
overlay.innerHTML =
|
|
202
|
+
`<div class="st-card">
|
|
203
|
+
<div class="st-head"><h2>ScreenBook self-test</h2><span class="st-summary" id="st-summary">0 / ${all.length}</span></div>
|
|
204
|
+
<div class="st-progress"><i id="st-bar"></i></div>
|
|
205
|
+
<div class="st-rows" id="st-rows"></div>
|
|
206
|
+
<div class="st-foot">
|
|
207
|
+
<span class="st-note">screens × themes × light/dark × ltr/rtl · zero console errors · meta present</span>
|
|
208
|
+
<button class="btn-ghost" onclick="location.search=''">Close</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>`;
|
|
211
|
+
farm = document.createElement('div');
|
|
212
|
+
overlay.append(farm);
|
|
213
|
+
|
|
214
|
+
const rowsBox = document.getElementById('st-rows');
|
|
215
|
+
const summary = document.getElementById('st-summary');
|
|
216
|
+
const bar = document.getElementById('st-bar');
|
|
217
|
+
const results = [];
|
|
218
|
+
let done = 0;
|
|
219
|
+
|
|
220
|
+
function report(r) {
|
|
221
|
+
results.push(r);
|
|
222
|
+
done++;
|
|
223
|
+
summary.textContent = `${done}${done <= all.length ? ` / ${all.length}` : ' checks'}`;
|
|
224
|
+
bar.style.width = `${Math.min(100, (done / Math.max(1, all.length)) * 100)}%`;
|
|
225
|
+
if (!r.ok) {
|
|
226
|
+
const row = document.createElement('div');
|
|
227
|
+
row.className = 'st-row is-fail';
|
|
228
|
+
row.innerHTML =
|
|
229
|
+
`<span class="st-ico">✕</span><span class="st-name">${esc(r.feature)}/${esc(r.screen)}</span>` +
|
|
230
|
+
`<span class="st-combo">${esc(r.theme)} · ${esc(r.mode)} · ${esc(r.dir)}</span>` +
|
|
231
|
+
`<span class="st-err">${esc(r.fail || '')}</span>`;
|
|
232
|
+
rowsBox.append(row);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const queue = all.slice();
|
|
237
|
+
await Promise.all(Array.from({ length: CONCURRENCY }, async () => {
|
|
238
|
+
while (queue.length) {
|
|
239
|
+
const combo = queue.shift();
|
|
240
|
+
report(await runCombo(project, combo));
|
|
241
|
+
}
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
/* embedded apps: boot each, then visit every screen it reports (EN-20 gate) */
|
|
245
|
+
for (const f of project.features.values()) {
|
|
246
|
+
if (f.type !== 'app') continue;
|
|
247
|
+
summary.textContent = `app: ${f.id}…`;
|
|
248
|
+
await runAppFeature(f, report);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const failed = results.filter((r) => !r.ok);
|
|
252
|
+
const passSummary = `${results.length - failed.length} / ${results.length} passed`;
|
|
253
|
+
summary.textContent = passSummary;
|
|
254
|
+
summary.classList.add(failed.length ? 'is-fail' : 'is-pass');
|
|
255
|
+
bar.style.background = failed.length ? 'var(--sb-danger)' : 'var(--sb-ok)';
|
|
256
|
+
if (!failed.length) {
|
|
257
|
+
const row = document.createElement('div');
|
|
258
|
+
row.className = 'st-row is-pass';
|
|
259
|
+
row.innerHTML = `<span class="st-ico">✓</span><span class="st-name">All screens green</span>` +
|
|
260
|
+
`<span class="st-combo">${results.length} render checks</span>`;
|
|
261
|
+
rowsBox.append(row);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result = {
|
|
265
|
+
total: results.length,
|
|
266
|
+
passed: results.length - failed.length,
|
|
267
|
+
failed: failed.length,
|
|
268
|
+
screens: [...new Set(results.map((r) => `${r.feature}/${r.screen}`))].length,
|
|
269
|
+
rows: results
|
|
270
|
+
};
|
|
271
|
+
window.__selftestResult = result;
|
|
272
|
+
window.__selftestDone = true;
|
|
273
|
+
document.title = failed.length ? `✕ selftest — ${failed.length} failing` : '✓ selftest green';
|
|
274
|
+
return result;
|
|
275
|
+
}
|