@symbiote-native/navigation 2.1.0 → 3.0.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 (237) hide show
  1. package/README.md +89 -14
  2. package/build/angular/drawer/index.js +23 -9
  3. package/build/angular/index.d.ts +4 -4
  4. package/build/angular/index.js +1 -1
  5. package/build/angular/injectors/inject-navigation.js +1 -1
  6. package/build/angular/navigation-scope.directive.js +1 -1
  7. package/build/angular/search-bar-ref.directive.js +1 -1
  8. package/build/angular/stack/index.js +20 -6
  9. package/build/angular/tabs/index.js +21 -7
  10. package/build/core/drawer-options/index.js +19 -5
  11. package/build/core/drawer-router-state/index.js +8 -2
  12. package/build/core/guards.js +3 -1
  13. package/build/core/header-bar-buttons.js +14 -4
  14. package/build/core/index.d.ts +4 -4
  15. package/build/core/index.js +2 -2
  16. package/build/core/navigator-state/index.js +12 -4
  17. package/build/core/render-drawer.js +4 -1
  18. package/build/core/render-stack.js +13 -4
  19. package/build/core/screen-options.js +1 -1
  20. package/build/core/search-bar-commands.js +1 -1
  21. package/build/core/state-persistence/index.js +6 -2
  22. package/build/core/tab-router-state/index.js +8 -2
  23. package/build/react/drawer/index.js +36 -13
  24. package/build/react/index.d.ts +7 -7
  25. package/build/react/index.js +3 -3
  26. package/build/react/stack/index.js +34 -8
  27. package/build/react/tabs/index.js +14 -4
  28. package/build/solid/drawer/index.d.ts +22 -0
  29. package/build/solid/drawer/index.js +394 -0
  30. package/build/solid/drawer-screen-props.d.ts +13 -0
  31. package/build/solid/drawer-screen-props.js +2 -0
  32. package/build/solid/host.d.ts +2 -0
  33. package/build/solid/host.js +23 -0
  34. package/build/solid/index.d.ts +19 -0
  35. package/build/solid/index.js +24 -0
  36. package/build/solid/linking.d.ts +3 -0
  37. package/build/solid/linking.js +55 -0
  38. package/build/solid/navigation-context.d.ts +13 -0
  39. package/build/solid/navigation-context.js +37 -0
  40. package/build/solid/primitives/create-focus-effect.d.ts +1 -0
  41. package/build/solid/primitives/create-focus-effect.js +30 -0
  42. package/build/solid/primitives/create-is-focused.d.ts +2 -0
  43. package/build/solid/primitives/create-is-focused.js +31 -0
  44. package/build/solid/primitives/create-navigation-state.d.ts +3 -0
  45. package/build/solid/primitives/create-navigation-state.js +30 -0
  46. package/build/solid/primitives/index.d.ts +12 -0
  47. package/build/solid/primitives/index.js +8 -0
  48. package/build/solid/primitives/use-drawer-navigation.d.ts +5 -0
  49. package/build/solid/primitives/use-drawer-navigation.js +15 -0
  50. package/build/solid/primitives/use-navigation.d.ts +8 -0
  51. package/build/solid/primitives/use-navigation.js +23 -0
  52. package/build/solid/primitives/use-route.d.ts +3 -0
  53. package/build/solid/primitives/use-route.js +14 -0
  54. package/build/solid/primitives/use-stack-navigation.d.ts +5 -0
  55. package/build/solid/primitives/use-stack-navigation.js +19 -0
  56. package/build/solid/primitives/use-tab-navigation.d.ts +5 -0
  57. package/build/solid/primitives/use-tab-navigation.js +15 -0
  58. package/build/solid/screen-props.d.ts +19 -0
  59. package/build/solid/screen-props.js +9 -0
  60. package/build/solid/screen-registry.d.ts +31 -0
  61. package/build/solid/screen-registry.js +86 -0
  62. package/build/solid/screen.d.ts +6 -0
  63. package/build/solid/screen.js +69 -0
  64. package/build/solid/stack/index.d.ts +15 -0
  65. package/build/solid/stack/index.js +348 -0
  66. package/build/solid/tab-screen-props.d.ts +13 -0
  67. package/build/solid/tab-screen-props.js +3 -0
  68. package/build/solid/tabs/index.d.ts +14 -0
  69. package/build/solid/tabs/index.js +228 -0
  70. package/build/svelte/descriptor-subtree.js +3 -1
  71. package/build/svelte/drawer/index.svelte +163 -25
  72. package/build/svelte/drawer-screen.svelte +2 -1
  73. package/build/svelte/index.d.ts +3 -3
  74. package/build/svelte/index.js +1 -1
  75. package/build/svelte/navigation-scope.svelte +4 -1
  76. package/build/svelte/runes/use-navigation.svelte.js +5 -1
  77. package/build/svelte/stack/index.svelte +43 -6
  78. package/build/svelte/stack/stack-screen.svelte +66 -9
  79. package/build/svelte/tabs/index.svelte +42 -20
  80. package/build/vue/composables/use-navigation.js +5 -1
  81. package/build/vue/drawer/index.js +30 -8
  82. package/build/vue/index.d.ts +3 -3
  83. package/build/vue/index.js +1 -1
  84. package/build/vue/navigation-context.js +1 -1
  85. package/build/vue/stack/index.js +17 -6
  86. package/build/vue/tabs/index.js +18 -5
  87. package/build-ngc/angular/drawer/index.js +31 -11
  88. package/build-ngc/angular/drawer/index.js.map +1 -1
  89. package/build-ngc/angular/index.d.ts +4 -4
  90. package/build-ngc/angular/index.js.map +1 -1
  91. package/build-ngc/angular/injectors/inject-focus-effect.js.map +1 -1
  92. package/build-ngc/angular/injectors/inject-is-focused.js.map +1 -1
  93. package/build-ngc/angular/injectors/inject-navigation-state.js.map +1 -1
  94. package/build-ngc/angular/injectors/inject-navigation.js +1 -1
  95. package/build-ngc/angular/injectors/inject-navigation.js.map +1 -1
  96. package/build-ngc/angular/linking.js.map +1 -1
  97. package/build-ngc/angular/navigation-context.service.js.map +1 -1
  98. package/build-ngc/angular/navigation-scope.directive.js +1 -1
  99. package/build-ngc/angular/navigation-scope.directive.js.map +1 -1
  100. package/build-ngc/angular/screen.directive.js.map +1 -1
  101. package/build-ngc/angular/search-bar-ref.directive.js +1 -1
  102. package/build-ngc/angular/search-bar-ref.directive.js.map +1 -1
  103. package/build-ngc/angular/stack/index.js +26 -8
  104. package/build-ngc/angular/stack/index.js.map +1 -1
  105. package/build-ngc/angular/tab-screen.directive.js.map +1 -1
  106. package/build-ngc/angular/tabs/index.js +21 -7
  107. package/build-ngc/angular/tabs/index.js.map +1 -1
  108. package/build-ngc/core/constants.js.map +1 -1
  109. package/build-ngc/core/drawer-options/index.js +19 -5
  110. package/build-ngc/core/drawer-options/index.js.map +1 -1
  111. package/build-ngc/core/drawer-router-state/index.js +8 -2
  112. package/build-ngc/core/drawer-router-state/index.js.map +1 -1
  113. package/build-ngc/core/guards.js +3 -1
  114. package/build-ngc/core/guards.js.map +1 -1
  115. package/build-ngc/core/header-bar-buttons.js +14 -4
  116. package/build-ngc/core/header-bar-buttons.js.map +1 -1
  117. package/build-ngc/core/index.d.ts +4 -4
  118. package/build-ngc/core/index.js +2 -2
  119. package/build-ngc/core/index.js.map +1 -1
  120. package/build-ngc/core/linking-config/index.js.map +1 -1
  121. package/build-ngc/core/navigation-events/index.js.map +1 -1
  122. package/build-ngc/core/navigator-handles/index.js.map +1 -1
  123. package/build-ngc/core/navigator-state/index.js +12 -4
  124. package/build-ngc/core/navigator-state/index.js.map +1 -1
  125. package/build-ngc/core/render-drawer.js +4 -1
  126. package/build-ngc/core/render-drawer.js.map +1 -1
  127. package/build-ngc/core/render-stack.js +13 -4
  128. package/build-ngc/core/render-stack.js.map +1 -1
  129. package/build-ngc/core/render-tabs.js.map +1 -1
  130. package/build-ngc/core/screen-options.js +1 -1
  131. package/build-ngc/core/screen-options.js.map +1 -1
  132. package/build-ngc/core/search-bar-commands.js +1 -1
  133. package/build-ngc/core/search-bar-commands.js.map +1 -1
  134. package/build-ngc/core/stack-router-state/index.js.map +1 -1
  135. package/build-ngc/core/state-persistence/index.js +6 -2
  136. package/build-ngc/core/state-persistence/index.js.map +1 -1
  137. package/build-ngc/core/tab-router-state/index.js +8 -2
  138. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  139. package/package.json +40 -15
  140. package/src/angular/drawer/index.ts +69 -25
  141. package/src/angular/index.ts +20 -4
  142. package/src/angular/injectors/inject-focus-effect.ts +8 -2
  143. package/src/angular/injectors/inject-is-focused.ts +6 -4
  144. package/src/angular/injectors/inject-navigation-state.ts +7 -4
  145. package/src/angular/injectors/inject-navigation.ts +12 -3
  146. package/src/angular/linking.ts +4 -1
  147. package/src/angular/navigation-context.service.ts +5 -2
  148. package/src/angular/navigation-scope.directive.ts +6 -2
  149. package/src/angular/screen.directive.ts +13 -3
  150. package/src/angular/search-bar-ref.directive.ts +10 -2
  151. package/src/angular/stack/index.ts +68 -22
  152. package/src/angular/tab-screen.directive.ts +3 -1
  153. package/src/angular/tabs/index.ts +36 -12
  154. package/src/core/constants.ts +6 -3
  155. package/src/core/drawer-options/index.ts +40 -13
  156. package/src/core/drawer-router-state/index.ts +11 -3
  157. package/src/core/guards.ts +3 -1
  158. package/src/core/header-bar-buttons.ts +41 -12
  159. package/src/core/index.ts +18 -4
  160. package/src/core/linking-config/index.ts +31 -10
  161. package/src/core/navigation-events/index.ts +8 -2
  162. package/src/core/navigator-handles/index.ts +8 -3
  163. package/src/core/navigator-props.ts +13 -4
  164. package/src/core/navigator-state/index.ts +24 -7
  165. package/src/core/render-drawer.ts +24 -9
  166. package/src/core/render-stack.ts +52 -13
  167. package/src/core/render-tabs.ts +24 -6
  168. package/src/core/screen-options.ts +15 -5
  169. package/src/core/search-bar-commands.ts +12 -3
  170. package/src/core/stack-router-state/index.ts +5 -2
  171. package/src/core/state-persistence/index.ts +13 -3
  172. package/src/core/tab-router-state/index.ts +10 -3
  173. package/src/react/drawer/index.ts +408 -344
  174. package/src/react/drawer-screen.ts +8 -2
  175. package/src/react/hooks/use-focus-effect.ts +8 -2
  176. package/src/react/hooks/use-is-focused.ts +6 -2
  177. package/src/react/hooks/use-navigation.ts +8 -2
  178. package/src/react/index.ts +37 -7
  179. package/src/react/linking.ts +4 -1
  180. package/src/react/navigation-context.ts +6 -2
  181. package/src/react/screen.ts +12 -3
  182. package/src/react/stack/index.ts +294 -231
  183. package/src/react/tab-screen.ts +3 -1
  184. package/src/react/tabs/index.ts +188 -158
  185. package/src/solid/drawer/index.ts +570 -0
  186. package/src/solid/drawer-screen-props.ts +24 -0
  187. package/src/solid/host.ts +29 -0
  188. package/src/solid/index.ts +87 -0
  189. package/src/solid/linking.ts +74 -0
  190. package/src/solid/navigation-context.ts +56 -0
  191. package/src/solid/primitives/create-focus-effect.ts +41 -0
  192. package/src/solid/primitives/create-is-focused.ts +41 -0
  193. package/src/solid/primitives/create-navigation-state.ts +44 -0
  194. package/src/solid/primitives/index.ts +12 -0
  195. package/src/solid/primitives/use-drawer-navigation.ts +25 -0
  196. package/src/solid/primitives/use-navigation.ts +44 -0
  197. package/src/solid/primitives/use-route.ts +18 -0
  198. package/src/solid/primitives/use-stack-navigation.ts +29 -0
  199. package/src/solid/primitives/use-tab-navigation.ts +25 -0
  200. package/src/solid/screen-props.ts +51 -0
  201. package/src/solid/screen-registry.ts +159 -0
  202. package/src/solid/screen.ts +80 -0
  203. package/src/solid/stack/index.ts +528 -0
  204. package/src/solid/tab-screen-props.ts +21 -0
  205. package/src/solid/tabs/index.ts +327 -0
  206. package/src/svelte/attachments.ts +3 -1
  207. package/src/svelte/descriptor-subtree.ts +3 -1
  208. package/src/svelte/drawer/index.svelte +163 -25
  209. package/src/svelte/drawer-screen-props.ts +8 -2
  210. package/src/svelte/drawer-screen.svelte +2 -1
  211. package/src/svelte/fabric-tree.test-helper.ts +37 -3
  212. package/src/svelte/index.ts +15 -3
  213. package/src/svelte/linking.svelte.ts +11 -5
  214. package/src/svelte/navigation-scope.svelte +4 -1
  215. package/src/svelte/runes/use-drawer-navigation.svelte.ts +3 -1
  216. package/src/svelte/runes/use-focus-effect.svelte.ts +8 -2
  217. package/src/svelte/runes/use-navigation-state.svelte.ts +3 -1
  218. package/src/svelte/runes/use-navigation.svelte.ts +13 -3
  219. package/src/svelte/runes/use-stack-navigation.svelte.ts +3 -1
  220. package/src/svelte/screen-props.ts +7 -2
  221. package/src/svelte/screen-registry.ts +9 -3
  222. package/src/svelte/stack/index.svelte +43 -6
  223. package/src/svelte/stack/stack-screen.svelte +66 -9
  224. package/src/svelte/svelte-compile.test-helper.ts +16 -12
  225. package/src/svelte/tab-screen-props.ts +3 -1
  226. package/src/svelte/tabs/index.svelte +42 -20
  227. package/src/vue/composables/use-navigation-state.ts +7 -4
  228. package/src/vue/composables/use-navigation.ts +13 -3
  229. package/src/vue/drawer/index.ts +122 -36
  230. package/src/vue/drawer-screen.ts +12 -5
  231. package/src/vue/index.ts +15 -3
  232. package/src/vue/linking.ts +7 -2
  233. package/src/vue/navigation-context.ts +17 -5
  234. package/src/vue/screen.ts +7 -2
  235. package/src/vue/stack/index.ts +82 -34
  236. package/src/vue/tab-screen.ts +3 -1
  237. package/src/vue/tabs/index.ts +51 -13
@@ -0,0 +1,348 @@
1
+ // Stack, the Solid lifecycle half. The route-stack transitions (navigator-state) and the
2
+ // options/props folds (screen-options, render-stack) live in @symbiote-native/navigation core,
3
+ // shared verbatim with the React/Vue/Svelte/Angular adapters; here Solid supplies the lifecycle -
4
+ // a signal for the pushed-route stack, a route-key counter, a callback `ref` for the push/pop/
5
+ // replace handle - plus the descriptor bridge for the header config leaf. Pushing/popping a route
6
+ // is an ordinary child insert/remove: RNSScreenStack diffs its RNSScreen children natively, so no
7
+ // imperative native command is needed here. Neither this nor the Screen marker imports
8
+ // react-native-screens' own React components (they are React internally and crash a non-React
9
+ // adapter); the native views are driven through the ViewConfig ../../register registers. See
10
+ // CLAUDE.md <third_party_rn_packages_are_react_only>.
11
+ //
12
+ // FOUR THINGS DIFFER FROM EVERY OTHER ADAPTER, and each is a place a naive port freezes:
13
+ //
14
+ // 1. THE REGISTRY IS EMPTY WHEN THIS BODY RUNS. Solid cannot inspect children (screen-registry.ts),
15
+ // and the markers only register once the collector Provider evaluates them - after the body has
16
+ // returned. So nothing is computed eagerly from the registry: the initial state is seeded lazily
17
+ // by `seed()` the first time a non-empty registry is observed.
18
+ //
19
+ // 2. ROUTES ARE KEYED BY `route.key`, NOT BY THE ROUTE OBJECT. `setParams` produces a NEW route
20
+ // object under the same key. A `<For each={routes()}>` keys on object identity and would tear a
21
+ // screen's whole subtree down and rebuild it on every setParams - losing native-owned state and
22
+ // every ref. Iterating the KEYS (a memo with an element-wise `equals`) makes a params change
23
+ // invisible to the list and visible only to the leaf accessors that read it.
24
+ //
25
+ // 3. THE SCREEN IS BUILT BEHIND AN EXPLICIT REBUILD BOUNDARY. Three things legitimately change a
26
+ // route's native SHAPE rather than its props: `screenViewName` (RN treats RNSModalScreen as a
27
+ // different view from RNSScreen), `inModal` (a modal nests an inner stack purely to host the
28
+ // header - isHeaderInModal in core/render-stack.ts), and whether the header config carries a
29
+ // search-bar child. Each is read in a `createMemo` over those discriminators ALONE with the
30
+ // build `untrack`ed - the shape .claude/rules/solid-descriptor-bridge.md §5 prescribes. Without
31
+ // the untrack the memo subscribes to every prop the build reads and re-creates the native screen
32
+ // on each header-title change. The screen COMPONENT is its own, narrower boundary inside that.
33
+ //
34
+ // 4. FOCUS/STATE EMISSION IS DEFERRED BY A MICROTASK. A screen's subtree - and the subscriptions
35
+ // createIsFocused/createNavigationState set up inside it - is built by the render effects of the
36
+ // very update that changed the state, so an inline emit would reach zero subscribers.
37
+ import { For, createEffect, createMemo, createSignal, onCleanup, untrack, } from 'solid-js';
38
+ import { descriptorToSolid } from '@symbiote-native/solid';
39
+ // `createComponent` comes from solid-js, not from the renderer: they are the SAME runtime call
40
+ // (untrack + invoke), but the renderer's is typed to return a host node, which a Solid component
41
+ // legitimately does not have to (a screen may render nothing). `insert`/`insertNode` are the
42
+ // renderer's own, since they mutate the engine tree.
43
+ import { createComponent } from 'solid-js';
44
+ import { insert, insertNode } from '@symbiote-native/solid/renderer';
45
+ import { Platform, debugNodeId, dlog, isSymbioteNode, } from '@symbiote-native/engine';
46
+ 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, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, reconcileStackRoutes, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
47
+ import { hostElement } from '../host.js';
48
+ import { NavigationScopeProvider, useNavigationScope, } from '../navigation-context.js';
49
+ import { ScreenCollectorProvider, createScreenSignal, toRegistry, } from '../screen-registry.js';
50
+ import { Screen } from '../screen.js';
51
+ // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
52
+ // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope.
53
+ const NAVIGATOR_PLATFORM = {
54
+ defaultHeaderBackTitleVisible: true,
55
+ };
56
+ const EMPTY_STATE = { routes: [] };
57
+ // Route keys must be unique per navigator INSTANCE. React/Vue take that from useId(); Solid has no
58
+ // equivalent, so a module counter stands in - read once, at construction.
59
+ let navigatorSequence = 0;
60
+ function sameShape(a, b) {
61
+ return (a.viewName === b.viewName &&
62
+ a.inModal === b.inModal &&
63
+ a.hasSearchBar === b.hasSearchBar);
64
+ }
65
+ function sameKeys(a, b) {
66
+ return a.length === b.length && a.every((key, index) => key === b[index]);
67
+ }
68
+ function resolveScreenOptions(entry, args, screenOptions) {
69
+ const own = typeof entry.options === 'function' ? entry.options(args) : entry.options;
70
+ return { ...screenOptions, ...own };
71
+ }
72
+ function StackImpl(props) {
73
+ // Read BEFORE this Stack provides its own per-screen scope below - becomes the `parent` link a
74
+ // nested screen's useNavigation().getParent() walks. undefined when this Stack is the root.
75
+ const parentScope = useNavigationScope();
76
+ const { screens, collector } = createScreenSignal('stack');
77
+ const registry = createMemo(() => toRegistry(screens()));
78
+ navigatorSequence += 1;
79
+ const routeIdPrefix = `stack-${navigatorSequence}`;
80
+ let routeSequence = 0;
81
+ function createRoute(name, params) {
82
+ routeSequence += 1;
83
+ return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params };
84
+ }
85
+ // One emitter per route.key, created lazily and pruned once the route is popped (below).
86
+ const emitters = new Map();
87
+ function emitterFor(routeKey) {
88
+ let emitter = emitters.get(routeKey);
89
+ if (emitter === undefined) {
90
+ emitter = createNavigationEmitter();
91
+ emitters.set(routeKey, emitter);
92
+ }
93
+ return emitter;
94
+ }
95
+ const [dispatched, setDispatched] = createSignal(null);
96
+ // The initial push, deferred until the markers have registered (header note 1). Cached in a plain
97
+ // local rather than a memo: it must run EXACTLY once - a memo re-running would mint a second
98
+ // route key for the same screen and silently reset the stack.
99
+ let seeded;
100
+ function seed(entries) {
101
+ if (seeded !== undefined)
102
+ return seeded;
103
+ // Deliberately NOT seeded from an empty registry, even when `initialRouteName` names the route:
104
+ // <For> maps a key exactly once, so a route minted before its marker exists would be handed a
105
+ // build with no component and stay blank forever. Waiting one update costs nothing - the
106
+ // markers register during the same flush this navigator is created in.
107
+ if (entries.size === 0) {
108
+ dlog('Stack: no <Stack.Screen> children registered');
109
+ return EMPTY_STATE;
110
+ }
111
+ const initialRouteName = props.initialRouteName ?? entries.keys().next().value;
112
+ if (initialRouteName === undefined)
113
+ return EMPTY_STATE;
114
+ seeded = createInitialNavigatorState(createRoute(initialRouteName, entries.get(initialRouteName)?.initialParams));
115
+ return seeded;
116
+ }
117
+ // A <Stack.Screen> marker can vanish while its route is still in the pushed history, which would
118
+ // leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling on READ
119
+ // keeps the repair out of any write path; the next dispatch then persists it.
120
+ const currentState = createMemo(() => {
121
+ const entries = registry();
122
+ const state = dispatched();
123
+ if (state === null)
124
+ return seed(entries);
125
+ return reconcileStackRoutes(state, [...entries.keys()]);
126
+ });
127
+ function dispatch(action) {
128
+ setDispatched(navigatorReducer(currentState(), action));
129
+ }
130
+ const handle = {
131
+ push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
132
+ pop: count => dispatch({ type: 'pop', count }),
133
+ popToTop: () => dispatch({ type: 'popToTop' }),
134
+ popTo: key => dispatch({ type: 'popTo', key }),
135
+ replace: (name, params) => dispatch({ type: 'replace', route: createRoute(name, params) }),
136
+ setParams: (params, key) => dispatch({ type: 'setParams', key, params }),
137
+ reset: nextState => dispatch({ type: 'reset', state: nextState }),
138
+ canGoBack: () => currentState().routes.length > 1,
139
+ };
140
+ props.ref?.(handle);
141
+ // Broadcasts the router state to every still-live route's emitter (createNavigationState's
142
+ // source) and prunes emitters for popped routes. Deferred by a microtask - header note 4.
143
+ createEffect(() => {
144
+ const current = currentState();
145
+ queueMicrotask(() => {
146
+ for (const route of current.routes) {
147
+ emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
148
+ }
149
+ for (const routeKey of [...emitters.keys()]) {
150
+ if (!current.routes.some(route => route.key === routeKey)) {
151
+ emitters.delete(routeKey);
152
+ }
153
+ }
154
+ });
155
+ });
156
+ // Header note 2: keyed on route KEYS, so a setParams (new route object, same key) never reaches
157
+ // <For> and never rebuilds a screen subtree.
158
+ const routeKeys = createMemo(() => currentState().routes.map(route => route.key), [], { equals: sameKeys });
159
+ function renderRoute(routeKey, index) {
160
+ const route = createMemo(() => currentState().routes.find(candidate => candidate.key === routeKey));
161
+ const entry = createMemo(() => {
162
+ const current = route();
163
+ return current === undefined ? undefined : registry().get(current.name);
164
+ });
165
+ const emitter = emitterFor(routeKey);
166
+ // The host node the app's search-bar ref last saw. `spread` re-runs a bag's `ref` on EVERY prop
167
+ // change, and an app expects one attach per node the way React's ref contract gives it.
168
+ let lastSearchBarNode = null;
169
+ // Keeping the PREVIOUS plan when the route or its marker vanishes: <For> disposes this item in
170
+ // the same update, but memo evaluation order inside one batch is not ours to choose, so the
171
+ // live prop accessors below must never observe an undefined plan mid-teardown.
172
+ const plan = createMemo(previous => {
173
+ const current = route();
174
+ const registered = entry();
175
+ if (current === undefined || registered === undefined)
176
+ return previous;
177
+ const merged = resolveScreenOptions(registered, { route: current, navigation: handle }, props.screenOptions);
178
+ const searchBarOptions = merged.headerSearchBarOptions;
179
+ return resolveScreenRenderPlan({
180
+ screenId: current.key,
181
+ index: index(),
182
+ routeCount: currentState().routes.length,
183
+ options: merged,
184
+ platform: NAVIGATOR_PLATFORM,
185
+ isAndroid: Platform.OS === 'android',
186
+ screenPassthrough: {
187
+ [SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
188
+ [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () => dispatch({ type: 'pop', count: 1 }),
189
+ // onAppear/onDisappear are the definitive visibility boundary (post-transition), so
190
+ // 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
191
+ // BEFORE the animation and would double-invoke createFocusEffect, so they only log.
192
+ [SCREEN_ON_WILL_APPEAR]: () => dlog(`Stack: route "${current.name}" will appear at t=${Date.now()}`),
193
+ [SCREEN_ON_APPEAR]: () => {
194
+ dlog(`Stack: route "${current.name}" appeared (focus) at t=${Date.now()}`);
195
+ emitter.emit(NAVIGATION_EVENT_FOCUS);
196
+ },
197
+ [SCREEN_ON_WILL_DISAPPEAR]: () => dlog(`Stack: route "${current.name}" will disappear at t=${Date.now()}`),
198
+ [SCREEN_ON_DISAPPEAR]: () => {
199
+ dlog(`Stack: route "${current.name}" disappeared (blur) at t=${Date.now()}`);
200
+ emitter.emit(NAVIGATION_EVENT_BLUR);
201
+ },
202
+ },
203
+ searchBarPassthrough: searchBarOptions === undefined
204
+ ? undefined
205
+ : {
206
+ ...buildSearchBarPassthrough(searchBarOptions, message => dlog(`Stack: route "${current.name}" ${message}`)),
207
+ // The imperative ref (ISearchBarCommands): `spread` calls a `ref` found in the bag
208
+ // with the host node, so it rides down inside passthrough untouched.
209
+ ref: (element) => {
210
+ const node = isSymbioteNode(element) ? element : null;
211
+ if (node === lastSearchBarNode)
212
+ return;
213
+ lastSearchBarNode = node;
214
+ dlog(`Stack: search bar ref, node=${node === null ? 'null' : debugNodeId(node)}`);
215
+ searchBarOptions.ref?.(node === null ? null : buildSearchBarHandle(() => node));
216
+ },
217
+ },
218
+ });
219
+ }, undefined);
220
+ // NOT an early `return undefined` when the plan is still unresolved. <For> maps a key exactly
221
+ // ONCE, so bailing here would strand the route with nothing to paint for the rest of its life -
222
+ // and an unresolved plan is the NORMAL first state, since the markers register after the
223
+ // navigator's body has run (header note 1).
224
+ const shape = createMemo(() => {
225
+ const current = plan();
226
+ if (current === undefined)
227
+ return undefined;
228
+ return {
229
+ viewName: current.screenViewName,
230
+ inModal: current.inModal,
231
+ hasSearchBar: current.searchBarProps !== undefined,
232
+ };
233
+ }, undefined, {
234
+ equals: (a, b) => a === b || (a !== undefined && b !== undefined && sameShape(a, b)),
235
+ });
236
+ function buildScreen(current) {
237
+ // Re-read rather than closed over: this runs on every rebuild boundary crossing, and the
238
+ // fallbacks below keep the live accessors from ever seeing `undefined` mid-teardown.
239
+ const builtPlan = plan();
240
+ const builtRoute = route();
241
+ const builtEntry = entry();
242
+ if (builtPlan === undefined ||
243
+ builtRoute === undefined ||
244
+ builtEntry === undefined) {
245
+ dlog(`Stack: no screen registered for route key "${routeKey}"`);
246
+ return undefined;
247
+ }
248
+ const livePlan = () => plan() ?? builtPlan;
249
+ const liveRoute = () => route() ?? builtRoute;
250
+ const initialComponent = builtEntry.component;
251
+ const scope = () => ({
252
+ route: liveRoute(),
253
+ navigation: handle,
254
+ emitter,
255
+ parent: parentScope?.(),
256
+ });
257
+ // The narrower boundary inside the shape one: a screen swapping its `component` rebuilds only
258
+ // the body, never the native screen.
259
+ const component = createMemo(() => entry()?.component ?? initialComponent);
260
+ // Must not be flattened away (collapsable: false, supplied by contentWrapperProps) -
261
+ // react-native-screens' native side finds THIS view type by class check to register a
262
+ // formSheet's content for sizing (RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME in core/constants.ts).
263
+ const contentWrapper = hostElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, () => livePlan().contentWrapperProps);
264
+ insert(contentWrapper, createComponent(NavigationScopeProvider, {
265
+ value: scope,
266
+ // The screen component is created HERE, inside the Provider's own children resolution,
267
+ // not in a memo declared above it - a Solid computation captures its owner at CREATION,
268
+ // so a component built outside this getter would run with the enclosing owner and see no
269
+ // navigation scope at all (useRoute would throw). Reading `component()` tracked makes the
270
+ // getter the rebuild boundary for a component swap; `untrack` around the build keeps the
271
+ // screen's own prop reads out of that boundary. No route/navigation props: the screen
272
+ // reads both through the primitives off the scope provided here.
273
+ get children() {
274
+ const Component = component();
275
+ return untrack(() => createComponent(Component, {}));
276
+ },
277
+ }));
278
+ // The header config (and its RNSSearchBar child, when the screen has one) is a pure
279
+ // Descriptor leaf, so it goes through the shared bridge verbatim, exactly like React's and
280
+ // Vue's do. Its child COUNT is part of `shape` above, which is what keeps the bridge's
281
+ // shape-stability contract satisfied when an app adds or removes headerSearchBarOptions.
282
+ const headerConfig = descriptorToSolid(() => livePlan().headerConfig);
283
+ const screen = hostElement(current.viewName, () => livePlan().screenProps);
284
+ if (!current.inModal) {
285
+ insertNode(screen, headerConfig);
286
+ insertNode(screen, contentWrapper);
287
+ return screen;
288
+ }
289
+ // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
290
+ // RNSScreenStack/RNSScreen purely to host the native header bar. Skipping this leaves
291
+ // RNSScreenStackHeaderConfig with no navigation controller to attach to, so the header
292
+ // silently never renders.
293
+ const innerStack = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => ({
294
+ style: livePlan().innerStackStyle,
295
+ }));
296
+ // activityState mirrors the outer Screen's - RNSScreen.mm treats an unset/inactive nested
297
+ // screen as not yet pushed, leaving it parked at its pre-push transition position (off past
298
+ // the bottom edge) instead of its real, presented frame.
299
+ const innerScreen = hostElement(RNS_SCREEN_VIEW_NAME, () => ({
300
+ style: livePlan().innerScreenStyle,
301
+ activityState: livePlan().activityState,
302
+ }));
303
+ insertNode(innerScreen, headerConfig);
304
+ insertNode(innerScreen, contentWrapper);
305
+ insertNode(innerStack, innerScreen);
306
+ insertNode(screen, innerStack);
307
+ return screen;
308
+ }
309
+ return createMemo(() => {
310
+ const current = shape();
311
+ if (current === undefined)
312
+ return undefined;
313
+ return untrack(() => buildScreen(current));
314
+ });
315
+ }
316
+ // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
317
+ // native signal that the WHOLE push/pop animation finished, as opposed to the per-screen
318
+ // onAppear/onDisappear. Kept behind DEBUG, never removed.
319
+ const stackProps = resolveStackProps({
320
+ passthrough: {
321
+ [STACK_ON_FINISH_TRANSITIONING]: () => dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
322
+ },
323
+ });
324
+ const root = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => stackProps);
325
+ // The <For> component is created EAGERLY and handed to `insert` as a value, never as
326
+ // `() => createComponent(For, …)`. The latter would read `each` inside insert's own render
327
+ // effect, so every route change would re-create the whole list instead of letting For's keyed
328
+ // mapping reuse the screens it already built.
329
+ insert(root, createComponent(For, {
330
+ get each() {
331
+ return routeKeys();
332
+ },
333
+ children: renderRoute,
334
+ }));
335
+ onCleanup(() => emitters.clear());
336
+ // `props.children` is READ here and nowhere else: reading it is what CREATES the markers, and
337
+ // they must be created inside this Provider so their collectStackScreen() finds the collector on
338
+ // the owner chain. They paint nothing, so the array is effectively just `root` - and `root` is a
339
+ // pre-built node reference, so a marker list that changes re-runs this getter without disturbing
340
+ // the navigator tree.
341
+ return createComponent(ScreenCollectorProvider, {
342
+ value: collector,
343
+ get children() {
344
+ return [props.children, root];
345
+ },
346
+ });
347
+ }
348
+ export const Stack = Object.assign(StackImpl, { Screen });
@@ -0,0 +1,13 @@
1
+ import type { Component } from 'solid-js';
2
+ import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
3
+ export type ITabScreenOptionsArgs = {
4
+ route: IRoute<unknown>;
5
+ navigation: ITabNavigatorHandle;
6
+ };
7
+ export type ITabScreenOptionsResolver = (args: ITabScreenOptionsArgs) => ITabOptions;
8
+ export type ITabScreenProps = {
9
+ name: string;
10
+ component: Component;
11
+ options?: ITabOptions | ITabScreenOptionsResolver;
12
+ initialParams?: unknown;
13
+ };
@@ -0,0 +1,3 @@
1
+ // Tab.Screen's public prop types. Same split rationale as screen-props.ts: `component` is a Solid
2
+ // value, everything else rides on the agnostic base from ../core.
3
+ export {};
@@ -0,0 +1,14 @@
1
+ import type { JSX } from '@symbiote-native/solid/jsx-runtime';
2
+ import type { ITabNavigatorHandle, ITabOptions } from '../../core';
3
+ import { TabScreen } from '../screen';
4
+ export type { ITabNavigatorHandle } from '../../core';
5
+ export type ITabProps = {
6
+ initialRouteName?: string;
7
+ screenOptions?: ITabOptions;
8
+ ref?: (handle: ITabNavigatorHandle) => void;
9
+ children?: JSX.Element;
10
+ };
11
+ declare function TabImpl(props: ITabProps): JSX.Element;
12
+ export declare const Tab: typeof TabImpl & {
13
+ Screen: typeof TabScreen;
14
+ };
@@ -0,0 +1,228 @@
1
+ // Tab, the Solid lifecycle half. The focused-index router (tab-router-state) and the tab-bar
2
+ // Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim with
3
+ // every other adapter; here Solid supplies the lifecycle - a signal for the dispatched router
4
+ // state, a callback `ref` for the jumpTo/setParams handle - plus the descriptor bridge for the
5
+ // tab-bar leaf. Unlike Stack, a bottom-tabs bar is a PURE-JS UI painting ordinary
6
+ // `symbiote-view`/`symbiote-text` primitives, so there is no react-native-screens ViewConfig to
7
+ // register here.
8
+ //
9
+ // TWO SOLID-ONLY REBUILD BOUNDARIES, both invisible in every other adapter:
10
+ //
11
+ // 1. THE TAB BAR'S SHAPE FOLLOWS ITS DATA. renderTabBar emits one child per item, and each item
12
+ // emits an icon wrapper only when it has an icon or a badge - so adding a screen, or a badge
13
+ // appearing, CHANGES the Descriptor's shape, which descriptorToSolid's shape guard rejects by
14
+ // design (it builds once and updates props in place). `barSignature` captures exactly the parts
15
+ // of the data the shape depends on, and the bar is rebuilt when that string changes and only
16
+ // then. Every other adapter re-renders the whole bar every time and never meets this.
17
+ //
18
+ // 2. ONLY THE FOCUSED SCREEN IS MOUNTED, so a focus change is a genuine rebuild while a
19
+ // `setParams` on the focused route must NOT be. The content memo therefore depends on the
20
+ // focused route KEY and its component identity alone, with the build `untrack`ed
21
+ // (.claude/rules/solid-descriptor-bridge.md §5); the route object itself reaches the screen
22
+ // through the scope accessor, which is what keeps useRoute() live without rebuilding anything.
23
+ import { createEffect, createMemo, createSignal, untrack } from 'solid-js';
24
+ import { descriptorToSolid } from '@symbiote-native/solid';
25
+ // createComponent from solid-js, insert from the renderer - see stack/index.ts's note.
26
+ import { createComponent } from 'solid-js';
27
+ import { insert } from '@symbiote-native/solid/renderer';
28
+ import { dlog } from '@symbiote-native/engine';
29
+ import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, diffFocusedRoute, isFocusedRoute, reconcileTabRoutes, renderTabBar, tabRouterReducer, } from '../../core/index.js';
30
+ import { hostElement } from '../host.js';
31
+ import { NavigationScopeProvider, useNavigationScope, } from '../navigation-context.js';
32
+ import { ScreenCollectorProvider, createScreenSignal, toRegistry, } from '../screen-registry.js';
33
+ import { TabScreen } from '../screen.js';
34
+ const TAB_CONTENT_STYLE = { flex: 1 };
35
+ const TAB_ROOT_STYLE = { flex: 1 };
36
+ let navigatorSequence = 0;
37
+ function resolveTabOptions(entry, args, screenOptions) {
38
+ const own = typeof entry.options === 'function' ? entry.options(args) : entry.options;
39
+ return { ...screenOptions, ...own };
40
+ }
41
+ // A Descriptor icon is spliced into the bar verbatim, so only its ROOT type is visible here - an
42
+ // app that swaps one Descriptor icon for another of the same type but a different child count
43
+ // would still trip the bridge's shape guard, with the guard's own message. That is the honest
44
+ // boundary: this signature covers the shape decisions render-tabs.ts itself makes.
45
+ function iconKind(icon) {
46
+ if (icon === undefined)
47
+ return '-';
48
+ return typeof icon === 'string' ? 'glyph' : icon.type;
49
+ }
50
+ function barSignature(items) {
51
+ return items
52
+ .map(item => `${item.key}:${iconKind(item.icon)}:${item.badge === undefined ? 0 : 1}`)
53
+ .join('|');
54
+ }
55
+ function TabImpl(props) {
56
+ // Read BEFORE this Tab provides its own per-screen scope - becomes the `parent` link a nested
57
+ // screen's useNavigation().getParent() walks. undefined when this Tab is the nesting root.
58
+ const parentScope = useNavigationScope();
59
+ const { screens, collector } = createScreenSignal('tab');
60
+ const registry = createMemo(() => toRegistry(screens()));
61
+ navigatorSequence += 1;
62
+ const routeIdPrefix = `tab-${navigatorSequence}`;
63
+ // Route keys are derived from the screen NAME, never a counter: a Tab's route list is rebuilt
64
+ // from the registry on every registration change, and a counter would mint a fresh key each time,
65
+ // re-mounting every screen for an unrelated marker edit.
66
+ const routes = createMemo(() => [...registry().entries()].map(([name, entry]) => ({
67
+ key: `${routeIdPrefix}-${name}`,
68
+ name,
69
+ params: entry.initialParams,
70
+ })));
71
+ // `null` means nothing has dispatched yet, which is what keeps initialRouteName honored when the
72
+ // markers arrive after the body has already run (screen-registry.ts).
73
+ const [dispatched, setDispatched] = createSignal(null);
74
+ const state = createMemo(() => {
75
+ const current = routes();
76
+ const previous = dispatched();
77
+ return previous === null
78
+ ? createInitialTabState(current, props.initialRouteName)
79
+ : reconcileTabRoutes(previous, current);
80
+ });
81
+ function dispatch(action) {
82
+ setDispatched(tabRouterReducer(state(), action));
83
+ }
84
+ const jumpTo = (name, params) => dispatch({ type: 'jumpTo', name, params });
85
+ const setParams = (params, key) => dispatch({ type: 'setParams', key, params });
86
+ const handle = { jumpTo, setParams };
87
+ props.ref?.(handle);
88
+ // One emitter per route.key, created lazily and cached for the navigator's whole lifetime. Emitter
89
+ // IDENTITY (stable, looked up by key) is deliberately decoupled from emit TIMING (deferred below):
90
+ // a scheme that recreated the emitter on each focus change raced the build of the very screen that
91
+ // was supposed to subscribe to it.
92
+ const emitters = new Map();
93
+ function emitterFor(routeKey) {
94
+ let emitter = emitters.get(routeKey);
95
+ if (emitter === undefined) {
96
+ emitter = createNavigationEmitter();
97
+ emitters.set(routeKey, emitter);
98
+ }
99
+ return emitter;
100
+ }
101
+ const focusedKey = createMemo(() => {
102
+ const current = state();
103
+ return current.routes[current.index]?.key;
104
+ });
105
+ // Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
106
+ // Stack's RNSScreen), so focus/blur is synthesized here. The emit is deferred to a microtask,
107
+ // which resolves only after Solid's whole effect flush has drained - including the build of the
108
+ // newly focused screen and the subscriptions createIsFocused/createFocusEffect register inside
109
+ // it. Emitting inline would reach zero subscribers.
110
+ let lastFocusedKey;
111
+ createEffect(() => {
112
+ const nextKey = focusedKey();
113
+ const { blurKey, focusKey } = diffFocusedRoute(lastFocusedKey, nextKey);
114
+ if (blurKey === undefined && focusKey === undefined)
115
+ return;
116
+ lastFocusedKey = nextKey;
117
+ queueMicrotask(() => {
118
+ if (blurKey !== undefined) {
119
+ dlog(`Tab: route "${blurKey}" blurred at t=${Date.now()}`);
120
+ emitterFor(blurKey).emit(NAVIGATION_EVENT_BLUR);
121
+ }
122
+ if (focusKey !== undefined) {
123
+ dlog(`Tab: route "${focusKey}" focused at t=${Date.now()}`);
124
+ emitterFor(focusKey).emit(NAVIGATION_EVENT_FOCUS);
125
+ }
126
+ });
127
+ });
128
+ const items = createMemo(() => {
129
+ const current = state();
130
+ const entries = registry();
131
+ return current.routes.map((route, index) => {
132
+ const entry = entries.get(route.name);
133
+ const focused = isFocusedRoute(index, current.index);
134
+ if (entry === undefined) {
135
+ dlog(`Tab: no screen registered for route name "${route.name}"`);
136
+ return { key: route.key, focused, label: route.name, passthrough: {} };
137
+ }
138
+ const options = resolveTabOptions(entry, { route, navigation: handle }, props.screenOptions);
139
+ return {
140
+ key: route.key,
141
+ focused,
142
+ label: options.tabBarLabel ?? options.title ?? route.name,
143
+ icon: options.tabBarIcon,
144
+ badge: options.tabBarBadge,
145
+ activeTintColor: options.tabBarActiveTintColor,
146
+ inactiveTintColor: options.tabBarInactiveTintColor,
147
+ passthrough: {
148
+ onPress: () => jumpTo(route.name),
149
+ accessibilityRole: 'tab',
150
+ accessibilityState: { selected: focused },
151
+ },
152
+ };
153
+ });
154
+ });
155
+ const focusedOptions = createMemo(() => {
156
+ const current = state();
157
+ const route = current.routes[current.index];
158
+ const entry = route === undefined ? undefined : registry().get(route.name);
159
+ if (entry === undefined || route === undefined)
160
+ return props.screenOptions;
161
+ return resolveTabOptions(entry, { route, navigation: handle }, props.screenOptions);
162
+ });
163
+ // Header note 1: the bar is rebuilt only when its Descriptor SHAPE would change; every tint,
164
+ // label and badge-value change flows through descriptorToSolid's prop diff on the live nodes.
165
+ const barShape = createMemo(() => barSignature(items()));
166
+ const tabBar = createMemo(() => {
167
+ barShape();
168
+ return untrack(() => descriptorToSolid(() => renderTabBar({
169
+ items: items(),
170
+ style: focusedOptions()?.tabBarStyle,
171
+ passthrough: {},
172
+ })));
173
+ });
174
+ // Header note 2. Only the focused route's screen is ever mounted (unlike Stack, which keeps every
175
+ // pushed route alive), so the previous screen's whole subtree - and every listener it registered
176
+ // - is torn down by Solid's ordinary disposal when focus moves on.
177
+ const focusedComponent = createMemo(() => {
178
+ const current = state();
179
+ const route = current.routes[current.index];
180
+ return route === undefined
181
+ ? undefined
182
+ : registry().get(route.name)?.component;
183
+ });
184
+ const content = createMemo(() => {
185
+ const routeKey = focusedKey();
186
+ const Component = focusedComponent();
187
+ return untrack(() => {
188
+ if (routeKey === undefined || Component === undefined)
189
+ return undefined;
190
+ const scope = () => ({
191
+ route: routeFor(routeKey),
192
+ navigation: handle,
193
+ emitter: emitterFor(routeKey),
194
+ parent: parentScope?.(),
195
+ });
196
+ return createComponent(NavigationScopeProvider, {
197
+ value: scope,
198
+ // No route/navigation props: the screen reads both through the primitives off the scope
199
+ // provided here (useRoute / useTabNavigation).
200
+ get children() {
201
+ return createComponent(Component, {});
202
+ },
203
+ });
204
+ });
205
+ });
206
+ // Reads the CURRENT object for a key rather than closing over the one that was focused at build
207
+ // time - the accessor is what makes `setParams` reach a mounted screen's useRoute().
208
+ function routeFor(routeKey) {
209
+ const found = state().routes.find(route => route.key === routeKey);
210
+ return found ?? { key: routeKey, name: '', params: undefined };
211
+ }
212
+ const contentHost = hostElement('symbiote-view', () => ({
213
+ style: TAB_CONTENT_STYLE,
214
+ }));
215
+ insert(contentHost, content);
216
+ const root = hostElement('symbiote-view', () => ({ style: TAB_ROOT_STYLE }));
217
+ // ONE array insert rather than two: a second `insert` on the same parent would find the first
218
+ // child already there and REPLACE it (solid-js/universal's insertExpression), while an array is
219
+ // reconciled by identity - contentHost keeps its node, only the bar is swapped when it rebuilds.
220
+ insert(root, () => [contentHost, tabBar()]);
221
+ return createComponent(ScreenCollectorProvider, {
222
+ value: collector,
223
+ get children() {
224
+ return [props.children, root];
225
+ },
226
+ });
227
+ }
228
+ export const Tab = Object.assign(TabImpl, { Screen: TabScreen });
@@ -15,7 +15,9 @@ import { mountDescriptorChildren } from '@symbiote-native/svelte/native-view-bri
15
15
  const TEXT_CHILD_MARK = '#';
16
16
  function shapeOf(children) {
17
17
  return children
18
- .map(child => typeof child === 'string' ? TEXT_CHILD_MARK : `${child.type}(${shapeOf(child.children)})`)
18
+ .map(child => typeof child === 'string'
19
+ ? TEXT_CHILD_MARK
20
+ : `${child.type}(${shapeOf(child.children)})`)
19
21
  .join(',');
20
22
  }
21
23
  export function createDescriptorSubtreeSync() {