@monimesl/dot-simulator 0.1.0-alpha.2 → 0.1.0-alpha.4

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.
@@ -15,72 +15,498 @@
15
15
  }
16
16
  return registry;
17
17
  }
18
+ const AmountInput = defineComponent({
19
+ name: "AmountInput",
20
+ props: {
21
+ value: "number",
22
+ currency: "string",
23
+ decimals: "number",
24
+ label: "string",
25
+ placeholder: "string",
26
+ hint: "string",
27
+ error: "string",
28
+ disabled: "boolean"
29
+ },
30
+ events: ["change"],
31
+ children: "none"
32
+ });
33
+ const Button = defineComponent({
34
+ name: "Button",
35
+ props: {
36
+ variant: ["primary", "secondary"],
37
+ disabled: "boolean",
38
+ loading: "boolean"
39
+ },
40
+ events: ["click"],
41
+ children: "text"
42
+ });
43
+ const SIZES = [
44
+ "small",
45
+ "medium",
46
+ "large"
47
+ ];
48
+ const SPACES = [
49
+ "none",
50
+ "small",
51
+ "medium",
52
+ "large"
53
+ ];
54
+ const TONES = [
55
+ "default",
56
+ "muted",
57
+ "info",
58
+ "success",
59
+ "warning",
60
+ "danger"
61
+ ];
62
+ const STATUS_TONES = [
63
+ "default",
64
+ "info",
65
+ "success",
66
+ "warning",
67
+ "danger"
68
+ ];
69
+ const ICON_NAMES = [
70
+ "arrow-down",
71
+ "arrow-left",
72
+ "arrow-right",
73
+ "arrow-up",
74
+ "bank",
75
+ "bell",
76
+ "bolt",
77
+ "calendar",
78
+ "card",
79
+ "cart",
80
+ "cash",
81
+ "check",
82
+ "chevron-down",
83
+ "chevron-left",
84
+ "chevron-right",
85
+ "chevron-up",
86
+ "clock",
87
+ "close",
88
+ "copy",
89
+ "download",
90
+ "edit",
91
+ "error",
92
+ "eye",
93
+ "eye-off",
94
+ "file",
95
+ "gift",
96
+ "grid",
97
+ "heart",
98
+ "help",
99
+ "history",
100
+ "home",
101
+ "image",
102
+ "info",
103
+ "list",
104
+ "location",
105
+ "lock",
106
+ "logout",
107
+ "mail",
108
+ "minus",
109
+ "mobile",
110
+ "more",
111
+ "phone",
112
+ "plus",
113
+ "qr-code",
114
+ "receipt",
115
+ "refresh",
116
+ "search",
117
+ "send",
118
+ "settings",
119
+ "share",
120
+ "star",
121
+ "success",
122
+ "trash",
123
+ "upload",
124
+ "user",
125
+ "users",
126
+ "wallet",
127
+ "warning",
128
+ "water",
129
+ "wifi"
130
+ ];
131
+ const Carousel = defineComponent({
132
+ name: "Carousel",
133
+ props: {
134
+ autoplay: "boolean",
135
+ interval: "number",
136
+ loop: "boolean",
137
+ indicator: ["dots", "none"],
138
+ gap: SPACES,
139
+ effect: ["slide", "scale"],
140
+ index: "number"
141
+ },
142
+ events: ["change"],
143
+ children: "elements"
144
+ });
145
+ const Chip = defineComponent({
146
+ name: "Chip",
147
+ props: {
148
+ label: "string",
149
+ icon: ICON_NAMES,
150
+ selected: "boolean",
151
+ disabled: "boolean"
152
+ },
153
+ events: ["tap"],
154
+ children: "none"
155
+ });
156
+ const CodeInput = defineComponent({
157
+ name: "CodeInput",
158
+ props: {
159
+ value: "string",
160
+ length: "number",
161
+ label: "string",
162
+ hint: "string",
163
+ error: "string",
164
+ secure: "boolean",
165
+ disabled: "boolean"
166
+ },
167
+ events: ["change", "complete"],
168
+ children: "none"
169
+ });
170
+ const Divider = defineComponent({
171
+ name: "Divider",
172
+ children: "none"
173
+ });
174
+ const EmptyState = defineComponent({
175
+ name: "EmptyState",
176
+ props: {
177
+ icon: ICON_NAMES,
178
+ title: "string",
179
+ text: "string"
180
+ },
181
+ children: "elements"
182
+ });
183
+ const Spinner = defineComponent({
184
+ name: "Spinner",
185
+ props: { size: SIZES },
186
+ children: "none"
187
+ });
188
+ const ProgressBar = defineComponent({
189
+ name: "ProgressBar",
190
+ props: { value: "number" },
191
+ children: "none"
192
+ });
193
+ const Notice = defineComponent({
194
+ name: "Notice",
195
+ props: {
196
+ tone: STATUS_TONES,
197
+ title: "string"
198
+ },
199
+ children: "text"
200
+ });
201
+ const Badge = defineComponent({
202
+ name: "Badge",
203
+ props: { tone: STATUS_TONES },
204
+ children: "text"
205
+ });
206
+ const Footer = defineComponent({
207
+ name: "Footer",
208
+ children: "elements"
209
+ });
210
+ const Grid = defineComponent({
211
+ name: "Grid",
212
+ props: {
213
+ columns: "number",
214
+ gap: SPACES
215
+ },
216
+ children: "elements"
217
+ });
218
+ const GridItem = defineComponent({
219
+ name: "GridItem",
220
+ props: {
221
+ label: "string",
222
+ icon: ICON_NAMES,
223
+ image: "string",
224
+ badge: "string"
225
+ },
226
+ events: ["tap"],
227
+ children: "none"
228
+ });
229
+ const Icon = defineComponent({
230
+ name: "Icon",
231
+ props: {
232
+ name: ICON_NAMES,
233
+ size: SIZES,
234
+ tone: TONES
235
+ },
236
+ children: "none",
237
+ inline: true
238
+ });
239
+ const Image = defineComponent({
240
+ name: "Image",
241
+ props: {
242
+ src: "string",
243
+ alt: "string",
244
+ width: "number",
245
+ height: "number",
246
+ fit: ["cover", "contain"],
247
+ shape: [
248
+ "rounded",
249
+ "square",
250
+ "circle"
251
+ ]
252
+ },
253
+ events: ["load", "error"],
254
+ children: "none"
255
+ });
256
+ const Input = defineComponent({
257
+ name: "Input",
258
+ props: {
259
+ value: "string",
260
+ placeholder: "string",
261
+ label: "string",
262
+ hint: "string",
263
+ error: "string",
264
+ prefix: "string",
265
+ suffix: "string",
266
+ type: [
267
+ "text",
268
+ "number",
269
+ "email",
270
+ "phone",
271
+ "password"
272
+ ],
273
+ maxLength: "number",
274
+ disabled: "boolean"
275
+ },
276
+ events: ["change", "submit"],
277
+ children: "none"
278
+ });
279
+ const List = defineComponent({
280
+ name: "List",
281
+ children: "elements"
282
+ });
283
+ const ListItem = defineComponent({
284
+ name: "ListItem",
285
+ props: {
286
+ title: "string",
287
+ subtitle: "string",
288
+ value: "string",
289
+ icon: ICON_NAMES,
290
+ image: "string"
291
+ },
292
+ events: ["tap"],
293
+ children: "elements"
294
+ });
295
+ const Sheet = defineComponent({
296
+ name: "Sheet",
297
+ props: {
298
+ open: "boolean",
299
+ title: "string"
300
+ },
301
+ events: ["close"],
302
+ children: "elements"
303
+ });
304
+ const Toast = defineComponent({
305
+ name: "Toast",
306
+ props: {
307
+ message: "string",
308
+ tone: STATUS_TONES,
309
+ duration: "number"
310
+ },
311
+ events: ["hide"],
312
+ children: "none"
313
+ });
314
+ const QRCode = defineComponent({
315
+ name: "QRCode",
316
+ props: {
317
+ value: "string",
318
+ size: "number",
319
+ label: "string"
320
+ },
321
+ children: "none"
322
+ });
323
+ const Select = defineComponent({
324
+ name: "Select",
325
+ props: {
326
+ value: "string",
327
+ label: "string",
328
+ placeholder: "string",
329
+ hint: "string",
330
+ error: "string",
331
+ disabled: "boolean"
332
+ },
333
+ events: ["change"],
334
+ children: "elements"
335
+ });
336
+ const Option = defineComponent({
337
+ name: "Option",
338
+ props: {
339
+ value: "string",
340
+ label: "string"
341
+ },
342
+ children: "none"
343
+ });
344
+ const Skeleton = defineComponent({
345
+ name: "Skeleton",
346
+ props: {
347
+ shape: [
348
+ "text",
349
+ "circle",
350
+ "box"
351
+ ],
352
+ lines: "number",
353
+ height: "number"
354
+ },
355
+ children: "none"
356
+ });
357
+ const Tabs = defineComponent({
358
+ name: "Tabs",
359
+ props: {
360
+ value: "string",
361
+ variant: ["underline", "segmented"]
362
+ },
363
+ events: ["change"],
364
+ children: "elements"
365
+ });
366
+ const Tab = defineComponent({
367
+ name: "Tab",
368
+ props: {
369
+ value: "string",
370
+ label: "string",
371
+ badge: "number"
372
+ },
373
+ children: "none"
374
+ });
375
+ const Text = defineComponent({
376
+ name: "Text",
377
+ props: {
378
+ variant: [
379
+ "title",
380
+ "body",
381
+ "caption"
382
+ ],
383
+ tone: TONES,
384
+ weight: [
385
+ "regular",
386
+ "medium",
387
+ "bold"
388
+ ],
389
+ align: [
390
+ "start",
391
+ "center",
392
+ "end"
393
+ ],
394
+ maxLines: "number"
395
+ },
396
+ children: "text",
397
+ inline: true
398
+ });
399
+ const TextArea = defineComponent({
400
+ name: "TextArea",
401
+ props: {
402
+ value: "string",
403
+ placeholder: "string",
404
+ label: "string",
405
+ hint: "string",
406
+ error: "string",
407
+ rows: "number",
408
+ maxLength: "number",
409
+ disabled: "boolean"
410
+ },
411
+ events: ["change"],
412
+ children: "none"
413
+ });
414
+ const Switch = defineComponent({
415
+ name: "Switch",
416
+ props: {
417
+ checked: "boolean",
418
+ label: "string",
419
+ disabled: "boolean"
420
+ },
421
+ events: ["change"],
422
+ children: "none"
423
+ });
424
+ const Checkbox = defineComponent({
425
+ name: "Checkbox",
426
+ props: {
427
+ checked: "boolean",
428
+ label: "string",
429
+ disabled: "boolean"
430
+ },
431
+ events: ["change"],
432
+ children: "none"
433
+ });
434
+ const RadioGroup = defineComponent({
435
+ name: "RadioGroup",
436
+ props: {
437
+ value: "string",
438
+ label: "string",
439
+ disabled: "boolean"
440
+ },
441
+ events: ["change"],
442
+ children: "elements"
443
+ });
18
444
  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",
445
+ AmountInput,
446
+ Badge,
447
+ Button,
448
+ Carousel,
449
+ Checkbox,
450
+ Chip,
451
+ CodeInput,
452
+ Divider,
453
+ EmptyState,
454
+ Footer,
455
+ Grid,
456
+ GridItem,
457
+ Icon,
458
+ Image,
459
+ Input,
460
+ List,
461
+ ListItem,
462
+ Notice,
463
+ Option,
464
+ ProgressBar,
465
+ QRCode,
466
+ Radio: defineComponent({
467
+ name: "Radio",
41
468
  props: {
42
469
  value: "string",
43
- placeholder: "string",
44
470
  label: "string",
45
- type: [
46
- "text",
47
- "number",
48
- "email",
49
- "phone",
50
- "password"
51
- ],
52
- maxLength: "number",
53
471
  disabled: "boolean"
54
472
  },
55
- events: ["change", "submit"],
56
473
  children: "none"
57
474
  }),
58
- Text: defineComponent({
59
- name: "Text",
60
- props: { variant: [
61
- "title",
62
- "body",
63
- "caption"
64
- ] },
65
- children: "text",
66
- inline: true
67
- }),
475
+ RadioGroup,
476
+ Select,
477
+ Sheet,
478
+ Skeleton,
479
+ Spinner,
480
+ Switch,
481
+ Tab,
482
+ Tabs,
483
+ Text,
484
+ TextArea,
485
+ Toast,
68
486
  View: defineComponent({
69
487
  name: "View",
70
488
  props: {
489
+ gap: SPACES,
71
490
  direction: ["column", "row"],
72
- gap: [
73
- "none",
74
- "small",
75
- "medium",
76
- "large"
77
- ],
78
491
  align: [
79
492
  "stretch",
80
493
  "start",
81
494
  "center",
82
495
  "end"
83
496
  ],
497
+ justify: [
498
+ "start",
499
+ "center",
500
+ "end",
501
+ "between"
502
+ ],
503
+ wrap: "boolean",
504
+ grow: "boolean",
505
+ padding: SPACES,
506
+ paddingTop: SPACES,
507
+ paddingStart: SPACES,
508
+ paddingEnd: SPACES,
509
+ paddingBottom: SPACES,
84
510
  variant: ["plain", "card"]
85
511
  },
86
512
  events: ["tap"],
@@ -96,6 +522,8 @@
96
522
  this.code = code;
97
523
  }
98
524
  };
525
+ const LABEL = "[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?";
526
+ new RegExp(`^(?:\\*\\.${LABEL}(?:\\.${LABEL})+|${LABEL}(?:\\.${LABEL})*)$`);
99
527
  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
528
  function majorVersion(version) {
101
529
  return parseVersion(version).major;
@@ -337,7 +765,7 @@
337
765
  };
338
766
  //#endregion
339
767
  //#region src/host.ts
340
- function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe }) {
768
+ function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe, restorePages = [], onBarsChange }) {
341
769
  checkCompatibility(manifest, {
342
770
  dashVersion,
343
771
  minRuntimeVersion
@@ -357,6 +785,18 @@
357
785
  ui.post(message);
358
786
  logic.post(message);
359
787
  };
788
+ const barsByInstance = /* @__PURE__ */ new Map();
789
+ const showBars = () => {
790
+ const top = stack.top;
791
+ const bars = top && barsByInstance.get(top.instanceId);
792
+ onBarsChange?.({
793
+ title: bars?.navigationBar.title ?? manifest.name,
794
+ navigationBarHidden: bars?.navigationBar.hidden ?? false,
795
+ navigationBarColor: bars?.navigationBar.backgroundColor,
796
+ navigationBarTextColor: bars?.navigationBar.textColor,
797
+ tabBarHidden: bars?.tabBar.hidden ?? false
798
+ }, stack.pages);
799
+ };
360
800
  const syncStack = () => {
361
801
  const message = {
362
802
  type: "syncStack",
@@ -366,6 +806,9 @@
366
806
  }
367
807
  };
368
808
  broadcast(message);
809
+ const open = new Set(stack.pages.map((entry) => entry.instanceId));
810
+ for (const instanceId of barsByInstance.keys()) if (!open.has(instanceId)) barsByInstance.delete(instanceId);
811
+ showBars();
369
812
  };
370
813
  const syncSystemContext = () => {
371
814
  broadcast({
@@ -381,13 +824,40 @@
381
824
  if (stack.top !== previousTop) calls.dismissUI();
382
825
  return true;
383
826
  };
827
+ const openFirstPages = () => {
828
+ const [first, ...rest] = restorePages.filter((entry) => manifest.pages.includes(entry.page));
829
+ if (!first) {
830
+ navigate({
831
+ action: "reset",
832
+ page: manifest.entry,
833
+ options: {}
834
+ });
835
+ return;
836
+ }
837
+ stack.apply({
838
+ action: "reset",
839
+ page: first.page,
840
+ options: first.options
841
+ });
842
+ for (const entry of rest) stack.apply({
843
+ action: "push",
844
+ page: entry.page,
845
+ options: entry.options
846
+ });
847
+ syncStack();
848
+ };
384
849
  const navigateFromPage = ({ from, ...navigation }) => {
385
850
  if (from === stack.top?.instanceId) navigate(navigation);
386
851
  };
387
852
  const hostMessageHandlers = /* @__PURE__ */ new Map([
388
853
  ["navigate", (payload) => navigateFromPage(payload)],
389
854
  ["hostCall", (payload) => calls.run(payload)],
390
- ["hostCancel", (payload) => calls.cancel(payload.id)]
855
+ ["hostCancel", (payload) => calls.cancel(payload.id)],
856
+ ["pageBars", (payload) => {
857
+ const { instanceId, ...bars } = payload;
858
+ barsByInstance.set(instanceId, bars);
859
+ showBars();
860
+ }]
391
861
  ]);
392
862
  const stopService = logic.listen((raw) => {
393
863
  const message = raw;
@@ -406,14 +876,13 @@
406
876
  logic.post(message);
407
877
  if (message.type === "connect") {
408
878
  syncSystemContext();
409
- navigate({
410
- action: "reset",
411
- page: manifest.entry,
412
- options: {}
413
- });
879
+ openFirstPages();
414
880
  }
415
881
  });
416
882
  return {
883
+ get pages() {
884
+ return stack.pages;
885
+ },
417
886
  back: () => navigate({
418
887
  action: "back",
419
888
  delta: 1
@@ -436,10 +905,24 @@
436
905
  //#region src/paths.ts
437
906
  const CLIENT_PATH = "/__dot/";
438
907
  const EVENTS_PATH = `${CLIENT_PATH}events`;
908
+ const HTTP_PATH = `${CLIENT_PATH}network/http/`;
909
+ const FILE_PATH = `${CLIENT_PATH}file/`;
439
910
  const DOT_PATH = "/dot/";
440
911
  const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;
441
912
  const MANIFEST_URL = `${DOT_PATH}manifest.json`;
442
913
  //#endregion
914
+ //#region src/client/dash/dev-server.ts
915
+ async function callDevServer(path, params, signal) {
916
+ const answer = await (await fetch(path, {
917
+ method: "POST",
918
+ headers: { "Content-Type": "application/json" },
919
+ body: JSON.stringify(params),
920
+ signal
921
+ })).json();
922
+ if ("error" in answer) throw new HostError(answer.error.code, answer.error.message);
923
+ return answer.result;
924
+ }
925
+ //#endregion
443
926
  //#region src/client/dash/system.ts
444
927
  const SHAKE_DURATION_MS = 300;
445
928
  const SHAKE = [
@@ -458,6 +941,30 @@
458
941
  phone.dataset.torch = state;
459
942
  note(`torch: ${state}`);
460
943
  }
944
+ async function copyText(note, text) {
945
+ try {
946
+ await navigator.clipboard.writeText(text);
947
+ } catch (error) {
948
+ throw new HostError("failed", `The browser kept the clipboard: ${error.message}`);
949
+ }
950
+ note(`copied: ${text}`);
951
+ }
952
+ function share(note, params) {
953
+ const parts = [
954
+ params.text,
955
+ params.url,
956
+ params.file && `file ${params.file}`
957
+ ].filter(Boolean);
958
+ if (parts.length === 0) throw new HostError("invalid", "Give share a text, a url or a file");
959
+ note(`shared: ${parts.join(" · ")}`);
960
+ }
961
+ function delay(milliseconds, signal) {
962
+ if (!Number.isFinite(milliseconds) || milliseconds < 0) throw new HostError("invalid", `A delay is a number of milliseconds, 0 or more, not ${milliseconds}`);
963
+ return new Promise((resolve) => {
964
+ const timer = setTimeout(resolve, milliseconds);
965
+ signal.addEventListener("abort", () => clearTimeout(timer));
966
+ });
967
+ }
461
968
  //#endregion
462
969
  //#region src/client/dom.ts
463
970
  function element(selector) {
@@ -506,13 +1013,39 @@
506
1013
  }
507
1014
  //#endregion
508
1015
  //#region src/client/dash/index.ts
1016
+ const HTTP_METHODS = [
1017
+ "request",
1018
+ "download",
1019
+ "upload"
1020
+ ];
1021
+ const FILE_OPERATIONS = [
1022
+ "read",
1023
+ "write",
1024
+ "stat",
1025
+ "list",
1026
+ "createFolder",
1027
+ "move",
1028
+ "copy",
1029
+ "remove",
1030
+ "save",
1031
+ "open",
1032
+ "usage"
1033
+ ];
509
1034
  function dashHandlers(phone, note) {
510
1035
  return {
511
1036
  "dash.ui.alert": ({ params, signal }) => alert(phone, params, signal),
512
1037
  "dash.system.vibrate": ({ params }) => vibrate(phone, note, params),
513
- "dash.system.torch": ({ params }) => torch(phone, note, params.state)
1038
+ "dash.system.torch": ({ params }) => torch(phone, note, params.state),
1039
+ "dash.system.copyText": ({ params }) => copyText(note, params.text),
1040
+ "dash.system.share": ({ params }) => share(note, params),
1041
+ "dash.system.delay": ({ params, signal }) => delay(params.milliseconds, signal),
1042
+ ...devServerHandlers("dash.network.http", HTTP_PATH, HTTP_METHODS),
1043
+ ...devServerHandlers("dash.file", FILE_PATH, FILE_OPERATIONS)
514
1044
  };
515
1045
  }
1046
+ function devServerHandlers(group, path, names) {
1047
+ return Object.fromEntries(names.map((name) => [`${group}.${name}`, ({ params, signal }) => callDevServer(`${path}${name}`, params, signal)]));
1048
+ }
516
1049
  //#endregion
517
1050
  //#region src/client/dash/permissions.ts
518
1051
  const PERMISSION_SUMMARIES = { account: "see your Dash account: your name, and your email or phone" };
@@ -567,10 +1100,15 @@
567
1100
  const errorBanner = element("#error");
568
1101
  const phone = element(".phone");
569
1102
  const schemeSelect = element("#scheme");
1103
+ const navigationBar = element(".navigation-bar");
1104
+ const navigationTitle = element("#navigation-title");
1105
+ const navigationBack = element("#navigation-back");
1106
+ const tabBar = element(".tab-bar");
570
1107
  const browserDark = matchMedia("(prefers-color-scheme: dark)");
571
1108
  let runningApp = null;
572
1109
  let launchCount = 0;
573
- async function launch() {
1110
+ let hotReloads = 0;
1111
+ async function launch(restorePages = []) {
574
1112
  stopRunningApp();
575
1113
  launchCount += 1;
576
1114
  const thisLaunch = launchCount;
@@ -599,7 +1137,9 @@
599
1137
  systemContext: systemContext(),
600
1138
  handlers: dashHandlers(phone, note),
601
1139
  askPermission: (request, signal) => askPermission(phone, note, request, signal),
602
- observe: record
1140
+ observe: record,
1141
+ restorePages,
1142
+ onBarsChange: showBars
603
1143
  });
604
1144
  worker.addEventListener("error", (event) => showError(`Service crashed: ${event.message}`));
605
1145
  screen.src = WEBVIEW_URL;
@@ -614,6 +1154,18 @@
614
1154
  runningApp.worker.terminate();
615
1155
  runningApp = null;
616
1156
  }
1157
+ function showBars(bars, pages) {
1158
+ navigationBar.hidden = bars.navigationBarHidden;
1159
+ navigationTitle.textContent = bars.title;
1160
+ navigationBack.hidden = pages.length < 2;
1161
+ setBarColor("--bar", bars.navigationBarColor);
1162
+ setBarColor("--bar-text", bars.navigationBarTextColor);
1163
+ tabBar.hidden = bars.tabBarHidden;
1164
+ }
1165
+ function setBarColor(property, color) {
1166
+ if (color) phone.style.setProperty(property, color);
1167
+ else phone.style.removeProperty(property);
1168
+ }
617
1169
  function goBack() {
618
1170
  if (runningApp && !runningApp.host.back()) note("back on the first page: a device would close the dot");
619
1171
  }
@@ -641,11 +1193,25 @@
641
1193
  while (log.childElementCount > MAX_LOG_ENTRIES) log.firstElementChild?.remove();
642
1194
  log.scrollTop = log.scrollHeight;
643
1195
  }
644
- function syncLaunch() {
645
- launch().catch((error) => {
1196
+ function syncLaunch(restorePages = []) {
1197
+ launch(restorePages).catch((error) => {
646
1198
  showError(`Could not start the dot: ${String(error)}`);
647
1199
  });
648
1200
  }
1201
+ function relaunch() {
1202
+ syncLaunch(runningApp?.host.pages ?? []);
1203
+ }
1204
+ function hotReload() {
1205
+ if (!runningApp) {
1206
+ syncLaunch();
1207
+ return;
1208
+ }
1209
+ hotReloads += 1;
1210
+ const reload = { dotHotReload: hotReloads };
1211
+ runningApp.worker.postMessage(reload);
1212
+ screen.contentWindow?.postMessage(reload, location.origin);
1213
+ note("dot updated in place");
1214
+ }
649
1215
  function showError(message) {
650
1216
  errorBanner.hidden = message === null;
651
1217
  errorBanner.textContent = message ?? "";
@@ -671,11 +1237,16 @@
671
1237
  const events = new EventSource(EVENTS_PATH);
672
1238
  events.addEventListener("reload", () => {
673
1239
  showError(null);
674
- syncLaunch();
1240
+ relaunch();
1241
+ });
1242
+ events.addEventListener("hot-reload", () => {
1243
+ showError(null);
1244
+ hotReload();
675
1245
  });
676
1246
  events.addEventListener("build-error", (event) => showError(String(event.data)));
677
- element("#reload").addEventListener("click", syncLaunch);
1247
+ element("#reload").addEventListener("click", () => syncLaunch());
678
1248
  element("#back").addEventListener("click", goBack);
1249
+ navigationBack.addEventListener("click", goBack);
679
1250
  element("#clear").addEventListener("click", () => log.replaceChildren());
680
1251
  schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
681
1252
  browserDark.addEventListener("change", () => changeScheme(browserScheme()));