@absolutejs/absolute 0.20.0-beta.57 → 0.20.0-beta.59

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.
@@ -1,5 +1,264 @@
1
+ // src/mobile/uiPrimitives.ts
2
+ var STYLE_ID = "absolute-mobile-ui-primitives";
3
+ var NAVIGATION_EVENT = "absolute:navigation-change";
4
+ var SHEET_EVENT = "absolute:sheet-change";
5
+ var BACK_EVENT = "absolute:back-request";
6
+ var ACTIVE_SHEET_SELECTOR = "dialog[data-absolute-sheet][open]";
7
+ var TAB_LINK_SELECTOR = "[data-absolute-tab-bar] a[href]";
8
+ var UI_STYLE = `
9
+ [data-absolute-app-shell] {
10
+ display: grid;
11
+ grid-template-rows: auto minmax(0, 1fr) auto;
12
+ box-sizing: border-box;
13
+ min-height: var(--absolute-available-height, 100dvh);
14
+ padding-inline: var(--absolute-safe-area-inset-left, 0px) var(--absolute-safe-area-inset-right, 0px);
15
+ }
16
+ [data-absolute-app-header] {
17
+ padding-top: var(--absolute-safe-area-inset-top, 0px);
18
+ }
19
+ [data-absolute-app-main], [data-absolute-navigation-stack] {
20
+ min-width: 0;
21
+ min-height: 0;
22
+ }
23
+ [data-absolute-app-main] {
24
+ overflow: auto;
25
+ overscroll-behavior-y: contain;
26
+ }
27
+ [data-absolute-navigation-stack] {
28
+ view-transition-name: absolute-mobile-stack;
29
+ }
30
+ [data-absolute-tab-bar] {
31
+ display: flex;
32
+ align-items: stretch;
33
+ justify-content: space-around;
34
+ box-sizing: border-box;
35
+ padding-bottom: var(--absolute-safe-area-inset-bottom, 0px);
36
+ background: Canvas;
37
+ color: CanvasText;
38
+ }
39
+ [data-absolute-tab-bar] > a {
40
+ display: grid;
41
+ flex: 1 1 0;
42
+ min-width: 0;
43
+ min-height: 2.75rem;
44
+ place-items: center;
45
+ padding: 0.375rem 0.5rem;
46
+ color: inherit;
47
+ text-align: center;
48
+ text-decoration: none;
49
+ touch-action: manipulation;
50
+ }
51
+ [data-absolute-tab-bar] > a[aria-current="page"] {
52
+ font-weight: 700;
53
+ }
54
+ dialog[data-absolute-sheet] {
55
+ box-sizing: border-box;
56
+ width: min(100%, var(--absolute-sheet-max-width, 42rem));
57
+ max-height: min(90dvh, var(--absolute-available-height, 90dvh));
58
+ margin: auto auto 0;
59
+ padding: 1rem max(1rem, var(--absolute-safe-area-inset-right, 0px)) max(1rem, var(--absolute-safe-area-inset-bottom, 0px)) max(1rem, var(--absolute-safe-area-inset-left, 0px));
60
+ overflow: auto;
61
+ border: 1px solid color-mix(in srgb, CanvasText 18%, transparent);
62
+ border-radius: 1rem 1rem 0 0;
63
+ background: Canvas;
64
+ color: CanvasText;
65
+ box-shadow: 0 -0.5rem 2rem color-mix(in srgb, CanvasText 18%, transparent);
66
+ }
67
+ dialog[data-absolute-sheet]::backdrop {
68
+ background: color-mix(in srgb, CanvasText 38%, transparent);
69
+ }
70
+ @keyframes absolute-mobile-forward-old { to { opacity: 0; transform: translateX(-12%); } }
71
+ @keyframes absolute-mobile-forward-new { from { opacity: 0; transform: translateX(12%); } }
72
+ @keyframes absolute-mobile-back-old { to { opacity: 0; transform: translateX(12%); } }
73
+ @keyframes absolute-mobile-back-new { from { opacity: 0; transform: translateX(-12%); } }
74
+ html[data-absolute-navigation-direction="forward"]::view-transition-old(absolute-mobile-stack) { animation: 180ms ease both absolute-mobile-forward-old; }
75
+ html[data-absolute-navigation-direction="forward"]::view-transition-new(absolute-mobile-stack) { animation: 180ms ease both absolute-mobile-forward-new; }
76
+ html[data-absolute-navigation-direction="back"]::view-transition-old(absolute-mobile-stack) { animation: 180ms ease both absolute-mobile-back-old; }
77
+ html[data-absolute-navigation-direction="back"]::view-transition-new(absolute-mobile-stack) { animation: 180ms ease both absolute-mobile-back-new; }
78
+ html[data-absolute-reduced-motion="reduce"]::view-transition-old(absolute-mobile-stack),
79
+ html[data-absolute-reduced-motion="reduce"]::view-transition-new(absolute-mobile-stack) { animation: none; }
80
+ `;
81
+ var currentPath = () => `${location.pathname}${location.search}${location.hash}`;
82
+ var ensureStyle = () => {
83
+ let style = document.getElementById(STYLE_ID);
84
+ if (!(style instanceof HTMLStyleElement)) {
85
+ style = document.createElement("style");
86
+ style.id = STYLE_ID;
87
+ style.textContent = UI_STYLE;
88
+ document.head.append(style);
89
+ }
90
+ };
91
+ var sheetById = (id) => {
92
+ const target = document.getElementById(id);
93
+ return target instanceof HTMLDialogElement && target.dataset.absoluteSheet !== undefined ? target : undefined;
94
+ };
95
+ var dispatchSheet = (sheet, open) => dispatchEvent(new CustomEvent(SHEET_EVENT, {
96
+ detail: { id: sheet.id, open }
97
+ }));
98
+ var focusSheet = (sheet) => {
99
+ const target = sheet.querySelector('[autofocus], button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
100
+ (target ?? sheet).focus();
101
+ };
102
+ var sheetOpeners = new WeakMap;
103
+ var closeAbsoluteMobileSheet = (target) => {
104
+ const sheet = typeof target === "string" ? sheetById(target) : target;
105
+ if (!sheet || !sheet.open)
106
+ return false;
107
+ if (typeof sheet.close === "function")
108
+ sheet.close();
109
+ else
110
+ sheet.removeAttribute("open");
111
+ sheet.removeAttribute("aria-modal");
112
+ sheetOpeners.get(sheet)?.focus();
113
+ sheetOpeners.delete(sheet);
114
+ dispatchSheet(sheet, false);
115
+ return true;
116
+ };
117
+ var openAbsoluteMobileSheet = (target, opener) => {
118
+ const sheet = typeof target === "string" ? sheetById(target) : target;
119
+ if (!sheet || sheet.dataset.absoluteSheet === undefined)
120
+ return false;
121
+ const active = document.querySelector(ACTIVE_SHEET_SELECTOR);
122
+ if (active && active !== sheet)
123
+ closeAbsoluteMobileSheet(active);
124
+ if (opener)
125
+ sheetOpeners.set(sheet, opener);
126
+ if (!sheet.open) {
127
+ if (typeof sheet.showModal === "function")
128
+ sheet.showModal();
129
+ else
130
+ sheet.setAttribute("open", "");
131
+ }
132
+ sheet.setAttribute("aria-modal", "true");
133
+ focusSheet(sheet);
134
+ dispatchSheet(sheet, true);
135
+ return true;
136
+ };
137
+ var readAbsoluteMobileLinkIntent = (anchor) => {
138
+ const mode = anchor.dataset.absoluteLink;
139
+ if (mode === "back")
140
+ return { kind: "back" };
141
+ if (mode === "external")
142
+ return { kind: "external" };
143
+ return { kind: "navigate", replace: mode === "replace" };
144
+ };
145
+ var requestAbsoluteMobileBack = () => {
146
+ const event = new CustomEvent(BACK_EVENT, { cancelable: true });
147
+ return !dispatchEvent(event);
148
+ };
149
+ var normalizePathname = (value) => {
150
+ try {
151
+ return new URL(value, location.href).pathname.replace(/\/$/u, "") || "/";
152
+ } catch {
153
+ return;
154
+ }
155
+ };
156
+ var syncTabLinks = (path) => {
157
+ const activePath = normalizePathname(path);
158
+ if (!activePath)
159
+ return;
160
+ document.querySelectorAll(TAB_LINK_SELECTOR).forEach((anchor) => {
161
+ const candidate = normalizePathname(anchor.href);
162
+ const prefix = anchor.dataset.absoluteTabMatch === "prefix";
163
+ const active = candidate !== undefined && (prefix ? activePath === candidate || candidate !== "/" && activePath.startsWith(`${candidate}/`) : activePath === candidate);
164
+ if (active)
165
+ anchor.setAttribute("aria-current", "page");
166
+ else
167
+ anchor.removeAttribute("aria-current");
168
+ });
169
+ };
170
+ var installAbsoluteMobileUiPrimitives = () => {
171
+ let disposed = false;
172
+ let scheduled = false;
173
+ let path = currentPath();
174
+ const refreshDocument = (nextPath = path) => {
175
+ if (disposed || !document.head || !document.body)
176
+ return;
177
+ path = nextPath;
178
+ ensureStyle();
179
+ syncTabLinks(path);
180
+ };
181
+ const scheduleRefresh = () => {
182
+ if (scheduled || disposed)
183
+ return;
184
+ scheduled = true;
185
+ queueMicrotask(() => {
186
+ scheduled = false;
187
+ refreshDocument();
188
+ });
189
+ };
190
+ const handleClick = (event) => {
191
+ if (!(event.target instanceof Element))
192
+ return;
193
+ const opener = event.target.closest("[data-absolute-sheet-open]");
194
+ if (opener?.dataset.absoluteSheetOpen) {
195
+ if (openAbsoluteMobileSheet(opener.dataset.absoluteSheetOpen, opener))
196
+ event.preventDefault();
197
+ return;
198
+ }
199
+ const closer = event.target.closest("[data-absolute-sheet-close]");
200
+ const sheet = closer?.closest("dialog[data-absolute-sheet]");
201
+ if (sheet && closeAbsoluteMobileSheet(sheet)) {
202
+ event.preventDefault();
203
+ return;
204
+ }
205
+ if (event.target instanceof HTMLDialogElement && event.target.dataset.absoluteSheet !== undefined) {
206
+ const rect = event.target.getBoundingClientRect();
207
+ const outside = event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
208
+ if (outside && closeAbsoluteMobileSheet(event.target))
209
+ event.preventDefault();
210
+ }
211
+ };
212
+ const handleCancel = (event) => {
213
+ if (!(event.target instanceof HTMLDialogElement))
214
+ return;
215
+ if (event.target.dataset.absoluteSheet === undefined)
216
+ return;
217
+ event.preventDefault();
218
+ closeAbsoluteMobileSheet(event.target);
219
+ };
220
+ const handleBack = (event) => {
221
+ const active = document.querySelector(ACTIVE_SHEET_SELECTOR);
222
+ if (!active)
223
+ return;
224
+ event.preventDefault();
225
+ closeAbsoluteMobileSheet(active);
226
+ };
227
+ const observer = new MutationObserver(scheduleRefresh);
228
+ observer.observe(document.documentElement, {
229
+ childList: true,
230
+ subtree: true
231
+ });
232
+ addEventListener("click", handleClick);
233
+ addEventListener("cancel", handleCancel, true);
234
+ addEventListener(BACK_EVENT, handleBack);
235
+ refreshDocument();
236
+ return {
237
+ refreshDocument,
238
+ requestBack: requestAbsoluteMobileBack,
239
+ dispose: () => {
240
+ if (disposed)
241
+ return;
242
+ disposed = true;
243
+ observer.disconnect();
244
+ removeEventListener("click", handleClick);
245
+ removeEventListener("cancel", handleCancel, true);
246
+ removeEventListener(BACK_EVENT, handleBack);
247
+ },
248
+ navigate: (detail) => {
249
+ path = detail.to;
250
+ document.documentElement.dataset.absoluteNavigationDirection = detail.direction;
251
+ refreshDocument(path);
252
+ dispatchEvent(new CustomEvent(NAVIGATION_EVENT, {
253
+ detail
254
+ }));
255
+ }
256
+ };
257
+ };
258
+
1
259
  // src/mobile/shellBootstrap.ts
2
260
  import { App } from "@capacitor/app";
261
+ import { Browser } from "@capacitor/browser";
3
262
 
4
263
  // src/mobile/producerContextState.ts
5
264
  var ABSOLUTE_MOBILE_PRODUCER_STORAGE_KEY = Symbol.for("absolutejs.mobileProducerAsyncLocalStorage");
@@ -199,7 +458,11 @@ var createAbsoluteMobilePageRequest = (manifest, path, options = {}) => {
199
458
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.pageId, page.pageId);
200
459
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.protocol, String(ABSOLUTE_MOBILE_PAGE_PROTOCOL_VERSION));
201
460
  headers.set(MOBILE_PAGE_REQUEST_HEADERS.runtime, manifest.runtime);
202
- return new Request(url, { headers, method: "GET" });
461
+ return new Request(url, {
462
+ headers,
463
+ method: "GET",
464
+ ...options.signal ? { signal: options.signal } : {}
465
+ });
203
466
  };
204
467
  var fetchAbsoluteMobilePage = async (manifest, path, options = {}) => {
205
468
  const request = createAbsoluteMobilePageRequest(manifest, path, options);
@@ -643,7 +906,7 @@ import {
643
906
  platform,
644
907
  systemBars
645
908
  } from "@absolutejs/devices";
646
- var STYLE_ID = "absolute-mobile-adaptive-shell";
909
+ var STYLE_ID2 = "absolute-mobile-adaptive-shell";
647
910
  var ANNOUNCER_ID = "absolute-mobile-announcer";
648
911
  var HOST_METRICS_EVENT = "absolute:native-host-metrics";
649
912
  var STATUS_STYLE = `
@@ -691,11 +954,11 @@ var viewportSize = () => ({
691
954
  width: finitePixels(globalThis.visualViewport?.width ?? innerWidth)
692
955
  });
693
956
  var setPixels = (name, value) => document.documentElement.style.setProperty(name, `${finitePixels(value)}px`);
694
- var ensureStyle = () => {
695
- let style = document.getElementById(STYLE_ID);
957
+ var ensureStyle2 = () => {
958
+ let style = document.getElementById(STYLE_ID2);
696
959
  if (!(style instanceof HTMLStyleElement)) {
697
960
  style = document.createElement("style");
698
- style.id = STYLE_ID;
961
+ style.id = STYLE_ID2;
699
962
  style.textContent = STATUS_STYLE;
700
963
  document.head.append(style);
701
964
  }
@@ -773,7 +1036,7 @@ var installAbsoluteMobileAdaptiveShell = async (devices = defaultDevices) => {
773
1036
  if (disposed || !document.head || !document.body)
774
1037
  return;
775
1038
  ensureViewport();
776
- ensureStyle();
1039
+ ensureStyle2();
777
1040
  const root = document.documentElement;
778
1041
  const current = state();
779
1042
  root.dataset.absoluteMobile = "";
@@ -884,9 +1147,268 @@ var installAbsoluteMobileAdaptiveShell = async (devices = defaultDevices) => {
884
1147
  };
885
1148
  };
886
1149
 
1150
+ // src/mobile/navigationLifecycle.ts
1151
+ var isAbortError = (error) => error instanceof DOMException ? error.name === "AbortError" : error instanceof Error && error.name === "AbortError";
1152
+ var cancelledResult = { kind: "cancelled" };
1153
+ var committedResult = { kind: "committed" };
1154
+ var failedResult = (error, phase) => ({ error, kind: "failed", phase });
1155
+ var createAbsoluteMobileNavigationCoordinator = (options) => {
1156
+ let activeLoad;
1157
+ let commitQueue = Promise.resolve();
1158
+ let disposed = false;
1159
+ let generation = 0;
1160
+ let lifecyclePhase = "idle";
1161
+ const navigate = async (request) => {
1162
+ if (disposed)
1163
+ return cancelledResult;
1164
+ const ownGeneration = generation += 1;
1165
+ activeLoad?.abort();
1166
+ const controller = new AbortController;
1167
+ activeLoad = controller;
1168
+ lifecyclePhase = "loading";
1169
+ options.onStart?.(request);
1170
+ let payload;
1171
+ try {
1172
+ payload = await options.load(request, controller.signal);
1173
+ } catch (error) {
1174
+ if (controller.signal.aborted || isAbortError(error) || ownGeneration !== generation || disposed) {
1175
+ if (ownGeneration === generation)
1176
+ lifecyclePhase = "idle";
1177
+ return cancelledResult;
1178
+ }
1179
+ lifecyclePhase = "idle";
1180
+ options.onFailure?.(error, "load", request);
1181
+ return failedResult(error, "load");
1182
+ }
1183
+ if (ownGeneration !== generation || disposed)
1184
+ return cancelledResult;
1185
+ if (activeLoad === controller)
1186
+ activeLoad = undefined;
1187
+ lifecyclePhase = "queued";
1188
+ const execute = async () => {
1189
+ if (ownGeneration !== generation || disposed) {
1190
+ if (ownGeneration === generation)
1191
+ lifecyclePhase = "idle";
1192
+ return cancelledResult;
1193
+ }
1194
+ lifecyclePhase = "committing";
1195
+ try {
1196
+ await options.commit(payload, request);
1197
+ options.onCommit?.(request);
1198
+ } catch (error) {
1199
+ if (ownGeneration === generation) {
1200
+ lifecyclePhase = "idle";
1201
+ options.onFailure?.(error, "commit", request);
1202
+ }
1203
+ return failedResult(error, "commit");
1204
+ }
1205
+ if (ownGeneration !== generation || disposed) {
1206
+ return cancelledResult;
1207
+ }
1208
+ lifecyclePhase = "idle";
1209
+ options.onSuccess?.(request);
1210
+ return committedResult;
1211
+ };
1212
+ const result = commitQueue.then(execute, execute);
1213
+ commitQueue = result.then(() => {
1214
+ return;
1215
+ }, () => {
1216
+ return;
1217
+ });
1218
+ return result;
1219
+ };
1220
+ return {
1221
+ cancelPending: () => {
1222
+ if (disposed || lifecyclePhase !== "loading" && lifecyclePhase !== "queued")
1223
+ return false;
1224
+ generation += 1;
1225
+ activeLoad?.abort();
1226
+ activeLoad = undefined;
1227
+ lifecyclePhase = "idle";
1228
+ return true;
1229
+ },
1230
+ dispose: () => {
1231
+ if (disposed)
1232
+ return;
1233
+ disposed = true;
1234
+ generation += 1;
1235
+ activeLoad?.abort();
1236
+ activeLoad = undefined;
1237
+ lifecyclePhase = "idle";
1238
+ },
1239
+ navigate: (request) => navigate(request),
1240
+ phase: () => lifecyclePhase
1241
+ };
1242
+ };
1243
+
1244
+ // src/mobile/navigationState.ts
1245
+ var CONTROL_SELECTOR = 'input, textarea, select, details, [contenteditable="true"]';
1246
+ var FOCUS_SELECTOR = '[data-absolute-navigation-focus], button, a[href], input, textarea, select, [tabindex]:not([tabindex="-1"])';
1247
+ var SCROLL_SELECTOR = "[data-absolute-app-main], [data-absolute-scroll-restoration]";
1248
+ var SENSITIVE_AUTOCOMPLETE = new Set([
1249
+ "cc-csc",
1250
+ "cc-number",
1251
+ "current-password",
1252
+ "new-password",
1253
+ "one-time-code"
1254
+ ]);
1255
+ var locatorFor = (element, elements) => {
1256
+ if (element.id)
1257
+ return { kind: "id", value: element.id };
1258
+ const name = element.getAttribute("name");
1259
+ if (name) {
1260
+ const matching = elements.filter((candidate) => candidate.getAttribute("name") === name);
1261
+ return { index: matching.indexOf(element), kind: "name", value: name };
1262
+ }
1263
+ return { index: elements.indexOf(element), kind: "index" };
1264
+ };
1265
+ var resolveLocator = (locator, selector) => {
1266
+ if (locator.kind === "id") {
1267
+ return document.getElementById(locator.value) ?? undefined;
1268
+ }
1269
+ if (locator.kind === "name") {
1270
+ return [...document.querySelectorAll(selector)].filter((element) => element.getAttribute("name") === locator.value)[locator.index];
1271
+ }
1272
+ return document.querySelectorAll(selector)[locator.index];
1273
+ };
1274
+ var shouldCaptureControl = (element) => {
1275
+ if (element.closest('[data-absolute-navigation-preserve="off"]'))
1276
+ return false;
1277
+ if (!(element instanceof HTMLInputElement))
1278
+ return true;
1279
+ if (element.type === "file" || element.type === "hidden" || element.type === "password")
1280
+ return false;
1281
+ return !SENSITIVE_AUTOCOMPLETE.has(element.autocomplete.toLowerCase());
1282
+ };
1283
+ var captureControl = (element, locator) => {
1284
+ const snapshot = {
1285
+ locator,
1286
+ tag: element.tagName.toLowerCase()
1287
+ };
1288
+ if (element instanceof HTMLInputElement) {
1289
+ if (element.type === "checkbox" || element.type === "radio") {
1290
+ snapshot.checked = element.checked;
1291
+ } else {
1292
+ snapshot.value = element.value;
1293
+ if (element.selectionStart !== null)
1294
+ snapshot.selectionStart = element.selectionStart;
1295
+ if (element.selectionEnd !== null)
1296
+ snapshot.selectionEnd = element.selectionEnd;
1297
+ }
1298
+ } else if (element instanceof HTMLTextAreaElement) {
1299
+ snapshot.value = element.value;
1300
+ snapshot.selectionStart = element.selectionStart;
1301
+ snapshot.selectionEnd = element.selectionEnd;
1302
+ } else if (element instanceof HTMLSelectElement) {
1303
+ snapshot.values = [...element.selectedOptions].map(({ value }) => value);
1304
+ } else if (element instanceof HTMLDetailsElement) {
1305
+ snapshot.open = element.open;
1306
+ } else if (element.getAttribute("contenteditable") === "true") {
1307
+ snapshot.value = element.textContent ?? "";
1308
+ }
1309
+ return snapshot;
1310
+ };
1311
+ var restoreControl = (snapshot) => {
1312
+ const element = resolveLocator(snapshot.locator, CONTROL_SELECTOR);
1313
+ if (!element || element.tagName.toLowerCase() !== snapshot.tag)
1314
+ return;
1315
+ if (element instanceof HTMLInputElement) {
1316
+ if (snapshot.checked !== undefined)
1317
+ element.checked = snapshot.checked;
1318
+ else if (snapshot.value !== undefined)
1319
+ element.value = snapshot.value;
1320
+ if (snapshot.selectionStart !== undefined && snapshot.selectionEnd !== undefined) {
1321
+ try {
1322
+ element.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
1323
+ } catch {}
1324
+ }
1325
+ } else if (element instanceof HTMLTextAreaElement) {
1326
+ if (snapshot.value !== undefined)
1327
+ element.value = snapshot.value;
1328
+ if (snapshot.selectionStart !== undefined && snapshot.selectionEnd !== undefined)
1329
+ element.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
1330
+ } else if (element instanceof HTMLSelectElement && snapshot.values) {
1331
+ for (const option of element.options) {
1332
+ option.selected = snapshot.values.includes(option.value);
1333
+ }
1334
+ } else if (element instanceof HTMLDetailsElement && snapshot.open !== undefined) {
1335
+ element.open = snapshot.open;
1336
+ } else if (element.getAttribute("contenteditable") === "true" && snapshot.value !== undefined) {
1337
+ element.textContent = snapshot.value;
1338
+ }
1339
+ };
1340
+ var focusElement = (element) => {
1341
+ if (!(element instanceof HTMLElement))
1342
+ return false;
1343
+ if (!element.matches(FOCUS_SELECTOR))
1344
+ element.tabIndex = -1;
1345
+ element.focus({ preventScroll: true });
1346
+ return document.activeElement === element;
1347
+ };
1348
+ var createAbsoluteMobileHistoryEntry = (path, index, entryId = crypto.randomUUID()) => ({
1349
+ absoluteMobile: true,
1350
+ entryId,
1351
+ index,
1352
+ path
1353
+ });
1354
+ var readAbsoluteMobileHistoryEntry = (value) => {
1355
+ if (typeof value !== "object" || value === null)
1356
+ return;
1357
+ if (Reflect.get(value, "absoluteMobile") !== true || typeof Reflect.get(value, "entryId") !== "string" || !Number.isSafeInteger(Reflect.get(value, "index")) || typeof Reflect.get(value, "path") !== "string")
1358
+ return;
1359
+ return {
1360
+ absoluteMobile: true,
1361
+ entryId: Reflect.get(value, "entryId"),
1362
+ index: Reflect.get(value, "index"),
1363
+ path: Reflect.get(value, "path")
1364
+ };
1365
+ };
1366
+ var captureAbsoluteMobileDocumentState = () => {
1367
+ const controls = [
1368
+ ...document.querySelectorAll(CONTROL_SELECTOR)
1369
+ ].filter(shouldCaptureControl);
1370
+ const focusable = [...document.querySelectorAll(FOCUS_SELECTOR)];
1371
+ const scrollable = [...document.querySelectorAll(SCROLL_SELECTOR)];
1372
+ const active = document.activeElement;
1373
+ return {
1374
+ controls: controls.map((element) => captureControl(element, locatorFor(element, controls))),
1375
+ ...active instanceof Element && focusable.includes(active) ? { focus: locatorFor(active, focusable) } : {},
1376
+ scroll: scrollable.map((element) => ({
1377
+ left: element.scrollLeft,
1378
+ locator: locatorFor(element, scrollable),
1379
+ top: element.scrollTop
1380
+ })),
1381
+ window: { x: window.scrollX, y: window.scrollY }
1382
+ };
1383
+ };
1384
+ var restoreAbsoluteMobileDocumentState = (snapshot) => {
1385
+ for (const control of snapshot.controls)
1386
+ restoreControl(control);
1387
+ const focus = snapshot.focus ? resolveLocator(snapshot.focus, FOCUS_SELECTOR) : undefined;
1388
+ focusElement(focus);
1389
+ for (const scroll of snapshot.scroll) {
1390
+ const element = resolveLocator(scroll.locator, SCROLL_SELECTOR);
1391
+ if (element)
1392
+ element.scrollTo(scroll.left, scroll.top);
1393
+ }
1394
+ window.scrollTo(snapshot.window.x, snapshot.window.y);
1395
+ };
1396
+ var resetAbsoluteMobileDocumentState = () => {
1397
+ for (const element of document.querySelectorAll(SCROLL_SELECTOR)) {
1398
+ element.scrollTo(0, 0);
1399
+ }
1400
+ window.scrollTo(0, 0);
1401
+ const autofocus = document.querySelector("[autofocus]");
1402
+ if (autofocus && focusElement(autofocus))
1403
+ return;
1404
+ const target = ["[data-absolute-navigation-focus]", "main h1", "h1", "main"].map((selector) => document.querySelector(selector)).find((element) => element !== null);
1405
+ focusElement(target ?? undefined);
1406
+ };
1407
+
887
1408
  // src/mobile/shellBootstrap.ts
888
1409
  var MANIFEST_PATH = "./absolute-mobile-manifest.json";
889
1410
  var STATUS_ID = "absolute-mobile-status";
1411
+ var NAVIGATION_ERROR_ID = "absolute-mobile-navigation-error";
890
1412
  var initialNavigationPath = (entry) => {
891
1413
  const expoPath = new URLSearchParams(location.search).get("absolutePath");
892
1414
  if (expoPath?.startsWith("/") && !expoPath.startsWith("//"))
@@ -898,20 +1420,19 @@ var currentNavigationPath = () => {
898
1420
  const expoPath = new URLSearchParams(location.search).get("absolutePath");
899
1421
  return expoPath?.startsWith("/") && !expoPath.startsWith("//") ? expoPath : `${location.pathname}${location.search}${location.hash}`;
900
1422
  };
901
- var pushNavigationHistory = (path) => {
1423
+ var historyUrl = (path) => {
902
1424
  if (!Reflect.get(globalThis, "__absoluteExpoBridge")) {
903
- history.pushState({ absoluteMobile: true }, "", path);
904
- return;
1425
+ return path;
905
1426
  }
906
1427
  const url = new URL(location.href);
907
1428
  url.search = "";
908
1429
  url.hash = "";
909
1430
  url.searchParams.set("absolutePath", path);
910
- history.pushState({ absoluteMobile: true }, "", url.href);
911
- const bridge = Reflect.get(globalThis, "__absoluteExpoBridge");
912
- if (typeof bridge === "object" && bridge !== null && typeof Reflect.get(bridge, "setPath") === "function") {
913
- Reflect.get(bridge, "setPath").call(bridge, path);
914
- }
1431
+ return url.href;
1432
+ };
1433
+ var writeNavigationHistory = (entry, mode) => {
1434
+ history[mode === "push" ? "pushState" : "replaceState"](entry, "", historyUrl(entry.path));
1435
+ notifyExpoNavigationPath(entry.path);
915
1436
  };
916
1437
  var notifyExpoNavigationPath = (path) => {
917
1438
  const bridge = Reflect.get(globalThis, "__absoluteExpoBridge");
@@ -921,6 +1442,7 @@ var notifyExpoNavigationPath = (path) => {
921
1442
  };
922
1443
  var navigationGeneration = 0;
923
1444
  var adaptiveShell;
1445
+ var uiPrimitives;
924
1446
  var notifyCapacitorSystemBarsDomReady = () => {
925
1447
  const provider = Reflect.get(globalThis, "CapacitorSystemBarsAndroidInterface");
926
1448
  if (typeof provider !== "object" || provider === null)
@@ -949,6 +1471,34 @@ var renderStatus = (message, kind = "loading") => {
949
1471
  status.textContent = message;
950
1472
  adaptiveShell?.refreshDocument();
951
1473
  };
1474
+ var clearNavigationFailure = () => {
1475
+ document.getElementById(NAVIGATION_ERROR_ID)?.remove();
1476
+ };
1477
+ var renderNavigationFailure = (message, retry) => {
1478
+ clearNavigationFailure();
1479
+ const error = document.createElement("aside");
1480
+ error.id = NAVIGATION_ERROR_ID;
1481
+ error.dataset.absoluteMobileNavigationError = "";
1482
+ error.setAttribute("role", "alert");
1483
+ error.style.cssText = "position:fixed;z-index:2147483646;inset:auto max(1rem,var(--absolute-safe-area-inset-right,0px)) max(1rem,var(--absolute-safe-area-inset-bottom,0px)) max(1rem,var(--absolute-safe-area-inset-left,0px));display:flex;align-items:center;justify-content:space-between;gap:1rem;box-sizing:border-box;padding:.75rem 1rem;border:1px solid color-mix(in srgb,CanvasText 24%,transparent);border-radius:.75rem;background:Canvas;color:CanvasText;box-shadow:0 .5rem 2rem color-mix(in srgb,CanvasText 20%,transparent)";
1484
+ const text = document.createElement("span");
1485
+ text.textContent = message;
1486
+ const button = document.createElement("button");
1487
+ button.type = "button";
1488
+ button.textContent = "Retry";
1489
+ button.addEventListener("click", retry, { once: true });
1490
+ error.append(text, button);
1491
+ document.body.append(error);
1492
+ };
1493
+ var markNavigationPending = (pending) => {
1494
+ if (pending) {
1495
+ document.body.dataset.absoluteMobileNavigationPending = "";
1496
+ document.body.setAttribute("aria-busy", "true");
1497
+ } else {
1498
+ delete document.body.dataset.absoluteMobileNavigationPending;
1499
+ document.body.removeAttribute("aria-busy");
1500
+ }
1501
+ };
952
1502
  var renderPageTarget = () => {
953
1503
  document.title = "AbsoluteJS";
954
1504
  document.head.innerHTML = '<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">';
@@ -974,47 +1524,89 @@ var installLocalPageStyle = (manifest, pageId, contract) => {
974
1524
  link.dataset.absoluteMobilePageStyle = page.bundleHash;
975
1525
  document.head.appendChild(link);
976
1526
  };
977
- var navigate = async (manifest, path, pushHistory, fetchImpl) => {
978
- await disposeAbsoluteMobilePage();
979
- renderStatus("Loading…");
980
- const envelope = await fetchAbsoluteMobilePage(manifest, path, {
981
- ...fetchImpl ? { fetch: fetchImpl } : {}
982
- });
983
- const activation = await activateAbsoluteMobilePage(envelope, {
984
- loadPage: ({ contract, pageId }) => {
985
- const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
986
- if (!page) {
987
- throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
988
- }
989
- if (page.framework === "html" || page.framework === "htmx") {
990
- return installAbsoluteMobileStaticDocument(manifest, page, localBundleUrl(manifest, pageId, contract));
1527
+ var waitForDocumentPaint = () => new Promise((resolve) => {
1528
+ if (typeof requestAnimationFrame !== "function") {
1529
+ setTimeout(resolve, 0);
1530
+ return;
1531
+ }
1532
+ requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
1533
+ });
1534
+ var commitNavigation = async (manifest, envelope, hadActivePage) => {
1535
+ if (!hadActivePage)
1536
+ renderStatus("Loading…");
1537
+ let activation;
1538
+ const commit = async () => {
1539
+ await disposeAbsoluteMobilePage();
1540
+ activation = await activateAbsoluteMobilePage(envelope, {
1541
+ loadPage: ({ contract, pageId }) => {
1542
+ const page = manifest.pages.find((candidate) => candidate.pageId === pageId && candidate.contract === contract);
1543
+ if (!page) {
1544
+ throw new TypeError(`The embedded app does not contain ${pageId} contract ${contract}.`);
1545
+ }
1546
+ if (page.framework === "html" || page.framework === "htmx") {
1547
+ return installAbsoluteMobileStaticDocument(manifest, page, localBundleUrl(manifest, pageId, contract));
1548
+ }
1549
+ renderPageTarget();
1550
+ installLocalPageStyle(manifest, pageId, contract);
1551
+ return import(localBundleUrl(manifest, pageId, contract));
991
1552
  }
992
- renderPageTarget();
993
- installLocalPageStyle(manifest, pageId, contract);
994
- return import(localBundleUrl(manifest, pageId, contract));
995
- }
996
- });
1553
+ });
1554
+ };
1555
+ const transitionDocument = document;
1556
+ if (hadActivePage && document.documentElement.dataset.absoluteReducedMotion !== "reduce" && transitionDocument.startViewTransition) {
1557
+ const transition = transitionDocument.startViewTransition(commit);
1558
+ await transition.updateCallbackDone;
1559
+ await transition.finished.catch(() => {
1560
+ return;
1561
+ });
1562
+ } else
1563
+ await commit();
1564
+ if (!activation)
1565
+ throw new TypeError("The mobile page did not produce an activation.");
997
1566
  if (activation.kind === "upgrade-required") {
998
1567
  renderStatus("This app version must be updated to continue.", "update");
999
- return;
1568
+ return false;
1000
1569
  }
1570
+ await waitForDocumentPaint();
1571
+ document.body.dataset.absoluteMobilePageActive = "";
1001
1572
  adaptiveShell?.refreshDocument();
1002
- if (pushHistory)
1003
- pushNavigationHistory(path);
1573
+ return true;
1004
1574
  };
1005
- var installAnchorNavigation = (manifest, onNavigate) => {
1575
+ var openExternalLink = (anchor, event) => {
1576
+ if (Reflect.get(globalThis, "__absoluteExpoBridge"))
1577
+ return;
1578
+ try {
1579
+ const url = new URL(anchor.href);
1580
+ if (url.username || url.password || url.protocol !== "http:" && url.protocol !== "https:")
1581
+ return;
1582
+ event.preventDefault();
1583
+ Browser.open({ url: url.href }).catch(() => location.assign(url.href));
1584
+ } catch {}
1585
+ };
1586
+ var installAnchorNavigation = (manifest, onNavigate, onBack) => {
1006
1587
  const handleClick = (event) => {
1007
1588
  if (!(event.target instanceof Element))
1008
1589
  return;
1009
1590
  const anchor = event.target.closest("a[href]");
1010
1591
  if (!(anchor instanceof HTMLAnchorElement) || anchor.target)
1011
1592
  return;
1593
+ const intent = readAbsoluteMobileLinkIntent(anchor);
1594
+ if (intent.kind === "back") {
1595
+ event.preventDefault();
1596
+ if (!uiPrimitives?.requestBack() && !onBack())
1597
+ history.back();
1598
+ return;
1599
+ }
1600
+ if (intent.kind === "external") {
1601
+ openExternalLink(anchor, event);
1602
+ return;
1603
+ }
1012
1604
  try {
1013
1605
  const path = resolveAbsoluteMobileNavigation(manifest, anchor.href, location.origin);
1014
1606
  if (!path)
1015
1607
  return;
1016
1608
  event.preventDefault();
1017
- onNavigate(path);
1609
+ onNavigate(path, intent.kind === "navigate" && intent.replace ? "replace" : "forward", intent.kind === "navigate" && intent.replace);
1018
1610
  } catch {}
1019
1611
  };
1020
1612
  addEventListener("click", handleClick);
@@ -1041,21 +1633,19 @@ var installDeepLinks = async (manifest, onNavigate, authRedirectUri) => {
1041
1633
  } catch {}
1042
1634
  return listener;
1043
1635
  };
1044
- var navigateWithFailureState = async (manifest, path, pushHistory, reinstallBrowserNavigation, fetchImpl) => {
1045
- try {
1046
- await navigate(manifest, path, pushHistory, fetchImpl);
1047
- } catch (error) {
1048
- console.error("[Absolute Mobile] Navigation failed:", error);
1049
- renderStatus(document.documentElement.dataset.absoluteNetwork === "offline" ? "You are offline. Reconnect to load this page." : "Unable to load this page. Check your connection and retry.", "error");
1050
- } finally {
1051
- reinstallBrowserNavigation();
1052
- }
1053
- };
1054
1636
  var startAbsoluteMobileShell = async (options = {}) => {
1055
1637
  notifyCapacitorSystemBarsDomReady();
1056
1638
  await adaptiveShell?.dispose();
1639
+ uiPrimitives?.dispose();
1057
1640
  adaptiveShell = await installAbsoluteMobileAdaptiveShell();
1641
+ uiPrimitives = installAbsoluteMobileUiPrimitives();
1058
1642
  const manifest = await readManifest();
1643
+ let activePath = initialNavigationPath(manifest.entry);
1644
+ let activeEntry = readAbsoluteMobileHistoryEntry(history.state) ?? createAbsoluteMobileHistoryEntry(activePath, 0);
1645
+ if (activeEntry.path !== activePath) {
1646
+ activeEntry = createAbsoluteMobileHistoryEntry(activePath, 0);
1647
+ }
1648
+ writeNavigationHistory(activeEntry, "replace");
1059
1649
  const auth = manifest.auth && options.createAuth ? await options.createAuth(manifest.auth, {
1060
1650
  beforeSignOut: options.beforeSignOut
1061
1651
  }) : undefined;
@@ -1065,25 +1655,144 @@ var startAbsoluteMobileShell = async (options = {}) => {
1065
1655
  options.connectPush?.(auth);
1066
1656
  if (auth && manifest.sync?.socketTickets)
1067
1657
  options.installSync?.(auth, manifest.sync);
1658
+ const snapshots = new Map;
1659
+ const targetEntries = new WeakMap;
1660
+ let suppressNextPop = false;
1661
+ let hasActivePage = false;
1068
1662
  let removeAnchorNavigation = () => {
1069
1663
  return;
1070
1664
  };
1071
- const handlePopState = () => {
1665
+ const coordinator = createAbsoluteMobileNavigationCoordinator({
1666
+ commit: async (envelope, request) => {
1667
+ if (hasActivePage) {
1668
+ snapshots.set(activeEntry.entryId, captureAbsoluteMobileDocumentState());
1669
+ }
1670
+ document.documentElement.dataset.absoluteNavigationDirection = request.direction;
1671
+ try {
1672
+ const previouslyActive = hasActivePage;
1673
+ hasActivePage = false;
1674
+ hasActivePage = await commitNavigation(manifest, envelope, previouslyActive);
1675
+ } finally {
1676
+ reinstallBrowserNavigation();
1677
+ }
1678
+ },
1679
+ load: (request, signal) => fetchAbsoluteMobilePage(manifest, request.path, {
1680
+ fetch: applicationFetch,
1681
+ signal
1682
+ }),
1683
+ onFailure: (error, phase, request) => {
1684
+ console.error("[Absolute Mobile] Navigation failed:", error);
1685
+ markNavigationPending(false);
1686
+ const targetEntry = targetEntries.get(request);
1687
+ if (targetEntry && targetEntry.index !== activeEntry.index) {
1688
+ suppressNextPop = true;
1689
+ history.go(activeEntry.index - targetEntry.index);
1690
+ }
1691
+ const message = document.documentElement.dataset.absoluteNetwork === "offline" ? "You are offline. Reconnect to load this page." : "Unable to load this page. Check your connection and retry.";
1692
+ if (phase === "load" && hasActivePage) {
1693
+ renderNavigationFailure(message, () => {
1694
+ if (targetEntry) {
1695
+ history.go(targetEntry.index - activeEntry.index);
1696
+ } else
1697
+ coordinator.navigate(request);
1698
+ });
1699
+ } else
1700
+ renderStatus(message, "error");
1701
+ },
1702
+ onStart: () => {
1703
+ clearNavigationFailure();
1704
+ markNavigationPending(true);
1705
+ },
1706
+ onSuccess: (request) => {
1707
+ let nextEntry = targetEntries.get(request);
1708
+ if (request.historyMode === "push") {
1709
+ nextEntry = createAbsoluteMobileHistoryEntry(request.path, activeEntry.index + 1);
1710
+ writeNavigationHistory(nextEntry, "push");
1711
+ } else if (request.historyMode === "replace") {
1712
+ nextEntry = createAbsoluteMobileHistoryEntry(request.path, activeEntry.index);
1713
+ writeNavigationHistory(nextEntry, "replace");
1714
+ }
1715
+ nextEntry ??= activeEntry;
1716
+ activeEntry = nextEntry;
1717
+ activePath = request.path;
1718
+ markNavigationPending(false);
1719
+ clearNavigationFailure();
1720
+ const snapshot = snapshots.get(activeEntry.entryId);
1721
+ if (snapshot)
1722
+ restoreAbsoluteMobileDocumentState(snapshot);
1723
+ else if (request.historyMode !== "none")
1724
+ resetAbsoluteMobileDocumentState();
1725
+ uiPrimitives?.navigate({
1726
+ direction: request.direction,
1727
+ from: request.from,
1728
+ to: request.path
1729
+ });
1730
+ }
1731
+ });
1732
+ const cancelPendingNavigation = () => {
1733
+ if (!coordinator.cancelPending())
1734
+ return false;
1735
+ markNavigationPending(false);
1736
+ clearNavigationFailure();
1737
+ return true;
1738
+ };
1739
+ const handlePopState = (event) => {
1072
1740
  const path = currentNavigationPath();
1741
+ if (suppressNextPop) {
1742
+ suppressNextPop = false;
1743
+ notifyExpoNavigationPath(activePath);
1744
+ return;
1745
+ }
1746
+ const targetEntry = readAbsoluteMobileHistoryEntry(event.state) ?? createAbsoluteMobileHistoryEntry(path, activeEntry.index - 1);
1073
1747
  notifyExpoNavigationPath(path);
1074
- navigateWithFailureState(manifest, path, false, reinstallBrowserNavigation, applicationFetch);
1748
+ const request = {
1749
+ direction: targetEntry.index > activeEntry.index ? "forward" : "back",
1750
+ from: activePath,
1751
+ historyMode: "none",
1752
+ path
1753
+ };
1754
+ targetEntries.set(request, targetEntry);
1755
+ coordinator.navigate(request);
1075
1756
  };
1076
1757
  const reinstallBrowserNavigation = () => {
1077
1758
  removeAnchorNavigation();
1078
1759
  removeEventListener("popstate", handlePopState);
1079
- removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate);
1760
+ uiPrimitives?.dispose();
1761
+ uiPrimitives = installAbsoluteMobileUiPrimitives();
1762
+ uiPrimitives.refreshDocument(activePath);
1763
+ removeAnchorNavigation = installAnchorNavigation(manifest, onNavigate, cancelPendingNavigation);
1080
1764
  addEventListener("popstate", handlePopState);
1081
1765
  };
1082
- const onNavigate = (path) => {
1083
- navigateWithFailureState(manifest, path, true, reinstallBrowserNavigation, applicationFetch);
1766
+ const onNavigate = (path, direction = "forward", replace = false) => {
1767
+ if (path === activePath && !replace && hasActivePage)
1768
+ return;
1769
+ coordinator.navigate({
1770
+ direction,
1771
+ from: activePath,
1772
+ historyMode: replace ? "replace" : "push",
1773
+ path
1774
+ });
1084
1775
  };
1085
- await navigateWithFailureState(manifest, initialNavigationPath(manifest.entry), false, reinstallBrowserNavigation, applicationFetch);
1776
+ reinstallBrowserNavigation();
1777
+ await coordinator.navigate({
1778
+ direction: "replace",
1779
+ from: activePath,
1780
+ historyMode: "none",
1781
+ path: activePath
1782
+ });
1086
1783
  await installDeepLinks(manifest, onNavigate, auth?.redirectUri);
1784
+ try {
1785
+ await App.addListener("backButton", ({ canGoBack }) => {
1786
+ if (uiPrimitives?.requestBack())
1787
+ return;
1788
+ if (cancelPendingNavigation())
1789
+ return;
1790
+ if (canGoBack)
1791
+ history.back();
1792
+ else
1793
+ App.exitApp();
1794
+ });
1795
+ } catch {}
1087
1796
  };
1088
1797
  export {
1089
1798
  startAbsoluteMobileShell