@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
@@ -1,5 +1,8 @@
1
1
  export function setTranslate(view: any, x = 0, y = 0): void {
2
- if (!view) {return}
2
+ if (!view) {
3
+ return
4
+ }
5
+
3
6
  view.translateX = x
4
7
  view.translateY = y
5
8
  }
@@ -1,5 +1,5 @@
1
1
  /** @jsxImportSource @nativescript-community/octane */
2
- import { useCallback, useEffect, useState } from 'octane';
2
+ import { useCallback, useEffect, useRef, useState } from 'octane';
3
3
  import type { MeasureBounds, MeasureResult, UseMeasureOptions } from './props';
4
4
 
5
5
  function readBounds(view: any): MeasureBounds | null {
@@ -11,14 +11,12 @@ function readBounds(view: any): MeasureBounds | null {
11
11
 
12
12
  /** Binds to a NativeScript view and observes screen-relative DIP bounds by default. */
13
13
  export function useMeasure(options: UseMeasureOptions = {}): MeasureResult {
14
- const [view, setView] = useState<any>(null);
15
14
  const [bounds, setBounds] = useState<MeasureBounds | null>(null);
15
+ const detach = useRef<(() => void) | null>(null);
16
16
  const observe = options.observe ?? true;
17
- const bind = useCallback((next: any) => {
18
- setView((current: any) => current === next ? current : next);
19
- }, []);
20
-
21
- useEffect(() => {
17
+ const bind = useCallback((view: any) => {
18
+ detach.current?.();
19
+ detach.current = null;
22
20
  if (!view) {
23
21
  setBounds(null);
24
22
  return;
@@ -28,39 +26,40 @@ export function useMeasure(options: UseMeasureOptions = {}): MeasureResult {
28
26
  const next = readBounds(view);
29
27
  // Fresh object each read — skip the setState when nothing moved
30
28
  // or the re-render → layoutChanged → update loop never ends.
31
- setBounds((prev: MeasureBounds | null) =>
32
- prev && next &&
33
- prev.x === next.x && prev.y === next.y &&
34
- prev.width === next.width && prev.height === next.height
29
+ setBounds((prev: MeasureBounds | null) => {
30
+ return prev && next &&
31
+ prev.x === next.x && prev.y === next.y &&
32
+ prev.width === next.width && prev.height === next.height
35
33
  ? prev
36
- : next,
37
- );
34
+ : next;
35
+ });
38
36
  };
39
37
 
40
- const onLoaded = update;
41
- view.on?.('loaded', onLoaded);
38
+ view.on?.('loaded', update);
42
39
  update();
40
+ const scrollParents: any[] = [];
43
41
  if (observe) {
44
42
  view.on?.('layoutChanged', update);
45
43
  let parent = view.parent;
46
44
  while (parent) {
47
45
  parent.on?.('scroll', update);
46
+ scrollParents.push(parent);
48
47
  parent = parent.parent;
49
48
  }
50
49
  }
51
50
 
52
- return () => {
53
- view.off?.('loaded', onLoaded);
51
+ detach.current = () => {
52
+ view.off?.('loaded', update);
54
53
  if (observe) {
55
54
  view.off?.('layoutChanged', update);
56
- let parent = view.parent;
57
- while (parent) {
55
+ for (const parent of scrollParents) {
58
56
  parent.off?.('scroll', update);
59
- parent = parent.parent;
60
57
  }
61
58
  }
62
59
  };
63
- }, [view, observe]);
60
+ }, [observe]);
61
+
62
+ useEffect(() => () => detach.current?.(), []);
64
63
 
65
64
  return { bind, bounds };
66
65
  }
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { VirtualListCellPool } from './virtual-list-cell-pool'
3
+ import { createVirtualListEntries } from './virtual-list-layout'
4
+
5
+ const item = (id: string, type = 'row') => ({ id, type })
6
+ const entriesFor = (items: ReturnType<typeof item>[]) =>
7
+ createVirtualListEntries(
8
+ items,
9
+ (row) => row.id,
10
+ (row) => row.type,
11
+ )
12
+
13
+ describe('VirtualList physical cell ownership', () => {
14
+ it('retains keyed hosts across prepend and recycles only compatible free hosts', () => {
15
+ const items = [item('a'), item('b'), item('c', 'other')]
16
+ const pool = new VirtualListCellPool<ReturnType<typeof item>>()
17
+ const initial = pool.update(entriesFor(items))
18
+ const a = initial.find((cell) => cell.entry?.key === 'a')!
19
+ const b = initial.find((cell) => cell.entry?.key === 'b')!
20
+ const c = initial.find((cell) => cell.entry?.key === 'c')!
21
+ const next = pool.update(entriesFor([item('new'), items[1], items[2]]))
22
+ expect(next.find((cell) => cell.entry?.key === 'new')!.id).toBe(a.id)
23
+ expect(next.find((cell) => cell.entry?.key === 'b')!.id).toBe(b.id)
24
+ expect(next.find((cell) => cell.entry?.key === 'c')!.id).toBe(c.id)
25
+ expect(pool.isCurrent(a.id, a.entry!.rowKey, a.generation)).toBe(false)
26
+ const prepended = pool.update(entriesFor([item('first'), item('new'), items[1], items[2]]))
27
+ expect(prepended.find((cell) => cell.entry?.key === 'b')!.id).toBe(b.id)
28
+ })
29
+
30
+ it('does not recycle a host across types and bounds free hosts after repeated changes', () => {
31
+ const pool = new VirtualListCellPool<ReturnType<typeof item>>()
32
+ const original = pool.update(entriesFor([item('first', 'type0')]))[0]
33
+ const different = pool.update(entriesFor([item('next', 'type1')])).find((cell) => cell.entry)!
34
+ expect(different.id).not.toBe(original.id)
35
+ for (let index = 2; index < 1000; index++) {
36
+ const cells = pool.update(entriesFor([item(String(index), 'type' + index)]))
37
+ expect(cells.length).toBeLessThanOrEqual(9)
38
+ }
39
+ })
40
+
41
+ it('invalidates work on item replacement, emptying, restoration and disposal', () => {
42
+ const pool = new VirtualListCellPool<ReturnType<typeof item>>()
43
+ const first = pool.update(entriesFor([item('same')]))[0]
44
+ const replacement = pool.update(entriesFor([item('same')]))[0]
45
+ expect(replacement.id).toBe(first.id)
46
+ expect(pool.isCurrent(first.id, first.entry!.rowKey, first.generation)).toBe(false)
47
+ expect(pool.update([])).toEqual([])
48
+ const restored = pool.update(entriesFor([item('same')]))[0]
49
+ expect(restored.id).not.toBe(first.id)
50
+ pool.clear()
51
+ expect(pool.isCurrent(restored.id, restored.entry!.rowKey, restored.generation)).toBe(false)
52
+ })
53
+ })
@@ -0,0 +1,81 @@
1
+ import type { VirtualListEntry } from './virtual-list-layout'
2
+
3
+ export interface VirtualListCell<T> {
4
+ id: number
5
+ typeKey: string
6
+ generation: number
7
+ entry: VirtualListEntry<T> | null
8
+ rows: readonly VirtualListEntry<T>[]
9
+ }
10
+
11
+ /** Reuse physical hosts without retaining an off-window logical row subtree. */
12
+ export class VirtualListCellPool<T> {
13
+ private cells: VirtualListCell<T>[] = []
14
+ private byId = new Map<number, VirtualListCell<T>>()
15
+ private nextId = 0
16
+
17
+ update(entries: readonly VirtualListEntry<T>[]): readonly VirtualListCell<T>[] {
18
+ if (entries.length === 0) {
19
+ this.clear()
20
+ return this.cells
21
+ }
22
+
23
+ const wanted = new Set(entries.map((entry) => entry.rowKey))
24
+ let cells = this.cells.map((cell) =>
25
+ cell.entry && !wanted.has(cell.entry.rowKey)
26
+ ? { ...cell, entry: null, rows: [], generation: cell.generation + 1 }
27
+ : cell,
28
+ )
29
+
30
+ const retained = new Map(
31
+ cells.filter((cell) => cell.entry).map((cell) => [cell.entry!.rowKey, cell.id]),
32
+ )
33
+
34
+ for (const entry of entries) {
35
+ const retainedId = retained.get(entry.rowKey)
36
+ let index =
37
+ retainedId === undefined
38
+ ? cells.findIndex((cell) => !cell.entry && cell.typeKey === entry.typeKey)
39
+ : cells.findIndex((cell) => cell.id === retainedId)
40
+
41
+ if (index === -1) {
42
+ index = cells.length
43
+ cells.push({
44
+ id: this.nextId++,
45
+ typeKey: entry.typeKey,
46
+ generation: 0,
47
+ entry: null,
48
+ rows: [],
49
+ })
50
+ }
51
+
52
+ const cell = cells[index]
53
+ if (cell.entry !== entry) {
54
+ // Immutable snapshots let renderer caches see assignment/index changes.
55
+ cells[index] = { ...cell, entry, rows: [entry], generation: cell.generation + 1 }
56
+ }
57
+ }
58
+
59
+ let spare = 0
60
+ cells = cells.filter((cell) => cell.entry || spare++ < 8)
61
+ cells.sort((a, b) => (a.entry?.index ?? Infinity) - (b.entry?.index ?? Infinity) || a.id - b.id)
62
+ this.cells = cells
63
+ this.byId = new Map(cells.map((cell) => [cell.id, cell]))
64
+ return cells
65
+ }
66
+
67
+ get(id: number): VirtualListCell<T> | undefined {
68
+ return this.byId.get(id)
69
+ }
70
+
71
+ isCurrent(id: number, rowKey: string, generation: number): boolean {
72
+ const cell = this.byId.get(id)
73
+ return cell?.entry?.rowKey === rowKey && cell.generation === generation
74
+ }
75
+
76
+ clear() {
77
+ this.cells = []
78
+ this.byId.clear()
79
+ // IDs remain monotonic across empty/restore so old work cannot match.
80
+ }
81
+ }
@@ -48,10 +48,9 @@ describe('VirtualList size index', () => {
48
48
  })
49
49
 
50
50
  it('rejects duplicate item keys', () => {
51
- expect(() => createVirtualListEntries(
52
- [{ id: 'same' }, { id: 'same' }],
53
- (item) => item.id,
54
- )).toThrow('duplicate key')
51
+ expect(() =>
52
+ createVirtualListEntries([{ id: 'same' }, { id: 'same' }], (item) => item.id),
53
+ ).toThrow('duplicate key')
55
54
  })
56
55
 
57
56
  it('uses a type estimate only after four rows are measured', () => {
@@ -61,10 +60,14 @@ describe('VirtualList size index', () => {
61
60
  () => 'row',
62
61
  )
63
62
 
64
- const measurements = new Map(entries.slice(0, 3).map((entry, index) => [
65
- virtualListMeasurementKey(entry.rowKey, false),
66
- { width: 100, height: 36 + index * 12 },
67
- ]))
63
+ const measurements = new Map(
64
+ entries
65
+ .slice(0, 3)
66
+ .map((entry, index) => [
67
+ virtualListMeasurementKey(entry.rowKey, false),
68
+ { width: 100, height: 36 + index * 12 },
69
+ ]),
70
+ )
68
71
 
69
72
  expect(estimateVirtualListSizes(entries, false, 100, measurements)[3]).toBe(
70
73
  VIRTUAL_LIST_ESTIMATED_ROW_SIZE,
@@ -38,7 +38,9 @@ export function createVirtualListEntries<T>(
38
38
 
39
39
  const keyToken = JSON.stringify([typeof key, key])
40
40
  if (seen.has(keyToken)) {
41
- throw new Error(`VirtualList keyExtractor returned a duplicate key at index ${index}: ${String(key)}`)
41
+ throw new Error(
42
+ `VirtualList keyExtractor returned a duplicate key at index ${index}: ${String(key)}`,
43
+ )
42
44
  }
43
45
 
44
46
  seen.add(keyToken)
@@ -89,12 +91,7 @@ export function estimateVirtualListSizes<T>(
89
91
  )
90
92
 
91
93
  const cached = measurements.get(cacheKey)
92
- if (
93
- cached &&
94
- width > 0 &&
95
- cached.width > 0 &&
96
- Math.abs(cached.width - width) < 0.5
97
- ) {
94
+ if (cached && width > 0 && cached.width > 0 && Math.abs(cached.width - width) < 0.5) {
98
95
  return cached.height
99
96
  }
100
97
 
@@ -130,7 +127,6 @@ export class VirtualListSizeIndex {
130
127
  this.tree[parent] += this.tree[index]
131
128
  }
132
129
  }
133
-
134
130
  }
135
131
 
136
132
  get(index: number): number {
@@ -32,6 +32,7 @@ export function openWindow(
32
32
  const closed = new Promise<void>((resolve) => {
33
33
  resolveClosed = resolve
34
34
  })
35
+
35
36
  const off = bridge().on('windows', 'closed', (wid: unknown) => {
36
37
  if (wid === id) {
37
38
  off()
@@ -50,10 +51,14 @@ export function openWindow(
50
51
  id,
51
52
  closed,
52
53
  close() {
53
- void bridge().call('windows', 'close', [id]).catch(() => {})
54
+ void bridge()
55
+ .call('windows', 'close', [id])
56
+ .catch(() => {})
54
57
  },
55
58
  setTitle(title: string) {
56
- void bridge().call('windows', 'setTitle', [id, title]).catch(() => {})
59
+ void bridge()
60
+ .call('windows', 'setTitle', [id, title])
61
+ .catch(() => {})
57
62
  },
58
63
  }
59
64
  }
@@ -6,5 +6,6 @@ export function openWindow(options: OpenWindowOptions = {}): void {
6
6
  console.warn('[octane-xplat] Window creation is unsupported by the current AppKit host.')
7
7
  return
8
8
  }
9
+
9
10
  open(options)
10
11
  }
@@ -64,11 +64,14 @@
64
64
  "InputTags.web.d.ts",
65
65
  "Kbd.d.ts",
66
66
  "Kbd.web.d.ts",
67
+ "KeyboardAvoiding.d.ts",
67
68
  "KeyboardAvoiding.mobile.d.ts",
69
+ "KeyboardAvoiding.web.d.ts",
68
70
  "Link.d.ts",
69
71
  "Link.web.d.ts",
70
72
  "ListItem.d.ts",
71
73
  "ListItem.web.d.ts",
74
+ "Markdown.d.ts",
72
75
  "Meter.d.ts",
73
76
  "Meter.web.d.ts",
74
77
  "NavLink.d.ts",
@@ -169,11 +172,14 @@
169
172
  "cx.d.ts",
170
173
  "escape-props.d.ts",
171
174
  "escape-props.web.d.ts",
175
+ "grid-auto-placement.d.ts",
176
+ "grid-placement.d.ts",
172
177
  "icons.d.ts",
173
178
  "index.d.ts",
174
179
  "index.linux.d.ts",
175
180
  "index.mobile.d.ts",
176
181
  "index.shared.d.ts",
182
+ "index.shared.web.d.ts",
177
183
  "index.web.d.ts",
178
184
  "ios/LiquidGlass.ios.d.ts",
179
185
  "ios/LiquidGlassContainer.ios.d.ts",
@@ -191,9 +197,12 @@
191
197
  "layout-child.d.ts",
192
198
  "layout-child.web.d.ts",
193
199
  "meter-math.d.ts",
200
+ "modal-focus.web.d.ts",
194
201
  "modal-presenter.mobile.d.ts",
195
202
  "modifier-factories.d.ts",
196
203
  "native/index.d.ts",
204
+ "navigation-request.d.ts",
205
+ "overlay-lifetime.mobile.d.ts",
197
206
  "pan.d.ts",
198
207
  "pan.web.d.ts",
199
208
  "platform.d.ts",
@@ -223,11 +232,14 @@
223
232
  "styled.d.ts",
224
233
  "styled.web.d.ts",
225
234
  "svg.mobile.d.ts",
235
+ "tap-to-blur.ios.d.ts",
226
236
  "tap-to-blur.mobile.d.ts",
237
+ "text-focus.d.ts",
227
238
  "text-write.d.ts",
228
239
  "text-write.web.d.ts",
229
240
  "theme/colorScheme.d.ts",
230
241
  "theme/colorScheme.linux.d.ts",
242
+ "theme/colorScheme.web.d.ts",
231
243
  "theme/status-bar-scheme.mobile.d.ts",
232
244
  "theme/theme-scheme.d.ts",
233
245
  "toast-anchor.d.ts",
@@ -243,9 +255,10 @@
243
255
  "use-store.web.d.ts",
244
256
  "useMeasure.d.ts",
245
257
  "useMeasure.web.d.ts",
246
- "vendor/ui-svg/index.common.d.ts",
247
- "vendor/ui-svg/index.d.ts",
248
- "vendor/ui-svg/index.ios.d.ts",
258
+ "vendor/ui-svg/src/index.common.d.ts",
259
+ "vendor/ui-svg/src/index.d.ts",
260
+ "vendor/ui-svg/src/index.ios.d.ts",
261
+ "virtual-list-cell-pool.d.ts",
249
262
  "virtual-list-layout.d.ts",
250
263
  "web/index.d.ts",
251
264
  "windows.d.ts",
@@ -1,3 +1,2 @@
1
- /** @jsxImportSource @nativescript-community/octane */
2
1
  import type { GridProps } from './props.js';
3
2
  export declare function Grid(props: GridProps): unknown;
@@ -0,0 +1,3 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import type { KeyboardAvoidingProps } from './props.js';
3
+ export declare function KeyboardAvoiding(props: KeyboardAvoidingProps): unknown;
@@ -0,0 +1,2 @@
1
+ import type { KeyboardAvoidingProps } from './props.js';
2
+ export declare function KeyboardAvoiding(props: KeyboardAvoidingProps): import("octane/jsx-runtime").JSX.Element;
@@ -0,0 +1,59 @@
1
+ /** Markdown AST — the serialized shape `xplat routes` bakes for `.md`
2
+ * routes (parse happens at codegen, not runtime). `MdNode` covers block
3
+ * level; `MdInline` covers text runs inside paragraphs/lists/quotes. */
4
+ export type MdInline = {
5
+ t: 'text';
6
+ text: string;
7
+ } | {
8
+ t: 'code';
9
+ text: string;
10
+ } | {
11
+ t: 'bold';
12
+ children: MdInline[];
13
+ } | {
14
+ t: 'em';
15
+ children: MdInline[];
16
+ } | {
17
+ t: 'link';
18
+ text: string;
19
+ href: string;
20
+ };
21
+ export type MdNode = {
22
+ t: 'h';
23
+ depth: 1 | 2 | 3 | 4 | 5 | 6;
24
+ children: MdInline[];
25
+ } | {
26
+ t: 'p';
27
+ children: MdInline[];
28
+ } | {
29
+ t: 'code';
30
+ lang?: string;
31
+ text: string;
32
+ } | {
33
+ t: 'list';
34
+ ordered: boolean;
35
+ items: MdInline[][];
36
+ } | {
37
+ t: 'quote';
38
+ children: MdInline[];
39
+ } | {
40
+ t: 'hr';
41
+ };
42
+ export interface MdDoc {
43
+ kind: 'octane-xplat/md';
44
+ v: 1;
45
+ children: MdNode[];
46
+ }
47
+ /** Renders a baked markdown doc with the shared vocabulary — Text
48
+ * blocks on every platform, no parser at runtime. `data` is the AST the
49
+ * codegen baked into routes.gen.data (or a doc built programmatically). */
50
+ export declare function Markdown(props: {
51
+ data?: MdDoc;
52
+ }): unknown;
53
+ /** Route screen for `.md` files — the codegen emits specs with this as
54
+ * `screen`, so a markdown file in the route dir is a route end-to-end.
55
+ * Receives baked data like any route (`props.data`). */
56
+ export declare function MarkdownScreen(props: {
57
+ data?: MdDoc;
58
+ error?: unknown;
59
+ }): unknown;
@@ -3,6 +3,6 @@ import type { TextAreaProps } from "./props.js";
3
3
  * `autoGrow` is its default; `rows`/`maxRows` have no NS equivalent, so the
4
4
  * leaf converts them to min/maxHeight in dips using the widget's own line
5
5
  * height (iOS `font.lineHeight`, Android `getLineHeight()` px→dip — measured
6
- * on `loaded`, re-fit every commit for prop/style changes). The driver's
7
- * text→textChange echo suppression applies here unchanged. */
6
+ * on `loaded`, re-fit every commit for prop/style changes). Imperative
7
+ * writes suppress their synchronous textChange echo through text-write. */
8
8
  export declare function TextArea(props: TextAreaProps): unknown;
@@ -1,6 +1,6 @@
1
1
  import type { TextAreaProps } from "./props.js";
2
2
  /** Multiline input — <textarea>. `autoGrow` re-fits height to content after
3
- * every commit via scrollHeight, capped by `maxRows` translated to a real
3
+ * input and value updates via scrollHeight, capped by `maxRows` translated to a real
4
4
  * max-height from the element's computed line-height; past the cap the
5
5
  * field scrolls. `useLayoutEffect` so the measure lands pre-paint in the
6
6
  * commit that wrote `value`. Deps are explicit — tsrx infers them from the
@@ -1,7 +1,6 @@
1
1
  import type { TextInputProps } from "./props.js";
2
- /** Controlled single-line input. The driver suppresses the textChange echo
3
- * of its own `text` writes (upstream #5, since 0.2.1) — no leaf-side
4
- * guard needed. `editable` defaults true at the leaf too: an undefined
2
+ /** Controlled single-line input. Imperative writes suppress their synchronous
3
+ * textChange echo through text-write. `editable` defaults true: an undefined
5
4
  * write coerces ios.userInteractionEnabled to NO (dead field) — the
6
5
  * driver's skip-undefined patch covers it, the ?? stays as documentation.
7
6
  * `text` isn't a bound prop: the leaf writes it imperatively so the
@@ -1,5 +1,5 @@
1
1
  import type { VirtualListProps } from './props.js';
2
- /** Shared vertical virtualization over NativeScript ScrollView. Off-window
3
- * rows unmount; measured row-height changes above the visible anchor adjust
4
- * the native scroll offset after layout. */
2
+ /** Shared vertical virtualization with pooled outer hosts and keyed logical
3
+ * rows. Off-window item subtrees unmount; measured heights position cells and
4
+ * preserve the visible keyed anchor after layout. */
5
5
  export declare function VirtualList<T = any>(props: VirtualListProps<T>): unknown;
@@ -4,6 +4,10 @@ export declare function applyEscapeProps(view: any, props: {
4
4
  ios?: any;
5
5
  android?: any;
6
6
  modifiers?: readonly NativeModifier[];
7
+ row?: number;
8
+ col?: number;
9
+ rowSpan?: number;
10
+ colSpan?: number;
7
11
  }): void;
8
12
  /** Translate the shared/ARIA spelling to NativeScript's narrower role enum. */
9
13
  export declare function nativeAccessibilityRole(role?: Role): string | undefined;
@@ -0,0 +1,13 @@
1
+ import type { GridChildPlacement } from './grid-placement.js';
2
+ export interface GridCell {
3
+ row: number;
4
+ col: number;
5
+ }
6
+ export interface GridAutoPlacement {
7
+ cells: GridCell[];
8
+ rowCount: number;
9
+ columnCount: number;
10
+ }
11
+ /** Match CSS Grid's row-flow placement for NativeScript GridLayout children. */
12
+ export declare function autoPlaceGridChildren(items: readonly GridChildPlacement[], rowTracks?: string, columnTracks?: string): GridAutoPlacement;
13
+ export declare function ensureGridTracks(spec: string | undefined, requiredCount: number): string;
@@ -0,0 +1,5 @@
1
+ import type { LayoutChildProps } from './props.js';
2
+ export type GridChildPlacement = Pick<LayoutChildProps, 'row' | 'col' | 'rowSpan' | 'colSpan'>;
3
+ /** Keep the authored Grid placement next to its mounted NativeScript view. */
4
+ export declare function rememberGridChildPlacement(view: unknown, props: GridChildPlacement): void;
5
+ export declare function gridChildPlacement(view: unknown): GridChildPlacement;
@@ -1,4 +1,3 @@
1
1
  export * from './index.shared.js';
2
2
  export { KeyboardAvoiding } from './KeyboardAvoiding.mobile.js';
3
- export type { KeyboardAvoidingProps } from './props.js';
4
3
  import './theme/status-bar-scheme.mobile.js';
@@ -8,6 +8,8 @@ export type { PanEvent, SwipeEvent, SetTranslate } from './props.js';
8
8
  export { setTranslate } from './translate.js';
9
9
  export { Spacer } from './Spacer.js';
10
10
  export { Text } from './Text.js';
11
+ export { Markdown, MarkdownScreen } from './Markdown.js';
12
+ export type { MdDoc, MdNode, MdInline } from './Markdown.js';
11
13
  export { RichText, RichTextSpan } from './RichText.js';
12
14
  export { Pressable } from './Pressable.js';
13
15
  export { Button } from './Button.js';
@@ -83,6 +85,7 @@ export { Tabs } from './Tabs.js';
83
85
  export type { TabSpec } from './Tabs.js';
84
86
  export { Switch } from './Switch.js';
85
87
  export { SafeArea } from './SafeArea.js';
88
+ export { KeyboardAvoiding } from './KeyboardAvoiding.js';
86
89
  export { Drawer } from './Drawer.js';
87
90
  export { useSafeAreaInsets } from './safeAreaInsets.js';
88
91
  export type { SafeAreaInsets } from './safeAreaInsets.js';
@@ -100,12 +103,12 @@ export { isNative } from './platform.js';
100
103
  export { registerStack, getStack, stackEntries } from './stacks.js';
101
104
  export { pushRoute, popRoute, routeStacks, pushDeepLink, routeFor, currentRoute, currentModalRoute, canGoBack, useCanGoBack, useRoute, useModalRoute, redirect, addBackInterceptor, registerScreens, registerRoutes, addRoutes, screenFor, hrefFor, layoutsForRoute, } from './route.js';
102
105
  export { useBackInterceptor } from './use-back.js';
103
- export { deriveRouteManifest, defineRoutes, mergeRouteManifests } from './route-table.js';
104
- export type { BeforeLoad, BeforeLoadArgs, LinkProps, NavLinkProps, Route, RouteContext, RouteHead, RouteHeadExport, RouteMeta, RouteManifest, RouteSpec, RouteSpecSet, ScreenTable, ModalOpenResult, OpenWindowOptions, } from './props.js';
106
+ export { deriveRouteManifest, defineRoutes, mergeRouteManifests, manifestToJson, } from './route-table.js';
107
+ export type { BeforeLoad, BeforeLoadArgs, LinkProps, NavLinkProps, Route, RouteContext, RouteHead, RouteHeadExport, RouteMeta, RouteManifest, RouteManifestJson, RouteJson, RouteDataMode, RouteNameOfPath, RouteParamsFromSpecs, RoutePathParams, RoutePresentationsFromSpecs, RouteSpec, RouteSpecSet, ManifestRouteNames, ManifestRouteParams, ManifestRoutePresentations, SpecRouteInfo, ScreenTable, ModalOpenResult, OpenWindowOptions, } from './props.js';
105
108
  export { createStore } from './store.js';
106
109
  export type { Store, ReadableStore } from './store.js';
107
110
  export { useStore } from './use-store.js';
108
111
  export { Sheet } from './Sheet.js';
109
112
  export { openSheet, closeSheet } from './sheet-service.js';
110
113
  export type { SheetProps, SheetOpenOptions, OpenSheet } from './props.js';
111
- export type { AbsoluteProps, DrawerProps, GridProps, ImageProps, LayoutChildProps, PressableProps, RichTextProps, RichTextSpanProps, RowProps, ScreenProps, SafeAreaProps, ScrollBoxProps, ScrollViewProps, SpacerProps, StackProps, SwitchProps, TabsProps, TextAreaProps, TextInputProps, TextProps, ViewProps, } from './props.js';
114
+ export type { AbsoluteProps, DrawerProps, GridProps, ImageProps, KeyboardAvoidingProps, LayoutChildProps, PressableProps, RichTextProps, RichTextSpanProps, RowProps, ScreenProps, SafeAreaProps, ScrollBoxProps, ScrollViewProps, SpacerProps, StackProps, SwitchProps, TabsProps, TextAreaProps, TextInputProps, TextProps, ViewProps, } from './props.js';