@lowdefy/shell 3.23.2 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (356) hide show
  1. package/LICENSE +183 -183
  2. package/README.md +364 -2
  3. package/app/(t0,t1,t2,t3,t4)/[...path].js +38 -0
  4. package/app/(t0,t1,t2,t3,t4)/_layout.js +48 -0
  5. package/app/(t0,t1,t2,t3,t4)/index.js +38 -0
  6. package/app/+native-intent.js +39 -0
  7. package/app/_layout.js +36 -0
  8. package/app.config.ts +183 -0
  9. package/appLinks.js +65 -0
  10. package/applyShellPlugins.js +43 -0
  11. package/authCallbackIntentFilters.js +46 -0
  12. package/babel.config.js +25 -0
  13. package/checkShellConfig.js +172 -0
  14. package/checkShellPlugins.js +74 -0
  15. package/getPrivacyManifest.js +39 -0
  16. package/metro.config.js +73 -0
  17. package/native/LowdefyAppShortcuts.swift.template +84 -0
  18. package/native/withAppShortcuts.js +60 -0
  19. package/native-fingerprint.json +16 -0
  20. package/package.json +120 -40
  21. package/profiles.js +607 -0
  22. package/readShellConfig.js +36 -0
  23. package/resolveShellPlugins.js +109 -0
  24. package/runtimeVersion.js +112 -0
  25. package/scripts/checkReleaseBundle.mjs +358 -0
  26. package/scripts/createDayjsLocales.mjs +67 -0
  27. package/scripts/createShellAssets.mjs +54 -0
  28. package/scripts/createShellVersion.mjs +83 -0
  29. package/scripts/createTypeManifest.mjs +205 -0
  30. package/shell.dev.json +11 -0
  31. package/shellVariant.js +126 -0
  32. package/src/ShellContext.js +32 -0
  33. package/src/actions/CopyToClipboard.js +29 -0
  34. package/src/actions/CopyToClipboard.test.js +33 -0
  35. package/src/actions/GeolocationCurrentPosition.js +61 -0
  36. package/src/actions/GeolocationCurrentPosition.test.js +79 -0
  37. package/src/actions/GetLocalStorage.js +36 -0
  38. package/src/actions/RemoveLocalStorage.js +31 -0
  39. package/src/actions/ScrollTo.js +66 -0
  40. package/src/actions/ScrollTo.test.js +83 -0
  41. package/src/actions/SetFocus.js +33 -0
  42. package/src/actions/SetFocus.test.js +49 -0
  43. package/src/actions/SetLocalStorage.js +34 -0
  44. package/src/actions/localStorage.test.js +68 -0
  45. package/src/actions/measureInScrollView.js +36 -0
  46. package/src/actions/nativeActions.js +41 -0
  47. package/src/auth/AuthButton.js +44 -0
  48. package/src/auth/AuthGate.js +57 -0
  49. package/src/auth/AuthLink.js +43 -0
  50. package/src/auth/LockScreen.js +67 -0
  51. package/src/auth/LockScreen.test.js +37 -0
  52. package/src/auth/SignInScreen.js +268 -0
  53. package/src/auth/SignInScreen.test.js +187 -0
  54. package/src/auth/createAppAuth.js +325 -0
  55. package/src/auth/createAppAuth.test.js +424 -0
  56. package/src/auth/createAuthApi.js +115 -0
  57. package/src/auth/createAuthApi.test.js +138 -0
  58. package/src/auth/createCoreAuth.js +96 -0
  59. package/src/auth/createCoreAuth.test.js +188 -0
  60. package/src/auth/createDeviceKey.js +62 -0
  61. package/src/auth/createDeviceKey.test.js +54 -0
  62. package/src/auth/createSessionStores.js +45 -0
  63. package/src/auth/createTokenStore.js +103 -0
  64. package/src/auth/createTokenStore.test.js +116 -0
  65. package/src/auth/getAuthSettings.js +24 -0
  66. package/src/auth/getAuthView.js +39 -0
  67. package/src/auth/getAuthView.test.js +52 -0
  68. package/src/auth/getSheetReturnUrl.js +35 -0
  69. package/src/auth/getTokenStoreOptions.js +46 -0
  70. package/src/auth/isBackgroundShell.js +26 -0
  71. package/src/auth/isSheetReturnLink.js +35 -0
  72. package/src/auth/parseQuery.js +39 -0
  73. package/src/auth/purgeAppData.js +80 -0
  74. package/src/auth/purgeAppData.test.js +104 -0
  75. package/src/auth/shouldLock.js +26 -0
  76. package/src/auth/signInWithSheet.js +97 -0
  77. package/src/auth/signInWithSheet.test.js +205 -0
  78. package/src/auth/styles.js +78 -0
  79. package/src/auth/unregisterPushToken.js +62 -0
  80. package/src/auth/unregisterPushToken.test.js +67 -0
  81. package/src/auth/usesBiometricUnlock.js +23 -0
  82. package/src/auth/watchSession.js +47 -0
  83. package/src/auth/withoutCookies.js +26 -0
  84. package/src/cache/createSqliteCacheStore.js +113 -0
  85. package/src/cache/createSqliteCacheStore.test.js +93 -0
  86. package/src/cache/getCacheKey.js +25 -0
  87. package/src/cache/getLastCacheKeyName.js +22 -0
  88. package/src/components/AppProvider.js +118 -0
  89. package/src/components/Frame.js +116 -0
  90. package/src/components/Frame.test.js +101 -0
  91. package/src/components/LoadingScreen.js +35 -0
  92. package/src/components/MenuButton.js +121 -0
  93. package/src/components/MenuButton.test.js +74 -0
  94. package/src/components/MessageScreen.js +51 -0
  95. package/src/components/NavigationTheme.js +33 -0
  96. package/src/components/NoConnectionBanner.js +51 -0
  97. package/src/components/NoConnectionBanner.test.js +73 -0
  98. package/src/components/OtherPageScreen.js +95 -0
  99. package/src/components/OtherPageScreen.test.js +74 -0
  100. package/src/components/PageScreen.js +254 -0
  101. package/src/components/PageScreen.test.js +492 -0
  102. package/src/components/RouteErrorScreen.js +47 -0
  103. package/src/components/ShellRoot.js +34 -0
  104. package/src/components/ShellRoot.test.js +62 -0
  105. package/src/components/ShellThemeProvider.js +45 -0
  106. package/src/components/SwitchToTab.js +36 -0
  107. package/src/components/SwitchToTab.test.js +63 -0
  108. package/src/components/TabIconImages.js +72 -0
  109. package/src/components/UpdateScreen.js +40 -0
  110. package/src/components/getNavigationTheme.js +44 -0
  111. package/src/components/getNavigationTheme.test.js +41 -0
  112. package/src/components/styles.js +50 -0
  113. package/src/components/useSurfaceTokens.js +47 -0
  114. package/src/config/getAppApiBase.js +34 -0
  115. package/src/config/getShellConfig.js +37 -0
  116. package/src/config/getShellConfig.test.js +80 -0
  117. package/src/createShellApp.js +126 -0
  118. package/src/dev/DevShell.js +239 -0
  119. package/src/dev/DevShell.test.js +233 -0
  120. package/src/dev/connect/ConfirmSheet.js +71 -0
  121. package/src/dev/connect/ConnectScreen.js +117 -0
  122. package/src/dev/connect/QrScanner.js +89 -0
  123. package/src/dev/connection/createDevShellConfig.js +47 -0
  124. package/src/dev/connection/createDevTransport.js +51 -0
  125. package/src/dev/connection/createDevTransport.test.js +87 -0
  126. package/src/dev/connection/createServerList.js +48 -0
  127. package/src/dev/connection/isLoopbackHost.js +25 -0
  128. package/src/dev/connection/pairedShellApp.test.js +104 -0
  129. package/src/dev/connection/parseDevLink.js +66 -0
  130. package/src/dev/connection/parseDevLink.test.js +97 -0
  131. package/src/dev/connection/parseQuery.js +33 -0
  132. package/src/dev/connection/parseServerInput.js +40 -0
  133. package/src/dev/connection/parseServerUrl.js +45 -0
  134. package/src/dev/devStyles.js +97 -0
  135. package/src/dev/index.js +21 -0
  136. package/src/dev/inject/createDeviceInjections.js +188 -0
  137. package/src/dev/inject/createDeviceInjections.test.js +224 -0
  138. package/src/dev/inject/createOfflineFetch.js +35 -0
  139. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  140. package/src/dev/inject/injectPush.js +60 -0
  141. package/src/dev/inject/injectShare.js +55 -0
  142. package/src/dev/inject/withInjectedDevice.js +98 -0
  143. package/src/dev/menu/BlockGallery.js +33 -0
  144. package/src/dev/menu/DevMenu.js +121 -0
  145. package/src/dev/menu/createLogBuffer.js +83 -0
  146. package/src/dev/menu/watchShake.js +52 -0
  147. package/src/dev/menu/watchShake.test.js +61 -0
  148. package/src/dev/recorder/createNativeRecorder.js +204 -0
  149. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  150. package/src/dev/redirectDevLink.js +30 -0
  151. package/src/dev/redirectDevLink.test.js +49 -0
  152. package/src/dev/steps/createStepRunner.js +184 -0
  153. package/src/dev/steps/createStepRunner.test.js +262 -0
  154. package/src/dev/steps/findBlock.js +78 -0
  155. package/src/dev/steps/isContextSettled.js +37 -0
  156. package/src/dev/steps/waitForSettle.js +39 -0
  157. package/src/dev/steps/waitUntil.js +40 -0
  158. package/src/dev/tab/DevTabClient.js +97 -0
  159. package/src/dev/tab/answerEval.js +39 -0
  160. package/src/dev/tab/answerInspect.js +45 -0
  161. package/src/dev/tab/createDevTabClient.js +272 -0
  162. package/src/dev/tab/createDevTabClient.test.js +317 -0
  163. package/src/dev/tab/describeTabError.js +35 -0
  164. package/src/dev/tab/devClientMarker.js +22 -0
  165. package/src/dev/tab/devClientMarker.test.js +23 -0
  166. package/src/dev/tab/findPageContext.js +43 -0
  167. package/src/dev/tab/getShownPage.js +42 -0
  168. package/src/dev/tab/readEventStream.js +68 -0
  169. package/src/dev/tab/reloadApp.js +42 -0
  170. package/src/dev/tab/reloadApp.test.js +115 -0
  171. package/src/device/DeviceHosts.js +32 -0
  172. package/src/device/basic/createFilesFeature.js +41 -0
  173. package/src/device/basic/createHapticsFeature.js +46 -0
  174. package/src/device/basic/createKeepAwakeFeature.js +37 -0
  175. package/src/device/basic/createPhotosFeature.js +40 -0
  176. package/src/device/basic/createRequestQueueFeature.js +37 -0
  177. package/src/device/basic/createShareFeature.js +63 -0
  178. package/src/device/basic/createShareFeature.test.js +47 -0
  179. package/src/device/basic/createSpeechFeature.js +43 -0
  180. package/src/device/basic/createUploadFeature.js +41 -0
  181. package/src/device/basic/toImagePickerOptions.js +41 -0
  182. package/src/device/basic/toPickedFile.js +54 -0
  183. package/src/device/calendar/index.js +68 -0
  184. package/src/device/contacts/index.js +67 -0
  185. package/src/device/createDevice.js +65 -0
  186. package/src/device/device.js +27 -0
  187. package/src/device/field/MapView.js +93 -0
  188. package/src/device/field/ScanCodeSheet.js +86 -0
  189. package/src/device/field/ScannerView.js +52 -0
  190. package/src/device/field/createAudioFeature.js +70 -0
  191. package/src/device/field/createBrightnessFeature.js +40 -0
  192. package/src/device/field/createCameraFeature.js +56 -0
  193. package/src/device/field/createDocumentScannerFeature.js +51 -0
  194. package/src/device/field/createLocationFeature.js +39 -0
  195. package/src/device/field/createScanRequests.js +55 -0
  196. package/src/device/field/index.js +39 -0
  197. package/src/device/field/scanRequests.js +22 -0
  198. package/src/device/injected.js +60 -0
  199. package/src/device/injected.test.js +39 -0
  200. package/src/device/nfc/index.js +89 -0
  201. package/src/device/nfc/toNfcRecord.js +44 -0
  202. package/src/device/nfc/toNfcRecord.test.js +48 -0
  203. package/src/device/tracking/createBacklog.js +63 -0
  204. package/src/device/tracking/createBacklog.test.js +47 -0
  205. package/src/device/tracking/index.js +211 -0
  206. package/src/device/tracking/index.test.js +110 -0
  207. package/src/host/createDeviceStore.js +32 -0
  208. package/src/host/createMemoryStore.js +32 -0
  209. package/src/host/createNetworkStatus.js +94 -0
  210. package/src/host/createNetworkStatus.test.js +94 -0
  211. package/src/host/createRouterAdapter.js +33 -0
  212. package/src/host/createShellFetch.js +62 -0
  213. package/src/host/createShellFetch.test.js +165 -0
  214. package/src/host/createShellGlobals.js +62 -0
  215. package/src/host/createShellHost.js +83 -0
  216. package/src/host/createShellHost.test.js +36 -0
  217. package/src/host/getOrigin.js +31 -0
  218. package/src/host/isNetworkError.js +24 -0
  219. package/src/host/isOnline.js +22 -0
  220. package/src/host/networkStatus.js +24 -0
  221. package/src/host/readServerError.js +45 -0
  222. package/src/host/readServerError.test.js +85 -0
  223. package/src/host/reportShellError.js +47 -0
  224. package/src/host/reportShellError.test.js +79 -0
  225. package/src/host/withNetworkErrors.js +38 -0
  226. package/src/host/withNetworkErrors.test.js +61 -0
  227. package/src/host/withShellHeader.js +25 -0
  228. package/src/host/withShellHeader.test.js +28 -0
  229. package/src/js/evaluateJsEntries.js +43 -0
  230. package/src/js/evaluateJsEntries.test.js +48 -0
  231. package/src/locale/createShellLocale.js +112 -0
  232. package/src/locale/createShellLocale.test.js +74 -0
  233. package/src/locale/dayjsLocales.js +165 -0
  234. package/src/locale/getDeviceLocales.js +23 -0
  235. package/src/locale/loadDayjsLocale.js +32 -0
  236. package/src/locale/resolveActiveLocale.js +46 -0
  237. package/src/locale/resolveActiveLocale.test.js +40 -0
  238. package/src/native/backgroundFetch.js +97 -0
  239. package/src/native/callEndpoint.js +63 -0
  240. package/src/native/createAppEventTrigger.js +70 -0
  241. package/src/native/currentApp.js +49 -0
  242. package/src/native/getPagePath.js +32 -0
  243. package/src/native/openPath.js +55 -0
  244. package/src/native/openPath.test.js +39 -0
  245. package/src/native/parseShellLink.js +38 -0
  246. package/src/native/parseShellLink.test.js +29 -0
  247. package/src/native/startAppServices.js +94 -0
  248. package/src/native/startAppServices.test.js +97 -0
  249. package/src/native/startShareIn.js +59 -0
  250. package/src/native/startShortcuts.js +88 -0
  251. package/src/native/startShortcuts.test.js +59 -0
  252. package/src/native/toSharePayload.js +47 -0
  253. package/src/native/toSharePayload.test.js +47 -0
  254. package/src/native/whenAppReady.js +48 -0
  255. package/src/navigation/SlotContext.js +24 -0
  256. package/src/navigation/frameSlots.js +27 -0
  257. package/src/navigation/getFrame.js +30 -0
  258. package/src/navigation/getInitialScreenOptions.js +63 -0
  259. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  260. package/src/navigation/getPresentation.js +28 -0
  261. package/src/navigation/getSlotRootPath.js +27 -0
  262. package/src/navigation/getStackOptions.js +185 -0
  263. package/src/navigation/getTabHref.js +45 -0
  264. package/src/navigation/getTabIcons.js +43 -0
  265. package/src/navigation/isPageUnderHeader.js +46 -0
  266. package/src/navigation/navigation.test.js +342 -0
  267. package/src/navigation/toAppLinkPath.js +43 -0
  268. package/src/navigation/toAppLinkPath.test.js +53 -0
  269. package/src/navigation/toRouterHref.js +29 -0
  270. package/src/notifications/createNotificationsFeature.js +61 -0
  271. package/src/notifications/getCategoryAction.js +31 -0
  272. package/src/notifications/handleNotificationResponse.js +82 -0
  273. package/src/notifications/registerPushToken.js +71 -0
  274. package/src/notifications/startNotifications.js +114 -0
  275. package/src/notifications/toCategoryActions.js +42 -0
  276. package/src/notifications/toNotificationEvent.js +36 -0
  277. package/src/notifications/toScheduleRequest.js +62 -0
  278. package/src/plugins/appTypes.js +28 -0
  279. package/src/plugins/prefixTypes.js +28 -0
  280. package/src/plugins/shellPlugins.json +15 -0
  281. package/src/plugins/typeManifest.js +789 -0
  282. package/src/plugins/types.js +64 -0
  283. package/src/plugins/types.test.js +173 -0
  284. package/src/polyfills/installGetRandomValues.js +28 -0
  285. package/src/queue/createRequestQueue.js +198 -0
  286. package/src/queue/createRequestQueue.test.js +215 -0
  287. package/src/queue/createSqliteQueueStore.js +121 -0
  288. package/src/queue/summarizeQueue.js +39 -0
  289. package/src/session/createAppSession.js +323 -0
  290. package/src/session/createAppSession.test.js +391 -0
  291. package/src/session/getPageView.js +59 -0
  292. package/src/session/getPageView.test.js +137 -0
  293. package/src/sync/encodePagePath.js +27 -0
  294. package/src/sync/fetchPage.js +72 -0
  295. package/src/sync/fetchPage.test.js +84 -0
  296. package/src/sync/fetchRoot.js +43 -0
  297. package/src/sync/fetchUser.js +33 -0
  298. package/src/sync/runLimited.js +36 -0
  299. package/src/sync/toRootConfig.js +35 -0
  300. package/src/test/createFakeNetwork.js +40 -0
  301. package/src/test/createFakeSecureStore.js +40 -0
  302. package/src/test/createFakeServer.js +98 -0
  303. package/src/test/createMemoryCacheStore.js +59 -0
  304. package/src/test/createNetworkError.js +24 -0
  305. package/src/test/expoImage.js +42 -0
  306. package/src/test/expoNotifications.js +40 -0
  307. package/src/test/expoQuickActions.js +22 -0
  308. package/src/test/expoSpeechRecognition.js +31 -0
  309. package/src/test/expoTaskManager.js +25 -0
  310. package/src/test/expoUi.js +55 -0
  311. package/src/test/expoUiBottomSheet.js +64 -0
  312. package/src/test/expoUiDateTimePicker.js +26 -0
  313. package/src/test/expoUiMenu.js +46 -0
  314. package/src/test/expoUiSegmentedControl.js +47 -0
  315. package/src/test/expoUiSwiftUi.js +62 -0
  316. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  317. package/src/test/expoWebBrowser.js +23 -0
  318. package/src/test/fixtures.js +98 -0
  319. package/src/test/nativeModule.js +33 -0
  320. package/src/test/renderPageScreen.js +109 -0
  321. package/src/types/checkTypeSet.js +39 -0
  322. package/src/types/collectPageTypes.js +49 -0
  323. package/src/types/collectPageTypes.test.js +90 -0
  324. package/src/types/findMissingPageTypes.js +40 -0
  325. package/src/version/compareVersions.js +38 -0
  326. package/src/version/isShellTooOld.js +30 -0
  327. package/src/version/shellVersion.js +21 -0
  328. package/src/version/shellVersion.test.js +32 -0
  329. package/src/widgets/fetchWidgetData.js +59 -0
  330. package/src/widgets/index.js +156 -0
  331. package/src/widgets/refreshWidgets.js +31 -0
  332. package/withExcludedNativeModules.js +93 -0
  333. package/dist/location.js +0 -33
  334. package/dist/public/apple-touch-icon.png +0 -0
  335. package/dist/public/icon-32.png +0 -0
  336. package/dist/public/icon-512.png +0 -0
  337. package/dist/public/icon.svg +0 -17
  338. package/dist/public/logo-dark-theme.png +0 -0
  339. package/dist/public/logo-light-theme.png +0 -0
  340. package/dist/public/logo-square-dark-theme.png +0 -0
  341. package/dist/public/logo-square-light-theme.png +0 -0
  342. package/dist/public/manifest.webmanifest +0 -16
  343. package/dist/shell/246.93432ff68186aae6c87b.js +0 -2
  344. package/dist/shell/246.93432ff68186aae6c87b.js.LICENSE.txt +0 -8
  345. package/dist/shell/454.b597ca21366afda000af.js +0 -2
  346. package/dist/shell/454.b597ca21366afda000af.js.LICENSE.txt +0 -14
  347. package/dist/shell/484.d645e6c09079f45ce044.js +0 -1
  348. package/dist/shell/706.a66931e02a549fd269bd.js +0 -2
  349. package/dist/shell/706.a66931e02a549fd269bd.js.LICENSE.txt +0 -8
  350. package/dist/shell/9.78be8c1b6c3986f404fc.js +0 -2
  351. package/dist/shell/9.78be8c1b6c3986f404fc.js.LICENSE.txt +0 -45
  352. package/dist/shell/922.43b7a3ebe22ef05b3049.js +0 -2
  353. package/dist/shell/922.43b7a3ebe22ef05b3049.js.LICENSE.txt +0 -14
  354. package/dist/shell/index.html +0 -57
  355. package/dist/shell/main.bde87d1433f0625faed6.js +0 -1
  356. package/dist/shell/runtime.1ead93159ba56c05b969.js +0 -1
@@ -0,0 +1,48 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * Waits until the open app can act for its caller: the auth has read the token and is open (not
19
+ * starting, not locked) and the session holds a root. Launch work (a notification button, a quick
20
+ * action, share-in, the queue's replay, background fetch) waits on it, so none of it runs with no
21
+ * token loaded or no root to read.
22
+ *
23
+ * @param {Object} params
24
+ * @param {Object} params.auth The app's auth (createAppAuth).
25
+ * @param {Object} params.session The app's session (createAppSession).
26
+ * @returns {{ promise: Promise<void>, stop: () => void }} stop stops waiting; the promise then never
27
+ * settles.
28
+ */
29
+ function whenAppReady({ auth, session }) {
30
+ const unsubscribes = [];
31
+ const stop = () => unsubscribes.splice(0).forEach((unsubscribe) => unsubscribe());
32
+ const promise = new Promise((resolve) => {
33
+ function check() {
34
+ if (auth.getState().status === 'open' && session.getState().root !== null) {
35
+ stop();
36
+ resolve();
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ if (!check()) {
42
+ unsubscribes.push(auth.subscribe(check), session.subscribe(check));
43
+ }
44
+ });
45
+ return { promise, stop };
46
+ }
47
+
48
+ export default whenAppReady;
@@ -0,0 +1,24 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { createContext } from 'react';
18
+
19
+ // The index of the frame group (app/(t0,t1,t2,t3,t4)) a screen lives in, which its group layout
20
+ // provides from the segment Expo Router gives it. A screen cannot read it from useSegments: those
21
+ // are the focused route's, so every tab rendered at once would take the focused tab's index.
22
+ const SlotContext = createContext(0);
23
+
24
+ export default SlotContext;
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The route groups the frame's screens live in: app/(t0,t1,t2,t3,t4) is one set of routes shared
18
+ // by five groups. With a tabs frame each tab is a group, with its own stack; mobile.menus.tabs has
19
+ // at most five items. A drawer or stack frame uses the first group only.
20
+ const FRAME_SLOTS = ['t0', 't1', 't2', 't3', 't4'];
21
+
22
+ function getSlotIndex(segment) {
23
+ return FRAME_SLOTS.indexOf(String(segment).replace(/^\(|\)$/g, ''));
24
+ }
25
+
26
+ export { getSlotIndex };
27
+ export default FRAME_SLOTS;
@@ -0,0 +1,30 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The frame an app's mobile menus declare: { kind: 'tabs', items }, { kind: 'drawer', items } or
18
+ // { kind: 'stack' }. The server drops the items a caller may not open, so a tabs frame left with
19
+ // fewer than two items, or a drawer with none, falls back to a stack.
20
+ function getFrame(menus) {
21
+ if ((menus?.tabs?.length ?? 0) >= 2) {
22
+ return { kind: 'tabs', items: menus.tabs };
23
+ }
24
+ if ((menus?.drawer?.length ?? 0) >= 1) {
25
+ return { kind: 'drawer', items: menus.drawer };
26
+ }
27
+ return { kind: 'stack', items: [] };
28
+ }
29
+
30
+ export default getFrame;
@@ -0,0 +1,63 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { type } from '@lowdefy/helpers';
18
+
19
+ function noop() {}
20
+
21
+ /**
22
+ * The header options a Screen page's config gives before the page renders, in the shape a Screen
23
+ * reports (client-native screen/Screen.js): the parts that size the bar (a large title, a search
24
+ * field) and how it is drawn (a backgroundTint's clear bar), from literal properties. The Screen
25
+ * reports its full options once it renders; these set the bar first, so the page's scroll view is
26
+ * first laid out under the bar it will have. iOS keeps a large title collapsed when the page's
27
+ * scroll view was laid out under a standard bar before the large title came.
28
+ *
29
+ * @param {Object} params
30
+ * @param {Object} params.pageConfig The page config, as the server sends it.
31
+ * @returns {Object|null} The options, or null for a page that is not a Screen.
32
+ */
33
+ function getInitialScreenOptions({ pageConfig }) {
34
+ if (pageConfig.type !== 'Screen') {
35
+ return null;
36
+ }
37
+ const properties = pageConfig.properties ?? {};
38
+ const headerSearch = properties.headerSearch;
39
+ return {
40
+ backgroundTint: !type.isNone(properties.backgroundTint),
41
+ headerActions: [],
42
+ headerBottom: false,
43
+ headerLeft: undefined,
44
+ headerRight: undefined,
45
+ headerSearch: type.isObject(headerSearch)
46
+ ? {
47
+ autoFocus: false,
48
+ hideWhenScrolling: headerSearch.hideWhenScrolling === true,
49
+ onCancel: noop,
50
+ onChangeText: noop,
51
+ onSearch: noop,
52
+ placeholder: type.isString(headerSearch.placeholder)
53
+ ? headerSearch.placeholder
54
+ : undefined,
55
+ }
56
+ : undefined,
57
+ largeTitle: properties.largeTitle === true,
58
+ presentation: type.isString(properties.presentation) ? properties.presentation : 'card',
59
+ title: type.isString(properties.title) ? properties.title : undefined,
60
+ };
61
+ }
62
+
63
+ export default getInitialScreenOptions;
@@ -0,0 +1,66 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import getInitialScreenOptions from './getInitialScreenOptions.js';
18
+
19
+ test('getInitialScreenOptions reads the bar a Screen config gives before it renders', () => {
20
+ const options = getInitialScreenOptions({
21
+ pageConfig: {
22
+ type: 'Screen',
23
+ properties: {
24
+ title: 'My work',
25
+ largeTitle: true,
26
+ headerSearch: { placeholder: 'Search designs', hideWhenScrolling: true },
27
+ backgroundTint: { color: '#ff7a45' },
28
+ },
29
+ },
30
+ });
31
+ expect(options).toMatchObject({
32
+ backgroundTint: true,
33
+ headerActions: [],
34
+ headerBottom: false,
35
+ largeTitle: true,
36
+ presentation: 'card',
37
+ title: 'My work',
38
+ });
39
+ expect(options.headerSearch).toMatchObject({
40
+ autoFocus: false,
41
+ hideWhenScrolling: true,
42
+ placeholder: 'Search designs',
43
+ });
44
+ // The Screen sets the search handlers once it renders.
45
+ expect(() => options.headerSearch.onChangeText('a')).not.toThrow();
46
+ });
47
+
48
+ test('getInitialScreenOptions takes only literal properties, and leaves the rest to the Screen', () => {
49
+ const options = getInitialScreenOptions({
50
+ pageConfig: {
51
+ type: 'Screen',
52
+ properties: { title: { _state: 'task.key' }, largeTitle: { _state: 'large' } },
53
+ },
54
+ });
55
+ expect(options).toMatchObject({
56
+ backgroundTint: false,
57
+ headerSearch: undefined,
58
+ largeTitle: false,
59
+ title: undefined,
60
+ });
61
+ expect(getInitialScreenOptions({ pageConfig: { type: 'Screen' } }).largeTitle).toBe(false);
62
+ });
63
+
64
+ test('getInitialScreenOptions gives nothing for a page that is not a Screen', () => {
65
+ expect(getInitialScreenOptions({ pageConfig: { type: 'Box', properties: {} } })).toBeNull();
66
+ });
@@ -0,0 +1,28 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // The native stack presentation for a Screen page's presentation property.
18
+ const PRESENTATIONS = {
19
+ card: 'card',
20
+ modal: 'modal',
21
+ sheet: 'formSheet',
22
+ };
23
+
24
+ function getPresentation(presentation) {
25
+ return PRESENTATIONS[presentation] ?? 'card';
26
+ }
27
+
28
+ export default getPresentation;
@@ -0,0 +1,27 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import getFrame from './getFrame.js';
18
+
19
+ // The page path a frame group's first screen shows: its tab's page, or the app's home page.
20
+ function getSlotRootPath({ root, slotIndex }) {
21
+ const frame = getFrame(root.menus);
22
+ const pageId = frame.kind === 'tabs' ? frame.items[slotIndex]?.pageId : root.homePageId;
23
+ const entry = root.pageIndex.find((indexEntry) => indexEntry.pageId === pageId);
24
+ return entry?.path ?? null;
25
+ }
26
+
27
+ export default getSlotRootPath;
@@ -0,0 +1,185 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React from 'react';
18
+ import { View } from 'react-native';
19
+
20
+ import getPresentation from './getPresentation.js';
21
+
22
+ const SIDES = { left: 'Left', right: 'Right' };
23
+
24
+ function toSfSymbol(name) {
25
+ return name === null ? undefined : { type: 'sfSymbol', name };
26
+ }
27
+
28
+ // A menu item as a native UIMenu action: its title, its SF Symbol, and red when destructive.
29
+ function toNativeMenuAction(item) {
30
+ return {
31
+ type: 'action',
32
+ destructive: item.destructive,
33
+ disabled: item.disabled,
34
+ icon: toSfSymbol(item.symbol),
35
+ label: item.title,
36
+ onPress: item.onPress,
37
+ };
38
+ }
39
+
40
+ // An action as a native iOS bar button: its SF Symbol, in the bar's shared glass background. An
41
+ // action with a menu is a bar button that opens a native menu (UIMenu) of its items, drawn with
42
+ // its title when its icon has no SF Symbol. Any other icon with no SF Symbol draws as its Lucide
43
+ // icon in a custom item.
44
+ function toNativeItem(action) {
45
+ if (action.menu !== null) {
46
+ return {
47
+ type: 'menu',
48
+ accessibilityLabel: action.title,
49
+ disabled: action.disabled,
50
+ icon: toSfSymbol(action.sfSymbol),
51
+ identifier: action.id,
52
+ label: action.title,
53
+ menu: { items: action.menu.map(toNativeMenuAction) },
54
+ };
55
+ }
56
+ if (action.sfSymbol === null) {
57
+ return { type: 'custom', element: action.render() };
58
+ }
59
+ return {
60
+ type: 'button',
61
+ accessibilityLabel: action.title,
62
+ disabled: action.disabled,
63
+ icon: { type: 'sfSymbol', name: action.sfSymbol },
64
+ identifier: action.id,
65
+ label: action.title,
66
+ onPress: action.onPress,
67
+ };
68
+ }
69
+
70
+ // One side of the header (left or right): the Screen's slot blocks, then its actions on that
71
+ // side. On iOS, a side with actions is a list of native bar items (the slot as a custom item); a
72
+ // side with only the slot is its custom view. Elsewhere, a side is a row of the slot and the
73
+ // actions as icon buttons. A side with neither is left out, so a drawer's menu button stays.
74
+ function getSideOptions({ actions, platform, side, slot }) {
75
+ const sideActions = actions.filter((action) => action.placement === side);
76
+ if (sideActions.length === 0 && slot === undefined) {
77
+ return {};
78
+ }
79
+ const name = SIDES[side];
80
+ if (platform === 'ios') {
81
+ if (sideActions.length === 0) {
82
+ return { [`header${name}`]: slot, [`unstable_header${name}Items`]: undefined };
83
+ }
84
+ return {
85
+ [`header${name}`]: undefined,
86
+ [`unstable_header${name}Items`]: () => [
87
+ ...(slot === undefined ? [] : [{ type: 'custom', element: slot() }]),
88
+ ...sideActions.map(toNativeItem),
89
+ ],
90
+ };
91
+ }
92
+ return {
93
+ [`header${name}`]: () => (
94
+ <View style={{ alignItems: 'center', columnGap: 4, flexDirection: 'row' }}>
95
+ {slot === undefined ? null : slot()}
96
+ {sideActions.map((action) => action.render())}
97
+ </View>
98
+ ),
99
+ };
100
+ }
101
+
102
+ // The native search bar's options from a Screen's headerSearch: its text events carry the text
103
+ // in nativeEvent.text. searchBarRef lets the screen focus it (autoFocus).
104
+ function getSearchOptions({ headerSearch, searchBarRef }) {
105
+ if (headerSearch === undefined) {
106
+ return undefined;
107
+ }
108
+ return {
109
+ autoFocus: headerSearch.autoFocus,
110
+ hideWhenScrolling: headerSearch.hideWhenScrolling,
111
+ onCancelButtonPress: () => headerSearch.onCancel(),
112
+ onChangeText: (event) => headerSearch.onChangeText(event.nativeEvent.text),
113
+ onSearchButtonPress: (event) => headerSearch.onSearch(event.nativeEvent.text),
114
+ placeholder: headerSearch.placeholder,
115
+ ref: searchBarRef,
116
+ };
117
+ }
118
+
119
+ // A Screen with a backgroundTint on iOS: a clear bar the page scrolls under, so the tint shows
120
+ // behind the status bar, the bar, the large title and the search field. iOS 26's glass bar is
121
+ // clear already, with the system's scroll edge effect under it once the page scrolls; before
122
+ // iOS 26 the bar stays clear at the scroll edge (the large title's appearance, which a standard
123
+ // title's scroll edge takes too) and takes the system chrome material once content scrolls
124
+ // under it. Elsewhere the bar is the stack's own: the options are set to undefined, since
125
+ // navigation.setOptions merges, so a tint that goes gives the bar back.
126
+ function getTintOptions({ backgroundTint, platform, platformVersion }) {
127
+ if (backgroundTint !== true || platform !== 'ios') {
128
+ return {
129
+ headerBlurEffect: undefined,
130
+ headerLargeStyle: undefined,
131
+ headerTransparent: undefined,
132
+ };
133
+ }
134
+ if (Number.parseInt(`${platformVersion}`, 10) >= 26) {
135
+ return { headerBlurEffect: undefined, headerLargeStyle: undefined, headerTransparent: true };
136
+ }
137
+ return {
138
+ headerBlurEffect: 'systemChromeMaterial',
139
+ headerLargeStyle: { backgroundColor: 'transparent' },
140
+ headerTransparent: true,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * The native stack options for a Screen page's header options (client-native screen/Screen.js):
146
+ * the title (a large title on iOS that collapses into the bar as the page scrolls), the
147
+ * presentation, a native search bar, and the header's slots and icon buttons at their natural
148
+ * size, as native bar buttons on iOS. A page with a headerBottom hides the bar's own hairline:
149
+ * the pinned strip under the bar draws one under itself. A page with a backgroundTint has a clear
150
+ * bar on iOS (getTintOptions).
151
+ *
152
+ * @param {Object} params
153
+ * @param {Object} params.options What the Screen reported.
154
+ * @param {string} params.platform Platform.OS.
155
+ * @param {string|number} [params.platformVersion] Platform.Version: iOS gives a string, "26.0".
156
+ * @param {Object} params.searchBarRef The ref the search bar's commands (focus) are set on.
157
+ */
158
+ function getStackOptions({ options, platform, platformVersion, searchBarRef }) {
159
+ const {
160
+ backgroundTint,
161
+ headerActions,
162
+ headerBottom,
163
+ headerLeft,
164
+ headerRight,
165
+ headerSearch,
166
+ largeTitle,
167
+ presentation,
168
+ title,
169
+ } = options;
170
+ return {
171
+ headerBackVisible: true,
172
+ // Undefined keeps the stack's own default.
173
+ headerShadowVisible: headerBottom === true ? false : undefined,
174
+ headerLargeTitleEnabled: largeTitle,
175
+ headerLargeTitleShadowVisible: false,
176
+ headerSearchBarOptions: getSearchOptions({ headerSearch, searchBarRef }),
177
+ presentation: getPresentation(presentation),
178
+ title: title ?? '',
179
+ ...getSideOptions({ actions: headerActions, platform, side: 'left', slot: headerLeft }),
180
+ ...getSideOptions({ actions: headerActions, platform, side: 'right', slot: headerRight }),
181
+ ...getTintOptions({ backgroundTint, platform, platformVersion }),
182
+ };
183
+ }
184
+
185
+ export default getStackOptions;
@@ -0,0 +1,45 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import getFrame from './getFrame.js';
18
+
19
+ /**
20
+ * The router href of the tab whose page is at `path`, with the query as the tab's params, or null
21
+ * when no tab shows that page (a stack or drawer frame, or a page no tab has). A tab's own page is
22
+ * shown on its tab, never stacked on another tab's screens, so the tab bar shows the tab that
23
+ * holds the page on screen.
24
+ *
25
+ * @param {Object} params
26
+ * @param {string} params.path The page path, without the leading "/".
27
+ * @param {string} [params.query] The query string, without "?".
28
+ * @param {Object} params.root The app's mobile root.
29
+ * @returns {string|null}
30
+ */
31
+ function getTabHref({ path, query, root }) {
32
+ const frame = getFrame(root?.menus);
33
+ if (frame.kind !== 'tabs') {
34
+ return null;
35
+ }
36
+ const slotIndex = frame.items.findIndex(
37
+ (item) => root.pageIndex.find((entry) => entry.pageId === item.pageId)?.path === path
38
+ );
39
+ if (slotIndex === -1) {
40
+ return null;
41
+ }
42
+ return `/(t${slotIndex})${query ? `?${query}` : ''}`;
43
+ }
44
+
45
+ export default getTabHref;
@@ -0,0 +1,43 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { getSfSymbol } from '@lowdefy/client-native';
18
+
19
+ /**
20
+ * How each tab of a tabs frame draws its icon: on iOS, an SF Symbol (the item's `sfSymbol`, or
21
+ * the symbol mapped from its Lucide or semantic icon name); otherwise, or for an icon with no
22
+ * symbol, its Lucide icon as an image (TabIconImages), from the icon data the build adds to the
23
+ * item. A tab with no icon has none.
24
+ *
25
+ * @param {Object} params
26
+ * @param {Object[]} params.items The tabs frame's items.
27
+ * @param {string} params.platform Platform.OS.
28
+ * @returns {Object[]} Per item, `{ pageId, sf, iconData }`: `sf` an SF Symbol name or null, and
29
+ * `iconData` the data to draw as an image, or null.
30
+ */
31
+ function getTabIcons({ items, platform }) {
32
+ return items.map((item) => {
33
+ const sf =
34
+ platform === 'ios' ? getSfSymbol({ icon: item.icon, sfSymbol: item.sfSymbol }) : null;
35
+ return {
36
+ iconData: sf === null ? item.iconData ?? null : null,
37
+ pageId: item.pageId,
38
+ sf,
39
+ };
40
+ });
41
+ }
42
+
43
+ export default getTabIcons;
@@ -0,0 +1,46 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * Whether a Screen page scrolls under its navigation bar, as the native stack lays it out: on iOS
19
+ * 26 and later the bar is clear Liquid Glass over the page; before that, iOS makes the bar
20
+ * translucent over the page for a large title or a search bar, and a Screen with a backgroundTint
21
+ * has a clear bar over the page (getStackOptions). Otherwise (and on Android) the page
22
+ * sits below the bar. A page under the bar starts its content at the bar's bottom edge with
23
+ * automatic content insets, so a headerBottom pins under the bar at the header height; a page
24
+ * below the bar pins it at the top of its scroll view.
25
+ *
26
+ * @param {Object} params
27
+ * @param {Object} params.options The Screen's header options (client-native screen/Screen.js).
28
+ * @param {string} params.platform Platform.OS.
29
+ * @param {string|number} params.platformVersion Platform.Version: iOS gives a string, "26.0".
30
+ * @returns {boolean}
31
+ */
32
+ function isPageUnderHeader({ options, platform, platformVersion }) {
33
+ if (platform !== 'ios') {
34
+ return false;
35
+ }
36
+ if (Number.parseInt(`${platformVersion}`, 10) >= 26) {
37
+ return true;
38
+ }
39
+ return (
40
+ options.backgroundTint === true ||
41
+ options.largeTitle === true ||
42
+ options.headerSearch !== undefined
43
+ );
44
+ }
45
+
46
+ export default isPageUnderHeader;