@metricinsights/pp-dev 1.5.0-beta.15 → 1.5.0-beta.17

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.
Files changed (61) hide show
  1. package/README.md +3 -2
  2. package/dist/README.md +3 -2
  3. package/dist/cjs/auth.provider-CejYwjcG.js +2 -0
  4. package/dist/cjs/auth.provider-CejYwjcG.js.map +1 -0
  5. package/dist/cjs/cli.js +1 -1
  6. package/dist/cjs/cli.js.map +1 -1
  7. package/dist/cjs/index-Cev20Ede.js +2 -0
  8. package/dist/cjs/index-Cev20Ede.js.map +1 -0
  9. package/dist/cjs/index.js +1 -1
  10. package/dist/cjs/{migrate-C2YzTFrM.js → migrate-MxrkBSEh.js} +2 -2
  11. package/dist/cjs/{migrate-C2YzTFrM.js.map → migrate-MxrkBSEh.js.map} +1 -1
  12. package/dist/cjs/package.json +1 -1
  13. package/dist/cjs/plugin.js +1 -1
  14. package/dist/cjs/plugin.js.map +1 -1
  15. package/dist/cjs/variables-editor-DW2WJZHw.js +106 -0
  16. package/dist/cjs/variables-editor-DW2WJZHw.js.map +1 -0
  17. package/dist/cjs/version-manifest-DjLJQWWa.js +2 -0
  18. package/dist/cjs/version-manifest-DjLJQWWa.js.map +1 -0
  19. package/dist/cjs/version-plugin-sUHrN2Jo.js +2 -0
  20. package/dist/cjs/version-plugin-sUHrN2Jo.js.map +1 -0
  21. package/dist/client/client.js +160 -61
  22. package/dist/client/client.js.map +1 -1
  23. package/dist/esm/auth.provider-BijrVDxp.js +2 -0
  24. package/dist/esm/auth.provider-BijrVDxp.js.map +1 -0
  25. package/dist/esm/cli.js +1 -1
  26. package/dist/esm/cli.js.map +1 -1
  27. package/dist/esm/index-CL8_j3ER.js +2 -0
  28. package/dist/esm/index-CL8_j3ER.js.map +1 -0
  29. package/dist/esm/index.js +1 -1
  30. package/dist/esm/{migrate-SXsPvdfV.js → migrate-B0hgQCl_.js} +2 -2
  31. package/dist/esm/{migrate-SXsPvdfV.js.map → migrate-B0hgQCl_.js.map} +1 -1
  32. package/dist/esm/package.json +1 -1
  33. package/dist/esm/plugin.js +1 -1
  34. package/dist/esm/plugin.js.map +1 -1
  35. package/dist/esm/{variables-editor-CWbcJAdI.js → variables-editor-w314pia8.js} +3 -3
  36. package/dist/esm/variables-editor-w314pia8.js.map +1 -0
  37. package/dist/esm/version-manifest-1KdgXTW2.js +2 -0
  38. package/dist/esm/version-manifest-1KdgXTW2.js.map +1 -0
  39. package/dist/esm/version-plugin-BExwn1e0.js +2 -0
  40. package/dist/esm/version-plugin-BExwn1e0.js.map +1 -0
  41. package/dist/types/index.d.ts +7 -6
  42. package/package.json +1 -1
  43. package/dist/cjs/auth.provider-DaLYQg0T.js +0 -2
  44. package/dist/cjs/auth.provider-DaLYQg0T.js.map +0 -1
  45. package/dist/cjs/index-BcjhRmxz.js +0 -2
  46. package/dist/cjs/index-BcjhRmxz.js.map +0 -1
  47. package/dist/cjs/variables-editor-BHiWb4-B.js +0 -106
  48. package/dist/cjs/variables-editor-BHiWb4-B.js.map +0 -1
  49. package/dist/cjs/version-manifest-Dq5x7D15.js +0 -2
  50. package/dist/cjs/version-manifest-Dq5x7D15.js.map +0 -1
  51. package/dist/cjs/version-plugin-C-8273qp.js +0 -2
  52. package/dist/cjs/version-plugin-C-8273qp.js.map +0 -1
  53. package/dist/esm/auth.provider-CaBF6-T3.js +0 -2
  54. package/dist/esm/auth.provider-CaBF6-T3.js.map +0 -1
  55. package/dist/esm/index-DewOudJ0.js +0 -2
  56. package/dist/esm/index-DewOudJ0.js.map +0 -1
  57. package/dist/esm/variables-editor-CWbcJAdI.js.map +0 -1
  58. package/dist/esm/version-manifest-C1RlO_jm.js +0 -2
  59. package/dist/esm/version-manifest-C1RlO_jm.js.map +0 -1
  60. package/dist/esm/version-plugin-aWKAiJZe.js +0 -2
  61. package/dist/esm/version-plugin-aWKAiJZe.js.map +0 -1
@@ -1,17 +1,13 @@
1
1
  /**
2
- * Minimal `import.meta.hot`-compatible context for environments without Vite HMR
3
- * (e.g. the `pp-dev next` server). The dev-panel client only uses `hot.on()` and
4
- * `hot.send()`; this shim implements those over a raw WebSocket using the same
5
- * custom-event wire shape Vite uses: `{ type: 'custom', event, data }`.
6
- *
7
- * Used only as a fallback: when `import.meta.hot` exists (Vite), that is used
8
- * instead and this code never runs.
9
- */
10
- /**
11
- * WebSocket path the pp-dev Next.js server listens on for dev-panel messages.
12
- * Kept in sync with `PP_DEV_HMR_WS_PATH` in `src/constants.ts` (server side).
2
+ * The WebSocket messages between the dev server and the dev panel, for both sides: the server
3
+ * (`src/lib/client.service.ts`, `src/lib/pp-ws-server.ts`) and the panel (`src/client/**`) import
4
+ * them from here, so they can't drift apart. No Node or DOM imports: the panel bundle includes it.
13
5
  */
6
+ /** WebSocket path the `pp-dev next` server listens on for dev-panel messages. */
14
7
  const PP_DEV_HMR_WS_PATH = '/@pp-dev-hmr';
8
+ /** `template:sync:response` code for a sync requested while another one runs. */
9
+ const SYNC_IN_PROGRESS_CODE = 'SYNC_IN_PROGRESS';
10
+
15
11
  const MAX_OUTBOX_SIZE = 50;
16
12
  function resolveWebSocketUrl() {
17
13
  const { protocol, host } = window.location;
@@ -23,6 +19,8 @@ function createPPDevHotContext() {
23
19
  /** Outgoing messages queued while the socket is not OPEN; flushed on connect. */
24
20
  const outbox = [];
25
21
  let socket = null;
22
+ /** The connection state last reported to the panel, so each change is reported once. */
23
+ let reported = null;
26
24
  let reconnectTimer = null;
27
25
  let reconnectDelay = 1_000;
28
26
  const MAX_RECONNECT_DELAY = 10_000;
@@ -58,10 +56,9 @@ function createPPDevHotContext() {
58
56
  scheduleReconnect();
59
57
  return;
60
58
  }
61
- let opened = false;
62
59
  socket.addEventListener('open', () => {
63
- opened = true;
64
60
  reconnectDelay = 1_000;
61
+ reported = 'connected';
65
62
  dispatch('vite:ws:connect', {});
66
63
  flushOutbox();
67
64
  });
@@ -80,9 +77,11 @@ function createPPDevHotContext() {
80
77
  socket.addEventListener('close', () => {
81
78
  socket = null;
82
79
  // Same events as Vite's own HMR client, so the panel can tell a lost connection apart
83
- // (e.g. a sync whose response will never arrive). Only for a socket that was open: a
84
- // failed reconnect attempt is not a new disconnect.
85
- if (opened) {
80
+ // (e.g. a sync whose response will never arrive, or a click that can't be sent). Once per
81
+ // loss: each failed reconnect attempt is not a new one, but a first connection that never
82
+ // opened is.
83
+ if (reported !== 'disconnected') {
84
+ reported = 'disconnected';
86
85
  dispatch('vite:ws:disconnect', {});
87
86
  }
88
87
  scheduleReconnect();
@@ -128,6 +127,9 @@ function createPPDevHotContext() {
128
127
  outbox.push(payload);
129
128
  }
130
129
  },
130
+ // Not part of Vite's API: lets the panel tell an open connection from one still connecting, whose
131
+ // queued messages would go out whenever it opens (see `src/client/sync-button.ts`).
132
+ isConnected: () => socket?.readyState === WebSocket.OPEN,
131
133
  // Unused by the dev-panel client; provided as no-ops to satisfy the shape.
132
134
  accept: noop,
133
135
  acceptExports: noop,
@@ -653,6 +655,37 @@ function initPanelSettings($panel, controller, hooks) {
653
655
  });
654
656
  }
655
657
 
658
+ /**
659
+ * The rules for a saved user's name, which is also its host (`<name>.localhost`), for both sides:
660
+ * the server checks them (`src/lib/user-registry.ts`) and the dev panel's Users popover
661
+ * (`src/client/users-panel.ts`) checks them as you type. No Node or DOM imports: the panel bundle
662
+ * includes it.
663
+ */
664
+ /** Longest user name; well under the 63 characters a DNS label allows. */
665
+ const MAX_USER_NAME_LENGTH = 32;
666
+ /**
667
+ * A DNS label: lowercase latin letters, digits and `-`, starting with a letter and not ending with
668
+ * `-`. Also used as an input's `pattern`, which browsers compile with the `v` flag: there an
669
+ * unescaped `-` in a class is a syntax error, and the whole pattern was silently ignored.
670
+ */
671
+ const USER_NAME_PATTERN = String.raw `[a-z](?:[a-z0-9\-]*[a-z0-9])?`;
672
+ const USER_NAME_REGEXP = new RegExp(`^(?:${USER_NAME_PATTERN})$`);
673
+ /** The suffix of a user's host: `viewer` works on `viewer.localhost`. */
674
+ const USER_HOST_SUFFIX = '.localhost';
675
+ /** Why `name` can't be a user name, or `undefined` if it can. */
676
+ function getUserNameError(name) {
677
+ if (!name) {
678
+ return 'The name is required';
679
+ }
680
+ if (name.length > MAX_USER_NAME_LENGTH) {
681
+ return `The name must be at most ${MAX_USER_NAME_LENGTH} characters long`;
682
+ }
683
+ if (!USER_NAME_REGEXP.test(name)) {
684
+ return 'The name may contain only lowercase latin letters, digits and "-", must start with a letter and must not end with "-"';
685
+ }
686
+ return undefined;
687
+ }
688
+
656
689
  /**
657
690
  * The dev panel's "Users" popover (PP-4597): the user of this tab, the saved users (open as, log
658
691
  * in, make default, remove) and adding a user by API token or by username + password. Each user
@@ -660,7 +693,8 @@ function initPanelSettings($panel, controller, hooks) {
660
693
  */
661
694
  const USERS_API = '/@api/users';
662
695
  const REFRESH_INTERVAL = 3000;
663
- const USER_HOST_REGEXP = /^([a-z](?:[a-z0-9-]*[a-z0-9])?)\.localhost$/;
696
+ // The server's name rules (src/shared/user-names.ts), not a copy of them.
697
+ const USER_HOST_REGEXP = new RegExp(`^(${USER_NAME_PATTERN})${USER_HOST_SUFFIX.replace('.', '\\.')}$`);
664
698
  /** Marks the user this tab works as, apart from the radio, which picks localhost's user. */
665
699
  const THIS_TAB_BADGE = '<span class="pp-dev-info__users-badge">this tab</span>';
666
700
  const STATE_LABELS = {
@@ -832,7 +866,7 @@ function initUsersPanel($panel, hooks) {
832
866
  const name = escapeHtml(user.name);
833
867
  if (user.kind === 'token' && user.name === editing) {
834
868
  return `<form class="pp-dev-info__users-row pp-dev-info__users-edit" data-user="${name}">
835
- <input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="32"
869
+ <input class="pp-dev-info__users-input pp-dev-info__users-edit-name" value="${name}" maxlength="${MAX_USER_NAME_LENGTH}" pattern="${USER_NAME_PATTERN}"
836
870
  autocomplete="off" spellcheck="false" aria-label="Name" data-focus-key="${name}:edit-name"
837
871
  title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
838
872
  <input class="pp-dev-info__users-input pp-dev-info__users-edit-token" type="password" autocomplete="off"
@@ -877,6 +911,9 @@ function initUsersPanel($panel, hooks) {
877
911
  $open.addEventListener('click', (ev) => {
878
912
  ev.preventDefault();
879
913
  const target = $open.dataset.open;
914
+ if (!target) {
915
+ return;
916
+ }
880
917
  window.open($open.dataset.raw ? target : samePageOn(target), '_blank');
881
918
  });
882
919
  });
@@ -889,7 +926,7 @@ function initUsersPanel($panel, hooks) {
889
926
  $list.querySelectorAll('[data-edit]').forEach(($edit) => {
890
927
  $edit.addEventListener('click', (ev) => {
891
928
  ev.preventDefault();
892
- startEditing($edit.dataset.edit);
929
+ startEditing($edit.dataset.edit ?? null);
893
930
  });
894
931
  });
895
932
  $list.querySelector('[data-cancel-edit]')?.addEventListener('click', (ev) => {
@@ -901,8 +938,11 @@ function initUsersPanel($panel, hooks) {
901
938
  ev.preventDefault();
902
939
  const name = editing;
903
940
  const $form = ev.currentTarget;
904
- const newName = $form.querySelector('.pp-dev-info__users-edit-name').value.trim();
905
- const token = $form.querySelector('.pp-dev-info__users-edit-token').value.trim();
941
+ if (!name) {
942
+ return;
943
+ }
944
+ const newName = $form.querySelector('.pp-dev-info__users-edit-name')?.value.trim() ?? '';
945
+ const token = $form.querySelector('.pp-dev-info__users-edit-token')?.value.trim() ?? '';
906
946
  const response = await request('PATCH', `${USERS_API}/${encodeURIComponent(name)}`, {
907
947
  name: newName,
908
948
  ...(token ? { token } : {}),
@@ -923,7 +963,8 @@ function initUsersPanel($panel, hooks) {
923
963
  $list.querySelectorAll('[data-remove]').forEach(($remove) => {
924
964
  $remove.addEventListener('click', async (ev) => {
925
965
  ev.preventDefault();
926
- if (await request('DELETE', `${USERS_API}/${encodeURIComponent($remove.dataset.remove)}`)) {
966
+ const name = $remove.dataset.remove;
967
+ if (name && (await request('DELETE', `${USERS_API}/${encodeURIComponent(name)}`))) {
927
968
  await refresh();
928
969
  }
929
970
  });
@@ -1004,20 +1045,34 @@ function initUsersPanel($panel, hooks) {
1004
1045
  const $name = $popover.querySelector('.pp-dev-info__users-name-input');
1005
1046
  const $token = $popover.querySelector('.pp-dev-info__users-token');
1006
1047
  const $submit = $popover.querySelector('.pp-dev-info__users-submit');
1007
- const body = addKind === 'token'
1008
- ? { name: $name.value.trim(), token: $token.value }
1009
- : { name: $name.value.trim(), kind: 'session' };
1048
+ if (!$name || !$token || !$submit) {
1049
+ return;
1050
+ }
1051
+ const name = $name.value.trim();
1052
+ const nameError = getUserNameError(name);
1053
+ if (nameError) {
1054
+ setError(nameError);
1055
+ return;
1056
+ }
1057
+ const body = addKind === 'token' ? { name, token: $token.value } : { name, kind: 'session' };
1058
+ // A session user logs in on a tab of its own. Opened here, while the click still counts as the
1059
+ // user's: opened after the request, Safari and Chrome blocked it as a popup.
1060
+ const loginTab = addKind === 'session' ? window.open('', '_blank') : null;
1010
1061
  $submit.disabled = true;
1011
1062
  const response = await request('POST', USERS_API, body);
1012
1063
  $submit.disabled = false;
1013
1064
  if (!response) {
1065
+ loginTab?.close();
1014
1066
  return;
1015
1067
  }
1016
1068
  const { user } = (await response.json());
1017
1069
  $name.value = '';
1018
1070
  $token.value = '';
1019
- if (user.kind === 'session' && user.loginUrl) {
1020
- window.open(user.loginUrl, '_blank');
1071
+ if (user.kind === 'session' && user.loginUrl && loginTab) {
1072
+ loginTab.location.href = user.loginUrl;
1073
+ }
1074
+ else {
1075
+ loginTab?.close();
1021
1076
  }
1022
1077
  await refresh();
1023
1078
  };
@@ -1042,8 +1097,8 @@ function initUsersPanel($panel, hooks) {
1042
1097
  <button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="token">API token</button>
1043
1098
  <button type="button" class="pp-dev-info__theme-btn pp-dev-info__users-kind" data-kind="session">Username + password</button>
1044
1099
  </div>
1045
- <input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="32"
1046
- autocomplete="off" spellcheck="false" pattern="[a-z]([a-z0-9-]*[a-z0-9])?"
1100
+ <input class="pp-dev-info__users-input pp-dev-info__users-name-input" placeholder="Name, e.g. viewer" maxlength="${MAX_USER_NAME_LENGTH}"
1101
+ autocomplete="off" spellcheck="false" pattern="${USER_NAME_PATTERN}"
1047
1102
  title="Lowercase latin letters, digits and -, starting with a letter. The user opens on <name>.localhost." />
1048
1103
  <input class="pp-dev-info__users-input pp-dev-info__users-token" type="password" placeholder="API token" autocomplete="off" />
1049
1104
  <button type="submit" class="pp-dev-info__vars-btn pp-dev-info__users-submit">Add user</button>
@@ -1059,7 +1114,7 @@ function initUsersPanel($panel, hooks) {
1059
1114
  renderKind();
1060
1115
  });
1061
1116
  });
1062
- $popover.querySelector('.pp-dev-info__users-add').addEventListener('submit', (ev) => {
1117
+ $popover.querySelector('.pp-dev-info__users-add')?.addEventListener('submit', (ev) => {
1063
1118
  ev.preventDefault();
1064
1119
  submit();
1065
1120
  });
@@ -1148,12 +1203,50 @@ function createPopupElement(opts) {
1148
1203
  $popup.appendChild($popupContent);
1149
1204
  return $popup;
1150
1205
  }
1151
-
1152
1206
  /**
1153
- * `template:sync:response` code for a sync requested while another one runs. Kept in sync with
1154
- * `SYNC_IN_PROGRESS_CODE` in `src/lib/client.service.ts` (server side, not importable here).
1207
+ * Calls `onElapsed` once the page has been visible for `duration` ms in all (time in a hidden tab
1208
+ * doesn't count). Returns a function that stops waiting, which also removes the page's
1209
+ * `visibilitychange` listener: one used to stay behind for every popup ever shown.
1155
1210
  */
1156
- const SYNC_IN_PROGRESS_CODE = 'SYNC_IN_PROGRESS';
1211
+ function startAutoDismiss(duration, onElapsed) {
1212
+ let remaining = duration;
1213
+ let lastUpdate = Date.now();
1214
+ let visible = true;
1215
+ let frame;
1216
+ const tick = () => {
1217
+ frame = undefined;
1218
+ if (!visible) {
1219
+ return;
1220
+ }
1221
+ const now = Date.now();
1222
+ remaining -= now - lastUpdate;
1223
+ lastUpdate = now;
1224
+ if (remaining <= 0) {
1225
+ stop();
1226
+ onElapsed();
1227
+ return;
1228
+ }
1229
+ frame = requestAnimationFrame(tick);
1230
+ };
1231
+ const onVisibilityChange = () => {
1232
+ visible = !document.hidden;
1233
+ if (visible) {
1234
+ lastUpdate = Date.now();
1235
+ frame ??= requestAnimationFrame(tick);
1236
+ }
1237
+ };
1238
+ const stop = () => {
1239
+ document.removeEventListener('visibilitychange', onVisibilityChange);
1240
+ if (frame !== undefined) {
1241
+ cancelAnimationFrame(frame);
1242
+ frame = undefined;
1243
+ }
1244
+ };
1245
+ document.addEventListener('visibilitychange', onVisibilityChange);
1246
+ frame = requestAnimationFrame(tick);
1247
+ return stop;
1248
+ }
1249
+
1157
1250
  const TITLE_AVAILABLE = 'Sync template';
1158
1251
  const TITLE_UNAVAILABLE = 'Sync is unavailable on this instance';
1159
1252
  /**
@@ -1168,6 +1261,11 @@ function initSyncButton(hot, $button, deps) {
1168
1261
  const reload = deps.reload ?? (() => window.location.reload());
1169
1262
  let available = !$button.disabled && !$button.classList.contains('disabled');
1170
1263
  let syncing = false;
1264
+ // Vite's connection is taken as open until reported lost: it may have opened before these handlers
1265
+ // were registered, and its client reloads the page once a lost one is back. The `pp-dev next`
1266
+ // transport says itself whether its socket is open, also while the first one is still connecting.
1267
+ let connected = true;
1268
+ const isConnected = () => connected && (hot.isConnected?.() ?? true);
1171
1269
  const applyState = () => {
1172
1270
  $button.disabled = syncing || !available;
1173
1271
  $button.classList.toggle('syncing', syncing);
@@ -1202,7 +1300,10 @@ function initSyncButton(hot, $button, deps) {
1202
1300
  confirmText: payload.confirmText,
1203
1301
  cancelText: payload.cancelText,
1204
1302
  });
1205
- hot.send('template:sync:action-response', { requestId: payload.requestId, approved });
1303
+ hot.send('template:sync:action-response', {
1304
+ requestId: payload.requestId,
1305
+ approved,
1306
+ });
1206
1307
  });
1207
1308
  hot.on('template:sync:response', (payload) => {
1208
1309
  deps.closeAllConfirmModals();
@@ -1235,7 +1336,11 @@ function initSyncButton(hot, $button, deps) {
1235
1336
  });
1236
1337
  // A response sent to a socket that has closed never arrives. Without this, the button would
1237
1338
  // spin, disabled, until the page is reloaded.
1339
+ hot.on('vite:ws:connect', () => {
1340
+ connected = true;
1341
+ });
1238
1342
  hot.on('vite:ws:disconnect', () => {
1343
+ connected = false;
1239
1344
  if (!syncing) {
1240
1345
  return;
1241
1346
  }
@@ -1252,6 +1357,16 @@ function initSyncButton(hot, $button, deps) {
1252
1357
  if (syncing || !available) {
1253
1358
  return;
1254
1359
  }
1360
+ // Sent now, the request would wait for a reconnect with the button spinning, and start a sync
1361
+ // whenever that happens. Say so instead, and let the click be repeated once connected.
1362
+ if (!isConnected()) {
1363
+ deps.infoPopup({
1364
+ title: 'No connection',
1365
+ content: 'The dev server is not connected. Check that it is running, then try again.',
1366
+ type: 'warning',
1367
+ });
1368
+ return;
1369
+ }
1255
1370
  syncing = true;
1256
1371
  applyState();
1257
1372
  hot.send('template:sync', {});
@@ -1329,7 +1444,16 @@ function animatePopup($popup, type) {
1329
1444
  function infoPopup(opts) {
1330
1445
  const $popup = createPopupElement(opts);
1331
1446
  const $closeButton = $popup.querySelector('.pp-dev-info__popup-title-close');
1447
+ let removed = false;
1448
+ let stopAutoDismiss;
1449
+ // Once only: closing it by hand used to leave its timer running, which removed it again and
1450
+ // counted it out of `activePopups` twice.
1332
1451
  const removePopup = async () => {
1452
+ if (removed) {
1453
+ return;
1454
+ }
1455
+ removed = true;
1456
+ stopAutoDismiss?.();
1333
1457
  await animatePopup($popup, 'exit');
1334
1458
  $popup.remove();
1335
1459
  updatePopupPositions();
@@ -1342,32 +1466,7 @@ function infoPopup(opts) {
1342
1466
  animatePopup($popup, 'enter');
1343
1467
  const duration = opts.duration ?? 10000;
1344
1468
  if (duration > 0) {
1345
- let remainingTime = duration;
1346
- let lastUpdate = Date.now();
1347
- let isVisible = true;
1348
- const scheduleDismiss = () => {
1349
- if (!isVisible) {
1350
- return;
1351
- }
1352
- const now = Date.now();
1353
- const elapsed = now - lastUpdate;
1354
- remainingTime -= elapsed;
1355
- lastUpdate = now;
1356
- if (remainingTime <= 0) {
1357
- removePopup();
1358
- return;
1359
- }
1360
- requestAnimationFrame(scheduleDismiss);
1361
- };
1362
- // Handle visibility change
1363
- document.addEventListener('visibilitychange', () => {
1364
- isVisible = !document.hidden;
1365
- if (isVisible) {
1366
- lastUpdate = Date.now();
1367
- requestAnimationFrame(scheduleDismiss);
1368
- }
1369
- });
1370
- requestAnimationFrame(scheduleDismiss);
1469
+ stopAutoDismiss = startAutoDismiss(duration, removePopup);
1371
1470
  }
1372
1471
  return { close: removePopup };
1373
1472
  }