@apexnative/navigation 1.0.0-alpha.3 → 1.0.0-alpha.4

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 (193) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/android/CLAUDE.md +17 -7
  3. package/android/CORE.md +8 -3
  4. package/android/RECIPES.md +9 -3
  5. package/android/src/main/java/dev/apexnative/navigation/NavigationApplication.kt +34 -13
  6. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutChecks.kt +5 -1
  7. package/android/src/main/java/dev/apexnative/navigation/layout/LayoutNode.kt +6 -2
  8. package/android/src/main/java/dev/apexnative/navigation/nativescreen/NativeScreenContext.kt +2 -2
  9. package/android/src/main/java/dev/apexnative/navigation/options/ButtonMenuOptions.kt +2 -2
  10. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +2 -2
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionsCache.kt +8 -4
  12. package/android/src/main/java/dev/apexnative/navigation/options/UserChoices.kt +159 -0
  13. package/android/src/main/java/dev/apexnative/navigation/react/CommandAnswer.kt +2 -24
  14. package/android/src/main/java/dev/apexnative/navigation/react/CommandContract.kt +59 -0
  15. package/android/src/main/java/dev/apexnative/navigation/react/Hinge.kt +27 -19
  16. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +5 -4
  17. package/android/src/main/java/dev/apexnative/navigation/react/PendingCalls.kt +48 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/events/AppearanceDispatcher.kt +17 -7
  19. package/android/src/main/java/dev/apexnative/navigation/slots/SlotContainers.kt +10 -7
  20. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +4 -3
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotViews.kt +40 -0
  22. package/android/src/main/java/dev/apexnative/navigation/utils/ImageKey.kt +66 -0
  23. package/android/src/main/java/dev/apexnative/navigation/utils/ImageLoader.kt +7 -89
  24. package/android/src/main/java/dev/apexnative/navigation/utils/ImageRequests.kt +76 -0
  25. package/android/src/main/java/dev/apexnative/navigation/utils/OptionImages.kt +262 -0
  26. package/android/src/main/java/dev/apexnative/navigation/utils/ViewController.kt +3 -1
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/AppliedTabCalls.kt +49 -0
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsController.kt +16 -6
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/bottomtabs/BottomTabsPresenter.kt +25 -24
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/ContentReadiness.kt +1 -1
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/component/RenderWait.kt +27 -7
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalCoordinator.kt +5 -5
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/Navigator.kt +4 -3
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/navigator/RootCoordinator.kt +6 -3
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/overlay/OverlayCoordinator.kt +8 -8
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/parent/Capabilities.kt +11 -4
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuController.kt +1 -1
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitPanes.kt +23 -0
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/splitview/SplitViewController.kt +58 -15
  40. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackCommands.kt +28 -18
  41. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +16 -7
  42. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackDisplay.kt +2 -1
  43. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +85 -29
  44. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +2 -2
  45. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +7 -5
  46. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +3 -3
  47. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/MenuChecks.kt +13 -56
  48. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +10 -2
  49. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/viewcontroller/ViewController.kt +56 -21
  50. package/android/src/main/java/dev/apexnative/navigation/views/bottomtabs/BottomTabs.kt +4 -2
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/Fab.kt +14 -10
  52. package/cli/doctor/checks/app.js +61 -13
  53. package/cli/migrate/native.js +18 -0
  54. package/cli/setup/source.js +2 -1
  55. package/cli/setup/steps/mainApplication.js +134 -19
  56. package/docs-md/architecture/transitions.md +6 -7
  57. package/docs-md/help/glossary.md +1 -1
  58. package/docs-md/help/troubleshooting.md +20 -3
  59. package/docs-md/index.json +4 -0
  60. package/docs-md/migration/from-react-native-navigation.md +8 -2
  61. package/docs-md/screen-ui/back-and-gestures.md +2 -2
  62. package/docs-md/start/installation.md +3 -1
  63. package/ios/CLAUDE.md +17 -5
  64. package/ios/CORE.md +13 -8
  65. package/ios/Contract/ANNCommandAnswer.mm +5 -20
  66. package/ios/Contract/ANNCommandContract.h +15 -0
  67. package/ios/Contract/ANNCommandContract.mm +78 -0
  68. package/ios/Core/ANNAppearanceDispatcher.mm +1 -1
  69. package/ios/Core/ANNErrorHandler.h +1 -1
  70. package/ios/Core/ANNErrorHandler.mm +3 -1
  71. package/ios/Core/ANNEventEmitter.h +1 -1
  72. package/ios/Core/ANNEventEmitter.mm +2 -2
  73. package/ios/Core/ANNJavaScriptCalls.h +34 -0
  74. package/ios/Core/ANNJavaScriptCalls.mm +61 -0
  75. package/ios/Core/ANNNavigationState.h +7 -3
  76. package/ios/Core/ANNNavigationState.mm +10 -5
  77. package/ios/Core/ANNNodeHelper.h +32 -12
  78. package/ios/Core/ANNNodeHelper.mm +100 -3
  79. package/ios/Core/ANNRenderWait.h +15 -1
  80. package/ios/Core/ANNRenderWait.mm +119 -17
  81. package/ios/Core/UIViewController+LayoutProtocol.h +9 -2
  82. package/ios/Core/UIViewController+LayoutProtocol.mm +8 -0
  83. package/ios/Layouts/Modal/ANNModalCoordinator.h +1 -1
  84. package/ios/Layouts/Modal/ANNModalCoordinator.mm +55 -14
  85. package/ios/Layouts/Screen/ANNComponentViewController.h +8 -0
  86. package/ios/Layouts/Screen/ANNComponentViewController.mm +32 -5
  87. package/ios/Layouts/Screen/ANNScreenPlace.h +41 -0
  88. package/ios/Layouts/Screen/ANNScreenPlace.mm +175 -0
  89. package/ios/Layouts/SplitView/ANNSplitViewCommands.h +3 -2
  90. package/ios/Layouts/SplitView/ANNSplitViewCommands.mm +105 -39
  91. package/ios/Layouts/SplitView/ANNSplitViewController.mm +4 -2
  92. package/ios/Layouts/Stack/ANNStackCommands.mm +1 -0
  93. package/ios/Layouts/Stack/ANNStackController.mm +26 -3
  94. package/ios/Layouts/Stack/ANNStackQueue.h +3 -1
  95. package/ios/Layouts/Stack/ANNStackQueue.mm +56 -43
  96. package/ios/Layouts/Stack/UINavigationController+ANNCommands.h +4 -2
  97. package/ios/Layouts/Stack/UINavigationController+ANNCommands.mm +2 -3
  98. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.h +4 -0
  99. package/ios/Layouts/Tabs/ANNBottomTabItemBuilder.mm +11 -0
  100. package/ios/Layouts/Tabs/ANNBottomTabsController.mm +13 -5
  101. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.h +2 -2
  102. package/ios/Layouts/Tabs/ANNBottomTabsPresenter.mm +109 -7
  103. package/ios/Options/ANNOptionsCache.h +7 -6
  104. package/ios/Options/ANNSearchBarOptions.h +4 -3
  105. package/ios/Options/ANNUserChoice.h +57 -0
  106. package/ios/Options/ANNUserChoice.mm +44 -0
  107. package/ios/Options/Params/ANNImageSource.h +61 -0
  108. package/ios/Options/Params/ANNImageSource.mm +101 -0
  109. package/ios/Options/Params/ANNOptionImages.h +37 -0
  110. package/ios/Options/Params/ANNOptionImages.mm +284 -0
  111. package/ios/Options/Params/Image.h +11 -2
  112. package/ios/Options/Params/Image.mm +15 -2
  113. package/ios/Options/Params/ImageParser.h +5 -5
  114. package/ios/Options/Params/ImageParser.mm +28 -46
  115. package/ios/Public/ApexNavigation/ApexNavigation.h +1 -1
  116. package/ios/RECIPES.md +11 -5
  117. package/ios/React/Slots/ANNSlotComponentView.mm +24 -17
  118. package/ios/React/Slots/ANNSlotContainers.h +14 -6
  119. package/ios/React/Slots/ANNSlotContainers.mm +75 -2
  120. package/ios/TopBar/ANNBarButtons.h +9 -5
  121. package/ios/TopBar/ANNBarButtons.mm +211 -63
  122. package/ios/TopBar/ANNIconButtonImage.h +1 -0
  123. package/ios/TopBar/ANNIconButtonImage.mm +34 -1
  124. package/ios/TopBar/ANNLargeTitlePresenter.h +13 -0
  125. package/ios/TopBar/ANNLargeTitlePresenter.mm +26 -3
  126. package/ios/TopBar/ANNSearchPresenter.mm +170 -19
  127. package/ios/TopBar/ANNTopBarPresenter.h +5 -0
  128. package/ios/TopBar/ANNTopBarPresenter.mm +60 -11
  129. package/ios/TurboModules/ANNTurboModule.mm +39 -21
  130. package/lib/module/ARCHITECTURE.md +18 -18
  131. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  132. package/lib/module/api/navigation.js.map +1 -1
  133. package/lib/module/engine/commands.js +9 -7
  134. package/lib/module/engine/commands.js.map +1 -1
  135. package/lib/module/engine/layout.js +13 -9
  136. package/lib/module/engine/layout.js.map +1 -1
  137. package/lib/module/engine/options.js +44 -11
  138. package/lib/module/engine/options.js.map +1 -1
  139. package/lib/module/engine/places.js +44 -7
  140. package/lib/module/engine/places.js.map +1 -1
  141. package/lib/module/engine/props.js +30 -1
  142. package/lib/module/engine/props.js.map +1 -1
  143. package/lib/module/engine/rememberedOptions.js +1 -1
  144. package/lib/module/engine/rememberedOptions.js.map +1 -1
  145. package/lib/module/engine/screens.js +22 -12
  146. package/lib/module/engine/screens.js.map +1 -1
  147. package/lib/module/interfaces/contract/commands.json +101 -0
  148. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +5 -2
  149. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  150. package/lib/typescript/api/events/navigationEvents.d.ts +1 -1
  151. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  152. package/lib/typescript/api/hooks/useNavigation.d.ts +1 -1
  153. package/lib/typescript/api/hooks/useNavigation.d.ts.map +1 -1
  154. package/lib/typescript/api/navigation.d.ts +4 -1
  155. package/lib/typescript/api/navigation.d.ts.map +1 -1
  156. package/lib/typescript/engine/commands.d.ts.map +1 -1
  157. package/lib/typescript/engine/options.d.ts +5 -2
  158. package/lib/typescript/engine/options.d.ts.map +1 -1
  159. package/lib/typescript/engine/places.d.ts +5 -2
  160. package/lib/typescript/engine/places.d.ts.map +1 -1
  161. package/lib/typescript/engine/props.d.ts +8 -1
  162. package/lib/typescript/engine/props.d.ts.map +1 -1
  163. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  164. package/lib/typescript/engine/screens.d.ts.map +1 -1
  165. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  166. package/lib/typescript/interfaces/ComponentEvents.d.ts.map +1 -1
  167. package/lib/typescript/interfaces/Events.d.ts +19 -10
  168. package/lib/typescript/interfaces/Events.d.ts.map +1 -1
  169. package/lib/typescript/interfaces/options/animations.d.ts +5 -2
  170. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  171. package/lib/typescript/interfaces/options/buttons.d.ts +13 -5
  172. package/lib/typescript/interfaces/options/buttons.d.ts.map +1 -1
  173. package/lib/typescript/interfaces/options/topBar.d.ts +22 -9
  174. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  175. package/package.json +1 -1
  176. package/src/ARCHITECTURE.md +18 -18
  177. package/src/adapters/NativeANNTurboModule.ts +5 -2
  178. package/src/api/events/navigationEvents.ts +1 -1
  179. package/src/api/hooks/useNavigation.ts +1 -1
  180. package/src/api/navigation.ts +4 -1
  181. package/src/engine/commands.ts +19 -7
  182. package/src/engine/layout.ts +16 -9
  183. package/src/engine/options.ts +39 -11
  184. package/src/engine/places.ts +56 -7
  185. package/src/engine/props.ts +30 -1
  186. package/src/engine/rememberedOptions.ts +4 -1
  187. package/src/engine/screens.tsx +23 -13
  188. package/src/interfaces/ComponentEvents.ts +2 -2
  189. package/src/interfaces/Events.ts +19 -10
  190. package/src/interfaces/contract/commands.json +101 -0
  191. package/src/interfaces/options/animations.ts +5 -2
  192. package/src/interfaces/options/buttons.ts +13 -5
  193. package/src/interfaces/options/topBar.ts +22 -9
@@ -22,7 +22,7 @@ import type { ScreenName, ShowArgs } from '../types';
22
22
  interface ScreenCommands {
23
23
  /** The screen the commands act from. */
24
24
  readonly screen: ScreenInfo;
25
- /** Shows a screen over this one in its stack; resolves with the id of the screen shown. */
25
+ /** Shows a screen over this one in its stack; resolves with the id of the screen shown (for a pair, the pair's id). */
26
26
  push<Name extends ScreenName>(name: Name, ...args: ShowArgs<Name, PushConfig>): Promise<string>;
27
27
  /**
28
28
  * Goes back to the screen before this one in its stack. The first screen of a split view column shown over the list
@@ -55,7 +55,10 @@ export interface AppNavigation {
55
55
  switchTo<const Layout extends Exclude<RootLayout, string>>(
56
56
  layout: Layout & NoInfer<CheckedScreens<UsesOfRoot<Layout>, RegisteredScreens>>
57
57
  ): Promise<void>;
58
- /** Shows a screen in the stack of the screen the user sees; resolves with the id of the screen shown. */
58
+ /**
59
+ * Shows a screen in the stack of the screen the user sees; resolves with the id of the screen shown (for a pair, the
60
+ * pair's id).
61
+ */
59
62
  push<Name extends ScreenName>(name: Name, ...args: ShowArgs<Name, PushConfig>): Promise<string>;
60
63
  /** Goes back in the stack of the screen the user sees; see `useNavigation().pop`. */
61
64
  pop(): Promise<string>;
@@ -9,7 +9,7 @@ import { whenNativeScreensReady } from './nativeScreens';
9
9
  import { commands as native } from './native';
10
10
  import { toNativeOptions } from './options';
11
11
  import { dropReactRenderMeasures } from './reactMeasures';
12
- import { columnOf, forgetPlace, notePlaces, placeOf, stackOfColumn } from './places';
12
+ import { columnOf, forgetPlace, layoutsOf, notePlaces, placeOf, stackOfColumn } from './places';
13
13
  import { forgetProps } from './props';
14
14
  import {
15
15
  forgetMergedOptions,
@@ -122,7 +122,7 @@ export function setRoot(root: LayoutRoot): Promise<string> {
122
122
 
123
123
  export function setDefaultOptions(options: Options): void {
124
124
  rememberDefaultOptions(options);
125
- native.setDefaultOptions(toNativeOptions(options));
125
+ native.setDefaultOptions(toNativeOptions(options, 'defaultOptions'));
126
126
  }
127
127
 
128
128
  // The show commands that wait in JS, by number: for lazy screens to load, for the first props of native screens, for a
@@ -141,7 +141,7 @@ const preparedScreens = new Set<string>();
141
141
  * engine remembers them by the node's id, so the root set again for a new launch of the native side shows them too.
142
142
  */
143
143
  export function mergeOptions(componentId: string, options: Options): void {
144
- const nativeOptions = toNativeOptions(options);
144
+ const nativeOptions = toNativeOptions(options, componentId);
145
145
  const send = () => {
146
146
  rememberMergedOptions(componentId, options);
147
147
  native.mergeOptions(componentId, nativeOptions);
@@ -225,7 +225,8 @@ export function push(
225
225
  from?.stack ?? null,
226
226
  (from?.index ?? 0) + 1,
227
227
  from?.menu,
228
- from?.layer
228
+ from?.layer,
229
+ layoutsOf(componentId)
229
230
  );
230
231
  return whenNativeScreensReady([node], () =>
231
232
  sendPush(commandId, componentId, node, prepares, wanted, goneOut).then((shownId) => {
@@ -363,7 +364,8 @@ export function setStackRoot(componentId: string, children: Layout[]): Promise<s
363
364
  from?.stack ?? null,
364
365
  index,
365
366
  from?.menu,
366
- from?.layer
367
+ from?.layer,
368
+ layoutsOf(componentId)
367
369
  )
368
370
  );
369
371
  return whenNativeScreensReady(nodes, () =>
@@ -383,7 +385,17 @@ export function showInColumn(componentId: string, column: string, layout: Layout
383
385
  showScreensFrom(componentId, node);
384
386
  const from = placeOf(componentId);
385
387
  const at = columnPlace(componentId, column);
386
- notePlaces(node, from?.container ?? 'root', at.stack, at.index, from?.menu, from?.layer);
388
+ // In the split view of the screen it comes from, and in the column's stack.
389
+ const within = at.stack ? [...layoutsOf(componentId), at.stack] : layoutsOf(componentId);
390
+ notePlaces(
391
+ node,
392
+ from?.container ?? 'root',
393
+ at.stack,
394
+ at.index,
395
+ from?.menu,
396
+ from?.layer,
397
+ within
398
+ );
387
399
  return whenNativeScreensReady([node], () =>
388
400
  inOrder(() =>
389
401
  sendShow(goneOut, () => native.showInColumn(commandId, componentId, column, node))
@@ -431,7 +443,7 @@ export function dismissModal(componentId: string, options?: Options): Promise<st
431
443
  dropReactRenderMeasures();
432
444
  const commandId = nextId('dismissModal');
433
445
  return inOrder(() =>
434
- native.dismissModal(commandId, componentId, options && toNativeOptions(options))
446
+ native.dismissModal(commandId, componentId, options && toNativeOptions(options, componentId))
435
447
  );
436
448
  }
437
449
 
@@ -122,7 +122,7 @@ function screen(component: LayoutComponent, inPair = false, keyed = !inPair): La
122
122
  type: 'Component',
123
123
  data: {
124
124
  name: rendersInSlot(name) ? slotName(id, name) : name,
125
- options: toNativeOptions(options),
125
+ options: toNativeOptions(options, id),
126
126
  ...(key !== undefined && { key }),
127
127
  },
128
128
  children: [],
@@ -167,12 +167,13 @@ function arrangement(
167
167
  }
168
168
 
169
169
  function external(component: ExternalComponent): LayoutNode {
170
+ const id = component.id || nextId('ExternalComponent');
170
171
  return {
171
- id: component.id || nextId('ExternalComponent'),
172
+ id,
172
173
  type: 'ExternalComponent',
173
174
  data: {
174
175
  name: String(component.name),
175
- options: toNativeOptions(component.options),
176
+ options: toNativeOptions(component.options, id),
176
177
  passProps: component.passProps,
177
178
  },
178
179
  children: [],
@@ -185,10 +186,11 @@ function parent(
185
186
  options: Options | undefined,
186
187
  children: Layout[] = []
187
188
  ): LayoutNode {
189
+ const nodeId = id || nextId(type);
188
190
  return {
189
- id: id || nextId(type),
191
+ id: nodeId,
190
192
  type,
191
- data: { options: toNativeOptions(options) },
193
+ data: { options: toNativeOptions(options, nodeId) },
192
194
  children: children.map(toNativeLayout),
193
195
  };
194
196
  }
@@ -213,10 +215,11 @@ function sideMenu(menu: LayoutSideMenu): LayoutNode {
213
215
  data: {},
214
216
  children: [toNativeLayout(content)],
215
217
  });
218
+ const id = menu.id || nextId('SideMenuRoot');
216
219
  return {
217
- id: menu.id || nextId('SideMenuRoot'),
220
+ id,
218
221
  type: 'SideMenuRoot',
219
- data: { options: toNativeOptions(menu.options) },
222
+ data: { options: toNativeOptions(menu.options, id) },
220
223
  children: [
221
224
  ...(menu.left ? [side('SideMenuLeft', menu.left)] : []),
222
225
  side('SideMenuCenter', menu.center),
@@ -238,10 +241,14 @@ function splitView(split: LayoutSplitView): LayoutNode {
238
241
  ] as [string, Layout | undefined][]
239
242
  ).filter((column): column is [string, Layout] => column[1] !== undefined);
240
243
  const present = split.master && split.detail ? columns : [];
244
+ const id = split.id || nextId('SplitView');
241
245
  return {
242
- id: split.id || nextId('SplitView'),
246
+ id,
243
247
  type: 'SplitView',
244
- data: { options: toNativeOptions(split.options), columns: present.map(([column]) => column) },
248
+ data: {
249
+ options: toNativeOptions(split.options, id),
250
+ columns: present.map(([column]) => column),
251
+ },
245
252
  children: present.map(([, content]) => toNativeLayout(content)),
246
253
  };
247
254
  }
@@ -1,17 +1,27 @@
1
1
  import { DynamicColorIOS, Image, Platform, processColor } from 'react-native';
2
2
  import type { ColorValue, ImageSourcePropType } from 'react-native';
3
3
  import { nextId } from './ids';
4
- import { rememberButtonRoot, setProps } from './props';
4
+ import { rememberButtonRoot, rememberOptionsComponent, setProps } from './props';
5
5
 
6
6
  type Fields = Record<string, unknown>;
7
7
 
8
8
  /**
9
9
  * The options as the native side reads them: colors in the format of the platform, images resolved, and every
10
10
  * component in them with an id of its own (its props stay in JavaScript). Options come in their own form only:
11
- * animations with `enter` and `exit`, `searchBar` and `interpolation` as objects.
11
+ * animations with `enter` and `exit`, `searchBar` and `interpolation` as objects. `owner` is the id of the node the
12
+ * options are for, or `'defaultOptions'`: a component without an id of its own gets one made of the owner, its place
13
+ * in the options and its name, the same for every `mergeOptions` of the node, so the native side keeps the mounted
14
+ * component and only its props change. Without an owner each conversion gives it a new one.
12
15
  */
13
- export function toNativeOptions<T>(options: T): T {
14
- return convert(options) as T;
16
+ export function toNativeOptions<T>(options: T, owner?: string): T {
17
+ return convert(options, undefined, { owner, path: '' }) as T;
18
+ }
19
+
20
+ // Where a value is in the options being converted: the node they are for, and the path to the value
21
+ // (`topBar.rightButtons[0].component`).
22
+ interface Place {
23
+ owner: string | undefined;
24
+ path: string;
15
25
  }
16
26
 
17
27
  /**
@@ -35,11 +45,16 @@ function isGroup(value: unknown): value is Fields {
35
45
  return isFields(value) && !Array.isArray(value);
36
46
  }
37
47
 
38
- function convert(value: unknown, key?: string): unknown {
48
+ function convert(value: unknown, key: string | undefined, place: Place): unknown {
39
49
  if (key !== undefined && isColor(key)) return nativeColor(value);
40
50
  if (key !== undefined && isImage(key)) return nativeImage(value);
41
51
  if (Array.isArray(value)) {
42
- const items = value.map((item) => convert(item));
52
+ // An item is known by its id where it has one (a button, a suggestion), so a component keeps its root when the
53
+ // list around it changes; by its place in the list otherwise.
54
+ const items = value.map((item, index) => {
55
+ const id = isFields(item) && typeof item.id === 'string' && item.id ? item.id : index;
56
+ return convert(item, undefined, { ...place, path: `${place.path}[${id}]` });
57
+ });
43
58
  if (key?.endsWith('Buttons') || key === 'buttons') items.forEach(noteButtonRoot);
44
59
  return key?.endsWith('Buttons') ? items.map(takeButtonProps) : items;
45
60
  }
@@ -47,9 +62,12 @@ function convert(value: unknown, key?: string): unknown {
47
62
  const result: Fields = {};
48
63
  for (const [name, item] of Object.entries(withSymbols(value))) {
49
64
  // The props of a component are its own, not options.
50
- result[name] = name === 'passProps' ? item : convert(item, name);
65
+ const path = place.path ? `${place.path}.${name}` : name;
66
+ result[name] = name === 'passProps' ? item : convert(item, name, { ...place, path });
51
67
  }
52
- return key === 'component' || key === 'resultsComponent' ? takeComponentProps(result) : result;
68
+ return key === 'component' || key === 'resultsComponent'
69
+ ? takeComponentProps(result, place)
70
+ : result;
53
71
  }
54
72
 
55
73
  function isColor(key: string): boolean {
@@ -105,13 +123,23 @@ function isFields(value: unknown): value is Fields {
105
123
  return typeof value === 'object' && value !== null;
106
124
  }
107
125
 
108
- // A component in the options gets an id; the native side creates it with that id, and its props wait here.
109
- function takeComponentProps({ passProps, ...component }: Fields): Fields {
110
- const componentId = (component.id as string | undefined) || nextId('CustomComponent');
126
+ // A component in the options gets an id; the native side creates it with that id, and its props wait here. Without
127
+ // an id of its own it is the owner's, the place of the component (`topBar.title`, `topBar.rightButtons[Save]` by the
128
+ // button's id, `topBar.searchBar.resultsComponent`) and its name: another name there is another component.
129
+ function takeComponentProps({ passProps, ...component }: Fields, place: Place): Fields {
130
+ const componentId = (component.id as string | undefined) || componentIdAt(place, component.name);
111
131
  if (passProps) setProps(componentId, passProps as object);
112
132
  return { ...component, componentId };
113
133
  }
114
134
 
135
+ function componentIdAt({ owner, path }: Place, name: unknown): string {
136
+ if (owner === undefined) return nextId('CustomComponent');
137
+ const at = path.endsWith('.component') ? path.slice(0, -'.component'.length) : path;
138
+ const id = `${owner}/${at}/${String(name)}`;
139
+ rememberOptionsComponent(owner, id);
140
+ return id;
141
+ }
142
+
115
143
  // The component of a bar button (the top bar's `*Buttons`, the toolbar's `buttons`) is a root the native side only
116
144
  // sizes: its wrapper centers the content in the button's slot.
117
145
  function noteButtonRoot(button: unknown): void {
@@ -1,6 +1,7 @@
1
1
  import type { SplitViewColumnsStackedEvent } from '../interfaces/Events';
2
2
  import type { LayoutNode } from './layout';
3
3
  import { onNativeEvent } from './native';
4
+ import { forgetProps } from './props';
4
5
  import { screenNameOf } from './slots';
5
6
 
6
7
  // Where each screen is, noted as the engine builds the layouts of the commands: what it shows in and its place in its
@@ -51,6 +52,11 @@ const stackedSplits = new Set<string>();
51
52
  let listeningToSplitViews = false;
52
53
  // The primaries of pairs shown alone, noted as their node is built, until their place is noted.
53
54
  const alone = new Set<string>();
55
+ // The layouts each screen shows in (its stack, pair, tabs, menu, split view, and the root of its layer), and how many
56
+ // live screens each layout holds. A layout whose last screen went is gone, and with it the props of the components the
57
+ // library named in its options: they stay as long as any of its screens may show them again.
58
+ const layoutsOfScreen = new Map<string, readonly string[]>();
59
+ const screensInLayout = new Map<string, number>();
54
60
  // The last screens that went, so a command from one of them can say so; a screen the engine never saw is not one.
55
61
  const gone = new Set<string>();
56
62
  const GONE_KEPT = 200;
@@ -69,9 +75,15 @@ export function isGone(screenId: string): boolean {
69
75
  return gone.has(screenId);
70
76
  }
71
77
 
78
+ /** The layouts the screen shows in, outermost first: those whose screens come with it in a command go in it too. */
79
+ export function layoutsOf(screenId: string): readonly string[] {
80
+ return layoutsOfScreen.get(screenId) ?? [];
81
+ }
82
+
72
83
  /**
73
84
  * Notes the screens of a layout, the first of a stack at `index`. A modal or an overlay given as a whole is a layer of
74
- * its own; screens added to one, such as a push, take the layer of the screen they come from.
85
+ * its own; screens added to one, such as a push, take the layer of the screen they come from, and `within`, the
86
+ * layouts they show in (`layoutsOf` that screen).
75
87
  */
76
88
  export function notePlaces(
77
89
  node: LayoutNode,
@@ -79,12 +91,14 @@ export function notePlaces(
79
91
  stack: string | null = null,
80
92
  index = 0,
81
93
  menu?: ScreenPlace['menu'],
82
- layer: string = container === 'root' ? 'root' : node.id
94
+ layer: string = container === 'root' ? 'root' : node.id,
95
+ within: readonly string[] = []
83
96
  ): void {
84
97
  if (node.type === 'Component' || node.type === 'ExternalComponent') {
85
98
  const name = node.data.name ?? '';
86
99
  gone.delete(node.id);
87
100
  columnOfScreen.delete(node.id);
101
+ noteLayouts(node.id, within);
88
102
  places.set(node.id, {
89
103
  name: screenNameOf(name),
90
104
  container,
@@ -99,7 +113,7 @@ export function notePlaces(
99
113
  } else if (node.type === 'Arrangement') {
100
114
  // One entry of its stack: both screens have its place there, and each knows the pair.
101
115
  node.children.forEach((child, childIndex) => {
102
- notePlaces(child, container, stack, index, menu, layer);
116
+ notePlaces(child, container, stack, index, menu, layer, [...within, node.id]);
103
117
  const place = places.get(child.id);
104
118
  if (place) {
105
119
  const placement = childIndex === 0 ? 'primary' : 'secondary';
@@ -109,7 +123,7 @@ export function notePlaces(
109
123
  } else if (node.type === 'Stack') {
110
124
  columnOfStack.delete(node.id);
111
125
  node.children.forEach((child, childIndex) =>
112
- notePlaces(child, container, node.id, childIndex, menu, layer)
126
+ notePlaces(child, container, node.id, childIndex, menu, layer, [...within, node.id])
113
127
  );
114
128
  } else if (node.type === 'SplitView') {
115
129
  // A split view sent again with its id starts anew: its columns are noted again, and it isn't one stack until the
@@ -118,7 +132,7 @@ export function notePlaces(
118
132
  const columns = node.data.columns ?? [];
119
133
  const stacks: Partial<Record<string, string>> = {};
120
134
  node.children.forEach((child, childIndex) => {
121
- notePlaces(child, container, null, 0, menu, layer);
135
+ notePlaces(child, container, null, 0, menu, layer, [...within, node.id]);
122
136
  const column = columns[childIndex];
123
137
  if (!column) return;
124
138
  if (child.type === 'Stack') stacks[column] = child.id;
@@ -128,12 +142,46 @@ export function notePlaces(
128
142
  } else if (node.type === 'SideMenuRoot') {
129
143
  for (const part of node.children) {
130
144
  const side = MENU_PARTS[part.type];
145
+ const parts = [...within, node.id, part.id];
131
146
  part.children.forEach((child) =>
132
- notePlaces(child, container, null, 0, side ? { id: node.id, part: side } : menu, layer)
147
+ notePlaces(
148
+ child,
149
+ container,
150
+ null,
151
+ 0,
152
+ side ? { id: node.id, part: side } : menu,
153
+ layer,
154
+ parts
155
+ )
133
156
  );
134
157
  }
135
158
  } else {
136
- node.children.forEach((child) => notePlaces(child, container, null, 0, menu, layer));
159
+ node.children.forEach((child) =>
160
+ notePlaces(child, container, null, 0, menu, layer, [...within, node.id])
161
+ );
162
+ }
163
+ }
164
+
165
+ // A screen noted again under its id (a layout sent again) leaves the layouts it was in before.
166
+ function noteLayouts(screenId: string, within: readonly string[]): void {
167
+ leaveLayouts(screenId);
168
+ const layouts = [...new Set(within)];
169
+ layoutsOfScreen.set(screenId, layouts);
170
+ for (const layout of layouts) screensInLayout.set(layout, (screensInLayout.get(layout) ?? 0) + 1);
171
+ }
172
+
173
+ function leaveLayouts(screenId: string): void {
174
+ const layouts = layoutsOfScreen.get(screenId);
175
+ if (!layouts) return;
176
+ layoutsOfScreen.delete(screenId);
177
+ for (const layout of layouts) {
178
+ const count = (screensInLayout.get(layout) ?? 1) - 1;
179
+ if (count > 0) {
180
+ screensInLayout.set(layout, count);
181
+ continue;
182
+ }
183
+ screensInLayout.delete(layout);
184
+ forgetProps(layout);
137
185
  }
138
186
  }
139
187
 
@@ -198,6 +246,7 @@ export function listenToSplitViews(): void {
198
246
 
199
247
  /** The screen is gone. */
200
248
  export function forgetPlace(screenId: string): void {
249
+ leaveLayouts(screenId);
201
250
  places.delete(screenId);
202
251
  columnOfScreen.delete(screenId);
203
252
  gone.add(screenId);
@@ -7,6 +7,11 @@ const watchers = new Map<string, Set<() => void>>();
7
7
  const none = {};
8
8
  // The roots that are the components of bar buttons: their wrapper centers the content in the button's slot.
9
9
  const buttonRoots = new Set<string>();
10
+ // The ids the library gave components in options, by the node whose options they are in. Such an id stays the same
11
+ // for every show of the node and may have several roots at once (a component of the default or a stack's options):
12
+ // its props go with the node, not with a root.
13
+ const optionComponents = new Map<string, Set<string>>();
14
+ const optionComponentIds = new Set<string>();
10
15
 
11
16
  export function setProps(id: string, props: object | undefined): void {
12
17
  if (props === undefined) byId.delete(id);
@@ -38,8 +43,32 @@ export function isButtonRoot(id: string): boolean {
38
43
  return buttonRoots.has(id);
39
44
  }
40
45
 
41
- /** The view is gone: its props, and whether it is a button's root, are not needed again. */
46
+ /** Notes an id the library gave a component in the options of the node `owner`. */
47
+ export function rememberOptionsComponent(owner: string, id: string): void {
48
+ let ids = optionComponents.get(owner);
49
+ if (!ids) optionComponents.set(owner, (ids = new Set()));
50
+ ids.add(id);
51
+ optionComponentIds.add(id);
52
+ }
53
+
54
+ /** Whether the library gave the id to a component in options: its props go with the node, not with a root. */
55
+ export function isOptionsComponent(id: string): boolean {
56
+ return optionComponentIds.has(id);
57
+ }
58
+
59
+ /**
60
+ * The view is gone: its props, and whether it is a button's root, are not needed again; nor are those of the
61
+ * components the library named in its options.
62
+ */
42
63
  export function forgetProps(id: string): void {
43
64
  byId.delete(id);
44
65
  buttonRoots.delete(id);
66
+ const ids = optionComponents.get(id);
67
+ if (!ids) return;
68
+ optionComponents.delete(id);
69
+ for (const component of ids) {
70
+ byId.delete(component);
71
+ buttonRoots.delete(component);
72
+ optionComponentIds.delete(component);
73
+ }
45
74
  }
@@ -60,7 +60,10 @@ export function rootGoesOut(nodes: LayoutNode[], again: SentRoot | undefined): v
60
60
  pairNodes(before.nodes, nodes, (old, node) => {
61
61
  const options = remembered.get(old.id);
62
62
  if (!options) return;
63
- node.data.options = mergeNativeOptions(node.data.options ?? {}, toNativeOptions(options));
63
+ node.data.options = mergeNativeOptions(
64
+ node.data.options ?? {},
65
+ toNativeOptions(options, node.id)
66
+ );
64
67
  moved.set(node.id, options);
65
68
  });
66
69
  }
@@ -2,7 +2,7 @@ import React, { type ComponentType, useCallback, useEffect, useSyncExternalStore
2
2
  import { AppRegistry } from 'react-native';
3
3
  import type { Options } from '../interfaces/Options';
4
4
  import { forgetPlace } from './places';
5
- import { forgetProps, propsOf, watchProps } from './props';
5
+ import { forgetProps, isOptionsComponent, propsOf, watchProps } from './props';
6
6
  import { forgetMergedOptions } from './rememberedOptions';
7
7
  import { hasSlot } from './slots';
8
8
  import type { NativeControllerProps } from './nativeScreens';
@@ -55,11 +55,12 @@ export type ScreenRootProps = { componentId: string } & Record<string, unknown>;
55
55
  const screens = new Map<string, EngineScreen>();
56
56
  const roots = new Map<string, ComponentType<{ componentId: string; rootTag?: number }>>();
57
57
  let unmounted: ((screenId: string) => void) | undefined;
58
- // The React root that shows each screen, by its root tag. After a JavaScript reload React Native starts the roots of
59
- // the old screens again in the new runtime, under the ids the new screens get too, and unmounts them once the new ones
60
- // show; their root tags are older, as the native side counts them up for as long as the app runs. A slot of the app's
61
- // one tree has no tag: while the one tree shows a screen with the id, no root of that id lets it go.
62
- const owners = new Map<string, number>();
58
+ // The React roots mounted under each id, by their root tags. One id can have several: after a JavaScript reload React
59
+ // Native starts the roots of the old screens again in the new runtime, under the ids the new screens get too, and
60
+ // unmounts them once the new ones show; a component of the default or a stack's options has one id and a root in the
61
+ // bar of every screen that shows it. The id lets its props go only with the last of them. A slot of the app's one tree
62
+ // has no tag: while the one tree shows a screen with the id, no root of that id lets it go.
63
+ const owners = new Map<string, Set<number>>();
63
64
 
64
65
  /** What runs when the root of a screen unmounts: the screen is gone. */
65
66
  export function onScreenUnmounted(listener: (screenId: string) => void): void {
@@ -214,17 +215,26 @@ export function screenRoot(
214
215
  );
215
216
  const props = useSyncExternalStore(watch, () => propsOf(componentId));
216
217
  useEffect(() => {
217
- if (rootTag !== undefined && rootTag > (owners.get(componentId) ?? -1)) {
218
- owners.set(componentId, rootTag);
218
+ if (rootTag !== undefined) {
219
+ const tags = owners.get(componentId) ?? new Set<number>();
220
+ tags.add(rootTag);
221
+ owners.set(componentId, tags);
219
222
  }
220
223
  return () => {
221
- // An old root of the same id leaves the screen alone, and so does any root while the one tree still has a slot
222
- // with the id: a later screen under the id, the restarted root of a screen after a reload into the one tree
223
- // (its slot may mount only once the app's provider lets it), the screen rendering again under the provider.
224
- if (rootTag !== undefined && owners.get(componentId) !== rootTag) return;
224
+ // A root while another root of the id is mounted leaves the screen alone, and so does any root while the one
225
+ // tree still has a slot with the id: a later screen under the id, the restarted root of a screen after a
226
+ // reload into the one tree (its slot may mount only once the app's provider lets it), the screen rendering
227
+ // again under the provider.
228
+ const tags = owners.get(componentId);
229
+ if (rootTag !== undefined && tags) {
230
+ tags.delete(rootTag);
231
+ if (tags.size > 0) return;
232
+ }
225
233
  if (hasSlot(componentId)) return;
226
234
  owners.delete(componentId);
227
- forgetProps(componentId);
235
+ // A component the library named in options keeps its props for the next root under its id (the node shows
236
+ // it again, or another screen shows the same default): they go with the node.
237
+ if (!isOptionsComponent(componentId)) forgetProps(componentId);
228
238
  forgetPlace(componentId);
229
239
  forgetMergedOptions(componentId);
230
240
  unmounted?.(componentId);
@@ -10,7 +10,7 @@ export type ComponentType =
10
10
 
11
11
  /** The fields every appearance event has. */
12
12
  interface ComponentAppearanceEvent {
13
- /** The id of the component: the screen, or the screen the bar component belongs to. */
13
+ /** The id of the component: of the screen, or the bar component's own id. */
14
14
  componentId: string;
15
15
  /** The name of the component: for a screen, its name without any internal prefix. */
16
16
  componentName: string;
@@ -60,7 +60,7 @@ export interface NavigationButtonPressedEvent {
60
60
  * `ApexNavigation.back`; the hardware back of a modal has `ApexNavigation.hardwareBackButton`.
61
61
  */
62
62
  buttonId: string;
63
- /** (iOS) For a button with `toggle`: its new state, already written into the options of the screen. */
63
+ /** (iOS) For a button with `toggle`: its new state, which the screen keeps. */
64
64
  selected?: boolean;
65
65
  }
66
66
 
@@ -54,8 +54,12 @@ export interface NavigationStateChangedEvent {
54
54
 
55
55
  /**
56
56
  * A tab was selected by the user: a press on the tab, a pick from the "More" list or a press on "More" that showed a
57
- * tab (iOS). It is sent after the selection, and only when the tab changed; a press on the current tab is reported by
58
- * `BottomTabPressedEvent` alone. Selecting a tab from code sends nothing.
57
+ * tab (iOS). It goes right after the `ComponentDidAppear` of the new tab's screen, before the navigation state and
58
+ * before the tab that was left goes back to its first screen (`popToRootOnBlur`), and only when the tab changed: a
59
+ * press on the selected tab or on a `selectTabOnPress: false` tab is reported by `BottomTabPressedEvent` alone.
60
+ * Selecting a tab from code sends nothing. A switch still running when another press, long press or choice from code
61
+ * comes ends first, with this event, so the events of one press never mix with the next; when that tab's screen
62
+ * hasn't drawn yet, this event goes before its held `ComponentDidAppear`.
59
63
  */
60
64
  export interface BottomTabSelectedEvent {
61
65
  /** The index of the tab that is now selected. */
@@ -67,24 +71,28 @@ export interface BottomTabSelectedEvent {
67
71
  }
68
72
 
69
73
  /**
70
- * A tab was pressed, the selected one too. It is sent before the tab is selected; with `selectTabOnPress: false` it is
71
- * the only thing that happens. A long press sends `BottomTabLongPressedEvent` instead.
74
+ * A tab was pressed, on the bar or on a cell of `bottomTabs.customRow`, the selected one too. It goes first, before
75
+ * anything the press does. Another tab: then the switch's `ComponentWillAppear`, `ComponentDidDisappear` and
76
+ * `ComponentDidAppear`, then `BottomTabSelectedEvent`. The selected tab again (`wasSelected: true`): its stack goes
77
+ * back to its first screen with the events of the user's back (`ScreenPopped`), or, on the first screen, its content
78
+ * scrolls to the top; with `popToRootOnPress: false` nothing follows. With `selectTabOnPress: false` it is the only
79
+ * event and the selection stays. A long press sends `BottomTabLongPressedEvent` instead.
72
80
  */
73
81
  export interface BottomTabPressedEvent {
74
82
  /** The index of the tab that was pressed. */
75
83
  tabIndex: number;
76
84
  /** The id of the tabs. */
77
85
  bottomTabsId: string;
78
- /** The id of the screen the tab shows: the top of its stack. */
86
+ /** The id of the screen the tab shows: the top of its stack, or the tab itself when it is a screen. */
79
87
  screenId?: string;
80
88
  /** Whether the tab was the selected one already; a press on the selected tab scrolls its content to the top. */
81
89
  wasSelected: boolean;
82
90
  }
83
91
 
84
92
  /**
85
- * A tab was pressed and held. The tab is not selected and gets no press event. On iOS it is not sent for the tabs
86
- * that are components (`bottomTab.component`, `bottomTabs.customRow`), for "More", or for the tab strip at the top of an
87
- * iPad. On Android it replaces the tooltip of the tab.
93
+ * A tab was pressed and held, on the bar or on a cell of `bottomTabs.customRow`. The tab is not selected and gets no
94
+ * press event. On iOS it is not sent for "More" or for the tab strip at the top of an iPad. On Android it replaces the
95
+ * tooltip of the tab.
88
96
  */
89
97
  export interface BottomTabLongPressedEvent {
90
98
  /** The index of the tab that was pressed. */
@@ -195,11 +203,12 @@ export type HingeState = 'closed' | 'partiallyOpen' | 'fullyOpen' | 'unknown';
195
203
  */
196
204
  export interface HingeChangedEvent {
197
205
  /**
198
- * The state of the hinge. On Android `closed` means the device has no fold, or its angle is below 90 degrees.
206
+ * The state of the hinge. On Android `closed` means the app's window has no fold, or the angle is below 90 degrees,
207
+ * so a device open flat with a window off the fold (split screen) reports `closed`; Android never gives `unknown`.
199
208
  */
200
209
  state?: HingeState;
201
210
  /**
202
- * The angle of the hinge in degrees (on iOS to tenths). Missing when the state is `unknown` and where the system
211
+ * The angle of the hinge in degrees, to tenths. Missing when the state is `unknown` and where the system
203
212
  * gives no angle.
204
213
  */
205
214
  angle?: number;