@monimesl/dot-simulator 0.1.0-alpha.15 → 0.1.0-alpha.18

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.
@@ -76,6 +76,49 @@
76
76
  "warning",
77
77
  "danger"
78
78
  ];
79
+ const TEXT_TONES = {
80
+ words: [...TONES, "onPrimary"],
81
+ or: "themeColor"
82
+ };
83
+ const BACKGROUNDS = {
84
+ words: [
85
+ "background",
86
+ "surface",
87
+ "primary",
88
+ "muted",
89
+ "info",
90
+ "success",
91
+ "warning",
92
+ "danger"
93
+ ],
94
+ or: "themeColor"
95
+ };
96
+ const BORDERS = [
97
+ "none",
98
+ "subtle",
99
+ "strong"
100
+ ];
101
+ const RADII = [
102
+ "none",
103
+ "small",
104
+ "medium",
105
+ "large",
106
+ "full"
107
+ ];
108
+ const LENGTH = {
109
+ words: ["fill"],
110
+ or: "number"
111
+ };
112
+ const BOX_PROPS = {
113
+ background: BACKGROUNDS,
114
+ border: BORDERS,
115
+ radius: RADII,
116
+ width: LENGTH,
117
+ height: LENGTH,
118
+ minHeight: "number",
119
+ maxWidth: "number",
120
+ aspect: ASPECTS
121
+ };
79
122
  const STATUS_TONES = [
80
123
  "default",
81
124
  "info",
@@ -305,6 +348,45 @@
305
348
  because: "the aspect sets the height"
306
349
  }]
307
350
  });
351
+ const Slider = defineComponent({
352
+ name: "Slider",
353
+ props: {
354
+ value: "number",
355
+ min: "number",
356
+ max: "number",
357
+ step: "number",
358
+ showValue: "boolean",
359
+ label: "string",
360
+ hint: "string",
361
+ disabled: "boolean"
362
+ },
363
+ events: ["change"],
364
+ children: "none"
365
+ });
366
+ const Prose = defineComponent({
367
+ name: "Prose",
368
+ props: { ...PADDING_PROPS },
369
+ children: "elements"
370
+ });
371
+ const Quote = defineComponent({
372
+ name: "Quote",
373
+ props: { cite: "string" },
374
+ children: "elements"
375
+ });
376
+ const BulletList = defineComponent({
377
+ name: "BulletList",
378
+ props: { ordered: "boolean" },
379
+ children: "elements"
380
+ });
381
+ const Bullet = defineComponent({
382
+ name: "Bullet",
383
+ children: "elements"
384
+ });
385
+ const RichText = defineComponent({
386
+ name: "RichText",
387
+ props: { markdown: "string" },
388
+ children: "none"
389
+ });
308
390
  const Input = defineComponent({
309
391
  name: "Input",
310
392
  props: {
@@ -462,11 +544,14 @@
462
544
  name: "Text",
463
545
  props: {
464
546
  variant: [
547
+ "display",
465
548
  "title",
549
+ "subtitle",
550
+ "lead",
466
551
  "body",
467
552
  "caption"
468
553
  ],
469
- tone: TONES,
554
+ tone: TEXT_TONES,
470
555
  weight: [
471
556
  "regular",
472
557
  "medium",
@@ -478,6 +563,19 @@
478
563
  "end"
479
564
  ],
480
565
  maxLines: "number",
566
+ decoration: [
567
+ "none",
568
+ "underline",
569
+ "strikethrough"
570
+ ],
571
+ numeric: "boolean",
572
+ leading: [
573
+ "tight",
574
+ "normal",
575
+ "relaxed"
576
+ ],
577
+ italic: "boolean",
578
+ selectable: "boolean",
481
579
  ...PADDING_PROPS
482
580
  },
483
581
  children: "text",
@@ -528,9 +626,51 @@
528
626
  events: ["change"],
529
627
  children: "elements"
530
628
  });
629
+ const Radio = defineComponent({
630
+ name: "Radio",
631
+ props: {
632
+ value: "string",
633
+ label: "string",
634
+ disabled: "boolean"
635
+ },
636
+ children: "none"
637
+ });
638
+ const View = defineComponent({
639
+ name: "View",
640
+ props: {
641
+ gap: SPACES,
642
+ direction: ["column", "row"],
643
+ align: [
644
+ "stretch",
645
+ "start",
646
+ "center",
647
+ "end"
648
+ ],
649
+ justify: [
650
+ "start",
651
+ "center",
652
+ "end",
653
+ "between"
654
+ ],
655
+ wrap: "boolean",
656
+ grow: "boolean",
657
+ ...PADDING_PROPS,
658
+ variant: ["plain", "card"],
659
+ ...BOX_PROPS
660
+ },
661
+ events: ["tap"],
662
+ children: "elements",
663
+ overrides: [{
664
+ prop: "aspect",
665
+ ignores: "height",
666
+ because: "the aspect sets the height"
667
+ }]
668
+ });
531
669
  const builtinComponents = {
532
670
  AmountInput,
533
671
  Badge,
672
+ Bullet,
673
+ BulletList,
534
674
  Button,
535
675
  Carousel,
536
676
  Checkbox,
@@ -551,53 +691,40 @@
551
691
  Notice,
552
692
  Option,
553
693
  ProgressBar,
694
+ Prose,
554
695
  QRCode,
555
- Radio: defineComponent({
556
- name: "Radio",
557
- props: {
558
- value: "string",
559
- label: "string",
560
- disabled: "boolean"
561
- },
562
- children: "none"
563
- }),
696
+ Quote,
697
+ Radio,
564
698
  RadioGroup,
699
+ RichText,
565
700
  Scroll,
566
701
  Select,
567
702
  Sheet,
568
703
  Skeleton,
704
+ Slider,
569
705
  Spinner,
706
+ Stack: defineComponent({
707
+ name: "Stack",
708
+ props: {
709
+ radius: BOX_PROPS.radius,
710
+ width: BOX_PROPS.width,
711
+ height: BOX_PROPS.height,
712
+ aspect: BOX_PROPS.aspect
713
+ },
714
+ children: "elements",
715
+ overrides: [{
716
+ prop: "aspect",
717
+ ignores: "height",
718
+ because: "the aspect sets the height"
719
+ }]
720
+ }),
570
721
  Switch,
571
722
  Tab,
572
723
  Tabs,
573
724
  Text,
574
725
  TextArea,
575
726
  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
- })
727
+ View
601
728
  };
602
729
  componentRegistry(Object.values(builtinComponents));
603
730
  var HostError = class extends Error {
@@ -678,7 +805,7 @@
678
805
  limit;
679
806
  #pages = [];
680
807
  #nextInstanceId = 1;
681
- constructor(limit = 8) {
808
+ constructor(limit = 16) {
682
809
  this.limit = limit;
683
810
  }
684
811
  get pages() {
@@ -779,7 +906,8 @@
779
906
  this.#succeed(id, await entry(call));
780
907
  return;
781
908
  }
782
- await this.#permissions.check(call.method, entry.permission, call.signal);
909
+ const permission = typeof entry.permission === "function" ? entry.permission(call.params) : entry.permission;
910
+ await this.#permissions.check(call.method, permission, call.signal);
783
911
  this.#succeed(id, await entry.handle(call));
784
912
  } catch (error) {
785
913
  this.#fail(id, error);
@@ -979,7 +1107,7 @@
979
1107
  }
980
1108
  //#endregion
981
1109
  //#region src/versions.ts
982
- const SIMULATOR_VERSIONS = { runtimeVersion: "0.1.0-alpha.15" };
1110
+ const SIMULATOR_VERSIONS = { runtimeVersion: "0.1.0-alpha.18" };
983
1111
  //#endregion
984
1112
  //#region src/paths.ts
985
1113
  const CLIENT_PATH = "/__dot/";
@@ -988,6 +1116,7 @@
988
1116
  const FILE_PATH = `${CLIENT_PATH}storage/file/`;
989
1117
  const CACHE_PATH = `${CLIENT_PATH}storage/cache/`;
990
1118
  const SECRET_PATH = `${CLIENT_PATH}storage/secret/`;
1119
+ const MEDIA_PATH = `${CLIENT_PATH}media/`;
991
1120
  const DOT_PATH = "/dot/";
992
1121
  const WEBVIEW_URL = `${DOT_PATH}ui/index.html`;
993
1122
  const MANIFEST_URL = `${DOT_PATH}manifest.json`;
@@ -1003,6 +1132,196 @@
1003
1132
  if ("error" in answer) throw new HostError(answer.error.code, answer.error.message);
1004
1133
  return answer.result;
1005
1134
  }
1135
+ function tooLargeMessage(name) {
1136
+ return `"${name}" is larger than 25 MB`;
1137
+ }
1138
+ //#endregion
1139
+ //#region src/client/dom.ts
1140
+ function element(selector) {
1141
+ const node = document.querySelector(selector);
1142
+ if (!node) throw new Error(`The simulator page is missing ${selector}`);
1143
+ return node;
1144
+ }
1145
+ function createElement(tag, className, text) {
1146
+ const node = document.createElement(tag);
1147
+ node.className = className;
1148
+ if (text !== void 0) node.textContent = text;
1149
+ return node;
1150
+ }
1151
+ //#endregion
1152
+ //#region src/client/dash/ui.ts
1153
+ const DEFAULT_ACTIONS = [{
1154
+ id: "ok",
1155
+ label: "OK"
1156
+ }];
1157
+ function alert(phone, options, signal, field) {
1158
+ return new Promise((resolve, reject) => {
1159
+ const layer = createElement("div", "dialog-layer");
1160
+ const dialog = createElement("div", "dialog");
1161
+ dialog.setAttribute("role", "alertdialog");
1162
+ const body = createElement("div", "dialog-body");
1163
+ body.append(createElement("div", "dialog-title", options.title));
1164
+ if (options.message) body.append(createElement("div", "dialog-message", options.message));
1165
+ if (field) body.append(field);
1166
+ const actions = options.actions ?? DEFAULT_ACTIONS;
1167
+ const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
1168
+ for (const action of actions) {
1169
+ const button = createElement("button", action.role ?? "default", action.label);
1170
+ button.addEventListener("click", () => {
1171
+ layer.remove();
1172
+ resolve({ action: action.id });
1173
+ });
1174
+ buttons.append(button);
1175
+ }
1176
+ dialog.append(body, buttons);
1177
+ layer.append(dialog);
1178
+ field?.addEventListener("keydown", (event) => {
1179
+ if (event.key === "Enter") buttons.lastElementChild?.click();
1180
+ });
1181
+ signal.addEventListener("abort", () => {
1182
+ layer.remove();
1183
+ reject(new HostError("cancelled", "The alert was dismissed"));
1184
+ });
1185
+ phone.append(layer);
1186
+ field?.focus();
1187
+ });
1188
+ }
1189
+ //#endregion
1190
+ //#region src/client/dash/media.ts
1191
+ const MAX_PHOTO_SIDE = 2048;
1192
+ const JPEG_QUALITY = .85;
1193
+ function photoPermission(params) {
1194
+ return photoSource(params) === "camera" ? "camera" : "photoLibrary";
1195
+ }
1196
+ async function pickPhoto(phone, note, params, signal) {
1197
+ const source = photoSource(params);
1198
+ await allowPicker(phone, source === "camera" ? "Take a photo" : "Choose a photo", signal);
1199
+ const file = await chooseFile("image/*", signal);
1200
+ const jpeg = await toJpeg(file);
1201
+ const name = jpegName(file.name);
1202
+ note(`photo (${source}): ${name}`);
1203
+ return keepOnDevServer("photo", {
1204
+ name,
1205
+ data: await base64(jpeg)
1206
+ }, signal);
1207
+ }
1208
+ async function pickFile(phone, note, params, signal) {
1209
+ const types = fileTypes(params);
1210
+ await allowPicker(phone, "Choose a file", signal);
1211
+ const file = await chooseFile(types.join(","), signal);
1212
+ if (file.size > 26214400) throw new HostError("invalid", tooLargeMessage(file.name));
1213
+ note(`file: ${file.name}`);
1214
+ const contentType = file.type || "application/octet-stream";
1215
+ return keepOnDevServer("file", {
1216
+ name: file.name,
1217
+ contentType,
1218
+ data: await base64(file)
1219
+ }, signal);
1220
+ }
1221
+ async function scanCode(phone, note, signal) {
1222
+ const field = createElement("input", "dialog-input");
1223
+ field.placeholder = "https://monime.io/pay/…";
1224
+ const { action } = await alert(phone, {
1225
+ title: "Scan a QR code",
1226
+ message: "Type or paste the text the code holds.",
1227
+ actions: [{
1228
+ id: "cancel",
1229
+ label: "Cancel",
1230
+ role: "cancel"
1231
+ }, {
1232
+ id: "scan",
1233
+ label: "Scan"
1234
+ }]
1235
+ }, signal, field);
1236
+ const text = field.value.trim();
1237
+ if (action !== "scan" || text === "") throw new HostError("cancelled", "No code was scanned");
1238
+ note(`scanned: ${text}`);
1239
+ return {
1240
+ text,
1241
+ format: "qr"
1242
+ };
1243
+ }
1244
+ function jpegName(fileName) {
1245
+ const dot = fileName.lastIndexOf(".");
1246
+ return `${(dot > 0 ? fileName.slice(0, dot) : fileName) || "photo"}.jpg`;
1247
+ }
1248
+ function photoSource(params) {
1249
+ const source = params?.source;
1250
+ if (source !== "library" && source !== "camera") throw new HostError("invalid", "\"source\" must be \"library\" or \"camera\"");
1251
+ return source;
1252
+ }
1253
+ function fileTypes(params) {
1254
+ const types = params?.types ?? [];
1255
+ if (!Array.isArray(types) || types.some((type) => typeof type !== "string" || !type.includes("/"))) throw new HostError("invalid", "\"types\" must list content types, such as \"application/pdf\" or \"image/*\"");
1256
+ return types;
1257
+ }
1258
+ async function allowPicker(phone, title, signal) {
1259
+ if (navigator.userActivation.isActive) return;
1260
+ const { action } = await alert(phone, {
1261
+ title,
1262
+ message: "The simulator picks a file from this computer.",
1263
+ actions: [{
1264
+ id: "cancel",
1265
+ label: "Cancel",
1266
+ role: "cancel"
1267
+ }, {
1268
+ id: "choose",
1269
+ label: "Choose"
1270
+ }]
1271
+ }, signal);
1272
+ if (action !== "choose") throw new HostError("cancelled", "Nothing was chosen");
1273
+ }
1274
+ function chooseFile(accept, signal) {
1275
+ return new Promise((resolve, reject) => {
1276
+ const input = document.createElement("input");
1277
+ input.type = "file";
1278
+ input.accept = accept;
1279
+ input.addEventListener("change", () => {
1280
+ const file = input.files?.[0];
1281
+ if (file) resolve(file);
1282
+ else reject(new HostError("cancelled", "Nothing was chosen"));
1283
+ });
1284
+ input.addEventListener("cancel", () => {
1285
+ reject(new HostError("cancelled", "Nothing was chosen"));
1286
+ });
1287
+ signal.addEventListener("abort", () => {
1288
+ reject(new HostError("cancelled", "The call ended before anything was chosen"));
1289
+ });
1290
+ input.click();
1291
+ });
1292
+ }
1293
+ async function toJpeg(file) {
1294
+ let image;
1295
+ try {
1296
+ image = await createImageBitmap(file, { imageOrientation: "from-image" });
1297
+ } catch {
1298
+ throw new HostError("failed", `"${file.name}" isn't a photo the simulator can read`);
1299
+ }
1300
+ const scale = Math.min(1, MAX_PHOTO_SIDE / Math.max(image.width, image.height));
1301
+ const canvas = new OffscreenCanvas(Math.round(image.width * scale), Math.round(image.height * scale));
1302
+ const context = canvas.getContext("2d");
1303
+ if (!context) throw new HostError("failed", "The browser couldn't draw the photo");
1304
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
1305
+ image.close();
1306
+ return canvas.convertToBlob({
1307
+ type: "image/jpeg",
1308
+ quality: JPEG_QUALITY
1309
+ });
1310
+ }
1311
+ function keepOnDevServer(kind, params, signal) {
1312
+ return callDevServer(`${MEDIA_PATH}${kind}`, params, signal);
1313
+ }
1314
+ function base64(blob) {
1315
+ return new Promise((resolve, reject) => {
1316
+ const reader = new FileReader();
1317
+ reader.addEventListener("load", () => {
1318
+ const url = reader.result;
1319
+ resolve(url.slice(url.indexOf(",") + 1));
1320
+ });
1321
+ reader.addEventListener("error", () => reject(new HostError("failed", "The file couldn't be read")));
1322
+ reader.readAsDataURL(blob);
1323
+ });
1324
+ }
1006
1325
  //#endregion
1007
1326
  //#region src/client/dash/system.ts
1008
1327
  const SHAKE_DURATION_MS = 300;
@@ -1059,51 +1378,55 @@
1059
1378
  });
1060
1379
  }
1061
1380
  //#endregion
1062
- //#region src/client/dom.ts
1063
- function element(selector) {
1064
- const node = document.querySelector(selector);
1065
- if (!node) throw new Error(`The simulator page is missing ${selector}`);
1066
- return node;
1067
- }
1068
- function createElement(tag, className, text) {
1069
- const node = document.createElement(tag);
1070
- node.className = className;
1071
- if (text !== void 0) node.textContent = text;
1072
- return node;
1381
+ //#region src/client/dash/pickers.ts
1382
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
1383
+ const TIME = /^\d{2}:\d{2}$/;
1384
+ function pickDate(phone, note, params, signal) {
1385
+ const { title, value, min, max } = params ?? {};
1386
+ for (const [name, date] of Object.entries({
1387
+ value,
1388
+ min,
1389
+ max
1390
+ })) if (date !== void 0 && !DATE.test(date)) throw new HostError("invalid", `"${name}" must be a date such as "2026-10-24", not "${date}"`);
1391
+ if (min && max && min > max) throw new HostError("invalid", `"min" (${min}) is after "max" (${max})`);
1392
+ return pick(phone, note, signal, {
1393
+ type: "date",
1394
+ title: title ?? "Choose a date",
1395
+ value,
1396
+ min,
1397
+ max
1398
+ });
1073
1399
  }
1074
- //#endregion
1075
- //#region src/client/dash/ui.ts
1076
- const DEFAULT_ACTIONS = [{
1077
- id: "ok",
1078
- label: "OK"
1079
- }];
1080
- function alert(phone, options, signal) {
1081
- return new Promise((resolve, reject) => {
1082
- const layer = createElement("div", "dialog-layer");
1083
- const dialog = createElement("div", "dialog");
1084
- dialog.setAttribute("role", "alertdialog");
1085
- const body = createElement("div", "dialog-body");
1086
- body.append(createElement("div", "dialog-title", options.title));
1087
- if (options.message) body.append(createElement("div", "dialog-message", options.message));
1088
- const actions = options.actions ?? DEFAULT_ACTIONS;
1089
- const buttons = createElement("div", actions.length === 2 ? "dialog-actions" : "dialog-actions stacked");
1090
- for (const action of actions) {
1091
- const button = createElement("button", action.role ?? "default", action.label);
1092
- button.addEventListener("click", () => {
1093
- layer.remove();
1094
- resolve({ action: action.id });
1095
- });
1096
- buttons.append(button);
1097
- }
1098
- dialog.append(body, buttons);
1099
- layer.append(dialog);
1100
- signal.addEventListener("abort", () => {
1101
- layer.remove();
1102
- reject(new HostError("cancelled", "The alert was dismissed"));
1103
- });
1104
- phone.append(layer);
1400
+ function pickTime(phone, note, params, signal) {
1401
+ const { title, value } = params ?? {};
1402
+ if (value !== void 0 && !TIME.test(value)) throw new HostError("invalid", `"value" must be a time such as "19:30", not "${value}"`);
1403
+ return pick(phone, note, signal, {
1404
+ type: "time",
1405
+ title: title ?? "Choose a time",
1406
+ value
1105
1407
  });
1106
1408
  }
1409
+ async function pick(phone, note, signal, { type, title, value, min, max }) {
1410
+ const field = createElement("input", "dialog-input");
1411
+ field.type = type;
1412
+ field.value = value ?? "";
1413
+ field.min = min ?? "";
1414
+ field.max = max ?? "";
1415
+ const { action } = await alert(phone, {
1416
+ title,
1417
+ actions: [{
1418
+ id: "cancel",
1419
+ label: "Cancel",
1420
+ role: "cancel"
1421
+ }, {
1422
+ id: "done",
1423
+ label: "Done"
1424
+ }]
1425
+ }, signal, field);
1426
+ if (action !== "done" || field.value === "") throw new HostError("cancelled", `No ${type} was chosen`);
1427
+ note(`${type}: ${field.value}`);
1428
+ return field.value;
1429
+ }
1107
1430
  //#endregion
1108
1431
  //#region src/client/dash/index.ts
1109
1432
  const HTTP_METHODS = [
@@ -1141,12 +1464,26 @@
1141
1464
  function dashHandlers(phone, note) {
1142
1465
  return {
1143
1466
  "dash.ui.alert": ({ params, signal }) => alert(phone, params, signal),
1467
+ "dash.ui.pickDate": ({ params, signal }) => pickDate(phone, note, params, signal),
1468
+ "dash.ui.pickTime": ({ params, signal }) => pickTime(phone, note, params, signal),
1144
1469
  "dash.system.vibrate": ({ params }) => vibrate(phone, note, params),
1145
1470
  "dash.system.torch": ({ params }) => torch(phone, note, params.state),
1146
1471
  "dash.system.copyText": ({ params }) => copyText(note, params.text),
1147
1472
  "dash.system.share": ({ params }) => share(note, params),
1148
1473
  "dash.system.openUrl": ({ params }) => openUrl(note, params.url),
1149
1474
  "dash.system.delay": ({ params, signal }) => delay(params.milliseconds, signal),
1475
+ "dash.media.pickPhoto": {
1476
+ permission: photoPermission,
1477
+ handle: ({ params, signal }) => pickPhoto(phone, note, params, signal)
1478
+ },
1479
+ "dash.media.scanCode": {
1480
+ permission: "camera",
1481
+ handle: ({ signal }) => scanCode(phone, note, signal)
1482
+ },
1483
+ "dash.storage.file.pick": {
1484
+ permission: "files",
1485
+ handle: ({ params, signal }) => pickFile(phone, note, params, signal)
1486
+ },
1150
1487
  ...devServerHandlers("dash.network.http", HTTP_PATH, HTTP_METHODS),
1151
1488
  ...devServerHandlers("dash.storage.file", FILE_PATH, FILE_OPERATIONS),
1152
1489
  ...devServerHandlers("dash.storage.cache", CACHE_PATH, CACHE_OPERATIONS),
@@ -1158,7 +1495,12 @@
1158
1495
  }
1159
1496
  //#endregion
1160
1497
  //#region src/client/dash/permissions.ts
1161
- const PERMISSION_SUMMARIES = { account: "see your Dash account: your name, and your email or phone" };
1498
+ const PERMISSION_SUMMARIES = {
1499
+ account: "see your Dash account: your name, and your email or phone",
1500
+ camera: "use your camera to take photos and scan codes",
1501
+ photoLibrary: "use photos you choose from your library",
1502
+ files: "use files you choose from your phone"
1503
+ };
1162
1504
  async function askPermission(phone, note, request, signal) {
1163
1505
  const { action } = await alert(phone, {
1164
1506
  title: `Allow “${request.dotName}” to ${PERMISSION_SUMMARIES[request.permission]}?`,
@@ -1205,8 +1547,11 @@
1205
1547
  //#endregion
1206
1548
  //#region src/client/simulator.ts
1207
1549
  const MAX_LOG_ENTRIES = 500;
1550
+ const MESSAGES_SHOWN_KEY = "dot-simulator-messages";
1208
1551
  const screen = element("#screen");
1209
1552
  const log = element("#log");
1553
+ const messagesPanel = element("#messages-panel");
1554
+ const messagesButton = element("#messages");
1210
1555
  const errorBanner = element("#error");
1211
1556
  const phone = element(".phone");
1212
1557
  const schemeSelect = element("#scheme");
@@ -1369,14 +1714,34 @@
1369
1714
  hotReload();
1370
1715
  });
1371
1716
  events.addEventListener("build-error", (event) => showError(String(event.data)));
1717
+ function showMessages(shown) {
1718
+ messagesPanel.hidden = !shown;
1719
+ messagesButton.setAttribute("aria-pressed", String(shown));
1720
+ }
1721
+ function toggleMessages() {
1722
+ const shown = messagesPanel.hidden;
1723
+ showMessages(shown);
1724
+ try {
1725
+ localStorage.setItem(MESSAGES_SHOWN_KEY, String(shown));
1726
+ } catch {}
1727
+ }
1728
+ function messagesShownLastTime() {
1729
+ try {
1730
+ return localStorage.getItem(MESSAGES_SHOWN_KEY) !== "false";
1731
+ } catch {
1732
+ return true;
1733
+ }
1734
+ }
1372
1735
  element("#reload").addEventListener("click", () => restart());
1373
1736
  element("#back").addEventListener("click", goBack);
1374
1737
  navigationBack.addEventListener("click", goBack);
1375
1738
  element("#clear").addEventListener("click", () => log.replaceChildren());
1739
+ messagesButton.addEventListener("click", toggleMessages);
1376
1740
  schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
1377
1741
  browserDark.addEventListener("change", () => changeScheme(browserScheme()));
1378
1742
  document.addEventListener("visibilitychange", syncSystemContext);
1379
1743
  changeScheme(browserScheme());
1744
+ showMessages(messagesShownLastTime());
1380
1745
  restart();
1381
1746
  //#endregion
1382
1747
  })();