@symbiote-native/navigation 2.0.3 → 2.1.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 (155) hide show
  1. package/build/angular/drawer/index.js +6 -4
  2. package/build/angular/stack/index.d.ts +2 -0
  3. package/build/angular/stack/index.js +23 -3
  4. package/build/angular/tabs/index.d.ts +1 -1
  5. package/build/angular/tabs/index.js +20 -7
  6. package/build/core/index.d.ts +2 -1
  7. package/build/core/index.js +5 -1
  8. package/build/core/stack-router-state/index.d.ts +2 -0
  9. package/build/core/stack-router-state/index.js +44 -0
  10. package/build/core/tab-router-state/index.d.ts +1 -0
  11. package/build/core/tab-router-state/index.js +19 -0
  12. package/build/react/drawer/index.js +23 -13
  13. package/build/react/stack/index.js +13 -2
  14. package/build/react/tabs/index.js +23 -9
  15. package/build/svelte/attachments.d.ts +5 -0
  16. package/build/svelte/attachments.js +61 -0
  17. package/build/svelte/descriptor-subtree.d.ts +3 -0
  18. package/build/svelte/descriptor-subtree.js +37 -0
  19. package/build/svelte/drawer/drawer-props.d.ts +14 -0
  20. package/build/svelte/drawer/drawer-props.js +3 -0
  21. package/build/svelte/drawer/index.d.ts +10 -0
  22. package/build/svelte/drawer/index.js +6 -0
  23. package/build/svelte/drawer/index.svelte +421 -0
  24. package/build/svelte/drawer/index.svelte.d.ts +10 -0
  25. package/build/svelte/drawer-screen-props.d.ts +13 -0
  26. package/build/svelte/drawer-screen-props.js +4 -0
  27. package/build/svelte/drawer-screen.svelte +24 -0
  28. package/build/svelte/drawer-screen.svelte.d.ts +4 -0
  29. package/build/svelte/index.d.ts +21 -0
  30. package/build/svelte/index.js +20 -0
  31. package/build/svelte/linking.svelte.d.ts +2 -0
  32. package/build/svelte/linking.svelte.js +55 -0
  33. package/build/svelte/navigation-context.d.ts +14 -0
  34. package/build/svelte/navigation-context.js +44 -0
  35. package/build/svelte/navigation-scope.svelte +24 -0
  36. package/build/svelte/navigation-scope.svelte.d.ts +9 -0
  37. package/build/svelte/registry-host.d.ts +1 -0
  38. package/build/svelte/registry-host.js +26 -0
  39. package/build/svelte/runes/index.d.ts +12 -0
  40. package/build/svelte/runes/index.js +12 -0
  41. package/build/svelte/runes/use-drawer-navigation.svelte.d.ts +6 -0
  42. package/build/svelte/runes/use-drawer-navigation.svelte.js +19 -0
  43. package/build/svelte/runes/use-focus-effect.svelte.d.ts +1 -0
  44. package/build/svelte/runes/use-focus-effect.svelte.js +31 -0
  45. package/build/svelte/runes/use-is-focused.svelte.d.ts +3 -0
  46. package/build/svelte/runes/use-is-focused.svelte.js +34 -0
  47. package/build/svelte/runes/use-navigation-state.svelte.d.ts +4 -0
  48. package/build/svelte/runes/use-navigation-state.svelte.js +39 -0
  49. package/build/svelte/runes/use-navigation.svelte.d.ts +9 -0
  50. package/build/svelte/runes/use-navigation.svelte.js +22 -0
  51. package/build/svelte/runes/use-route.svelte.d.ts +4 -0
  52. package/build/svelte/runes/use-route.svelte.js +12 -0
  53. package/build/svelte/runes/use-stack-navigation.svelte.d.ts +6 -0
  54. package/build/svelte/runes/use-stack-navigation.svelte.js +22 -0
  55. package/build/svelte/runes/use-tab-navigation.svelte.d.ts +6 -0
  56. package/build/svelte/runes/use-tab-navigation.svelte.js +19 -0
  57. package/build/svelte/screen-props.d.ts +21 -0
  58. package/build/svelte/screen-props.js +6 -0
  59. package/build/svelte/screen-registry.d.ts +17 -0
  60. package/build/svelte/screen-registry.js +57 -0
  61. package/build/svelte/screen.svelte +28 -0
  62. package/build/svelte/screen.svelte.d.ts +4 -0
  63. package/build/svelte/stack/index.d.ts +14 -0
  64. package/build/svelte/stack/index.js +7 -0
  65. package/build/svelte/stack/index.svelte +224 -0
  66. package/build/svelte/stack/index.svelte.d.ts +15 -0
  67. package/build/svelte/stack/stack-props.d.ts +20 -0
  68. package/build/svelte/stack/stack-props.js +4 -0
  69. package/build/svelte/stack/stack-screen.svelte +148 -0
  70. package/build/svelte/stack/stack-screen.svelte.d.ts +4 -0
  71. package/build/svelte/tab-screen-props.d.ts +13 -0
  72. package/build/svelte/tab-screen-props.js +4 -0
  73. package/build/svelte/tab-screen.svelte +26 -0
  74. package/build/svelte/tab-screen.svelte.d.ts +4 -0
  75. package/build/svelte/tabs/index.d.ts +8 -0
  76. package/build/svelte/tabs/index.js +6 -0
  77. package/build/svelte/tabs/index.svelte +247 -0
  78. package/build/svelte/tabs/index.svelte.d.ts +8 -0
  79. package/build/svelte/tabs/tab-props.d.ts +7 -0
  80. package/build/svelte/tabs/tab-props.js +3 -0
  81. package/build/vue/drawer/index.js +6 -5
  82. package/build/vue/stack/index.js +26 -12
  83. package/build/vue/tabs/index.js +48 -35
  84. package/build-ngc/angular/drawer/index.js +9 -7
  85. package/build-ngc/angular/drawer/index.js.map +1 -1
  86. package/build-ngc/angular/drawer-screen.directive.js +3 -3
  87. package/build-ngc/angular/navigation-context.service.js +3 -3
  88. package/build-ngc/angular/navigation-scope.directive.js +3 -3
  89. package/build-ngc/angular/screen.directive.js +3 -3
  90. package/build-ngc/angular/search-bar-ref.directive.js +3 -3
  91. package/build-ngc/angular/stack/index.d.ts +2 -0
  92. package/build-ngc/angular/stack/index.js +26 -6
  93. package/build-ngc/angular/stack/index.js.map +1 -1
  94. package/build-ngc/angular/tab-screen.directive.js +3 -3
  95. package/build-ngc/angular/tabs/index.d.ts +1 -1
  96. package/build-ngc/angular/tabs/index.js +23 -10
  97. package/build-ngc/angular/tabs/index.js.map +1 -1
  98. package/build-ngc/core/index.d.ts +2 -1
  99. package/build-ngc/core/index.js +5 -1
  100. package/build-ngc/core/index.js.map +1 -1
  101. package/build-ngc/core/stack-router-state/index.d.ts +2 -0
  102. package/build-ngc/core/stack-router-state/index.js +45 -0
  103. package/build-ngc/core/stack-router-state/index.js.map +1 -0
  104. package/build-ngc/core/tab-router-state/index.d.ts +1 -0
  105. package/build-ngc/core/tab-router-state/index.js +19 -0
  106. package/build-ngc/core/tab-router-state/index.js.map +1 -1
  107. package/package.json +28 -14
  108. package/src/angular/drawer/index.ts +6 -4
  109. package/src/angular/stack/index.ts +23 -2
  110. package/src/angular/tabs/index.ts +19 -5
  111. package/src/core/index.ts +11 -1
  112. package/src/core/stack-router-state/index.ts +49 -0
  113. package/src/core/tab-router-state/index.ts +25 -0
  114. package/src/react/drawer/index.ts +28 -13
  115. package/src/react/stack/index.ts +15 -1
  116. package/src/react/tabs/index.ts +43 -10
  117. package/src/svelte/attachments.ts +68 -0
  118. package/src/svelte/descriptor-subtree.ts +49 -0
  119. package/src/svelte/drawer/drawer-props.ts +28 -0
  120. package/src/svelte/drawer/index.svelte +421 -0
  121. package/src/svelte/drawer/index.ts +10 -0
  122. package/src/svelte/drawer-screen-props.ts +20 -0
  123. package/src/svelte/drawer-screen.svelte +24 -0
  124. package/src/svelte/fabric-tree.test-helper.ts +61 -0
  125. package/src/svelte/index.ts +71 -0
  126. package/src/svelte/linking.svelte.ts +66 -0
  127. package/src/svelte/navigation-context.ts +62 -0
  128. package/src/svelte/navigation-scope.svelte +24 -0
  129. package/src/svelte/registry-host.ts +26 -0
  130. package/src/svelte/runes/index.ts +17 -0
  131. package/src/svelte/runes/use-drawer-navigation.svelte.ts +29 -0
  132. package/src/svelte/runes/use-focus-effect.svelte.ts +35 -0
  133. package/src/svelte/runes/use-is-focused.svelte.ts +38 -0
  134. package/src/svelte/runes/use-navigation-state.svelte.ts +46 -0
  135. package/src/svelte/runes/use-navigation.svelte.ts +39 -0
  136. package/src/svelte/runes/use-route.svelte.ts +16 -0
  137. package/src/svelte/runes/use-stack-navigation.svelte.ts +32 -0
  138. package/src/svelte/runes/use-tab-navigation.svelte.ts +29 -0
  139. package/src/svelte/screen-props.ts +45 -0
  140. package/src/svelte/screen-registry.ts +92 -0
  141. package/src/svelte/screen.svelte +28 -0
  142. package/src/svelte/stack/index.svelte +224 -0
  143. package/src/svelte/stack/index.ts +11 -0
  144. package/src/svelte/stack/stack-props.ts +31 -0
  145. package/src/svelte/stack/stack-screen.svelte +148 -0
  146. package/src/svelte/svelte-ambient.d.ts +6 -0
  147. package/src/svelte/svelte-compile.test-helper.ts +143 -0
  148. package/src/svelte/tab-screen-props.ts +20 -0
  149. package/src/svelte/tab-screen.svelte +26 -0
  150. package/src/svelte/tabs/index.svelte +247 -0
  151. package/src/svelte/tabs/index.ts +10 -0
  152. package/src/svelte/tabs/tab-props.ts +13 -0
  153. package/src/vue/drawer/index.ts +6 -5
  154. package/src/vue/stack/index.ts +28 -11
  155. package/src/vue/tabs/index.ts +68 -65
@@ -0,0 +1,224 @@
1
+ <script lang="ts" module>
2
+ // Stack, the Svelte lifecycle half. The route-stack transitions (navigator-state) and the
3
+ // options/props folds (screen-options, render-stack) live in @symbiote-native/navigation core,
4
+ // shared verbatim with the React/Vue/Angular entries; here Svelte supplies the lifecycle -
5
+ // `$state.raw` for the pushed-route stack (its twin of useReducer: reassign from the same pure
6
+ // reducer), a module counter for route-key generation, `export function`s for the push/pop/
7
+ // replace navigator handle (Svelte's twin of Vue's expose(), reachable via `bind:this`) - plus
8
+ // the react-native-screens chrome per route (stack-screen.svelte). Pushing/popping a route is
9
+ // an ordinary child mount/unmount: RNSScreenStack diffs its RNSScreen children natively, so no
10
+ // imperative native command is needed here at all. Neither this nor the Screen marker imports
11
+ // react-native-screens' own React components (ScreenStack.tsx et al - hooks, crashes a
12
+ // non-React adapter); the native views are driven directly through the ViewConfig ../../register
13
+ // registers. See CLAUDE.md <third_party_rn_packages_are_react_only>.
14
+ //
15
+ // HOW SCREENS ARE DISCOVERED: not by reading `children`, which Svelte hands over as an opaque
16
+ // Snippet - the markers register themselves through the context collector (../screen-registry.ts)
17
+ // while the snippet renders, and the route list derives from that. The snippet is rendered
18
+ // inside a collapsed `symbiote-text` (../registry-host.ts) so whitespace between two markers
19
+ // can never become an illegal raw-text child of a native view.
20
+
21
+ // Route keys must be unique per Stack INSTANCE (Svelte has no useId); a module counter is the
22
+ // same mechanism the Angular entry uses.
23
+ let stackInstanceCounter = 0;
24
+
25
+ const STACK_ROOT_PROPS: Record<string, unknown> = { style: { flex: 1 } };
26
+ </script>
27
+
28
+ <script lang="ts">
29
+ import type { Component } from 'svelte';
30
+ import { dlog } from '@symbiote-native/engine';
31
+ import {
32
+ NAVIGATION_EVENT_STATE,
33
+ RNS_SCREEN_STACK_VIEW_NAME,
34
+ STACK_ON_FINISH_TRANSITIONING,
35
+ createInitialNavigatorState,
36
+ createNavigationEmitter,
37
+ navigatorReducer,
38
+ reconcileStackRoutes,
39
+ resolveStackProps,
40
+ } from '../../core';
41
+ import type {
42
+ INavigationEmitter,
43
+ INavigatorAction,
44
+ INavigatorHandle,
45
+ INavigatorState,
46
+ IRoute,
47
+ } from '../../core';
48
+ import { hostProps } from '../attachments';
49
+ import { getNavigationScope } from '../navigation-context';
50
+ import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
51
+ import { setScreenCollector, toRegistry, withoutScreen } from '../screen-registry';
52
+ import type { IRegisteredScreen } from '../screen-registry';
53
+ import type { IScreenProps, ISvelteScreenOptions } from '../screen-props';
54
+ import StackScreen from './stack-screen.svelte';
55
+ import type { IStackProps } from './stack-props';
56
+
57
+ let { initialRouteName, screenOptions, children }: IStackProps = $props();
58
+
59
+ // Read BEFORE this Stack establishes its own per-screen NavigationScope - becomes the `parent`
60
+ // link a nested screen's useNavigation().getParent() walks (e.g. this Stack rendered as a Tab
61
+ // screen's content reaches that Tab via this value). undefined when this Stack is the nesting
62
+ // root. Kept as the BOX, not its unwrapped value, so every read below sees the current one.
63
+ const parentScope = getNavigationScope();
64
+
65
+ const routeIdPrefix = `stack-${(stackInstanceCounter += 1)}`;
66
+
67
+ // `$state.raw` + reassignment, never a mutated array: an entry holds a component reference (a
68
+ // plain function) behind live getters, and a deep reactive proxy would wrap both.
69
+ let screens = $state.raw<IRegisteredScreen<IScreenProps['options']>[]>([]);
70
+ setScreenCollector<IScreenProps['options']>({
71
+ kind: 'stack',
72
+ register: screen => {
73
+ screens = [...screens, screen];
74
+ },
75
+ unregister: screen => {
76
+ screens = withoutScreen(screens, screen);
77
+ },
78
+ });
79
+
80
+ const registry = $derived(toRegistry(screens));
81
+
82
+ let routeSequence = 0;
83
+ function createRoute(name: string, params: unknown): IRoute<unknown> {
84
+ routeSequence += 1;
85
+ return { key: `${routeIdPrefix}-${name}-${routeSequence}`, name, params };
86
+ }
87
+
88
+ // The pushed stack only exists once something has dispatched; until then the state derives from
89
+ // whatever the markers registered. `seededState` is a PLAIN local (not `$state`), so memoizing
90
+ // it inside the derivation below is not a state write during a derivation - it just makes the
91
+ // one route-key-allocating call happen exactly once, no matter how often the derivation re-runs
92
+ // as markers register.
93
+ let pushedState = $state.raw<INavigatorState | null>(null);
94
+ let seededState: INavigatorState | undefined;
95
+
96
+ function seedState(): INavigatorState {
97
+ if (seededState !== undefined) return seededState;
98
+ const startRouteName = initialRouteName ?? registry.keys().next().value;
99
+ if (startRouteName === undefined) {
100
+ dlog('Stack: no <Stack.Screen> children registered');
101
+ // Deliberately NOT memoized: markers may still be registering, and the next derivation run
102
+ // should get a real initial route rather than being stuck on this placeholder.
103
+ return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
104
+ }
105
+ seededState = createInitialNavigatorState(
106
+ createRoute(startRouteName, registry.get(startRouteName)?.initialParams),
107
+ );
108
+ return seededState;
109
+ }
110
+
111
+ // A `<Stack.Screen>` marker can unregister while its route is still in the pushed history (a
112
+ // marker behind an `{#if}`, a data-driven screen list), which would leave that entry with nothing
113
+ // for componentFor() to mount (reconcileStackRoutes' header). The reconciliation belongs in the
114
+ // derivation because it is PURE - it hands back the same state reference when nothing changed, so
115
+ // it writes no state and cannot re-trigger itself; `dispatch` below then persists the pruned
116
+ // history, since it reduces over this value rather than over `pushedState`.
117
+ const state = $derived(reconcileStackRoutes(pushedState ?? seedState(), [...registry.keys()]));
118
+
119
+ function dispatch(action: INavigatorAction): void {
120
+ pushedState = navigatorReducer(state, action);
121
+ }
122
+
123
+ // One emitter per route.key, created lazily the first time a route is rendered and pruned once
124
+ // it is popped off the stack (the broadcast effect below).
125
+ const emitters = new Map<string, INavigationEmitter>();
126
+ function emitterFor(routeKey: string): INavigationEmitter {
127
+ let emitter = emitters.get(routeKey);
128
+ if (emitter === undefined) {
129
+ emitter = createNavigationEmitter();
130
+ emitters.set(routeKey, emitter);
131
+ }
132
+ return emitter;
133
+ }
134
+
135
+ export function push(name: string, params?: unknown): void {
136
+ dispatch({ type: 'push', route: createRoute(name, params) });
137
+ }
138
+ export function pop(count?: number): void {
139
+ dispatch({ type: 'pop', count });
140
+ }
141
+ export function popToTop(): void {
142
+ dispatch({ type: 'popToTop' });
143
+ }
144
+ export function popTo(key: string): void {
145
+ dispatch({ type: 'popTo', key });
146
+ }
147
+ export function replace(name: string, params?: unknown): void {
148
+ dispatch({ type: 'replace', route: createRoute(name, params) });
149
+ }
150
+ export function setParams(params: unknown, key?: string): void {
151
+ dispatch({ type: 'setParams', key, params });
152
+ }
153
+ export function reset(nextState: INavigatorState): void {
154
+ dispatch({ type: 'reset', state: nextState });
155
+ }
156
+ export function canGoBack(): boolean {
157
+ return state.routes.length > 1;
158
+ }
159
+
160
+ // The same handle, as a value, for everything that needs it INSIDE this component (the options
161
+ // fold's `navigation` argument, every route's NavigationScope). `bind:this` exposes the
162
+ // `export function`s above; this object is the internal twin of that surface.
163
+ const handle: INavigatorHandle = {
164
+ push,
165
+ pop,
166
+ popToTop,
167
+ popTo,
168
+ replace,
169
+ setParams,
170
+ reset,
171
+ canGoBack,
172
+ };
173
+
174
+ function popOne(): void {
175
+ dispatch({ type: 'pop', count: 1 });
176
+ }
177
+
178
+ // Broadcasts the router state to every still-live route's emitter (useNavigationState's source)
179
+ // after each commit, and prunes emitters for routes popped off the stack. Runs as an $effect
180
+ // rather than inline in the render path (Vue's shape), so the first broadcast lands AFTER the
181
+ // initial route's own screen has mounted and subscribed.
182
+ $effect(() => {
183
+ const current = state;
184
+ for (const route of current.routes) {
185
+ emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
186
+ }
187
+ for (const routeKey of [...emitters.keys()]) {
188
+ if (!current.routes.some(route => route.key === routeKey)) emitters.delete(routeKey);
189
+ }
190
+ });
191
+
192
+ // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
193
+ // native signal that the WHOLE push/pop animation has finished (as opposed to onAppear/
194
+ // onDisappear, which are per-screen) - logging it lets the per-screen appear/disappear
195
+ // timestamps be checked against the actual transition-complete moment. Kept behind DEBUG,
196
+ // never removed.
197
+ const stackProps = resolveStackProps({
198
+ passthrough: {
199
+ [STACK_ON_FINISH_TRANSITIONING]: () => dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
200
+ },
201
+ });
202
+
203
+ function optionsFor(route: IRoute<unknown>): ISvelteScreenOptions {
204
+ const entry = registry.get(route.name);
205
+ const own =
206
+ entry === undefined
207
+ ? undefined
208
+ : typeof entry.options === 'function'
209
+ ? entry.options({ route, navigation: handle })
210
+ : entry.options;
211
+ return { ...screenOptions, ...own };
212
+ }
213
+
214
+ function componentFor(route: IRoute<unknown>): Component | undefined {
215
+ const entry = registry.get(route.name);
216
+ if (entry === undefined) {
217
+ dlog(`Stack: no screen registered for route name "${route.name}"`);
218
+ return undefined;
219
+ }
220
+ return entry.component;
221
+ }
222
+ </script>
223
+
224
+ <symbiote-view p={STACK_ROOT_PROPS}><symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>{@render children?.()}</symbiote-text><svelte:element this={RNS_SCREEN_STACK_VIEW_NAME} {@attach hostProps(stackProps)}>{#each state.routes as route, index (route.key)}{@const screenComponent = componentFor(route)}{#if screenComponent !== undefined}<StackScreen {route} {index} routeCount={state.routes.length} options={optionsFor(route)} navigation={handle} emitter={emitterFor(route.key)} parentScope={parentScope?.current} component={screenComponent} onPopRequested={popOne} />{/if}{/each}</svelte:element></symbiote-view>
@@ -0,0 +1,11 @@
1
+ // `Stack.Screen` alongside the standalone `Screen` export, matching React's and Vue's barrels.
2
+ // A compiled Svelte component is an ordinary function object, so attaching the marker to it is
3
+ // the same Object.assign the other entries do - and `<Stack.Screen />` resolves through Svelte's
4
+ // member-expression component tags (verified against the real compiler).
5
+ import StackImpl from './index.svelte';
6
+ import Screen from '../screen.svelte';
7
+
8
+ export const Stack = Object.assign(StackImpl, { Screen });
9
+
10
+ export type { INavigatorHandle } from '../../core';
11
+ export type { IStackProps } from './stack-props';
@@ -0,0 +1,31 @@
1
+ // Stack's own public prop surface, and the internal per-route prop surface stack-screen.svelte
2
+ // takes. Both live in a plain `.ts` file for the tsc-cannot-see-a-`.svelte`-named-export reason
3
+ // ../screen-props.ts documents.
4
+
5
+ import type { Component, Snippet } from 'svelte';
6
+ import type { INavigationEmitter, INavigatorHandle, IRoute } from '../../core';
7
+ import type { INavigationScopeValue } from '../navigation-context';
8
+ import type { ISvelteScreenOptions } from '../screen-props';
9
+
10
+ // React's `children?: ReactNode` / Vue's default slot become a Svelte Snippet holding the
11
+ // `<Stack.Screen>` markers.
12
+ export type IStackProps = {
13
+ initialRouteName?: string;
14
+ screenOptions?: ISvelteScreenOptions;
15
+ children?: Snippet;
16
+ };
17
+
18
+ export type IStackScreenProps = {
19
+ route: IRoute<unknown>;
20
+ index: number;
21
+ routeCount: number;
22
+ options: ISvelteScreenOptions;
23
+ navigation: INavigatorHandle;
24
+ emitter: INavigationEmitter;
25
+ // The ambient scope the Stack itself read on mount (undefined at the nesting root) - becomes
26
+ // the `parent` link a nested screen's useNavigation().getParent() walks.
27
+ parentScope: INavigationScopeValue | undefined;
28
+ component: Component;
29
+ // Fired by RNSScreen's own onDismissed / header back button; the Stack pops one route.
30
+ onPopRequested: () => void;
31
+ };
@@ -0,0 +1,148 @@
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 = { defaultHeaderBackTitleVisible: true };
9
+
10
+ // react-native-screens' RNSScreenStackHeaderConfig.mm requires every header child to be an
11
+ // RNSScreenStackHeaderSubview; `type: 'searchBar'` is how it knows which slot this one fills.
12
+ const HEADER_SUBVIEW_PROPS: Record<string, unknown> = { type: 'searchBar' };
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ // One mounted route's native chrome. Split out of index.svelte so each route owns its own plan
17
+ // derivation and its own attachments, torn down by an ordinary component unmount when the route
18
+ // is popped - the Svelte equivalent of Vue's per-route render-loop closure.
19
+ //
20
+ // Every react-native-screens view here goes through `<svelte:element this={'RNSScreen'}>`
21
+ // rather than a literal tag: their Fabric names are capitalized and un-hyphenated, so a literal
22
+ // tag would parse as a COMPONENT reference in a Svelte template. Their props ride an
23
+ // `{@attach hostProps(...)}` attachment rather than an attribute, because a dynamic tag
24
+ // compiles through Svelte's generic setAttribute path and never the custom-element property-SET
25
+ // path the object bag depends on - see ../attachments.ts for the full reasoning.
26
+ //
27
+ // The whole per-route tree is packed edge-to-edge with zero whitespace between sibling tags:
28
+ // svelte-adapter-dom-shim skill §16, where a stray space would become a real RCTRawText child
29
+ // of a react-native-screens view.
30
+ import { Platform, dlog } from '@symbiote-native/engine';
31
+ import {
32
+ NAVIGATION_EVENT_BLUR,
33
+ NAVIGATION_EVENT_FOCUS,
34
+ RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME,
35
+ RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME,
36
+ RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME,
37
+ RNS_SCREEN_STACK_VIEW_NAME,
38
+ RNS_SCREEN_VIEW_NAME,
39
+ RNS_SEARCH_BAR_VIEW_NAME,
40
+ SCREEN_ON_APPEAR,
41
+ SCREEN_ON_DISAPPEAR,
42
+ SCREEN_ON_DISMISSED,
43
+ SCREEN_ON_HEADER_BACK_BUTTON_CLICKED,
44
+ SCREEN_ON_WILL_APPEAR,
45
+ SCREEN_ON_WILL_DISAPPEAR,
46
+ buildSearchBarPassthrough,
47
+ resolveScreenRenderPlan,
48
+ } from '../../core';
49
+ import type { IScreenRenderPlan } from '../../core';
50
+ import { hostProps, searchBarRef } from '../attachments';
51
+ import NavigationScope from '../navigation-scope.svelte';
52
+ import type { INavigationScopeValue } from '../navigation-context';
53
+ import type { IStackScreenProps } from './stack-props';
54
+
55
+ let {
56
+ route,
57
+ index,
58
+ routeCount,
59
+ options,
60
+ navigation,
61
+ emitter,
62
+ parentScope,
63
+ // Destructured under a capitalized name because a Svelte template resolves a component tag
64
+ // only from a capitalized identifier.
65
+ component: ScreenComponent,
66
+ onPopRequested,
67
+ }: IStackScreenProps = $props();
68
+
69
+ const searchBarOptions = $derived(options.headerSearchBarOptions);
70
+
71
+ const plan = $derived.by<IScreenRenderPlan>(() =>
72
+ resolveScreenRenderPlan({
73
+ screenId: route.key,
74
+ index,
75
+ routeCount,
76
+ options,
77
+ platform: NAVIGATOR_PLATFORM,
78
+ isAndroid: Platform.OS === 'android',
79
+ screenPassthrough: {
80
+ [SCREEN_ON_DISMISSED]: onPopRequested,
81
+ [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: onPopRequested,
82
+ // onAppear/onDisappear are the definitive visibility boundary (post-transition-
83
+ // animation), so 'focus'/'blur' fire exactly once per transition; onWillAppear/
84
+ // onWillDisappear fire BEFORE the animation runs, so wiring them to emit() too would
85
+ // double-invoke useFocusEffect per transition - they only get a debug log here.
86
+ [SCREEN_ON_WILL_APPEAR]: () =>
87
+ dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
88
+ [SCREEN_ON_APPEAR]: () => {
89
+ dlog(`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`);
90
+ emitter.emit(NAVIGATION_EVENT_FOCUS);
91
+ },
92
+ [SCREEN_ON_WILL_DISAPPEAR]: () =>
93
+ dlog(`Stack: route "${route.name}" will disappear at t=${Date.now()}`),
94
+ [SCREEN_ON_DISAPPEAR]: () => {
95
+ dlog(`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`);
96
+ emitter.emit(NAVIGATION_EVENT_BLUR);
97
+ },
98
+ },
99
+ // The imperative SearchBarCommands ref rides its OWN attachment on the RNSSearchBar leaf
100
+ // below (../attachments.ts), never this passthrough map - same split Angular uses, so no
101
+ // `ref` key can leak through to Fabric as a real prop.
102
+ searchBarPassthrough: searchBarOptions
103
+ ? buildSearchBarPassthrough(searchBarOptions, message =>
104
+ dlog(`Stack: route "${route.name}" ${message}`),
105
+ )
106
+ : undefined,
107
+ }),
108
+ );
109
+
110
+ // Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
111
+ // values resolved onto the native RNSScreen, once per mounted route - rules a stackAnimation/
112
+ // transitionDuration mismatch against react-native-screens' own native default in or out. Kept
113
+ // behind DEBUG, never removed.
114
+ let hasLoggedScreenProps = false;
115
+ $effect(() => {
116
+ const screenProps = plan.screenProps;
117
+ if (hasLoggedScreenProps) return;
118
+ hasLoggedScreenProps = true;
119
+ dlog(
120
+ `Stack: route "${route.name}" resolved screen props ` +
121
+ `stackAnimation=${String(screenProps.stackAnimation)} ` +
122
+ `stackPresentation=${String(screenProps.stackPresentation)} ` +
123
+ `transitionDuration=${String(screenProps.transitionDuration)} ` +
124
+ `gestureEnabled=${String(screenProps.gestureEnabled)} at t=${Date.now()}`,
125
+ );
126
+ });
127
+
128
+ // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
129
+ // RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's comment
130
+ // in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no navigation
131
+ // controller to attach to, so the header silently never renders. activityState mirrors the
132
+ // outer screen's own value - RNSScreen.mm treats an unset/inactive nested screen as not yet
133
+ // pushed, leaving it parked at its pre-push transition position.
134
+ const innerStackProps = $derived<Record<string, unknown>>({ style: plan.innerStackStyle });
135
+ const innerScreenProps = $derived<Record<string, unknown>>({
136
+ style: plan.innerScreenStyle,
137
+ activityState: plan.activityState,
138
+ });
139
+
140
+ const scopeValue = $derived<INavigationScopeValue>({
141
+ route,
142
+ navigation,
143
+ emitter,
144
+ parent: parentScope,
145
+ });
146
+ </script>
147
+
148
+ {#snippet chrome()}<svelte:element this={RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME} {@attach hostProps(plan.headerConfig.props)}>{#if plan.searchBarProps !== undefined}<svelte:element this={RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME} {@attach hostProps(HEADER_SUBVIEW_PROPS)}><svelte:element this={RNS_SEARCH_BAR_VIEW_NAME} {@attach hostProps(plan.searchBarProps)} {@attach searchBarRef(searchBarOptions?.ref)}></svelte:element></svelte:element>{/if}</svelte:element><svelte:element this={RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME} {@attach hostProps(plan.contentWrapperProps)}><NavigationScope value={scopeValue}><ScreenComponent /></NavigationScope></svelte:element>{/snippet}<svelte:element this={plan.screenViewName} {@attach hostProps(plan.screenProps)}>{#if plan.inModal}<svelte:element this={RNS_SCREEN_STACK_VIEW_NAME} {@attach hostProps(innerStackProps)}><svelte:element this={RNS_SCREEN_VIEW_NAME} {@attach hostProps(innerScreenProps)}>{@render chrome()}</svelte:element></svelte:element>{:else}{@render chrome()}{/if}</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,143 @@
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 = { generate: 'client', fragments: 'tree', css: 'external' } as const;
40
+ // The §16 tell: a single-space text entry inside a compiled `from_tree([...])` array, i.e. real
41
+ // whitespace between two sibling nodes that became a real RCTRawText engine node.
42
+ const STRAY_WHITESPACE_PATTERN = /,\s*'\s+'\s*,/g;
43
+ const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
44
+
45
+ export type ISvelteHarness = {
46
+ // Compile a real `.svelte` file plus everything it imports; returns the compiled entry path.
47
+ compileFile(sveltePath: string): string;
48
+ // Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
49
+ compileSource(dir: string, name: string, source: string): string;
50
+ // The number of stray single-space text entries across everything compiled so far - must be 0.
51
+ strayWhitespaceCount(): number;
52
+ cleanup(): void;
53
+ };
54
+
55
+ export function createSvelteHarness(
56
+ id: string,
57
+ aliases: Readonly<Record<string, string>> = {},
58
+ ): ISvelteHarness {
59
+ const written: string[] = [];
60
+ const compiledBySource = new Map<string, string>();
61
+ let strayWhitespace = 0;
62
+
63
+ function outPathFor(sveltePath: string): string {
64
+ const stem = basename(sveltePath).replace(/\./g, '-');
65
+ return join(dirname(sveltePath), `.smoke-compiled-${id}-${stem}.mjs`);
66
+ }
67
+
68
+ // `./` is prepended unless the path already walks up: every compiled file's basename STARTS
69
+ // with a dot (`.smoke-compiled-...`), so a bare "starts with '.'" check would leave it looking
70
+ // like a bare package specifier to the resolver.
71
+ function relativeSpecifier(fromFile: string, target: string): string {
72
+ const specifier = relative(dirname(fromFile), target);
73
+ return specifier.startsWith('..') ? specifier : `./${specifier}`;
74
+ }
75
+
76
+ function rewrite(code: string, outPath: string): string {
77
+ let rewritten = code.replace(
78
+ SVELTE_SPECIFIER_PATTERN,
79
+ (_match, quote: string, spec: string) => {
80
+ const childSource = resolve(dirname(outPath), spec);
81
+ const childOut = compileFile(childSource);
82
+ return `${quote}${relativeSpecifier(outPath, childOut)}${quote}`;
83
+ },
84
+ );
85
+ for (const [specifier, target] of Object.entries(aliases)) {
86
+ rewritten = rewritten.split(`'${specifier}'`).join(`'${relativeSpecifier(outPath, target)}'`);
87
+ rewritten = rewritten.split(`"${specifier}"`).join(`"${relativeSpecifier(outPath, target)}"`);
88
+ }
89
+ return rewritten;
90
+ }
91
+
92
+ function emit(sveltePath: string, source: string, isRuneModule: boolean): string {
93
+ const outPath = outPathFor(sveltePath);
94
+ // Registered BEFORE compiling so a cycle between two components terminates.
95
+ compiledBySource.set(sveltePath, outPath);
96
+ written.push(outPath);
97
+ const filename = basename(sveltePath);
98
+ const code = isRuneModule
99
+ ? compileSvelteModuleFile(source, `${filename}.ts`)
100
+ : compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
101
+ strayWhitespace += (code.match(STRAY_WHITESPACE_PATTERN) ?? []).length;
102
+ writeFileSync(outPath, rewrite(code, outPath));
103
+ return outPath;
104
+ }
105
+
106
+ // A `./x.svelte` specifier is a COMPONENT when `x.svelte` exists on disk and a rune MODULE
107
+ // when `x.svelte.ts` does - the same two-way split the Metro transformer makes on the real
108
+ // filename.
109
+ function compileFile(sveltePath: string): string {
110
+ const cached = compiledBySource.get(sveltePath);
111
+ if (cached !== undefined) return cached;
112
+ const runeModulePath = `${sveltePath}.ts`;
113
+ if (!existsSync(sveltePath) && existsSync(runeModulePath)) {
114
+ return emit(sveltePath, readFileSync(runeModulePath, 'utf8'), true);
115
+ }
116
+ return emit(sveltePath, readFileSync(sveltePath, 'utf8'), false);
117
+ }
118
+
119
+ return {
120
+ compileFile,
121
+ compileSource(dir, name, source) {
122
+ return emit(join(dir, `${name}.svelte`), source, false);
123
+ },
124
+ strayWhitespaceCount: () => strayWhitespace,
125
+ cleanup() {
126
+ for (const file of written) rmSync(file, { force: true });
127
+ written.length = 0;
128
+ compiledBySource.clear();
129
+ },
130
+ };
131
+ }
132
+
133
+ // The compiled module's default export, narrowed without a cast so a test never silently mounts
134
+ // `undefined`.
135
+ export async function loadComponent(compiledPath: string): Promise<unknown> {
136
+ // pathToFileURL rather than a `file://${...}` template: the literal form makes Vite's import
137
+ // analyzer log an "Invalid file URL" warning for the un-substituted placeholder.
138
+ const mod: unknown = await import(pathToFileURL(compiledPath).href);
139
+ if (mod === null || typeof mod !== 'object' || !('default' in mod)) {
140
+ throw new Error(`${compiledPath} produced no default export`);
141
+ }
142
+ return mod.default;
143
+ }
@@ -0,0 +1,20 @@
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 = (args: ITabScreenOptionsArgs) => ITabOptions;
14
+
15
+ export type ITabScreenProps = {
16
+ name: string;
17
+ component: Component;
18
+ options?: ITabOptions | ITabScreenOptionsResolver;
19
+ initialParams?: unknown;
20
+ };
@@ -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>