@monimesl/dot-simulator 0.1.0-alpha.10

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,1386 @@
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 AmountInput = defineComponent({
19
+ name: "AmountInput",
20
+ props: {
21
+ value: "number",
22
+ currency: "string",
23
+ minorUnits: "number",
24
+ unit: ["minor", "major"],
25
+ label: "string",
26
+ placeholder: "string",
27
+ hint: "string",
28
+ error: "string",
29
+ disabled: "boolean"
30
+ },
31
+ events: ["change"],
32
+ children: "none"
33
+ });
34
+ const Button = defineComponent({
35
+ name: "Button",
36
+ props: {
37
+ variant: ["primary", "secondary"],
38
+ disabled: "boolean",
39
+ loading: "boolean"
40
+ },
41
+ events: ["click"],
42
+ children: "text"
43
+ });
44
+ const SIZES = [
45
+ "small",
46
+ "medium",
47
+ "large"
48
+ ];
49
+ const SPACES = [
50
+ "none",
51
+ "small",
52
+ "medium",
53
+ "large"
54
+ ];
55
+ const PADDING_PROPS = {
56
+ padding: SPACES,
57
+ paddingTop: SPACES,
58
+ paddingStart: SPACES,
59
+ paddingEnd: SPACES,
60
+ paddingBottom: SPACES
61
+ };
62
+ const ASPECTS = [
63
+ "1:1",
64
+ "4:3",
65
+ "3:2",
66
+ "16:9",
67
+ "3:4",
68
+ "2:3",
69
+ "9:16"
70
+ ];
71
+ const TONES = [
72
+ "default",
73
+ "muted",
74
+ "info",
75
+ "success",
76
+ "warning",
77
+ "danger"
78
+ ];
79
+ const STATUS_TONES = [
80
+ "default",
81
+ "info",
82
+ "success",
83
+ "warning",
84
+ "danger"
85
+ ];
86
+ const ICON_NAMES = [
87
+ "arrow-down",
88
+ "arrow-left",
89
+ "arrow-right",
90
+ "arrow-up",
91
+ "bank",
92
+ "bell",
93
+ "bolt",
94
+ "calendar",
95
+ "card",
96
+ "cart",
97
+ "cash",
98
+ "check",
99
+ "chevron-down",
100
+ "chevron-left",
101
+ "chevron-right",
102
+ "chevron-up",
103
+ "clock",
104
+ "close",
105
+ "copy",
106
+ "download",
107
+ "edit",
108
+ "error",
109
+ "external-link",
110
+ "eye",
111
+ "eye-off",
112
+ "file",
113
+ "gift",
114
+ "globe",
115
+ "grid",
116
+ "headphones",
117
+ "heart",
118
+ "help",
119
+ "history",
120
+ "home",
121
+ "image",
122
+ "info",
123
+ "link",
124
+ "list",
125
+ "location",
126
+ "lock",
127
+ "logout",
128
+ "mail",
129
+ "microphone",
130
+ "minus",
131
+ "mobile",
132
+ "more",
133
+ "music",
134
+ "phone",
135
+ "playlist",
136
+ "plus",
137
+ "qr-code",
138
+ "receipt",
139
+ "refresh",
140
+ "search",
141
+ "send",
142
+ "settings",
143
+ "share",
144
+ "star",
145
+ "success",
146
+ "ticket",
147
+ "trash",
148
+ "upload",
149
+ "user",
150
+ "users",
151
+ "video",
152
+ "wallet",
153
+ "warning",
154
+ "water",
155
+ "wifi"
156
+ ];
157
+ const Carousel = defineComponent({
158
+ name: "Carousel",
159
+ props: {
160
+ autoplay: "boolean",
161
+ interval: "number",
162
+ loop: "boolean",
163
+ indicator: ["dots", "none"],
164
+ gap: SPACES,
165
+ effect: ["slide", "scale"],
166
+ index: "number",
167
+ ...PADDING_PROPS
168
+ },
169
+ events: ["change"],
170
+ children: "elements"
171
+ });
172
+ const Chip = defineComponent({
173
+ name: "Chip",
174
+ props: {
175
+ label: "string",
176
+ icon: ICON_NAMES,
177
+ selected: "boolean",
178
+ disabled: "boolean"
179
+ },
180
+ events: ["tap"],
181
+ children: "none"
182
+ });
183
+ const CodeInput = defineComponent({
184
+ name: "CodeInput",
185
+ props: {
186
+ value: "string",
187
+ length: "number",
188
+ label: "string",
189
+ hint: "string",
190
+ error: "string",
191
+ secure: "boolean",
192
+ disabled: "boolean"
193
+ },
194
+ events: ["change", "complete"],
195
+ children: "none"
196
+ });
197
+ const DetailRow = defineComponent({
198
+ name: "DetailRow",
199
+ props: {
200
+ label: "string",
201
+ value: "string",
202
+ tone: TONES,
203
+ weight: [
204
+ "regular",
205
+ "medium",
206
+ "bold"
207
+ ],
208
+ ...PADDING_PROPS
209
+ },
210
+ children: "none"
211
+ });
212
+ const Divider = defineComponent({
213
+ name: "Divider",
214
+ children: "none"
215
+ });
216
+ const EmptyState = defineComponent({
217
+ name: "EmptyState",
218
+ props: {
219
+ icon: ICON_NAMES,
220
+ title: "string",
221
+ text: "string",
222
+ ...PADDING_PROPS
223
+ },
224
+ children: "elements"
225
+ });
226
+ const Spinner = defineComponent({
227
+ name: "Spinner",
228
+ props: { size: SIZES },
229
+ children: "none"
230
+ });
231
+ const ProgressBar = defineComponent({
232
+ name: "ProgressBar",
233
+ props: { value: "number" },
234
+ children: "none"
235
+ });
236
+ const Notice = defineComponent({
237
+ name: "Notice",
238
+ props: {
239
+ tone: STATUS_TONES,
240
+ title: "string",
241
+ ...PADDING_PROPS
242
+ },
243
+ children: "text"
244
+ });
245
+ const Badge = defineComponent({
246
+ name: "Badge",
247
+ props: { tone: STATUS_TONES },
248
+ children: "text"
249
+ });
250
+ const Footer = defineComponent({
251
+ name: "Footer",
252
+ props: PADDING_PROPS,
253
+ children: "elements"
254
+ });
255
+ const Grid = defineComponent({
256
+ name: "Grid",
257
+ props: {
258
+ columns: "number",
259
+ gap: SPACES,
260
+ ...PADDING_PROPS
261
+ },
262
+ children: "elements"
263
+ });
264
+ const GridItem = defineComponent({
265
+ name: "GridItem",
266
+ props: {
267
+ label: "string",
268
+ icon: ICON_NAMES,
269
+ image: "string",
270
+ badge: "string"
271
+ },
272
+ events: ["tap"],
273
+ children: "none"
274
+ });
275
+ const Icon = defineComponent({
276
+ name: "Icon",
277
+ props: {
278
+ name: ICON_NAMES,
279
+ size: SIZES,
280
+ tone: TONES
281
+ },
282
+ children: "none",
283
+ inline: true
284
+ });
285
+ const Image = defineComponent({
286
+ name: "Image",
287
+ props: {
288
+ src: "string",
289
+ alt: "string",
290
+ width: "number",
291
+ height: "number",
292
+ fit: ["cover", "contain"],
293
+ shape: [
294
+ "rounded",
295
+ "square",
296
+ "circle"
297
+ ],
298
+ aspect: ASPECTS
299
+ },
300
+ events: ["load", "error"],
301
+ children: "none",
302
+ overrides: [{
303
+ prop: "aspect",
304
+ ignores: "height",
305
+ because: "the aspect sets the height"
306
+ }]
307
+ });
308
+ const Input = defineComponent({
309
+ name: "Input",
310
+ props: {
311
+ value: "string",
312
+ placeholder: "string",
313
+ label: "string",
314
+ hint: "string",
315
+ error: "string",
316
+ prefix: "string",
317
+ suffix: "string",
318
+ type: [
319
+ "text",
320
+ "number",
321
+ "email",
322
+ "phone",
323
+ "password"
324
+ ],
325
+ maxLength: "number",
326
+ disabled: "boolean"
327
+ },
328
+ events: ["change", "submit"],
329
+ children: "none"
330
+ });
331
+ const Link = defineComponent({
332
+ name: "Link",
333
+ props: {
334
+ href: "string",
335
+ tone: TONES
336
+ },
337
+ events: ["tap"],
338
+ children: "text",
339
+ inline: true
340
+ });
341
+ const List = defineComponent({
342
+ name: "List",
343
+ props: {
344
+ size: ["sm", "md"],
345
+ ...PADDING_PROPS
346
+ },
347
+ events: ["endReached"],
348
+ children: "elements"
349
+ });
350
+ const ListItem = defineComponent({
351
+ name: "ListItem",
352
+ props: {
353
+ title: "string",
354
+ subtitle: "string",
355
+ value: "string",
356
+ icon: ICON_NAMES,
357
+ image: "string",
358
+ ...PADDING_PROPS
359
+ },
360
+ events: ["tap"],
361
+ children: "elements"
362
+ });
363
+ const Sheet = defineComponent({
364
+ name: "Sheet",
365
+ props: {
366
+ open: "boolean",
367
+ title: "string",
368
+ showCloseIcon: "boolean",
369
+ ...PADDING_PROPS
370
+ },
371
+ events: ["close"],
372
+ children: "elements"
373
+ });
374
+ const Toast = defineComponent({
375
+ name: "Toast",
376
+ props: {
377
+ message: "string",
378
+ tone: STATUS_TONES,
379
+ duration: "number"
380
+ },
381
+ events: ["hide"],
382
+ children: "none"
383
+ });
384
+ const QRCode = defineComponent({
385
+ name: "QRCode",
386
+ props: {
387
+ value: "string",
388
+ size: "number",
389
+ label: "string"
390
+ },
391
+ children: "none"
392
+ });
393
+ const Select = defineComponent({
394
+ name: "Select",
395
+ props: {
396
+ value: "string",
397
+ label: "string",
398
+ placeholder: "string",
399
+ hint: "string",
400
+ error: "string",
401
+ disabled: "boolean"
402
+ },
403
+ events: ["change"],
404
+ children: "elements"
405
+ });
406
+ const Option = defineComponent({
407
+ name: "Option",
408
+ props: {
409
+ value: "string",
410
+ label: "string"
411
+ },
412
+ children: "none"
413
+ });
414
+ const Scroll = defineComponent({
415
+ name: "Scroll",
416
+ props: {
417
+ gap: SPACES,
418
+ snap: [
419
+ "none",
420
+ "start",
421
+ "center"
422
+ ],
423
+ index: "number",
424
+ ...PADDING_PROPS
425
+ },
426
+ events: ["change"],
427
+ children: "elements"
428
+ });
429
+ const Skeleton = defineComponent({
430
+ name: "Skeleton",
431
+ props: {
432
+ shape: [
433
+ "text",
434
+ "circle",
435
+ "box"
436
+ ],
437
+ lines: "number",
438
+ height: "number"
439
+ },
440
+ children: "none"
441
+ });
442
+ const Tabs = defineComponent({
443
+ name: "Tabs",
444
+ props: {
445
+ value: "string",
446
+ variant: ["underline", "segmented"],
447
+ ...PADDING_PROPS
448
+ },
449
+ events: ["change"],
450
+ children: "elements"
451
+ });
452
+ const Tab = defineComponent({
453
+ name: "Tab",
454
+ props: {
455
+ value: "string",
456
+ label: "string",
457
+ badge: "number"
458
+ },
459
+ children: "none"
460
+ });
461
+ const Text = defineComponent({
462
+ name: "Text",
463
+ props: {
464
+ variant: [
465
+ "title",
466
+ "body",
467
+ "caption"
468
+ ],
469
+ tone: TONES,
470
+ weight: [
471
+ "regular",
472
+ "medium",
473
+ "bold"
474
+ ],
475
+ align: [
476
+ "start",
477
+ "center",
478
+ "end"
479
+ ],
480
+ maxLines: "number",
481
+ ...PADDING_PROPS
482
+ },
483
+ children: "text",
484
+ inline: true
485
+ });
486
+ const TextArea = defineComponent({
487
+ name: "TextArea",
488
+ props: {
489
+ value: "string",
490
+ placeholder: "string",
491
+ label: "string",
492
+ hint: "string",
493
+ error: "string",
494
+ rows: "number",
495
+ maxLength: "number",
496
+ disabled: "boolean"
497
+ },
498
+ events: ["change"],
499
+ children: "none"
500
+ });
501
+ const Switch = defineComponent({
502
+ name: "Switch",
503
+ props: {
504
+ checked: "boolean",
505
+ label: "string",
506
+ disabled: "boolean"
507
+ },
508
+ events: ["change"],
509
+ children: "none"
510
+ });
511
+ const Checkbox = defineComponent({
512
+ name: "Checkbox",
513
+ props: {
514
+ checked: "boolean",
515
+ label: "string",
516
+ disabled: "boolean"
517
+ },
518
+ events: ["change"],
519
+ children: "none"
520
+ });
521
+ const RadioGroup = defineComponent({
522
+ name: "RadioGroup",
523
+ props: {
524
+ value: "string",
525
+ label: "string",
526
+ disabled: "boolean"
527
+ },
528
+ events: ["change"],
529
+ children: "elements"
530
+ });
531
+ const builtinComponents = {
532
+ AmountInput,
533
+ Badge,
534
+ Button,
535
+ Carousel,
536
+ Checkbox,
537
+ Chip,
538
+ CodeInput,
539
+ DetailRow,
540
+ Divider,
541
+ EmptyState,
542
+ Footer,
543
+ Grid,
544
+ GridItem,
545
+ Icon,
546
+ Image,
547
+ Input,
548
+ Link,
549
+ List,
550
+ ListItem,
551
+ Notice,
552
+ Option,
553
+ ProgressBar,
554
+ QRCode,
555
+ Radio: defineComponent({
556
+ name: "Radio",
557
+ props: {
558
+ value: "string",
559
+ label: "string",
560
+ disabled: "boolean"
561
+ },
562
+ children: "none"
563
+ }),
564
+ RadioGroup,
565
+ Scroll,
566
+ Select,
567
+ Sheet,
568
+ Skeleton,
569
+ Spinner,
570
+ Switch,
571
+ Tab,
572
+ Tabs,
573
+ Text,
574
+ TextArea,
575
+ Toast,
576
+ View: defineComponent({
577
+ name: "View",
578
+ props: {
579
+ gap: SPACES,
580
+ direction: ["column", "row"],
581
+ align: [
582
+ "stretch",
583
+ "start",
584
+ "center",
585
+ "end"
586
+ ],
587
+ justify: [
588
+ "start",
589
+ "center",
590
+ "end",
591
+ "between"
592
+ ],
593
+ wrap: "boolean",
594
+ grow: "boolean",
595
+ ...PADDING_PROPS,
596
+ variant: ["plain", "card"]
597
+ },
598
+ events: ["tap"],
599
+ children: "elements"
600
+ })
601
+ };
602
+ componentRegistry(Object.values(builtinComponents));
603
+ var HostError = class extends Error {
604
+ name = "HostError";
605
+ code;
606
+ constructor(code, message) {
607
+ super(message);
608
+ this.code = code;
609
+ }
610
+ };
611
+ const LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
612
+ new RegExp(`^(?:\\*\\.${LABEL}(?:\\.${LABEL})+|${LABEL}(?:\\.${LABEL})*)$`);
613
+ 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-]+)*))?$/;
614
+ function majorVersion(version) {
615
+ return parseVersion(version).major;
616
+ }
617
+ function isVersionAtLeast(version, minimum) {
618
+ return compareVersions(version, minimum) >= 0;
619
+ }
620
+ function compareVersions(left, right) {
621
+ const leftVersion = parseVersion(left);
622
+ const rightVersion = parseVersion(right);
623
+ const differences = [
624
+ leftVersion.major - rightVersion.major,
625
+ leftVersion.minor - rightVersion.minor,
626
+ leftVersion.patch - rightVersion.patch
627
+ ];
628
+ for (const difference of differences) if (difference !== 0) return difference;
629
+ return comparePrereleases(leftVersion.prerelease, rightVersion.prerelease);
630
+ }
631
+ function parseVersion(version) {
632
+ const match = SEMVER_PATTERN.exec(version);
633
+ if (!match) throw new Error(`"${version}" is not a semantic version`);
634
+ const [, major, minor, patch, prerelease] = match;
635
+ return {
636
+ major: Number(major),
637
+ minor: Number(minor),
638
+ patch: Number(patch),
639
+ prerelease: prerelease === void 0 ? [] : prerelease.split(".")
640
+ };
641
+ }
642
+ function comparePrereleases(left, right) {
643
+ if (left.length === 0 || right.length === 0) return right.length - left.length;
644
+ const shared = Math.min(left.length, right.length);
645
+ for (let index = 0; index < shared; index++) {
646
+ const order = compareIdentifiers(left[index], right[index]);
647
+ if (order !== 0) return order;
648
+ }
649
+ return left.length - right.length;
650
+ }
651
+ function compareIdentifiers(left, right) {
652
+ const leftIsNumber = /^\d+$/.test(left);
653
+ const rightIsNumber = /^\d+$/.test(right);
654
+ if (leftIsNumber && rightIsNumber) return Number(left) - Number(right);
655
+ if (leftIsNumber) return -1;
656
+ if (rightIsNumber) return 1;
657
+ if (left < right) return -1;
658
+ if (left > right) return 1;
659
+ return 0;
660
+ }
661
+ function isReactCompatible(hostVersion, dotVersion) {
662
+ return majorVersion(hostVersion) === majorVersion(dotVersion) && isVersionAtLeast(hostVersion, dotVersion);
663
+ }
664
+ function createAdaptorTransport(adapter) {
665
+ return {
666
+ post: (message) => adapter.postMessage(message),
667
+ listen(listener) {
668
+ const onMessage = (event) => listener(event.data);
669
+ adapter.addEventListener("message", onMessage);
670
+ return () => adapter.removeEventListener("message", onMessage);
671
+ }
672
+ };
673
+ }
674
+ var NavigationStack = class {
675
+ limit;
676
+ #pages = [];
677
+ #nextInstanceId = 1;
678
+ constructor(limit = 8) {
679
+ this.limit = limit;
680
+ }
681
+ get pages() {
682
+ return this.#pages;
683
+ }
684
+ get top() {
685
+ return this.#pages.at(-1);
686
+ }
687
+ apply(navigation) {
688
+ switch (navigation.action) {
689
+ case "push":
690
+ if (this.#pages.length >= this.limit) return false;
691
+ break;
692
+ case "replace":
693
+ this.#pages.pop();
694
+ break;
695
+ case "reset":
696
+ this.#pages = [];
697
+ break;
698
+ case "back": {
699
+ const count = Math.min(navigation.delta, this.#pages.length - 1);
700
+ if (count < 1) return false;
701
+ this.#pages = this.#pages.slice(0, -count);
702
+ return true;
703
+ }
704
+ }
705
+ const { page, options } = navigation;
706
+ this.#pages = [...this.#pages, {
707
+ instanceId: this.#nextInstanceId++,
708
+ page,
709
+ options
710
+ }];
711
+ return true;
712
+ }
713
+ };
714
+ //#endregion
715
+ //#region src/compatibility.ts
716
+ var IncompatibleDotError = class extends Error {
717
+ name = "IncompatibleDotError";
718
+ reason;
719
+ constructor(reason, message) {
720
+ super(message);
721
+ this.reason = reason;
722
+ }
723
+ };
724
+ function checkCompatibility(manifest, { dashVersion, minRuntimeVersion, reactVersions }) {
725
+ if (!isVersionAtLeast(manifest.runtime, minRuntimeVersion)) throw new IncompatibleDotError("runtimeTooOld", `This dot was built with an old Dot runtime, ${manifest.runtime}. Rebuild it with @monimesl/dot-runtime ${minRuntimeVersion} or newer.`);
726
+ const minDashVersion = manifest.dash?.minVersion;
727
+ if (minDashVersion !== void 0 && !isVersionAtLeast(dashVersion, minDashVersion)) throw new IncompatibleDotError("dashTooOld", `This dot needs Dash ${minDashVersion} or newer. This is Dash ${dashVersion}.`);
728
+ if (reactVersions !== void 0 && !reactVersions.some((version) => isReactCompatible(version, manifest.react))) throw new IncompatibleDotError("reactUnavailable", `This dot needs React ${manifest.react}, which this host doesn't have.`);
729
+ }
730
+ //#endregion
731
+ //#region src/host-calls.ts
732
+ const UI_METHOD_PREFIX = "dash.ui.";
733
+ var HostCallRunner = class {
734
+ #handlers;
735
+ #permissions;
736
+ #answer;
737
+ #running = /* @__PURE__ */ new Map();
738
+ constructor(handlers, permissions, answer) {
739
+ this.#handlers = handlers;
740
+ this.#permissions = permissions;
741
+ this.#answer = answer;
742
+ }
743
+ run({ id, method, params, timeout }) {
744
+ const entry = this.#handlers[method];
745
+ if (!entry) {
746
+ this.#answer({
747
+ id,
748
+ error: {
749
+ code: "unsupported",
750
+ message: `This host doesn't support "${method}"`
751
+ }
752
+ });
753
+ return;
754
+ }
755
+ const call = {
756
+ method,
757
+ controller: new AbortController()
758
+ };
759
+ if (timeout !== void 0) call.timer = setTimeout(() => {
760
+ this.#fail(id, new HostError("timeout", `"${method}" took longer than ${timeout}ms`));
761
+ }, timeout);
762
+ this.#running.set(id, call);
763
+ this.#invoke(id, entry, {
764
+ method,
765
+ params,
766
+ signal: call.controller.signal
767
+ });
768
+ }
769
+ cancel(id) {
770
+ this.#end(id)?.controller.abort();
771
+ }
772
+ dismissUI() {
773
+ for (const [id, call] of this.#running) if (call.method.startsWith(UI_METHOD_PREFIX)) this.#fail(id, new HostError("cancelled", "The page that showed this dialog is no longer on screen"));
774
+ }
775
+ async #invoke(id, entry, call) {
776
+ try {
777
+ if (typeof entry === "function") {
778
+ this.#succeed(id, await entry(call));
779
+ return;
780
+ }
781
+ await this.#permissions.check(call.method, entry.permission, call.signal);
782
+ this.#succeed(id, await entry.handle(call));
783
+ } catch (error) {
784
+ this.#fail(id, error);
785
+ }
786
+ }
787
+ #succeed(id, value) {
788
+ if (this.#end(id)) this.#answer({
789
+ id,
790
+ value
791
+ });
792
+ }
793
+ #fail(id, error) {
794
+ const call = this.#end(id);
795
+ if (!call) return;
796
+ call.controller.abort();
797
+ this.#answer({
798
+ id,
799
+ error: describeError(error)
800
+ });
801
+ }
802
+ #end(id) {
803
+ const call = this.#running.get(id);
804
+ if (call) {
805
+ this.#running.delete(id);
806
+ clearTimeout(call.timer);
807
+ }
808
+ return call;
809
+ }
810
+ };
811
+ function describeError(error) {
812
+ if (error instanceof HostError) return {
813
+ code: error.code,
814
+ message: error.message
815
+ };
816
+ return {
817
+ code: "failed",
818
+ message: error instanceof Error ? error.message : String(error)
819
+ };
820
+ }
821
+ //#endregion
822
+ //#region src/permissions.ts
823
+ var PermissionGate = class {
824
+ #dotName;
825
+ #declared;
826
+ #ask;
827
+ #answers = /* @__PURE__ */ new Map();
828
+ constructor(dotName, declared, ask) {
829
+ this.#dotName = dotName;
830
+ this.#declared = declared;
831
+ this.#ask = ask;
832
+ }
833
+ async check(method, permission, signal) {
834
+ const reason = this.#declared[permission];
835
+ if (reason === void 0) throw new HostError("denied", `"${method}" needs the "${permission}" permission. Add it to permissions in dot.json, with a reason to show the person.`);
836
+ let allowed = this.#answers.get(permission);
837
+ if (allowed === void 0) {
838
+ allowed = await this.#ask({
839
+ dotName: this.#dotName,
840
+ permission,
841
+ reason
842
+ }, signal);
843
+ this.#answers.set(permission, allowed);
844
+ }
845
+ if (!allowed) throw new HostError("denied", `The person didn't allow the "${permission}" permission`);
846
+ }
847
+ };
848
+ //#endregion
849
+ //#region src/host.ts
850
+ function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe, restorePages = [], onBarsChange }) {
851
+ checkCompatibility(manifest, {
852
+ dashVersion,
853
+ minRuntimeVersion
854
+ });
855
+ const stack = new NavigationStack();
856
+ let context = systemContext;
857
+ const calls = new HostCallRunner(handlers, new PermissionGate(manifest.name, manifest.permissions ?? {}, askPermission), (result) => {
858
+ const message = {
859
+ type: "hostResult",
860
+ payload: result
861
+ };
862
+ observe?.("fromHost", message);
863
+ logic.post(message);
864
+ });
865
+ const broadcast = (message) => {
866
+ observe?.("fromHost", message);
867
+ ui.post(message);
868
+ logic.post(message);
869
+ };
870
+ const barsByInstance = /* @__PURE__ */ new Map();
871
+ const showBars = () => {
872
+ const top = stack.top;
873
+ const bars = top && barsByInstance.get(top.instanceId);
874
+ onBarsChange?.({
875
+ title: bars?.navigationBar.title ?? manifest.name,
876
+ navigationBarHidden: bars?.navigationBar.hidden ?? false,
877
+ navigationBarColor: bars?.navigationBar.backgroundColor,
878
+ navigationBarTextColor: bars?.navigationBar.textColor,
879
+ tabBarHidden: bars?.tabBar.hidden ?? false
880
+ }, stack.pages);
881
+ };
882
+ const syncStack = () => {
883
+ const message = {
884
+ type: "syncStack",
885
+ payload: {
886
+ pages: stack.pages,
887
+ limit: stack.limit
888
+ }
889
+ };
890
+ broadcast(message);
891
+ const open = new Set(stack.pages.map((entry) => entry.instanceId));
892
+ for (const instanceId of barsByInstance.keys()) if (!open.has(instanceId)) barsByInstance.delete(instanceId);
893
+ showBars();
894
+ };
895
+ const syncSystemContext = () => {
896
+ broadcast({
897
+ type: "syncSystemContext",
898
+ payload: context
899
+ });
900
+ };
901
+ const navigate = (navigation) => {
902
+ if (navigation.action !== "back" && !manifest.pages.includes(navigation.page)) return false;
903
+ const previousTop = stack.top;
904
+ if (!stack.apply(navigation)) return false;
905
+ syncStack();
906
+ if (stack.top !== previousTop) calls.dismissUI();
907
+ return true;
908
+ };
909
+ const openFirstPages = () => {
910
+ const [first, ...rest] = restorePages.filter((entry) => manifest.pages.includes(entry.page));
911
+ if (!first) {
912
+ navigate({
913
+ action: "reset",
914
+ page: manifest.entry,
915
+ options: {}
916
+ });
917
+ return;
918
+ }
919
+ stack.apply({
920
+ action: "reset",
921
+ page: first.page,
922
+ options: first.options
923
+ });
924
+ for (const entry of rest) stack.apply({
925
+ action: "push",
926
+ page: entry.page,
927
+ options: entry.options
928
+ });
929
+ syncStack();
930
+ };
931
+ const navigateFromPage = ({ from, ...navigation }) => {
932
+ if (from === stack.top?.instanceId) navigate(navigation);
933
+ };
934
+ const hostMessageHandlers = /* @__PURE__ */ new Map([
935
+ ["navigate", (payload) => navigateFromPage(payload)],
936
+ ["hostCall", (payload) => calls.run(payload)],
937
+ ["hostCancel", (payload) => calls.cancel(payload.id)],
938
+ ["pageBars", (payload) => {
939
+ const { instanceId, ...bars } = payload;
940
+ barsByInstance.set(instanceId, bars);
941
+ showBars();
942
+ }]
943
+ ]);
944
+ const stopService = logic.listen((raw) => {
945
+ const message = raw;
946
+ const handleHostMessage = hostMessageHandlers.get(message.type);
947
+ if (handleHostMessage) {
948
+ observe?.("toHost", message);
949
+ handleHostMessage(message.payload);
950
+ return;
951
+ }
952
+ observe?.("toWebview", message);
953
+ ui.post(message);
954
+ });
955
+ const stopWebview = ui.listen((raw) => {
956
+ const message = raw;
957
+ observe?.("toService", message);
958
+ logic.post(message);
959
+ if (message.type === "connect") {
960
+ syncSystemContext();
961
+ openFirstPages();
962
+ }
963
+ });
964
+ return {
965
+ get pages() {
966
+ return stack.pages;
967
+ },
968
+ back: () => navigate({
969
+ action: "back",
970
+ delta: 1
971
+ }),
972
+ setSystemContext: (next) => {
973
+ context = next;
974
+ syncSystemContext();
975
+ },
976
+ stop: () => {
977
+ stopService();
978
+ stopWebview();
979
+ }
980
+ };
981
+ }
982
+ const SIMULATOR_VERSIONS = {
983
+ dashVersion: "1.0.0",
984
+ minRuntimeVersion: "0.1.0-alpha.1"
985
+ };
986
+ //#endregion
987
+ //#region src/paths.ts
988
+ const CLIENT_PATH = "/__dot/";
989
+ const EVENTS_PATH = `${CLIENT_PATH}events`;
990
+ const HTTP_PATH = `${CLIENT_PATH}network/http/`;
991
+ const FILE_PATH = `${CLIENT_PATH}storage/file/`;
992
+ const CACHE_PATH = `${CLIENT_PATH}storage/cache/`;
993
+ const SECRET_PATH = `${CLIENT_PATH}storage/secret/`;
994
+ const DOT_PATH = "/dot/";
995
+ const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;
996
+ const MANIFEST_URL = `${DOT_PATH}manifest.json`;
997
+ //#endregion
998
+ //#region src/client/dash/dev-server.ts
999
+ async function callDevServer(path, params, signal) {
1000
+ const answer = await (await fetch(path, {
1001
+ method: "POST",
1002
+ headers: { "Content-Type": "application/json" },
1003
+ body: JSON.stringify(params),
1004
+ signal
1005
+ })).json();
1006
+ if ("error" in answer) throw new HostError(answer.error.code, answer.error.message);
1007
+ return answer.result;
1008
+ }
1009
+ //#endregion
1010
+ //#region src/client/dash/system.ts
1011
+ const SHAKE_DURATION_MS = 300;
1012
+ const SHAKE = [
1013
+ { transform: "translateX(0)" },
1014
+ { transform: "translateX(-5px)" },
1015
+ { transform: "translateX(5px)" },
1016
+ { transform: "translateX(-3px)" },
1017
+ { transform: "translateX(3px)" },
1018
+ { transform: "translateX(0)" }
1019
+ ];
1020
+ async function vibrate(phone, note, options) {
1021
+ note(`vibrated: ${options.style}`);
1022
+ await phone.animate(SHAKE, { duration: SHAKE_DURATION_MS }).finished;
1023
+ }
1024
+ function torch(phone, note, state) {
1025
+ phone.dataset.torch = state;
1026
+ note(`torch: ${state}`);
1027
+ }
1028
+ async function copyText(note, text) {
1029
+ try {
1030
+ await navigator.clipboard.writeText(text);
1031
+ } catch (error) {
1032
+ throw new HostError("failed", `The browser blocked the clipboard: ${error.message}`);
1033
+ }
1034
+ note(`copied: ${text}`);
1035
+ }
1036
+ function share(note, params) {
1037
+ const parts = [
1038
+ params.text,
1039
+ params.url,
1040
+ params.file && `file ${params.file}`
1041
+ ].filter(Boolean);
1042
+ if (parts.length === 0) throw new HostError("invalid", "share needs a text, a url or a file");
1043
+ note(`shared: ${parts.join(" · ")}`);
1044
+ }
1045
+ function openUrl(note, url) {
1046
+ if (!isHttps(url)) throw new HostError("invalid", `Dash only opens https addresses, not ${url}`);
1047
+ window.open(url, "_blank", "noopener");
1048
+ note(`opened: ${url}`);
1049
+ }
1050
+ function isHttps(url) {
1051
+ try {
1052
+ return new URL(url).protocol === "https:";
1053
+ } catch {
1054
+ return false;
1055
+ }
1056
+ }
1057
+ function delay(milliseconds, signal) {
1058
+ if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new HostError("invalid", `A delay is a number of milliseconds, 0 or more, not ${milliseconds}`);
1059
+ return new Promise((resolve) => {
1060
+ const timer = setTimeout(resolve, milliseconds);
1061
+ signal.addEventListener("abort", () => clearTimeout(timer));
1062
+ });
1063
+ }
1064
+ //#endregion
1065
+ //#region src/client/dom.ts
1066
+ function element(selector) {
1067
+ const node = document.querySelector(selector);
1068
+ if (!node) throw new Error(`The simulator page is missing ${selector}`);
1069
+ return node;
1070
+ }
1071
+ function createElement(tag, className, text) {
1072
+ const node = document.createElement(tag);
1073
+ node.className = className;
1074
+ if (text !== void 0) node.textContent = text;
1075
+ return node;
1076
+ }
1077
+ //#endregion
1078
+ //#region src/client/dash/ui.ts
1079
+ const DEFAULT_ACTIONS = [{
1080
+ id: "ok",
1081
+ label: "OK"
1082
+ }];
1083
+ function alert(phone, options, signal) {
1084
+ return new Promise((resolve, reject) => {
1085
+ const layer = createElement("div", "dialog-layer");
1086
+ const dialog = createElement("div", "dialog");
1087
+ dialog.setAttribute("role", "alertdialog");
1088
+ const body = createElement("div", "dialog-body");
1089
+ body.append(createElement("div", "dialog-title", options.title));
1090
+ if (options.message) body.append(createElement("div", "dialog-message", options.message));
1091
+ const actions = options.actions ?? DEFAULT_ACTIONS;
1092
+ const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
1093
+ for (const action of actions) {
1094
+ const button = createElement("button", action.role ?? "default", action.label);
1095
+ button.addEventListener("click", () => {
1096
+ layer.remove();
1097
+ resolve({ action: action.id });
1098
+ });
1099
+ buttons.append(button);
1100
+ }
1101
+ dialog.append(body, buttons);
1102
+ layer.append(dialog);
1103
+ signal.addEventListener("abort", () => {
1104
+ layer.remove();
1105
+ reject(new HostError("cancelled", "The alert was dismissed"));
1106
+ });
1107
+ phone.append(layer);
1108
+ });
1109
+ }
1110
+ //#endregion
1111
+ //#region src/client/dash/index.ts
1112
+ const HTTP_METHODS = [
1113
+ "request",
1114
+ "download",
1115
+ "upload"
1116
+ ];
1117
+ const FILE_OPERATIONS = [
1118
+ "read",
1119
+ "write",
1120
+ "stat",
1121
+ "list",
1122
+ "createFolder",
1123
+ "move",
1124
+ "copy",
1125
+ "remove",
1126
+ "save",
1127
+ "open",
1128
+ "usage"
1129
+ ];
1130
+ const CACHE_OPERATIONS = [
1131
+ "get",
1132
+ "set",
1133
+ "remove",
1134
+ "exists",
1135
+ "keys",
1136
+ "clear"
1137
+ ];
1138
+ const SECRET_OPERATIONS = [
1139
+ "get",
1140
+ "set",
1141
+ "remove",
1142
+ "clear"
1143
+ ];
1144
+ function dashHandlers(phone, note) {
1145
+ return {
1146
+ "dash.ui.alert": ({ params, signal }) => alert(phone, params, signal),
1147
+ "dash.system.vibrate": ({ params }) => vibrate(phone, note, params),
1148
+ "dash.system.torch": ({ params }) => torch(phone, note, params.state),
1149
+ "dash.system.copyText": ({ params }) => copyText(note, params.text),
1150
+ "dash.system.share": ({ params }) => share(note, params),
1151
+ "dash.system.openUrl": ({ params }) => openUrl(note, params.url),
1152
+ "dash.system.delay": ({ params, signal }) => delay(params.milliseconds, signal),
1153
+ ...devServerHandlers("dash.network.http", HTTP_PATH, HTTP_METHODS),
1154
+ ...devServerHandlers("dash.storage.file", FILE_PATH, FILE_OPERATIONS),
1155
+ ...devServerHandlers("dash.storage.cache", CACHE_PATH, CACHE_OPERATIONS),
1156
+ ...devServerHandlers("dash.storage.secret", SECRET_PATH, SECRET_OPERATIONS)
1157
+ };
1158
+ }
1159
+ function devServerHandlers(group, path, names) {
1160
+ return Object.fromEntries(names.map((name) => [`${group}.${name}`, ({ params, signal }) => callDevServer(`${path}${name}`, params, signal)]));
1161
+ }
1162
+ //#endregion
1163
+ //#region src/client/dash/permissions.ts
1164
+ const PERMISSION_SUMMARIES = { account: "see your Dash account: your name, and your email or phone" };
1165
+ async function askPermission(phone, note, request, signal) {
1166
+ const { action } = await alert(phone, {
1167
+ title: `Allow “${request.dotName}” to ${PERMISSION_SUMMARIES[request.permission]}?`,
1168
+ message: request.reason,
1169
+ actions: [{
1170
+ id: "deny",
1171
+ label: "Don't Allow",
1172
+ role: "cancel"
1173
+ }, {
1174
+ id: "allow",
1175
+ label: "Allow"
1176
+ }]
1177
+ }, signal);
1178
+ const allowed = action === "allow";
1179
+ if (allowed) note(`permission ${request.permission}: allowed`);
1180
+ else note(`permission ${request.permission}: not allowed`);
1181
+ return allowed;
1182
+ }
1183
+ //#endregion
1184
+ //#region src/client/wire.ts
1185
+ function isWire(data) {
1186
+ return typeof data?.dot === "string";
1187
+ }
1188
+ function wireTransport(channel) {
1189
+ return {
1190
+ post: (message) => channel.post({ dot: JSON.stringify(message) }),
1191
+ listen: (listener) => channel.listen((data) => {
1192
+ if (isWire(data)) listener(JSON.parse(data.dot));
1193
+ })
1194
+ };
1195
+ }
1196
+ function windowChannel(other) {
1197
+ return {
1198
+ post: (message) => other()?.postMessage(message, location.origin),
1199
+ listen(listener) {
1200
+ const onMessage = (event) => {
1201
+ if (event.source === other()) listener(event.data);
1202
+ };
1203
+ window.addEventListener("message", onMessage);
1204
+ return () => window.removeEventListener("message", onMessage);
1205
+ }
1206
+ };
1207
+ }
1208
+ //#endregion
1209
+ //#region src/client/simulator.ts
1210
+ const MAX_LOG_ENTRIES = 500;
1211
+ const screen = element("#screen");
1212
+ const log = element("#log");
1213
+ const errorBanner = element("#error");
1214
+ const phone = element(".phone");
1215
+ const schemeSelect = element("#scheme");
1216
+ const navigationBar = element(".navigation-bar");
1217
+ const navigationTitle = element("#navigation-title");
1218
+ const navigationBack = element("#navigation-back");
1219
+ const tabBar = element(".tab-bar");
1220
+ const clock = element(".clock");
1221
+ const browserDark = matchMedia("(prefers-color-scheme: dark)");
1222
+ const timeFormat = new Intl.DateTimeFormat("en-GB", {
1223
+ hour: "2-digit",
1224
+ minute: "2-digit",
1225
+ hourCycle: "h23"
1226
+ });
1227
+ function showTime() {
1228
+ clock.textContent = timeFormat.format(/* @__PURE__ */ new Date());
1229
+ }
1230
+ showTime();
1231
+ setInterval(showTime, 1e3);
1232
+ let runningDot = null;
1233
+ let launchCount = 0;
1234
+ let hotReloads = 0;
1235
+ async function launch(restorePages = []) {
1236
+ stopRunningDot();
1237
+ launchCount += 1;
1238
+ const thisLaunch = launchCount;
1239
+ if (log.childElementCount > 0) note("dot restarted");
1240
+ let manifest;
1241
+ try {
1242
+ manifest = await (await fetch(MANIFEST_URL)).json();
1243
+ } catch {
1244
+ showError(`There's nothing to run, because ${MANIFEST_URL} is missing. Check whether the build failed.`);
1245
+ return;
1246
+ }
1247
+ if (thisLaunch !== launchCount) return;
1248
+ try {
1249
+ checkCompatibility(manifest, SIMULATOR_VERSIONS);
1250
+ } catch (error) {
1251
+ screen.src = "about:blank";
1252
+ showError(error.message);
1253
+ return;
1254
+ }
1255
+ const worker = new Worker(`${CLIENT_PATH}service-bridge.js`);
1256
+ const host = startHost({
1257
+ logic: wireTransport(createAdaptorTransport(worker)),
1258
+ ui: wireTransport(windowChannel(() => screen.contentWindow)),
1259
+ manifest,
1260
+ ...SIMULATOR_VERSIONS,
1261
+ systemContext: systemContext(),
1262
+ handlers: dashHandlers(phone, note),
1263
+ askPermission: (request, signal) => askPermission(phone, note, request, signal),
1264
+ observe: record,
1265
+ restorePages,
1266
+ onBarsChange: showBars
1267
+ });
1268
+ worker.addEventListener("error", (event) => showError(`The dot's service crashed: ${event.message}`));
1269
+ screen.src = WEBVIEW_URL;
1270
+ runningDot = {
1271
+ host,
1272
+ worker
1273
+ };
1274
+ }
1275
+ function stopRunningDot() {
1276
+ if (!runningDot) return;
1277
+ runningDot.host.stop();
1278
+ runningDot.worker.terminate();
1279
+ runningDot = null;
1280
+ }
1281
+ function showBars(bars, pages) {
1282
+ navigationBar.hidden = bars.navigationBarHidden;
1283
+ navigationTitle.textContent = bars.title;
1284
+ navigationBack.hidden = pages.length < 2;
1285
+ setBarColor("--bar", bars.navigationBarColor);
1286
+ setBarColor("--bar-text", bars.navigationBarTextColor);
1287
+ tabBar.hidden = bars.tabBarHidden;
1288
+ }
1289
+ function setBarColor(property, color) {
1290
+ if (color) phone.style.setProperty(property, color);
1291
+ else phone.style.removeProperty(property);
1292
+ }
1293
+ function goBack() {
1294
+ if (runningDot && !runningDot.host.back()) note("back on the first page, where a phone would close the dot");
1295
+ }
1296
+ const ARROWS = {
1297
+ toHost: "both --> host",
1298
+ fromHost: "host --> both",
1299
+ toService: "ui --> logic",
1300
+ toWebview: "ui <-- logic"
1301
+ };
1302
+ function note(text) {
1303
+ log.append(createElement("li", "note", text));
1304
+ }
1305
+ function record(direction, message) {
1306
+ const entry = createElement("li", direction);
1307
+ const details = document.createElement("details");
1308
+ const summary = document.createElement("summary");
1309
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour12: false });
1310
+ summary.append(createElement("span", "time", time), createElement("span", "arrow", ARROWS[direction]), createElement("span", "type", message.type));
1311
+ const payload = document.createElement("pre");
1312
+ payload.textContent = JSON.stringify(message.payload, null, 2);
1313
+ details.append(summary, payload);
1314
+ entry.append(details);
1315
+ if (message.type === "error") entry.classList.add("failed");
1316
+ log.append(entry);
1317
+ while (log.childElementCount > MAX_LOG_ENTRIES) log.firstElementChild?.remove();
1318
+ log.scrollTop = log.scrollHeight;
1319
+ }
1320
+ function restart(restorePages = []) {
1321
+ launch(restorePages).catch((error) => {
1322
+ showError(`Could not start the dot: ${String(error)}`);
1323
+ });
1324
+ }
1325
+ function relaunch() {
1326
+ restart(runningDot?.host.pages ?? []);
1327
+ }
1328
+ function hotReload() {
1329
+ if (!runningDot) {
1330
+ restart();
1331
+ return;
1332
+ }
1333
+ hotReloads += 1;
1334
+ const reload = { dotHotReload: hotReloads };
1335
+ runningDot.worker.postMessage(reload);
1336
+ screen.contentWindow?.postMessage(reload, location.origin);
1337
+ note("dot updated in place");
1338
+ }
1339
+ function showError(message) {
1340
+ errorBanner.hidden = message === null;
1341
+ errorBanner.textContent = message ?? "";
1342
+ }
1343
+ function syncSystemContext() {
1344
+ runningDot?.host.setSystemContext(systemContext());
1345
+ }
1346
+ function systemContext() {
1347
+ return {
1348
+ scheme: schemeSelect.value,
1349
+ appState: appState()
1350
+ };
1351
+ }
1352
+ function appState() {
1353
+ if (document.visibilityState === "hidden") return "background";
1354
+ return "active";
1355
+ }
1356
+ function browserScheme() {
1357
+ return browserDark.matches ? "dark" : "light";
1358
+ }
1359
+ function changeScheme(scheme) {
1360
+ schemeSelect.value = scheme;
1361
+ phone.dataset.scheme = scheme;
1362
+ syncSystemContext();
1363
+ }
1364
+ const events = new EventSource(EVENTS_PATH);
1365
+ events.addEventListener("reload", () => {
1366
+ showError(null);
1367
+ relaunch();
1368
+ });
1369
+ events.addEventListener("hot-reload", () => {
1370
+ showError(null);
1371
+ hotReload();
1372
+ });
1373
+ events.addEventListener("build-error", (event) => showError(String(event.data)));
1374
+ element("#reload").addEventListener("click", () => restart());
1375
+ element("#back").addEventListener("click", goBack);
1376
+ navigationBack.addEventListener("click", goBack);
1377
+ element("#clear").addEventListener("click", () => log.replaceChildren());
1378
+ schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
1379
+ browserDark.addEventListener("change", () => changeScheme(browserScheme()));
1380
+ document.addEventListener("visibilitychange", syncSystemContext);
1381
+ changeScheme(browserScheme());
1382
+ restart();
1383
+ //#endregion
1384
+ })();
1385
+
1386
+ //# sourceMappingURL=simulator.js.map