@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
@@ -17,7 +17,10 @@ function isFabricNode(value: unknown): value is IFabricNode {
17
17
  return typeof value === 'object' && value !== null;
18
18
  }
19
19
 
20
- export function walkLive(root: unknown, visit: (node: IFabricNode) => void): void {
20
+ export function walkLive(
21
+ root: unknown,
22
+ visit: (node: IFabricNode) => void,
23
+ ): void {
21
24
  if (!isFabricNode(root)) return;
22
25
  visit(root);
23
26
  for (const child of root.children ?? []) walkLive(child, visit);
@@ -31,7 +34,10 @@ export function findAllLive(root: unknown, viewName: string): IFabricNode[] {
31
34
  return found;
32
35
  }
33
36
 
34
- export function findLive(root: unknown, viewName: string): IFabricNode | undefined {
37
+ export function findLive(
38
+ root: unknown,
39
+ viewName: string,
40
+ ): IFabricNode | undefined {
35
41
  return findAllLive(root, viewName)[0];
36
42
  }
37
43
 
@@ -39,7 +45,10 @@ export function countLive(root: unknown, viewName: string): number {
39
45
  return findAllLive(root, viewName).length;
40
46
  }
41
47
 
42
- export function findLiveByTestId(root: unknown, testID: string): IFabricNode | undefined {
48
+ export function findLiveByTestId(
49
+ root: unknown,
50
+ testID: string,
51
+ ): IFabricNode | undefined {
43
52
  let found: IFabricNode | undefined;
44
53
  walkLive(root, node => {
45
54
  if (found === undefined && node.props?.testID === testID) found = node;
@@ -47,6 +56,31 @@ export function findLiveByTestId(root: unknown, testID: string): IFabricNode | u
47
56
  return found;
48
57
  }
49
58
 
59
+ const TEXT_CONTAINER_VIEW_NAMES = new Set(['RCTText', 'RCTVirtualText']);
60
+
61
+ // Every raw text committed under a parent that cannot hold one - an invalid Fabric child.
62
+ // The gap Svelte leaves between two sibling tags compiles to a ' ' text node, and the shim
63
+ // drops it exactly when the parent takes no raw text (dom-shim/text.ts, svelte-adapter-dom-shim
64
+ // §16b); inside an RCTText the same string is a real word separator and must survive. So the
65
+ // parent, not the string, is what makes a raw text legal, and this returns the ones that are
66
+ // not. Formatted `viewName > "text"` so a failure names the offender.
67
+ export function rawTextsOutsideTextContainer(root: unknown): string[] {
68
+ const found: string[] = [];
69
+ const visit = (node: unknown, parent: IFabricNode | undefined): void => {
70
+ if (!isFabricNode(node)) return;
71
+ const parentName = parent?.viewName;
72
+ if (
73
+ node.viewName === 'RCTRawText' &&
74
+ (parentName === undefined || !TEXT_CONTAINER_VIEW_NAMES.has(parentName))
75
+ ) {
76
+ found.push(`${String(parentName)} > ${JSON.stringify(node.props?.text)}`);
77
+ }
78
+ for (const child of node.children ?? []) visit(child, node);
79
+ };
80
+ visit(root, undefined);
81
+ return found;
82
+ }
83
+
50
84
  // A depth-indented `viewName` outline, for asserting the exact committed SHAPE (which is what
51
85
  // catches a stray whitespace text node, an extra wrapper, or a missing header).
52
86
  export function outline(root: unknown): string[] {
@@ -16,7 +16,11 @@ export type {
16
16
  ISvelteScreenOptions,
17
17
  ISvelteSearchBarOptions,
18
18
  } from './screen-props';
19
- export type { IScreenOptions, IStackAnimation, IStackPresentation } from '../core';
19
+ export type {
20
+ IScreenOptions,
21
+ IStackAnimation,
22
+ IStackPresentation,
23
+ } from '../core';
20
24
 
21
25
  export {
22
26
  useNavigation,
@@ -34,7 +38,10 @@ export type {
34
38
  ITabNavigationHandle,
35
39
  IDrawerNavigationHandle,
36
40
  } from './runes';
37
- export type { INavigationScopeValue, IAnyNavigatorHandle } from './navigation-context';
41
+ export type {
42
+ INavigationScopeValue,
43
+ IAnyNavigatorHandle,
44
+ } from './navigation-context';
38
45
 
39
46
  export { useLinkingIntegration } from './linking.svelte';
40
47
  export type { ILinkingConfig, IScreenLinkingConfig } from '../core';
@@ -68,4 +75,9 @@ export type {
68
75
  IDrawerScreenOptionsResolver,
69
76
  IDrawerScreenProps,
70
77
  } from './drawer-screen-props';
71
- export type { IDrawerOptions, IDrawerScreenOptions, IDrawerType, IDrawerPosition } from '../core';
78
+ export type {
79
+ IDrawerOptions,
80
+ IDrawerScreenOptions,
81
+ IDrawerType,
82
+ IDrawerPosition,
83
+ } from '../core';
@@ -34,7 +34,10 @@ export function useLinkingIntegration(
34
34
 
35
35
  let cancelled = false;
36
36
 
37
- const applyRoute = (url: string, dispatch: (name: string, params?: unknown) => void): void => {
37
+ const applyRoute = (
38
+ url: string,
39
+ dispatch: (name: string, params?: unknown) => void,
40
+ ): void => {
38
41
  const route = resolveRouteFromUrl(config, url);
39
42
  if (route === null) {
40
43
  dlog(`useLinkingIntegration: no route resolved for "${url}"`);
@@ -53,10 +56,13 @@ export function useLinkingIntegration(
53
56
  dlog(`useLinkingIntegration: getInitialURL failed: ${String(error)}`);
54
57
  });
55
58
 
56
- const subscription: IEventSubscription = Linking.addEventListener('url', (event: IUrlEvent) => {
57
- dlog(`useLinkingIntegration: url event "${event.url}"`);
58
- applyRoute(event.url, handle.push);
59
- });
59
+ const subscription: IEventSubscription = Linking.addEventListener(
60
+ 'url',
61
+ (event: IUrlEvent) => {
62
+ dlog(`useLinkingIntegration: url event "${event.url}"`);
63
+ applyRoute(event.url, handle.push);
64
+ },
65
+ );
60
66
 
61
67
  return () => {
62
68
  cancelled = true;
@@ -12,7 +12,10 @@
12
12
  import { setNavigationScope } from './navigation-context';
13
13
  import type { INavigationScopeValue } from './navigation-context';
14
14
 
15
- let { value, children }: { value: INavigationScopeValue; children?: Snippet } = $props();
15
+ let {
16
+ value,
17
+ children,
18
+ }: { value: INavigationScopeValue; children?: Snippet } = $props();
16
19
 
17
20
  setNavigationScope({
18
21
  get current(): INavigationScopeValue {
@@ -8,7 +8,9 @@ import type { INavigationHandle } from './use-navigation.svelte';
8
8
  export type IDrawerNavigationHandle = IDrawerNavigatorHandle &
9
9
  Pick<INavigationHandle, 'addListener' | 'getParent'>;
10
10
 
11
- export function useDrawerNavigation(): { readonly current: IDrawerNavigationHandle } {
11
+ export function useDrawerNavigation(): {
12
+ readonly current: IDrawerNavigationHandle;
13
+ } {
12
14
  const navigation = useNavigation();
13
15
  const narrowed = $derived.by<IDrawerNavigationHandle>(() => {
14
16
  const value = navigation.current;
@@ -24,8 +24,14 @@ export function useFocusEffect(effect: () => (() => void) | void): void {
24
24
  // once. The teardown also runs any still-pending cleanup, matching Vue's onUnmounted.
25
25
  $effect(() => {
26
26
  const { emitter } = untrack(() => scope.current);
27
- const unsubscribeFocus = emitter.addListener(NAVIGATION_EVENT_FOCUS, runEffect);
28
- const unsubscribeBlur = emitter.addListener(NAVIGATION_EVENT_BLUR, runCleanup);
27
+ const unsubscribeFocus = emitter.addListener(
28
+ NAVIGATION_EVENT_FOCUS,
29
+ runEffect,
30
+ );
31
+ const unsubscribeBlur = emitter.addListener(
32
+ NAVIGATION_EVENT_BLUR,
33
+ runCleanup,
34
+ );
29
35
  return () => {
30
36
  unsubscribeFocus();
31
37
  unsubscribeBlur();
@@ -23,7 +23,9 @@ function isNavigatorState(value: unknown): value is INavigatorState {
23
23
  return isRecord(value) && Array.isArray(value.routes);
24
24
  }
25
25
 
26
- export function useNavigationState<TResult>(selector: (state: INavigatorState) => TResult): {
26
+ export function useNavigationState<TResult>(
27
+ selector: (state: INavigatorState) => TResult,
28
+ ): {
27
29
  readonly current: TResult;
28
30
  } {
29
31
  const scope = requireNavigationScope('useNavigationState');
@@ -8,12 +8,18 @@
8
8
  // §21). A caller reads `useNavigation().current` inside a `$derived`/template/`$effect`, exactly
9
9
  // like unwrapping Vue's `ComputedRef` via `.value`.
10
10
 
11
- import type { INavigationEventListener, INavigationEventName } from '../../core';
11
+ import type {
12
+ INavigationEventListener,
13
+ INavigationEventName,
14
+ } from '../../core';
12
15
  import { requireNavigationScope } from '../navigation-context';
13
16
  import type { IAnyNavigatorHandle } from '../navigation-context';
14
17
 
15
18
  export type INavigationHandle = IAnyNavigatorHandle & {
16
- addListener: (event: INavigationEventName, listener: INavigationEventListener) => () => void;
19
+ addListener: (
20
+ event: INavigationEventName,
21
+ listener: INavigationEventListener,
22
+ ) => () => void;
17
23
  // Walks exactly ONE hop up the scope's `parent` chain to the enclosing navigator's handle -
18
24
  // e.g. a Tab screen nested inside a Stack screen calling getParent() to push a new Stack route.
19
25
  // Callers narrow the union themselves ('push' in parent, etc.). Deliberately NOT
@@ -28,7 +34,11 @@ export function useNavigation(): { readonly current: INavigationHandle } {
28
34
  const scope = requireNavigationScope('useNavigation');
29
35
  const handle = $derived.by<INavigationHandle>(() => {
30
36
  const { navigation, emitter, parent } = scope.current;
31
- return { ...navigation, addListener: emitter.addListener, getParent: () => parent?.navigation };
37
+ return {
38
+ ...navigation,
39
+ addListener: emitter.addListener,
40
+ getParent: () => parent?.navigation,
41
+ };
32
42
  });
33
43
 
34
44
  return {
@@ -11,7 +11,9 @@ import type { INavigationHandle } from './use-navigation.svelte';
11
11
  export type IStackNavigationHandle = INavigatorHandle &
12
12
  Pick<INavigationHandle, 'addListener' | 'getParent'>;
13
13
 
14
- export function useStackNavigation(): { readonly current: IStackNavigationHandle } {
14
+ export function useStackNavigation(): {
15
+ readonly current: IStackNavigationHandle;
16
+ } {
15
17
  const navigation = useNavigation();
16
18
  const narrowed = $derived.by<IStackNavigationHandle>(() => {
17
19
  const value = navigation.current;
@@ -23,7 +23,10 @@ export type ISvelteSearchBarOptions = ISearchBarOptions & {
23
23
  ref?: { current: ISearchBarCommands | null };
24
24
  };
25
25
 
26
- export type ISvelteScreenOptions = Omit<IScreenOptions, 'headerSearchBarOptions'> & {
26
+ export type ISvelteScreenOptions = Omit<
27
+ IScreenOptions,
28
+ 'headerSearchBarOptions'
29
+ > & {
27
30
  headerSearchBarOptions?: ISvelteSearchBarOptions;
28
31
  };
29
32
 
@@ -35,7 +38,9 @@ export type IScreenOptionsArgs = {
35
38
  navigation: INavigatorHandle;
36
39
  };
37
40
 
38
- export type IScreenOptionsResolver = (args: IScreenOptionsArgs) => ISvelteScreenOptions;
41
+ export type IScreenOptionsResolver = (
42
+ args: IScreenOptionsArgs,
43
+ ) => ISvelteScreenOptions;
39
44
 
40
45
  export type IScreenProps = {
41
46
  name: string;
@@ -45,7 +45,9 @@ export type IScreenCollector<TOptions> = {
45
45
  const SCREEN_COLLECTOR_KEY = Symbol('symbiote-navigation-screen-collector');
46
46
 
47
47
  // Called once from a navigator's own init, before it renders the children snippet.
48
- export function setScreenCollector<TOptions>(collector: IScreenCollector<TOptions>): void {
48
+ export function setScreenCollector<TOptions>(
49
+ collector: IScreenCollector<TOptions>,
50
+ ): void {
49
51
  setContext(SCREEN_COLLECTOR_KEY, collector);
50
52
  }
51
53
 
@@ -55,9 +57,13 @@ export function collectScreen<TOptions>(
55
57
  kind: INavigatorKind,
56
58
  screen: IRegisteredScreen<TOptions>,
57
59
  ): void {
58
- const collector = getContext<IScreenCollector<TOptions> | undefined>(SCREEN_COLLECTOR_KEY);
60
+ const collector = getContext<IScreenCollector<TOptions> | undefined>(
61
+ SCREEN_COLLECTOR_KEY,
62
+ );
59
63
  if (collector === undefined) {
60
- dlog(`navigation: <${kind} screen "${screen.name}"> rendered outside any navigator, ignored`);
64
+ dlog(
65
+ `navigation: <${kind} screen "${screen.name}"> rendered outside any navigator, ignored`,
66
+ );
61
67
  return;
62
68
  }
63
69
  if (collector.kind !== kind) {
@@ -48,7 +48,11 @@
48
48
  import { hostProps } from '../attachments';
49
49
  import { getNavigationScope } from '../navigation-context';
50
50
  import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
51
- import { setScreenCollector, toRegistry, withoutScreen } from '../screen-registry';
51
+ import {
52
+ setScreenCollector,
53
+ toRegistry,
54
+ withoutScreen,
55
+ } from '../screen-registry';
52
56
  import type { IRegisteredScreen } from '../screen-registry';
53
57
  import type { IScreenProps, ISvelteScreenOptions } from '../screen-props';
54
58
  import StackScreen from './stack-screen.svelte';
@@ -100,7 +104,11 @@
100
104
  dlog('Stack: no <Stack.Screen> children registered');
101
105
  // Deliberately NOT memoized: markers may still be registering, and the next derivation run
102
106
  // should get a real initial route rather than being stuck on this placeholder.
103
- return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
107
+ return createInitialNavigatorState({
108
+ key: routeIdPrefix,
109
+ name: '',
110
+ params: undefined,
111
+ });
104
112
  }
105
113
  seededState = createInitialNavigatorState(
106
114
  createRoute(startRouteName, registry.get(startRouteName)?.initialParams),
@@ -114,7 +122,9 @@
114
122
  // derivation because it is PURE - it hands back the same state reference when nothing changed, so
115
123
  // it writes no state and cannot re-trigger itself; `dispatch` below then persists the pruned
116
124
  // history, since it reduces over this value rather than over `pushedState`.
117
- const state = $derived(reconcileStackRoutes(pushedState ?? seedState(), [...registry.keys()]));
125
+ const state = $derived(
126
+ reconcileStackRoutes(pushedState ?? seedState(), [...registry.keys()]),
127
+ );
118
128
 
119
129
  function dispatch(action: INavigatorAction): void {
120
130
  pushedState = navigatorReducer(state, action);
@@ -185,7 +195,8 @@
185
195
  emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, current);
186
196
  }
187
197
  for (const routeKey of [...emitters.keys()]) {
188
- if (!current.routes.some(route => route.key === routeKey)) emitters.delete(routeKey);
198
+ if (!current.routes.some(route => route.key === routeKey))
199
+ emitters.delete(routeKey);
189
200
  }
190
201
  });
191
202
 
@@ -196,7 +207,8 @@
196
207
  // never removed.
197
208
  const stackProps = resolveStackProps({
198
209
  passthrough: {
199
- [STACK_ON_FINISH_TRANSITIONING]: () => dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
210
+ [STACK_ON_FINISH_TRANSITIONING]: () =>
211
+ dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
200
212
  },
201
213
  });
202
214
 
@@ -221,4 +233,29 @@
221
233
  }
222
234
  </script>
223
235
 
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>
236
+ <symbiote-view p={STACK_ROOT_PROPS}>
237
+ <symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>
238
+ {@render children?.()}
239
+ </symbiote-text>
240
+ <svelte:element
241
+ this={RNS_SCREEN_STACK_VIEW_NAME}
242
+ {@attach hostProps(stackProps)}
243
+ >
244
+ {#each state.routes as route, index (route.key)}
245
+ {@const screenComponent = componentFor(route)}
246
+ {#if screenComponent !== undefined}
247
+ <StackScreen
248
+ {route}
249
+ {index}
250
+ routeCount={state.routes.length}
251
+ options={optionsFor(route)}
252
+ navigation={handle}
253
+ emitter={emitterFor(route.key)}
254
+ parentScope={parentScope?.current}
255
+ component={screenComponent}
256
+ onPopRequested={popOne}
257
+ />
258
+ {/if}
259
+ {/each}
260
+ </svelte:element>
261
+ </symbiote-view>
@@ -5,7 +5,9 @@
5
5
  // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single
6
6
  // constant stands in for the per-platform injection point ISliderPlatform-style adapters use
7
7
  // elsewhere.
8
- const NAVIGATOR_PLATFORM: INavigatorPlatform = { defaultHeaderBackTitleVisible: true };
8
+ const NAVIGATOR_PLATFORM: INavigatorPlatform = {
9
+ defaultHeaderBackTitleVisible: true,
10
+ };
9
11
 
10
12
  // react-native-screens' RNSScreenStackHeaderConfig.mm requires every header child to be an
11
13
  // RNSScreenStackHeaderSubview; `type: 'searchBar'` is how it knows which slot this one fills.
@@ -24,9 +26,9 @@
24
26
  // compiles through Svelte's generic setAttribute path and never the custom-element property-SET
25
27
  // path the object bag depends on - see ../attachments.ts for the full reasoning.
26
28
  //
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.
29
+ // Whitespace between sibling tags in the per-route tree is inert: the shim drops a
30
+ // whitespace-only text node whose parent takes no raw text, so it never becomes an RCTRawText
31
+ // child of a react-native-screens view (dom-shim/text.ts, svelte-adapter-dom-shim skill §16b).
30
32
  import { Platform, dlog } from '@symbiote-native/engine';
31
33
  import {
32
34
  NAVIGATION_EVENT_BLUR,
@@ -86,13 +88,19 @@
86
88
  [SCREEN_ON_WILL_APPEAR]: () =>
87
89
  dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
88
90
  [SCREEN_ON_APPEAR]: () => {
89
- dlog(`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`);
91
+ dlog(
92
+ `Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`,
93
+ );
90
94
  emitter.emit(NAVIGATION_EVENT_FOCUS);
91
95
  },
92
96
  [SCREEN_ON_WILL_DISAPPEAR]: () =>
93
- dlog(`Stack: route "${route.name}" will disappear at t=${Date.now()}`),
97
+ dlog(
98
+ `Stack: route "${route.name}" will disappear at t=${Date.now()}`,
99
+ ),
94
100
  [SCREEN_ON_DISAPPEAR]: () => {
95
- dlog(`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`);
101
+ dlog(
102
+ `Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`,
103
+ );
96
104
  emitter.emit(NAVIGATION_EVENT_BLUR);
97
105
  },
98
106
  },
@@ -131,7 +139,9 @@
131
139
  // controller to attach to, so the header silently never renders. activityState mirrors the
132
140
  // outer screen's own value - RNSScreen.mm treats an unset/inactive nested screen as not yet
133
141
  // pushed, leaving it parked at its pre-push transition position.
134
- const innerStackProps = $derived<Record<string, unknown>>({ style: plan.innerStackStyle });
142
+ const innerStackProps = $derived<Record<string, unknown>>({
143
+ style: plan.innerStackStyle,
144
+ });
135
145
  const innerScreenProps = $derived<Record<string, unknown>>({
136
146
  style: plan.innerScreenStyle,
137
147
  activityState: plan.activityState,
@@ -145,4 +155,51 @@
145
155
  });
146
156
  </script>
147
157
 
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>
158
+ {#snippet chrome()}
159
+ <svelte:element
160
+ this={RNS_SCREEN_STACK_HEADER_CONFIG_VIEW_NAME}
161
+ {@attach hostProps(plan.headerConfig.props)}
162
+ >
163
+ {#if plan.searchBarProps !== undefined}
164
+ <svelte:element
165
+ this={RNS_SCREEN_STACK_HEADER_SUBVIEW_VIEW_NAME}
166
+ {@attach hostProps(HEADER_SUBVIEW_PROPS)}
167
+ >
168
+ <svelte:element
169
+ this={RNS_SEARCH_BAR_VIEW_NAME}
170
+ {@attach hostProps(plan.searchBarProps)}
171
+ {@attach searchBarRef(searchBarOptions?.ref)}
172
+ />
173
+ </svelte:element>
174
+ {/if}
175
+ </svelte:element>
176
+ <svelte:element
177
+ this={RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME}
178
+ {@attach hostProps(plan.contentWrapperProps)}
179
+ >
180
+ <NavigationScope value={scopeValue}>
181
+ <ScreenComponent />
182
+ </NavigationScope>
183
+ </svelte:element>
184
+ {/snippet}
185
+
186
+ <svelte:element
187
+ this={plan.screenViewName}
188
+ {@attach hostProps(plan.screenProps)}
189
+ >
190
+ {#if plan.inModal}
191
+ <svelte:element
192
+ this={RNS_SCREEN_STACK_VIEW_NAME}
193
+ {@attach hostProps(innerStackProps)}
194
+ >
195
+ <svelte:element
196
+ this={RNS_SCREEN_VIEW_NAME}
197
+ {@attach hostProps(innerScreenProps)}
198
+ >
199
+ {@render chrome()}
200
+ </svelte:element>
201
+ </svelte:element>
202
+ {:else}
203
+ {@render chrome()}
204
+ {/if}
205
+ </svelte:element>
@@ -36,10 +36,11 @@ const {
36
36
  compileSvelteModuleFile: (source: string, filename: string) => string;
37
37
  } = metroSvelteTransformer;
38
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;
39
+ const COMPILE_OPTIONS = {
40
+ generate: 'client',
41
+ fragments: 'tree',
42
+ css: 'external',
43
+ } as const;
43
44
  const SVELTE_SPECIFIER_PATTERN = /(['"])(\.{1,2}\/[^'"]*\.svelte)\1/g;
44
45
 
45
46
  export type ISvelteHarness = {
@@ -47,8 +48,6 @@ export type ISvelteHarness = {
47
48
  compileFile(sveltePath: string): string;
48
49
  // Compile an inline fixture written into `dir`, so its own relative imports resolve from there.
49
50
  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
51
  cleanup(): void;
53
52
  };
54
53
 
@@ -58,7 +57,6 @@ export function createSvelteHarness(
58
57
  ): ISvelteHarness {
59
58
  const written: string[] = [];
60
59
  const compiledBySource = new Map<string, string>();
61
- let strayWhitespace = 0;
62
60
 
63
61
  function outPathFor(sveltePath: string): string {
64
62
  const stem = basename(sveltePath).replace(/\./g, '-');
@@ -83,13 +81,21 @@ export function createSvelteHarness(
83
81
  },
84
82
  );
85
83
  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)}"`);
84
+ rewritten = rewritten
85
+ .split(`'${specifier}'`)
86
+ .join(`'${relativeSpecifier(outPath, target)}'`);
87
+ rewritten = rewritten
88
+ .split(`"${specifier}"`)
89
+ .join(`"${relativeSpecifier(outPath, target)}"`);
88
90
  }
89
91
  return rewritten;
90
92
  }
91
93
 
92
- function emit(sveltePath: string, source: string, isRuneModule: boolean): string {
94
+ function emit(
95
+ sveltePath: string,
96
+ source: string,
97
+ isRuneModule: boolean,
98
+ ): string {
93
99
  const outPath = outPathFor(sveltePath);
94
100
  // Registered BEFORE compiling so a cycle between two components terminates.
95
101
  compiledBySource.set(sveltePath, outPath);
@@ -98,7 +104,6 @@ export function createSvelteHarness(
98
104
  const code = isRuneModule
99
105
  ? compileSvelteModuleFile(source, `${filename}.ts`)
100
106
  : compile(source, { ...COMPILE_OPTIONS, filename }).js.code;
101
- strayWhitespace += (code.match(STRAY_WHITESPACE_PATTERN) ?? []).length;
102
107
  writeFileSync(outPath, rewrite(code, outPath));
103
108
  return outPath;
104
109
  }
@@ -121,7 +126,6 @@ export function createSvelteHarness(
121
126
  compileSource(dir, name, source) {
122
127
  return emit(join(dir, `${name}.svelte`), source, false);
123
128
  },
124
- strayWhitespaceCount: () => strayWhitespace,
125
129
  cleanup() {
126
130
  for (const file of written) rmSync(file, { force: true });
127
131
  written.length = 0;
@@ -10,7 +10,9 @@ export type ITabScreenOptionsArgs = {
10
10
  navigation: ITabNavigatorHandle;
11
11
  };
12
12
 
13
- export type ITabScreenOptionsResolver = (args: ITabScreenOptionsArgs) => ITabOptions;
13
+ export type ITabScreenOptionsResolver = (
14
+ args: ITabScreenOptionsArgs,
15
+ ) => ITabOptions;
14
16
 
15
17
  export type ITabScreenProps = {
16
18
  name: string;
@@ -48,7 +48,11 @@
48
48
  import type { INavigationScopeValue } from '../navigation-context';
49
49
  import NavigationScope from '../navigation-scope.svelte';
50
50
  import { SCREEN_REGISTRY_HOST_PROPS } from '../registry-host';
51
- import { setScreenCollector, toRegistry, withoutScreen } from '../screen-registry';
51
+ import {
52
+ setScreenCollector,
53
+ toRegistry,
54
+ withoutScreen,
55
+ } from '../screen-registry';
52
56
  import type { IRegisteredScreen } from '../screen-registry';
53
57
  import type { ITabScreenProps } from '../tab-screen-props';
54
58
  import type { ITabProps } from './tab-props';
@@ -94,7 +98,8 @@
94
98
  let dispatchedState = $state.raw<ITabRouterState | null>(null);
95
99
 
96
100
  const state = $derived.by<ITabRouterState>(() => {
97
- if (registeredRoutes.length === 0) dlog('Tab: no <Tab.Screen> children registered');
101
+ if (registeredRoutes.length === 0)
102
+ dlog('Tab: no <Tab.Screen> children registered');
98
103
  const dispatched = dispatchedState;
99
104
  return dispatched === null
100
105
  ? createInitialTabState(registeredRoutes, initialRouteName)
@@ -207,29 +212,33 @@
207
212
  );
208
213
 
209
214
  const tabBar = $derived.by<IDescriptor>(() =>
210
- renderTabBar({ items, style: focusedOptions?.tabBarStyle, passthrough: {} }),
215
+ renderTabBar({
216
+ items,
217
+ style: focusedOptions?.tabBarStyle,
218
+ passthrough: {},
219
+ }),
211
220
  );
212
221
 
213
222
  // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
214
223
  // alive), so a fresh NavigationScope per focus change is sufficient - the previous screen's
215
224
  // whole subtree (and any listeners it registered) is torn down by an ordinary unmount when
216
225
  // focus moves on.
217
- const focusedScreen = $derived.by<{ scope: INavigationScopeValue; component: Component } | undefined>(
218
- () => {
219
- if (focusedRoute === undefined) return undefined;
220
- const entry = registry.get(focusedRoute.name);
221
- if (entry === undefined) return undefined;
222
- return {
223
- component: entry.component,
224
- scope: {
225
- route: focusedRoute,
226
- navigation: handle,
227
- emitter: emitterFor(focusedRoute.key),
228
- parent: parentScope?.current,
229
- },
230
- };
231
- },
232
- );
226
+ const focusedScreen = $derived.by<
227
+ { scope: INavigationScopeValue; component: Component } | undefined
228
+ >(() => {
229
+ if (focusedRoute === undefined) return undefined;
230
+ const entry = registry.get(focusedRoute.name);
231
+ if (entry === undefined) return undefined;
232
+ return {
233
+ component: entry.component,
234
+ scope: {
235
+ route: focusedRoute,
236
+ navigation: handle,
237
+ emitter: emitterFor(focusedRoute.key),
238
+ parent: parentScope?.current,
239
+ },
240
+ };
241
+ });
233
242
 
234
243
  // The bar's ROOT stays a literal template tag (so `bind:this` has a statically known tag) and
235
244
  // only its children go through the Descriptor bridge - the uniform shape every Svelte component
@@ -244,4 +253,17 @@
244
253
  });
245
254
  </script>
246
255
 
247
- <symbiote-view p={TAB_ROOT_PROPS}><symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>{@render children?.()}</symbiote-text><symbiote-view p={TAB_CONTENT_PROPS}>{#if focusedScreen !== undefined}{@const FocusedComponent = focusedScreen.component}<NavigationScope value={focusedScreen.scope}><FocusedComponent /></NavigationScope>{/if}</symbiote-view><symbiote-view p={tabBar.props} bind:this={tabBarHost}></symbiote-view></symbiote-view>
256
+ <symbiote-view p={TAB_ROOT_PROPS}>
257
+ <symbiote-text p={SCREEN_REGISTRY_HOST_PROPS}>
258
+ {@render children?.()}
259
+ </symbiote-text>
260
+ <symbiote-view p={TAB_CONTENT_PROPS}>
261
+ {#if focusedScreen !== undefined}
262
+ {@const FocusedComponent = focusedScreen.component}
263
+ <NavigationScope value={focusedScreen.scope}>
264
+ <FocusedComponent />
265
+ </NavigationScope>
266
+ {/if}
267
+ </symbiote-view>
268
+ <symbiote-view p={tabBar.props} bind:this={tabBarHost} />
269
+ </symbiote-view>