@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,528 @@
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
+
38
+ import {
39
+ For,
40
+ createEffect,
41
+ createMemo,
42
+ createSignal,
43
+ onCleanup,
44
+ untrack,
45
+ } from 'solid-js';
46
+ import type { Accessor } from 'solid-js';
47
+ import { descriptorToSolid } from '@symbiote-native/solid';
48
+ import type { JSX } from '@symbiote-native/solid/jsx-runtime';
49
+ // `createComponent` comes from solid-js, not from the renderer: they are the SAME runtime call
50
+ // (untrack + invoke), but the renderer's is typed to return a host node, which a Solid component
51
+ // legitimately does not have to (a screen may render nothing). `insert`/`insertNode` are the
52
+ // renderer's own, since they mutate the engine tree.
53
+ import { createComponent } from 'solid-js';
54
+ import { insert, insertNode } from '@symbiote-native/solid/renderer';
55
+ import {
56
+ Platform,
57
+ debugNodeId,
58
+ dlog,
59
+ isSymbioteNode,
60
+ } from '@symbiote-native/engine';
61
+ import type { ISymbioteNode } from '@symbiote-native/engine';
62
+ import {
63
+ NAVIGATION_EVENT_BLUR,
64
+ NAVIGATION_EVENT_FOCUS,
65
+ NAVIGATION_EVENT_STATE,
66
+ RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
67
+ RNS_SCREEN_STACK_VIEW_NAME,
68
+ RNS_SCREEN_VIEW_NAME,
69
+ SCREEN_ON_APPEAR,
70
+ SCREEN_ON_DISAPPEAR,
71
+ SCREEN_ON_DISMISSED,
72
+ SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
73
+ SCREEN_ON_WILL_APPEAR,
74
+ SCREEN_ON_WILL_DISAPPEAR,
75
+ STACK_ON_FINISH_TRANSITIONING,
76
+ buildSearchBarHandle,
77
+ buildSearchBarPassthrough,
78
+ createInitialNavigatorState,
79
+ createNavigationEmitter,
80
+ navigatorReducer,
81
+ reconcileStackRoutes,
82
+ resolveScreenRenderPlan,
83
+ resolveStackProps,
84
+ } from '../../core';
85
+ import type {
86
+ INavigationEmitter,
87
+ INavigatorHandle,
88
+ INavigatorPlatform,
89
+ INavigatorState,
90
+ IRoute,
91
+ IScreenRenderPlan,
92
+ } from '../../core';
93
+ import { hostElement } from '../host';
94
+ import {
95
+ NavigationScopeProvider,
96
+ useNavigationScope,
97
+ } from '../navigation-context';
98
+ import type { INavigationScope } from '../navigation-context';
99
+ import {
100
+ ScreenCollectorProvider,
101
+ createScreenSignal,
102
+ toRegistry,
103
+ } from '../screen-registry';
104
+ import type { IRegisteredScreen } from '../screen-registry';
105
+ import { Screen } from '../screen';
106
+ import type {
107
+ IScreenOptionsArgs,
108
+ IScreenProps,
109
+ ISolidScreenOptions,
110
+ } from '../screen-props';
111
+
112
+ export type { INavigatorHandle } from '../../core';
113
+
114
+ // React's `children?: ReactNode` and Vue's default slot become Solid's `children` - the markers,
115
+ // which register themselves rather than being scanned. `ref` is Solid's own spelling of Vue's
116
+ // `expose()`: a callback the compiler hands the navigator handle.
117
+ export type IStackProps = {
118
+ initialRouteName?: string;
119
+ screenOptions?: ISolidScreenOptions;
120
+ ref?: (handle: INavigatorHandle) => void;
121
+ children?: JSX.Element;
122
+ };
123
+
124
+ // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
125
+ // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope.
126
+ const NAVIGATOR_PLATFORM: INavigatorPlatform = {
127
+ defaultHeaderBackTitleVisible: true,
128
+ };
129
+
130
+ const EMPTY_STATE: INavigatorState = { routes: [] };
131
+
132
+ // Route keys must be unique per navigator INSTANCE. React/Vue take that from useId(); Solid has no
133
+ // equivalent, so a module counter stands in - read once, at construction.
134
+ let navigatorSequence = 0;
135
+
136
+ type IStackScreenEntry = IRegisteredScreen<IScreenProps['options']>;
137
+
138
+ // What makes a route's NATIVE shape different rather than just its props (header note 3).
139
+ type IScreenShape = {
140
+ viewName: string;
141
+ inModal: boolean;
142
+ hasSearchBar: boolean;
143
+ };
144
+
145
+ function sameShape(a: IScreenShape, b: IScreenShape): boolean {
146
+ return (
147
+ a.viewName === b.viewName &&
148
+ a.inModal === b.inModal &&
149
+ a.hasSearchBar === b.hasSearchBar
150
+ );
151
+ }
152
+
153
+ function sameKeys(a: readonly string[], b: readonly string[]): boolean {
154
+ return a.length === b.length && a.every((key, index) => key === b[index]);
155
+ }
156
+
157
+ function resolveScreenOptions(
158
+ entry: IStackScreenEntry,
159
+ args: IScreenOptionsArgs,
160
+ screenOptions: ISolidScreenOptions | undefined,
161
+ ): ISolidScreenOptions {
162
+ const own =
163
+ typeof entry.options === 'function' ? entry.options(args) : entry.options;
164
+ return { ...screenOptions, ...own };
165
+ }
166
+
167
+ function StackImpl(props: IStackProps): JSX.Element {
168
+ // Read BEFORE this Stack provides its own per-screen scope below - becomes the `parent` link a
169
+ // nested screen's useNavigation().getParent() walks. undefined when this Stack is the root.
170
+ const parentScope = useNavigationScope();
171
+
172
+ const { screens, collector } = createScreenSignal<
173
+ 'stack',
174
+ IScreenProps['options']
175
+ >('stack');
176
+ const registry = createMemo(() => toRegistry(screens()));
177
+
178
+ navigatorSequence += 1;
179
+ const routeIdPrefix = `stack-${navigatorSequence}`;
180
+ let routeSequence = 0;
181
+
182
+ function createRoute(name: string, params: unknown): IRoute<unknown> {
183
+ routeSequence += 1;
184
+ return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params };
185
+ }
186
+
187
+ // One emitter per route.key, created lazily and pruned once the route is popped (below).
188
+ const emitters = new Map<string, INavigationEmitter>();
189
+ function emitterFor(routeKey: string): INavigationEmitter {
190
+ let emitter = emitters.get(routeKey);
191
+ if (emitter === undefined) {
192
+ emitter = createNavigationEmitter();
193
+ emitters.set(routeKey, emitter);
194
+ }
195
+ return emitter;
196
+ }
197
+
198
+ const [dispatched, setDispatched] = createSignal<INavigatorState | null>(
199
+ null,
200
+ );
201
+
202
+ // The initial push, deferred until the markers have registered (header note 1). Cached in a plain
203
+ // local rather than a memo: it must run EXACTLY once - a memo re-running would mint a second
204
+ // route key for the same screen and silently reset the stack.
205
+ let seeded: INavigatorState | undefined;
206
+ function seed(entries: Map<string, IStackScreenEntry>): INavigatorState {
207
+ if (seeded !== undefined) return seeded;
208
+ // Deliberately NOT seeded from an empty registry, even when `initialRouteName` names the route:
209
+ // <For> maps a key exactly once, so a route minted before its marker exists would be handed a
210
+ // build with no component and stay blank forever. Waiting one update costs nothing - the
211
+ // markers register during the same flush this navigator is created in.
212
+ if (entries.size === 0) {
213
+ dlog('Stack: no <Stack.Screen> children registered');
214
+ return EMPTY_STATE;
215
+ }
216
+ const initialRouteName =
217
+ props.initialRouteName ?? entries.keys().next().value;
218
+ if (initialRouteName === undefined) return EMPTY_STATE;
219
+ seeded = createInitialNavigatorState(
220
+ createRoute(
221
+ initialRouteName,
222
+ entries.get(initialRouteName)?.initialParams,
223
+ ),
224
+ );
225
+ return seeded;
226
+ }
227
+
228
+ // A <Stack.Screen> marker can vanish while its route is still in the pushed history, which would
229
+ // leave that entry with nothing to render (reconcileStackRoutes' header). Reconciling on READ
230
+ // keeps the repair out of any write path; the next dispatch then persists it.
231
+ const currentState = createMemo<INavigatorState>(() => {
232
+ const entries = registry();
233
+ const state = dispatched();
234
+ if (state === null) return seed(entries);
235
+ return reconcileStackRoutes(state, [...entries.keys()]);
236
+ });
237
+
238
+ function dispatch(action: Parameters<typeof navigatorReducer>[1]): void {
239
+ setDispatched(navigatorReducer(currentState(), action));
240
+ }
241
+
242
+ const handle: INavigatorHandle = {
243
+ push: (name, params) =>
244
+ dispatch({ type: 'push', route: createRoute(name, params) }),
245
+ pop: count => dispatch({ type: 'pop', count }),
246
+ popToTop: () => dispatch({ type: 'popToTop' }),
247
+ popTo: key => dispatch({ type: 'popTo', key }),
248
+ replace: (name, params) =>
249
+ dispatch({ type: 'replace', route: createRoute(name, params) }),
250
+ setParams: (params, key) => dispatch({ type: 'setParams', key, params }),
251
+ reset: nextState => dispatch({ type: 'reset', state: nextState }),
252
+ canGoBack: () => currentState().routes.length > 1,
253
+ };
254
+ props.ref?.(handle);
255
+
256
+ // Broadcasts the router state to every still-live route's emitter (createNavigationState's
257
+ // source) and prunes emitters for popped routes. Deferred by a microtask - header note 4.
258
+ createEffect(() => {
259
+ const current = currentState();
260
+ queueMicrotask(() => {
261
+ for (const route of current.routes) {
262
+ emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
263
+ }
264
+ for (const routeKey of [...emitters.keys()]) {
265
+ if (!current.routes.some(route => route.key === routeKey)) {
266
+ emitters.delete(routeKey);
267
+ }
268
+ }
269
+ });
270
+ });
271
+
272
+ // Header note 2: keyed on route KEYS, so a setParams (new route object, same key) never reaches
273
+ // <For> and never rebuilds a screen subtree.
274
+ const routeKeys = createMemo<readonly string[]>(
275
+ () => currentState().routes.map(route => route.key),
276
+ [],
277
+ { equals: sameKeys },
278
+ );
279
+
280
+ function renderRoute(routeKey: string, index: Accessor<number>): JSX.Element {
281
+ const route = createMemo(() =>
282
+ currentState().routes.find(candidate => candidate.key === routeKey),
283
+ );
284
+ const entry = createMemo(() => {
285
+ const current = route();
286
+ return current === undefined ? undefined : registry().get(current.name);
287
+ });
288
+ const emitter = emitterFor(routeKey);
289
+ // The host node the app's search-bar ref last saw. `spread` re-runs a bag's `ref` on EVERY prop
290
+ // change, and an app expects one attach per node the way React's ref contract gives it.
291
+ let lastSearchBarNode: ISymbioteNode | null = null;
292
+
293
+ // Keeping the PREVIOUS plan when the route or its marker vanishes: <For> disposes this item in
294
+ // the same update, but memo evaluation order inside one batch is not ours to choose, so the
295
+ // live prop accessors below must never observe an undefined plan mid-teardown.
296
+ const plan = createMemo<IScreenRenderPlan | undefined>(previous => {
297
+ const current = route();
298
+ const registered = entry();
299
+ if (current === undefined || registered === undefined) return previous;
300
+
301
+ const merged = resolveScreenOptions(
302
+ registered,
303
+ { route: current, navigation: handle },
304
+ props.screenOptions,
305
+ );
306
+ const searchBarOptions = merged.headerSearchBarOptions;
307
+
308
+ return resolveScreenRenderPlan({
309
+ screenId: current.key,
310
+ index: index(),
311
+ routeCount: currentState().routes.length,
312
+ options: merged,
313
+ platform: NAVIGATOR_PLATFORM,
314
+ isAndroid: Platform.OS === 'android',
315
+ screenPassthrough: {
316
+ [SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
317
+ [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () =>
318
+ dispatch({ type: 'pop', count: 1 }),
319
+ // onAppear/onDisappear are the definitive visibility boundary (post-transition), so
320
+ // 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
321
+ // BEFORE the animation and would double-invoke createFocusEffect, so they only log.
322
+ [SCREEN_ON_WILL_APPEAR]: () =>
323
+ dlog(
324
+ `Stack: route "${current.name}" will appear at t=${Date.now()}`,
325
+ ),
326
+ [SCREEN_ON_APPEAR]: () => {
327
+ dlog(
328
+ `Stack: route "${current.name}" appeared (focus) at t=${Date.now()}`,
329
+ );
330
+ emitter.emit(NAVIGATION_EVENT_FOCUS);
331
+ },
332
+ [SCREEN_ON_WILL_DISAPPEAR]: () =>
333
+ dlog(
334
+ `Stack: route "${current.name}" will disappear at t=${Date.now()}`,
335
+ ),
336
+ [SCREEN_ON_DISAPPEAR]: () => {
337
+ dlog(
338
+ `Stack: route "${current.name}" disappeared (blur) at t=${Date.now()}`,
339
+ );
340
+ emitter.emit(NAVIGATION_EVENT_BLUR);
341
+ },
342
+ },
343
+ searchBarPassthrough:
344
+ searchBarOptions === undefined
345
+ ? undefined
346
+ : {
347
+ ...buildSearchBarPassthrough(searchBarOptions, message =>
348
+ dlog(`Stack: route "${current.name}" ${message}`),
349
+ ),
350
+ // The imperative ref (ISearchBarCommands): `spread` calls a `ref` found in the bag
351
+ // with the host node, so it rides down inside passthrough untouched.
352
+ ref: (element: unknown): void => {
353
+ const node = isSymbioteNode(element) ? element : null;
354
+ if (node === lastSearchBarNode) return;
355
+ lastSearchBarNode = node;
356
+ dlog(
357
+ `Stack: search bar ref, node=${node === null ? 'null' : debugNodeId(node)}`,
358
+ );
359
+ searchBarOptions.ref?.(
360
+ node === null ? null : buildSearchBarHandle(() => node),
361
+ );
362
+ },
363
+ },
364
+ });
365
+ }, undefined);
366
+
367
+ // NOT an early `return undefined` when the plan is still unresolved. <For> maps a key exactly
368
+ // ONCE, so bailing here would strand the route with nothing to paint for the rest of its life -
369
+ // and an unresolved plan is the NORMAL first state, since the markers register after the
370
+ // navigator's body has run (header note 1).
371
+ const shape = createMemo<IScreenShape | undefined>(
372
+ () => {
373
+ const current = plan();
374
+ if (current === undefined) return undefined;
375
+ return {
376
+ viewName: current.screenViewName,
377
+ inModal: current.inModal,
378
+ hasSearchBar: current.searchBarProps !== undefined,
379
+ };
380
+ },
381
+ undefined,
382
+ {
383
+ equals: (a, b) =>
384
+ a === b || (a !== undefined && b !== undefined && sameShape(a, b)),
385
+ },
386
+ );
387
+
388
+ function buildScreen(current: IScreenShape): ISymbioteNode | undefined {
389
+ // Re-read rather than closed over: this runs on every rebuild boundary crossing, and the
390
+ // fallbacks below keep the live accessors from ever seeing `undefined` mid-teardown.
391
+ const builtPlan = plan();
392
+ const builtRoute = route();
393
+ const builtEntry = entry();
394
+ if (
395
+ builtPlan === undefined ||
396
+ builtRoute === undefined ||
397
+ builtEntry === undefined
398
+ ) {
399
+ dlog(`Stack: no screen registered for route key "${routeKey}"`);
400
+ return undefined;
401
+ }
402
+ const livePlan = (): IScreenRenderPlan => plan() ?? builtPlan;
403
+ const liveRoute = (): IRoute<unknown> => route() ?? builtRoute;
404
+ const initialComponent = builtEntry.component;
405
+
406
+ const scope: INavigationScope = () => ({
407
+ route: liveRoute(),
408
+ navigation: handle,
409
+ emitter,
410
+ parent: parentScope?.(),
411
+ });
412
+
413
+ // The narrower boundary inside the shape one: a screen swapping its `component` rebuilds only
414
+ // the body, never the native screen.
415
+ const component = createMemo(
416
+ () => entry()?.component ?? initialComponent,
417
+ );
418
+
419
+ // Must not be flattened away (collapsable: false, supplied by contentWrapperProps) -
420
+ // react-native-screens' native side finds THIS view type by class check to register a
421
+ // formSheet's content for sizing (RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME in core/constants.ts).
422
+ const contentWrapper = hostElement(
423
+ RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
424
+ () => livePlan().contentWrapperProps,
425
+ );
426
+ insert(
427
+ contentWrapper,
428
+ createComponent(NavigationScopeProvider, {
429
+ value: scope,
430
+ // The screen component is created HERE, inside the Provider's own children resolution,
431
+ // not in a memo declared above it - a Solid computation captures its owner at CREATION,
432
+ // so a component built outside this getter would run with the enclosing owner and see no
433
+ // navigation scope at all (useRoute would throw). Reading `component()` tracked makes the
434
+ // getter the rebuild boundary for a component swap; `untrack` around the build keeps the
435
+ // screen's own prop reads out of that boundary. No route/navigation props: the screen
436
+ // reads both through the primitives off the scope provided here.
437
+ get children(): JSX.Element {
438
+ const Component = component();
439
+ return untrack(() => createComponent(Component, {}));
440
+ },
441
+ }),
442
+ );
443
+
444
+ // The header config (and its RNSSearchBar child, when the screen has one) is a pure
445
+ // Descriptor leaf, so it goes through the shared bridge verbatim, exactly like React's and
446
+ // Vue's do. Its child COUNT is part of `shape` above, which is what keeps the bridge's
447
+ // shape-stability contract satisfied when an app adds or removes headerSearchBarOptions.
448
+ const headerConfig = descriptorToSolid(() => livePlan().headerConfig);
449
+
450
+ const screen = hostElement(
451
+ current.viewName,
452
+ () => livePlan().screenProps,
453
+ );
454
+ if (!current.inModal) {
455
+ insertNode(screen, headerConfig);
456
+ insertNode(screen, contentWrapper);
457
+ return screen;
458
+ }
459
+
460
+ // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
461
+ // RNSScreenStack/RNSScreen purely to host the native header bar. Skipping this leaves
462
+ // RNSScreenStackHeaderConfig with no navigation controller to attach to, so the header
463
+ // silently never renders.
464
+ const innerStack = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => ({
465
+ style: livePlan().innerStackStyle,
466
+ }));
467
+ // activityState mirrors the outer Screen's - RNSScreen.mm treats an unset/inactive nested
468
+ // screen as not yet pushed, leaving it parked at its pre-push transition position (off past
469
+ // the bottom edge) instead of its real, presented frame.
470
+ const innerScreen = hostElement(RNS_SCREEN_VIEW_NAME, () => ({
471
+ style: livePlan().innerScreenStyle,
472
+ activityState: livePlan().activityState,
473
+ }));
474
+ insertNode(innerScreen, headerConfig);
475
+ insertNode(innerScreen, contentWrapper);
476
+ insertNode(innerStack, innerScreen);
477
+ insertNode(screen, innerStack);
478
+ return screen;
479
+ }
480
+
481
+ return createMemo(() => {
482
+ const current = shape();
483
+ if (current === undefined) return undefined;
484
+ return untrack(() => buildScreen(current));
485
+ });
486
+ }
487
+
488
+ // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
489
+ // native signal that the WHOLE push/pop animation finished, as opposed to the per-screen
490
+ // onAppear/onDisappear. Kept behind DEBUG, never removed.
491
+ const stackProps = resolveStackProps({
492
+ passthrough: {
493
+ [STACK_ON_FINISH_TRANSITIONING]: () =>
494
+ dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
495
+ },
496
+ });
497
+
498
+ const root = hostElement(RNS_SCREEN_STACK_VIEW_NAME, () => stackProps);
499
+ // The <For> component is created EAGERLY and handed to `insert` as a value, never as
500
+ // `() => createComponent(For, …)`. The latter would read `each` inside insert's own render
501
+ // effect, so every route change would re-create the whole list instead of letting For's keyed
502
+ // mapping reuse the screens it already built.
503
+ insert(
504
+ root,
505
+ createComponent(For, {
506
+ get each(): readonly string[] {
507
+ return routeKeys();
508
+ },
509
+ children: renderRoute,
510
+ }),
511
+ );
512
+
513
+ onCleanup(() => emitters.clear());
514
+
515
+ // `props.children` is READ here and nowhere else: reading it is what CREATES the markers, and
516
+ // they must be created inside this Provider so their collectStackScreen() finds the collector on
517
+ // the owner chain. They paint nothing, so the array is effectively just `root` - and `root` is a
518
+ // pre-built node reference, so a marker list that changes re-runs this getter without disturbing
519
+ // the navigator tree.
520
+ return createComponent(ScreenCollectorProvider, {
521
+ value: collector,
522
+ get children(): JSX.Element {
523
+ return [props.children, root];
524
+ },
525
+ });
526
+ }
527
+
528
+ export const Stack = Object.assign(StackImpl, { Screen });
@@ -0,0 +1,21 @@
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
+
4
+ import type { Component } from 'solid-js';
5
+ import type { IRoute, ITabNavigatorHandle, ITabOptions } from '../core';
6
+
7
+ export type ITabScreenOptionsArgs = {
8
+ route: IRoute<unknown>;
9
+ navigation: ITabNavigatorHandle;
10
+ };
11
+
12
+ export type ITabScreenOptionsResolver = (
13
+ args: ITabScreenOptionsArgs,
14
+ ) => ITabOptions;
15
+
16
+ export type ITabScreenProps = {
17
+ name: string;
18
+ component: Component;
19
+ options?: ITabOptions | ITabScreenOptionsResolver;
20
+ initialParams?: unknown;
21
+ };