@symbiote-native/navigation 2.0.0 → 2.0.1

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/package.json +11 -10
  2. package/build/angular/drawer/index.d.ts +0 -76
  3. package/build/angular/drawer/index.js +0 -508
  4. package/build/angular/drawer-screen.directive.d.ts +0 -14
  5. package/build/angular/drawer-screen.directive.js +0 -87
  6. package/build/angular/index.d.ts +0 -23
  7. package/build/angular/index.js +0 -22
  8. package/build/angular/injectors/index.d.ts +0 -13
  9. package/build/angular/injectors/index.js +0 -8
  10. package/build/angular/injectors/inject-drawer-navigation.d.ts +0 -4
  11. package/build/angular/injectors/inject-drawer-navigation.js +0 -12
  12. package/build/angular/injectors/inject-focus-effect.d.ts +0 -2
  13. package/build/angular/injectors/inject-focus-effect.js +0 -29
  14. package/build/angular/injectors/inject-is-focused.d.ts +0 -2
  15. package/build/angular/injectors/inject-is-focused.js +0 -20
  16. package/build/angular/injectors/inject-navigation-state.d.ts +0 -3
  17. package/build/angular/injectors/inject-navigation-state.js +0 -35
  18. package/build/angular/injectors/inject-navigation.d.ts +0 -7
  19. package/build/angular/injectors/inject-navigation.js +0 -16
  20. package/build/angular/injectors/inject-route.d.ts +0 -3
  21. package/build/angular/injectors/inject-route.js +0 -20
  22. package/build/angular/injectors/inject-stack-navigation.d.ts +0 -4
  23. package/build/angular/injectors/inject-stack-navigation.js +0 -15
  24. package/build/angular/injectors/inject-tab-navigation.d.ts +0 -4
  25. package/build/angular/injectors/inject-tab-navigation.js +0 -11
  26. package/build/angular/linking.d.ts +0 -3
  27. package/build/angular/linking.js +0 -43
  28. package/build/angular/navigation-context.service.d.ts +0 -13
  29. package/build/angular/navigation-context.service.js +0 -105
  30. package/build/angular/navigation-scope.directive.d.ts +0 -10
  31. package/build/angular/navigation-scope.directive.js +0 -90
  32. package/build/angular/screen.directive.d.ts +0 -22
  33. package/build/angular/screen.directive.js +0 -90
  34. package/build/angular/search-bar-ref.directive.d.ts +0 -10
  35. package/build/angular/search-bar-ref.directive.js +0 -88
  36. package/build/angular/stack/index.d.ts +0 -55
  37. package/build/angular/stack/index.js +0 -386
  38. package/build/angular/tab-screen.directive.d.ts +0 -14
  39. package/build/angular/tab-screen.directive.js +0 -87
  40. package/build/angular/tabs/index.d.ts +0 -42
  41. package/build/angular/tabs/index.js +0 -263
  42. package/build/core/constants.d.ts +0 -27
  43. package/build/core/constants.js +0 -61
  44. package/build/core/drawer-options/index.d.ts +0 -64
  45. package/build/core/drawer-options/index.js +0 -200
  46. package/build/core/drawer-router-state/index.d.ts +0 -21
  47. package/build/core/drawer-router-state/index.js +0 -40
  48. package/build/core/guards.d.ts +0 -2
  49. package/build/core/guards.js +0 -22
  50. package/build/core/header-bar-buttons.d.ts +0 -9
  51. package/build/core/header-bar-buttons.js +0 -163
  52. package/build/core/index.d.ts +0 -29
  53. package/build/core/index.js +0 -32
  54. package/build/core/linking-config/index.d.ts +0 -13
  55. package/build/core/linking-config/index.js +0 -171
  56. package/build/core/navigation-events/index.d.ts +0 -16
  57. package/build/core/navigation-events/index.js +0 -57
  58. package/build/core/navigator-handles/index.d.ts +0 -30
  59. package/build/core/navigator-handles/index.js +0 -15
  60. package/build/core/navigator-props.d.ts +0 -216
  61. package/build/core/navigator-props.js +0 -7
  62. package/build/core/navigator-state/index.d.ts +0 -36
  63. package/build/core/navigator-state/index.js +0 -81
  64. package/build/core/render-drawer.d.ts +0 -13
  65. package/build/core/render-drawer.js +0 -88
  66. package/build/core/render-stack.d.ts +0 -35
  67. package/build/core/render-stack.js +0 -294
  68. package/build/core/render-tabs.d.ts +0 -20
  69. package/build/core/render-tabs.js +0 -97
  70. package/build/core/screen-options.d.ts +0 -4
  71. package/build/core/screen-options.js +0 -133
  72. package/build/core/search-bar-commands.d.ts +0 -10
  73. package/build/core/search-bar-commands.js +0 -42
  74. package/build/core/state-persistence/index.d.ts +0 -3
  75. package/build/core/state-persistence/index.js +0 -23
  76. package/build/core/tab-options.d.ts +0 -13
  77. package/build/core/tab-options.js +0 -10
  78. package/build/core/tab-router-state/index.d.ts +0 -19
  79. package/build/core/tab-router-state/index.js +0 -51
  80. package/build/react/collect-registry.d.ts +0 -4
  81. package/build/react/collect-registry.js +0 -15
  82. package/build/react/drawer/index.d.ts +0 -29
  83. package/build/react/drawer/index.js +0 -277
  84. package/build/react/drawer-screen.d.ts +0 -14
  85. package/build/react/drawer-screen.js +0 -8
  86. package/build/react/hooks/index.d.ts +0 -12
  87. package/build/react/hooks/index.js +0 -8
  88. package/build/react/hooks/use-drawer-navigation.d.ts +0 -4
  89. package/build/react/hooks/use-drawer-navigation.js +0 -12
  90. package/build/react/hooks/use-focus-effect.d.ts +0 -2
  91. package/build/react/hooks/use-focus-effect.js +0 -29
  92. package/build/react/hooks/use-is-focused.d.ts +0 -1
  93. package/build/react/hooks/use-is-focused.js +0 -22
  94. package/build/react/hooks/use-navigation-state/index.d.ts +0 -2
  95. package/build/react/hooks/use-navigation-state/index.js +0 -39
  96. package/build/react/hooks/use-navigation.d.ts +0 -7
  97. package/build/react/hooks/use-navigation.js +0 -16
  98. package/build/react/hooks/use-route.d.ts +0 -2
  99. package/build/react/hooks/use-route.js +0 -8
  100. package/build/react/hooks/use-stack-navigation.d.ts +0 -4
  101. package/build/react/hooks/use-stack-navigation.js +0 -14
  102. package/build/react/hooks/use-tab-navigation.d.ts +0 -4
  103. package/build/react/hooks/use-tab-navigation.js +0 -12
  104. package/build/react/index.d.ts +0 -24
  105. package/build/react/index.js +0 -21
  106. package/build/react/linking.d.ts +0 -3
  107. package/build/react/linking.js +0 -47
  108. package/build/react/navigation-context.d.ts +0 -10
  109. package/build/react/navigation-context.js +0 -22
  110. package/build/react/screen.d.ts +0 -22
  111. package/build/react/screen.js +0 -7
  112. package/build/react/stack/index.d.ts +0 -13
  113. package/build/react/stack/index.js +0 -202
  114. package/build/react/tab-screen.d.ts +0 -15
  115. package/build/react/tab-screen.js +0 -8
  116. package/build/react/tabs/index.d.ts +0 -12
  117. package/build/react/tabs/index.js +0 -116
  118. package/build/register.d.ts +0 -7
  119. package/build/register.js +0 -58
  120. package/build/vue/composables/index.d.ts +0 -12
  121. package/build/vue/composables/index.js +0 -8
  122. package/build/vue/composables/use-drawer-navigation.d.ts +0 -5
  123. package/build/vue/composables/use-drawer-navigation.js +0 -15
  124. package/build/vue/composables/use-focus-effect.d.ts +0 -1
  125. package/build/vue/composables/use-focus-effect.js +0 -31
  126. package/build/vue/composables/use-is-focused.d.ts +0 -2
  127. package/build/vue/composables/use-is-focused.js +0 -28
  128. package/build/vue/composables/use-navigation-state.d.ts +0 -3
  129. package/build/vue/composables/use-navigation-state.js +0 -29
  130. package/build/vue/composables/use-navigation.d.ts +0 -8
  131. package/build/vue/composables/use-navigation.js +0 -13
  132. package/build/vue/composables/use-route.d.ts +0 -3
  133. package/build/vue/composables/use-route.js +0 -9
  134. package/build/vue/composables/use-stack-navigation.d.ts +0 -5
  135. package/build/vue/composables/use-stack-navigation.js +0 -18
  136. package/build/vue/composables/use-tab-navigation.d.ts +0 -5
  137. package/build/vue/composables/use-tab-navigation.js +0 -15
  138. package/build/vue/drawer/index.d.ts +0 -19
  139. package/build/vue/drawer/index.js +0 -390
  140. package/build/vue/drawer-screen.d.ts +0 -14
  141. package/build/vue/drawer-screen.js +0 -12
  142. package/build/vue/index.d.ts +0 -21
  143. package/build/vue/index.js +0 -19
  144. package/build/vue/linking.d.ts +0 -3
  145. package/build/vue/linking.js +0 -57
  146. package/build/vue/navigation-context.d.ts +0 -16
  147. package/build/vue/navigation-context.js +0 -65
  148. package/build/vue/screen.d.ts +0 -21
  149. package/build/vue/screen.js +0 -12
  150. package/build/vue/stack/index.d.ts +0 -9
  151. package/build/vue/stack/index.js +0 -279
  152. package/build/vue/tab-screen.d.ts +0 -14
  153. package/build/vue/tab-screen.js +0 -11
  154. package/build/vue/tabs/index.d.ts +0 -10
  155. package/build/vue/tabs/index.js +0 -209
@@ -1,14 +0,0 @@
1
- // useNavigation() returns a union (IAnyNavigatorHandle) since it doesn't know which navigator
2
- // mounted the calling component. When a component genuinely knows it only ever renders under a
3
- // Stack (the common case for anything nested a few levels below a Stack screen), this hook
4
- // narrows that union ONCE, here, so call sites never write their own 'push' in navigation check.
5
- import { isStackNavigatorHandle } from '../../core/index.js';
6
- import { useNavigation } from './use-navigation.js';
7
- export function useStackNavigation() {
8
- const navigation = useNavigation();
9
- if (!isStackNavigatorHandle(navigation)) {
10
- throw new Error('useStackNavigation() was called from a component whose nearest navigator is not a Stack. ' +
11
- 'Use useNavigation() instead if this component can render under more than one navigator kind.');
12
- }
13
- return navigation;
14
- }
@@ -1,4 +0,0 @@
1
- import type { ITabNavigatorHandle } from '../../core';
2
- import type { INavigationHandle } from './use-navigation';
3
- export type ITabNavigationHandle = ITabNavigatorHandle & Pick<INavigationHandle, 'addListener' | 'getParent'>;
4
- export declare function useTabNavigation(): ITabNavigationHandle;
@@ -1,12 +0,0 @@
1
- // Twin of use-stack-navigation.ts, narrowing to a Tab handle instead. See that file's header for
2
- // why this exists: it hides the union-narrowing guard inside the library instead of every call site.
3
- import { isTabNavigatorHandle } from '../../core/index.js';
4
- import { useNavigation } from './use-navigation.js';
5
- export function useTabNavigation() {
6
- const navigation = useNavigation();
7
- if (!isTabNavigatorHandle(navigation)) {
8
- throw new Error('useTabNavigation() was called from a component whose nearest navigator is not a Tab. ' +
9
- 'Use useNavigation() instead if this component can render under more than one navigator kind.');
10
- }
11
- return navigation;
12
- }
@@ -1,24 +0,0 @@
1
- import '../register';
2
- export { Stack } from './stack';
3
- export type { INavigatorHandle, IStackProps } from './stack';
4
- export { Screen } from './screen';
5
- export type { IScreenOptionsArgs, IScreenOptionsResolver, IScreenProps } from './screen';
6
- export type { IScreenOptions, IStackAnimation, IStackPresentation } from '../core';
7
- export { useNavigation, useRoute, useIsFocused, useFocusEffect, useNavigationState } from './hooks';
8
- export type { INavigationHandle } from './hooks';
9
- export { useStackNavigation, useTabNavigation, useDrawerNavigation } from './hooks';
10
- export type { IStackNavigationHandle, ITabNavigationHandle, IDrawerNavigationHandle, } from './hooks';
11
- export { NavigationContext } from './navigation-context';
12
- export type { INavigationContextValue, IAnyNavigatorHandle } from './navigation-context';
13
- export { useLinkingIntegration } from './linking';
14
- export type { ILinkingConfig, IScreenLinkingConfig } from '../core';
15
- export { Tab } from './tabs';
16
- export type { ITabNavigatorHandle, ITabProps } from './tabs';
17
- export { TabScreen } from './tab-screen';
18
- export type { ITabScreenOptionsArgs, ITabScreenOptionsResolver, ITabScreenProps, } from './tab-screen';
19
- export type { ITabOptions, ITabBarIcon } from '../core';
20
- export { Drawer } from './drawer';
21
- export type { IDrawerNavigatorHandle, IDrawerProps, IDrawerDescriptorMap } from './drawer';
22
- export { DrawerScreen } from './drawer-screen';
23
- export type { IDrawerScreenOptionsArgs, IDrawerScreenOptionsResolver, IDrawerScreenProps, } from './drawer-screen';
24
- export type { IDrawerOptions, IDrawerScreenOptions, IDrawerType, IDrawerPosition } from '../core';
@@ -1,21 +0,0 @@
1
- // @symbiote-native/navigation/react: the React native-stack navigator over react-native-screens'
2
- // native view primitives. Importing this barrel first registers the native views' ViewConfigs
3
- // (../register, a side-effect import of the three codegen specs - never react-native-screens' own
4
- // React components), then exposes Stack (with Stack.Screen attached) and the navigator handle.
5
- import '../register';
6
- export { Stack } from './stack/index.js';
7
- export { Screen } from './screen.js';
8
- export { useNavigation, useRoute, useIsFocused, useFocusEffect, useNavigationState } from './hooks/index.js';
9
- export { useStackNavigation, useTabNavigation, useDrawerNavigation } from './hooks/index.js';
10
- export { NavigationContext } from './navigation-context.js';
11
- export { useLinkingIntegration } from './linking.js';
12
- // Tab: the bottom-tabs navigator, a PURE-JS UI (no react-native-screens views involved, so no
13
- // extra ViewConfig registration is needed beyond the ../register import above).
14
- export { Tab } from './tabs/index.js';
15
- export { TabScreen } from './tab-screen.js';
16
- // Drawer: the swipeable drawer navigator, a PURE-JS UI (PanResponder + Animated, no
17
- // react-native-screens views involved, so no extra ViewConfig registration is needed beyond the
18
- // ../register import above). See react/drawer.ts's header for the feasibility note re:
19
- // react-native-gesture-handler / react-native-reanimated parity gaps.
20
- export { Drawer } from './drawer/index.js';
21
- export { DrawerScreen } from './drawer-screen.js';
@@ -1,3 +0,0 @@
1
- import type { ILinkingConfig } from '../core';
2
- import type { INavigatorHandle } from '../core';
3
- export declare function useLinkingIntegration(config: ILinkingConfig, navigatorHandle: INavigatorHandle): void;
@@ -1,47 +0,0 @@
1
- // useLinkingIntegration: wires the framework-agnostic linking config (../core's
2
- // resolveRouteFromUrl) onto an existing Stack ref's INavigatorHandle. Deliberately NOT called
3
- // from inside stack.ts - app code owns the wiring (a <Stack ref={...}> plus this hook alongside
4
- // it), the same top-level-prop shape @react-navigation keeps `linking` in on NavigationContainer
5
- // rather than baking it into the router itself.
6
- import { useEffect, useRef } from 'react';
7
- import { Linking, dlog } from '@symbiote-native/engine';
8
- import { resolveRouteFromUrl } from '../core/index.js';
9
- export function useLinkingIntegration(config, navigatorHandle) {
10
- // Refs, not effect deps: `navigatorHandle` is re-created by Stack on every push/pop
11
- // (see stack.ts's useMemo deps including `state.routes.length`), so depending on it directly
12
- // would resubscribe on every navigation instead of once on mount.
13
- const configRef = useRef(config);
14
- configRef.current = config;
15
- const handleRef = useRef(navigatorHandle);
16
- handleRef.current = navigatorHandle;
17
- useEffect(() => {
18
- function applyRoute(url, dispatch) {
19
- const route = resolveRouteFromUrl(configRef.current, url);
20
- if (route === null) {
21
- dlog(`useLinkingIntegration: no route resolved for "${url}"`);
22
- return;
23
- }
24
- dispatch(route.name, route.params);
25
- }
26
- let cancelled = false;
27
- Linking.getInitialURL()
28
- .then(url => {
29
- if (cancelled || url === null)
30
- return;
31
- dlog(`useLinkingIntegration: initial URL "${url}"`);
32
- applyRoute(url, handleRef.current.replace);
33
- })
34
- .catch((error) => {
35
- dlog(`useLinkingIntegration: getInitialURL failed: ${String(error)}`);
36
- });
37
- const subscription = Linking.addEventListener('url', (event) => {
38
- dlog(`useLinkingIntegration: url event "${event.url}"`);
39
- applyRoute(event.url, handleRef.current.push);
40
- });
41
- return () => {
42
- cancelled = true;
43
- subscription.remove();
44
- };
45
- // Mount-once: freshness of config/navigatorHandle is read through the refs above.
46
- }, []);
47
- }
@@ -1,10 +0,0 @@
1
- import type { INavigationEmitter, IRoute, IAnyNavigatorHandle } from '../core';
2
- export type { IAnyNavigatorHandle } from '../core';
3
- export type INavigationContextValue = {
4
- route: IRoute<unknown>;
5
- navigation: IAnyNavigatorHandle;
6
- emitter: INavigationEmitter;
7
- parent?: INavigationContextValue;
8
- };
9
- export declare const NavigationContext: import("react").Context<INavigationContextValue | undefined>;
10
- export declare function useRequiredNavigationContext(hookName: string): INavigationContextValue;
@@ -1,22 +0,0 @@
1
- // The lifecycle half of the framework-agnostic emitter (../core/navigation-events): a plain
2
- // React Context so a screen's subtree can read its own route, navigator handle, and per-route
3
- // emitter without prop-drilling - the same role @react-navigation's NavigationContext /
4
- // NavigationRouteContext play, collapsed into one context since a symbiote screen only ever needs
5
- // its OWN route (no per-navigator param-list generics in v1 scope - see screen.ts). Provided
6
- // per-screen by each of stack.ts/tabs.ts/drawer.ts's render loop, consumed by the hooks in
7
- // ./hooks. `parent` threads the ambient context a navigator read on ITS OWN mount (undefined at
8
- // the root) into the value it provides to its own screens, forming a linked list - this is what
9
- // lets a screen nested inside e.g. a Stack-screen-renders-a-Tab composition reach the enclosing
10
- // Stack via useNavigation().getParent().
11
- import { createContext, useContext } from 'react';
12
- export const NavigationContext = createContext(undefined);
13
- // Every hook in ./hooks needs the same `useContext(NavigationContext)` + missing-provider throw,
14
- // so it's co-located here instead of repeated per hook. `hookName` keeps each hook's own name in
15
- // the thrown message.
16
- export function useRequiredNavigationContext(hookName) {
17
- const context = useContext(NavigationContext);
18
- if (!context) {
19
- throw new Error(`${hookName} must be used within a screen rendered by <Stack>, <Tab>, or <Drawer>`);
20
- }
21
- return context;
22
- }
@@ -1,22 +0,0 @@
1
- import type { FC, ReactElement, RefObject } from 'react';
2
- import type { IRoute } from '../core';
3
- import type { ISearchBarCommands, ISearchBarOptions, IScreenOptions } from '../core';
4
- import type { INavigatorHandle } from '../core';
5
- export type IReactSearchBarOptions = ISearchBarOptions & {
6
- ref?: RefObject<ISearchBarCommands | null>;
7
- };
8
- export type IReactScreenOptions = Omit<IScreenOptions, 'headerSearchBarOptions'> & {
9
- headerSearchBarOptions?: IReactSearchBarOptions;
10
- };
11
- export type IScreenOptionsArgs = {
12
- route: IRoute<unknown>;
13
- navigation: INavigatorHandle;
14
- };
15
- export type IScreenOptionsResolver = (args: IScreenOptionsArgs) => IReactScreenOptions;
16
- export type IScreenProps = {
17
- name: string;
18
- component: FC;
19
- options?: IReactScreenOptions | IScreenOptionsResolver;
20
- initialParams?: unknown;
21
- };
22
- export declare function Screen(_props: IScreenProps): ReactElement | null;
@@ -1,7 +0,0 @@
1
- // Stack.Screen: a declarative marker, never rendered on its own - Stack reads its props via
2
- // React.Children to build the static name -> {component, options} registry, then mounts the
3
- // registered component itself for each pushed route. Mirrors @react-navigation's JSX-config
4
- // screens, minus the linking/param-list generics (v1 scope).
5
- export function Screen(_props) {
6
- return null;
7
- }
@@ -1,13 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { INavigatorHandle } from '../../core';
3
- import { Screen } from '../screen';
4
- import type { IReactScreenOptions } from '../screen';
5
- export type { INavigatorHandle } from '../../core';
6
- export type IStackProps = {
7
- initialRouteName?: string;
8
- screenOptions?: IReactScreenOptions;
9
- children?: ReactNode;
10
- };
11
- export declare const Stack: import("react").ForwardRefExoticComponent<IStackProps & import("react").RefAttributes<INavigatorHandle>> & {
12
- Screen: typeof Screen;
13
- };
@@ -1,202 +0,0 @@
1
- // Stack, the React 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 Vue/Angular adapters; here React supplies the lifecycle - useReducer
4
- // for the pushed-route stack, useId + a ref counter for route-key generation, useImperativeHandle
5
- // for the push/pop/replace navigator handle - plus the descriptor bridge for the header config
6
- // leaf. Pushing/popping a route is an ordinary child mount/unmount: RNSScreenStack diffs its
7
- // RNSScreen children natively, so no imperative native command is needed here at all. Neither
8
- // this nor the Screen marker imports react-native-screens' own React components (ScreenStack.tsx
9
- // et al - hooks, crashes a non-React adapter); the native views are driven directly through the
10
- // ViewConfig ../register registers. See CLAUDE.md <third_party_rn_packages_are_react_only>.
11
- import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, useRef, } from 'react';
12
- import { descriptorToReact } from '@symbiote-native/react';
13
- import { Platform, debugNodeId, dlog } from '@symbiote-native/engine';
14
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_STATE, RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, RNS_SCREEN_STACK_VIEW_NAME, RNS_SCREEN_VIEW_NAME, SCREEN_ON_APPEAR, SCREEN_ON_DISAPPEAR, SCREEN_ON_DISMISSED, SCREEN_ON_HEADER_BACK_BUTTON_CLICKED, SCREEN_ON_WILL_APPEAR, SCREEN_ON_WILL_DISAPPEAR, STACK_ON_FINISH_TRANSITIONING, buildSearchBarHandle, buildSearchBarPassthrough, computeActivityState, createInitialNavigatorState, createNavigationEmitter, navigatorReducer, resolveScreenRenderPlan, resolveStackProps, } from '../../core/index.js';
15
- import { collectRegistry } from '../collect-registry.js';
16
- import { NavigationContext } from '../navigation-context.js';
17
- import { Screen } from '../screen.js';
18
- // backTitleVisible defaults to `true` on both platforms per the codegen spec's own default
19
- // (CT.WithDefault<boolean, 'true'>) - no ios/android divergence in v1 scope, so a single constant
20
- // stands in for the per-platform injection point ISliderPlatform-style adapters use elsewhere.
21
- const NAVIGATOR_PLATFORM = { defaultHeaderBackTitleVisible: true };
22
- function isScreenElement(child) {
23
- return isValidElement(child) && child.type === Screen;
24
- }
25
- function resolveScreenOptions(entry, optionsArgs, screenOptions) {
26
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
27
- return { ...screenOptions, ...own };
28
- }
29
- const StackImpl = forwardRef((props, forwardedRef) => {
30
- // Read BEFORE establishing this Stack's own Context value below - becomes the `parent` link a
31
- // nested screen's useNavigation().getParent() walks (e.g. this Stack rendered as a Tab screen's
32
- // content reaches that Tab via this value). undefined when this Stack is the nesting root.
33
- const ambientContext = useContext(NavigationContext);
34
- const registry = useMemo(() => collectRegistry(props.children, isScreenElement), [props.children]);
35
- const routeIdPrefix = useId();
36
- const routeSequence = useRef(0);
37
- // One emitter per route.key, keyed exactly like routeSequence's counter is scoped - created
38
- // lazily the first time a route is rendered, pruned once it's popped off the stack (below).
39
- const emitters = useRef(new Map()).current;
40
- // Investigation instrumentation (flicker-on-focus bug): route keys whose resolved
41
- // transition/animation-timing props have already been logged, so the once-per-mount dump below
42
- // doesn't spam on every re-render. Kept behind DEBUG, never removed.
43
- const loggedScreenPropKeys = useRef(new Set()).current;
44
- const emitterFor = useCallback((routeKey) => {
45
- let emitter = emitters.get(routeKey);
46
- if (!emitter) {
47
- emitter = createNavigationEmitter();
48
- emitters.set(routeKey, emitter);
49
- }
50
- return emitter;
51
- }, [emitters]);
52
- const createRoute = useCallback((name, params) => {
53
- routeSequence.current += 1;
54
- return { key: `${routeIdPrefix}-${name}-${routeSequence.current}`, name, params };
55
- }, [routeIdPrefix]);
56
- const [state, dispatch] = useReducer(navigatorReducer, undefined, () => {
57
- const initialRouteName = props.initialRouteName ?? registry.keys().next().value;
58
- if (initialRouteName === undefined) {
59
- dlog('Stack: no <Stack.Screen> children registered');
60
- return createInitialNavigatorState({ key: routeIdPrefix, name: '', params: undefined });
61
- }
62
- return createInitialNavigatorState(createRoute(initialRouteName, registry.get(initialRouteName)?.initialParams));
63
- });
64
- const handle = useMemo(() => ({
65
- push: (name, params) => dispatch({ type: 'push', route: createRoute(name, params) }),
66
- pop: count => dispatch({ type: 'pop', count }),
67
- popToTop: () => dispatch({ type: 'popToTop' }),
68
- popTo: key => dispatch({ type: 'popTo', key }),
69
- replace: (name, params) => dispatch({ type: 'replace', route: createRoute(name, params) }),
70
- setParams: (params, key) => dispatch({ type: 'setParams', key, params }),
71
- reset: nextState => dispatch({ type: 'reset', state: nextState }),
72
- canGoBack: () => state.routes.length > 1,
73
- }), [createRoute, state.routes.length]);
74
- useImperativeHandle(forwardedRef, () => handle, [handle]);
75
- // Broadcasts the router state to every still-live route's emitter (useNavigationState's
76
- // source) after each commit, and prunes emitters for routes popped off the stack - mirroring
77
- // React's own subscribe-in-effect pattern rather than emitting mid-render, which would call a
78
- // descendant's setState while this component is still rendering.
79
- useEffect(() => {
80
- for (const route of state.routes) {
81
- emitterFor(route.key).emit(NAVIGATION_EVENT_STATE, state);
82
- }
83
- for (const routeKey of emitters.keys()) {
84
- if (!state.routes.some(route => route.key === routeKey))
85
- emitters.delete(routeKey);
86
- }
87
- }, [state, emitterFor, emitters]);
88
- // Investigation instrumentation (flicker-on-focus bug): STACK_ON_FINISH_TRANSITIONING is the
89
- // native signal that the WHOLE push/pop animation has finished (as opposed to onAppear/
90
- // onDisappear, which are per-screen) - logging it lets the per-screen appear/disappear
91
- // timestamps above be checked against the actual transition-complete moment. Kept behind
92
- // DEBUG, never removed.
93
- const stackProps = resolveStackProps({
94
- passthrough: {
95
- [STACK_ON_FINISH_TRANSITIONING]: () => dlog(`Stack: onFinishTransitioning at t=${Date.now()}`),
96
- },
97
- });
98
- const children = state.routes.map((route, index) => {
99
- const entry = registry.get(route.name);
100
- if (!entry) {
101
- dlog(`Stack: no screen registered for route name "${route.name}"`);
102
- return null;
103
- }
104
- const screenOptionsArgs = { route, navigation: handle };
105
- const mergedOptions = resolveScreenOptions(entry, screenOptionsArgs, props.screenOptions);
106
- const activityState = computeActivityState(index, state.routes.length);
107
- // Investigation instrumentation (flicker-on-focus bug): fires on EVERY Stack render, not just
108
- // on transitions, so the log stream shows whether a route's activityState/index ever changes
109
- // outside of a push/pop dispatch (e.g. from an unrelated re-render racing the native
110
- // transition). Kept behind DEBUG per <keep_logs_gate_behind_DEBUG> - never remove.
111
- dlog(`Stack: render route "${route.name}" index=${index}/${state.routes.length - 1} ` +
112
- `activityState=${activityState} at t=${Date.now()}`);
113
- const routeEmitter = emitterFor(route.key);
114
- const searchBarOptions = mergedOptions.headerSearchBarOptions;
115
- const plan = resolveScreenRenderPlan({
116
- screenId: route.key,
117
- index,
118
- routeCount: state.routes.length,
119
- options: mergedOptions,
120
- platform: NAVIGATOR_PLATFORM,
121
- isAndroid: Platform.OS === 'android',
122
- screenPassthrough: {
123
- [SCREEN_ON_DISMISSED]: () => dispatch({ type: 'pop', count: 1 }),
124
- [SCREEN_ON_HEADER_BACK_BUTTON_CLICKED]: () => dispatch({ type: 'pop', count: 1 }),
125
- // onAppear/onDisappear are the definitive visibility boundary (post-transition-animation),
126
- // so 'focus'/'blur' fire exactly once per transition; onWillAppear/onWillDisappear fire
127
- // BEFORE the animation runs, so wiring them to emit() too would double-invoke
128
- // useFocusEffect per transition - they only get a debug log here.
129
- [SCREEN_ON_WILL_APPEAR]: () => dlog(`Stack: route "${route.name}" will appear at t=${Date.now()}`),
130
- [SCREEN_ON_APPEAR]: () => {
131
- dlog(`Stack: route "${route.name}" appeared (focus) at t=${Date.now()}`);
132
- routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
133
- },
134
- [SCREEN_ON_WILL_DISAPPEAR]: () => dlog(`Stack: route "${route.name}" will disappear at t=${Date.now()}`),
135
- [SCREEN_ON_DISAPPEAR]: () => {
136
- dlog(`Stack: route "${route.name}" disappeared (blur) at t=${Date.now()}`);
137
- routeEmitter.emit(NAVIGATION_EVENT_BLUR);
138
- },
139
- },
140
- searchBarPassthrough: searchBarOptions
141
- ? {
142
- ...buildSearchBarPassthrough(searchBarOptions, message => dlog(`Stack: route "${route.name}" ${message}`)),
143
- // The imperative ref (SearchBarCommands): a callback ref attached straight to the
144
- // RNSSearchBar host element via `passthrough.ref` - createElement extracts `ref` from
145
- // a props object same as ScrollView/TextInput's own ref binding (see
146
- // descriptor-to-react's comment), so the host instance IS the ISymbioteNode. Built
147
- // fresh per mount/unmount, never captured eagerly (buildSearchBarHandle's own
148
- // lazy-getter contract is satisfied trivially here since `node` is already resolved).
149
- ref: (node) => {
150
- // Investigation instrumentation (search-bar-ref "node not committed" bug): compare
151
- // this debugNodeId against the mirror.set/dispatchViewCommand logs in commit.ts -
152
- // same id on both sides proves the ref really does hold the committed node; a
153
- // mismatch proves a stale closure instead. Kept behind DEBUG, never removed.
154
- dlog(`Stack: search bar ref callback, node=${node === null ? 'null' : debugNodeId(node)} at t=${Date.now()}`);
155
- const appRef = searchBarOptions.ref;
156
- if (!appRef)
157
- return;
158
- appRef.current = node === null ? null : buildSearchBarHandle(() => node);
159
- },
160
- }
161
- : undefined,
162
- });
163
- // Investigation instrumentation (flicker-on-focus bug): the actual timing/z-order-relevant
164
- // values resolved onto the native RNSScreen, once per route.key (not every render) - rules a
165
- // stackAnimation/transitionDuration mismatch against react-native-screens' own native default
166
- // in or out. Kept behind DEBUG, never removed.
167
- if (!loggedScreenPropKeys.has(route.key)) {
168
- loggedScreenPropKeys.add(route.key);
169
- dlog(`Stack: route "${route.name}" resolved screen props ` +
170
- `stackAnimation=${String(plan.screenProps.stackAnimation)} ` +
171
- `stackPresentation=${String(plan.screenProps.stackPresentation)} ` +
172
- `transitionDuration=${String(plan.screenProps.transitionDuration)} ` +
173
- `gestureEnabled=${String(plan.screenProps.gestureEnabled)} at t=${Date.now()}`);
174
- }
175
- // Must not be flattened away (collapsable: false) - react-native-screens' native side
176
- // finds THIS specific view type by class check to register a formSheet's content for
177
- // sizing (see RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME's comment in core/constants.ts). A
178
- // `push` screen doesn't need it, but a `formSheet` one is otherwise left with no content
179
- // ever attached natively.
180
- const content = createElement(RNS_SCREEN_CONTENT_WRAPPER_VIEW_NAME, plan.contentWrapperProps, createElement(NavigationContext.Provider, { value: { route, navigation: handle, emitter: routeEmitter, parent: ambientContext } }, createElement(entry.component)));
181
- // react-native-screens' own Screen.tsx swaps in a DIFFERENT Fabric component
182
- // ('RNSModalScreen') for a modally-presented screen - its native updateLayoutMetrics: relies on
183
- // this exact class to know it must NOT apply Yoga's computed frame (see resolveScreenViewName's
184
- // comment in core/render-stack.ts). The nested Screen isHeaderInModal adds below is always
185
- // plain 'RNSScreen': react-native-screens' own inner Screen never carries a presentation prop
186
- // either, since it exists purely to host the header, not to be modally presented itself.
187
- // A modal/formSheet screen has no UINavigationController of its own on iOS - nest an inner
188
- // RNSScreenStack/RNSScreen purely to host the native header bar (see isHeaderInModal's
189
- // comment in core/render-stack.ts). Skipping this leaves RNSScreenStackHeaderConfig with no
190
- // navigation controller to attach to, so the header silently never renders.
191
- return plan.inModal
192
- ? createElement(plan.screenViewName, { key: route.key, ...plan.screenProps }, createElement(RNS_SCREEN_STACK_VIEW_NAME, { style: plan.innerStackStyle }, createElement(RNS_SCREEN_VIEW_NAME,
193
- // activityState mirrors the outer Screen's own value - react-native-screens'
194
- // RNSScreen.mm treats an unset/inactive nested screen as not yet pushed, leaving it
195
- // parked at its pre-push transition position (off past the bottom edge) instead of
196
- // its real, presented frame.
197
- { style: plan.innerScreenStyle, activityState: plan.activityState }, descriptorToReact(plan.headerConfig), content)))
198
- : createElement(plan.screenViewName, { key: route.key, ...plan.screenProps }, descriptorToReact(plan.headerConfig), content);
199
- });
200
- return createElement(RNS_SCREEN_STACK_VIEW_NAME, stackProps, ...children);
201
- });
202
- export const Stack = Object.assign(StackImpl, { Screen });
@@ -1,15 +0,0 @@
1
- import type { FC, ReactElement } from 'react';
2
- import type { IRoute, ITabOptions } from '../core';
3
- import type { ITabNavigatorHandle } from '../core';
4
- export type ITabScreenOptionsArgs = {
5
- route: IRoute<unknown>;
6
- navigation: ITabNavigatorHandle;
7
- };
8
- export type ITabScreenOptionsResolver = (args: ITabScreenOptionsArgs) => ITabOptions;
9
- export type ITabScreenProps = {
10
- name: string;
11
- component: FC;
12
- options?: ITabOptions | ITabScreenOptionsResolver;
13
- initialParams?: unknown;
14
- };
15
- export declare function TabScreen(_props: ITabScreenProps): ReactElement | null;
@@ -1,8 +0,0 @@
1
- // Tab.Screen: a declarative marker, never rendered on its own - Tab reads its props via
2
- // React.Children to build the static name -> {component, options} registry, then mounts the
3
- // FOCUSED route's component itself. Mirrors react/screen.ts's Screen (Stack's twin) minus the
4
- // stack-only concepts (no push/pop lifecycle events to wire); see CLAUDE.md
5
- // <third_party_rn_packages_are_react_only> - this and Tab import nothing from react-native.
6
- export function TabScreen(_props) {
7
- return null;
8
- }
@@ -1,12 +0,0 @@
1
- import type { ReactNode } from 'react';
2
- import type { ITabNavigatorHandle, ITabOptions } from '../../core';
3
- import { TabScreen } from '../tab-screen';
4
- export type { ITabNavigatorHandle } from '../../core';
5
- export type ITabProps = {
6
- initialRouteName?: string;
7
- screenOptions?: ITabOptions;
8
- children?: ReactNode;
9
- };
10
- export declare const Tab: import("react").ForwardRefExoticComponent<ITabProps & import("react").RefAttributes<ITabNavigatorHandle>> & {
11
- Screen: typeof TabScreen;
12
- };
@@ -1,116 +0,0 @@
1
- // Tab, the React lifecycle half. The focused-index router (tab-router-state) and the tab-bar
2
- // Descriptor builder (render-tabs) live in @symbiote-native/navigation core, shared verbatim
3
- // with the Vue/Angular adapters; here React supplies the lifecycle - useReducer for the focused
4
- // index, useId for route-key generation, useImperativeHandle for the jumpTo/setParams handle -
5
- // plus the descriptor bridge for the tab-bar leaf, exactly like Stack bridges its header config
6
- // (react/stack.ts). Unlike Stack, a bottom-tabs bar is a PURE-JS UI: it paints ordinary
7
- // `symbiote-view`/`symbiote-text` primitives via the shared render fn, so there is no
8
- // react-native-screens ViewConfig to register here - Tab needs no `../register` import.
9
- import { createElement, forwardRef, isValidElement, useCallback, useContext, useEffect, useId, useImperativeHandle, useMemo, useReducer, } from 'react';
10
- import { descriptorToReact } from '@symbiote-native/react';
11
- import { dlog } from '@symbiote-native/engine';
12
- import { NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_FOCUS, createInitialTabState, createNavigationEmitter, isFocusedRoute, renderTabBar, tabRouterReducer, } from '../../core/index.js';
13
- import { collectRegistry } from '../collect-registry.js';
14
- import { NavigationContext } from '../navigation-context.js';
15
- import { TabScreen } from '../tab-screen.js';
16
- function isTabScreenElement(child) {
17
- return isValidElement(child) && child.type === TabScreen;
18
- }
19
- function resolveTabOptions(entry, optionsArgs, screenOptions) {
20
- const own = typeof entry.options === 'function' ? entry.options(optionsArgs) : entry.options;
21
- return { ...screenOptions, ...own };
22
- }
23
- const TAB_CONTENT_STYLE = { flex: 1 };
24
- const TAB_ROOT_STYLE = { flex: 1 };
25
- const TabImpl = forwardRef((props, forwardedRef) => {
26
- // Read BEFORE establishing this Tab's own Context value below - becomes the `parent` link a
27
- // nested screen's useNavigation().getParent() walks (e.g. this Tab rendered as a Stack screen's
28
- // content reaches that Stack via this value). undefined when this Tab is the nesting root.
29
- const ambientContext = useContext(NavigationContext);
30
- const registry = useMemo(() => collectRegistry(props.children, isTabScreenElement), [props.children]);
31
- const routeIdPrefix = useId();
32
- const routes = useMemo(() => Array.from(registry.entries()).map(([name, entry]) => ({
33
- key: `${routeIdPrefix}-${name}`,
34
- name,
35
- params: entry.initialParams,
36
- })), [registry, routeIdPrefix]);
37
- const [state, dispatch] = useReducer(tabRouterReducer, undefined, () => createInitialTabState(routes, props.initialRouteName));
38
- if (routes.length === 0)
39
- dlog('Tab: no <Tab.Screen> children registered');
40
- const jumpTo = useCallback((name, params) => dispatch({ type: 'jumpTo', name, params }), []);
41
- const setParams = useCallback((params, key) => dispatch({ type: 'setParams', key, params }), []);
42
- const handle = useMemo(() => ({ jumpTo, setParams }), [jumpTo, setParams]);
43
- useImperativeHandle(forwardedRef, () => handle, [handle]);
44
- const focusedRoute = state.routes[state.index];
45
- const items = state.routes.map((route, index) => {
46
- const entry = registry.get(route.name);
47
- const focused = isFocusedRoute(index, state.index);
48
- if (!entry) {
49
- dlog(`Tab: no screen registered for route name "${route.name}"`);
50
- return {
51
- key: route.key,
52
- focused,
53
- label: route.name,
54
- passthrough: {},
55
- };
56
- }
57
- const options = resolveTabOptions(entry, { route, navigation: handle }, props.screenOptions);
58
- return {
59
- key: route.key,
60
- focused,
61
- label: options.tabBarLabel ?? options.title ?? route.name,
62
- icon: options.tabBarIcon,
63
- badge: options.tabBarBadge,
64
- activeTintColor: options.tabBarActiveTintColor,
65
- inactiveTintColor: options.tabBarInactiveTintColor,
66
- passthrough: {
67
- onPress: () => jumpTo(route.name),
68
- accessibilityRole: 'tab',
69
- accessibilityState: { selected: focused },
70
- },
71
- };
72
- });
73
- const focusedEntry = focusedRoute ? registry.get(focusedRoute.name) : undefined;
74
- const focusedOptions = focusedEntry && focusedRoute
75
- ? resolveTabOptions(focusedEntry, { route: focusedRoute, navigation: handle }, props.screenOptions)
76
- : props.screenOptions;
77
- const tabBar = descriptorToReact(renderTabBar({ items, style: focusedOptions?.tabBarStyle, passthrough: {} }));
78
- // Only the focused route's screen is ever mounted (unlike Stack, which keeps every pushed route
79
- // alive), so a fresh emitter per focus change is sufficient - no per-route emitter map to prune,
80
- // because the previous screen's whole subtree (and any listeners it registered) is torn down by
81
- // an ordinary React unmount when focus moves on. Keyed on the route KEY rather than the route
82
- // object so a setParams-only change (new route object, same key) doesn't spuriously re-fire
83
- // focus/blur.
84
- const focusedRouteKey = focusedRoute?.key;
85
- const routeEmitter = useMemo(() => createNavigationEmitter(), [focusedRouteKey]);
86
- // Tab paints its own bar in pure JS - there is no native onAppear/onDisappear to hook (unlike
87
- // Stack's RNSScreen), so focus/blur is synthesized here: mount = focus, cleanup = blur, exactly
88
- // what an effect keyed on focusedRouteKey already encodes - no diffFocusedRoute indirection
89
- // needed (unlike Vue/Angular, which diff real prev/next keys inside an imperative watch/CD
90
- // callback that has no mount/cleanup pairing of its own).
91
- useEffect(() => {
92
- if (focusedRouteKey === undefined)
93
- return undefined;
94
- dlog(`Tab: route "${focusedRoute?.name}" focused`);
95
- routeEmitter.emit(NAVIGATION_EVENT_FOCUS);
96
- return () => {
97
- dlog(`Tab: route "${focusedRoute?.name}" blurred`);
98
- routeEmitter.emit(NAVIGATION_EVENT_BLUR);
99
- };
100
- // focusedRoute omitted deliberately: only its .key (tracked via focusedRouteKey) should
101
- // re-run this effect - see the comment above on focusedRouteKey.
102
- // eslint-disable-next-line react-hooks/exhaustive-deps
103
- }, [routeEmitter, focusedRouteKey]);
104
- const content = focusedEntry && focusedRoute
105
- ? createElement(NavigationContext.Provider, {
106
- value: {
107
- route: focusedRoute,
108
- navigation: handle,
109
- emitter: routeEmitter,
110
- parent: ambientContext,
111
- },
112
- }, createElement(focusedEntry.component))
113
- : null;
114
- return createElement('symbiote-view', { style: TAB_ROOT_STYLE }, createElement('symbiote-view', { style: TAB_CONTENT_STYLE }, content), tabBar);
115
- });
116
- export const Tab = Object.assign(TabImpl, { Screen: TabScreen });
@@ -1,7 +0,0 @@
1
- import 'react-native-screens/src/fabric/ScreenNativeComponent';
2
- import 'react-native-screens/src/fabric/ModalScreenNativeComponent';
3
- import 'react-native-screens/src/fabric/ScreenStackNativeComponent';
4
- import 'react-native-screens/src/fabric/ScreenStackHeaderConfigNativeComponent';
5
- import 'react-native-screens/src/fabric/ScreenStackHeaderSubviewNativeComponent';
6
- import 'react-native-screens/src/fabric/SearchBarNativeComponent';
7
- import 'react-native-screens/src/fabric/ScreenContentWrapperNativeComponent';