@octane-xplat/ui 0.7.2 → 0.9.0

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 (219) hide show
  1. package/README.md +4 -3
  2. package/dist/native/Button.tsrx.js +1 -1
  3. package/dist/native/CommandPalette.tsrx.js +7 -1
  4. package/dist/native/Grid.tsrx.js +31 -2
  5. package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
  6. package/dist/native/Markdown.tsrx.js +233 -0
  7. package/dist/native/Overlay.tsrx.js +15 -26
  8. package/dist/native/Pagination.tsrx.js +15 -12
  9. package/dist/native/PinInput.tsrx.js +25 -7
  10. package/dist/native/Popover.tsrx.js +33 -23
  11. package/dist/native/Pressable.tsrx.js +4 -1
  12. package/dist/native/Screen.tsrx.js +1 -1
  13. package/dist/native/SearchInput.tsrx.js +6 -3
  14. package/dist/native/Sheet.tsrx.js +23 -38
  15. package/dist/native/Slider.tsrx.js +23 -14
  16. package/dist/native/Text.tsrx.js +16 -0
  17. package/dist/native/TextArea.tsrx.js +6 -3
  18. package/dist/native/TextInput.tsrx.js +6 -3
  19. package/dist/native/VirtualList.tsrx.js +305 -121
  20. package/dist/native/WebView.tsrx.js +17 -2
  21. package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
  22. package/dist/native/escape-props.js +2 -0
  23. package/dist/native/grid-auto-placement.js +83 -0
  24. package/dist/native/grid-placement.js +20 -0
  25. package/dist/native/index.shared.js +7 -5
  26. package/dist/native/keyboard-inset.mobile.js +5 -5
  27. package/dist/native/navigation-request.js +26 -0
  28. package/dist/native/octane-client-build.json +1 -1
  29. package/dist/native/overlay-lifetime.mobile.js +65 -0
  30. package/dist/native/pan.tsrx.js +16 -2
  31. package/dist/native/route-table.js +90 -10
  32. package/dist/native/route.js +26 -16
  33. package/dist/native/shared.js +7 -5
  34. package/dist/native/sheet-service.js +2 -2
  35. package/dist/native/svg.mobile.js +1 -1
  36. package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
  37. package/dist/native/text-focus.js +10 -0
  38. package/dist/native/text-write.js +18 -11
  39. package/dist/native/toast-anchor.tsrx.js +1 -1
  40. package/dist/native/ui.js +7 -6
  41. package/dist/native/useMeasure.tsrx.js +17 -18
  42. package/dist/native/vendor/ui-svg/index.android.js +2 -2
  43. package/dist/native/vendor/ui-svg/index.ios.js +2 -2
  44. package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
  45. package/dist/native/virtual-list-cell-pool.js +63 -0
  46. package/dist/web/Button.web.tsrx.js +1 -1
  47. package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
  48. package/dist/web/Markdown.tsrx.js +98 -0
  49. package/dist/web/Overlay.web.tsrx.js +81 -32
  50. package/dist/web/PinInput.web.tsrx.js +24 -10
  51. package/dist/web/Pressable.web.tsrx.js +169 -53
  52. package/dist/web/SearchInput.web.tsrx.js +2 -2
  53. package/dist/web/Sheet.web.tsrx.js +20 -7
  54. package/dist/web/TextArea.web.tsrx.js +22 -17
  55. package/dist/web/TextInput.web.tsrx.js +2 -2
  56. package/dist/web/VirtualList.web.tsrx.js +220 -109
  57. package/dist/web/WebView.web.tsrx.js +3 -3
  58. package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
  59. package/dist/web/modal-focus.web.js +95 -0
  60. package/dist/web/navigation-request.js +26 -0
  61. package/dist/web/octane-client-build.json +1 -1
  62. package/dist/web/pan.web.tsrx.js +27 -10
  63. package/dist/web/route-table.js +90 -10
  64. package/dist/web/route.web.js +88 -22
  65. package/dist/web/sheet-service.web.js +8 -0
  66. package/dist/web/theme/colorScheme.web.js +31 -4
  67. package/dist/web/ui.js +8 -6
  68. package/dist/web/virtual-list-cell-pool.js +63 -0
  69. package/package.json +21 -19
  70. package/platforms/android/native-api-usage.json +7 -7
  71. package/src/CommandPalette.tsrx +8 -1
  72. package/src/Grid.tsrx +32 -1
  73. package/src/Icon.macos.tsrx +1 -0
  74. package/src/KeyboardAvoiding.macos.tsrx +7 -1
  75. package/src/KeyboardAvoiding.mobile.tsrx +2 -2
  76. package/src/KeyboardAvoiding.tsrx +15 -0
  77. package/src/KeyboardAvoiding.web.tsrx +15 -0
  78. package/src/Markdown.tsrx +121 -0
  79. package/src/Meter.tsrx +1 -0
  80. package/src/Overlay.tsrx +15 -30
  81. package/src/Overlay.web.tsrx +12 -3
  82. package/src/Pagination.tsrx +6 -4
  83. package/src/PinInput.macos.tsrx +20 -6
  84. package/src/PinInput.mobile.test.ts +107 -0
  85. package/src/PinInput.tsrx +20 -6
  86. package/src/PinInput.web.test.tsrx +42 -0
  87. package/src/PinInput.web.tsrx +20 -6
  88. package/src/Popover.tsrx +36 -22
  89. package/src/Pressable.tsrx +1 -1
  90. package/src/Pressable.web.test.tsrx +36 -0
  91. package/src/Pressable.web.tsrx +37 -6
  92. package/src/RouteHost.macos.tsrx +1 -0
  93. package/src/Screen.tsrx +1 -1
  94. package/src/SearchInput.tsrx +4 -3
  95. package/src/SearchInput.web.tsrx +1 -1
  96. package/src/Sheet.tsrx +21 -42
  97. package/src/Sheet.web.tsrx +9 -1
  98. package/src/Slider.tsrx +13 -11
  99. package/src/Text.tsrx +36 -0
  100. package/src/TextArea.tsrx +6 -5
  101. package/src/TextArea.web.test.tsrx +39 -0
  102. package/src/TextArea.web.tsrx +7 -5
  103. package/src/TextInput.tsrx +6 -6
  104. package/src/TextInput.web.tsrx +1 -1
  105. package/src/VirtualList.macos.tsrx +93 -6
  106. package/src/VirtualList.mobile.test.tsrx +232 -0
  107. package/src/VirtualList.tsrx +285 -81
  108. package/src/VirtualList.web.tsrx +131 -25
  109. package/src/WebView.tsrx +20 -10
  110. package/src/WebView.web.tsrx +3 -0
  111. package/src/android/BottomNavigationView.android.tsrx +1 -0
  112. package/src/android/MaterialDialog.android.tsrx +1 -0
  113. package/src/anim.macos.tsrx +1 -0
  114. package/src/apply-native-modifiers.native.ts +4 -1
  115. package/src/child-array.ts +12 -7
  116. package/src/colorScheme.macos.ts +1 -0
  117. package/src/controls.macos.tsrx +4 -1
  118. package/src/data-driver.mobile.test.ts +120 -0
  119. package/src/data-lifecycle.mobile.test.ts +251 -0
  120. package/src/escape-props.ts +13 -1
  121. package/src/grid-auto-placement.ts +163 -0
  122. package/src/grid-placement.ts +29 -0
  123. package/src/hosted-unsupported.macos.tsrx +1 -0
  124. package/src/index.macos.ts +23 -3
  125. package/src/index.mobile.ts +0 -1
  126. package/src/index.shared.ts +22 -1
  127. package/src/index.shared.web.ts +291 -0
  128. package/src/index.web.ts +1 -1
  129. package/src/input-readiness.web.test.tsrx +82 -0
  130. package/src/ios/UITabBar.ios.tsrx +1 -0
  131. package/src/keyboard-inset.mobile.test.ts +62 -0
  132. package/src/keyboard-inset.mobile.ts +63 -43
  133. package/src/layout-shells.macos.tsrx +1 -5
  134. package/src/macos-extras.macos.tsrx +2 -0
  135. package/src/modal-focus.web.test.tsrx +63 -0
  136. package/src/modal-focus.web.ts +162 -0
  137. package/src/navigation-request.ts +37 -0
  138. package/src/overlay-lifecycle.mobile.test.ts +316 -0
  139. package/src/overlay-lifetime.mobile.ts +107 -0
  140. package/src/pan.mobile.test.tsrx +116 -0
  141. package/src/pan.tsrx +20 -2
  142. package/src/pan.web.tsrx +35 -7
  143. package/src/platformProps.ts +1 -0
  144. package/src/popover.mobile.test.ts +9 -1
  145. package/src/pressable-accessibility.mobile.test.ts +30 -0
  146. package/src/props.ts +163 -3
  147. package/src/refresh.ts +6 -1
  148. package/src/refresh.web.test.ts +77 -77
  149. package/src/route-android.test.ts +162 -0
  150. package/src/route-history.web.test.ts +80 -0
  151. package/src/route-navigation.web.test.ts +173 -0
  152. package/src/route-table.test.ts +188 -9
  153. package/src/route-table.ts +138 -10
  154. package/src/route.macos.ts +56 -13
  155. package/src/route.ts +54 -24
  156. package/src/route.web.ts +141 -24
  157. package/src/sheet-detents.ts +5 -3
  158. package/src/sheet-service.macos.ts +5 -1
  159. package/src/sheet-service.ts +1 -1
  160. package/src/sheet-service.web.ts +8 -0
  161. package/src/sheet-snap.test.ts +23 -23
  162. package/src/sheet-snap.ts +6 -1
  163. package/src/svg.mobile.ts +9 -3
  164. package/src/tap-to-blur.ios.ts +106 -0
  165. package/src/tap-to-blur.mobile.ts +21 -95
  166. package/src/text-focus.ts +12 -0
  167. package/src/text-input.mobile.test.ts +89 -0
  168. package/src/text-write.ts +21 -11
  169. package/src/theme/colorScheme.linux.ts +2 -4
  170. package/src/theme/colorScheme.web.ts +51 -4
  171. package/src/theme/theme-scheme.macos.ts +8 -2
  172. package/src/theme/tokens.css +1740 -1718
  173. package/src/translate.macos.ts +4 -1
  174. package/src/useMeasure.tsrx +20 -21
  175. package/src/virtual-list-cell-pool.test.ts +53 -0
  176. package/src/virtual-list-cell-pool.ts +81 -0
  177. package/src/virtual-list-layout.test.ts +11 -8
  178. package/src/virtual-list-layout.ts +4 -8
  179. package/src/windows.linux.ts +7 -2
  180. package/src/windows.macos.ts +1 -0
  181. package/types/generated/.tsrx-typegen-manifest.json +16 -3
  182. package/types/generated/Grid.d.ts +0 -1
  183. package/types/generated/KeyboardAvoiding.d.ts +3 -0
  184. package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
  185. package/types/generated/Markdown.d.ts +59 -0
  186. package/types/generated/TextArea.d.ts +2 -2
  187. package/types/generated/TextArea.web.d.ts +1 -1
  188. package/types/generated/TextInput.d.ts +2 -3
  189. package/types/generated/VirtualList.d.ts +3 -3
  190. package/types/generated/escape-props.d.ts +4 -0
  191. package/types/generated/grid-auto-placement.d.ts +13 -0
  192. package/types/generated/grid-placement.d.ts +5 -0
  193. package/types/generated/index.mobile.d.ts +0 -1
  194. package/types/generated/index.shared.d.ts +6 -3
  195. package/types/generated/index.shared.web.d.ts +114 -0
  196. package/types/generated/index.web.d.ts +1 -1
  197. package/types/generated/modal-focus.web.d.ts +3 -0
  198. package/types/generated/navigation-request.d.ts +10 -0
  199. package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
  200. package/types/generated/props.d.ts +111 -3
  201. package/types/generated/route-table.d.ts +14 -3
  202. package/types/generated/svg.mobile.d.ts +2 -2
  203. package/types/generated/tap-to-blur.ios.d.ts +13 -0
  204. package/types/generated/tap-to-blur.mobile.d.ts +1 -15
  205. package/types/generated/text-focus.d.ts +3 -0
  206. package/types/generated/text-write.d.ts +3 -1
  207. package/types/generated/theme/colorScheme.web.d.ts +5 -0
  208. package/types/generated/virtual-list-cell-pool.d.ts +18 -0
  209. package/types/index.macos.d.ts +15 -3
  210. /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
  211. /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
  212. /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
  213. /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
  214. /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
  215. /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
  216. /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
  217. /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
  218. /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
  219. /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
@@ -0,0 +1,95 @@
1
+ //#region src/modal-focus.web.ts
2
+ var scopes = [];
3
+ var originalInert = /* @__PURE__ */ new Map();
4
+ var observer;
5
+ function topScope() {
6
+ return scopes[scopes.length - 1];
7
+ }
8
+ function updateIsolation() {
9
+ const top = topScope();
10
+ if (!top) {
11
+ for (const [element, inert] of originalInert) if (!inert) element.removeAttribute("inert");
12
+ originalInert.clear();
13
+ return;
14
+ }
15
+ for (const element of Array.from(top.layer.ownerDocument.body.children)) {
16
+ if (!originalInert.has(element)) originalInert.set(element, element.hasAttribute("inert"));
17
+ const inert = originalInert.get(element) || !element.contains(top.layer);
18
+ element.toggleAttribute("inert", inert);
19
+ }
20
+ }
21
+ function tabStops(panel) {
22
+ return Array.from(panel.querySelectorAll("a[href],button,input,textarea,select,[tabindex],[contenteditable=\"true\"]")).filter((element) => {
23
+ if (element.tabIndex < 0 || element.matches(":disabled") || element.closest("[inert],[hidden]")) return false;
24
+ for (let parent = element; parent; parent = parent.parentElement) {
25
+ const style = getComputedStyle(parent);
26
+ if (style.display === "none" || style.visibility === "hidden") return false;
27
+ }
28
+ return true;
29
+ });
30
+ }
31
+ /** Own keyboard focus only for a modal body portal. Inert preserves the
32
+ * background's state while hiding it from both focus and the browser AX tree. */
33
+ function isolateModalFocus(layer, panel, dismiss) {
34
+ const document = layer.ownerDocument;
35
+ const scope = {
36
+ layer,
37
+ panel,
38
+ trigger: document.activeElement
39
+ };
40
+ const tabIndex = panel.getAttribute("tabindex");
41
+ if (tabIndex === null) panel.tabIndex = -1;
42
+ scopes.push(scope);
43
+ updateIsolation();
44
+ (tabStops(panel)[0] ?? panel).focus({ preventScroll: true });
45
+ if (!observer) {
46
+ observer = new MutationObserver(updateIsolation);
47
+ observer.observe(document.body, { childList: true });
48
+ }
49
+ const focusInside = () => (tabStops(panel)[0] ?? panel).focus({ preventScroll: true });
50
+ const onFocus = (event) => {
51
+ if (topScope() === scope && !panel.contains(event.target)) focusInside();
52
+ };
53
+ const onKey = (event) => {
54
+ if (topScope() !== scope || event.isComposing || event.keyCode === 229) return;
55
+ if (event.key === "Escape") {
56
+ event.preventDefault();
57
+ event.stopPropagation();
58
+ dismiss();
59
+ } else if (event.key === "Tab") {
60
+ const stops = tabStops(panel);
61
+ const index = stops.indexOf(document.activeElement);
62
+ if (!stops.length || index < 0 || (event.shiftKey ? index === 0 : index === stops.length - 1)) {
63
+ event.preventDefault();
64
+ (stops.length ? stops[event.shiftKey ? stops.length - 1 : 0] : panel).focus({ preventScroll: true });
65
+ }
66
+ }
67
+ };
68
+ document.addEventListener("focusin", onFocus);
69
+ document.addEventListener("keydown", onKey, true);
70
+ return () => {
71
+ const wasTop = topScope() === scope;
72
+ const index = scopes.indexOf(scope);
73
+ if (index < 0) return;
74
+ for (const other of scopes) if (other !== scope && other.trigger && scope.layer.contains(other.trigger)) other.trigger = scope.trigger;
75
+ scopes.splice(index, 1);
76
+ document.removeEventListener("focusin", onFocus);
77
+ document.removeEventListener("keydown", onKey, true);
78
+ if (tabIndex === null) panel.removeAttribute("tabindex");
79
+ updateIsolation();
80
+ if (!scopes.length) {
81
+ observer?.disconnect();
82
+ observer = void 0;
83
+ }
84
+ if (wasTop) {
85
+ const trigger = scope.trigger;
86
+ if (trigger?.isConnected && !trigger.closest("[inert]")) trigger.focus({ preventScroll: true });
87
+ else {
88
+ const remaining = topScope();
89
+ if (remaining) (tabStops(remaining.panel)[0] ?? remaining.panel).focus({ preventScroll: true });
90
+ }
91
+ }
92
+ };
93
+ }
94
+ //#endregion
95
+ export { isolateModalFocus };
@@ -0,0 +1,26 @@
1
+ //#region src/navigation-request.ts
2
+ function createNavigationRequests() {
3
+ const versions = /* @__PURE__ */ new Map();
4
+ const invalidate = (stack) => {
5
+ const version = (versions.get(stack) ?? 0) + 1;
6
+ versions.set(stack, version);
7
+ return version;
8
+ };
9
+ return {
10
+ invalidate,
11
+ begin(stack) {
12
+ const owned = /* @__PURE__ */ new Map([[stack, invalidate(stack)]]);
13
+ const isCurrent = () => [...owned].every(([name, version]) => versions.get(name) === version);
14
+ return {
15
+ isCurrent,
16
+ claim(name) {
17
+ if (!isCurrent()) return false;
18
+ if (!owned.has(name)) owned.set(name, invalidate(name));
19
+ return true;
20
+ }
21
+ };
22
+ }
23
+ };
24
+ }
25
+ //#endregion
26
+ export { createNavigationRequests };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": 1,
3
- "buildId": "b6a64d5b-cfff-418e-921b-443efbc3bad2",
3
+ "buildId": "ff3183ba-911e-4372-ad13-5b2ce15c5b1b",
4
4
  "mode": "production",
5
5
  "capabilities": {
6
6
  "independentHydration": false
@@ -1,26 +1,43 @@
1
- import { hookSlots, useEffect } from "octane";
1
+ import { hookSlots, useEffect, useRef } from "octane";
2
2
  //#region src/pan.web.tsrx
3
- var _h$0 = /* @__PURE__ */ Symbol(/* @__PURE__ */ hookSlots(1));
3
+ var _hs$ = /* @__PURE__ */ hookSlots(2);
4
+ var _h$0 = /* @__PURE__ */ Symbol(_hs$);
5
+ var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
6
+ var VELOCITY_MAX_AGE_MS = 100;
4
7
  function usePan(el, cb) {
8
+ const callback = useRef(cb, _h$0);
9
+ callback.current = cb;
10
+ const enabled = Boolean(cb);
5
11
  useEffect(() => {
6
12
  const node = el.current;
7
- if (!node || !cb) return;
8
- let sx = 0, sy = 0, active = false, last = 0, lx = 0, t0 = 0;
13
+ if (!node || !enabled) return;
14
+ let sx = 0, sy = 0, active = false;
15
+ let last = 0, lx = 0, t0 = 0;
16
+ let vx = 0, vy = 0, velocityAt = 0;
9
17
  const emit = (e, state) => {
10
18
  const now = performance.now();
11
19
  const dt = Math.max(now - t0, 1) / 1e3;
12
- const vx = (e.clientX - lx) / dt;
13
- const vy = (e.clientY - last) / dt;
20
+ const dx = e.clientX - lx;
21
+ const dy = e.clientY - last;
22
+ if (state === "began") {
23
+ vx = vy = 0;
24
+ velocityAt = now;
25
+ } else if (dx !== 0 || dy !== 0) {
26
+ vx = dx / dt;
27
+ vy = dy / dt;
28
+ velocityAt = now;
29
+ }
14
30
  lx = e.clientX;
15
31
  last = e.clientY;
16
32
  t0 = now;
17
- cb({
33
+ const fresh = now - velocityAt <= VELOCITY_MAX_AGE_MS;
34
+ callback.current?.({
18
35
  x: e.clientX - sx,
19
36
  y: e.clientY - sy,
20
37
  dx: e.clientX - sx,
21
38
  dy: e.clientY - sy,
22
- vx,
23
- vy,
39
+ vx: fresh ? vx : 0,
40
+ vy: fresh ? vy : 0,
24
41
  state,
25
42
  target: node
26
43
  });
@@ -60,7 +77,7 @@ function usePan(el, cb) {
60
77
  node.removeEventListener("pointerup", up);
61
78
  node.removeEventListener("pointercancel", cancel);
62
79
  };
63
- }, [el, cb], _h$0);
80
+ }, [el, enabled], _h$1);
64
81
  }
65
82
  //#endregion
66
83
  export { usePan };
@@ -2,7 +2,22 @@
2
2
  var EXT = /\.(tsrx|tsx|ts|mts|cts|js|mjs|cjs|jsx)$/;
3
3
  var SUFFIX = /\.(web|mobile|ios|android|macos|windows|linux)$/;
4
4
  var PARAM = /^\[(.+)\]$/;
5
+ /** Build-time loader modules — `docs.loader.ts` pairs with route `docs`
6
+ * for `dataMode: 'baked'`. Never a route itself, never in a runtime glob. */
7
+ var LOADER_FILE = /\.loader\.(ts|mts|cts|js|mjs|cjs|jsx)$/;
5
8
  var PRESENT = /\+(modal|fade|push)$/;
9
+ /** Loader for a `dataMode: 'baked'` route — resolves the name out of its
10
+ * owning manifest's `baked` map at push time. The map is assigned after
11
+ * the manifest is built (generated glue) or passed in (RouteSpecSet), so
12
+ * the lookup reads it lazily. A missing entry throws, landing as the
13
+ * screen's `error` prop like a live loader rejection. */
14
+ function bakedLookup(manifest, name) {
15
+ return () => {
16
+ const baked = manifest()?.baked;
17
+ if (baked && Object.prototype.hasOwnProperty.call(baked, name)) return baked[name];
18
+ throw new Error(`[octane-xplat] baked route '${name}' has no baked data — run \`xplat routes\` to regenerate`);
19
+ };
20
+ }
6
21
  /** Internal error carrier used by the public `redirect()` helper. Keeping
7
22
  * this in the shared route table gives both platform leaves the same guard
8
23
  * behavior without importing a platform runtime. */
@@ -35,7 +50,9 @@ function deriveRouteManifest(files, prefer, dir = "app") {
35
50
  const layouts = {};
36
51
  const loaders = {};
37
52
  const warned = /* @__PURE__ */ new Set();
53
+ const self = {};
38
54
  for (const key of Object.keys(files).sort()) {
55
+ if (LOADER_FILE.test(key)) continue;
39
56
  let rel = key.replace(/^\.?\//, "");
40
57
  if (rel.startsWith(prefix)) rel = rel.slice(prefix.length);
41
58
  rel = rel.replace(EXT, "");
@@ -74,8 +91,13 @@ function deriveRouteManifest(files, prefer, dir = "app") {
74
91
  file: key,
75
92
  presentation
76
93
  };
94
+ const dataMode = files[key]?.dataMode;
95
+ if (dataMode === "baked" || dataMode === "live") meta.dataMode = dataMode;
77
96
  const loader = files[key]?.loader;
78
- if (typeof loader === "function") meta.loader = loader;
97
+ if (meta.dataMode === "baked") {
98
+ meta.loader = bakedLookup(() => self.m, name);
99
+ if (typeof loader === "function") console.warn(`[octane-xplat] baked route '${name}' also exports a loader — move it to a .loader.ts sibling so its imports stay out of the bundle`);
100
+ } else if (typeof loader === "function") meta.loader = loader;
79
101
  const beforeLoad = files[key]?.beforeLoad;
80
102
  if (typeof beforeLoad === "function") meta.beforeLoad = beforeLoad;
81
103
  if (files[key]?.head && (typeof files[key].head === "object" || typeof files[key].head === "function")) meta.head = files[key].head;
@@ -102,12 +124,14 @@ function deriveRouteManifest(files, prefer, dir = "app") {
102
124
  if (meta.loader) loaders[meta.name] = meta.loader;
103
125
  }
104
126
  routes.sort(bySpecificity);
105
- return {
127
+ const manifest = {
106
128
  screens,
107
129
  routes,
108
130
  layouts,
109
131
  loaders
110
132
  };
133
+ self.m = manifest;
134
+ return manifest;
111
135
  }
112
136
  /** Match-order comparator: static segments (2) outrank params (1), ties
113
137
  * break alphabetically so ordering is total and stable across merges. */
@@ -131,7 +155,12 @@ var mergeWarned = /* @__PURE__ */ new Set();
131
155
  * from their own sources, e.g. `readdirSync` over a content dir). Each
132
156
  * spec's `path` uses the route-dir vocabulary; `layouts` keys are path
133
157
  * prefixes playing the `_layout` role. Same-name duplicates inside the
134
- * set keep the later entry with a warn, matching merge precedence. */
158
+ * set keep the later entry with a warn, matching merge precedence.
159
+ *
160
+ * The return is branded with the spec-derived route types — read them
161
+ * with `ManifestRouteParams`/`ManifestRouteNames`/
162
+ * `ManifestRoutePresentations` to merge programmatic names into the
163
+ * app's typed navigation surface (`RouteName | …`, `RouteParams & …`). */
135
164
  function defineRoutes(input) {
136
165
  const set = Array.isArray(input) ? { routes: input } : input;
137
166
  const specs = set.routes;
@@ -140,6 +169,7 @@ function defineRoutes(input) {
140
169
  const layouts = {};
141
170
  const metas = /* @__PURE__ */ new Map();
142
171
  const warned = /* @__PURE__ */ new Set();
172
+ const self = {};
143
173
  for (const [key, layout] of Object.entries(extra ?? {})) {
144
174
  const dir = key.replace(/^\/+|\/+$/g, "");
145
175
  if (dir !== key) console.warn(`[octane-xplat] layout key '${key}' normalized to '${dir}'`);
@@ -155,7 +185,11 @@ function defineRoutes(input) {
155
185
  file: spec.source ?? `programmatic:${spec.path}`,
156
186
  presentation: spec.presentation
157
187
  };
158
- if (spec.loader) meta.loader = spec.loader;
188
+ if (spec.dataMode) meta.dataMode = spec.dataMode;
189
+ if (spec.dataMode === "baked") {
190
+ meta.loader = bakedLookup(() => self.m, name);
191
+ if (spec.loader) console.warn(`[octane-xplat] baked route '${name}' also carries a loader — bake it in the host and pass the result via 'baked' so its code stays out of the bundle`);
192
+ } else if (spec.loader) meta.loader = spec.loader;
159
193
  if (spec.beforeLoad) meta.beforeLoad = spec.beforeLoad;
160
194
  if (spec.head) meta.head = spec.head;
161
195
  if (metas.has(name) && !warned.has(name)) {
@@ -172,12 +206,15 @@ function defineRoutes(input) {
172
206
  const routes = [...metas.values()].sort(bySpecificity);
173
207
  const loaders = {};
174
208
  for (const meta of routes) if (meta.loader) loaders[meta.name] = meta.loader;
175
- return {
209
+ const manifest = {
176
210
  screens,
177
211
  routes,
178
212
  layouts,
179
- loaders
213
+ loaders,
214
+ baked: set.baked
180
215
  };
216
+ self.m = manifest;
217
+ return manifest;
181
218
  }
182
219
  /** Compose manifests into one — file-derived base plus any number of
183
220
  * programmatic sets (`registerRoutes(mergeRouteManifests(fromDir, dynamic))`).
@@ -188,9 +225,11 @@ function defineRoutes(input) {
188
225
  function mergeRouteManifests(...manifests) {
189
226
  const screens = {};
190
227
  const layouts = {};
228
+ const baked = {};
191
229
  const winners = /* @__PURE__ */ new Map();
192
230
  for (const m of manifests) {
193
231
  Object.assign(layouts, m.layouts);
232
+ Object.assign(baked, m.baked);
194
233
  Object.assign(screens, m.screens);
195
234
  for (const meta of m.routes) {
196
235
  const prev = winners.get(meta.name);
@@ -208,7 +247,38 @@ function mergeRouteManifests(...manifests) {
208
247
  screens,
209
248
  routes,
210
249
  layouts,
211
- loaders
250
+ loaders,
251
+ baked
252
+ };
253
+ }
254
+ /** Serialize a manifest to the normalized `RouteManifestJson` shape — the
255
+ * same list `xplat routes` emits for file routes, produced here for
256
+ * programmatic/merged manifests so hosts consume one schema. */
257
+ function manifestToJson(manifest) {
258
+ return {
259
+ version: 1,
260
+ layouts: Object.keys(manifest.layouts).sort(),
261
+ screens: Object.keys(manifest.screens).sort(),
262
+ routes: manifest.routes.map((meta) => {
263
+ const segs = meta.name.split("/");
264
+ const layoutDirs = manifest.layouts[""] ? [""] : [];
265
+ for (let i = 1; i < segs.length; i++) {
266
+ const d = segs.slice(0, i).join("/");
267
+ if (manifest.layouts[d]) layoutDirs.push(d);
268
+ }
269
+ return {
270
+ name: meta.name,
271
+ path: meta.name === "index" ? "" : meta.segments.join("/"),
272
+ params: [...meta.params],
273
+ ...meta.presentation ? { presentation: meta.presentation } : {},
274
+ ...meta.dataMode ? { dataMode: meta.dataMode } : {},
275
+ layouts: layoutDirs,
276
+ source: meta.file,
277
+ loader: !!meta.loader,
278
+ guard: !!meta.beforeLoad,
279
+ head: !!meta.head
280
+ };
281
+ })
212
282
  };
213
283
  }
214
284
  /** Match URL path segments against a manifest — returns the winning meta
@@ -309,9 +379,19 @@ function linkPath(url) {
309
379
  * is the stack prefix ('/demos/demo/x' → stack 'demos'). Unmatched names
310
380
  * fall through as literal routes for pre-manifest callers. */
311
381
  function matchUrl(routes, url) {
312
- const [p, qs] = url.split("?");
382
+ url = url.split("#", 1)[0];
383
+ const at = url.indexOf("?");
384
+ const p = at === -1 ? url : url.slice(0, at);
385
+ const qs = at === -1 ? void 0 : url.slice(at + 1);
313
386
  const segs = p.split("/").filter(Boolean);
314
- const query = parseQueryString(qs);
387
+ let query;
388
+ try {
389
+ if (!decodeURIComponent(p)) return null;
390
+ query = parseQueryString(qs);
391
+ } catch (error) {
392
+ if (error instanceof URIError) return null;
393
+ throw error;
394
+ }
315
395
  if (!segs.length) return null;
316
396
  const finish = (stack, m, fallbackName) => ({
317
397
  stack,
@@ -347,4 +427,4 @@ function layoutChain(layouts, name) {
347
427
  return chain;
348
428
  }
349
429
  //#endregion
350
- export { RouteRedirect, buildRoutePath, defineRoutes, deriveRouteManifest, layoutChain, linkPath, matchRoute, matchUrl, mergeRouteManifests, parseQueryString };
430
+ export { RouteRedirect, buildRoutePath, defineRoutes, deriveRouteManifest, layoutChain, linkPath, manifestToJson, matchRoute, matchUrl, mergeRouteManifests, parseQueryString };
@@ -1,6 +1,8 @@
1
+ import { createNavigationRequests } from "./navigation-request.js";
1
2
  import { RouteRedirect, buildRoutePath, layoutChain, linkPath, matchUrl, mergeRouteManifests } from "./route-table.js";
2
3
  import { hookSlots, useSyncExternalStore } from "octane";
3
4
  //#region src/route.web.ts
5
+ var navigationRequests = createNavigationRequests();
4
6
  var baseManifest = {
5
7
  screens: {},
6
8
  routes: [],
@@ -18,7 +20,7 @@ function rebuildRegistry() {
18
20
  routeLayouts = merged.layouts;
19
21
  routeLoaders = merged.loaders ?? {};
20
22
  if (current !== void 0) {
21
- current = parse();
23
+ restoreRouteState();
22
24
  emit();
23
25
  }
24
26
  }
@@ -40,7 +42,8 @@ function registerScreens(table, manifest) {
40
42
  function registerRoutes(manifest) {
41
43
  baseManifest = manifest;
42
44
  rebuildRegistry();
43
- applyHead(read());
45
+ read();
46
+ applyHead(modalRoute ?? current ?? null);
44
47
  }
45
48
  /** Layer a programmatic manifest (from `defineRoutes`) over the registered
46
49
  * routes — same-name entries win over the base with a warn, so a host
@@ -49,7 +52,8 @@ function registerRoutes(manifest) {
49
52
  function addRoutes(manifest) {
50
53
  dynamicManifests.push(manifest);
51
54
  rebuildRegistry();
52
- applyHead(read());
55
+ read();
56
+ applyHead(modalRoute ?? current ?? null);
53
57
  }
54
58
  function screenFor(name) {
55
59
  return screens[name];
@@ -63,6 +67,53 @@ var listeners = /* @__PURE__ */ new Set();
63
67
  var current;
64
68
  var modalRoute = null;
65
69
  var historyDepth = history.state?.__octaneXplatDepth ?? 0;
70
+ var historyRoutes = /* @__PURE__ */ new Map();
71
+ var historyEntryPrefix = Math.random().toString(36).slice(2);
72
+ var nextHistoryEntry = 0;
73
+ function rememberRouteState(entry = historyEntryPrefix + ":" + ++nextHistoryEntry) {
74
+ historyRoutes.set(entry, {
75
+ current: current ?? null,
76
+ modal: modalRoute
77
+ });
78
+ return entry;
79
+ }
80
+ function restoreRouteState() {
81
+ const saved = historyRoutes.get(history.state?.__octaneXplatEntry);
82
+ if (saved) {
83
+ current = saved.current;
84
+ modalRoute = saved.modal;
85
+ } else {
86
+ const route = parse();
87
+ modalRoute = route?.presentation === "modal" ? route : null;
88
+ current = modalRoute ? null : route;
89
+ if (route) hydrateRoute(route);
90
+ }
91
+ }
92
+ function hydrateRoute(route) {
93
+ const loader = routeLoaders[route.name] ?? metaFor(route.name)?.loader;
94
+ if (!loader || Object.prototype.hasOwnProperty.call(route, "loaderData") || Object.prototype.hasOwnProperty.call(route, "loaderError")) return;
95
+ const finish = (result) => {
96
+ const prepared = {
97
+ ...route,
98
+ ...result
99
+ };
100
+ let changed = false;
101
+ if (current === route) {
102
+ current = prepared;
103
+ changed = true;
104
+ }
105
+ if (modalRoute === route) {
106
+ modalRoute = prepared;
107
+ changed = true;
108
+ }
109
+ for (const saved of historyRoutes.values()) {
110
+ if (saved.current === route) saved.current = prepared;
111
+ if (saved.modal === route) saved.modal = prepared;
112
+ }
113
+ if (changed) emit();
114
+ };
115
+ Promise.resolve().then(() => loader(route.params)).then((loaderData) => finish({ loaderData }), (loaderError) => finish({ loaderError }));
116
+ }
66
117
  var ROUTE_HEAD_ATTR = "data-octane-xplat-route-head";
67
118
  var scrollPositions = /* @__PURE__ */ new Map();
68
119
  var lastKey = location.pathname + location.search;
@@ -76,8 +127,8 @@ function parse() {
76
127
  return matchUrl(routes, location.pathname + location.search);
77
128
  }
78
129
  function read() {
79
- if (current === void 0) current = parse();
80
- return current;
130
+ if (current === void 0) restoreRouteState();
131
+ return current ?? null;
81
132
  }
82
133
  function emit() {
83
134
  listeners.forEach((l) => l());
@@ -109,10 +160,11 @@ function applyHead(r) {
109
160
  function contextFor(r) {
110
161
  return r.context ?? {};
111
162
  }
112
- async function prepareRoute(r, redirects = 0) {
163
+ async function prepareRoute(r, request, redirects = 0) {
164
+ if (!request.isCurrent()) return;
113
165
  const beforeLoad = metaFor(r.name)?.beforeLoad;
114
166
  if (!beforeLoad) {
115
- commitRoute(r);
167
+ commitRoute(r, request);
116
168
  return;
117
169
  }
118
170
  if (redirects > 16) {
@@ -131,10 +183,11 @@ async function prepareRoute(r, redirects = 0) {
131
183
  commitRoute({
132
184
  ...r,
133
185
  context
134
- });
186
+ }, request);
135
187
  } catch (e) {
188
+ if (!request.isCurrent()) return;
136
189
  if (e instanceof RouteRedirect) {
137
- await prepareRoute(e.route, redirects + 1);
190
+ await prepareRoute(e.route, request, redirects + 1);
138
191
  return;
139
192
  }
140
193
  console.warn(`[octane-xplat] beforeLoad('${r.name}') rejected: ${e?.message ?? e}`);
@@ -144,13 +197,15 @@ function redirect(r) {
144
197
  throw new RouteRedirect(r);
145
198
  }
146
199
  function pushRoute(r) {
200
+ const request = navigationRequests.begin("history");
147
201
  if (metaFor(r.name)?.beforeLoad) {
148
- prepareRoute(r);
202
+ prepareRoute(r, request);
149
203
  return;
150
204
  }
151
- commitRoute(r);
205
+ commitRoute(r, request);
152
206
  }
153
- function commitRoute(r) {
207
+ function commitRoute(r, request) {
208
+ if (!request.isCurrent()) return;
154
209
  const route = {
155
210
  ...r,
156
211
  presentation: r.presentation ?? presentationFor(r.name)
@@ -160,21 +215,30 @@ function commitRoute(r) {
160
215
  Promise.resolve().then(() => loader(route.params)).then((loaderData) => commitRoute({
161
216
  ...route,
162
217
  loaderData
163
- }), (loaderError) => commitRoute({
218
+ }, request), (loaderError) => commitRoute({
164
219
  ...route,
165
220
  loaderError
166
- }));
221
+ }, request));
167
222
  return;
168
223
  }
169
224
  saveScroll();
225
+ read();
226
+ history.replaceState({
227
+ ...history.state,
228
+ __octaneXplatDepth: historyDepth,
229
+ __octaneXplatEntry: rememberRouteState(history.state?.__octaneXplatEntry)
230
+ }, "");
170
231
  historyDepth += 1;
171
- history.pushState({ __octaneXplatDepth: historyDepth }, "", buildRoutePath(routes, route));
172
- lastKey = scrollKey();
173
232
  if (route.presentation === "modal") modalRoute = route;
174
233
  else {
175
234
  current = route;
176
235
  modalRoute = null;
177
236
  }
237
+ history.pushState({
238
+ __octaneXplatDepth: historyDepth,
239
+ __octaneXplatEntry: rememberRouteState()
240
+ }, "", buildRoutePath(routes, route));
241
+ lastKey = scrollKey();
178
242
  applyHead(modalRoute ?? current ?? null);
179
243
  emit();
180
244
  }
@@ -185,6 +249,7 @@ function presentationFor(name) {
185
249
  * per-stack pops aren't expressible, so `stack` is accepted for parity
186
250
  * and ignored. A pushed modal is the top entry, so back dismisses it. */
187
251
  function popRoute(_stack = "root") {
252
+ navigationRequests.invalidate("history");
188
253
  history.back();
189
254
  }
190
255
  var backInterceptorWarned = false;
@@ -200,12 +265,12 @@ function addBackInterceptor(_fn) {
200
265
  return () => {};
201
266
  }
202
267
  window.addEventListener("popstate", () => {
268
+ navigationRequests.invalidate("history");
203
269
  saveScroll();
204
270
  historyDepth = history.state?.__octaneXplatDepth ?? 0;
205
271
  lastKey = scrollKey();
206
- modalRoute = null;
207
- current = parse();
208
- applyHead(current);
272
+ restoreRouteState();
273
+ applyHead(modalRoute ?? current ?? null);
209
274
  restoreScroll();
210
275
  emit();
211
276
  });
@@ -225,6 +290,7 @@ function currentRoute() {
225
290
  }
226
291
  /** The modal route overlaying the current one, if any. */
227
292
  function currentModalRoute() {
293
+ read();
228
294
  return modalRoute;
229
295
  }
230
296
  /** Deep-link entry, web half — the URL *is* the route state, so this is a
@@ -232,7 +298,7 @@ function currentModalRoute() {
232
298
  * apps wire `onDeepLink(pushDeepLink)`). */
233
299
  function pushDeepLink(url) {
234
300
  const r = matchUrl(routes, linkPath(url));
235
- if (!r) {
301
+ if (!r || !screenFor(r.name)) {
236
302
  console.warn(`[octane-xplat] pushDeepLink('${url}') dropped — no route matches.`);
237
303
  return false;
238
304
  }
@@ -252,8 +318,8 @@ function useRoute(stack) {
252
318
  /** Whether the selected conceptual stack has an in-app route to pop. A
253
319
  * browser's pre-app history is intentionally not counted. */
254
320
  function canGoBack(stack = "root") {
255
- const route = read();
256
- return historyDepth > 0 && route?.stack === stack;
321
+ read();
322
+ return historyDepth > 0 && (modalRoute ?? current)?.stack === stack;
257
323
  }
258
324
  function useCanGoBack(stack = "root") {
259
325
  return useSyncExternalStore((cb) => {
@@ -1,4 +1,5 @@
1
1
  import { applyThemeClasses } from "./theme/theme-scheme.js";
2
+ import { isolateModalFocus } from "./modal-focus.web.js";
2
3
  import { attachSheetDetents } from "./sheet-detents.web.js";
3
4
  import { createRoot } from "octane";
4
5
  //#region src/sheet-service.web.ts
@@ -16,10 +17,12 @@ var openSheet = (Component, params, options = {}) => new Promise((resolve) => {
16
17
  panel.className = "vx-sheet";
17
18
  if (options.detents?.length) panel.classList.add("vx-sheet-detents");
18
19
  let finished = false;
20
+ let releaseFocus;
19
21
  const finish = (result) => {
20
22
  if (finished) return;
21
23
  finished = true;
22
24
  active.delete(entry);
25
+ releaseFocus?.();
23
26
  unbindTheme();
24
27
  detents?.detach();
25
28
  root.unmount();
@@ -45,6 +48,11 @@ var openSheet = (Component, params, options = {}) => new Promise((resolve) => {
45
48
  close: finish
46
49
  });
47
50
  active.add(entry);
51
+ if (options.shadeCover ?? true) {
52
+ panel.setAttribute("role", "dialog");
53
+ panel.setAttribute("aria-modal", "true");
54
+ releaseFocus = isolateModalFocus(layer, panel, () => finish());
55
+ }
48
56
  });
49
57
  //#endregion
50
58
  export { closeSheet, openSheet };