@apexnative/navigation 1.0.0-alpha.6 → 1.0.0-alpha.8

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 (242) hide show
  1. package/ApexNavigation.podspec +2 -1
  2. package/CHANGELOG.md +15 -0
  3. package/android/CLAUDE.md +19 -10
  4. package/android/CORE.md +3 -2
  5. package/android/consumer-rules.pro +6 -0
  6. package/android/src/main/java/dev/apexnative/navigation/options/BottomTabOptions.kt +1 -1
  7. package/android/src/main/java/dev/apexnative/navigation/options/ButtonOptions.kt +1 -1
  8. package/android/src/main/java/dev/apexnative/navigation/options/LargeTitleOptions.kt +1 -1
  9. package/android/src/main/java/dev/apexnative/navigation/options/LayoutAnimation.kt +1 -1
  10. package/android/src/main/java/dev/apexnative/navigation/options/OptionKey.kt +18 -0
  11. package/android/src/main/java/dev/apexnative/navigation/options/OptionPaths.kt +91 -0
  12. package/android/src/main/java/dev/apexnative/navigation/options/SheetOptions.kt +7 -4
  13. package/android/src/main/java/dev/apexnative/navigation/options/TitleOptions.kt +2 -2
  14. package/android/src/main/java/dev/apexnative/navigation/options/TopBarAppearanceOptions.kt +1 -1
  15. package/android/src/main/java/dev/apexnative/navigation/options/TopBarOptions.kt +16 -7
  16. package/android/src/main/java/dev/apexnative/navigation/react/ContentHeights.kt +95 -0
  17. package/android/src/main/java/dev/apexnative/navigation/react/ContentMounts.kt +66 -0
  18. package/android/src/main/java/dev/apexnative/navigation/react/NavigationTurboModule.kt +4 -0
  19. package/android/src/main/java/dev/apexnative/navigation/react/OptionsCommands.kt +41 -3
  20. package/android/src/main/java/dev/apexnative/navigation/react/ReactView.kt +16 -1
  21. package/android/src/main/java/dev/apexnative/navigation/slots/SlotScreenView.kt +27 -7
  22. package/android/src/main/java/dev/apexnative/navigation/slots/SlotView.kt +12 -1
  23. package/android/src/main/java/dev/apexnative/navigation/utils/MaterialTheme.kt +5 -6
  24. package/android/src/main/java/dev/apexnative/navigation/utils/SystemAnimations.kt +36 -0
  25. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalSheet.kt +329 -55
  26. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/ModalViews.kt +26 -5
  27. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/modal/SheetStops.kt +60 -2
  28. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/sidemenu/SideMenuMotion.kt +8 -2
  29. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/BackPreview.kt +32 -4
  30. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackController.kt +46 -7
  31. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackQueue.kt +15 -3
  32. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/StackViews.kt +7 -4
  33. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabMotion.kt +142 -0
  34. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/fab/FabPresenter.kt +215 -43
  35. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/search/StackSearchPresenter.kt +25 -26
  36. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/BarRowTransition.kt +3 -2
  37. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/ButtonPlaces.kt +11 -5
  38. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarPresenter.kt +86 -39
  39. package/android/src/main/java/dev/apexnative/navigation/viewcontrollers/stack/topbar/TopBarVisibility.kt +9 -2
  40. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementTransitions.kt +124 -181
  41. package/android/src/main/java/dev/apexnative/navigation/views/element/TargetsWait.kt +8 -1
  42. package/android/src/main/java/dev/apexnative/navigation/views/element/TransitionLayer.kt +37 -112
  43. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ElementPictures.kt +56 -3
  44. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/FlightGeometry.kt +170 -153
  45. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMorph.kt +199 -0
  46. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/SharedAnimatorFactory.kt +55 -45
  47. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/{TextChangeAnimator.kt → TextMorph.kt} +79 -129
  48. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewMorph.kt +108 -0
  49. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabCopy.kt +85 -0
  50. package/android/src/main/java/dev/apexnative/navigation/views/stack/fab/FabMenu.kt +13 -0
  51. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TitleBar.kt +5 -24
  52. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/TopBar.kt +51 -34
  53. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonComponentCell.kt +6 -6
  54. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarButtonView.kt +113 -0
  55. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/BarMenu.kt +219 -0
  56. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonBar.kt +121 -175
  57. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonMenus.kt +34 -64
  58. package/android/src/main/java/dev/apexnative/navigation/views/stack/topbar/titlebar/ButtonRow.kt +216 -0
  59. package/android/src/main/jni/ANNContentHeights.cpp +82 -0
  60. package/android/src/main/res/drawable/apex_navigation_ic_arrow_right.xml +14 -0
  61. package/android/src/main/res/layout/apex_navigation_menu_header.xml +19 -0
  62. package/android/src/main/res/layout/apex_navigation_menu_item.xml +68 -0
  63. package/android/src/main/res/values/styles.xml +1 -1
  64. package/android/src/main/res/values-night/styles.xml +1 -1
  65. package/cli/doctor/checks/app.js +16 -1
  66. package/cli/project.js +4 -0
  67. package/cli/setup/index.js +5 -0
  68. package/cli/setup/steps/metro.js +46 -0
  69. package/docs-md/architecture/native-screens.md +4 -0
  70. package/docs-md/architecture/theme-and-context.md +131 -12
  71. package/docs-md/architecture/transitions.md +43 -4
  72. package/docs-md/flows/bottom-sheets.md +4 -2
  73. package/docs-md/fundamentals/screens-and-props.md +9 -1
  74. package/docs-md/guides/cli.md +3 -1
  75. package/docs-md/help/troubleshooting.md +1 -1
  76. package/docs-md/index.json +29 -1
  77. package/docs-md/screen-ui/back-and-gestures.md +3 -1
  78. package/docs-md/screen-ui/bar-buttons.md +1 -1
  79. package/docs-md/screen-ui/toolbar-and-fab.md +2 -0
  80. package/docs-md/start/installation.md +16 -1
  81. package/ios/Animations/ANNFrameDriver.h +55 -15
  82. package/ios/Animations/ANNFrameDriver.mm +214 -60
  83. package/ios/Animations/ANNTransitionAnimator.h +26 -5
  84. package/ios/Animations/ANNTransitionAnimator.mm +49 -3
  85. package/ios/Animations/ANNTransitionPlan.h +2 -2
  86. package/ios/Animations/ANNTransitionPlan.mm +10 -0
  87. package/ios/Animations/Elements/ANNElementFinder.h +2 -2
  88. package/ios/Animations/Elements/ANNElementFinder.mm +2 -2
  89. package/ios/Animations/Elements/ANNElementTransition.h +3 -3
  90. package/ios/Animations/Elements/ANNElementTransition.mm +2 -2
  91. package/ios/Animations/Elements/ANNSharedElementFlight.h +3 -3
  92. package/ios/Animations/Elements/ANNSharedElementFlight.mm +181 -43
  93. package/ios/Animations/Interpolators/ANNViewTracks.h +6 -5
  94. package/ios/Animations/Interpolators/ANNViewTracks.mm +51 -21
  95. package/ios/CLAUDE.md +19 -8
  96. package/ios/CORE.md +1 -1
  97. package/ios/Core/ANNAppDelegate.mm +0 -2
  98. package/ios/Core/ANNDeepLinkQueue.h +6 -5
  99. package/ios/Core/ANNDeepLinkQueue.mm +22 -10
  100. package/ios/Core/ANNSceneDelegate.mm +2 -2
  101. package/ios/Core/ANNUtils.h +7 -0
  102. package/ios/Core/ANNUtils.mm +10 -0
  103. package/ios/Core/ApexNavigation.mm +6 -2
  104. package/ios/Layouts/Overlay/ANNOverlayCoordinator.mm +1 -7
  105. package/ios/Layouts/Root/ANNRootCoordinator.mm +45 -15
  106. package/ios/Layouts/Screen/ANNComponentViewController.mm +4 -1
  107. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.h +11 -2
  108. package/ios/Layouts/Stack/ANNStackAnimatingDelegate.mm +14 -7
  109. package/ios/Layouts/Stack/ANNStackController.h +8 -0
  110. package/ios/Layouts/Stack/ANNStackController.mm +153 -11
  111. package/ios/Layouts/Stack/ANNSwipePopInteraction.h +26 -0
  112. package/ios/Layouts/Stack/ANNSwipePopInteraction.mm +185 -0
  113. package/ios/Layouts/Stack/InteractivePopGestureDelegate.mm +28 -4
  114. package/ios/Layouts/Tabs/ANNTabsPreloadWindow.mm +2 -4
  115. package/ios/Options/ANNAnimationOptions.h +2 -1
  116. package/ios/Options/ANNAnimationOptions.mm +1 -1
  117. package/ios/Options/ANNOptions.h +7 -0
  118. package/ios/Options/ANNOptions.mm +45 -0
  119. package/ios/Options/ANNOptionsReader.mm +2 -4
  120. package/ios/Options/ANNStackPopAnimationOptions.h +13 -0
  121. package/ios/Options/ANNStackPopAnimationOptions.mm +11 -0
  122. package/ios/Options/Params/ColorParser.h +4 -3
  123. package/ios/Options/Params/ColorParser.mm +22 -4
  124. package/ios/React/ANNSurfaceHostingView.h +8 -0
  125. package/ios/React/ANNSurfaceHostingView.mm +28 -1
  126. package/ios/React/Slots/ANNSlotHost.mm +1 -3
  127. package/ios/React/Slots/ANNSlotScreenView.mm +8 -2
  128. package/ios/TurboModules/ANNTurboModule.mm +46 -0
  129. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.cpp +20 -0
  130. package/ios/cpp/react/renderer/components/ApexNavigationSpec/ANNSlotState.h +5 -14
  131. package/lib/module/ARCHITECTURE.md +9 -3
  132. package/lib/module/adapters/NativeANNTurboModule.js.map +1 -1
  133. package/lib/module/api/app/AppRegistry.js +14 -2
  134. package/lib/module/api/app/AppRegistry.js.map +1 -1
  135. package/lib/module/api/app/createNavigation.js +188 -66
  136. package/lib/module/api/app/createNavigation.js.map +1 -1
  137. package/lib/module/api/events/navigationEvents.js +6 -2
  138. package/lib/module/api/events/navigationEvents.js.map +1 -1
  139. package/lib/module/api/linking/linking.js +11 -0
  140. package/lib/module/api/linking/linking.js.map +1 -1
  141. package/lib/module/api/screens/defineScreen.js +45 -3
  142. package/lib/module/api/screens/defineScreen.js.map +1 -1
  143. package/lib/module/api/screens/nativeScreens.js +31 -6
  144. package/lib/module/api/screens/nativeScreens.js.map +1 -1
  145. package/lib/module/api/screens/registerScreens.js +193 -35
  146. package/lib/module/api/screens/registerScreens.js.map +1 -1
  147. package/lib/module/api/theme/theme.js +5 -0
  148. package/lib/module/api/theme/theme.js.map +1 -1
  149. package/lib/module/engine/commands.js +21 -1
  150. package/lib/module/engine/commands.js.map +1 -1
  151. package/lib/module/engine/layout.js +7 -2
  152. package/lib/module/engine/layout.js.map +1 -1
  153. package/lib/module/engine/nativeScreens.js +9 -0
  154. package/lib/module/engine/nativeScreens.js.map +1 -1
  155. package/lib/module/engine/options.js +5 -4
  156. package/lib/module/engine/options.js.map +1 -1
  157. package/lib/module/engine/optionsChange.js +102 -0
  158. package/lib/module/engine/optionsChange.js.map +1 -0
  159. package/lib/module/engine/refresh.js +85 -0
  160. package/lib/module/engine/refresh.js.map +1 -0
  161. package/lib/module/engine/rememberedOptions.js +60 -0
  162. package/lib/module/engine/rememberedOptions.js.map +1 -1
  163. package/lib/module/engine/screens.js +26 -0
  164. package/lib/module/engine/screens.js.map +1 -1
  165. package/lib/typescript/adapters/NativeANNTurboModule.d.ts +7 -0
  166. package/lib/typescript/adapters/NativeANNTurboModule.d.ts.map +1 -1
  167. package/lib/typescript/api/app/AppRegistry.d.ts.map +1 -1
  168. package/lib/typescript/api/app/createNavigation.d.ts +5 -0
  169. package/lib/typescript/api/app/createNavigation.d.ts.map +1 -1
  170. package/lib/typescript/api/events/navigationEvents.d.ts +2 -2
  171. package/lib/typescript/api/events/navigationEvents.d.ts.map +1 -1
  172. package/lib/typescript/api/index.d.ts +1 -1
  173. package/lib/typescript/api/index.d.ts.map +1 -1
  174. package/lib/typescript/api/linking/linking.d.ts +8 -0
  175. package/lib/typescript/api/linking/linking.d.ts.map +1 -1
  176. package/lib/typescript/api/screens/defineScreen.d.ts +23 -4
  177. package/lib/typescript/api/screens/defineScreen.d.ts.map +1 -1
  178. package/lib/typescript/api/screens/nativeScreens.d.ts +8 -1
  179. package/lib/typescript/api/screens/nativeScreens.d.ts.map +1 -1
  180. package/lib/typescript/api/screens/registerScreens.d.ts.map +1 -1
  181. package/lib/typescript/api/theme/theme.d.ts +2 -0
  182. package/lib/typescript/api/theme/theme.d.ts.map +1 -1
  183. package/lib/typescript/engine/commands.d.ts +5 -0
  184. package/lib/typescript/engine/commands.d.ts.map +1 -1
  185. package/lib/typescript/engine/layout.d.ts.map +1 -1
  186. package/lib/typescript/engine/nativeScreens.d.ts +5 -0
  187. package/lib/typescript/engine/nativeScreens.d.ts.map +1 -1
  188. package/lib/typescript/engine/optionsChange.d.ts +28 -0
  189. package/lib/typescript/engine/optionsChange.d.ts.map +1 -0
  190. package/lib/typescript/engine/refresh.d.ts +35 -0
  191. package/lib/typescript/engine/refresh.d.ts.map +1 -0
  192. package/lib/typescript/engine/rememberedOptions.d.ts +13 -0
  193. package/lib/typescript/engine/rememberedOptions.d.ts.map +1 -1
  194. package/lib/typescript/engine/screens.d.ts +7 -0
  195. package/lib/typescript/engine/screens.d.ts.map +1 -1
  196. package/lib/typescript/interfaces/ComponentEvents.d.ts +2 -2
  197. package/lib/typescript/interfaces/options/animations.d.ts +40 -15
  198. package/lib/typescript/interfaces/options/animations.d.ts.map +1 -1
  199. package/lib/typescript/interfaces/options/bottomTabs.d.ts +2 -2
  200. package/lib/typescript/interfaces/options/buttons.d.ts +2 -2
  201. package/lib/typescript/interfaces/options/modals.d.ts +6 -7
  202. package/lib/typescript/interfaces/options/modals.d.ts.map +1 -1
  203. package/lib/typescript/interfaces/options/stack.d.ts +8 -3
  204. package/lib/typescript/interfaces/options/stack.d.ts.map +1 -1
  205. package/lib/typescript/interfaces/options/topBar.d.ts +14 -0
  206. package/lib/typescript/interfaces/options/topBar.d.ts.map +1 -1
  207. package/metro/acceptPlugin.js +57 -0
  208. package/metro/babelTransformer.js +45 -0
  209. package/metro/index.js +36 -0
  210. package/package.json +3 -1
  211. package/src/ARCHITECTURE.md +9 -3
  212. package/src/adapters/NativeANNTurboModule.ts +7 -0
  213. package/src/api/app/AppRegistry.ts +16 -2
  214. package/src/api/app/createNavigation.ts +198 -66
  215. package/src/api/events/navigationEvents.ts +9 -3
  216. package/src/api/index.ts +1 -0
  217. package/src/api/linking/linking.ts +22 -1
  218. package/src/api/screens/defineScreen.ts +49 -5
  219. package/src/api/screens/nativeScreens.ts +40 -4
  220. package/src/api/screens/registerScreens.tsx +185 -42
  221. package/src/api/theme/theme.ts +5 -0
  222. package/src/engine/commands.ts +21 -0
  223. package/src/engine/layout.ts +11 -2
  224. package/src/engine/nativeScreens.ts +10 -1
  225. package/src/engine/options.ts +6 -8
  226. package/src/engine/optionsChange.ts +107 -0
  227. package/src/engine/refresh.ts +97 -0
  228. package/src/engine/rememberedOptions.ts +70 -0
  229. package/src/engine/screens.tsx +24 -0
  230. package/src/interfaces/ComponentEvents.ts +2 -2
  231. package/src/interfaces/options/animations.ts +40 -15
  232. package/src/interfaces/options/bottomTabs.ts +2 -2
  233. package/src/interfaces/options/buttons.ts +2 -2
  234. package/src/interfaces/options/modals.ts +6 -7
  235. package/src/interfaces/options/stack.ts +8 -3
  236. package/src/interfaces/options/topBar.ts +14 -0
  237. package/android/src/main/java/dev/apexnative/navigation/views/element/ElementStub.kt +0 -31
  238. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ImageMatrixAnimator.kt +0 -134
  239. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PlainViewAnimator.kt +0 -78
  240. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/PropertyAnimatorCreator.kt +0 -33
  241. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ReactImageAnimator.kt +0 -57
  242. package/android/src/main/java/dev/apexnative/navigation/views/element/animators/ViewChangeAnimator.kt +0 -146
@@ -1,15 +1,11 @@
1
1
  package dev.apexnative.navigation.views.stack.topbar.titlebar
2
2
 
3
- import android.annotation.SuppressLint
4
3
  import android.content.Context
5
4
  import android.graphics.Color
6
5
  import android.graphics.drawable.Drawable
7
6
  import android.text.SpannableString
8
7
  import android.text.Spanned
9
8
  import android.text.style.ForegroundColorSpan
10
- import android.view.Menu
11
- import android.view.MenuItem
12
- import androidx.appcompat.view.menu.MenuBuilder
13
9
  import com.google.android.material.color.MaterialColors
14
10
  import dev.apexnative.navigation.options.ButtonMenuOptions
15
11
  import dev.apexnative.navigation.utils.MaterialTheme
@@ -17,78 +13,52 @@ import androidx.appcompat.R as AppCompatR
17
13
  import com.google.android.material.R as MaterialR
18
14
 
19
15
  /**
20
- * Puts the items of a bar menu (the overflow menu, the menu of a button) into an Android menu: their titles and icons,
21
- * the destructive ones red, the checked ones with a checkmark. A pick of an item calls `onPick` with its id; with
22
- * `singleSelection` the checkmark moves to it in the open menu, and the caller writes it into the screen's options.
16
+ * Turns the items of a bar menu (the overflow menu, the menu of a button) into the entries of a [BarMenu]: their
17
+ * titles and icons, the destructive ones red, the checked ones with a check mark, the items of a `singleSelection` menu
18
+ * with radio buttons. Icons take `textColor` (`topBar.menuTextColor`), else the theme's on-surface-variant color. A
19
+ * pick of an item calls `onPick` with its id; the caller writes a `singleSelection` choice into the screen's options.
23
20
  */
24
- object ButtonMenus {
25
- fun fill(
21
+ internal object ButtonMenus {
22
+ fun entries(
26
23
  context: Context,
27
- menu: Menu,
28
- group: Int,
29
- firstOrder: Int,
30
24
  options: ButtonMenuOptions,
31
- showAsAction: Int?,
25
+ textColor: Int?,
32
26
  loadIcon: (String) -> Drawable?,
33
27
  onPick: (String) -> Unit,
34
- ) {
35
- addElements(context, menu, group, firstOrder, options.items, options.singleSelection, showAsAction, loadIcon, onPick)
36
- }
28
+ ): List<BarMenu.Entry> = entriesOf(context, options.items, options.singleSelection, textColor, loadIcon, onPick)
37
29
 
38
- private fun addElements(
30
+ private fun entriesOf(
39
31
  context: Context,
40
- menu: Menu,
41
- group: Int,
42
- firstOrder: Int,
43
32
  elements: List<ButtonMenuOptions.Element>,
44
33
  singleSelection: Boolean,
45
- showAsAction: Int?,
34
+ textColor: Int?,
46
35
  loadIcon: (String) -> Drawable?,
47
36
  onPick: (String) -> Unit,
48
- ) {
49
- val submenus = mutableListOf<MenuItem>()
50
- elements.forEachIndexed { index, element ->
51
- val order = firstOrder + index
52
- val item = when (element) {
53
- is ButtonMenuOptions.Submenu -> menu.addSubMenu(group, Menu.NONE, order, element.title).also {
54
- addElements(context, it, group, 0, element.items, element.singleSelection, null, loadIcon, onPick)
55
- }.item.also { submenus += it }
56
- is ButtonMenuOptions.Item -> menu.add(group, Menu.NONE, order, title(context, element)).apply {
57
- isEnabled = !element.disabled
58
- if (singleSelection || element.checked) {
59
- isCheckable = true
60
- isChecked = element.checked
61
- }
62
- setOnMenuItemClickListener { picked ->
63
- if (singleSelection) picked.isChecked = true
64
- onPick(element.id)
65
- true
66
- }
67
- }
68
- }
69
- val destructive = element is ButtonMenuOptions.Item && element.destructive
70
- val tint = if (destructive) errorColor(context) else iconColor(context)
71
- element.icon?.let { uri -> item.icon = loadIcon(uri)?.mutate()?.also { it.setTint(tint) } }
72
- if (showAsAction != null) item.setShowAsAction(showAsAction)
73
- }
74
- if (singleSelection) {
75
- menu.setGroupCheckable(group, true, true)
76
- // The group made the submenus checkable too: they open, and have no checkmark.
77
- submenus.forEach { it.isCheckable = false }
37
+ ): List<BarMenu.Entry> = elements.map { element ->
38
+ val destructive = element is ButtonMenuOptions.Item && element.destructive
39
+ val tint = if (destructive) errorColor(context) else textColor ?: iconColor(context)
40
+ val icon = element.icon?.let(loadIcon)?.mutate()?.also { it.setTint(tint) }
41
+ when (element) {
42
+ is ButtonMenuOptions.Submenu -> BarMenu.Entry(
43
+ element.title,
44
+ icon,
45
+ submenu = entriesOf(context, element.items, element.singleSelection, textColor, loadIcon, onPick),
46
+ testId = element.testID,
47
+ )
48
+ is ButtonMenuOptions.Item -> BarMenu.Entry(
49
+ title(context, element),
50
+ icon,
51
+ enabled = !element.disabled,
52
+ check = when {
53
+ singleSelection -> BarMenu.Check.Radio
54
+ element.checked -> BarMenu.Check.Box
55
+ else -> BarMenu.Check.None
56
+ },
57
+ checked = element.checked,
58
+ testId = element.testID,
59
+ onPick = { onPick(element.id) },
60
+ )
78
61
  }
79
- if (elements.any { it.icon != null }) showIcons(menu)
80
- }
81
-
82
- /** Some item of the menu, or of a submenu, has an icon. */
83
- fun hasIcons(options: ButtonMenuOptions): Boolean = hasIcons(options.items)
84
-
85
- private fun hasIcons(elements: List<ButtonMenuOptions.Element>): Boolean =
86
- elements.any { it.icon != null || (it is ButtonMenuOptions.Submenu && hasIcons(it.items)) }
87
-
88
- // Material's menus have their icons at the start of the items; the toolbar's menus leave them out by default.
89
- @SuppressLint("RestrictedApi")
90
- private fun showIcons(menu: Menu) {
91
- (menu as? MenuBuilder)?.setOptionalIconsVisible(true)
92
62
  }
93
63
 
94
64
  private fun title(context: Context, item: ButtonMenuOptions.Item): CharSequence {
@@ -0,0 +1,216 @@
1
+ package dev.apexnative.navigation.views.stack.topbar.titlebar
2
+
3
+ import android.annotation.SuppressLint
4
+ import android.content.Context
5
+ import android.content.res.ColorStateList
6
+ import android.graphics.drawable.Drawable
7
+ import android.view.MotionEvent
8
+ import android.view.View
9
+ import android.view.ViewGroup
10
+ import androidx.appcompat.R as AppCompatR
11
+ import androidx.appcompat.widget.AppCompatImageButton
12
+ import androidx.appcompat.widget.AppCompatImageView
13
+ import androidx.appcompat.widget.TooltipCompat
14
+ import androidx.core.graphics.drawable.DrawableCompat
15
+ import androidx.core.widget.ImageViewCompat
16
+ import com.google.android.material.R as MaterialR
17
+ import com.google.android.material.theme.overlay.MaterialThemeOverlay
18
+ import dev.apexnative.navigation.utils.MaterialTheme
19
+ import dev.apexnative.navigation.utils.UiUtils
20
+ import kotlin.math.max
21
+ import kotlin.math.min
22
+ import kotlin.math.roundToInt
23
+
24
+ /**
25
+ * The buttons of one side of the bar's row, from the start of the line: the back button ([navigationIcon]), the
26
+ * buttons ([setButtons]), what the core always puts after them ([setTrailing]: the search's magnifier) and "⋮"
27
+ * ([overflowButton]). They stand in the middle of the row, at most 56 dp high, as a Material 3 toolbar stands its
28
+ * buttons, with the toolbar's theme (its ripples); right to left the line mirrors. Main thread only.
29
+ */
30
+ @SuppressLint("ViewConstructor")
31
+ class ButtonRow(context: Context) : ViewGroup(themed(context)) {
32
+ private val maxButtonHeight = UiUtils.dpToPx(this.context, MAX_BUTTON_HEIGHT_DP).roundToInt()
33
+ private var navigation: AppCompatImageButton? = null
34
+ private var buttons: List<View> = emptyList()
35
+ private var trailing: View? = null
36
+ private var overflow: OverflowButton? = null
37
+ private var overflowColor: Int? = null
38
+
39
+ /** The back button's icon; none hides the button. */
40
+ var navigationIcon: Drawable?
41
+ get() = navigation?.takeIf { it.visibility == VISIBLE }?.drawable
42
+ set(value) {
43
+ if (value == null) {
44
+ navigation?.let {
45
+ it.setImageDrawable(null)
46
+ it.visibility = GONE
47
+ }
48
+ return
49
+ }
50
+ val button = navigation ?: AppCompatImageButton(context, null, AppCompatR.attr.toolbarNavigationButtonStyle)
51
+ .also {
52
+ navigation = it
53
+ arrange()
54
+ }
55
+ button.setImageDrawable(value)
56
+ button.visibility = VISIBLE
57
+ }
58
+
59
+ /** The back button's label: its accessibility label and tooltip. */
60
+ var navigationContentDescription: CharSequence?
61
+ get() = navigation?.contentDescription
62
+ set(value) {
63
+ val button = navigation ?: return
64
+ button.contentDescription = value
65
+ TooltipCompat.setTooltipText(button, value)
66
+ }
67
+
68
+ /** The back button's view while it shows; none. */
69
+ val navigationButton: View?
70
+ get() = navigation?.takeIf { it.visibility == VISIBLE }
71
+
72
+ private var navigationListener: OnClickListener? = null
73
+
74
+ /** What a press of the back button does. */
75
+ fun setNavigationOnClickListener(listener: OnClickListener?) {
76
+ navigationListener = listener
77
+ navigation?.setOnClickListener(listener)
78
+ }
79
+
80
+ /** The buttons, in the line's order from its start. A view in another parent moves here. */
81
+ fun setButtons(views: List<View>) {
82
+ buttons = views
83
+ arrange()
84
+ }
85
+
86
+ /** What stands after the buttons, before "⋮"; none. */
87
+ fun setTrailing(view: View?) {
88
+ if (trailing === view) return
89
+ trailing = view
90
+ arrange()
91
+ }
92
+
93
+ /** What stands after the buttons now. */
94
+ val trailingView: View?
95
+ get() = trailing
96
+
97
+ /**
98
+ * "⋮", made the first time it is asked for and shown with [setOverflowShown]: the theme's
99
+ * `actionOverflowButtonStyle`, labelled "More options" in the user's language.
100
+ */
101
+ val overflowButton: View
102
+ get() = overflow ?: OverflowButton(context).also {
103
+ it.visibility = GONE
104
+ overflowColor?.let { color -> ImageViewCompat.setImageTintList(it, ColorStateList.valueOf(color)) }
105
+ overflow = it
106
+ arrange()
107
+ }
108
+
109
+ /** "⋮" shows at the end of the line, or not. */
110
+ fun setOverflowShown(shown: Boolean) {
111
+ if (!shown && overflow == null) return
112
+ overflowButton.visibility = if (shown) VISIBLE else GONE
113
+ }
114
+
115
+ /** The color of "⋮", now or once it is made. */
116
+ fun setOverflowColor(color: Int) {
117
+ overflowColor = color
118
+ overflow?.let { ImageViewCompat.setImageTintList(it, ColorStateList.valueOf(color)) }
119
+ }
120
+
121
+ // The children in the line's order, so accessibility reads them in it too; a view that stays where it is isn't
122
+ // moved, which would detach it.
123
+ private fun arrange() {
124
+ val wanted = listOfNotNull(navigation) + buttons + listOfNotNull(trailing, overflow)
125
+ for (index in childCount - 1 downTo 0) {
126
+ if (getChildAt(index) !in wanted) removeViewAt(index)
127
+ }
128
+ wanted.forEachIndexed { index, view ->
129
+ if (index < childCount && getChildAt(index) === view) return@forEachIndexed
130
+ (view.parent as? ViewGroup)?.removeView(view)
131
+ addView(view, index, LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT))
132
+ }
133
+ navigation?.setOnClickListener(navigationListener)
134
+ }
135
+
136
+ // As a toolbar, the row takes the touches around its buttons: they don't reach a screen under the bar.
137
+ @SuppressLint("ClickableViewAccessibility")
138
+ override fun onTouchEvent(event: MotionEvent?): Boolean = true
139
+
140
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
141
+ val height = MeasureSpec.getSize(heightMeasureSpec)
142
+ val widthMode = MeasureSpec.getMode(widthMeasureSpec)
143
+ val widthSize = MeasureSpec.getSize(widthMeasureSpec)
144
+ val buttonHeight = min(height, maxButtonHeight)
145
+ var used = 0
146
+ for (index in 0 until childCount) {
147
+ val child = getChildAt(index)
148
+ if (child.visibility == GONE) continue
149
+ val widthSpec = if (widthMode == MeasureSpec.UNSPECIFIED) {
150
+ MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
151
+ } else {
152
+ MeasureSpec.makeMeasureSpec(max(widthSize - used, 0), MeasureSpec.AT_MOST)
153
+ }
154
+ // The back button takes the whole height a toolbar gives its buttons; the others as much as they need.
155
+ val heightMode = if (child === navigation) MeasureSpec.EXACTLY else MeasureSpec.AT_MOST
156
+ child.measure(widthSpec, MeasureSpec.makeMeasureSpec(buttonHeight, heightMode))
157
+ used += child.measuredWidth
158
+ }
159
+ setMeasuredDimension(resolveSize(used, widthMeasureSpec), height)
160
+ }
161
+
162
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
163
+ val width = r - l
164
+ val height = b - t
165
+ val rtl = layoutDirection == LAYOUT_DIRECTION_RTL
166
+ var start = 0
167
+ for (index in 0 until childCount) {
168
+ val child = getChildAt(index)
169
+ if (child.visibility == GONE) continue
170
+ val w = child.measuredWidth
171
+ val x = if (rtl) width - start - w else start
172
+ val top = (height - child.measuredHeight) / 2
173
+ child.layout(x, top, x + w, top + child.measuredHeight)
174
+ start += w
175
+ }
176
+ }
177
+
178
+ // "⋮" as a toolbar draws it: its ripple a square around the icon, its label as its tooltip.
179
+ @SuppressLint("ViewConstructor")
180
+ private class OverflowButton(context: Context) :
181
+ AppCompatImageView(context, null, AppCompatR.attr.actionOverflowButtonStyle) {
182
+ init {
183
+ isClickable = true
184
+ isFocusable = true
185
+ TooltipCompat.setTooltipText(this, contentDescription)
186
+ }
187
+
188
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
189
+ super.onSizeChanged(w, h, oldw, oldh)
190
+ val background = background ?: return
191
+ if (drawable == null) return
192
+ val half = max(w, h) / 2
193
+ val centerX = (w + paddingLeft - paddingRight) / 2
194
+ val centerY = (h + paddingTop - paddingBottom) / 2
195
+ DrawableCompat.setHotspotBounds(background, centerX - half, centerY - half, centerX + half, centerY + half)
196
+ }
197
+ }
198
+
199
+ private companion object {
200
+ // A toolbar's `maxButtonHeight`.
201
+ const val MAX_BUTTON_HEIGHT_DP = 56f
202
+
203
+ // The theme a Material 3 toolbar gives its buttons (`toolbarStyle`'s overlay: its ripples), kept in the
204
+ // current light or dark mode.
205
+ fun themed(context: Context): Context {
206
+ val wrapped = MaterialThemeOverlay.wrap(
207
+ context,
208
+ null,
209
+ AppCompatR.attr.toolbarStyle,
210
+ MaterialR.style.Widget_Material3_Toolbar,
211
+ )
212
+ if (wrapped !== context) MaterialTheme.followOverlay(wrapped)
213
+ return wrapped
214
+ }
215
+ }
216
+ }
@@ -0,0 +1,82 @@
1
+ // The height of React content laid out at a width without a height limit, for the Kotlin side
2
+ // (`dev.apexnative.navigation.react.ContentHeights`): a sheet with the 'content' detent. Fabric lays out a copy of the
3
+ // tree for it, so the shown layout stays as it is: a React root's surface here, a slot's content in the slot's own
4
+ // layout (`ANNSlotShadowNode`), which keeps the height in `ANNSlotContentHeights`.
5
+
6
+ #include <fbjni/fbjni.h>
7
+ #include <jni.h>
8
+ #include <react/fabric/SurfaceHandlerBinding.h>
9
+ #include <react/renderer/components/ApexNavigationSpec/ANNSlotState.h>
10
+ #include <react/renderer/core/LayoutConstraints.h>
11
+ #include <react/renderer/core/LayoutContext.h>
12
+
13
+ #include <limits>
14
+
15
+ using namespace facebook;
16
+
17
+ namespace {
18
+
19
+ std::string slotIdOf(jstring slotId) {
20
+ return slotId ? jni::wrap_alias(slotId)->toStdString() : std::string{};
21
+ }
22
+
23
+ // A React root's surface: React Native keeps the getter of its surface handler unmangled for the native side.
24
+ struct JReactSurfaceImpl : jni::JavaClass<JReactSurfaceImpl> {
25
+ static constexpr auto kJavaDescriptor = "Lcom/facebook/react/runtime/ReactSurfaceImpl;";
26
+
27
+ const react::SurfaceHandler &surfaceHandler() const {
28
+ static const auto method =
29
+ javaClassStatic()->getMethod<react::SurfaceHandlerBinding::javaobject()>("getSurfaceHandler");
30
+ return method(self())->cthis()->getSurfaceHandler();
31
+ }
32
+ };
33
+
34
+ } // namespace
35
+
36
+ /**
37
+ * The height of the surface's content at [width] points without a height limit, in points: `SurfaceHandler::measure`,
38
+ * as iOS's surface measures for `sizeThatFits`. 0 for a surface that doesn't run; -1 on a failure.
39
+ */
40
+ extern "C" JNIEXPORT jfloat JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeSurfaceHeight(
41
+ JNIEnv * /*env*/, jclass /*type*/, jobject surface, jfloat width, jfloat pointScale, jfloat fontScale, jboolean rtl,
42
+ jboolean swapLeftAndRight) {
43
+ try {
44
+ auto ref = jni::wrap_alias(static_cast<JReactSurfaceImpl::javaobject>(surface));
45
+ react::LayoutConstraints constraints{};
46
+ constraints.minimumSize = react::Size{.width = width, .height = 0};
47
+ constraints.maximumSize = react::Size{.width = width, .height = std::numeric_limits<react::Float>::infinity()};
48
+ constraints.layoutDirection = rtl ? react::LayoutDirection::RightToLeft : react::LayoutDirection::LeftToRight;
49
+ react::LayoutContext context{};
50
+ context.pointScaleFactor = pointScale;
51
+ context.fontSizeMultiplier = fontScale;
52
+ context.swapLeftAndRightInRTL = swapLeftAndRight;
53
+ return ref->surfaceHandler().measure(constraints, context).height;
54
+ } catch (...) {
55
+ jni::translatePendingCppExceptionToJavaException();
56
+ return -1;
57
+ }
58
+ }
59
+
60
+ /** The slot's content is measured from its next layout on (`ANNSlotContentHeights::request`). */
61
+ extern "C" JNIEXPORT void JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeRequestSlot(
62
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
63
+ react::ANNSlotContentHeights::request(slotIdOf(slotId));
64
+ }
65
+
66
+ /** Whether the slot's content is measured. */
67
+ extern "C" JNIEXPORT jboolean JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeIsSlotRequested(
68
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
69
+ return react::ANNSlotContentHeights::isRequested(slotIdOf(slotId));
70
+ }
71
+
72
+ /** The height of the slot's content at its last layout, in points; negative before it. */
73
+ extern "C" JNIEXPORT jfloat JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeSlotHeight(
74
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
75
+ return react::ANNSlotContentHeights::height(slotIdOf(slotId));
76
+ }
77
+
78
+ /** The slot's screen went: nothing more is measured for it. */
79
+ extern "C" JNIEXPORT void JNICALL Java_dev_apexnative_navigation_react_ContentHeights_nativeForgetSlot(
80
+ JNIEnv * /*env*/, jclass /*type*/, jstring slotId) {
81
+ react::ANNSlotContentHeights::forget(slotIdOf(slotId));
82
+ }
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Material Icons "arrow_right" (Apache 2.0, android/MATERIAL_SYMBOLS_LICENSE.txt): the arrow of a submenu item in a
3
+ top bar menu, mirrored right to left. -->
4
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
5
+ android:width="24dp"
6
+ android:height="24dp"
7
+ android:autoMirrored="true"
8
+ android:tint="?attr/colorControlNormal"
9
+ android:viewportWidth="24"
10
+ android:viewportHeight="24">
11
+ <path
12
+ android:fillColor="@android:color/white"
13
+ android:pathData="M10,17l5,-5 -5,-5v10z" />
14
+ </vector>
@@ -0,0 +1,19 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- The title over the items of a submenu of a top bar menu (BarMenu), as AppCompat shows a submenu's title. -->
3
+ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
4
+ android:layout_width="match_parent"
5
+ android:layout_height="?attr/listPreferredItemHeightSmall"
6
+ android:minWidth="196dp"
7
+ android:paddingStart="16dp"
8
+ android:paddingEnd="16dp">
9
+
10
+ <TextView
11
+ android:id="@+id/apex_navigation_menu_title"
12
+ android:layout_width="match_parent"
13
+ android:layout_height="wrap_content"
14
+ android:layout_gravity="center_vertical"
15
+ android:ellipsize="end"
16
+ android:singleLine="true"
17
+ android:textAlignment="viewStart"
18
+ android:textAppearance="?attr/textAppearancePopupMenuHeader" />
19
+ </FrameLayout>
@@ -0,0 +1,68 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!--
3
+ An item of a menu of the top bar (BarMenu), as AppCompat lays out a popup menu's item: the icon, the title, the
4
+ submenu's arrow, the check mark of a checkable item. The inflater of the activity turns the check box and the radio
5
+ button into Material ones, as it does for AppCompat's menus.
6
+ -->
7
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
8
+ android:layout_width="match_parent"
9
+ android:layout_height="?attr/listPreferredItemHeightSmall"
10
+ android:gravity="center_vertical"
11
+ android:minWidth="196dp"
12
+ android:orientation="horizontal"
13
+ android:paddingEnd="16dp">
14
+
15
+ <ImageView
16
+ android:id="@+id/apex_navigation_menu_icon"
17
+ android:layout_width="wrap_content"
18
+ android:layout_height="wrap_content"
19
+ android:layout_marginStart="8dp"
20
+ android:layout_marginTop="8dp"
21
+ android:layout_marginEnd="-8dp"
22
+ android:layout_marginBottom="8dp"
23
+ android:duplicateParentState="true"
24
+ android:importantForAccessibility="no"
25
+ android:scaleType="centerInside"
26
+ android:visibility="gone" />
27
+
28
+ <TextView
29
+ android:id="@+id/apex_navigation_menu_title"
30
+ android:layout_width="0dp"
31
+ android:layout_height="wrap_content"
32
+ android:layout_marginStart="16dp"
33
+ android:layout_weight="1"
34
+ android:duplicateParentState="true"
35
+ android:ellipsize="end"
36
+ android:singleLine="true"
37
+ android:textAlignment="viewStart"
38
+ android:textAppearance="?attr/textAppearanceLargePopupMenu" />
39
+
40
+ <ImageView
41
+ android:id="@+id/apex_navigation_menu_arrow"
42
+ android:layout_width="wrap_content"
43
+ android:layout_height="wrap_content"
44
+ android:layout_marginStart="8dp"
45
+ android:duplicateParentState="true"
46
+ android:importantForAccessibility="no"
47
+ android:scaleType="center"
48
+ android:src="@drawable/apex_navigation_ic_arrow_right"
49
+ android:visibility="gone" />
50
+
51
+ <CheckBox
52
+ android:id="@+id/apex_navigation_menu_check"
53
+ android:layout_width="wrap_content"
54
+ android:layout_height="wrap_content"
55
+ android:clickable="false"
56
+ android:duplicateParentState="true"
57
+ android:focusable="false"
58
+ android:visibility="gone" />
59
+
60
+ <RadioButton
61
+ android:id="@+id/apex_navigation_menu_radio"
62
+ android:layout_width="wrap_content"
63
+ android:layout_height="wrap_content"
64
+ android:clickable="false"
65
+ android:duplicateParentState="true"
66
+ android:focusable="false"
67
+ android:visibility="gone" />
68
+ </LinearLayout>
@@ -8,7 +8,7 @@
8
8
  <style name="ThemeOverlay.ApexNavigation.TopBar" parent="ThemeOverlay.Material3.Toolbar.Surface" />
9
9
 
10
10
  <!--
11
- The popup theme of the bar's toolbars, for the "⋮" menu, when the app's Material 3 theme is fixed to light or to
11
+ The popup theme of the bar's "⋮" menu, when the app's Material 3 theme is fixed to light or to
12
12
  dark and doesn't follow the device's mode: a day/night overlay that gives the menu Material 3's colors of the
13
13
  current mode where the app's theme shows the other one, and leaves the app's colors in the mode it shows
14
14
  (values-night has the dark side). An app theme that follows the mode needs none: the menu takes the bar's theme.
@@ -1,6 +1,6 @@
1
1
  <?xml version="1.0" encoding="utf-8"?>
2
2
  <resources>
3
- <!-- The popup themes of the bar's toolbars in dark mode: see values/styles.xml. -->
3
+ <!-- The popup themes of the bar's "⋮" in dark mode: see values/styles.xml. -->
4
4
  <style name="ThemeOverlay.ApexNavigation.TopBar.PopupOverLight" parent="ThemeOverlay.Material3.Dark" />
5
5
  <style name="ThemeOverlay.ApexNavigation.TopBar.PopupOverDark" parent="" />
6
6
  </resources>
@@ -100,11 +100,12 @@ function appChecks({ root, run }) {
100
100
  const project = findProject(root, run);
101
101
  const plans = planProject(project);
102
102
  const app = plans.find(({ file }) => file !== undefined && file === project.app);
103
+ const metro = plans.find(({ file }) => file !== undefined && file === project.metroConfig);
103
104
  const manifest = plans.find(({ file }) => file !== undefined && file === project.manifest);
104
105
  const native = plans.filter((plan) => plan.platform !== 'js' && plan !== manifest);
105
106
  const types = plans.filter(({ name }) => name === ENV_FILE || name === 'tsconfig.json');
106
107
  const entry = plans.filter(
107
- (plan) => plan.platform === 'js' && plan !== app && !types.includes(plan)
108
+ (plan) => plan.platform === 'js' && plan !== app && plan !== metro && !types.includes(plan)
108
109
  );
109
110
  checks.push(filesCheck('Native files', native), filesCheck('Entry point', entry));
110
111
  const typed = typesCheck(root, types);
@@ -120,6 +121,20 @@ function appChecks({ root, run }) {
120
121
  });
121
122
  }
122
123
 
124
+ // Without the navigation's Metro setup an edit that isn't a boundary of Fast Refresh reloads the app.
125
+ if (metro && metro.status !== 'done') {
126
+ checks.push({
127
+ group: 'app',
128
+ title: 'Fast Refresh',
129
+ status: 'warn',
130
+ detail: `${metro.name} doesn't use withApexNavigation: some edits reload the app`,
131
+ fix:
132
+ metro.status === 'manual'
133
+ ? { label: 'Wrap the config metro.config.js exports in withApexNavigation' }
134
+ : SETUP,
135
+ });
136
+ }
137
+
123
138
  // Predictive back is optional: without it the navigation works, and Android plays no back animation of its own.
124
139
  if (manifest && manifest.status !== 'done') {
125
140
  checks.push({
package/cli/project.js CHANGED
@@ -22,6 +22,7 @@ const { exec } = require('./exec');
22
22
  * infoPlist?: string;
23
23
  * entry?: string;
24
24
  * app?: string;
25
+ * metroConfig?: string;
25
26
  * typescript: boolean;
26
27
  * xcodeProject?: string;
27
28
  * androidPackage?: string;
@@ -75,6 +76,9 @@ function findProject(root, run = exec) {
75
76
  app: ['App.tsx', 'App.ts', 'App.jsx', 'App.js']
76
77
  .map((name) => path.join(root, name))
77
78
  .find((file) => fs.existsSync(file)),
79
+ metroConfig: ['metro.config.js', 'metro.config.cjs']
80
+ .map((name) => path.join(root, name))
81
+ .find((file) => fs.existsSync(file)),
78
82
  typescript: ['App.tsx', 'App.ts', 'tsconfig.json'].some((name) =>
79
83
  fs.existsSync(path.join(root, name))
80
84
  ),
@@ -16,6 +16,7 @@ const { planEntry, navigationFile, HOW_TO: ENTRY_HOW_TO } = require('./steps/ent
16
16
  const { planEnv, targetOf } = require('./steps/env');
17
17
  const { planInfoPlist } = require('./steps/infoPlist');
18
18
  const { planMainActivity } = require('./steps/mainActivity');
19
+ const { planMetro } = require('./steps/metro');
19
20
  const { planSceneDelegate } = require('./steps/sceneDelegate');
20
21
  const { planMainApplication } = require('./steps/mainApplication');
21
22
  const { planManifest } = require('./steps/manifest');
@@ -134,6 +135,10 @@ function planProject(project) {
134
135
  plans.push(...planEnv(project.root, navigation, ts));
135
136
  // The template's App renders React Native's status bar; one moved elsewhere or written anew has nothing to change.
136
137
  if (project.app) plans.push(planFile('App.tsx', 'js', project.app, planApp, APP_HOW_TO));
138
+ // Fast Refresh for every edit; an app without a Metro config of its own has nothing to change.
139
+ if (project.metroConfig) {
140
+ plans.push(planFile('metro.config.js', 'js', project.metroConfig, planMetro, ''));
141
+ }
137
142
  return plans;
138
143
  }
139
144