@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,251 @@
1
+ import {
2
+ Profile,
3
+ SuspenseProfile,
4
+ NestedProfile,
5
+ Retained,
6
+ Reader,
7
+ sharedSelection$,
8
+ sharedCalls,
9
+ } from '../tests/data-lifecycle.fixture.mobile.tsrx'
10
+
11
+ import { afterEach, describe, expect, it, vi } from 'vitest'
12
+ import {
13
+ createObjectContainer,
14
+ createObjectDriver,
15
+ createUniversalRoot,
16
+ flushUniversalSync,
17
+ } from 'octane/universal/native'
18
+
19
+ import { type QueryContext } from 'octane/signals'
20
+
21
+ type Request = {
22
+ id: string
23
+ signal: AbortSignal
24
+ resolve: (value: string) => void
25
+ reject: (error: Error) => void
26
+ }
27
+
28
+ function requests() {
29
+ const calls: Request[] = []
30
+ const load = (id: string, { signal }: QueryContext<string>) =>
31
+ new Promise<string>((resolve, reject) => {
32
+ calls.push({ id, signal, resolve, reject })
33
+ })
34
+
35
+ return { calls, load }
36
+ }
37
+
38
+ const roots: ReturnType<typeof createUniversalRoot>[] = []
39
+ function mount(Component: any, props: any) {
40
+ const container = createObjectContainer('nativescript')
41
+ const root = createUniversalRoot(container, createObjectDriver('nativescript'))
42
+ roots.push(root)
43
+ root.render(Component, props)
44
+ return {
45
+ root,
46
+ container,
47
+ text: () =>
48
+ container.children
49
+ .filter((node: any) => node.type === 'label')
50
+ .map((node: any) => node.props.text),
51
+ }
52
+ }
53
+
54
+ async function settle() {
55
+ for (let i = 0; i < 12; i++) {
56
+ await Promise.resolve()
57
+ }
58
+
59
+ flushUniversalSync(() => {})
60
+ }
61
+
62
+ afterEach(() => {
63
+ for (const root of roots.splice(0)) {
64
+ root.unmount()
65
+ }
66
+ })
67
+
68
+ describe('compiler-owned query lifecycle on universal roots', () => {
69
+ it('suspends on first data, catches a query error, and retries the boundary', async () => {
70
+ const { calls, load } = requests()
71
+ const view = mount(SuspenseProfile, { load })
72
+ expect(view.text()).toEqual(['pending'])
73
+ calls[0].reject(new Error('offline'))
74
+ await settle()
75
+ expect(view.container.children[0].props.text).toBe('offline')
76
+ flushUniversalSync(() => view.container.dispatchEvent(view.container.children[0], 'tap', {}))
77
+ await settle()
78
+ expect(view.text()).toEqual(['pending'])
79
+ expect(calls).toHaveLength(2)
80
+ calls[1].resolve('recovered')
81
+ await settle()
82
+ expect(view.text()).toEqual(['recovered'])
83
+ })
84
+
85
+ it('shares the declaring component query with a child reader', async () => {
86
+ const { calls, load } = requests()
87
+ const view = mount(NestedProfile, { load })
88
+ expect(calls).toHaveLength(1)
89
+ calls[0].resolve('nested result')
90
+ await settle()
91
+ expect(view.text()).toEqual(['ready', 'nested result'])
92
+ view.root.unmount()
93
+ expect(view.container.children).toEqual([])
94
+ const pendingView = mount(NestedProfile, { load })
95
+ expect(calls).toHaveLength(2)
96
+ pendingView.root.unmount()
97
+ expect(calls[1].signal.aborted).toBe(true)
98
+ calls[1].resolve('after child reader unmounted')
99
+ await settle()
100
+ expect(pendingView.container.children).toEqual([])
101
+ })
102
+
103
+ it('isolates stacked instances, retires requests, and ignores late responses', async () => {
104
+ const { calls, load } = requests()
105
+ const first = mount(Profile, { id: 'first', load })
106
+ const second = mount(Profile, { id: 'second', load })
107
+ expect(calls.map((call) => call.id)).toEqual(['first', 'second'])
108
+ calls[0].resolve('one')
109
+ calls[1].resolve('two')
110
+ await settle()
111
+ expect(first.text()).toEqual(['first:ready:one:false'])
112
+ expect(second.text()).toEqual(['second:ready:two:false'])
113
+ second.root.render(Profile, { id: 'second', nextId: 'third', load })
114
+ flushUniversalSync(() =>
115
+ second.container.dispatchEvent(second.container.children[4], 'tap', {}),
116
+ )
117
+
118
+ await settle()
119
+ expect(calls.map((call) => call.id)).toEqual(['first', 'second', 'third'])
120
+ second.root.render(Profile, { id: 'second', nextId: 'fourth', load })
121
+ flushUniversalSync(() =>
122
+ second.container.dispatchEvent(second.container.children[4], 'tap', {}),
123
+ )
124
+
125
+ await settle()
126
+ expect(calls[2].signal.aborted).toBe(true)
127
+ calls[3].resolve('four')
128
+ await settle()
129
+ calls[2].resolve('late third')
130
+ await settle()
131
+ expect(second.text()).toEqual(['fourth:ready:four:false'])
132
+ expect(first.text()).toEqual(['first:ready:one:false'])
133
+ second.root.render(Profile, { id: 'second', nextId: 'fifth', load })
134
+ flushUniversalSync(() =>
135
+ second.container.dispatchEvent(second.container.children[4], 'tap', {}),
136
+ )
137
+
138
+ await settle()
139
+ second.root.unmount()
140
+ expect(calls[4].signal.aborted).toBe(true)
141
+ calls[4].resolve('after unmount')
142
+ await settle()
143
+ expect(second.container.children).toEqual([])
144
+ })
145
+
146
+ it('reports pending, error, retry, refetch, and reset separately', async () => {
147
+ const { calls, load } = requests()
148
+ const view = mount(Profile, { id: 'a', load })
149
+ expect(view.text()).toEqual(['a:pending:none:false'])
150
+ calls[0].reject(new Error('offline'))
151
+ await settle()
152
+ expect(view.text()).toEqual(['a:error:none:false'])
153
+ flushUniversalSync(() => view.container.dispatchEvent(view.container.children[1], 'tap', {}))
154
+ await settle()
155
+ expect(view.text()).toEqual(['a:pending:none:false'])
156
+ calls[1].resolve('one')
157
+ await settle()
158
+ flushUniversalSync(() => view.container.dispatchEvent(view.container.children[2], 'tap', {}))
159
+ await settle()
160
+ expect(view.text()).toEqual(['a:ready:one:true'])
161
+ calls[2].resolve('two')
162
+ await settle()
163
+ flushUniversalSync(() => view.container.dispatchEvent(view.container.children[3], 'tap', {}))
164
+ await settle()
165
+ expect(view.text()).toEqual(['a:pending:two:false'])
166
+ calls[3].resolve('three')
167
+ await settle()
168
+ expect(view.text()).toEqual(['a:ready:three:false'])
169
+ })
170
+
171
+ it('updates a module signal across roots and disconnects unmounted readers', async () => {
172
+ const selection$ = sharedSelection$
173
+ const calls = sharedCalls
174
+ selection$.set('a')
175
+ let renders = 0
176
+ const props = {
177
+ onRender: () => {
178
+ renders++
179
+ },
180
+ }
181
+
182
+ const page = mount(Reader, props)
183
+ const sheet = mount(Reader, props)
184
+ const modal = mount(Reader, props)
185
+ expect(calls).toHaveLength(1)
186
+ calls[0].resolve('one')
187
+ await settle()
188
+ for (const view of [page, sheet, modal]) {
189
+ expect(view.text()).toEqual(['a:ready:one'])
190
+ }
191
+
192
+ flushUniversalSync(() => selection$.set('b'))
193
+ await settle()
194
+ expect(calls).toHaveLength(2)
195
+ calls[1].resolve('two')
196
+ await settle()
197
+ for (const view of [page, sheet, modal]) {
198
+ expect(view.text()).toEqual(['b:ready:two'])
199
+ }
200
+
201
+ for (const view of [page, sheet, modal]) {
202
+ view.root.unmount()
203
+ }
204
+
205
+ const before = renders
206
+ flushUniversalSync(() => selection$.set(''))
207
+ await settle()
208
+ expect(renders).toBe(before)
209
+ })
210
+
211
+ it('delivers a tap that re-suspends, hides retained hosts, and reports handler errors', async () => {
212
+ let resolve!: (value: string) => void
213
+ const pending = new Promise<string>((done) => {
214
+ resolve = done
215
+ })
216
+
217
+ const error = new Error('handler failure')
218
+ const fail = vi.fn(() => {
219
+ throw error
220
+ })
221
+
222
+ const uncaught = vi.fn()
223
+ const container = createObjectContainer('nativescript')
224
+ const root = createUniversalRoot(container, createObjectDriver('nativescript'), {
225
+ onUncaughtError: uncaught,
226
+ })
227
+
228
+ roots.push(root)
229
+ root.render(Retained, { pending, fail })
230
+ const body = container.children[0]
231
+ expect(body.props.text).toBe('ready')
232
+ flushUniversalSync(() => container.dispatchEvent(body, 'tap', {}))
233
+ await settle()
234
+ expect(container.children[0]).toBe(body)
235
+ expect(body.visible).toBe(false)
236
+ expect(container.children[1]).toMatchObject({ props: { text: 'pending' }, visible: true })
237
+ expect(container.commits.flatMap((batch: any) => batch.commands ?? batch)).toEqual(
238
+ expect.arrayContaining([expect.objectContaining({ op: 'visibility', state: 'hidden' })]),
239
+ )
240
+
241
+ resolve('resolved')
242
+ await settle()
243
+ expect(container.children[0]).toBe(body)
244
+ expect(body.visible).toBe(true)
245
+ expect(body.props.text).toBe('resolved')
246
+ container.dispatchEvent(container.children[1], 'tap', {})
247
+ await settle()
248
+ expect(fail).toHaveBeenCalledOnce()
249
+ expect(uncaught).toHaveBeenCalledWith(error)
250
+ })
251
+ })
@@ -1,20 +1,32 @@
1
1
  import { isAndroid, isIOS } from '@nativescript/core'
2
2
  import type { NativeModifier, Role } from './props'
3
3
  import { applyNativeModifiers } from './apply-native-modifiers.native'
4
+ import { rememberGridChildPlacement } from './grid-placement'
4
5
 
5
6
  /** Apply only the active device's escape bag after the primitive's own props. */
6
7
  export function applyEscapeProps(
7
8
  view: any,
8
- props: { ios?: any; android?: any; modifiers?: readonly NativeModifier[] },
9
+ props: {
10
+ ios?: any
11
+ android?: any
12
+ modifiers?: readonly NativeModifier[]
13
+ row?: number
14
+ col?: number
15
+ rowSpan?: number
16
+ colSpan?: number
17
+ },
9
18
  ): void {
10
19
  if (!view) {
11
20
  return
12
21
  }
13
22
 
23
+ rememberGridChildPlacement(view, props)
24
+
14
25
  const bag = isIOS ? props.ios : isAndroid ? props.android : undefined
15
26
  if (bag) {
16
27
  Object.assign(view, bag)
17
28
  }
29
+
18
30
  applyNativeModifiers(view, props.modifiers)
19
31
  }
20
32
 
@@ -0,0 +1,163 @@
1
+ import type { GridChildPlacement } from './grid-placement'
2
+
3
+ export interface GridCell {
4
+ row: number
5
+ col: number
6
+ }
7
+
8
+ export interface GridAutoPlacement {
9
+ cells: GridCell[]
10
+ rowCount: number
11
+ columnCount: number
12
+ }
13
+
14
+ function gridIndex(value: unknown): number | null {
15
+ if (value == null || value === '') {
16
+ return null
17
+ }
18
+
19
+ const index = Number(value)
20
+ return Number.isInteger(index) && index >= 0 ? index : null
21
+ }
22
+
23
+ function gridSpan(value: unknown): number {
24
+ const span = Number(value)
25
+ return Number.isInteger(span) && span > 0 ? span : 1
26
+ }
27
+
28
+ function trackCount(spec?: string): number {
29
+ return spec?.split(',').filter((track) => track.trim().length > 0).length ?? 0
30
+ }
31
+
32
+ /** Match CSS Grid's row-flow placement for NativeScript GridLayout children. */
33
+ export function autoPlaceGridChildren(
34
+ items: readonly GridChildPlacement[],
35
+ rowTracks?: string,
36
+ columnTracks?: string,
37
+ ): GridAutoPlacement {
38
+ const placements = items.map((item) => ({
39
+ row: gridIndex(item.row),
40
+ col: gridIndex(item.col),
41
+ rowSpan: gridSpan(item.rowSpan),
42
+ colSpan: gridSpan(item.colSpan),
43
+ }))
44
+
45
+ const columnCount = Math.max(
46
+ 1,
47
+ trackCount(columnTracks),
48
+ ...placements
49
+ .filter((placement) => placement.col != null)
50
+ .map((placement) => placement.col! + placement.colSpan),
51
+ ...placements.map((placement) => placement.colSpan),
52
+ )
53
+
54
+ const occupied = new Set<string>()
55
+ const isFree = (row: number, col: number, rowSpan: number, colSpan: number) => {
56
+ for (let y = row; y < row + rowSpan; y++) {
57
+ for (let x = col; x < col + colSpan; x++) {
58
+ if (occupied.has(`${y},${x}`)) {
59
+ return false
60
+ }
61
+ }
62
+ }
63
+
64
+ return true
65
+ }
66
+
67
+ const occupy = (row: number, col: number, rowSpan: number, colSpan: number) => {
68
+ for (let y = row; y < row + rowSpan; y++) {
69
+ for (let x = col; x < col + colSpan; x++) {
70
+ occupied.add(`${y},${x}`)
71
+ }
72
+ }
73
+ }
74
+
75
+ // Definite placements reserve their cells before automatic items flow.
76
+ for (const placement of placements) {
77
+ if (placement.row != null && placement.col != null) {
78
+ occupy(placement.row, placement.col, placement.rowSpan, placement.colSpan)
79
+ }
80
+ }
81
+
82
+ for (const placement of placements) {
83
+ if (placement.row == null || placement.col != null) {
84
+ continue
85
+ }
86
+
87
+ let col = 0
88
+ while (!isFree(placement.row, col, placement.rowSpan, placement.colSpan)) {
89
+ col++
90
+ }
91
+
92
+ placement.col = col
93
+ occupy(placement.row, col, placement.rowSpan, placement.colSpan)
94
+ }
95
+
96
+ for (const placement of placements) {
97
+ if (placement.col == null || placement.row != null) {
98
+ continue
99
+ }
100
+
101
+ let row = 0
102
+ while (!isFree(row, placement.col, placement.rowSpan, placement.colSpan)) {
103
+ row++
104
+ }
105
+
106
+ placement.row = row
107
+ occupy(row, placement.col, placement.rowSpan, placement.colSpan)
108
+ }
109
+
110
+ let cursorRow = 0
111
+ let cursorCol = 0
112
+ for (const placement of placements) {
113
+ if (placement.row != null && placement.col != null) {
114
+ continue
115
+ }
116
+
117
+ while (true) {
118
+ if (cursorCol + placement.colSpan > columnCount) {
119
+ cursorRow++
120
+ cursorCol = 0
121
+ continue
122
+ }
123
+
124
+ if (isFree(cursorRow, cursorCol, placement.rowSpan, placement.colSpan)) {
125
+ break
126
+ }
127
+
128
+ cursorCol++
129
+ }
130
+
131
+ placement.row = cursorRow
132
+ placement.col = cursorCol
133
+ occupy(cursorRow, cursorCol, placement.rowSpan, placement.colSpan)
134
+ cursorCol += placement.colSpan
135
+ if (cursorCol >= columnCount) {
136
+ cursorRow++
137
+ cursorCol = 0
138
+ }
139
+ }
140
+
141
+ return {
142
+ cells: placements.map(({ row, col }) => ({ row: row!, col: col! })),
143
+ rowCount: Math.max(
144
+ trackCount(rowTracks),
145
+ ...placements.map((item) => item.row! + item.rowSpan),
146
+ ),
147
+ columnCount: Math.max(columnCount, ...placements.map((item) => item.col! + item.colSpan)),
148
+ }
149
+ }
150
+
151
+ export function ensureGridTracks(spec: string | undefined, requiredCount: number): string {
152
+ const tracks =
153
+ spec
154
+ ?.split(',')
155
+ .map((track) => track.trim())
156
+ .filter(Boolean) ?? []
157
+
158
+ while (tracks.length < requiredCount) {
159
+ tracks.push('auto')
160
+ }
161
+
162
+ return tracks.join(',')
163
+ }
@@ -0,0 +1,29 @@
1
+ import type { LayoutChildProps } from './props'
2
+
3
+ export type GridChildPlacement = Pick<LayoutChildProps, 'row' | 'col' | 'rowSpan' | 'colSpan'>
4
+
5
+ const placements = new WeakMap<object, GridChildPlacement>()
6
+
7
+ /** Keep the authored Grid placement next to its mounted NativeScript view. */
8
+ export function rememberGridChildPlacement(view: unknown, props: GridChildPlacement): void {
9
+ if ((typeof view !== 'object' || view === null) && typeof view !== 'function') {
10
+ return
11
+ }
12
+
13
+ const placement: GridChildPlacement = {}
14
+ for (const key of ['row', 'col', 'rowSpan', 'colSpan'] as const) {
15
+ if (props[key] !== undefined) {
16
+ placement[key] = props[key]
17
+ }
18
+ }
19
+
20
+ if (Object.keys(placement).length) {
21
+ placements.set(view as object, placement)
22
+ } else {
23
+ placements.delete(view as object)
24
+ }
25
+ }
26
+
27
+ export function gridChildPlacement(view: unknown): GridChildPlacement {
28
+ return typeof view === 'object' && view !== null ? (placements.get(view) ?? {}) : {}
29
+ }
@@ -4,6 +4,7 @@ import type { CameraViewProps, WebViewProps } from './props'
4
4
  export function WebView(_props: WebViewProps) @{
5
5
  <label className="vx-unsupported">WebView is unsupported by the AppKit host</label>
6
6
  }
7
+
7
8
  export function CameraView(_props: CameraViewProps) @{
8
9
  <label className="vx-unsupported">Camera capture is unsupported by the AppKit host</label>
9
10
  }
@@ -8,7 +8,15 @@ export type { PanEvent, SwipeEvent, SetTranslate } from './props'
8
8
  export { setTranslate } from './translate.macos'
9
9
  export { Text, RichText, RichTextSpan } from './Text.macos.tsrx'
10
10
  export { Pressable } from './Pressable.macos.tsrx'
11
- export { Button, Checkbox, Switch, Slider, SegmentedControl, SearchInput, ActivityIndicator } from './controls.macos.tsrx'
11
+ export {
12
+ Button,
13
+ Checkbox,
14
+ Switch,
15
+ Slider,
16
+ SegmentedControl,
17
+ SearchInput,
18
+ ActivityIndicator,
19
+ } from './controls.macos.tsrx'
12
20
 
13
21
  export { Collapsible } from './Collapsible.macos.tsrx'
14
22
  export { Accordion } from './Accordion.macos.tsrx'
@@ -50,6 +58,7 @@ export { Drawer } from './Drawer.macos.tsrx'
50
58
 
51
59
  export { Hoverable } from './Hoverable.tsrx'
52
60
  export { Tooltip } from './Tooltip.tsrx'
61
+ export { Markdown, MarkdownScreen } from './Markdown.tsrx'
53
62
  export { Link } from './Link.macos.tsrx'
54
63
  export { NavLink } from './NavLink.macos.tsrx'
55
64
  export { TextInput, TextArea } from './text-controls.macos.tsrx'
@@ -60,14 +69,25 @@ export { WebView, CameraView } from './hosted-unsupported.macos.tsrx'
60
69
  export { Overlay, Popover, Sheet } from './surfaces.macos.tsrx'
61
70
  export { showToast } from './toast-anchor.macos.tsrx'
62
71
  export { useAnimation } from './anim.macos.tsrx'
63
- export { useThemeScheme, getThemeScheme, setThemePreference, getThemePreference, themeSchemeClasses, onThemeSchemeChange, applyThemeClasses } from './theme/theme-scheme'
72
+ export {
73
+ useThemeScheme,
74
+ getThemeScheme,
75
+ setThemePreference,
76
+ getThemePreference,
77
+ themeSchemeClasses,
78
+ onThemeSchemeChange,
79
+ applyThemeClasses,
80
+ } from './theme/theme-scheme'
81
+
64
82
  export { useColorScheme, getColorScheme } from './colorScheme.macos'
65
83
  export { styled } from './styled.macos.tsrx'
66
84
  export { openWindow } from './windows.macos'
67
85
  export { Screen } from './Screen.macos.tsrx'
68
86
  export { Tabs } from './Tabs.macos.tsrx'
69
87
  export type { TabSpec } from './props'
70
- export { SafeArea, KeyboardAvoiding } from './layout-shells.macos.tsrx'
88
+ export { SafeArea } from './layout-shells.macos.tsrx'
89
+ export { KeyboardAvoiding } from './KeyboardAvoiding.macos.tsrx'
90
+ export type { KeyboardAvoidingProps } from './props'
71
91
  export { useSafeAreaInsets } from './safeAreaInsets.macos.tsrx'
72
92
  export { useMeasure } from './useMeasure.macos.tsrx'
73
93
  export { Meter, Heading } from './macos-extras.macos.tsrx'
@@ -1,6 +1,5 @@
1
1
  export * from './index.shared'
2
2
  export { KeyboardAvoiding } from './KeyboardAvoiding.mobile.tsrx'
3
- export type { KeyboardAvoidingProps } from './props'
4
3
 
5
4
  // Keep native status-bar icon appearance synced with the effective theme scheme.
6
5
  import './theme/status-bar-scheme.mobile'
@@ -9,6 +9,8 @@ export type { PanEvent, SwipeEvent, SetTranslate } from './props'
9
9
  export { setTranslate } from './translate'
10
10
  export { Spacer } from './Spacer'
11
11
  export { Text } from './Text'
12
+ export { Markdown, MarkdownScreen } from './Markdown'
13
+ export type { MdDoc, MdNode, MdInline } from './Markdown'
12
14
  export { RichText, RichTextSpan } from './RichText'
13
15
  export { Pressable } from './Pressable'
14
16
  export { Button } from './Button'
@@ -169,6 +171,7 @@ export { Tabs } from './Tabs'
169
171
  export type { TabSpec } from './Tabs'
170
172
  export { Switch } from './Switch'
171
173
  export { SafeArea } from './SafeArea'
174
+ export { KeyboardAvoiding } from './KeyboardAvoiding'
172
175
  export { Drawer } from './Drawer'
173
176
  export { useSafeAreaInsets } from './safeAreaInsets'
174
177
  export type { SafeAreaInsets } from './safeAreaInsets'
@@ -218,7 +221,13 @@ export {
218
221
 
219
222
  export { useBackInterceptor } from './use-back'
220
223
 
221
- export { deriveRouteManifest, defineRoutes, mergeRouteManifests } from './route-table'
224
+ export {
225
+ deriveRouteManifest,
226
+ defineRoutes,
227
+ mergeRouteManifests,
228
+ manifestToJson,
229
+ } from './route-table'
230
+
222
231
  export type {
223
232
  BeforeLoad,
224
233
  BeforeLoadArgs,
@@ -230,8 +239,19 @@ export type {
230
239
  RouteHeadExport,
231
240
  RouteMeta,
232
241
  RouteManifest,
242
+ RouteManifestJson,
243
+ RouteJson,
244
+ RouteDataMode,
245
+ RouteNameOfPath,
246
+ RouteParamsFromSpecs,
247
+ RoutePathParams,
248
+ RoutePresentationsFromSpecs,
233
249
  RouteSpec,
234
250
  RouteSpecSet,
251
+ ManifestRouteNames,
252
+ ManifestRouteParams,
253
+ ManifestRoutePresentations,
254
+ SpecRouteInfo,
235
255
  ScreenTable,
236
256
  ModalOpenResult,
237
257
  OpenWindowOptions,
@@ -249,6 +269,7 @@ export type {
249
269
  DrawerProps,
250
270
  GridProps,
251
271
  ImageProps,
272
+ KeyboardAvoidingProps,
252
273
  LayoutChildProps,
253
274
  PressableProps,
254
275
  RichTextProps,