@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,116 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ vi.mock('@nativescript/core', () => ({
5
+ Utils: { layout: { getDisplayDensity: () => 2 } },
6
+ }))
7
+
8
+ import {
9
+ createObjectContainer,
10
+ createObjectDriver,
11
+ createUniversalRoot,
12
+ } from 'octane/universal/native'
13
+
14
+ import { usePan } from './pan.tsrx'
15
+ import type { PanEvent } from './props'
16
+
17
+ const events: PanEvent[] = []
18
+
19
+ function PanHost() @{
20
+ const onPan = usePan((event) => events.push(event))
21
+
22
+ <flexboxlayout id="pan-host" onPan={onPan} />
23
+ }
24
+
25
+ function mountPanHost() {
26
+ const container = createObjectContainer('nativescript')
27
+ const root = createUniversalRoot(container, createObjectDriver('nativescript'))
28
+ root.render(PanHost as any, {})
29
+ return { container, root, host: container.children[0] }
30
+ }
31
+
32
+ function nativeEvent(action: number) {
33
+ return { getActionMasked: () => action }
34
+ }
35
+
36
+ function tracker() {
37
+ return {
38
+ addMovement: vi.fn(),
39
+ computeCurrentVelocity: vi.fn(),
40
+ getXVelocity: () => 800,
41
+ getYVelocity: () => -240,
42
+ recycle: vi.fn(),
43
+ }
44
+ }
45
+
46
+ function installAndroid(trackerInstance: ReturnType<typeof tracker>) {
47
+ vi.stubGlobal('android', {
48
+ view: {
49
+ MotionEvent: { ACTION_CANCEL: 3 },
50
+ VelocityTracker: { obtain: () => trackerInstance },
51
+ },
52
+ })
53
+ }
54
+
55
+ afterEach(() => {
56
+ events.length = 0
57
+ vi.unstubAllGlobals()
58
+ })
59
+
60
+ describe('native pan normalization', () => {
61
+ it('preserves Android ACTION_CANCEL as cancelled and clears release velocity', () => {
62
+ const velocityTracker = tracker()
63
+ installAndroid(velocityTracker)
64
+ const { container, root, host } = mountPanHost()
65
+ const target = { id: 'native-pan-target' }
66
+
67
+ container.dispatchEvent(host, 'pan', {
68
+ state: 1,
69
+ deltaX: 0,
70
+ deltaY: 0,
71
+ android: { current: nativeEvent(2) },
72
+ view: target,
73
+ })
74
+
75
+ container.dispatchEvent(host, 'pan', {
76
+ // NativeScript's Android pan recognizer reports ended for both
77
+ // ACTION_UP and ACTION_CANCEL; the MotionEvent disambiguates them.
78
+ state: 3,
79
+ deltaX: 40,
80
+ deltaY: 0,
81
+ android: { current: nativeEvent(3) },
82
+ view: target,
83
+ })
84
+
85
+ expect(events.map((event) => event.state)).toEqual(['began', 'cancelled'])
86
+ expect(events[1]).toMatchObject({ state: 'cancelled', vx: 0, vy: 0, target })
87
+ expect(velocityTracker.recycle).toHaveBeenCalledOnce()
88
+ expect(velocityTracker.addMovement).toHaveBeenCalledOnce()
89
+ root.unmount()
90
+ })
91
+
92
+ it('keeps ACTION_UP ended and converts Android velocity from px/s to dip/s', () => {
93
+ const velocityTracker = tracker()
94
+ installAndroid(velocityTracker)
95
+ const { container, root, host } = mountPanHost()
96
+
97
+ container.dispatchEvent(host, 'pan', {
98
+ state: 1,
99
+ deltaX: 0,
100
+ deltaY: 0,
101
+ android: { current: nativeEvent(0) },
102
+ })
103
+
104
+ container.dispatchEvent(host, 'pan', {
105
+ state: 3,
106
+ deltaX: 40,
107
+ deltaY: 0,
108
+ android: { current: nativeEvent(1) },
109
+ })
110
+
111
+ expect(events.map((event) => event.state)).toEqual(['began', 'ended'])
112
+ expect(events[1]).toMatchObject({ state: 'ended', vx: 400, vy: -120 })
113
+ expect(velocityTracker.recycle).toHaveBeenCalledOnce()
114
+ root.unmount()
115
+ })
116
+ })
package/src/pan.tsrx CHANGED
@@ -11,6 +11,12 @@ import type { PanEvent } from './props';
11
11
  const PAN_STATE = ['cancelled', 'began', 'moved', 'ended'] as const;
12
12
 
13
13
  function panVelocity(e: any, state: number, trackerRef: { current: any }): { vx: number; vy: number } {
14
+ if (state === 0) {
15
+ trackerRef.current?.recycle?.()
16
+ trackerRef.current = null
17
+ return { vx: 0, vy: 0 }
18
+ }
19
+
14
20
  const recognizer = e.ios;
15
21
  const nativeView = e.view?.nativeViewProtected;
16
22
  if (recognizer?.velocityInView && nativeView) {
@@ -45,13 +51,25 @@ function panVelocity(e: any, state: number, trackerRef: { current: any }): { vx:
45
51
  return velocity;
46
52
  }
47
53
 
54
+ function nativePanState(e: any): number {
55
+ const motionEvent = e.android?.current;
56
+ if (!motionEvent) return e.state;
57
+
58
+ const action = motionEvent.getActionMasked?.();
59
+ const actionCancel = (globalThis as any).android?.view?.MotionEvent?.ACTION_CANCEL ?? 3;
60
+ // NativeScript Android reports ACTION_CANCEL as `ended`; the MotionEvent
61
+ // preserves the distinction needed by the shared pan contract.
62
+ return action === actionCancel ? 0 : e.state;
63
+ }
64
+
48
65
  export function usePan(cb?: (e: PanEvent) => void): ((e: any) => void) | undefined {
49
66
  const velocityTracker = useRef<any>(null);
50
67
  // Memoized so the driver's observer doesn't churn per render — each
51
68
  // re-subscribe would re-attach a UIPanGestureRecognizer.
52
69
  const handler = useCallback((e: any) => {
53
- const state = PAN_STATE[e.state] ?? 'moved';
54
- const velocity = panVelocity(e, e.state, velocityTracker);
70
+ const nativeState = nativePanState(e);
71
+ const state = PAN_STATE[nativeState] ?? 'moved';
72
+ const velocity = panVelocity(e, nativeState, velocityTracker);
55
73
  cb?.({
56
74
  x: e.deltaX, y: e.deltaY, dx: e.deltaX, dy: e.deltaY,
57
75
  vx: velocity.vx, vy: velocity.vy,
package/src/pan.web.tsrx CHANGED
@@ -1,23 +1,51 @@
1
- import { useEffect } from 'octane';
1
+ import { useEffect, useRef } from 'octane';
2
2
  import type { PanEvent } from './props';
3
3
 
4
+ const VELOCITY_MAX_AGE_MS = 100;
5
+
4
6
  /** Shared pan plumbing for web leaves — pointerdown/move/up/cancel with
5
7
  * pointer capture, normalized to the shared gesture shape
6
8
  * {x,y,dx,dy,vx,vy,state,target}. `pointermove` isn't in octane's
7
9
  * delegated-event set, so listeners attach to the element directly —
8
10
  * that's why callers pass the bound-element ref. */
9
11
  export function usePan(el: { current: HTMLElement | null }, cb?: (e: PanEvent) => void): void {
12
+ const callback = useRef(cb);
13
+ callback.current = cb;
14
+ const enabled = Boolean(cb);
10
15
  useEffect(() => {
11
16
  const node = el.current;
12
- if (!node || !cb) return;
13
- let sx = 0, sy = 0, active = false, last = 0, lx = 0, t0 = 0;
17
+ if (!node || !enabled) return;
18
+ let sx = 0, sy = 0, active = false;
19
+ let last = 0, lx = 0, t0 = 0;
20
+ let vx = 0, vy = 0, velocityAt = 0;
14
21
  const emit = (e: PointerEvent, state: string) => {
15
22
  const now = performance.now();
16
23
  const dt = Math.max(now - t0, 1) / 1000;
17
- const vx = (e.clientX - lx) / dt;
18
- const vy = (e.clientY - last) / dt;
24
+ const dx = e.clientX - lx;
25
+ const dy = e.clientY - last;
26
+ if (state === 'began') {
27
+ vx = vy = 0;
28
+ velocityAt = now;
29
+ } else if (dx !== 0 || dy !== 0) {
30
+ // Preserve this sample for pointer-up when it repeats the final coordinates.
31
+ vx = dx / dt;
32
+ vy = dy / dt;
33
+ velocityAt = now;
34
+ }
35
+
19
36
  lx = e.clientX; last = e.clientY; t0 = now;
20
- cb({ x: e.clientX - sx, y: e.clientY - sy, dx: e.clientX - sx, dy: e.clientY - sy, vx, vy, state, target: node });
37
+ const fresh = now - velocityAt <= VELOCITY_MAX_AGE_MS;
38
+ callback.current?.({
39
+ x: e.clientX - sx,
40
+ y: e.clientY - sy,
41
+ dx: e.clientX - sx,
42
+ dy: e.clientY - sy,
43
+ vx: fresh ? vx : 0,
44
+ vy: fresh ? vy : 0,
45
+ state,
46
+ target: node,
47
+ });
48
+
21
49
  };
22
50
 
23
51
  const down = (e: PointerEvent) => { active = true; sx = e.clientX; sy = e.clientY; t0 = performance.now(); lx = sx; last = sy; node.setPointerCapture?.(e.pointerId); emit(e, 'began'); };
@@ -34,5 +62,5 @@ export function usePan(el: { current: HTMLElement | null }, cb?: (e: PanEvent) =
34
62
  node.removeEventListener('pointerup', up);
35
63
  node.removeEventListener('pointercancel', cancel);
36
64
  };
37
- }, [el, cb]);
65
+ }, [el, enabled]);
38
66
  }
@@ -19,5 +19,6 @@ export function applyNativeProps(
19
19
  if (bag) {
20
20
  Object.assign(el, bag)
21
21
  }
22
+
22
23
  applyNativeModifiers(el, props.modifiers)
23
24
  }
@@ -13,6 +13,8 @@ vi.mock('@nativescript/core', () => ({
13
13
  horizontalAlignment = ''
14
14
  verticalAlignment = ''
15
15
  style: Record<string, unknown> = {}
16
+ on() {}
17
+ off() {}
16
18
  constructor() {
17
19
  hosts.push(this)
18
20
  }
@@ -32,7 +34,13 @@ vi.mock('./root-layout.mobile', () => ({
32
34
  }),
33
35
  }))
34
36
 
35
- import { createObjectContainer, createObjectDriver, createUniversalRoot, flushUniversalSync } from 'octane/universal/native'
37
+ import {
38
+ createObjectContainer,
39
+ createObjectDriver,
40
+ createUniversalRoot,
41
+ flushUniversalSync,
42
+ } from 'octane/universal/native'
43
+
36
44
  import { Popover } from './Popover.tsrx'
37
45
 
38
46
  // The full-screen host must not wear the caller's className — a backgrounded
@@ -0,0 +1,30 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ createObjectContainer,
4
+ createObjectDriver,
5
+ createUniversalRoot,
6
+ } from 'octane/universal/native'
7
+
8
+ vi.mock('@nativescript/core', () => ({ isIOS: false, isAndroid: true }))
9
+ vi.mock('./pan.tsrx', () => ({ usePan: () => undefined }))
10
+ import { Pressable } from './Pressable.tsrx'
11
+
12
+ describe('native disabled Pressable accessibility state', () => {
13
+ it('reports disabled ahead of selected/checked and restores state when enabled', () => {
14
+ const container = createObjectContainer('nativescript')
15
+ const root = createUniversalRoot(container, createObjectDriver('nativescript'))
16
+ root.render(Pressable as any, {
17
+ disabled: true,
18
+ accessibilityState: { selected: true, checked: true },
19
+ })
20
+
21
+ expect(container.children[0].props.accessibilityState).toBe('disabled')
22
+ root.render(Pressable as any, {
23
+ disabled: false,
24
+ accessibilityState: { selected: true, checked: true },
25
+ })
26
+
27
+ expect(container.children[0].props.accessibilityState).toBe('selected')
28
+ root.unmount()
29
+ })
30
+ })
package/src/props.ts CHANGED
@@ -364,7 +364,8 @@ export interface TextAreaProps extends TextInputProps {
364
364
  * minHeight at the widget's measured line height. With `autoGrow` it
365
365
  * becomes the starting height instead of a fixed one. */
366
366
  rows?: number
367
- /** Grow to fit content, capped by `maxRows`. Native TextView grows by
367
+ /** Grow on input and value updates, capped by `maxRows`, including when
368
+ * `value` is omitted. Native TextView grows by
368
369
  * default — this prop exists so web (<textarea> is fixed-rows) matches;
369
370
  * without it, native gets a fixed `rows`-high box like web. */
370
371
  autoGrow?: boolean
@@ -1041,8 +1042,14 @@ export interface RouteMeta {
1041
1042
  /** Optional `loader` named export — a prefetch hook, not a data layer.
1042
1043
  * pushRoute fires it (fire-and-forget) before navigating so the
1043
1044
  * screen's `query$` reads hit a warm cache; boot/deep-link routes
1044
- * skip it (the screen mounts in the same tick anyway). */
1045
+ * skip it (the screen mounts in the same tick anyway). Ignored when
1046
+ * `dataMode: 'baked'` — baked routes resolve from `manifest.baked`. */
1045
1047
  loader?: (params: Record<string, unknown>) => unknown
1048
+ /** Data provenance: 'live' (default) runs `loader` at navigation;
1049
+ * 'baked' reads build-time output from the manifest's `baked` map —
1050
+ * for file routes the loader lives in a `<route>.loader.ts` sibling
1051
+ * that `xplat routes`/`xplat build` executes and never bundles. */
1052
+ dataMode?: RouteDataMode
1046
1053
  /** Optional guard awaited before a push commits. A thrown `redirect(...)`
1047
1054
  * short-circuits the attempted route. */
1048
1055
  beforeLoad?: BeforeLoad
@@ -1050,6 +1057,10 @@ export interface RouteMeta {
1050
1057
  head?: RouteHeadExport
1051
1058
  }
1052
1059
 
1060
+ /** How a route's data is produced — `'live'` runs the loader at navigation,
1061
+ * `'baked'` freezes loader output into the bundle at build time. */
1062
+ export type RouteDataMode = 'live' | 'baked'
1063
+
1053
1064
  /** Output of `deriveRouteManifest` — `screens` feeds `registerScreens`
1054
1065
  * (native pushRoute + web outlet fallback), `routes` feeds web URL
1055
1066
  * matching, `layouts` catalogs `_layout` files by directory ('' = root). */
@@ -1058,6 +1069,10 @@ export interface RouteManifest {
1058
1069
  routes: RouteMeta[]
1059
1070
  layouts: Record<string, any>
1060
1071
  loaders?: Record<string, (params: Record<string, unknown>) => unknown | Promise<unknown>>
1072
+ /** Build-time loader output for `dataMode: 'baked'` routes — the
1073
+ * generated `routes.gen.*` glue assigns it from `routes.gen.data`;
1074
+ * programmatic hosts pass it on `RouteSpecSet`/`addRoutes` directly. */
1075
+ baked?: Record<string, unknown>
1061
1076
  }
1062
1077
 
1063
1078
  /** One programmatic route entry for `defineRoutes` — the manifest fields a
@@ -1070,6 +1085,7 @@ export interface RouteSpec {
1070
1085
  /** Default presentation, like a `+modal`/`+fade` filename suffix. */
1071
1086
  presentation?: 'push' | 'modal' | 'fade'
1072
1087
  loader?: RouteMeta['loader']
1088
+ dataMode?: RouteDataMode
1073
1089
  beforeLoad?: BeforeLoad
1074
1090
  head?: RouteHeadExport
1075
1091
  /** Diagnostics label recorded on `RouteMeta.file` (warn strings). */
@@ -1078,11 +1094,155 @@ export interface RouteSpec {
1078
1094
 
1079
1095
  /** Input to `defineRoutes`: the route list plus optional path-keyed layouts
1080
1096
  * — `layouts: { docs: Shell }` plays the `_layout.tsrx` role for every
1081
- * route under `docs/*`. */
1097
+ * route under `docs/*`. `baked` carries build-time data for
1098
+ * `dataMode: 'baked'` specs — the host computes it (a content dir read, a
1099
+ * build step) and ships it in the manifest. */
1082
1100
  export interface RouteSpecSet {
1083
1101
  routes: readonly RouteSpec[]
1084
1102
  layouts?: Record<string, any>
1103
+ baked?: Record<string, unknown>
1104
+ }
1105
+
1106
+ /** The host-consumable route schema — what `xplat routes` writes to
1107
+ * `routes.gen.manifest.json` and `manifestToJson` produces from any
1108
+ * RouteManifest. Same shape whether the routes came from the file dir
1109
+ * or `defineRoutes`, so an external host (e.g. a web SSR tier) consumes
1110
+ * one normalized list. `loader`/`guard`/`head` are presence flags — the
1111
+ * functions don't cross the JSON boundary. */
1112
+ export interface RouteManifestJson {
1113
+ version: 1
1114
+ /** Layout dirs present, '' = the root `_layout` — sorted. */
1115
+ layouts: string[]
1116
+ /** Registered screen names, including any without a URL pattern. */
1117
+ screens: string[]
1118
+ routes: RouteJson[]
1119
+ }
1120
+
1121
+ export interface RouteJson {
1122
+ name: string
1123
+ /** Colon-syntax path — 'docs/:slug', '' for the root index. */
1124
+ path: string
1125
+ params: string[]
1126
+ presentation?: 'push' | 'modal' | 'fade'
1127
+ dataMode?: RouteDataMode
1128
+ /** Layout dirs applying to this route, root '' first, innermost last. */
1129
+ layouts: string[]
1130
+ /** Source label — file path or `programmatic:<path>`. */
1131
+ source: string
1132
+ loader: boolean
1133
+ guard: boolean
1134
+ head: boolean
1135
+ }
1136
+
1137
+ // ---------- programmatic route typing ----------
1138
+
1139
+ /** Phantom key carrying a `defineRoutes` manifest's spec-derived route
1140
+ * types — type-level only, never present at runtime. Module-private so the
1141
+ * published declarations carry no value binding for it. */
1142
+ declare const specRouteTypes: unique symbol
1143
+
1144
+ /** ':id' or '[id]' segment → 'id'; a static segment contributes nothing. */
1145
+ type RouteParamOf<S extends string> = S extends `:${infer P}`
1146
+ ? P
1147
+ : S extends `[${infer P}]`
1148
+ ? P
1149
+ : never
1150
+
1151
+ type TrimSlashes<P extends string> = P extends `/${infer R}`
1152
+ ? TrimSlashes<R>
1153
+ : P extends `${infer R}/`
1154
+ ? TrimSlashes<R>
1155
+ : P
1156
+
1157
+ /** Route-dir vocabulary at the type level: '[id]' normalizes to ':id'. */
1158
+ type NormSegment<S extends string> = S extends `[${infer P}]` ? `:${P}` : S
1159
+
1160
+ type NormPath<P extends string> = P extends `${infer Head}/${infer Tail}`
1161
+ ? `${NormSegment<Head>}/${NormPath<Tail>}`
1162
+ : NormSegment<P>
1163
+
1164
+ /** The route name a spec `path` produces — `''`/`'index'` and a trailing
1165
+ * 'index' segment normalize like `specSegments` does at runtime. */
1166
+ export type RouteNameOfPath<P extends string> =
1167
+ NormPath<TrimSlashes<P>> extends infer N extends string
1168
+ ? N extends '' | 'index'
1169
+ ? 'index'
1170
+ : N extends `${infer R}/index`
1171
+ ? R
1172
+ : N
1173
+ : never
1174
+
1175
+ /** Param names in a spec path — `'docs/[slug]'` and `'docs/:slug'` both
1176
+ * give `'slug'`; static-only paths give `never`. */
1177
+ export type RoutePathParams<Path extends string> =
1178
+ TrimSlashes<Path> extends infer P extends string
1179
+ ? P extends `${infer Head}/${infer Tail}`
1180
+ ? RouteParamOf<Head> | RoutePathParams<Tail>
1181
+ : RouteParamOf<P>
1182
+ : never
1183
+
1184
+ type RouteParamRecord<Path extends string> = { [K in RoutePathParams<Path>]: string }
1185
+
1186
+ type SpecPaths<Specs extends readonly RouteSpec[]> = Specs[number] extends infer S
1187
+ ? S extends { path: infer P extends string }
1188
+ ? P
1189
+ : never
1190
+ : never
1191
+
1192
+ type SpecsToParamMap<P extends string> = P extends string
1193
+ ? { [N in RouteNameOfPath<P>]: RouteParamRecord<P> }
1194
+ : never
1195
+
1196
+ type UnionToIntersection<U> = (U extends unknown ? (value: U) => void : never) extends (
1197
+ value: infer I,
1198
+ ) => void
1199
+ ? I
1200
+ : never
1201
+
1202
+ /** name → params map for a spec list — same shape `routes.gen.types.ts`
1203
+ * emits for file routes, so the two merge with `&`/`|`. */
1204
+ export type RouteParamsFromSpecs<Specs extends readonly RouteSpec[]> = UnionToIntersection<
1205
+ SpecsToParamMap<SpecPaths<Specs>>
1206
+ >
1207
+
1208
+ type SpecPresentations<S> = S extends {
1209
+ path: infer P extends string
1210
+ presentation: infer Pr
1211
+ }
1212
+ ? { [N in RouteNameOfPath<P>]: Pr }
1213
+ : {}
1214
+
1215
+ export type RoutePresentationsFromSpecs<Specs extends readonly RouteSpec[]> = UnionToIntersection<
1216
+ SpecPresentations<Specs[number]>
1217
+ >
1218
+
1219
+ /** The spec-derived typing record `defineRoutes` brands onto its manifest
1220
+ * return — read it with the `ManifestRoute*` helpers, never directly. */
1221
+ export interface SpecRouteInfo<Specs extends readonly RouteSpec[]> {
1222
+ readonly [specRouteTypes]: {
1223
+ params: RouteParamsFromSpecs<Specs>
1224
+ presentations: RoutePresentationsFromSpecs<Specs>
1225
+ }
1226
+ }
1227
+
1228
+ /** Pull the `{name: params}` map out of a `defineRoutes` manifest's type —
1229
+ * `RouteParams & ManifestRouteParams<typeof manifest>` merges file and
1230
+ * programmatic routes into one typed navigation surface. A plain
1231
+ * `RouteManifest` (untagged) extracts to `{}`, so `ManifestRouteNames`
1232
+ * is `never` and unions degrade to the file-derived names. */
1233
+ export type ManifestRouteParams<M> = M extends {
1234
+ readonly [specRouteTypes]: { params: infer P }
1235
+ }
1236
+ ? P
1237
+ : {}
1238
+
1239
+ export type ManifestRouteNames<M> = keyof ManifestRouteParams<M> & string
1240
+
1241
+ export type ManifestRoutePresentations<M> = M extends {
1242
+ readonly [specRouteTypes]: { presentations: infer P }
1085
1243
  }
1244
+ ? P
1245
+ : {}
1086
1246
 
1087
1247
  export interface OpenWindowOptions {
1088
1248
  /** Data made available to the native window content resolver. */
package/src/refresh.ts CHANGED
@@ -125,7 +125,12 @@ export function createPullToRefresh(host: RefreshHost): RefreshController {
125
125
  }
126
126
 
127
127
  const apply = () => {
128
- sv.contentInset = makeInsets(next, sv.contentInset.left, sv.contentInset.bottom, sv.contentInset.right)
128
+ sv.contentInset = makeInsets(
129
+ next,
130
+ sv.contentInset.left,
131
+ sv.contentInset.bottom,
132
+ sv.contentInset.right,
133
+ )
129
134
  }
130
135
 
131
136
  const UIView = (globalThis as any).UIView