@monimesl/dot-simulator 0.1.0-alpha.3 → 0.1.0-alpha.5

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,6 +15,21 @@
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
+ });
18
33
  const Button = defineComponent({
19
34
  name: "Button",
20
35
  props: {
@@ -25,15 +40,17 @@
25
40
  events: ["click"],
26
41
  children: "text"
27
42
  });
28
- const Divider = defineComponent({
29
- name: "Divider",
30
- children: "none"
31
- });
32
43
  const SIZES = [
33
44
  "small",
34
45
  "medium",
35
46
  "large"
36
47
  ];
48
+ const SPACES = [
49
+ "none",
50
+ "small",
51
+ "medium",
52
+ "large"
53
+ ];
37
54
  const TONES = [
38
55
  "default",
39
56
  "muted",
@@ -111,6 +128,58 @@
111
128
  "water",
112
129
  "wifi"
113
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
+ });
114
183
  const Spinner = defineComponent({
115
184
  name: "Spinner",
116
185
  props: { size: SIZES },
@@ -138,6 +207,25 @@
138
207
  name: "Footer",
139
208
  children: "elements"
140
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
+ });
141
229
  const Icon = defineComponent({
142
230
  name: "Icon",
143
231
  props: {
@@ -190,6 +278,7 @@
190
278
  });
191
279
  const List = defineComponent({
192
280
  name: "List",
281
+ props: { size: ["sm", "md"] },
193
282
  children: "elements"
194
283
  });
195
284
  const ListItem = defineComponent({
@@ -208,7 +297,8 @@
208
297
  name: "Sheet",
209
298
  props: {
210
299
  open: "boolean",
211
- title: "string"
300
+ title: "string",
301
+ showCloseIcon: "boolean"
212
302
  },
213
303
  events: ["close"],
214
304
  children: "elements"
@@ -223,6 +313,15 @@
223
313
  events: ["hide"],
224
314
  children: "none"
225
315
  });
316
+ const QRCode = defineComponent({
317
+ name: "QRCode",
318
+ props: {
319
+ value: "string",
320
+ size: "number",
321
+ label: "string"
322
+ },
323
+ children: "none"
324
+ });
226
325
  const Select = defineComponent({
227
326
  name: "Select",
228
327
  props: {
@@ -257,6 +356,24 @@
257
356
  },
258
357
  children: "none"
259
358
  });
359
+ const Tabs = defineComponent({
360
+ name: "Tabs",
361
+ props: {
362
+ value: "string",
363
+ variant: ["underline", "segmented"]
364
+ },
365
+ events: ["change"],
366
+ children: "elements"
367
+ });
368
+ const Tab = defineComponent({
369
+ name: "Tab",
370
+ props: {
371
+ value: "string",
372
+ label: "string",
373
+ badge: "number"
374
+ },
375
+ children: "none"
376
+ });
260
377
  const Text = defineComponent({
261
378
  name: "Text",
262
379
  props: {
@@ -326,27 +443,19 @@
326
443
  events: ["change"],
327
444
  children: "elements"
328
445
  });
329
- const Radio = defineComponent({
330
- name: "Radio",
331
- props: {
332
- value: "string",
333
- label: "string",
334
- disabled: "boolean"
335
- },
336
- children: "none"
337
- });
338
- const SPACE = [
339
- "none",
340
- "small",
341
- "medium",
342
- "large"
343
- ];
344
446
  const builtinComponents = {
447
+ AmountInput,
345
448
  Badge,
346
449
  Button,
450
+ Carousel,
347
451
  Checkbox,
452
+ Chip,
453
+ CodeInput,
348
454
  Divider,
455
+ EmptyState,
349
456
  Footer,
457
+ Grid,
458
+ GridItem,
350
459
  Icon,
351
460
  Image,
352
461
  Input,
@@ -355,20 +464,31 @@
355
464
  Notice,
356
465
  Option,
357
466
  ProgressBar,
358
- Radio,
467
+ QRCode,
468
+ Radio: defineComponent({
469
+ name: "Radio",
470
+ props: {
471
+ value: "string",
472
+ label: "string",
473
+ disabled: "boolean"
474
+ },
475
+ children: "none"
476
+ }),
359
477
  RadioGroup,
360
478
  Select,
361
479
  Sheet,
362
480
  Skeleton,
363
481
  Spinner,
364
482
  Switch,
483
+ Tab,
484
+ Tabs,
365
485
  Text,
366
486
  TextArea,
367
487
  Toast,
368
488
  View: defineComponent({
369
489
  name: "View",
370
490
  props: {
371
- gap: SPACE,
491
+ gap: SPACES,
372
492
  direction: ["column", "row"],
373
493
  align: [
374
494
  "stretch",
@@ -384,11 +504,11 @@
384
504
  ],
385
505
  wrap: "boolean",
386
506
  grow: "boolean",
387
- padding: SPACE,
388
- paddingTop: SPACE,
389
- paddingStart: SPACE,
390
- paddingEnd: SPACE,
391
- paddingBottom: SPACE,
507
+ padding: SPACES,
508
+ paddingTop: SPACES,
509
+ paddingStart: SPACES,
510
+ paddingEnd: SPACES,
511
+ paddingBottom: SPACES,
392
512
  variant: ["plain", "card"]
393
513
  },
394
514
  events: ["tap"],
@@ -647,7 +767,7 @@
647
767
  };
648
768
  //#endregion
649
769
  //#region src/host.ts
650
- function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe }) {
770
+ function startHost({ logic, ui, manifest, dashVersion, minRuntimeVersion, systemContext, handlers = {}, askPermission, observe, restorePages = [], onBarsChange }) {
651
771
  checkCompatibility(manifest, {
652
772
  dashVersion,
653
773
  minRuntimeVersion
@@ -667,6 +787,18 @@
667
787
  ui.post(message);
668
788
  logic.post(message);
669
789
  };
790
+ const barsByInstance = /* @__PURE__ */ new Map();
791
+ const showBars = () => {
792
+ const top = stack.top;
793
+ const bars = top && barsByInstance.get(top.instanceId);
794
+ onBarsChange?.({
795
+ title: bars?.navigationBar.title ?? manifest.name,
796
+ navigationBarHidden: bars?.navigationBar.hidden ?? false,
797
+ navigationBarColor: bars?.navigationBar.backgroundColor,
798
+ navigationBarTextColor: bars?.navigationBar.textColor,
799
+ tabBarHidden: bars?.tabBar.hidden ?? false
800
+ }, stack.pages);
801
+ };
670
802
  const syncStack = () => {
671
803
  const message = {
672
804
  type: "syncStack",
@@ -676,6 +808,9 @@
676
808
  }
677
809
  };
678
810
  broadcast(message);
811
+ const open = new Set(stack.pages.map((entry) => entry.instanceId));
812
+ for (const instanceId of barsByInstance.keys()) if (!open.has(instanceId)) barsByInstance.delete(instanceId);
813
+ showBars();
679
814
  };
680
815
  const syncSystemContext = () => {
681
816
  broadcast({
@@ -691,13 +826,40 @@
691
826
  if (stack.top !== previousTop) calls.dismissUI();
692
827
  return true;
693
828
  };
829
+ const openFirstPages = () => {
830
+ const [first, ...rest] = restorePages.filter((entry) => manifest.pages.includes(entry.page));
831
+ if (!first) {
832
+ navigate({
833
+ action: "reset",
834
+ page: manifest.entry,
835
+ options: {}
836
+ });
837
+ return;
838
+ }
839
+ stack.apply({
840
+ action: "reset",
841
+ page: first.page,
842
+ options: first.options
843
+ });
844
+ for (const entry of rest) stack.apply({
845
+ action: "push",
846
+ page: entry.page,
847
+ options: entry.options
848
+ });
849
+ syncStack();
850
+ };
694
851
  const navigateFromPage = ({ from, ...navigation }) => {
695
852
  if (from === stack.top?.instanceId) navigate(navigation);
696
853
  };
697
854
  const hostMessageHandlers = /* @__PURE__ */ new Map([
698
855
  ["navigate", (payload) => navigateFromPage(payload)],
699
856
  ["hostCall", (payload) => calls.run(payload)],
700
- ["hostCancel", (payload) => calls.cancel(payload.id)]
857
+ ["hostCancel", (payload) => calls.cancel(payload.id)],
858
+ ["pageBars", (payload) => {
859
+ const { instanceId, ...bars } = payload;
860
+ barsByInstance.set(instanceId, bars);
861
+ showBars();
862
+ }]
701
863
  ]);
702
864
  const stopService = logic.listen((raw) => {
703
865
  const message = raw;
@@ -716,14 +878,13 @@
716
878
  logic.post(message);
717
879
  if (message.type === "connect") {
718
880
  syncSystemContext();
719
- navigate({
720
- action: "reset",
721
- page: manifest.entry,
722
- options: {}
723
- });
881
+ openFirstPages();
724
882
  }
725
883
  });
726
884
  return {
885
+ get pages() {
886
+ return stack.pages;
887
+ },
727
888
  back: () => navigate({
728
889
  action: "back",
729
890
  delta: 1
@@ -940,11 +1101,20 @@
940
1101
  const log = element("#log");
941
1102
  const errorBanner = element("#error");
942
1103
  const phone = element(".phone");
1104
+ const platformSelect = element("#platform");
1105
+ const inputModeSelect = element("#input-mode");
943
1106
  const schemeSelect = element("#scheme");
1107
+ const navigationBar = element(".navigation-bar");
1108
+ const navigationTitle = element("#navigation-title");
1109
+ const navigationBack = element("#navigation-back");
1110
+ const tabBar = element(".tab-bar");
944
1111
  const browserDark = matchMedia("(prefers-color-scheme: dark)");
1112
+ const COARSE_POINTER_MEDIA = /\(\s*(?:any-)?pointer\s*:\s*coarse\s*\)/gi;
1113
+ const EMULATED_TOUCH_STYLE = "data-dot-simulator-touch";
945
1114
  let runningApp = null;
946
1115
  let launchCount = 0;
947
- async function launch() {
1116
+ let hotReloads = 0;
1117
+ async function launch(restorePages = []) {
948
1118
  stopRunningApp();
949
1119
  launchCount += 1;
950
1120
  const thisLaunch = launchCount;
@@ -973,7 +1143,9 @@
973
1143
  systemContext: systemContext(),
974
1144
  handlers: dashHandlers(phone, note),
975
1145
  askPermission: (request, signal) => askPermission(phone, note, request, signal),
976
- observe: record
1146
+ observe: record,
1147
+ restorePages,
1148
+ onBarsChange: showBars
977
1149
  });
978
1150
  worker.addEventListener("error", (event) => showError(`Service crashed: ${event.message}`));
979
1151
  screen.src = WEBVIEW_URL;
@@ -988,6 +1160,18 @@
988
1160
  runningApp.worker.terminate();
989
1161
  runningApp = null;
990
1162
  }
1163
+ function showBars(bars, pages) {
1164
+ navigationBar.hidden = bars.navigationBarHidden;
1165
+ navigationTitle.textContent = bars.title;
1166
+ navigationBack.hidden = pages.length < 2;
1167
+ setBarColor("--bar", bars.navigationBarColor);
1168
+ setBarColor("--bar-text", bars.navigationBarTextColor);
1169
+ tabBar.hidden = bars.tabBarHidden;
1170
+ }
1171
+ function setBarColor(property, color) {
1172
+ if (color) phone.style.setProperty(property, color);
1173
+ else phone.style.removeProperty(property);
1174
+ }
991
1175
  function goBack() {
992
1176
  if (runningApp && !runningApp.host.back()) note("back on the first page: a device would close the dot");
993
1177
  }
@@ -1015,17 +1199,32 @@
1015
1199
  while (log.childElementCount > MAX_LOG_ENTRIES) log.firstElementChild?.remove();
1016
1200
  log.scrollTop = log.scrollHeight;
1017
1201
  }
1018
- function syncLaunch() {
1019
- launch().catch((error) => {
1202
+ function syncLaunch(restorePages = []) {
1203
+ launch(restorePages).catch((error) => {
1020
1204
  showError(`Could not start the dot: ${String(error)}`);
1021
1205
  });
1022
1206
  }
1207
+ function relaunch() {
1208
+ syncLaunch(runningApp?.host.pages ?? []);
1209
+ }
1210
+ function hotReload() {
1211
+ if (!runningApp) {
1212
+ syncLaunch();
1213
+ return;
1214
+ }
1215
+ hotReloads += 1;
1216
+ const reload = { dotHotReload: hotReloads };
1217
+ runningApp.worker.postMessage(reload);
1218
+ screen.contentWindow?.postMessage(reload, location.origin);
1219
+ note("dot updated in place");
1220
+ }
1023
1221
  function showError(message) {
1024
1222
  errorBanner.hidden = message === null;
1025
1223
  errorBanner.textContent = message ?? "";
1026
1224
  }
1027
1225
  function systemContext() {
1028
1226
  return {
1227
+ platform: platformSelect.value,
1029
1228
  scheme: schemeSelect.value,
1030
1229
  appState: appState()
1031
1230
  };
@@ -1042,21 +1241,81 @@
1042
1241
  phone.dataset.scheme = scheme;
1043
1242
  runningApp?.host.setSystemContext(systemContext());
1044
1243
  }
1244
+ function changeInputMode(mode) {
1245
+ inputModeSelect.value = mode;
1246
+ phone.dataset.inputMode = mode;
1247
+ applyInputMode();
1248
+ }
1249
+ function applyInputMode() {
1250
+ const document = screen.contentDocument;
1251
+ if (!document) return;
1252
+ document.documentElement.dataset.dotSimulatorInput = inputModeSelect.value;
1253
+ document.querySelector(`style[${EMULATED_TOUCH_STYLE}]`)?.remove();
1254
+ if (inputModeSelect.value !== "touch") return;
1255
+ const css = coarsePointerCss(document);
1256
+ if (!css) return;
1257
+ const style = document.createElement("style");
1258
+ style.setAttribute(EMULATED_TOUCH_STYLE, "");
1259
+ style.textContent = css;
1260
+ document.head.append(style);
1261
+ }
1262
+ function coarsePointerCss(document) {
1263
+ let css = "";
1264
+ for (const sheet of document.styleSheets) try {
1265
+ css += coarsePointerRules(sheet.cssRules);
1266
+ } catch {}
1267
+ return css;
1268
+ }
1269
+ function coarsePointerRules(rules) {
1270
+ let css = "";
1271
+ for (const rule of rules) {
1272
+ const nestedRules = rule.cssRules;
1273
+ if (!nestedRules) continue;
1274
+ if (rule.type === CSSRule.MEDIA_RULE) {
1275
+ const media = rule;
1276
+ COARSE_POINTER_MEDIA.lastIndex = 0;
1277
+ if (COARSE_POINTER_MEDIA.test(media.conditionText)) {
1278
+ COARSE_POINTER_MEDIA.lastIndex = 0;
1279
+ const condition = media.conditionText.replace(COARSE_POINTER_MEDIA, "(min-width: 0px)");
1280
+ const contents = Array.from(media.cssRules, (child) => child.cssText).join("\n");
1281
+ css += condition === "(min-width: 0px)" ? contents : `@media ${condition}{${contents}}`;
1282
+ continue;
1283
+ }
1284
+ }
1285
+ const contents = coarsePointerRules(nestedRules);
1286
+ if (!contents) continue;
1287
+ const openingBrace = rule.cssText.indexOf("{");
1288
+ if (openingBrace !== -1) css += `${rule.cssText.slice(0, openingBrace)}{${contents}}`;
1289
+ }
1290
+ return css;
1291
+ }
1045
1292
  const events = new EventSource(EVENTS_PATH);
1046
1293
  events.addEventListener("reload", () => {
1047
1294
  showError(null);
1048
- syncLaunch();
1295
+ relaunch();
1296
+ });
1297
+ events.addEventListener("hot-reload", () => {
1298
+ showError(null);
1299
+ hotReload();
1049
1300
  });
1050
1301
  events.addEventListener("build-error", (event) => showError(String(event.data)));
1051
- element("#reload").addEventListener("click", syncLaunch);
1302
+ element("#reload").addEventListener("click", () => syncLaunch());
1052
1303
  element("#back").addEventListener("click", goBack);
1304
+ navigationBack.addEventListener("click", goBack);
1053
1305
  element("#clear").addEventListener("click", () => log.replaceChildren());
1054
1306
  schemeSelect.addEventListener("change", () => changeScheme(schemeSelect.value));
1307
+ inputModeSelect.addEventListener("change", () => changeInputMode(inputModeSelect.value));
1308
+ platformSelect.addEventListener("change", () => {
1309
+ phone.dataset.platform = platformSelect.value;
1310
+ runningApp?.host.setSystemContext(systemContext());
1311
+ });
1312
+ screen.addEventListener("load", applyInputMode);
1055
1313
  browserDark.addEventListener("change", () => changeScheme(browserScheme()));
1056
1314
  document.addEventListener("visibilitychange", () => {
1057
1315
  runningApp?.host.setSystemContext(systemContext());
1058
1316
  });
1059
1317
  changeScheme(browserScheme());
1318
+ changeInputMode("touch");
1060
1319
  syncLaunch();
1061
1320
  //#endregion
1062
1321
  })();