@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
@@ -0,0 +1,205 @@
1
+ <script lang="ts" module>
2
+ import type { INavigatorPlatform } from '../../core';
3
+
4
+ // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
5
+ // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single
6
+ // constant stands in for the per-platform injection point ISliderPlatform-style adapters use
7
+ // elsewhere.
8
+ const NAVIGATOR_PLATFORM: INavigatorPlatform = {
9
+ defaultHeaderBackTitleVisible: true,
10
+ };
11
+
12
+ // react-native-screens' RNSScreenStackHeaderConfig.mm requires every header child to be an
13
+ // RNSScreenStackHeaderSubview; `type: 'searchBar'` is how it knows which slot this one fills.
14
+ const HEADER_SUBVIEW_PROPS: Record<string, unknown> = { type: 'searchBar' };
15
+ </script>
16
+
17
+ <script lang="ts">
18
+ // One mounted route's native chrome. Split out of index.svelte so each route owns its own plan
19
+ // derivation and its own attachments, torn down by an ordinary component unmount when the route
20
+ // is popped - the Svelte equivalent of Vue's per-route render-loop closure.
21
+ //
22
+ // Every react-native-screens view here goes through `<svelte:element this={'RNSScreen'}>`
23
+ // rather than a literal tag: their Fabric names are capitalized and un-hyphenated, so a literal
24
+ // tag would parse as a COMPONENT reference in a Svelte template. Their props ride an
25
+ // `{@attach hostProps(...)}` attachment rather than an attribute, because a dynamic tag
26
+ // compiles through Svelte's generic setAttribute path and never the custom-element property-SET
27
+ // path the object bag depends on - see ../attachments.ts for the full reasoning.
28
+ //
29
+ // Whitespace between sibling tags in the per-route tree is inert: the shim drops a
30
+ // whitespace-only text node whose parent takes no raw text, so it never becomes an RCTRawText
31
+ // child of a react-native-screens view (dom-shim/text.ts, svelte-adapter-dom-shim skill §16b).
32
+ import { Platform, dlog } from '@symbiote-native/engine';
33
+ import {
34
+ NAVIGATION_EVENT_BLUR,
35
+ NAVIGATION_EVENT_FOCUS,
36
+ RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
37
+ RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME,
38
+ RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME,
39
+ RNS_SCREEN_STACK_VIEW_NAME,
40
+ RNS_SCREEN_VIEW_NAME,
41
+ RNS_SEARCH_BAR_VIEW_NAME,
42
+ SCREEN_ON_APPEAR,
43
+ SCREEN_ON_DISAPPEAR,
44
+ SCREEN_ON_DISMISSED,
45
+ SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
46
+ SCREEN_ON_WILL_APPEAR,
47
+ SCREEN_ON_WILL_DISAPPEAR,
48
+ buildSearchBarPassthrough,
49
+ resolveScreenRenderPlan,
50
+ } from '../../core';
51
+ import type { IScreenRenderPlan } from '../../core';
52
+ import { hostProps, searchBarRef } from '../attachments';
53
+ import NavigationScope from '../navigation-scope.svelte';
54
+ import type { INavigationScopeValue } from '../navigation-context';
55
+ import type { IStackScreenProps } from './stack-props';
56
+
57
+ let {
58
+ route,
59
+ index,
60
+ routeCount,
61
+ options,
62
+ navigation,
63
+ emitter,
64
+ parentScope,
65
+ // Destructured under a capitalized name because a Svelte template resolves a component tag
66
+ // only from a capitalized identifier.
67
+ component: ScreenComponent,
68
+ onPopRequested,
69
+ }: IStackScreenProps = $props();
70
+
71
+ const searchBarOptions = $derived(options.headerSearchBarOptions);
72
+
73
+ const plan = $derived.by<IScreenRenderPlan>(() =>
74
+ resolveScreenRenderPlan({
75
+ screenId: route.key,
76
+ index,
77
+ routeCount,
78
+ options,
79
+ platform: NAVIGATOR_PLATFORM,
80
+ isAndroid: Platform.OS === 'android',
81
+ screenPassthrough: {
82
+ [SCREEN_ON_DISMISSED]: onPopRequested,
83
+ [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: onPopRequested,
84
+ // onAppear/onDisappear are the definitive visibility boundary (post-transition-
85
+ // animation), so 'focus'/'blur' fire exactly once per transition; onWillAppear/
86
+ // onWillDisappear fire BEFORE the animation runs, so wiring them to emit() too would
87
+ // double-invoke useFocusEffect per transition - they only get a debug log here.
88
+ [SCREEN_ON_WILL_APPEAR]: () =>
89
+ dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
90
+ [SCREEN_ON_APPEAR]: () => {
91
+ dlog(
92
+ `Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`,
93
+ );
94
+ emitter.emit(NAVIGATION_EVENT_FOCUS);
95
+ },
96
+ [SCREEN_ON_WILL_DISAPPEAR]: () =>
97
+ dlog(
98
+ `Stack: route "${route.name}" will disappear at t=${Date.now()}`,
99
+ ),
100
+ [SCREEN_ON_DISAPPEAR]: () => {
101
+ dlog(
102
+ `Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`,
103
+ );
104
+ emitter.emit(NAVIGATION_EVENT_BLUR);
105
+ },
106
+ },
107
+ // The imperative SearchBarCommands ref rides its OWN attachment on the RNSSearchBar leaf
108
+ // below (../attachments.ts), never this passthrough map - same split Angular uses, so no
109
+ // `ref` key can leak through to Fabric as a real prop.
110
+ searchBarPassthrough: searchBarOptions
111
+ ? buildSearchBarPassthrough(searchBarOptions, message =>
112
+ dlog(`Stack: route "${route.name}" ${message}`),
113
+ )
114
+ : undefined,
115
+ }),
116
+ );
117
+
118
+ // Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
119
+ // values resolved onto the native RNSScreen, once per mounted route - rules a stackAnimation/
120
+ // transitionDuration mismatch against react-native-screens' own native default in or out. Kept
121
+ // behind DEBUG, never removed.
122
+ let hasLoggedScreenProps = false;
123
+ $effect(() => {
124
+ const screenProps = plan.screenProps;
125
+ if (hasLoggedScreenProps) return;
126
+ hasLoggedScreenProps = true;
127
+ dlog(
128
+ `Stack: route "${route.name}" resolved screen props ` +
129
+ `stackAnimation=${String(screenProps.stackAnimation)} ` +
130
+ `stackPresentation=${String(screenProps.stackPresentation)} ` +
131
+ `transitionDuration=${String(screenProps.transitionDuration)} ` +
132
+ `gestureEnabled=${String(screenProps.gestureEnabled)} at t=${Date.now()}`,
133
+ );
134
+ });
135
+
136
+ // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
137
+ // RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's comment
138
+ // in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no navigation
139
+ // controller to attach to, so the header silently never renders. activityState mirrors the
140
+ // outer screen's own value - RNSScreen.mm treats an unset/inactive nested screen as not yet
141
+ // pushed, leaving it parked at its pre-push transition position.
142
+ const innerStackProps = $derived<Record<string, unknown>>({
143
+ style: plan.innerStackStyle,
144
+ });
145
+ const innerScreenProps = $derived<Record<string, unknown>>({
146
+ style: plan.innerScreenStyle,
147
+ activityState: plan.activityState,
148
+ });
149
+
150
+ const scopeValue = $derived<INavigationScopeValue>({
151
+ route,
152
+ navigation,
153
+ emitter,
154
+ parent: parentScope,
155
+ });
156
+ </script>
157
+
158
+ {#snippet chrome()}
159
+ <svelte:element
160
+ this={RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME}
161
+ {@attach hostProps(plan.headerConfig.props)}
162
+ >
163
+ {#if plan.searchBarProps !== undefined}
164
+ <svelte:element
165
+ this={RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME}
166
+ {@attach hostProps(HEADER_SUBVIEW_PROPS)}
167
+ >
168
+ <svelte:element
169
+ this={RNS_SEARCH_BAR_VIEW_NAME}
170
+ {@attach hostProps(plan.searchBarProps)}
171
+ {@attach searchBarRef(searchBarOptions?.ref)}
172
+ />
173
+ </svelte:element>
174
+ {/if}
175
+ </svelte:element>
176
+ <svelte:element
177
+ this={RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME}
178
+ {@attach hostProps(plan.contentWrapperProps)}
179
+ >
180
+ <NavigationScope value={scopeValue}>
181
+ <ScreenComponent />
182
+ </NavigationScope>
183
+ </svelte:element>
184
+ {/snippet}
185
+
186
+ <svelte:element
187
+ this={plan.screenViewName}
188
+ {@attach hostProps(plan.screenProps)}
189
+ >
190
+ {#if plan.inModal}
191
+ <svelte:element
192
+ this={RNS_SCREEN_STACK_VIEW_NAME}
193
+ {@attach hostProps(innerStackProps)}
194
+ >
195
+ <svelte:element
196
+ this={RNS_SCREEN_VIEW_NAME}
197
+ {@attach hostProps(innerScreenProps)}
198
+ >
199
+ {@render chrome()}
200
+ </svelte:element>
201
+ </svelte:element>
202
+ {:else}
203
+ {@render chrome()}
204
+ {/if}
205
+ </svelte:element>
@@ -0,0 +1,6 @@
1
+ // Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
2
+ // program. adapters/svelte gets these for free because render.ts does a real value import from
3
+ // 'svelte' (any file in a program importing a module loads its ambient globals for the whole
4
+ // program); this package has no such import, so a triple-slash reference does the same job
5
+ // explicitly rather than relying on an incidental side-effect.
6
+ /// <reference types="svelte" />
@@ -0,0 +1,147 @@
1
+ // The compile-and-load harness every `*.smoke.test.ts` in this folder uses.
2
+ //
3
+ // There is no `.svelte`-aware bundler wired into this repo's vitest (svelte-adapter-dom-shim
4
+ // skill §15, a deliberate choice), so a test that needs to RUN a component compiles the real
5
+ // source with `svelte/compiler` itself and dynamic-`import()`s the output. Three rules fall out
6
+ // of that, all of them learned the hard way in the adapter's own smokes:
7
+ //
8
+ // 1. The compiled file must sit NEXT TO the real source, because its own relative imports
9
+ // (`./stack-screen.svelte`, `../navigation-context`) resolve from wherever the compiled FILE
10
+ // lives, not from where the source did.
11
+ // 2. Every `.svelte` specifier inside the compiled output has to be rewritten to the compiled
12
+ // twin, recursively - a navigator pulls in stack-screen.svelte, which pulls in
13
+ // navigation-scope.svelte.
14
+ // 3. Node's `import()` caches by path, so re-writing new content to a path a previous test
15
+ // already imported hands back the STALE module. Every harness instance therefore stamps its
16
+ // own id into the filenames.
17
+ //
18
+ // `aliases` covers the one case relative rewriting cannot: `@symbiote-native/svelte`'s main
19
+ // barrel re-exports real `.svelte` sources, which Vite's plain (svelte-plugin-free) transform
20
+ // cannot parse at all - so a test that needs something from it points the specifier at a module
21
+ // it compiled itself instead.
22
+
23
+ import { compile } from 'svelte/compiler';
24
+ import { basename, dirname, join, relative, resolve } from 'node:path';
25
+ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
26
+ import { pathToFileURL } from 'node:url';
27
+ // The real Metro pipeline's own `.svelte.ts` compile step (TS-strip, then `compileModule` to
28
+ // desugar the runes). Reused rather than reimplemented, so the runes in ./runes are exercised
29
+ // through the ACTUAL shipped compile path - a bare, uncompiled `$state`/`$effect` call throws
30
+ // `rune_outside_svelte` at runtime. Default-imported because it is a `.cjs` module.
31
+ import metroSvelteTransformer from '@symbiote-native/svelte/metro-svelte-transformer';
32
+
33
+ const {
34
+ compileSvelteModuleFile,
35
+ }: {
36
+ compileSvelteModuleFile: (source: string, filename: string) => string;
37
+ } = metroSvelteTransformer;
38
+
39
+ const COMPILE_OPTIONS = {
40
+ generate: 'client',
41
+ fragments: 'tree',
42
+ css: 'external',
43
+ } as const;
44
+ const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
45
+
46
+ export type ISvelteHarness = {
47
+ // Compile a real `.svelte` file plus everything it imports; returns the compiled entry path.
48
+ compileFile(sveltePath: string): string;
49
+ // Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
50
+ compileSource(dir: string, name: string, source: string): string;
51
+ cleanup(): void;
52
+ };
53
+
54
+ export function createSvelteHarness(
55
+ id: string,
56
+ aliases: Readonly<Record<string, string>> = {},
57
+ ): ISvelteHarness {
58
+ const written: string[] = [];
59
+ const compiledBySource = new Map<string, string>();
60
+
61
+ function outPathFor(sveltePath: string): string {
62
+ const stem = basename(sveltePath).replace(/\./g, '-');
63
+ return join(dirname(sveltePath), `.smoke-compiled-${id}-${stem}.mjs`);
64
+ }
65
+
66
+ // `./` is prepended unless the path already walks up: every compiled file's basename STARTS
67
+ // with a dot (`.smoke-compiled-...`), so a bare "starts with '.'" check would leave it looking
68
+ // like a bare package specifier to the resolver.
69
+ function relativeSpecifier(fromFile: string, target: string): string {
70
+ const specifier = relative(dirname(fromFile), target);
71
+ return specifier.startsWith('..') ? specifier : `./${specifier}`;
72
+ }
73
+
74
+ function rewrite(code: string, outPath: string): string {
75
+ let rewritten = code.replace(
76
+ SVELTE_SPECIFIER_PATTERN,
77
+ (_match, quote: string, spec: string) => {
78
+ const childSource = resolve(dirname(outPath), spec);
79
+ const childOut = compileFile(childSource);
80
+ return `${quote}${relativeSpecifier(outPath, childOut)}${quote}`;
81
+ },
82
+ );
83
+ for (const [specifier, target] of Object.entries(aliases)) {
84
+ rewritten = rewritten
85
+ .split(`'${specifier}'`)
86
+ .join(`'${relativeSpecifier(outPath, target)}'`);
87
+ rewritten = rewritten
88
+ .split(`"${specifier}"`)
89
+ .join(`"${relativeSpecifier(outPath, target)}"`);
90
+ }
91
+ return rewritten;
92
+ }
93
+
94
+ function emit(
95
+ sveltePath: string,
96
+ source: string,
97
+ isRuneModule: boolean,
98
+ ): string {
99
+ const outPath = outPathFor(sveltePath);
100
+ // Registered BEFORE compiling so a cycle between two components terminates.
101
+ compiledBySource.set(sveltePath, outPath);
102
+ written.push(outPath);
103
+ const filename = basename(sveltePath);
104
+ const code = isRuneModule
105
+ ? compileSvelteModuleFile(source, `${filename}.ts`)
106
+ : compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
107
+ writeFileSync(outPath, rewrite(code, outPath));
108
+ return outPath;
109
+ }
110
+
111
+ // A `./x.svelte` specifier is a COMPONENT when `x.svelte` exists on disk and a rune MODULE
112
+ // when `x.svelte.ts` does - the same two-way split the Metro transformer makes on the real
113
+ // filename.
114
+ function compileFile(sveltePath: string): string {
115
+ const cached = compiledBySource.get(sveltePath);
116
+ if (cached !== undefined) return cached;
117
+ const runeModulePath = `${sveltePath}.ts`;
118
+ if (!existsSync(sveltePath) && existsSync(runeModulePath)) {
119
+ return emit(sveltePath, readFileSync(runeModulePath, 'utf8'), true);
120
+ }
121
+ return emit(sveltePath, readFileSync(sveltePath, 'utf8'), false);
122
+ }
123
+
124
+ return {
125
+ compileFile,
126
+ compileSource(dir, name, source) {
127
+ return emit(join(dir, `${name}.svelte`), source, false);
128
+ },
129
+ cleanup() {
130
+ for (const file of written) rmSync(file, { force: true });
131
+ written.length = 0;
132
+ compiledBySource.clear();
133
+ },
134
+ };
135
+ }
136
+
137
+ // The compiled module's default export, narrowed without a cast so a test never silently mounts
138
+ // `undefined`.
139
+ export async function loadComponent(compiledPath: string): Promise<unknown> {
140
+ // pathToFileURL rather than a `file://${...}` template: the literal form makes Vite's import
141
+ // analyzer log an "Invalid file URL" warning for the un-substituted placeholder.
142
+ const mod: unknown = await import(pathToFileURL(compiledPath).href);
143
+ if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
144
+ throw new Error(`${compiledPath} produced no default export`);
145
+ }
146
+ return mod.default;
147
+ }
@@ -0,0 +1,22 @@
1
+ // Tab.Screen's public prop surface - mirrors screen-props.ts minus the stack-only concepts (no
2
+ // push/pop lifecycle, no header search bar). Split out of `tab-screen.svelte` for the same
3
+ // tsc-cannot-see-a-`.svelte`-named-export reason screen-props.ts documents.
4
+
5
+ import type { Component } from 'svelte';
6
+ import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
7
+
8
+ export type ITabScreenOptionsArgs = {
9
+ route: IRoute<unknown>;
10
+ navigation: ITabNavigatorHandle;
11
+ };
12
+
13
+ export type ITabScreenOptionsResolver = (
14
+ args: ITabScreenOptionsArgs,
15
+ ) => ITabOptions;
16
+
17
+ export type ITabScreenProps = {
18
+ name: string;
19
+ component: Component;
20
+ options?: ITabOptions | ITabScreenOptionsResolver;
21
+ initialParams?: unknown;
22
+ };
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ // Tab.Screen: a declarative marker that paints nothing, registered through the same
3
+ // context-based collector Stack.Screen uses (screen-registry.ts). The `'tab'` kind tag is what
4
+ // stops it from silently registering with an ancestor Stack/Drawer when it is written in the
5
+ // wrong place. See CLAUDE.md <third_party_rn_packages_are_react_only> - this and Tab import
6
+ // nothing from react-native.
7
+ import { collectScreen } from './screen-registry';
8
+ import type { ITabScreenProps } from './tab-screen-props';
9
+
10
+ let { name, component, options, initialParams }: ITabScreenProps = $props();
11
+
12
+ collectScreen('tab', {
13
+ get name(): ITabScreenProps['name'] {
14
+ return name;
15
+ },
16
+ get component(): ITabScreenProps['component'] {
17
+ return component;
18
+ },
19
+ get options(): ITabScreenProps['options'] {
20
+ return options;
21
+ },
22
+ get initialParams(): unknown {
23
+ return initialParams;
24
+ },
25
+ });
26
+ </script>
@@ -0,0 +1,269 @@
1
+ <script lang="ts" module>
2
+ // Tab, the Svelte lifecycle half. The focused-index router (tab-router-state) and the tab-bar
3
+ // Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
4
+ // with the React/Vue/Angular entries; here Svelte supplies the lifecycle - `$state.raw` for the
5
+ // router (its twin of useReducer), a module counter for route-key generation, `export
6
+ // function`s for the jumpTo/setParams handle - plus the Descriptor bridge for the tab-bar leaf.
7
+ // Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary `symbiote-view`/
8
+ // `symbiote-text` primitives via the shared render fn, so there is no react-native-screens
9
+ // ViewConfig to register here - Tab needs no `../../register` import.
10
+ //
11
+ // Screens are discovered through the context collector (../screen-registry.ts), not by reading
12
+ // `children` - see stack/index.svelte's header for why, and ../registry-host.ts for where the
13
+ // markers are rendered.
14
+ let tabInstanceCounter = 0;
15
+
16
+ const TAB_ROOT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
17
+ const TAB_CONTENT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import type { Component } from 'svelte';
22
+ import { onDestroy, tick } from 'svelte';
23
+ import { dlog } from '@symbiote-native/engine';
24
+ import type { ShimElement } from '@symbiote-native/svelte/native-view-bridge';
25
+ import {
26
+ NAVIGATION_EVENT_BLUR,
27
+ NAVIGATION_EVENT_FOCUS,
28
+ createInitialTabState,
29
+ createNavigationEmitter,
30
+ diffFocusedRoute,
31
+ isFocusedRoute,
32
+ reconcileTabRoutes,
33
+ renderTabBar,
34
+ tabRouterReducer,
35
+ } from '../../core';
36
+ import type {
37
+ IDescriptor,
38
+ INavigationEmitter,
39
+ IRoute,
40
+ ITabBarItemView,
41
+ ITabNavigatorHandle,
42
+ ITabOptions,
43
+ ITabRouterAction,
44
+ ITabRouterState,
45
+ } from '../../core';
46
+ import { createDescriptorSubtreeSync } from '../descriptor-subtree';
47
+ import { getNavigationScope } from '../navigation-context';
48
+ import type { INavigationScopeValue } from '../navigation-context';
49
+ import NavigationScope from '../navigation-scope.svelte';
50
+ import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
51
+ import {
52
+ setScreenCollector,
53
+ toRegistry,
54
+ withoutScreen,
55
+ } from '../screen-registry';
56
+ import type { IRegisteredScreen } from '../screen-registry';
57
+ import type { ITabScreenProps } from '../tab-screen-props';
58
+ import type { ITabProps } from './tab-props';
59
+
60
+ let { initialRouteName, screenOptions, children }: ITabProps = $props();
61
+
62
+ // Read BEFORE this Tab establishes its own per-screen NavigationScope - becomes the `parent`
63
+ // link a nested screen's useNavigation().getParent() walks. undefined at the nesting root.
64
+ const parentScope = getNavigationScope();
65
+
66
+ const routeIdPrefix = `tab-${(tabInstanceCounter += 1)}`;
67
+
68
+ let screens = $state.raw<IRegisteredScreen<ITabScreenProps['options']>[]>([]);
69
+ setScreenCollector<ITabScreenProps['options']>({
70
+ kind: 'tab',
71
+ register: screen => {
72
+ screens = [...screens, screen];
73
+ },
74
+ unregister: screen => {
75
+ screens = withoutScreen(screens, screen);
76
+ },
77
+ });
78
+
79
+ const registry = $derived(toRegistry(screens));
80
+
81
+ // A tab's route list is a PROJECTION of the registry, not navigation history the way Stack's
82
+ // is: a marker registering or unregistering must add or drop its tab. Unlike Stack, a route key
83
+ // here is derived from the route NAME rather than an incrementing sequence, so re-deriving this
84
+ // list on every registry change re-keys nothing.
85
+ const registeredRoutes = $derived.by<IRoute<unknown>[]>(() =>
86
+ [...registry.entries()].map(([name, entry]) => ({
87
+ key: `${routeIdPrefix}-${name}`,
88
+ name,
89
+ params: entry.initialParams,
90
+ })),
91
+ );
92
+
93
+ // The dispatched half is the only STATE here; the registry half is re-derived and reconciled
94
+ // against it on every change (reconcileTabRoutes, core - it is what preserves each surviving
95
+ // route's key/params and keeps the focus on the same route NAME). Nothing needs memoizing
96
+ // inside the derivation, so there is no state write during a derivation to avoid in the first
97
+ // place. Until something dispatches there is nothing to reconcile against, hence the seed.
98
+ let dispatchedState = $state.raw<ITabRouterState | null>(null);
99
+
100
+ const state = $derived.by<ITabRouterState>(() => {
101
+ if (registeredRoutes.length === 0)
102
+ dlog('Tab: no <Tab.Screen> children registered');
103
+ const dispatched = dispatchedState;
104
+ return dispatched === null
105
+ ? createInitialTabState(registeredRoutes, initialRouteName)
106
+ : reconcileTabRoutes(dispatched, registeredRoutes);
107
+ });
108
+
109
+ function dispatch(action: ITabRouterAction): void {
110
+ dispatchedState = tabRouterReducer(state, action);
111
+ }
112
+
113
+ export function jumpTo(name: string, params?: unknown): void {
114
+ dispatch({ type: 'jumpTo', name, params });
115
+ }
116
+ export function setParams(params: unknown, key: string): void {
117
+ dispatch({ type: 'setParams', key, params });
118
+ }
119
+
120
+ const handle: ITabNavigatorHandle = { jumpTo, setParams };
121
+
122
+ // One emitter per route.key, created lazily and cached for the navigator's whole lifetime -
123
+ // mirrors Stack's own `emitters` map. This decouples emitter IDENTITY (stable, looked up by
124
+ // key, read below when a route's NavigationScope is built) from emit TIMING (must wait until
125
+ // the focused screen has actually mounted and subscribed).
126
+ const emitters = new Map<string, INavigationEmitter>();
127
+ function emitterFor(routeKey: string): INavigationEmitter {
128
+ let emitter = emitters.get(routeKey);
129
+ if (emitter === undefined) {
130
+ emitter = createNavigationEmitter();
131
+ emitters.set(routeKey, emitter);
132
+ }
133
+ return emitter;
134
+ }
135
+
136
+ // Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
137
+ // Stack's RNSScreen), so focus/blur is synthesized here. Keyed on the route KEY (not the route
138
+ // object) so a setParams-only change doesn't spuriously re-fire focus/blur.
139
+ //
140
+ // The bookkeeping updates immediately, but the actual emit is deferred to `tick()` - Svelte's
141
+ // twin of Vue's nextTick, and needed for the same reason: the newly-focused screen's own
142
+ // `$effect`s (its useIsFocused/useFocusEffect subscriptions) are created during THIS flush,
143
+ // after this effect was, so emitting synchronously here would reach zero subscribers.
144
+ let previousFocusedKey: string | undefined;
145
+ $effect(() => {
146
+ const nextKey = state.routes[state.index]?.key;
147
+ const { blurKey, focusKey } = diffFocusedRoute(previousFocusedKey, nextKey);
148
+ if (blurKey === undefined && focusKey === undefined) return;
149
+ previousFocusedKey = nextKey;
150
+ void tick().then(() => {
151
+ if (blurKey !== undefined) {
152
+ dlog(`Tab: route "${blurKey}" blurred at t=${Date.now()}`);
153
+ emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
154
+ }
155
+ if (focusKey !== undefined) {
156
+ dlog(`Tab: route "${focusKey}" focused at t=${Date.now()}`);
157
+ emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
158
+ }
159
+ });
160
+ });
161
+
162
+ onDestroy(() => {
163
+ if (previousFocusedKey !== undefined) {
164
+ emitterFor(previousFocusedKey).emit(NAVIGATION_EVENT_BLUR);
165
+ }
166
+ });
167
+
168
+ function resolveTabOptions(
169
+ entry: IRegisteredScreen<ITabScreenProps['options']> | undefined,
170
+ route: IRoute<unknown>,
171
+ ): ITabOptions {
172
+ const own =
173
+ entry === undefined
174
+ ? undefined
175
+ : typeof entry.options === 'function'
176
+ ? entry.options({ route, navigation: handle })
177
+ : entry.options;
178
+ return { ...screenOptions, ...own };
179
+ }
180
+
181
+ const items = $derived.by<ITabBarItemView[]>(() =>
182
+ state.routes.map((route, index) => {
183
+ const entry = registry.get(route.name);
184
+ const focused = isFocusedRoute(index, state.index);
185
+ if (entry === undefined) {
186
+ dlog(`Tab: no screen registered for route name "${route.name}"`);
187
+ return { key: route.key, focused, label: route.name, passthrough: {} };
188
+ }
189
+ const options = resolveTabOptions(entry, route);
190
+ return {
191
+ key: route.key,
192
+ focused,
193
+ label: options.tabBarLabel ?? options.title ?? route.name,
194
+ icon: options.tabBarIcon,
195
+ badge: options.tabBarBadge,
196
+ activeTintColor: options.tabBarActiveTintColor,
197
+ inactiveTintColor: options.tabBarInactiveTintColor,
198
+ passthrough: {
199
+ onPress: () => jumpTo(route.name),
200
+ accessibilityRole: 'tab',
201
+ accessibilityState: { selected: focused },
202
+ },
203
+ };
204
+ }),
205
+ );
206
+
207
+ const focusedRoute = $derived(state.routes[state.index]);
208
+ const focusedOptions = $derived(
209
+ focusedRoute === undefined
210
+ ? screenOptions
211
+ : resolveTabOptions(registry.get(focusedRoute.name), focusedRoute),
212
+ );
213
+
214
+ const tabBar = $derived.by<IDescriptor>(() =>
215
+ renderTabBar({
216
+ items,
217
+ style: focusedOptions?.tabBarStyle,
218
+ passthrough: {},
219
+ }),
220
+ );
221
+
222
+ // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
223
+ // alive), so a fresh NavigationScope per focus change is sufficient - the previous screen's
224
+ // whole subtree (and any listeners it registered) is torn down by an ordinary unmount when
225
+ // focus moves on.
226
+ const focusedScreen = $derived.by<
227
+ { scope: INavigationScopeValue; component: Component } | undefined
228
+ >(() => {
229
+ if (focusedRoute === undefined) return undefined;
230
+ const entry = registry.get(focusedRoute.name);
231
+ if (entry === undefined) return undefined;
232
+ return {
233
+ component: entry.component,
234
+ scope: {
235
+ route: focusedRoute,
236
+ navigation: handle,
237
+ emitter: emitterFor(focusedRoute.key),
238
+ parent: parentScope?.current,
239
+ },
240
+ };
241
+ });
242
+
243
+ // The bar's ROOT stays a literal template tag (so `bind:this` has a statically known tag) and
244
+ // only its children go through the Descriptor bridge - the uniform shape every Svelte component
245
+ // consuming a `render-*.ts` uses (svelte-adapter-dom-shim skill §19). The item count varies with
246
+ // the registry, hence the shape-change-tolerant wrapper rather than the raw bridge.
247
+ let tabBarHost = $state.raw<ShimElement | null>(null);
248
+ const syncTabBarChildren = createDescriptorSubtreeSync();
249
+ $effect(() => {
250
+ const host = tabBarHost;
251
+ const barChildren = tabBar.children;
252
+ syncTabBarChildren(host, barChildren);
253
+ });
254
+ </script>
255
+
256
+ <symbiote-view p={TAB_ROOT_PROPS}>
257
+ <symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>
258
+ {@render children?.()}
259
+ </symbiote-text>
260
+ <symbiote-view p={TAB_CONTENT_PROPS}>
261
+ {#if focusedScreen !== undefined}
262
+ {@const FocusedComponent = focusedScreen.component}
263
+ <NavigationScope value={focusedScreen.scope}>
264
+ <FocusedComponent />
265
+ </NavigationScope>
266
+ {/if}
267
+ </symbiote-view>
268
+ <symbiote-view p={tabBar.props} bind:this={tabBarHost} />
269
+ </symbiote-view>