@monimesl/dot-simulator 0.1.0-alpha.2

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.
@@ -0,0 +1,690 @@
1
+ (function() {
2
+ //#region ../../core/dist/index.js
3
+ function defineComponent(manifest) {
4
+ return {
5
+ props: {},
6
+ events: [],
7
+ ...manifest
8
+ };
9
+ }
10
+ function componentRegistry(manifests) {
11
+ const registry = {};
12
+ for (const manifest of manifests) {
13
+ if (manifest.name in registry) throw new Error(`Component <${manifest.name}> is defined twice`);
14
+ registry[manifest.name] = manifest;
15
+ }
16
+ return registry;
17
+ }
18
+ const builtinComponents = {
19
+ Button: defineComponent({
20
+ name: "Button",
21
+ props: {
22
+ variant: ["primary", "secondary"],
23
+ disabled: "boolean"
24
+ },
25
+ events: ["click"],
26
+ children: "text"
27
+ }),
28
+ Image: defineComponent({
29
+ name: "Image",
30
+ props: {
31
+ src: "string",
32
+ alt: "string",
33
+ width: "number",
34
+ height: "number"
35
+ },
36
+ events: ["load", "error"],
37
+ children: "none"
38
+ }),
39
+ Input: defineComponent({
40
+ name: "Input",
41
+ props: {
42
+ value: "string",
43
+ placeholder: "string",
44
+ label: "string",
45
+ type: [
46
+ "text",
47
+ "number",
48
+ "email",
49
+ "phone",
50
+ "password"
51
+ ],
52
+ maxLength: "number",
53
+ disabled: "boolean"
54
+ },
55
+ events: ["change", "submit"],
56
+ children: "none"
57
+ }),
58
+ Text: defineComponent({
59
+ name: "Text",
60
+ props: { variant: [
61
+ "title",
62
+ "body",
63
+ "caption"
64
+ ] },
65
+ children: "text",
66
+ inline: true
67
+ }),
68
+ View: defineComponent({
69
+ name: "View",
70
+ props: {
71
+ direction: ["column", "row"],
72
+ gap: [
73
+ "none",
74
+ "small",
75
+ "medium",
76
+ "large"
77
+ ],
78
+ align: [
79
+ "stretch",
80
+ "start",
81
+ "center",
82
+ "end"
83
+ ],
84
+ variant: ["plain", "card"]
85
+ },
86
+ events: ["tap"],
87
+ children: "elements"
88
+ })
89
+ };
90
+ componentRegistry(Object.values(builtinComponents));
91
+ var HostError = class extends Error {
92
+ name = "HostError";
93
+ code;
94
+ constructor(code, message) {
95
+ super(message);
96
+ this.code = code;
97
+ }
98
+ };
99
+ const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
100
+ function majorVersion(version) {
101
+ return parseVersion(version).major;
102
+ }
103
+ function isVersionAtLeast(version, minimum) {
104
+ return compareVersions(version, minimum) >= 0;
105
+ }
106
+ function compareVersions(left, right) {
107
+ const leftVersion = parseVersion(left);
108
+ const rightVersion = parseVersion(right);
109
+ const differences = [
110
+ leftVersion.major - rightVersion.major,
111
+ leftVersion.minor - rightVersion.minor,
112
+ leftVersion.patch - rightVersion.patch
113
+ ];
114
+ for (const difference of differences) if (difference !== 0) return difference;
115
+ return comparePrereleases(leftVersion.prerelease, rightVersion.prerelease);
116
+ }
117
+ function parseVersion(version) {
118
+ const match = SEMVER_PATTERN.exec(version);
119
+ if (!match) throw new Error(`"${version}" is not a semantic version`);
120
+ const [, major, minor, patch, prerelease] = match;
121
+ return {
122
+ major: Number(major),
123
+ minor: Number(minor),
124
+ patch: Number(patch),
125
+ prerelease: prerelease === void 0 ? [] : prerelease.split(".")
126
+ };
127
+ }
128
+ function comparePrereleases(left, right) {
129
+ if (left.length === 0 || right.length === 0) return right.length - left.length;
130
+ const shared = Math.min(left.length, right.length);
131
+ for (let index = 0; index < shared; index++) {
132
+ const order = compareIdentifiers(left[index], right[index]);
133
+ if (order !== 0) return order;
134
+ }
135
+ return left.length - right.length;
136
+ }
137
+ function compareIdentifiers(left, right) {
138
+ const leftIsNumber = /^\d+$/.test(left);
139
+ const rightIsNumber = /^\d+$/.test(right);
140
+ if (leftIsNumber && rightIsNumber) return Number(left) - Number(right);
141
+ if (leftIsNumber) return -1;
142
+ if (rightIsNumber) return 1;
143
+ if (left < right) return -1;
144
+ if (left > right) return 1;
145
+ return 0;
146
+ }
147
+ function isReactCompatible(hostVersion, dotVersion) {
148
+ return majorVersion(hostVersion) === majorVersion(dotVersion) && isVersionAtLeast(hostVersion, dotVersion);
149
+ }
150
+ function createAdaptorTransport(adapter) {
151
+ return {
152
+ post: (message) => adapter.postMessage(message),
153
+ listen(listener) {
154
+ const onMessage = (event) => listener(event.data);
155
+ adapter.addEventListener("message", onMessage);
156
+ return () => adapter.removeEventListener("message", onMessage);
157
+ }
158
+ };
159
+ }
160
+ var NavigationStack = class {
161
+ limit;
162
+ #pages = [];
163
+ #nextInstanceId = 1;
164
+ constructor(limit = 8) {
165
+ this.limit = limit;
166
+ }
167
+ get pages() {
168
+ return this.#pages;
169
+ }
170
+ get top() {
171
+ return this.#pages.at(-1);
172
+ }
173
+ /**
174
+ * Applies a navigation, or refuses it and returns false: a push onto a full stack, or going back
175
+ * from the first page (which the host handles itself, e.g. by closing the dot).
176
+ */
177
+ apply(navigation) {
178
+ switch (navigation.action) {
179
+ case "push":
180
+ if (this.#pages.length >= this.limit) return false;
181
+ break;
182
+ case "replace":
183
+ this.#pages.pop();
184
+ break;
185
+ case "reset":
186
+ this.#pages = [];
187
+ break;
188
+ case "back": {
189
+ const count = Math.min(navigation.delta, this.#pages.length - 1);
190
+ if (count < 1) return false;
191
+ this.#pages = this.#pages.slice(0, -count);
192
+ return true;
193
+ }
194
+ }
195
+ const { page, options } = navigation;
196
+ this.#pages = [...this.#pages, {
197
+ instanceId: this.#nextInstanceId++,
198
+ page,
199
+ options
200
+ }];
201
+ return true;
202
+ }
203
+ };
204
+ //#endregion
205
+ //#region src/compatibility.ts
206
+ var IncompatibleDotError = class extends Error {
207
+ name = "IncompatibleDotError";
208
+ reason;
209
+ constructor(reason, message) {
210
+ super(message);
211
+ this.reason = reason;
212
+ }
213
+ };
214
+ function checkCompatibility(manifest, { dashVersion, minRuntimeVersion, reactVersions }) {
215
+ if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) throw new IncompatibleDotError("runtimeTooOld", `This dot was built with Dot runtime ${manifest.runtime}, but this host runs ${minRuntimeVersion} or newer. Rebuild it with a newer @monimesl/dot-runtime.`);
216
+ const minDashVersion = manifest.dash?.minVersion;
217
+ if (minDashVersion !== void 0 && !isVersionAtLeast(dashVersion, minDashVersion)) throw new IncompatibleDotError("dashTooOld", `This dot needs Dash ${minDashVersion} or newer, and this is Dash ${dashVersion}.`);
218
+ if (reactVersions !== void 0 && !reactVersions.some((version) => isReactCompatible(version, manifest.react))) throw new IncompatibleDotError("reactUnavailable", `This dot needs React ${manifest.react} or a newer ${majorVersion(manifest.react)}.x, which this host doesn't ship.`);
219
+ }
220
+ //#endregion
221
+ //#region src/host-calls.ts
222
+ const UI_METHOD_PREFIX = "dash.ui.";
223
+ var HostCallRunner = class {
224
+ #handlers;
225
+ #permissions;
226
+ #answer;
227
+ #running = /* @__PURE__ */ new Map();
228
+ constructor(handlers, permissions, answer) {
229
+ this.#handlers = handlers;
230
+ this.#permissions = permissions;
231
+ this.#answer = answer;
232
+ }
233
+ run({ id, method, params, timeout }) {
234
+ const entry = this.#handlers[method];
235
+ if (!entry) {
236
+ this.#answer({
237
+ id,
238
+ error: {
239
+ code: "unsupported",
240
+ message: `This host has no "${method}"`
241
+ }
242
+ });
243
+ return;
244
+ }
245
+ const call = {
246
+ method,
247
+ controller: new AbortController()
248
+ };
249
+ if (timeout !== void 0) call.timer = setTimeout(() => {
250
+ this.#fail(id, new HostError("timeout", `"${method}" took longer than ${timeout}ms`));
251
+ }, timeout);
252
+ this.#running.set(id, call);
253
+ this.#invoke(id, entry, {
254
+ method,
255
+ params,
256
+ signal: call.controller.signal
257
+ });
258
+ }
259
+ cancel(id) {
260
+ this.#end(id)?.controller.abort();
261
+ }
262
+ dismissUI() {
263
+ for (const [id, call] of this.#running) if (call.method.startsWith(UI_METHOD_PREFIX)) this.#fail(id, new HostError("cancelled", "The page it was shown on is no longer on top"));
264
+ }
265
+ async #invoke(id, entry, call) {
266
+ try {
267
+ if (typeof entry === "function") {
268
+ this.#succeed(id, await entry(call));
269
+ return;
270
+ }
271
+ await this.#permissions.check(call.method, entry.permission, call.signal);
272
+ this.#succeed(id, await entry.handle(call));
273
+ } catch (error) {
274
+ this.#fail(id, error);
275
+ }
276
+ }
277
+ #succeed(id, value) {
278
+ if (this.#end(id)) this.#answer({
279
+ id,
280
+ value
281
+ });
282
+ }
283
+ #fail(id, error) {
284
+ const call = this.#end(id);
285
+ if (!call) return;
286
+ call.controller.abort();
287
+ this.#answer({
288
+ id,
289
+ error: describeError(error)
290
+ });
291
+ }
292
+ #end(id) {
293
+ const call = this.#running.get(id);
294
+ if (call) {
295
+ this.#running.delete(id);
296
+ clearTimeout(call.timer);
297
+ }
298
+ return call;
299
+ }
300
+ };
301
+ function describeError(error) {
302
+ if (error instanceof HostError) return {
303
+ code: error.code,
304
+ message: error.message
305
+ };
306
+ return {
307
+ code: "failed",
308
+ message: error instanceof Error ? error.message : String(error)
309
+ };
310
+ }
311
+ //#endregion
312
+ //#region src/permissions.ts
313
+ var PermissionGate = class {
314
+ #dotName;
315
+ #declared;
316
+ #ask;
317
+ #answers = /* @__PURE__ */ new Map();
318
+ constructor(dotName, declared, ask) {
319
+ this.#dotName = dotName;
320
+ this.#declared = declared;
321
+ this.#ask = ask;
322
+ }
323
+ async check(method, permission, signal) {
324
+ const reason = this.#declared[permission];
325
+ if (reason === void 0) throw new HostError("denied", `"${method}" needs the "${permission}" permission. Add it to dot.json's permissions, with the reason Dash shows the person.`);
326
+ let allowed = this.#answers.get(permission);
327
+ if (allowed === void 0) {
328
+ allowed = await this.#ask({
329
+ dotName: this.#dotName,
330
+ permission,
331
+ reason
332
+ }, signal);
333
+ this.#answers.set(permission, allowed);
334
+ }
335
+ if (!allowed) throw new HostError("denied", `The person did not allow "${permission}"`);
336
+ }
337
+ };
338
+ //#endregion
339
+ //#region src/host.ts
340
+ function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe }) {
341
+ checkCompatibility(manifest, {
342
+ dashVersion,
343
+ minRuntimeVersion
344
+ });
345
+ const stack = new NavigationStack();
346
+ let context = systemContext;
347
+ const calls = new HostCallRunner(handlers, new PermissionGate(manifest.name, manifest.permissions ?? {}, askPermission), (result) => {
348
+ const message = {
349
+ type: "hostResult",
350
+ payload: result
351
+ };
352
+ observe?.("fromHost", message);
353
+ logic.post(message);
354
+ });
355
+ const broadcast = (message) => {
356
+ observe?.("fromHost", message);
357
+ ui.post(message);
358
+ logic.post(message);
359
+ };
360
+ const syncStack = () => {
361
+ const message = {
362
+ type: "syncStack",
363
+ payload: {
364
+ pages: stack.pages,
365
+ limit: stack.limit
366
+ }
367
+ };
368
+ broadcast(message);
369
+ };
370
+ const syncSystemContext = () => {
371
+ broadcast({
372
+ type: "syncSystemContext",
373
+ payload: context
374
+ });
375
+ };
376
+ const navigate = (navigation) => {
377
+ if (navigation.action !== "back" && !manifest.pages.includes(navigation.page)) return false;
378
+ const previousTop = stack.top;
379
+ if (!stack.apply(navigation)) return false;
380
+ syncStack();
381
+ if (stack.top !== previousTop) calls.dismissUI();
382
+ return true;
383
+ };
384
+ const navigateFromPage = ({ from, ...navigation }) => {
385
+ if (from === stack.top?.instanceId) navigate(navigation);
386
+ };
387
+ const hostMessageHandlers = /* @__PURE__ */ new Map([
388
+ ["navigate", (payload) => navigateFromPage(payload)],
389
+ ["hostCall", (payload) => calls.run(payload)],
390
+ ["hostCancel", (payload) => calls.cancel(payload.id)]
391
+ ]);
392
+ const stopService = logic.listen((raw) => {
393
+ const message = raw;
394
+ const handleHostMessage = hostMessageHandlers.get(message.type);
395
+ if (handleHostMessage) {
396
+ observe?.("toHost", message);
397
+ handleHostMessage(message.payload);
398
+ return;
399
+ }
400
+ observe?.("toWebview", message);
401
+ ui.post(message);
402
+ });
403
+ const stopWebview = ui.listen((raw) => {
404
+ const message = raw;
405
+ observe?.("toService", message);
406
+ logic.post(message);
407
+ if (message.type === "connect") {
408
+ syncSystemContext();
409
+ navigate({
410
+ action: "reset",
411
+ page: manifest.entry,
412
+ options: {}
413
+ });
414
+ }
415
+ });
416
+ return {
417
+ back: () => navigate({
418
+ action: "back",
419
+ delta: 1
420
+ }),
421
+ setSystemContext: (next) => {
422
+ context = next;
423
+ syncSystemContext();
424
+ },
425
+ stop: () => {
426
+ stopService();
427
+ stopWebview();
428
+ }
429
+ };
430
+ }
431
+ const SIMULATOR_VERSIONS = {
432
+ dashVersion: "1.0.0",
433
+ minRuntimeVersion: "0.1.0-alpha.1"
434
+ };
435
+ //#endregion
436
+ //#region src/paths.ts
437
+ const CLIENT_PATH = "/__dot/";
438
+ const EVENTS_PATH = `${CLIENT_PATH}events`;
439
+ const DOT_PATH = "/dot/";
440
+ const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;
441
+ const MANIFEST_URL = `${DOT_PATH}manifest.json`;
442
+ //#endregion
443
+ //#region src/client/dash/system.ts
444
+ const SHAKE_DURATION_MS = 300;
445
+ const SHAKE = [
446
+ { transform: "translateX(0)" },
447
+ { transform: "translateX(-5px)" },
448
+ { transform: "translateX(5px)" },
449
+ { transform: "translateX(-3px)" },
450
+ { transform: "translateX(3px)" },
451
+ { transform: "translateX(0)" }
452
+ ];
453
+ async function vibrate(phone, note, options) {
454
+ note(`vibrated: ${options.style}`);
455
+ await phone.animate(SHAKE, { duration: SHAKE_DURATION_MS }).finished;
456
+ }
457
+ function torch(phone, note, state) {
458
+ phone.dataset.torch = state;
459
+ note(`torch: ${state}`);
460
+ }
461
+ //#endregion
462
+ //#region src/client/dom.ts
463
+ function element(selector) {
464
+ const node = document.querySelector(selector);
465
+ if (!node) throw new Error(`Simulator page is missing ${selector}`);
466
+ return node;
467
+ }
468
+ function createElement(tag, className, text) {
469
+ const node = document.createElement(tag);
470
+ node.className = className;
471
+ if (text !== void 0) node.textContent = text;
472
+ return node;
473
+ }
474
+ //#endregion
475
+ //#region src/client/dash/ui.ts
476
+ const DEFAULT_ACTIONS = [{
477
+ id: "ok",
478
+ label: "OK"
479
+ }];
480
+ function alert(phone, options, signal) {
481
+ return new Promise((resolve, reject) => {
482
+ const layer = createElement("div", "dialog-layer");
483
+ const dialog = createElement("div", "dialog");
484
+ dialog.setAttribute("role", "alertdialog");
485
+ const body = createElement("div", "dialog-body");
486
+ body.append(createElement("div", "dialog-title", options.title));
487
+ if (options.message) body.append(createElement("div", "dialog-message", options.message));
488
+ const actions = options.actions ?? DEFAULT_ACTIONS;
489
+ const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
490
+ for (const action of actions) {
491
+ const button = createElement("button", action.role ?? "default", action.label);
492
+ button.addEventListener("click", () => {
493
+ layer.remove();
494
+ resolve({ action: action.id });
495
+ });
496
+ buttons.append(button);
497
+ }
498
+ dialog.append(body, buttons);
499
+ layer.append(dialog);
500
+ signal.addEventListener("abort", () => {
501
+ layer.remove();
502
+ reject(new HostError("cancelled", "The alert was dismissed"));
503
+ });
504
+ phone.append(layer);
505
+ });
506
+ }
507
+ //#endregion
508
+ //#region src/client/dash/index.ts
509
+ function dashHandlers(phone, note) {
510
+ return {
511
+ "dash.ui.alert": ({ params, signal }) => alert(phone, params, signal),
512
+ "dash.system.vibrate": ({ params }) => vibrate(phone, note, params),
513
+ "dash.system.torch": ({ params }) => torch(phone, note, params.state)
514
+ };
515
+ }
516
+ //#endregion
517
+ //#region src/client/dash/permissions.ts
518
+ const PERMISSION_SUMMARIES = { account: "see your Dash account: your name, and your email or phone" };
519
+ async function askPermission(phone, note, request, signal) {
520
+ const { action } = await alert(phone, {
521
+ title: `Allow “${request.dotName}” to ${PERMISSION_SUMMARIES[request.permission]}?`,
522
+ message: request.reason,
523
+ actions: [{
524
+ id: "deny",
525
+ label: "Don't Allow",
526
+ role: "cancel"
527
+ }, {
528
+ id: "allow",
529
+ label: "Allow"
530
+ }]
531
+ }, signal);
532
+ const allowed = action === "allow";
533
+ if (allowed) note(`permission ${request.permission}: allowed`);
534
+ else note(`permission ${request.permission}: not allowed`);
535
+ return allowed;
536
+ }
537
+ //#endregion
538
+ //#region src/client/wire.ts
539
+ function isWire(data) {
540
+ return typeof data?.dot === "string";
541
+ }
542
+ function wireTransport(channel) {
543
+ return {
544
+ post: (message) => channel.post({ dot: JSON.stringify(message) }),
545
+ listen: (listener) => channel.listen((data) => {
546
+ if (isWire(data)) listener(JSON.parse(data.dot));
547
+ })
548
+ };
549
+ }
550
+ function windowChannel(other) {
551
+ return {
552
+ post: (message) => other()?.postMessage(message, location.origin),
553
+ listen(listener) {
554
+ const onMessage = (event) => {
555
+ if (event.source === other()) listener(event.data);
556
+ };
557
+ window.addEventListener("message", onMessage);
558
+ return () => window.removeEventListener("message", onMessage);
559
+ }
560
+ };
561
+ }
562
+ //#endregion
563
+ //#region src/client/simulator.ts
564
+ const MAX_LOG_ENTRIES = 500;
565
+ const screen = element("#screen");
566
+ const log = element("#log");
567
+ const errorBanner = element("#error");
568
+ const phone = element(".phone");
569
+ const schemeSelect = element("#scheme");
570
+ const browserDark = matchMedia("(prefers-color-scheme: dark)");
571
+ let runningApp = null;
572
+ let launchCount = 0;
573
+ async function launch() {
574
+ stopRunningApp();
575
+ launchCount += 1;
576
+ const thisLaunch = launchCount;
577
+ if (log.childElementCount > 0) note("dot restarted");
578
+ let manifest;
579
+ try {
580
+ manifest = await (await fetch(MANIFEST_URL)).json();
581
+ } catch {
582
+ showError(`Nothing to run: ${MANIFEST_URL} is missing. Did the build fail?`);
583
+ return;
584
+ }
585
+ if (thisLaunch !== launchCount) return;
586
+ try {
587
+ checkCompatibility(manifest, SIMULATOR_VERSIONS);
588
+ } catch (error) {
589
+ screen.src = "about:blank";
590
+ showError(error.message);
591
+ return;
592
+ }
593
+ const worker = new Worker(`${CLIENT_PATH}service-bridge.js`);
594
+ const host = startHost({
595
+ logic: wireTransport(createAdaptorTransport(worker)),
596
+ ui: wireTransport(windowChannel(() => screen.contentWindow)),
597
+ manifest,
598
+ ...SIMULATOR_VERSIONS,
599
+ systemContext: systemContext(),
600
+ handlers: dashHandlers(phone, note),
601
+ askPermission: (request, signal) => askPermission(phone, note, request, signal),
602
+ observe: record
603
+ });
604
+ worker.addEventListener("error", (event) => showError(`Service crashed: ${event.message}`));
605
+ screen.src = WEBVIEW_URL;
606
+ runningApp = {
607
+ host,
608
+ worker
609
+ };
610
+ }
611
+ function stopRunningApp() {
612
+ if (!runningApp) return;
613
+ runningApp.host.stop();
614
+ runningApp.worker.terminate();
615
+ runningApp = null;
616
+ }
617
+ function goBack() {
618
+ if (runningApp && !runningApp.host.back()) note("back on the first page: a device would close the dot");
619
+ }
620
+ const ARROWS = {
621
+ toHost: "both --> host",
622
+ fromHost: "host --> both",
623
+ toService: "ui --> logic",
624
+ toWebview: "logic <-- ui"
625
+ };
626
+ function note(text) {
627
+ log.append(createElement("li", "note", text));
628
+ }
629
+ function record(direction, message) {
630
+ const entry = createElement("li", direction);
631
+ const details = document.createElement("details");
632
+ const summary = document.createElement("summary");
633
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour12: false });
634
+ summary.append(createElement("span", "time", time), createElement("span", "arrow", ARROWS[direction]), createElement("span", "type", message.type));
635
+ const payload = document.createElement("pre");
636
+ payload.textContent = JSON.stringify(message.payload, null, 2);
637
+ details.append(summary, payload);
638
+ entry.append(details);
639
+ if (message.type === "error") entry.classList.add("failed");
640
+ log.append(entry);
641
+ while (log.childElementCount > MAX_LOG_ENTRIES) log.firstElementChild?.remove();
642
+ log.scrollTop = log.scrollHeight;
643
+ }
644
+ function syncLaunch() {
645
+ launch().catch((error) => {
646
+ showError(`Could not start the dot: ${String(error)}`);
647
+ });
648
+ }
649
+ function showError(message) {
650
+ errorBanner.hidden = message === null;
651
+ errorBanner.textContent = message ?? "";
652
+ }
653
+ function systemContext() {
654
+ return {
655
+ scheme: schemeSelect.value,
656
+ appState: appState()
657
+ };
658
+ }
659
+ function appState() {
660
+ if (document.visibilityState === "hidden") return "background";
661
+ return "active";
662
+ }
663
+ function browserScheme() {
664
+ return browserDark.matches ? "dark" : "light";
665
+ }
666
+ function changeScheme(scheme) {
667
+ schemeSelect.value = scheme;
668
+ phone.dataset.scheme = scheme;
669
+ runningApp?.host.setSystemContext(systemContext());
670
+ }
671
+ const events = new EventSource(EVENTS_PATH);
672
+ events.addEventListener("reload", () => {
673
+ showError(null);
674
+ syncLaunch();
675
+ });
676
+ events.addEventListener("build-error", (event) => showError(String(event.data)));
677
+ element("#reload").addEventListener("click", syncLaunch);
678
+ element("#back").addEventListener("click", goBack);
679
+ element("#clear").addEventListener("click", () => log.replaceChildren());
680
+ schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
681
+ browserDark.addEventListener("change", () => changeScheme(browserScheme()));
682
+ document.addEventListener("visibilitychange", () => {
683
+ runningApp?.host.setSystemContext(systemContext());
684
+ });
685
+ changeScheme(browserScheme());
686
+ syncLaunch();
687
+ //#endregion
688
+ })();
689
+
690
+ //# sourceMappingURL=simulator.js.map