@symbiote-native/navigation 2.0.3 → 2.2.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 (320) hide show
  1. package/README.md +42 -12
  2. package/build/angular/drawer/index.js +29 -13
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/injectors/inject-navigation.js +1 -1
  5. package/build/angular/navigation-scope.directive.js +1 -1
  6. package/build/angular/search-bar-ref.directive.js +1 -1
  7. package/build/angular/stack/index.d.ts +2 -0
  8. package/build/angular/stack/index.js +43 -9
  9. package/build/angular/tabs/index.d.ts +1 -1
  10. package/build/angular/tabs/index.js +40 -13
  11. package/build/core/drawer-options/index.js +19 -5
  12. package/build/core/drawer-router-state/index.js +8 -2
  13. package/build/core/guards.js +3 -1
  14. package/build/core/header-bar-buttons.js +14 -4
  15. package/build/core/index.d.ts +6 -5
  16. package/build/core/index.js +7 -3
  17. package/build/core/navigator-state/index.js +12 -4
  18. package/build/core/render-drawer.js +4 -1
  19. package/build/core/render-stack.js +13 -4
  20. package/build/core/screen-options.js +1 -1
  21. package/build/core/search-bar-commands.js +1 -1
  22. package/build/core/stack-router-state/index.d.ts +2 -0
  23. package/build/core/stack-router-state/index.js +44 -0
  24. package/build/core/state-persistence/index.js +6 -2
  25. package/build/core/tab-router-state/index.d.ts +1 -0
  26. package/build/core/tab-router-state/index.js +27 -2
  27. package/build/react/drawer/index.js +59 -26
  28. package/build/react/index.d.ts +7 -7
  29. package/build/react/index.js +2 -2
  30. package/build/react/stack/index.js +47 -10
  31. package/build/react/tabs/index.js +36 -12
  32. package/build/solid/drawer/index.d.ts +22 -0
  33. package/build/solid/drawer/index.js +394 -0
  34. package/build/solid/drawer-screen-props.d.ts +13 -0
  35. package/build/solid/drawer-screen-props.js +2 -0
  36. package/build/solid/host.d.ts +2 -0
  37. package/build/solid/host.js +23 -0
  38. package/build/solid/index.d.ts +19 -0
  39. package/build/solid/index.js +24 -0
  40. package/build/solid/linking.d.ts +3 -0
  41. package/build/solid/linking.js +55 -0
  42. package/build/solid/navigation-context.d.ts +13 -0
  43. package/build/solid/navigation-context.js +37 -0
  44. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  45. package/build/solid/primitives/create-focus-effect.js +30 -0
  46. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  47. package/build/solid/primitives/create-is-focused.js +31 -0
  48. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  49. package/build/solid/primitives/create-navigation-state.js +30 -0
  50. package/build/solid/primitives/index.d.ts +12 -0
  51. package/build/solid/primitives/index.js +8 -0
  52. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  53. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  54. package/build/solid/primitives/use-navigation.d.ts +8 -0
  55. package/build/solid/primitives/use-navigation.js +23 -0
  56. package/build/solid/primitives/use-route.d.ts +3 -0
  57. package/build/solid/primitives/use-route.js +14 -0
  58. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  59. package/build/solid/primitives/use-stack-navigation.js +19 -0
  60. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  61. package/build/solid/primitives/use-tab-navigation.js +15 -0
  62. package/build/solid/screen-props.d.ts +19 -0
  63. package/build/solid/screen-props.js +9 -0
  64. package/build/solid/screen-registry.d.ts +31 -0
  65. package/build/solid/screen-registry.js +86 -0
  66. package/build/solid/screen.d.ts +6 -0
  67. package/build/solid/screen.js +69 -0
  68. package/build/solid/stack/index.d.ts +15 -0
  69. package/build/solid/stack/index.js +348 -0
  70. package/build/solid/tab-screen-props.d.ts +13 -0
  71. package/build/solid/tab-screen-props.js +3 -0
  72. package/build/solid/tabs/index.d.ts +14 -0
  73. package/build/solid/tabs/index.js +228 -0
  74. package/build/svelte/attachments.d.ts +5 -0
  75. package/build/svelte/attachments.js +61 -0
  76. package/build/svelte/descriptor-subtree.d.ts +3 -0
  77. package/build/svelte/descriptor-subtree.js +39 -0
  78. package/build/svelte/drawer/drawer-props.d.ts +14 -0
  79. package/build/svelte/drawer/drawer-props.js +3 -0
  80. package/build/svelte/drawer/index.d.ts +10 -0
  81. package/build/svelte/drawer/index.js +6 -0
  82. package/build/svelte/drawer/index.svelte +559 -0
  83. package/build/svelte/drawer/index.svelte.d.ts +10 -0
  84. package/build/svelte/drawer-screen-props.d.ts +13 -0
  85. package/build/svelte/drawer-screen-props.js +4 -0
  86. package/build/svelte/drawer-screen.svelte +25 -0
  87. package/build/svelte/drawer-screen.svelte.d.ts +4 -0
  88. package/build/svelte/index.d.ts +21 -0
  89. package/build/svelte/index.js +20 -0
  90. package/build/svelte/linking.svelte.d.ts +2 -0
  91. package/build/svelte/linking.svelte.js +55 -0
  92. package/build/svelte/navigation-context.d.ts +14 -0
  93. package/build/svelte/navigation-context.js +44 -0
  94. package/build/svelte/navigation-scope.svelte +27 -0
  95. package/build/svelte/navigation-scope.svelte.d.ts +9 -0
  96. package/build/svelte/registry-host.d.ts +1 -0
  97. package/build/svelte/registry-host.js +26 -0
  98. package/build/svelte/runes/index.d.ts +12 -0
  99. package/build/svelte/runes/index.js +12 -0
  100. package/build/svelte/runes/use-drawer-navigation.svelte.d.ts +6 -0
  101. package/build/svelte/runes/use-drawer-navigation.svelte.js +19 -0
  102. package/build/svelte/runes/use-focus-effect.svelte.d.ts +1 -0
  103. package/build/svelte/runes/use-focus-effect.svelte.js +31 -0
  104. package/build/svelte/runes/use-is-focused.svelte.d.ts +3 -0
  105. package/build/svelte/runes/use-is-focused.svelte.js +34 -0
  106. package/build/svelte/runes/use-navigation-state.svelte.d.ts +4 -0
  107. package/build/svelte/runes/use-navigation-state.svelte.js +39 -0
  108. package/build/svelte/runes/use-navigation.svelte.d.ts +9 -0
  109. package/build/svelte/runes/use-navigation.svelte.js +26 -0
  110. package/build/svelte/runes/use-route.svelte.d.ts +4 -0
  111. package/build/svelte/runes/use-route.svelte.js +12 -0
  112. package/build/svelte/runes/use-stack-navigation.svelte.d.ts +6 -0
  113. package/build/svelte/runes/use-stack-navigation.svelte.js +22 -0
  114. package/build/svelte/runes/use-tab-navigation.svelte.d.ts +6 -0
  115. package/build/svelte/runes/use-tab-navigation.svelte.js +19 -0
  116. package/build/svelte/screen-props.d.ts +21 -0
  117. package/build/svelte/screen-props.js +6 -0
  118. package/build/svelte/screen-registry.d.ts +17 -0
  119. package/build/svelte/screen-registry.js +57 -0
  120. package/build/svelte/screen.svelte +28 -0
  121. package/build/svelte/screen.svelte.d.ts +4 -0
  122. package/build/svelte/stack/index.d.ts +14 -0
  123. package/build/svelte/stack/index.js +7 -0
  124. package/build/svelte/stack/index.svelte +261 -0
  125. package/build/svelte/stack/index.svelte.d.ts +15 -0
  126. package/build/svelte/stack/stack-props.d.ts +20 -0
  127. package/build/svelte/stack/stack-props.js +4 -0
  128. package/build/svelte/stack/stack-screen.svelte +205 -0
  129. package/build/svelte/stack/stack-screen.svelte.d.ts +4 -0
  130. package/build/svelte/tab-screen-props.d.ts +13 -0
  131. package/build/svelte/tab-screen-props.js +4 -0
  132. package/build/svelte/tab-screen.svelte +26 -0
  133. package/build/svelte/tab-screen.svelte.d.ts +4 -0
  134. package/build/svelte/tabs/index.d.ts +8 -0
  135. package/build/svelte/tabs/index.js +6 -0
  136. package/build/svelte/tabs/index.svelte +269 -0
  137. package/build/svelte/tabs/index.svelte.d.ts +8 -0
  138. package/build/svelte/tabs/tab-props.d.ts +7 -0
  139. package/build/svelte/tabs/tab-props.js +3 -0
  140. package/build/vue/composables/use-navigation.js +5 -1
  141. package/build/vue/drawer/index.js +36 -13
  142. package/build/vue/index.d.ts +3 -3
  143. package/build/vue/navigation-context.js +1 -1
  144. package/build/vue/stack/index.js +43 -18
  145. package/build/vue/tabs/index.js +65 -39
  146. package/build-ngc/angular/drawer/index.js +40 -18
  147. package/build-ngc/angular/drawer/index.js.map +1 -1
  148. package/build-ngc/angular/drawer-screen.directive.js +3 -3
  149. package/build-ngc/angular/index.d.ts +4 -4
  150. package/build-ngc/angular/index.js.map +1 -1
  151. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  152. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  153. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  154. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  155. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  156. package/build-ngc/angular/linking.js.map +1 -1
  157. package/build-ngc/angular/navigation-context.service.js +3 -3
  158. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  159. package/build-ngc/angular/navigation-scope.directive.js +4 -4
  160. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  161. package/build-ngc/angular/screen.directive.js +3 -3
  162. package/build-ngc/angular/screen.directive.js.map +1 -1
  163. package/build-ngc/angular/search-bar-ref.directive.js +4 -4
  164. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  165. package/build-ngc/angular/stack/index.d.ts +2 -0
  166. package/build-ngc/angular/stack/index.js +52 -14
  167. package/build-ngc/angular/stack/index.js.map +1 -1
  168. package/build-ngc/angular/tab-screen.directive.js +3 -3
  169. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  170. package/build-ngc/angular/tabs/index.d.ts +1 -1
  171. package/build-ngc/angular/tabs/index.js +43 -16
  172. package/build-ngc/angular/tabs/index.js.map +1 -1
  173. package/build-ngc/core/constants.js.map +1 -1
  174. package/build-ngc/core/drawer-options/index.js +19 -5
  175. package/build-ngc/core/drawer-options/index.js.map +1 -1
  176. package/build-ngc/core/drawer-router-state/index.js +8 -2
  177. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  178. package/build-ngc/core/guards.js +3 -1
  179. package/build-ngc/core/guards.js.map +1 -1
  180. package/build-ngc/core/header-bar-buttons.js +14 -4
  181. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  182. package/build-ngc/core/index.d.ts +6 -5
  183. package/build-ngc/core/index.js +7 -3
  184. package/build-ngc/core/index.js.map +1 -1
  185. package/build-ngc/core/linking-config/index.js.map +1 -1
  186. package/build-ngc/core/navigation-events/index.js.map +1 -1
  187. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  188. package/build-ngc/core/navigator-state/index.js +12 -4
  189. package/build-ngc/core/navigator-state/index.js.map +1 -1
  190. package/build-ngc/core/render-drawer.js +4 -1
  191. package/build-ngc/core/render-drawer.js.map +1 -1
  192. package/build-ngc/core/render-stack.js +13 -4
  193. package/build-ngc/core/render-stack.js.map +1 -1
  194. package/build-ngc/core/render-tabs.js.map +1 -1
  195. package/build-ngc/core/screen-options.js +1 -1
  196. package/build-ngc/core/screen-options.js.map +1 -1
  197. package/build-ngc/core/search-bar-commands.js +1 -1
  198. package/build-ngc/core/search-bar-commands.js.map +1 -1
  199. package/build-ngc/core/stack-router-state/index.d.ts +2 -0
  200. package/build-ngc/core/stack-router-state/index.js +45 -0
  201. package/build-ngc/core/stack-router-state/index.js.map +1 -0
  202. package/build-ngc/core/state-persistence/index.js +6 -2
  203. package/build-ngc/core/state-persistence/index.js.map +1 -1
  204. package/build-ngc/core/tab-router-state/index.d.ts +1 -0
  205. package/build-ngc/core/tab-router-state/index.js +27 -2
  206. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  207. package/package.json +44 -16
  208. package/src/angular/drawer/index.ts +75 -29
  209. package/src/angular/index.ts +20 -4
  210. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  211. package/src/angular/injectors/inject-is-focused.ts +6 -4
  212. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  213. package/src/angular/injectors/inject-navigation.ts +12 -3
  214. package/src/angular/linking.ts +4 -1
  215. package/src/angular/navigation-context.service.ts +5 -2
  216. package/src/angular/navigation-scope.directive.ts +6 -2
  217. package/src/angular/screen.directive.ts +13 -3
  218. package/src/angular/search-bar-ref.directive.ts +10 -2
  219. package/src/angular/stack/index.ts +91 -24
  220. package/src/angular/tab-screen.directive.ts +3 -1
  221. package/src/angular/tabs/index.ts +54 -16
  222. package/src/core/constants.ts +6 -3
  223. package/src/core/drawer-options/index.ts +40 -13
  224. package/src/core/drawer-router-state/index.ts +11 -3
  225. package/src/core/guards.ts +3 -1
  226. package/src/core/header-bar-buttons.ts +41 -12
  227. package/src/core/index.ts +29 -5
  228. package/src/core/linking-config/index.ts +31 -10
  229. package/src/core/navigation-events/index.ts +8 -2
  230. package/src/core/navigator-handles/index.ts +8 -3
  231. package/src/core/navigator-props.ts +13 -4
  232. package/src/core/navigator-state/index.ts +24 -7
  233. package/src/core/render-drawer.ts +24 -9
  234. package/src/core/render-stack.ts +52 -13
  235. package/src/core/render-tabs.ts +24 -6
  236. package/src/core/screen-options.ts +15 -5
  237. package/src/core/search-bar-commands.ts +12 -3
  238. package/src/core/stack-router-state/index.ts +52 -0
  239. package/src/core/state-persistence/index.ts +13 -3
  240. package/src/core/tab-router-state/index.ts +35 -3
  241. package/src/react/drawer/index.ts +409 -330
  242. package/src/react/drawer-screen.ts +8 -2
  243. package/src/react/hooks/use-focus-effect.ts +8 -2
  244. package/src/react/hooks/use-is-focused.ts +6 -2
  245. package/src/react/hooks/use-navigation.ts +8 -2
  246. package/src/react/index.ts +37 -7
  247. package/src/react/linking.ts +4 -1
  248. package/src/react/navigation-context.ts +6 -2
  249. package/src/react/screen.ts +12 -3
  250. package/src/react/stack/index.ts +297 -220
  251. package/src/react/tab-screen.ts +3 -1
  252. package/src/react/tabs/index.ts +202 -139
  253. package/src/solid/drawer/index.ts +570 -0
  254. package/src/solid/drawer-screen-props.ts +24 -0
  255. package/src/solid/host.ts +29 -0
  256. package/src/solid/index.ts +87 -0
  257. package/src/solid/linking.ts +74 -0
  258. package/src/solid/navigation-context.ts +56 -0
  259. package/src/solid/primitives/create-focus-effect.ts +41 -0
  260. package/src/solid/primitives/create-is-focused.ts +41 -0
  261. package/src/solid/primitives/create-navigation-state.ts +44 -0
  262. package/src/solid/primitives/index.ts +12 -0
  263. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  264. package/src/solid/primitives/use-navigation.ts +44 -0
  265. package/src/solid/primitives/use-route.ts +18 -0
  266. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  267. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  268. package/src/solid/screen-props.ts +51 -0
  269. package/src/solid/screen-registry.ts +159 -0
  270. package/src/solid/screen.ts +80 -0
  271. package/src/solid/stack/index.ts +528 -0
  272. package/src/solid/tab-screen-props.ts +21 -0
  273. package/src/solid/tabs/index.ts +327 -0
  274. package/src/svelte/attachments.ts +70 -0
  275. package/src/svelte/descriptor-subtree.ts +51 -0
  276. package/src/svelte/drawer/drawer-props.ts +28 -0
  277. package/src/svelte/drawer/index.svelte +559 -0
  278. package/src/svelte/drawer/index.ts +10 -0
  279. package/src/svelte/drawer-screen-props.ts +26 -0
  280. package/src/svelte/drawer-screen.svelte +25 -0
  281. package/src/svelte/fabric-tree.test-helper.ts +95 -0
  282. package/src/svelte/index.ts +83 -0
  283. package/src/svelte/linking.svelte.ts +72 -0
  284. package/src/svelte/navigation-context.ts +62 -0
  285. package/src/svelte/navigation-scope.svelte +27 -0
  286. package/src/svelte/registry-host.ts +26 -0
  287. package/src/svelte/runes/index.ts +17 -0
  288. package/src/svelte/runes/use-drawer-navigation.svelte.ts +31 -0
  289. package/src/svelte/runes/use-focus-effect.svelte.ts +41 -0
  290. package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
  291. package/src/svelte/runes/use-navigation-state.svelte.ts +48 -0
  292. package/src/svelte/runes/use-navigation.svelte.ts +49 -0
  293. package/src/svelte/runes/use-route.svelte.ts +16 -0
  294. package/src/svelte/runes/use-stack-navigation.svelte.ts +34 -0
  295. package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
  296. package/src/svelte/screen-props.ts +50 -0
  297. package/src/svelte/screen-registry.ts +98 -0
  298. package/src/svelte/screen.svelte +28 -0
  299. package/src/svelte/stack/index.svelte +261 -0
  300. package/src/svelte/stack/index.ts +11 -0
  301. package/src/svelte/stack/stack-props.ts +31 -0
  302. package/src/svelte/stack/stack-screen.svelte +205 -0
  303. package/src/svelte/svelte-ambient.d.ts +6 -0
  304. package/src/svelte/svelte-compile.test-helper.ts +147 -0
  305. package/src/svelte/tab-screen-props.ts +22 -0
  306. package/src/svelte/tab-screen.svelte +26 -0
  307. package/src/svelte/tabs/index.svelte +269 -0
  308. package/src/svelte/tabs/index.ts +10 -0
  309. package/src/svelte/tabs/tab-props.ts +13 -0
  310. package/src/vue/composables/use-navigation-state.ts +7 -4
  311. package/src/vue/composables/use-navigation.ts +13 -3
  312. package/src/vue/drawer/index.ts +128 -41
  313. package/src/vue/drawer-screen.ts +12 -5
  314. package/src/vue/index.ts +15 -3
  315. package/src/vue/linking.ts +7 -2
  316. package/src/vue/navigation-context.ts +17 -5
  317. package/src/vue/screen.ts +7 -2
  318. package/src/vue/stack/index.ts +108 -43
  319. package/src/vue/tab-screen.ts +3 -1
  320. package/src/vue/tabs/index.ts +109 -68
@@ -11,19 +11,23 @@
11
11
  import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, } from 'react';
12
12
  import { descriptorToReact } from '@symbiote-native/react';
13
13
  import { Platform, debugNodeId, dlog } from '@symbiote-native/engine';
14
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
14
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
15
15
  import { collectRegistry } from '../collect-registry.js';
16
16
  import { NavigationContext } from '../navigation-context.js';
17
17
  import { Screen } from '../screen.js';
18
18
  // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
19
19
  // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single constant
20
20
  // stands in for the per-platform injection point ISliderPlatform-style adapters use elsewhere.
21
- const NAVIGATOR_PLATFORM = { defaultHeaderBackTitleVisible: true };
21
+ const NAVIGATOR_PLATFORM = {
22
+ defaultHeaderBackTitleVisible: true,
23
+ };
22
24
  function isScreenElement(child) {
23
25
  return isValidElement(child) && child.type === Screen;
24
26
  }
25
27
  function resolveScreenOptions(entry, optionsArgs, screenOptions) {
26
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
28
+ const own = typeof entry.options === 'function'
29
+ ? entry.options(optionsArgs)
30
+ : entry.options;
27
31
  return { ...screenOptions, ...own };
28
32
  }
29
33
  const StackImpl = forwardRef((props, forwardedRef) => {
@@ -51,16 +55,35 @@ const StackImpl = forwardRef((props, forwardedRef) => {
51
55
  }, [emitters]);
52
56
  const createRoute = useCallback((name, params) => {
53
57
  routeSequence.current += 1;
54
- return { key: `${routeIdPrefix}-${name}-${routeSequence.current}`, name, params };
58
+ return {
59
+ key: `${routeIdPrefix}-${name}-${routeSequence.current}`,
60
+ name,
61
+ params,
62
+ };
55
63
  }, [routeIdPrefix]);
56
- const [state, dispatch] = useReducer(navigatorReducer, undefined, () => {
64
+ const registeredNames = useMemo(() => [...registry.keys()], [registry]);
65
+ const [dispatchedState, dispatch] = useReducer(navigatorReducer, undefined, () => {
57
66
  const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
58
67
  if (initialRouteName === undefined) {
59
68
  dlog('Stack: no <Stack.Screen> children registered');
60
- return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
69
+ return createInitialNavigatorState({
70
+ key: routeIdPrefix,
71
+ name: '',
72
+ params: undefined,
73
+ });
61
74
  }
62
75
  return createInitialNavigatorState(createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams));
63
76
  });
77
+ // A <Stack.Screen> marker can unregister while its route is still in the pushed history, which
78
+ // would leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling here
79
+ // rather than in an effect repairs the CURRENT paint - the call is pure and hands back the same
80
+ // reference when nothing changed, so it is safe during render; the effect below only PERSISTS
81
+ // the repair, so the next push builds on the pruned history instead of the phantom one.
82
+ const state = reconcileStackRoutes(dispatchedState, registeredNames);
83
+ useEffect(() => {
84
+ if (state !== dispatchedState)
85
+ dispatch({ type: 'reset', state });
86
+ }, [state, dispatchedState]);
64
87
  const handle = useMemo(() => ({
65
88
  push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
66
89
  pop: count => dispatch({ type: 'pop', count }),
@@ -101,7 +124,10 @@ const StackImpl = forwardRef((props, forwardedRef) => {
101
124
  dlog(`Stack: no screen registered for route name "${route.name}"`);
102
125
  return null;
103
126
  }
104
- const screenOptionsArgs = { route, navigation: handle };
127
+ const screenOptionsArgs = {
128
+ route,
129
+ navigation: handle,
130
+ };
105
131
  const mergedOptions = resolveScreenOptions(entry, screenOptionsArgs, props.screenOptions);
106
132
  const activityState = computeActivityState(index, state.routes.length);
107
133
  // Investigation instrumentation (flicker-on-focus bug): fires on EVERY Stack render, not just
@@ -155,7 +181,8 @@ const StackImpl = forwardRef((props, forwardedRef) => {
155
181
  const appRef = searchBarOptions.ref;
156
182
  if (!appRef)
157
183
  return;
158
- appRef.current = node === null ? null : buildSearchBarHandle(() => node);
184
+ appRef.current =
185
+ node === null ? null : buildSearchBarHandle(() => node);
159
186
  },
160
187
  }
161
188
  : undefined,
@@ -177,7 +204,14 @@ const StackImpl = forwardRef((props, forwardedRef) => {
177
204
  // sizing (see RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME's comment in core/constants.ts). A
178
205
  // `push` screen doesn't need it, but a `formSheet` one is otherwise left with no content
179
206
  // ever attached natively.
180
- const content = createElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, plan.contentWrapperProps, createElement(NavigationContext.Provider, { value: { route, navigation: handle, emitter: routeEmitter, parent: ambientContext } }, createElement(entry.component)));
207
+ const content = createElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, plan.contentWrapperProps, createElement(NavigationContext.Provider, {
208
+ value: {
209
+ route,
210
+ navigation: handle,
211
+ emitter: routeEmitter,
212
+ parent: ambientContext,
213
+ },
214
+ }, createElement(entry.component)));
181
215
  // react-native-screens' own Screen.tsx swaps in a DIFFERENT Fabric component
182
216
  // ('RNSModalScreen') for a modally-presented screen - its native updateLayoutMetrics: relies on
183
217
  // this exact class to know it must NOT apply Yoga's computed frame (see resolveScreenViewName's
@@ -194,7 +228,10 @@ const StackImpl = forwardRef((props, forwardedRef) => {
194
228
  // RNSScreen.mm treats an unset/inactive nested screen as not yet pushed, leaving it
195
229
  // parked at its pre-push transition position (off past the bottom edge) instead of
196
230
  // its real, presented frame.
197
- { style: plan.innerScreenStyle, activityState: plan.activityState }, descriptorToReact(plan.headerConfig), content)))
231
+ {
232
+ style: plan.innerScreenStyle,
233
+ activityState: plan.activityState,
234
+ }, descriptorToReact(plan.headerConfig), content)))
198
235
  : createElement(plan.screenViewName, { key: route.key, ...plan.screenProps }, descriptorToReact(plan.headerConfig), content);
199
236
  });
200
237
  return createElement(RNS_SCREEN_STACK_VIEW_NAME, stackProps, ...children);
@@ -1,15 +1,15 @@
1
1
  // Tab, the React lifecycle half. The focused-index router (tab-router-state) and the tab-bar
2
2
  // Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
3
- // with the Vue/Angular adapters; here React supplies the lifecycle - useReducer for the focused
4
- // index, useId for route-key generation, useImperativeHandle for the jumpTo/setParams handle -
5
- // plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header config
6
- // (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
3
+ // with the Vue/Angular adapters; here React supplies the lifecycle - useState for the dispatched
4
+ // router state, useId for route-key generation, useImperativeHandle for the jumpTo/setParams
5
+ // handle - plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header
6
+ // config (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
7
7
  // `symbiote-view`/`symbiote-text` primitives via the shared render fn, so there is no
8
8
  // react-native-screens ViewConfig to register here - Tab needs no `../register` import.
9
- import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, } from 'react';
9
+ import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useState, } from 'react';
10
10
  import { descriptorToReact } from '@symbiote-native/react';
11
11
  import { dlog } from '@symbiote-native/engine';
12
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
12
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
13
13
  import { collectRegistry } from '../collect-registry.js';
14
14
  import { NavigationContext } from '../navigation-context.js';
15
15
  import { TabScreen } from '../tab-screen.js';
@@ -17,7 +17,9 @@ function isTabScreenElement(child) {
17
17
  return isValidElement(child) && child.type === TabScreen;
18
18
  }
19
19
  function resolveTabOptions(entry, optionsArgs, screenOptions) {
20
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
20
+ const own = typeof entry.options === 'function'
21
+ ? entry.options(optionsArgs)
22
+ : entry.options;
21
23
  return { ...screenOptions, ...own };
22
24
  }
23
25
  const TAB_CONTENT_STYLE = { flex: 1 };
@@ -34,11 +36,27 @@ const TabImpl = forwardRef((props, forwardedRef) => {
34
36
  name,
35
37
  params: entry.initialParams,
36
38
  })), [registry, routeIdPrefix]);
37
- const [state, dispatch] = useReducer(tabRouterReducer, undefined, () => createInitialTabState(routes, props.initialRouteName));
39
+ // A <Tab.Screen> can appear or disappear after mount (a marker behind a conditional, a
40
+ // data-driven screen list), so the route list is re-derived from `children` on every render and
41
+ // the STORED state is reconciled against it (reconcileTabRoutes, core - it preserves each
42
+ // surviving route's key and accumulated params and keeps focus on the same route NAME) rather
43
+ // than staying frozen at whatever the first render saw. `null` means no marker had registered
44
+ // yet: seeding an empty list would resolve initialRouteName against nothing and lose it, so the
45
+ // state stays derived until either a marker arrives or something dispatches.
46
+ const [storedState, setStoredState] = useState(() => routes.length === 0
47
+ ? null
48
+ : createInitialTabState(routes, props.initialRouteName));
49
+ const resolveState = useCallback((stored) => stored === null
50
+ ? createInitialTabState(routes, props.initialRouteName)
51
+ : reconcileTabRoutes(stored, routes), [routes, props.initialRouteName]);
52
+ const state = resolveState(storedState);
38
53
  if (routes.length === 0)
39
54
  dlog('Tab: no <Tab.Screen> children registered');
40
- const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), []);
41
- const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), []);
55
+ // The updater re-resolves rather than closing over `state` so that two dispatches batched into
56
+ // one render still compose: the second reduces over the first's result, not a stale snapshot.
57
+ const dispatch = useCallback((action) => setStoredState(current => tabRouterReducer(resolveState(current), action)), [resolveState]);
58
+ const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), [dispatch]);
59
+ const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), [dispatch]);
42
60
  const handle = useMemo(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
43
61
  useImperativeHandle(forwardedRef, () => handle, [handle]);
44
62
  const focusedRoute = state.routes[state.index];
@@ -70,11 +88,17 @@ const TabImpl = forwardRef((props, forwardedRef) => {
70
88
  },
71
89
  };
72
90
  });
73
- const focusedEntry = focusedRoute ? registry.get(focusedRoute.name) : undefined;
91
+ const focusedEntry = focusedRoute
92
+ ? registry.get(focusedRoute.name)
93
+ : undefined;
74
94
  const focusedOptions = focusedEntry && focusedRoute
75
95
  ? resolveTabOptions(focusedEntry, { route: focusedRoute, navigation: handle }, props.screenOptions)
76
96
  : props.screenOptions;
77
- const tabBar = descriptorToReact(renderTabBar({ items, style: focusedOptions?.tabBarStyle, passthrough: {} }));
97
+ const tabBar = descriptorToReact(renderTabBar({
98
+ items,
99
+ style: focusedOptions?.tabBarStyle,
100
+ passthrough: {},
101
+ }));
78
102
  // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
79
103
  // alive), so a fresh emitter per focus change is sufficient - no per-route emitter map to prune,
80
104
  // because the previous screen's whole subtree (and any listeners it registered) is torn down by
@@ -0,0 +1,22 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import type { JSX } from '@symbiote-native/solid/jsx-runtime';
3
+ import type { IStyleProp, IViewStyle } from '@symbiote-native/engine';
4
+ import type { IDrawerDescriptorMap, IDrawerNavigatorHandle, IDrawerOptions, IDrawerRouterState } from '../../core';
5
+ import { DrawerScreen } from '../screen';
6
+ export type { IDrawerNavigatorHandle, IDrawerDescriptorMap } from '../../core';
7
+ export type IDrawerContentSlotProps = {
8
+ state: IDrawerRouterState;
9
+ descriptors: IDrawerDescriptorMap;
10
+ navigation: IDrawerNavigatorHandle;
11
+ };
12
+ export type IDrawerProps = IDrawerOptions & {
13
+ initialRouteName?: string;
14
+ drawerStyle?: IStyleProp<IViewStyle>;
15
+ drawerContent?: (props: Accessor<IDrawerContentSlotProps>) => JSX.Element;
16
+ ref?: (handle: IDrawerNavigatorHandle) => void;
17
+ children?: JSX.Element;
18
+ };
19
+ declare function DrawerImpl(props: IDrawerProps): JSX.Element;
20
+ export declare const Drawer: typeof DrawerImpl & {
21
+ Screen: typeof DrawerScreen;
22
+ };
@@ -0,0 +1,394 @@
1
+ // Drawer, the Solid lifecycle half. The open/closed + focused-route router (drawer-router-state)
2
+ // and the pure swipe/geometry math (drawer-options) live in @symbiote-native/navigation core,
3
+ // shared verbatim with every other adapter; here Solid supplies the lifecycle - a signal for the
4
+ // router, a PanResponder built ONCE whose callbacks read options/window-width/isOpen LIVE (Solid
5
+ // props are getters, so a gesture callback always sees the current value with no ref-mirroring
6
+ // dance), an Animated.Value driving the slide/opacity transforms, and a callback `ref` for the
7
+ // open/close/toggle/jumpTo handle. Tab is the closer sibling (both are fixed-route-list,
8
+ // no-react-native-screens navigators); Stack's push/pop and native-screen bridging don't apply.
9
+ //
10
+ // FEASIBILITY NOTE (mirrored verbatim from the React/Vue twins - the gaps are architectural, not
11
+ // adapter-specific): the REAL @react-navigation/drawer is built on react-native-gesture-handler +
12
+ // react-native-reanimated, neither of which this codebase depends on. What's built here reaches the
13
+ // same swipe-to-open/close + front/back/slide/permanent behavior using only PanResponder +
14
+ // Animated (both already in @symbiote-native/engine). See the explicit gap list at the bottom.
15
+ //
16
+ // THE SOLID-ONLY PART: renderDrawer's Descriptor SHAPE follows `drawerType` - the slot ORDER
17
+ // changes, and 'permanent' drops the animation entirely. descriptorToSolid builds once and updates
18
+ // props in place, so a type/position change is a genuine rebuild boundary and is asked for by name
19
+ // (a createMemo over those discriminators alone, build untracked -
20
+ // .claude/rules/solid-descriptor-bridge.md §5). Everything else - width, overlay colour, the live
21
+ // interpolations, the focused screen's params - flows through prop accessors on the existing nodes.
22
+ //
23
+ // `drawerContent` is a render prop taking an ACCESSOR, called once and untracked, not a plain
24
+ // value: §4 of the same rules file. A value snapshot would freeze the drawer's own content at the
25
+ // state it was built with, and re-invoking it from inside a tracked scope would replace the whole
26
+ // panel subtree mid-gesture.
27
+ import { createEffect, createMemo, createSignal, mergeProps, onCleanup, untrack, } from 'solid-js';
28
+ import { Animated, createWindowDimensions } from '@symbiote-native/solid';
29
+ // createComponent from solid-js, insert from the renderer - see stack/index.ts's note.
30
+ import { createComponent } from 'solid-js';
31
+ import { insert } from '@symbiote-native/solid/renderer';
32
+ import { PanResponder, dlog } from '@symbiote-native/engine';
33
+ import { DRAWER_DEFAULT_OVERLAY_COLOR, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialDrawerRouterState, createNavigationEmitter, diffFocusedRoute, drawerChildOrder, drawerRouterReducer, isDrawerAnimated, renderDrawer, resolveDragProgress, resolveDrawerGeometry, resolveDrawerPosition, resolveDrawerSlotInterpolation, resolveDrawerType, resolveSwipeIntent, shouldClaimDrawerSwipe, } from '../../core/index.js';
34
+ import { hostElement } from '../host.js';
35
+ import { NavigationScopeProvider, useNavigationScope, } from '../navigation-context.js';
36
+ import { ScreenCollectorProvider, createScreenSignal, toRegistry, } from '../screen-registry.js';
37
+ import { DrawerScreen } from '../screen.js';
38
+ const DRAWER_SNAP_DURATION = 250;
39
+ let navigatorSequence = 0;
40
+ function resolveDrawerScreenOptions(entry, args) {
41
+ if (typeof entry.options === 'function')
42
+ return entry.options(args);
43
+ return entry.options ?? {};
44
+ }
45
+ function sameDrawerShape(a, b) {
46
+ return (a.type === b.type && a.position === b.position && a.animated === b.animated);
47
+ }
48
+ function DrawerImpl(props) {
49
+ const parentScope = useNavigationScope();
50
+ const { screens, collector } = createScreenSignal('drawer');
51
+ const registry = createMemo(() => toRegistry(screens()));
52
+ navigatorSequence += 1;
53
+ const routeIdPrefix = `drawer-${navigatorSequence}`;
54
+ // Every field read live off `props`, so a gesture callback (built once, below) always negotiates
55
+ // against the CURRENT options - Solid props are getters, which is what removes React's
56
+ // useRef(value) mirroring here.
57
+ function currentOptions() {
58
+ return {
59
+ drawerType: props.drawerType,
60
+ drawerPosition: props.drawerPosition,
61
+ drawerWidth: props.drawerWidth,
62
+ overlayColor: props.overlayColor,
63
+ swipeEnabled: props.swipeEnabled,
64
+ swipeEdgeWidth: props.swipeEdgeWidth,
65
+ swipeMinDistance: props.swipeMinDistance,
66
+ swipeMinVelocity: props.swipeMinVelocity,
67
+ };
68
+ }
69
+ // Route keys are derived from the screen NAME, so the derivation is pure and re-runnable.
70
+ const routes = createMemo(() => [...registry().entries()].map(([name, entry]) => ({
71
+ key: `${routeIdPrefix}-${name}`,
72
+ name,
73
+ params: entry.initialParams,
74
+ })));
75
+ // `null` means nothing has dispatched yet - and until then the initial state is RE-DERIVED from
76
+ // the registry on every read rather than cached. That is not an optimisation detail, it is what
77
+ // makes the drawer correct: each marker registers with its OWN signal write, and Solid can
78
+ // recompute a downstream memo between two of them, so a one-shot seed reliably captured only the
79
+ // FIRST screen and every later jumpTo was a no-op against a one-route router. Once the app
80
+ // dispatches, the route list freezes - matching React's and Vue's, which build it once in their
81
+ // setup pass and never reconcile (a marker added after that is ignored on every adapter).
82
+ const [dispatched, setDispatched] = createSignal(null);
83
+ const state = createMemo(() => {
84
+ const current = dispatched();
85
+ if (current !== null)
86
+ return current;
87
+ const initial = routes();
88
+ if (initial.length === 0) {
89
+ dlog('Drawer: no <Drawer.Screen> children registered');
90
+ }
91
+ return createInitialDrawerRouterState(initial, props.initialRouteName);
92
+ });
93
+ function dispatch(action) {
94
+ setDispatched(drawerRouterReducer(state(), action));
95
+ }
96
+ const windowDimensions = createWindowDimensions();
97
+ // progress: 0 closed -> 1 open, the single Animated.Value every slide/opacity transform below
98
+ // interpolates from. A plain const: a Solid body runs once, so this needs no re-creation guard
99
+ // the way React's useRef(new Animated.Value(...)).current does.
100
+ const progress = new Animated.Value(state().isOpen ? 1 : 0);
101
+ // Where a drag STARTS from, in progress units. Always exactly 0 or 1: a gesture only begins at
102
+ // rest, since terminate/release always snap back before another grant can fire.
103
+ let dragStartProgress = 0;
104
+ function animateProgressTo(open) {
105
+ dlog(`Drawer: animateProgressTo(open=${open}) at t=${Date.now()}`);
106
+ Animated.timing(progress, {
107
+ toValue: open ? 1 : 0,
108
+ duration: DRAWER_SNAP_DURATION,
109
+ // Native-driver wiring is deferred for v1 - see the header feasibility note.
110
+ useNativeDriver: false,
111
+ }).start();
112
+ }
113
+ const handle = {
114
+ openDrawer: () => {
115
+ animateProgressTo(true);
116
+ dispatch({ type: 'openDrawer' });
117
+ },
118
+ closeDrawer: () => {
119
+ animateProgressTo(false);
120
+ dispatch({ type: 'closeDrawer' });
121
+ },
122
+ toggleDrawer: () => {
123
+ animateProgressTo(!state().isOpen);
124
+ dispatch({ type: 'toggleDrawer' });
125
+ },
126
+ jumpTo: (name) => {
127
+ // Both sides of the dispatch, because an unregistered name is a documented reducer no-op that
128
+ // hands the SAME state back: animating off the pre-dispatch snapshot alone would slide the
129
+ // panel shut while the router still says isOpen. The signal is written synchronously inside
130
+ // dispatch, so the second read is already the reducer's own answer.
131
+ const wasOpen = state().isOpen;
132
+ dispatch({ type: 'jumpTo', name });
133
+ if (wasOpen && !state().isOpen)
134
+ animateProgressTo(false);
135
+ },
136
+ };
137
+ props.ref?.(handle);
138
+ const panResponder = PanResponder.create({
139
+ onStartShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'start'),
140
+ onMoveShouldSetPanResponder: (event, gestureState) => shouldClaimDrawerSwipe(event, gestureState, windowDimensions().width, state().isOpen, currentOptions(), 'move'),
141
+ onPanResponderGrant: () => {
142
+ dragStartProgress = state().isOpen ? 1 : 0;
143
+ },
144
+ onPanResponderMove: (_event, gestureState) => {
145
+ progress.setValue(resolveDragProgress(gestureState, dragStartProgress, currentOptions()));
146
+ },
147
+ onPanResponderRelease: (_event, gestureState) => {
148
+ const intent = resolveSwipeIntent(gestureState, state().isOpen, currentOptions());
149
+ const open = intent === 'open';
150
+ dlog(`Drawer: gesture release -> ${open ? 'open' : 'close'}`);
151
+ animateProgressTo(open);
152
+ dispatch(open ? { type: 'openDrawer' } : { type: 'closeDrawer' });
153
+ },
154
+ onPanResponderTerminate: () => {
155
+ animateProgressTo(state().isOpen);
156
+ },
157
+ });
158
+ const emitters = new Map();
159
+ function emitterFor(routeKey) {
160
+ let emitter = emitters.get(routeKey);
161
+ if (emitter === undefined) {
162
+ emitter = createNavigationEmitter();
163
+ emitters.set(routeKey, emitter);
164
+ }
165
+ return emitter;
166
+ }
167
+ const focusedKey = createMemo(() => {
168
+ const current = state();
169
+ return current.routes[current.index]?.key;
170
+ });
171
+ // Deferred by a microtask for the same reason Tab's is: the newly focused screen's subtree, and
172
+ // the subscriptions inside it, are built by the effect flush this callback would otherwise run
173
+ // inside.
174
+ let lastFocusedKey;
175
+ createEffect(() => {
176
+ const nextKey = focusedKey();
177
+ const { blurKey, focusKey } = diffFocusedRoute(lastFocusedKey, nextKey);
178
+ if (blurKey === undefined && focusKey === undefined)
179
+ return;
180
+ lastFocusedKey = nextKey;
181
+ queueMicrotask(() => {
182
+ if (blurKey !== undefined) {
183
+ dlog(`Drawer: route "${blurKey}" blurred at t=${Date.now()}`);
184
+ emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
185
+ }
186
+ if (focusKey !== undefined) {
187
+ dlog(`Drawer: route "${focusKey}" focused at t=${Date.now()}`);
188
+ emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
189
+ }
190
+ });
191
+ });
192
+ onCleanup(() => {
193
+ if (lastFocusedKey !== undefined) {
194
+ emitterFor(lastFocusedKey).emit(NAVIGATION_EVENT_BLUR);
195
+ }
196
+ });
197
+ function routeFor(routeKey) {
198
+ const found = state().routes.find(route => route.key === routeKey);
199
+ return found ?? { key: routeKey, name: '', params: undefined };
200
+ }
201
+ const focusedComponent = createMemo(() => {
202
+ const current = state();
203
+ const route = current.routes[current.index];
204
+ return route === undefined
205
+ ? undefined
206
+ : registry().get(route.name)?.component;
207
+ });
208
+ const content = createMemo(() => {
209
+ const routeKey = focusedKey();
210
+ const Component = focusedComponent();
211
+ return untrack(() => {
212
+ if (routeKey === undefined || Component === undefined)
213
+ return undefined;
214
+ const scope = () => ({
215
+ route: routeFor(routeKey),
216
+ navigation: handle,
217
+ emitter: emitterFor(routeKey),
218
+ parent: parentScope?.(),
219
+ });
220
+ return createComponent(NavigationScopeProvider, {
221
+ value: scope,
222
+ get children() {
223
+ return createComponent(Component, {});
224
+ },
225
+ });
226
+ });
227
+ });
228
+ const descriptors = createMemo(() => {
229
+ const map = {};
230
+ const entries = registry();
231
+ for (const route of state().routes) {
232
+ const entry = entries.get(route.name);
233
+ if (entry === undefined)
234
+ continue;
235
+ map[route.key] = {
236
+ options: resolveDrawerScreenOptions(entry, {
237
+ route,
238
+ navigation: handle,
239
+ }),
240
+ navigation: handle,
241
+ };
242
+ }
243
+ return map;
244
+ });
245
+ // Called ONCE, untracked, with an accessor - trap §4. Calling it per change (or handing it a
246
+ // value) would rebuild the whole panel subtree on every drawer state change, which mid-gesture is
247
+ // exactly how a responder grant gets lost.
248
+ //
249
+ // LAZY, though, and inside a memo of its OWN - two separate reasons, both load-bearing.
250
+ // (a) Not called from this body directly: the markers have not registered yet at that point, so a
251
+ // render prop reading `state`/`descriptors` at its own top level (which trap §4 documents as
252
+ // a frozen read) would snapshot an EMPTY navigator.
253
+ // (b) Not a plain `let` cache either. Whatever computation first CALLS the render prop OWNS every
254
+ // memo the returned subtree created, and disposes them when it re-runs - so a cached node
255
+ // handed back to a re-running slot effect is a node whose own reactivity has been torn down,
256
+ // silently frozen at its first paint. A dependency-free memo has a stable owner (this
257
+ // component) and runs exactly once, which gives both properties at the same time.
258
+ const drawerContent = createMemo(() => untrack(() => props.drawerContent?.(() => ({
259
+ state: state(),
260
+ descriptors: descriptors(),
261
+ navigation: handle,
262
+ }))));
263
+ const shape = createMemo(() => {
264
+ const options = currentOptions();
265
+ return {
266
+ type: resolveDrawerType(options),
267
+ position: resolveDrawerPosition(options),
268
+ animated: isDrawerAnimated(options),
269
+ };
270
+ }, { type: '', position: '', animated: false }, { equals: sameDrawerShape });
271
+ function buildDrawer(current) {
272
+ const order = drawerChildOrder(currentOptions());
273
+ const rootDescriptor = createMemo(() => {
274
+ const options = currentOptions();
275
+ return renderDrawer({
276
+ overlayColor: props.overlayColor ?? DRAWER_DEFAULT_OVERLAY_COLOR,
277
+ drawerStyle: props.drawerStyle,
278
+ contentPassthrough: {},
279
+ overlayPassthrough: current.animated
280
+ ? {
281
+ pointerEvents: state().isOpen ? 'auto' : 'none',
282
+ onStartShouldSetResponder: () => true,
283
+ onResponderRelease: () => {
284
+ animateProgressTo(false);
285
+ dispatch({ type: 'closeDrawer' });
286
+ },
287
+ }
288
+ : {},
289
+ panelPassthrough: {},
290
+ }, options);
291
+ });
292
+ function slotDescriptor(slot) {
293
+ const child = rootDescriptor().children[order.indexOf(slot)];
294
+ if (child === undefined || typeof child === 'string') {
295
+ throw new Error(`Drawer: renderDrawer produced no "${slot}" slot`);
296
+ }
297
+ return child;
298
+ }
299
+ // Each animated style holds an AnimatedInterpolation node, not a plain number/colour - it feeds
300
+ // only Animated.View's deliberately permissive `style?: unknown`, never the plain-IViewStyle
301
+ // branch. Recomputed reactively (geometry follows drawerWidth), which reaches the live node as
302
+ // an ordinary prop update rather than a rebuild.
303
+ function animatedStyle(slot) {
304
+ if (!current.animated)
305
+ return undefined;
306
+ const geometry = resolveDrawerGeometry(currentOptions());
307
+ // Three branches, not one call on a `slot` union: resolveDrawerSlotInterpolation is OVERLOADED
308
+ // per slot (only the overlay's result carries `opacity`), and a union argument matches no
309
+ // overload. The branch is also what makes the opacity read type-safe without a cast.
310
+ if (slot === 'overlay') {
311
+ // The overlay is a full-screen absolutely-positioned sibling BELOW content in paint order;
312
+ // for 'slide' the content itself translates away, so the overlay's translateX shares
313
+ // content's range rather than staying pinned full-screen.
314
+ const overlay = resolveDrawerSlotInterpolation(geometry, 'overlay');
315
+ return {
316
+ opacity: progress.interpolate(overlay.opacity),
317
+ transform: [{ translateX: progress.interpolate(overlay.translateX) }],
318
+ };
319
+ }
320
+ const interpolation = slot === 'content'
321
+ ? resolveDrawerSlotInterpolation(geometry, 'content')
322
+ : resolveDrawerSlotInterpolation(geometry, 'panel');
323
+ return {
324
+ transform: [
325
+ { translateX: progress.interpolate(interpolation.translateX) },
326
+ ],
327
+ };
328
+ }
329
+ function slotChildren(slot) {
330
+ if (slot === 'content')
331
+ return content();
332
+ if (slot === 'panel')
333
+ return drawerContent();
334
+ return undefined;
335
+ }
336
+ function buildSlot(slot) {
337
+ const style = animatedStyle(slot);
338
+ if (style === undefined) {
339
+ const node = hostElement(slotDescriptor(slot).type, () => slotDescriptor(slot).props);
340
+ const children = slotChildren(slot);
341
+ if (children !== undefined)
342
+ insert(node, children);
343
+ return node;
344
+ }
345
+ // ONE source, never a spread followed by an explicit prop: mergeProps takes the first
346
+ // NON-undefined value scanning back-to-front, so a later `undefined` silently loses to an
347
+ // earlier value (.claude/rules/solid-descriptor-bridge.md §6). Collapsing to a single bag
348
+ // removes the question. mergeProps still earns its place - it wraps the function source in a
349
+ // memo and hands Animated.View a reactive proxy, which is what keeps the slot's props live
350
+ // without rebuilding the node.
351
+ return createComponent(Animated.View, mergeProps(() => ({
352
+ ...slotDescriptor(slot).props,
353
+ style: [slotDescriptor(slot).props.style, animatedStyle(slot)],
354
+ get children() {
355
+ return slotChildren(slot);
356
+ },
357
+ })));
358
+ }
359
+ const root = hostElement('symbiote-view', () => ({
360
+ ...rootDescriptor().props,
361
+ ...panResponder.panHandlers,
362
+ }));
363
+ insert(root, order.map(buildSlot));
364
+ return root;
365
+ }
366
+ const drawer = createMemo(() => {
367
+ const current = shape();
368
+ return untrack(() => buildDrawer(current));
369
+ });
370
+ const host = hostElement('symbiote-view', () => ({ style: { flex: 1 } }));
371
+ insert(host, drawer);
372
+ return createComponent(ScreenCollectorProvider, {
373
+ value: collector,
374
+ get children() {
375
+ return [props.children, host];
376
+ },
377
+ });
378
+ }
379
+ export const Drawer = Object.assign(DrawerImpl, { Screen: DrawerScreen });
380
+ // --- Explicit gap list vs the real react-native-gesture-handler + react-native-reanimated
381
+ // @react-navigation/drawer (mirrored verbatim from the React twin - the gaps are architectural,
382
+ // not adapter-specific) ---
383
+ // 1. `configureGestureHandler` - a raw react-native-gesture-handler `Gesture` object escape hatch.
384
+ // No PanResponder equivalent exists; not ported.
385
+ // 2. Simultaneous/failure gesture RELATIONSHIPS (gesture-handler's declarative composition vs a
386
+ // nested ScrollView, another PanResponder, etc.) - PanResponder only offers negotiation via the
387
+ // should-set boolean gates used here (edge-start + dominant-axis), which is more prone to an
388
+ // accidental hijack of a nested horizontal ScrollView/Swiper.
389
+ // 3. `useDrawerProgress` - a Reanimated SharedValue read on the UI thread. `progress` here is a
390
+ // JS-thread AnimatedValue; interpolating it works, but without native-driver wiring it does not
391
+ // carry the same synchronous UI-thread guarantee under JS-thread load.
392
+ // 4. `hideStatusBarOnOpen` / `keyboardDismissMode` / `statusBarAnimation` / `overlayStyle` - not
393
+ // wired in this pass; straightforward additions once StatusBar/Keyboard module wiring is needed
394
+ // here (not a PanResponder/Animated limitation, just unscoped for v1).
@@ -0,0 +1,13 @@
1
+ import type { Component } from 'solid-js';
2
+ import type { IDrawerNavigatorHandle, IDrawerScreenOptions, IRoute } from '../core';
3
+ export type IDrawerScreenOptionsArgs = {
4
+ route: IRoute<unknown>;
5
+ navigation: IDrawerNavigatorHandle;
6
+ };
7
+ export type IDrawerScreenOptionsResolver = (args: IDrawerScreenOptionsArgs) => IDrawerScreenOptions;
8
+ export type IDrawerScreenProps = {
9
+ name: string;
10
+ component: Component;
11
+ options?: IDrawerScreenOptions | IDrawerScreenOptionsResolver;
12
+ initialParams?: unknown;
13
+ };
@@ -0,0 +1,2 @@
1
+ // Drawer.Screen's public prop types. Same split rationale as screen-props.ts.
2
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { ISymbioteNode } from '@symbiote-native/engine';
2
+ export declare function hostElement(type: string, props: () => Record<string, unknown>): ISymbioteNode;