@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,40 +0,0 @@
1
- // DrawerRouter: the logic half (framework-agnostic, zero render), sibling to navigator-state.ts
2
- // and tab-router-state.ts. Unlike the stack's push/pop route array, a drawer's route list is
3
- // FIXED at mount (one entry per declared <Drawer.Screen>, collected by the adapter exactly like
4
- // Stack's/Tab's collectRegistry) - the reducer only tracks which one is focused (`index`) and
5
- // whether the drawer panel is open. Route identity/shape is `IRoute`, reused verbatim from
6
- // navigator-state.ts (same convention tab-router-state.ts follows) so all three navigators share
7
- // one route shape.
8
- const INITIAL_FOCUSED_INDEX = 0;
9
- export function createInitialDrawerRouterState(routes, initialRouteName) {
10
- if (initialRouteName === undefined) {
11
- return { routes, index: INITIAL_FOCUSED_INDEX, isOpen: false };
12
- }
13
- const index = routes.findIndex(route => route.name === initialRouteName);
14
- return { routes, index: index === -1 ? INITIAL_FOCUSED_INDEX : index, isOpen: false };
15
- }
16
- // Mirrors @react-navigation/drawer's DrawerActions: jumpTo focuses a route by NAME (TabRouter's
17
- // own convention, not the stack's by-key popTo) AND closes the drawer - selecting a destination
18
- // is itself the dismissal gesture, so a plain focus without also closing would leave the panel
19
- // covering the just-selected screen.
20
- export function drawerRouterReducer(state, action) {
21
- switch (action.type) {
22
- case 'jumpTo': {
23
- const index = state.routes.findIndex(route => route.name === action.name);
24
- if (index === -1)
25
- return state;
26
- return index === state.index && !state.isOpen ? state : { ...state, index, isOpen: false };
27
- }
28
- case 'openDrawer':
29
- return state.isOpen ? state : { ...state, isOpen: true };
30
- case 'closeDrawer':
31
- return state.isOpen ? { ...state, isOpen: false } : state;
32
- case 'toggleDrawer':
33
- return { ...state, isOpen: !state.isOpen };
34
- default:
35
- return state;
36
- }
37
- }
38
- export function focusedDrawerRoute(state) {
39
- return state.routes[state.index];
40
- }
@@ -1,2 +0,0 @@
1
- export declare function isRecord(value: unknown): value is Record<string, unknown>;
2
- export declare function mergeParams(current: unknown, incoming: unknown): unknown;
@@ -1,22 +0,0 @@
1
- // Runtime guards shared across the navigation package's core. Centralizes what used to be ~11
2
- // independent copies of the same `typeof value === 'object' && value !== null` check scattered
3
- // across core and every adapter (route-param merges, persisted-JSON validation, vnode-prop
4
- // narrowing, gesture-event field reads) - several of which had silently drifted on array
5
- // handling, one excluding arrays, another not, for the same kind of check.
6
- //
7
- // Excludes arrays: every current call site (route params, persisted navigator-state JSON,
8
- // touch-event objects, vnode props) only ever receives a genuine plain object, never a bare list
9
- // standing in for one - so `Array.isArray` narrows out a real footgun (a malformed persisted
10
- // value shaped like `[]` would otherwise pass this guard and fail confusingly deeper in, on a
11
- // missing `.key`/`.name` field, instead of here) at zero cost to the realistic inputs.
12
- export function isRecord(value) {
13
- return typeof value === 'object' && value !== null && !Array.isArray(value);
14
- }
15
- // Mirrors @react-navigation's CommonActions.setParams: a shallow merge onto the current params,
16
- // but only when both sides are genuine plain objects - an array (or any other non-record) on
17
- // either side means a clean replace instead, never a numeric-key-mangled merge. Shared by
18
- // navigator-state.ts (Stack) and tab-router-state.ts (Tabs) so the two reducers can't drift on
19
- // this rule independently.
20
- export function mergeParams(current, incoming) {
21
- return isRecord(current) && isRecord(incoming) ? { ...current, ...incoming } : incoming;
22
- }
@@ -1,9 +0,0 @@
1
- import type { ISymbioteEvent } from '@symbiote-native/engine';
2
- import type { IHeaderBarButtonItem } from './navigator-props';
3
- export declare function prepareHeaderBarButtonItems(items: IHeaderBarButtonItem[] | undefined, side: 'left' | 'right'): unknown[] | undefined;
4
- type IHeaderBarButtonDispatch = {
5
- onPressHeaderBarButtonItem?: (event: ISymbioteEvent) => void;
6
- onPressHeaderBarButtonMenuItem?: (event: ISymbioteEvent) => void;
7
- };
8
- export declare function buildHeaderBarButtonDispatch(leftItems: IHeaderBarButtonItem[] | undefined, rightItems: IHeaderBarButtonItem[] | undefined): IHeaderBarButtonDispatch;
9
- export {};
@@ -1,163 +0,0 @@
1
- // Header bar-button payload + dispatch: the native-shaped `headerLeftBarButtonItems`/
2
- // `headerRightBarButtonItems` fold react-native-screens' own ScreenStackHeaderConfig component
3
- // would send down, plus the buttonId/menuId -> onPress lookup that answers RNSScreenStackHeader-
4
- // Config's `onPressHeaderBarButtonItem`/`onPressHeaderBarButtonMenuItem` events. Self-contained:
5
- // only `resolveHeaderConfigView` (screen-options.ts) calls into this file, as a black box - the
6
- // id-tagging scheme below is this cluster's own internal invariant, never a cross-file contract.
7
- import { imageStatics } from '@symbiote-native/components';
8
- import { processColor } from '@symbiote-native/engine';
9
- // RNS flattens `icon` into sfSymbolName/xcassetName/imageSource/templateSource at native-payload
10
- // time (both the structured `icon` field AND the flat native keys reach native - RNS's own
11
- // prepareHeaderBarButtonItems keeps both), and resolves image assets through the same resolver
12
- // the Image component uses.
13
- function prepareIcon(icon) {
14
- if (!icon)
15
- return {};
16
- switch (icon.type) {
17
- case 'sfSymbol':
18
- return { sfSymbolName: icon.name };
19
- case 'xcasset':
20
- return { xcassetName: icon.name };
21
- case 'imageSource':
22
- return { imageSource: imageStatics.resolveAssetSource(icon.imageSource) };
23
- case 'templateSource':
24
- return { templateSource: imageStatics.resolveAssetSource(icon.templateSource) };
25
- }
26
- }
27
- // The one id algorithm both the payload tagger (prepareMenuItems/prepareHeaderBarButtonItems)
28
- // and the dispatch-lookup builder (collectMenuHandlers/collectBarButtonHandlers) call - so a
29
- // change to the id scheme genuinely can't desync payload tagging from dispatch lookup.
30
- function buildMenuId(path, index, side) {
31
- return `${path}-${index}-${side}`;
32
- }
33
- function buildButtonId(index, side) {
34
- return `${index}-${side}`;
35
- }
36
- function prepareTitleStyle(titleStyle) {
37
- if (!titleStyle)
38
- return undefined;
39
- return {
40
- ...titleStyle,
41
- color: titleStyle.color === undefined ? undefined : processColor(titleStyle.color),
42
- };
43
- }
44
- function prepareBadge(badge) {
45
- if (!badge)
46
- return undefined;
47
- return {
48
- ...badge,
49
- style: badge.style
50
- ? {
51
- ...badge.style,
52
- color: badge.style.color === undefined ? undefined : processColor(badge.style.color),
53
- backgroundColor: badge.style.backgroundColor === undefined
54
- ? undefined
55
- : processColor(badge.style.backgroundColor),
56
- }
57
- : undefined,
58
- };
59
- }
60
- // onPress is captured separately into the buttonId/menuId -> handler map (collectMenuHandlers/
61
- // collectBarButtonHandlers below) and must never reach native: a function value fails Fabric's
62
- // dynamic-value serialization, which silently drops the WHOLE headerLeftBarButtonItems/
63
- // headerRightBarButtonItems array it's nested in - not just the one bad field - so bar buttons and
64
- // menu actions never reached native at all, despite the buttonId/menuId dispatch wiring itself
65
- // being entirely correct.
66
- function excludeOnPress(item) {
67
- const { onPress: _onPress, ...rest } = item;
68
- return rest;
69
- }
70
- // Shared fields common to the 'button' and 'menu' item variants (title/icon/tint/badge/...) - the
71
- // 'spacing' variant carries none of these and passes through untouched.
72
- function prepareSharedFields(item) {
73
- return {
74
- ...(item.type === 'button' ? excludeOnPress(item) : item),
75
- ...prepareIcon(item.icon),
76
- titleStyle: prepareTitleStyle(item.titleStyle),
77
- tintColor: item.tintColor === undefined ? undefined : processColor(item.tintColor),
78
- badge: prepareBadge(item.badge),
79
- };
80
- }
81
- // Recursively tags every menu action with a `menuId` derived from its tree position
82
- // (`${path}-${index}-${side}`), mirroring RNS's own prepareMenu exactly - the id must match what
83
- // buildHeaderBarButtonDispatch computes below, or a press silently no-ops.
84
- function prepareMenuItems(menuLike, index, side, path = '') {
85
- return {
86
- ...menuLike,
87
- items: menuLike.items.map((menuItem, menuIndex) => {
88
- const currentPath = path ? `${path}.${menuIndex}` : `${menuIndex}`;
89
- const icon = prepareIcon(menuItem.icon);
90
- if (menuItem.type === 'submenu') {
91
- return { ...menuItem, ...icon, ...prepareMenuItems(menuItem, index, side, currentPath) };
92
- }
93
- return {
94
- ...excludeOnPress(menuItem),
95
- ...icon,
96
- menuId: buildMenuId(currentPath, index, side),
97
- };
98
- }),
99
- };
100
- }
101
- // Builds the native-shaped payload react-native-screens' own ScreenStackHeaderConfig component
102
- // would send down (buttonId/menuId-tagged, colors processColor'd) - this is what
103
- // IHeaderConfigViewProps.headerLeftBarButtonItems/headerRightBarButtonItems actually carry.
104
- export function prepareHeaderBarButtonItems(items, side) {
105
- if (!items)
106
- return undefined;
107
- return items.map((item, index) => {
108
- if (item.type === 'spacing')
109
- return item;
110
- const prepared = prepareSharedFields(item);
111
- if (item.type === 'button')
112
- return { ...prepared, buttonId: buildButtonId(index, side) };
113
- return { ...prepared, menu: prepareMenuItems(item.menu, index, side) };
114
- });
115
- }
116
- function collectMenuHandlers(items, index, side, path, handlers) {
117
- items.forEach((item, menuIndex) => {
118
- const currentPath = path ? `${path}.${menuIndex}` : `${menuIndex}`;
119
- if (item.type === 'submenu') {
120
- collectMenuHandlers(item.items, index, side, currentPath, handlers);
121
- return;
122
- }
123
- handlers.set(buildMenuId(currentPath, index, side), item.onPress);
124
- });
125
- }
126
- function collectBarButtonHandlers(items, side, buttonHandlers, menuHandlers) {
127
- items?.forEach((item, index) => {
128
- if (item.type === 'button') {
129
- buttonHandlers.set(buildButtonId(index, side), item.onPress);
130
- }
131
- else if (item.type === 'menu') {
132
- collectMenuHandlers(item.menu.items, index, side, '', menuHandlers);
133
- }
134
- });
135
- }
136
- function readStringField(record, key) {
137
- const value = record[key];
138
- return typeof value === 'string' ? value : undefined;
139
- }
140
- // The buttonId/menuId → onPress lookup is built independently from prepareHeaderBarButtonItems'
141
- // native payload (rather than re-parsing it back out of the `unknown[]` result), over the
142
- // ORIGINAL typed items - but both sides call the same buildMenuId/buildButtonId, so the id
143
- // scheme lives in one place and can't desync payload tagging from dispatch lookup.
144
- export function buildHeaderBarButtonDispatch(leftItems, rightItems) {
145
- const buttonHandlers = new Map();
146
- const menuHandlers = new Map();
147
- collectBarButtonHandlers(leftItems, 'left', buttonHandlers, menuHandlers);
148
- collectBarButtonHandlers(rightItems, 'right', buttonHandlers, menuHandlers);
149
- if (buttonHandlers.size === 0 && menuHandlers.size === 0)
150
- return {};
151
- return {
152
- onPressHeaderBarButtonItem: event => {
153
- const buttonId = readStringField(event.nativeEvent, 'buttonId');
154
- if (buttonId !== undefined)
155
- buttonHandlers.get(buttonId)?.();
156
- },
157
- onPressHeaderBarButtonMenuItem: event => {
158
- const menuId = readStringField(event.nativeEvent, 'menuId');
159
- if (menuId !== undefined)
160
- menuHandlers.get(menuId)?.();
161
- },
162
- };
163
- }
@@ -1,29 +0,0 @@
1
- export * from './constants';
2
- export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state';
3
- export type { IRoute, INavigatorState, INavigatorAction } from './navigator-state';
4
- export type { IStackAnimation, IStackPresentation, IHeaderBackButtonDisplayMode, IHeaderUserInterfaceStyle, IOptionalBooleanNativeProp, ISheetAllowedDetents, ISheetLargestUndimmedDetent, ISheetInitialDetent, IHeaderBarButtonIcon, IHeaderBarButtonItemBadge, IHeaderBarButtonItemAction, IHeaderBarButtonMenuAction, IHeaderBarButtonSubmenu, IHeaderBarButtonItemMenu, IHeaderBarButtonItemSpacing, IHeaderBarButtonItem, ISearchBarAutoCapitalize, ISearchBarPlacement, ISearchBarOptions, IScreenOrientation, IScreenOptions, INavigatorPlatform, IScreenViewProps, IHeaderConfigViewProps, IStackViewProps, ISearchBarViewProps, } from './navigator-props';
5
- export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options';
6
- export { buildSearchBarHandle } from './search-bar-commands';
7
- export type { ISearchBarCommands } from './search-bar-commands';
8
- export { resolveScreenProps, resolveStackProps, renderHeaderConfig, resolveSearchBarProps, resolveScreenContentWrapperStyle, resolveScreenViewName, isHeaderInModal, resolveHeaderInModalStackStyle, resolveHeaderInModalScreenStyle, buildSearchBarPassthrough, resolveScreenRenderPlan, } from './render-stack';
9
- export type { IScreenRenderPlanInput, IScreenRenderPlan } from './render-stack';
10
- export { serializeNavigatorState, deserializeNavigatorState } from './state-persistence';
11
- export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_STATE, NAVIGATION_EVENT_BEFORE_REMOVE, } from './navigation-events';
12
- export type { INavigationEmitter, INavigationEventListener, INavigationEventName, IFocusTransition, } from './navigation-events';
13
- export { isRecord } from './guards';
14
- export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state';
15
- export type { ITabRouterState, ITabRouterAction } from './tab-router-state';
16
- export type { ITabBarIcon, ITabOptions } from './tab-options';
17
- export { renderTabBarItem, renderTabBar } from './render-tabs';
18
- export type { ITabBarItemView, ITabBarViewProps } from './render-tabs';
19
- export { resolveRouteFromUrl, resolveUrlFromRoute } from './linking-config';
20
- export type { ILinkingConfig, IScreenLinkingConfig } from './linking-config';
21
- export { createInitialDrawerRouterState, drawerRouterReducer, focusedDrawerRoute, } from './drawer-router-state';
22
- export type { IDrawerRouterState, IDrawerRouterAction } from './drawer-router-state';
23
- export type { IDrawerType, IDrawerPosition, IDrawerOptions, IDrawerScreenOptions, IDrawerGeometry, ISwipeIntent, } from './drawer-options';
24
- export { DRAWER_DEFAULT_TYPE, DRAWER_DEFAULT_POSITION, DRAWER_DEFAULT_WIDTH, DRAWER_DEFAULT_OVERLAY_COLOR, DRAWER_DEFAULT_SWIPE_ENABLED, DRAWER_DEFAULT_SWIPE_EDGE_WIDTH, DRAWER_DEFAULT_SWIPE_MIN_DISTANCE, DRAWER_DEFAULT_SWIPE_MIN_VELOCITY, resolveDrawerType, resolveDrawerPosition, resolveDrawerWidth, isDrawerAnimated, isDrawerOverlayVisible, resolveDrawerGeometry, isSwipeStartInEdge, isHorizontalDrag, resolveSwipeIntent, clamp01, startPageXOf, resolveDragProgress, shouldClaimDrawerSwipe, resolveDrawerSlotInterpolation, } from './drawer-options';
25
- export type { IDrawerInterpolationRange, IDrawerContentSlotInterpolation, IDrawerOverlaySlotInterpolation, IDrawerPanelSlotInterpolation, } from './drawer-options';
26
- export { renderDrawer, drawerChildOrder } from './render-drawer';
27
- export type { IDrawerSlot, IDrawerViewProps } from './render-drawer';
28
- export type { INavigatorHandle, ITabNavigatorHandle, IDrawerNavigatorHandle, IDrawerDescriptorMap, IAnyNavigatorHandle, } from './navigator-handles';
29
- export { isStackNavigatorHandle, isTabNavigatorHandle, isDrawerNavigatorHandle, } from './navigator-handles';
@@ -1,32 +0,0 @@
1
- // @symbiote-native/navigation (core): the framework-agnostic half of the native stack navigator -
2
- // a pure route-stack reducer plus the react-native-screens prop folds and native-leaf prop
3
- // resolvers. symbiote ships zero runtime metadata for RNSScreen/RNSScreenStack/
4
- // RNSScreenStackHeaderConfig - the engine derives their events and color processors from
5
- // react-native-screens' own ViewConfig - so this layer is the pure JS every adapter reuses
6
- // verbatim, supplying only its lifecycle (route-key generation, useReducer/reactive state) and
7
- // the descriptor bridge.
8
- export * from './constants.js';
9
- export { createInitialNavigatorState, navigatorReducer, computeActivityState, isTopRoute, } from './navigator-state/index.js';
10
- export { resolveHeaderConfigView, resolveScreenView, resolveSearchBarView } from './screen-options.js';
11
- export { buildSearchBarHandle } from './search-bar-commands.js';
12
- export { resolveScreenProps, resolveStackProps, renderHeaderConfig, resolveSearchBarProps, resolveScreenContentWrapperStyle, resolveScreenViewName, isHeaderInModal, resolveHeaderInModalStackStyle, resolveHeaderInModalScreenStyle, buildSearchBarPassthrough, resolveScreenRenderPlan, } from './render-stack.js';
13
- export { serializeNavigatorState, deserializeNavigatorState } from './state-persistence/index.js';
14
- export { createNavigationEmitter, diffFocusedRoute, NAVIGATION_EVENT_FOCUS, NAVIGATION_EVENT_BLUR, NAVIGATION_EVENT_STATE, NAVIGATION_EVENT_BEFORE_REMOVE, } from './navigation-events/index.js';
15
- // Runtime guards shared across core (and re-exported for adapters narrowing their own untyped
16
- // inputs - vnode props, gesture/event data, persisted JSON) - see guards.ts's header for which
17
- // call sites this consolidates and why array-exclusion is the right default everywhere it's used.
18
- export { isRecord } from './guards.js';
19
- // Bottom-tabs navigator: a FOCUSED-INDEX router (not a stack - see tab-router-state.ts) driving
20
- // a pure-JS tab bar render, framework-agnostic and shared verbatim by every adapter.
21
- export { createInitialTabState, tabRouterReducer, isFocusedRoute } from './tab-router-state/index.js';
22
- export { renderTabBarItem, renderTabBar } from './render-tabs.js';
23
- // Linking config: resolves a URL to a route (and back), the piece @react-navigation's
24
- // NavigationContainer `linking` prop provides on top of the framework-agnostic Linking module.
25
- export { resolveRouteFromUrl, resolveUrlFromRoute } from './linking-config/index.js';
26
- // Drawer navigator: an OPEN/CLOSED-flag router over a fixed route list (see
27
- // drawer-router-state.ts's header for why it isn't a push/pop stack), a pure swipe/geometry math
28
- // module, and a Descriptor render fn - all framework-agnostic, shared verbatim by every adapter.
29
- export { createInitialDrawerRouterState, drawerRouterReducer, focusedDrawerRoute, } from './drawer-router-state/index.js';
30
- export { DRAWER_DEFAULT_TYPE, DRAWER_DEFAULT_POSITION, DRAWER_DEFAULT_WIDTH, DRAWER_DEFAULT_OVERLAY_COLOR, DRAWER_DEFAULT_SWIPE_ENABLED, DRAWER_DEFAULT_SWIPE_EDGE_WIDTH, DRAWER_DEFAULT_SWIPE_MIN_DISTANCE, DRAWER_DEFAULT_SWIPE_MIN_VELOCITY, resolveDrawerType, resolveDrawerPosition, resolveDrawerWidth, isDrawerAnimated, isDrawerOverlayVisible, resolveDrawerGeometry, isSwipeStartInEdge, isHorizontalDrag, resolveSwipeIntent, clamp01, startPageXOf, resolveDragProgress, shouldClaimDrawerSwipe, resolveDrawerSlotInterpolation, } from './drawer-options/index.js';
31
- export { renderDrawer, drawerChildOrder } from './render-drawer.js';
32
- export { isStackNavigatorHandle, isTabNavigatorHandle, isDrawerNavigatorHandle, } from './navigator-handles/index.js';
@@ -1,13 +0,0 @@
1
- import type { IRoute } from '../navigator-state';
2
- export type IScreenLinkingConfig = string | {
3
- path?: string;
4
- screens?: Record<string, IScreenLinkingConfig>;
5
- };
6
- export type ILinkingConfig = {
7
- prefixes: string[];
8
- config: {
9
- screens: Record<string, IScreenLinkingConfig>;
10
- };
11
- };
12
- export declare function resolveRouteFromUrl(config: ILinkingConfig, url: string): IRoute<unknown> | null;
13
- export declare function resolveUrlFromRoute(config: ILinkingConfig, route: IRoute<unknown>): string | null;
@@ -1,171 +0,0 @@
1
- // Linking config: the piece @react-navigation's `NavigationContainer`'s `linking` prop provides
2
- // on top of the framework-agnostic `Linking` module (core/engine/src/linking) - resolving a URL
3
- // ('myapp://user/42', '/user/42') to a route, and a route back to a URL. This is OUR OWN type,
4
- // not imported from react-navigation, kept close to its shape (`prefixes` + `config.screens`)
5
- // only so the DX is familiar.
6
- //
7
- // Why hand-rolled matching instead of react-router's `matchPath`/`generatePath` (evaluated per
8
- // task): react-router 8's package has no subpath export for the pure `lib/router/utils.ts`
9
- // matcher - its only general entry point (`.`) eagerly imports `./lib/dom/*`
10
- // (BrowserRouter/ScrollRestoration/cookies) and `./lib/server-runtime/*`, code that assumes DOM
11
- // and Node globals a Metro/Hermes RN bundle doesn't have; its `peerDependencies` also pin
12
- // `react-dom`, which this DOM-free monorepo never installs (see CLAUDE.md
13
- // react_native_is_an_explicit_top_level_peer - no foreign runtime singleton gets smuggled in).
14
- // On top of the bundling risk, `matchPath`'s/`generatePath`'s param-name extraction is a
15
- // template-literal-type trick that only fires for a compile-time LITERAL path string; our
16
- // patterns come from a runtime config object (`Path` widens to plain `string`), so the generic
17
- // degrades to an empty params type and using the result would force an `as` cast - forbidden by
18
- // this project's TS conventions. A minimal `:param`-segment matcher sidesteps both problems and
19
- // is the right size for a single flat Stack (no optional/splat segments - no nested-navigator use
20
- // for them yet).
21
- import { dlog } from '@symbiote-native/engine';
22
- import { isRecord } from '../guards.js';
23
- // Strips leading/trailing slashes so segments join predictably regardless of how the caller
24
- // wrote a path ('feed/:sort', '/feed/:sort/', etc).
25
- function normalizeSegment(segment) {
26
- return segment.replace(/^\/+|\/+$/g, '');
27
- }
28
- function joinPath(parent, child) {
29
- const parts = [normalizeSegment(parent), normalizeSegment(child)].filter(part => part.length > 0);
30
- return parts.join('/');
31
- }
32
- // Walks `screens` recursively, accumulating the path down to each LEAF screen. Unlike
33
- // react-navigation (which resolves nested config into nested navigation state), our navigator is
34
- // a single flat Stack - a `screens` group with no `path` of its own is transparent (consumes no
35
- // URL segment); a group WITH a `path` but no nested `screens` is itself a leaf using that path.
36
- function flattenScreens(screens, parentPath, out) {
37
- for (const [name, entry] of Object.entries(screens)) {
38
- if (typeof entry === 'string') {
39
- out.push({ name, pattern: joinPath(parentPath, entry) });
40
- continue;
41
- }
42
- const nextParent = entry.path !== undefined ? joinPath(parentPath, entry.path) : parentPath;
43
- if (entry.screens !== undefined) {
44
- flattenScreens(entry.screens, nextParent, out);
45
- }
46
- else if (entry.path !== undefined) {
47
- out.push({ name, pattern: nextParent });
48
- }
49
- else {
50
- dlog(`linking-config: screen "${name}" has neither a path nor nested screens, skipping`);
51
- }
52
- }
53
- return out;
54
- }
55
- function dynamicSegmentCount(pattern) {
56
- if (pattern.length === 0)
57
- return 0;
58
- return pattern.split('/').filter(segment => segment.startsWith(':')).length;
59
- }
60
- function segmentCount(pattern) {
61
- return pattern.length === 0 ? 0 : pattern.split('/').length;
62
- }
63
- // More static segments should win over a param that would also match (e.g. a literal
64
- // '/user/me' pattern over '/user/:id'), mirroring how react-navigation prefers exact matches.
65
- function bySpecificity(a, b) {
66
- const dynamicDiff = dynamicSegmentCount(a.pattern) - dynamicSegmentCount(b.pattern);
67
- if (dynamicDiff !== 0)
68
- return dynamicDiff;
69
- return segmentCount(b.pattern) - segmentCount(a.pattern);
70
- }
71
- function matchPattern(pattern, pathname) {
72
- const patternSegments = pattern.length === 0 ? [] : pattern.split('/');
73
- const pathSegments = pathname.length === 0 ? [] : pathname.split('/');
74
- if (patternSegments.length !== pathSegments.length)
75
- return null;
76
- const params = {};
77
- for (let index = 0; index < patternSegments.length; index += 1) {
78
- const patternSegment = patternSegments[index];
79
- const pathSegment = pathSegments[index];
80
- if (patternSegment.startsWith(':')) {
81
- params[patternSegment.slice(1)] = decodeURIComponent(pathSegment);
82
- }
83
- else if (patternSegment !== pathSegment) {
84
- return null;
85
- }
86
- }
87
- return params;
88
- }
89
- function stripQueryAndHash(url) {
90
- const match = /[?#]/.exec(url);
91
- return match === null ? url : url.slice(0, match.index);
92
- }
93
- // Resolves a full URL down to the bare path segment string ('user/42', no leading slash) by
94
- // stripping the longest matching configured prefix, falling back to treating the URL as an
95
- // already-bare path when it starts with '/' (the task's second example form). Returns null when
96
- // neither applies - an unrecognized scheme/host, not "no route matched".
97
- function extractPathname(config, url) {
98
- const withoutQuery = stripQueryAndHash(url);
99
- const sortedPrefixes = [...config.prefixes].sort((a, b) => b.length - a.length);
100
- for (const prefix of sortedPrefixes) {
101
- if (withoutQuery.startsWith(prefix)) {
102
- return normalizeSegment(withoutQuery.slice(prefix.length));
103
- }
104
- }
105
- if (withoutQuery.startsWith('/'))
106
- return normalizeSegment(withoutQuery);
107
- return null;
108
- }
109
- export function resolveRouteFromUrl(config, url) {
110
- const pathname = extractPathname(config, url);
111
- if (pathname === null) {
112
- dlog(`linking-config: url "${url}" matched no configured prefix`);
113
- return null;
114
- }
115
- const candidates = flattenScreens(config.config.screens, '', []).sort(bySpecificity);
116
- for (const candidate of candidates) {
117
- const params = matchPattern(candidate.pattern, pathname);
118
- if (params !== null) {
119
- dlog(`linking-config: resolved "${url}" -> ${candidate.name}`);
120
- // `key` is not real route identity here - the navigator mints its own on push/replace
121
- // (stack.ts's createRoute); `name` is a stable enough placeholder to satisfy IRoute's shape.
122
- return {
123
- key: candidate.name,
124
- name: candidate.name,
125
- params: Object.keys(params).length > 0 ? params : undefined,
126
- };
127
- }
128
- }
129
- dlog(`linking-config: url "${url}" (path "${pathname}") matched no configured screen`);
130
- return null;
131
- }
132
- // The inverse of matchPattern: fills a pattern's ':param' segments from `params`, or returns
133
- // null when a required param is missing (mirrors react-router's generatePath throwing, minus the
134
- // throw - a resolver returning null reads more naturally at the call site here).
135
- function fillPattern(pattern, params) {
136
- if (pattern.length === 0)
137
- return '';
138
- const source = isRecord(params) ? params : {};
139
- const segments = [];
140
- for (const segment of pattern.split('/')) {
141
- if (!segment.startsWith(':')) {
142
- segments.push(segment);
143
- continue;
144
- }
145
- const value = source[segment.slice(1)];
146
- if (value === undefined || value === null)
147
- return null;
148
- segments.push(encodeURIComponent(String(value)));
149
- }
150
- return segments.join('/');
151
- }
152
- export function resolveUrlFromRoute(config, route) {
153
- const candidate = flattenScreens(config.config.screens, '', []).find(entry => entry.name === route.name);
154
- if (candidate === undefined) {
155
- dlog(`linking-config: no configured screen for route name "${route.name}"`);
156
- return null;
157
- }
158
- const path = fillPattern(candidate.pattern, route.params);
159
- if (path === null) {
160
- dlog(`linking-config: route "${route.name}" is missing a param required by "${candidate.pattern}"`);
161
- return null;
162
- }
163
- const prefix = config.prefixes[0];
164
- if (prefix === undefined)
165
- return path.length > 0 ? `/${path}` : '/';
166
- // A prefix already carrying its own trailing separator ('myapp://') must not be truncated to
167
- // 'myapp:/' by naively slicing one char off - only add a '/' when the prefix doesn't have one.
168
- if (path.length === 0)
169
- return prefix;
170
- return prefix.endsWith('/') ? `${prefix}${path}` : `${prefix}/${path}`;
171
- }
@@ -1,16 +0,0 @@
1
- export declare const NAVIGATION_EVENT_FOCUS = "focus";
2
- export declare const NAVIGATION_EVENT_BLUR = "blur";
3
- export declare const NAVIGATION_EVENT_STATE = "state";
4
- export declare const NAVIGATION_EVENT_BEFORE_REMOVE = "beforeRemove";
5
- export type INavigationEventName = typeof NAVIGATION_EVENT_FOCUS | typeof NAVIGATION_EVENT_BLUR | typeof NAVIGATION_EVENT_STATE | typeof NAVIGATION_EVENT_BEFORE_REMOVE;
6
- export type INavigationEventListener<TData = unknown> = (data: TData) => void;
7
- export type INavigationEmitter = {
8
- emit: (event: INavigationEventName, data?: unknown) => void;
9
- addListener: (event: INavigationEventName, listener: INavigationEventListener) => () => void;
10
- };
11
- export declare function createNavigationEmitter(): INavigationEmitter;
12
- export type IFocusTransition = {
13
- blurKey?: string;
14
- focusKey?: string;
15
- };
16
- export declare function diffFocusedRoute(prevKey: string | undefined, nextKey: string | undefined): IFocusTransition;
@@ -1,57 +0,0 @@
1
- // The framework-agnostic half of what @react-navigation exposes as `navigation.addListener` /
2
- // `useFocusEffect` / `useIsFocused` / `useNavigationState`: a plain pub/sub emitter, zero React.
3
- // addListener is NOT React-specific there either - React's hooks are thin useEffect wrappers
4
- // around exactly this subscribe/unsubscribe shape (confirmed against the 8.x docs: `const
5
- // unsubscribe = navigation.addListener('focus', cb); return unsubscribe;` inside a bare
6
- // `useEffect`). One emitter per route (created by the adapter, e.g. stack.ts's per-route
7
- // `emitters` map) carries that route's own focus/blur/state lifecycle; every adapter's lifecycle
8
- // hooks subscribe to it, matching this file's single responsibility split from `constants.ts`
9
- // (which only names Fabric view/prop-key strings, not this JS-only event system).
10
- //
11
- // diffFocusedRoute below centralizes the "which route lost focus, which gained it" comparison
12
- // that tabs.ts and drawer.ts independently reimplemented in all three adapters (each watching its
13
- // own router state for the focused index/key changing, then manually emitting BLUR to the old key
14
- // and FOCUS to the new one, plus an initial FOCUS seed on first mount) - the diff is pure and
15
- // identical everywhere; only WHEN to run it differs per framework (a `useEffect` dependency array,
16
- // a Vue `watch`, an idempotent-per-read Angular method), so that half stays adapter-owned.
17
- import { dlog } from '@symbiote-native/engine';
18
- export const NAVIGATION_EVENT_FOCUS = 'focus';
19
- export const NAVIGATION_EVENT_BLUR = 'blur';
20
- export const NAVIGATION_EVENT_STATE = 'state';
21
- export const NAVIGATION_EVENT_BEFORE_REMOVE = 'beforeRemove';
22
- export function createNavigationEmitter() {
23
- const listenersByEvent = new Map();
24
- function addListener(event, listener) {
25
- let listeners = listenersByEvent.get(event);
26
- if (!listeners) {
27
- listeners = new Set();
28
- listenersByEvent.set(event, listeners);
29
- }
30
- listeners.add(listener);
31
- return () => {
32
- listeners?.delete(listener);
33
- };
34
- }
35
- function emit(event, data) {
36
- const listeners = listenersByEvent.get(event);
37
- // Investigation instrumentation (flicker-on-focus bug): every emission, whether or not a
38
- // listener is attached, so the log stream shows if focus/blur ever fires with 0 subscribers
39
- // (a silent no-op) vs racing an actual effect. Kept behind DEBUG, never removed.
40
- dlog(`Navigation emitter: emit "${event}" (${listeners?.size ?? 0} listener(s)) at t=${Date.now()}`);
41
- if (!listeners)
42
- return;
43
- for (const listener of listeners)
44
- listener(data);
45
- }
46
- return { emit, addListener };
47
- }
48
- export function diffFocusedRoute(prevKey, nextKey) {
49
- if (prevKey === nextKey)
50
- return {};
51
- const transition = {};
52
- if (prevKey !== undefined)
53
- transition.blurKey = prevKey;
54
- if (nextKey !== undefined)
55
- transition.focusKey = nextKey;
56
- return transition;
57
- }
@@ -1,30 +0,0 @@
1
- import type { INavigatorState } from '../navigator-state';
2
- import type { IDrawerScreenOptions } from '../drawer-options';
3
- export type INavigatorHandle = {
4
- push: (name: string, params?: unknown) => void;
5
- pop: (count?: number) => void;
6
- popToTop: () => void;
7
- popTo: (key: string) => void;
8
- replace: (name: string, params?: unknown) => void;
9
- setParams: (params: unknown, key?: string) => void;
10
- reset: (state: INavigatorState) => void;
11
- canGoBack: () => boolean;
12
- };
13
- export type ITabNavigatorHandle = {
14
- jumpTo: (name: string, params?: unknown) => void;
15
- setParams: (params: unknown, key: string) => void;
16
- };
17
- export type IDrawerNavigatorHandle = {
18
- openDrawer: () => void;
19
- closeDrawer: () => void;
20
- toggleDrawer: () => void;
21
- jumpTo: (name: string) => void;
22
- };
23
- export type IDrawerDescriptorMap = Record<string, {
24
- options: IDrawerScreenOptions;
25
- navigation: IDrawerNavigatorHandle;
26
- }>;
27
- export type IAnyNavigatorHandle = INavigatorHandle | ITabNavigatorHandle | IDrawerNavigatorHandle;
28
- export declare function isStackNavigatorHandle(handle: IAnyNavigatorHandle): handle is INavigatorHandle;
29
- export declare function isDrawerNavigatorHandle(handle: IAnyNavigatorHandle): handle is IDrawerNavigatorHandle;
30
- export declare function isTabNavigatorHandle(handle: IAnyNavigatorHandle): handle is ITabNavigatorHandle;
@@ -1,15 +0,0 @@
1
- // Imperative navigator handles: framework-agnostic (scalars, `unknown`, plain function
2
- // signatures over `INavigatorState`/`IDrawerScreenOptions` - no framework element, ref, or
3
- // children in any field), so each shape is declared once here and every adapter re-exports it
4
- // verbatim (see CLAUDE.md <prop_types_split_agnostic_vs_per_adapter>).
5
- export function isStackNavigatorHandle(handle) {
6
- return 'push' in handle;
7
- }
8
- export function isDrawerNavigatorHandle(handle) {
9
- return 'openDrawer' in handle;
10
- }
11
- // jumpTo exists on both Tab and Drawer handles, so a Tab handle is a jumpTo-having handle that
12
- // isn't a Drawer handle (Drawer's own openDrawer/closeDrawer/toggleDrawer rule it out).
13
- export function isTabNavigatorHandle(handle) {
14
- return 'jumpTo' in handle && !isDrawerNavigatorHandle(handle);
15
- }