@lowdefy/shell 0.0.0-experimental-20261019052246

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 (239) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +308 -0
  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 +30 -0
  7. package/app/_layout.js +33 -0
  8. package/app.config.ts +168 -0
  9. package/applyShellPlugins.js +43 -0
  10. package/authCallbackIntentFilters.js +46 -0
  11. package/babel.config.js +25 -0
  12. package/checkShellConfig.js +145 -0
  13. package/checkShellPlugins.js +74 -0
  14. package/getPrivacyManifest.js +39 -0
  15. package/metro.config.js +73 -0
  16. package/native-fingerprint.json +12 -0
  17. package/package.json +130 -0
  18. package/profiles.js +274 -0
  19. package/readShellConfig.js +36 -0
  20. package/resolveShellPlugins.js +109 -0
  21. package/runtimeVersion.js +105 -0
  22. package/scripts/checkReleaseBundle.mjs +358 -0
  23. package/scripts/createDayjsLocales.mjs +67 -0
  24. package/scripts/createShellAssets.mjs +54 -0
  25. package/scripts/createShellVersion.mjs +83 -0
  26. package/scripts/createTypeManifest.mjs +205 -0
  27. package/shell.dev.json +11 -0
  28. package/shellVariant.js +111 -0
  29. package/src/ShellContext.js +32 -0
  30. package/src/actions/CopyToClipboard.js +29 -0
  31. package/src/actions/CopyToClipboard.test.js +33 -0
  32. package/src/actions/GeolocationCurrentPosition.js +55 -0
  33. package/src/actions/GeolocationCurrentPosition.test.js +66 -0
  34. package/src/actions/GetLocalStorage.js +36 -0
  35. package/src/actions/RemoveLocalStorage.js +31 -0
  36. package/src/actions/ScrollTo.js +66 -0
  37. package/src/actions/ScrollTo.test.js +83 -0
  38. package/src/actions/SetFocus.js +33 -0
  39. package/src/actions/SetFocus.test.js +49 -0
  40. package/src/actions/SetLocalStorage.js +34 -0
  41. package/src/actions/localStorage.test.js +68 -0
  42. package/src/actions/measureInScrollView.js +36 -0
  43. package/src/actions/nativeActions.js +41 -0
  44. package/src/auth/AuthButton.js +44 -0
  45. package/src/auth/AuthGate.js +57 -0
  46. package/src/auth/AuthLink.js +43 -0
  47. package/src/auth/LockScreen.js +67 -0
  48. package/src/auth/LockScreen.test.js +37 -0
  49. package/src/auth/SignInScreen.js +256 -0
  50. package/src/auth/SignInScreen.test.js +182 -0
  51. package/src/auth/createAppAuth.js +306 -0
  52. package/src/auth/createAppAuth.test.js +414 -0
  53. package/src/auth/createAuthApi.js +99 -0
  54. package/src/auth/createAuthApi.test.js +135 -0
  55. package/src/auth/createCoreAuth.js +96 -0
  56. package/src/auth/createCoreAuth.test.js +188 -0
  57. package/src/auth/createSessionStores.js +45 -0
  58. package/src/auth/createTokenStore.js +103 -0
  59. package/src/auth/createTokenStore.test.js +116 -0
  60. package/src/auth/getAuthSettings.js +24 -0
  61. package/src/auth/getAuthView.js +39 -0
  62. package/src/auth/getAuthView.test.js +52 -0
  63. package/src/auth/getSheetReturnUrl.js +35 -0
  64. package/src/auth/getTokenStoreOptions.js +46 -0
  65. package/src/auth/isBackgroundShell.js +26 -0
  66. package/src/auth/isSheetReturnLink.js +35 -0
  67. package/src/auth/parseQuery.js +39 -0
  68. package/src/auth/purgeAppData.js +45 -0
  69. package/src/auth/shouldLock.js +26 -0
  70. package/src/auth/signInWithSheet.js +97 -0
  71. package/src/auth/signInWithSheet.test.js +205 -0
  72. package/src/auth/styles.js +78 -0
  73. package/src/auth/unregisterPushToken.js +21 -0
  74. package/src/auth/usesBiometricUnlock.js +23 -0
  75. package/src/auth/watchSession.js +47 -0
  76. package/src/auth/withoutCookies.js +26 -0
  77. package/src/cache/createSqliteCacheStore.js +113 -0
  78. package/src/cache/createSqliteCacheStore.test.js +93 -0
  79. package/src/cache/getCacheKey.js +25 -0
  80. package/src/cache/getLastCacheKeyName.js +22 -0
  81. package/src/components/AppProvider.js +90 -0
  82. package/src/components/Frame.js +116 -0
  83. package/src/components/Frame.test.js +101 -0
  84. package/src/components/LoadingScreen.js +35 -0
  85. package/src/components/MenuButton.js +108 -0
  86. package/src/components/MenuButton.test.js +51 -0
  87. package/src/components/MessageScreen.js +51 -0
  88. package/src/components/NavigationTheme.js +33 -0
  89. package/src/components/NoConnectionBanner.js +51 -0
  90. package/src/components/NoConnectionBanner.test.js +73 -0
  91. package/src/components/OtherPageScreen.js +56 -0
  92. package/src/components/PageScreen.js +244 -0
  93. package/src/components/PageScreen.test.js +492 -0
  94. package/src/components/ShellRoot.js +34 -0
  95. package/src/components/ShellRoot.test.js +62 -0
  96. package/src/components/ShellThemeProvider.js +45 -0
  97. package/src/components/SwitchToTab.js +36 -0
  98. package/src/components/SwitchToTab.test.js +63 -0
  99. package/src/components/TabIconImages.js +72 -0
  100. package/src/components/UpdateScreen.js +36 -0
  101. package/src/components/getNavigationTheme.js +44 -0
  102. package/src/components/getNavigationTheme.test.js +41 -0
  103. package/src/components/styles.js +50 -0
  104. package/src/components/useSurfaceTokens.js +47 -0
  105. package/src/config/getAppApiBase.js +31 -0
  106. package/src/config/getShellConfig.js +35 -0
  107. package/src/config/getShellConfig.test.js +80 -0
  108. package/src/createShellApp.js +105 -0
  109. package/src/dev/DevShell.js +228 -0
  110. package/src/dev/DevShell.test.js +233 -0
  111. package/src/dev/connect/ConfirmSheet.js +71 -0
  112. package/src/dev/connect/ConnectScreen.js +117 -0
  113. package/src/dev/connect/QrScanner.js +89 -0
  114. package/src/dev/connection/createDevShellConfig.js +47 -0
  115. package/src/dev/connection/createDevTransport.js +51 -0
  116. package/src/dev/connection/createDevTransport.test.js +87 -0
  117. package/src/dev/connection/createServerList.js +48 -0
  118. package/src/dev/connection/isLoopbackHost.js +25 -0
  119. package/src/dev/connection/pairedShellApp.test.js +104 -0
  120. package/src/dev/connection/parseDevLink.js +66 -0
  121. package/src/dev/connection/parseDevLink.test.js +97 -0
  122. package/src/dev/connection/parseQuery.js +33 -0
  123. package/src/dev/connection/parseServerInput.js +40 -0
  124. package/src/dev/connection/parseServerUrl.js +45 -0
  125. package/src/dev/devStyles.js +97 -0
  126. package/src/dev/index.js +21 -0
  127. package/src/dev/inject/createDeviceInjections.js +185 -0
  128. package/src/dev/inject/createDeviceInjections.test.js +208 -0
  129. package/src/dev/inject/createOfflineFetch.js +35 -0
  130. package/src/dev/inject/createOfflineFetch.test.js +68 -0
  131. package/src/dev/menu/BlockGallery.js +33 -0
  132. package/src/dev/menu/DevMenu.js +121 -0
  133. package/src/dev/menu/createLogBuffer.js +83 -0
  134. package/src/dev/menu/watchShake.js +52 -0
  135. package/src/dev/menu/watchShake.test.js +61 -0
  136. package/src/dev/recorder/createNativeRecorder.js +204 -0
  137. package/src/dev/recorder/createNativeRecorder.test.js +307 -0
  138. package/src/dev/redirectDevLink.js +30 -0
  139. package/src/dev/redirectDevLink.test.js +49 -0
  140. package/src/dev/steps/createStepRunner.js +184 -0
  141. package/src/dev/steps/createStepRunner.test.js +262 -0
  142. package/src/dev/steps/findBlock.js +78 -0
  143. package/src/dev/steps/isContextSettled.js +37 -0
  144. package/src/dev/steps/waitForSettle.js +39 -0
  145. package/src/dev/steps/waitUntil.js +40 -0
  146. package/src/dev/tab/DevTabClient.js +97 -0
  147. package/src/dev/tab/answerEval.js +39 -0
  148. package/src/dev/tab/answerInspect.js +45 -0
  149. package/src/dev/tab/answerLoadState.js +50 -0
  150. package/src/dev/tab/createDevTabClient.js +277 -0
  151. package/src/dev/tab/createDevTabClient.test.js +314 -0
  152. package/src/dev/tab/devClientMarker.js +22 -0
  153. package/src/dev/tab/devClientMarker.test.js +23 -0
  154. package/src/dev/tab/findPageContext.js +43 -0
  155. package/src/dev/tab/getShownPage.js +42 -0
  156. package/src/dev/tab/readEventStream.js +68 -0
  157. package/src/dev/tab/reloadApp.js +42 -0
  158. package/src/dev/tab/reloadApp.test.js +115 -0
  159. package/src/device/injected.js +60 -0
  160. package/src/device/injected.test.js +39 -0
  161. package/src/host/createDeviceStore.js +32 -0
  162. package/src/host/createMemoryStore.js +32 -0
  163. package/src/host/createNetworkStatus.js +94 -0
  164. package/src/host/createNetworkStatus.test.js +94 -0
  165. package/src/host/createRouterAdapter.js +33 -0
  166. package/src/host/createShellFetch.js +60 -0
  167. package/src/host/createShellFetch.test.js +150 -0
  168. package/src/host/createShellGlobals.js +62 -0
  169. package/src/host/createShellHost.js +73 -0
  170. package/src/host/createShellHost.test.js +36 -0
  171. package/src/host/getOrigin.js +26 -0
  172. package/src/host/isOnline.js +22 -0
  173. package/src/host/networkStatus.js +24 -0
  174. package/src/host/withShellHeader.js +25 -0
  175. package/src/host/withShellHeader.test.js +28 -0
  176. package/src/js/evaluateJsEntries.js +43 -0
  177. package/src/js/evaluateJsEntries.test.js +48 -0
  178. package/src/locale/createShellLocale.js +112 -0
  179. package/src/locale/createShellLocale.test.js +74 -0
  180. package/src/locale/dayjsLocales.js +165 -0
  181. package/src/locale/getDeviceLocales.js +23 -0
  182. package/src/locale/loadDayjsLocale.js +32 -0
  183. package/src/locale/resolveActiveLocale.js +46 -0
  184. package/src/locale/resolveActiveLocale.test.js +40 -0
  185. package/src/navigation/SlotContext.js +24 -0
  186. package/src/navigation/frameSlots.js +27 -0
  187. package/src/navigation/getFrame.js +30 -0
  188. package/src/navigation/getInitialScreenOptions.js +63 -0
  189. package/src/navigation/getInitialScreenOptions.test.js +66 -0
  190. package/src/navigation/getPresentation.js +28 -0
  191. package/src/navigation/getSlotRootPath.js +27 -0
  192. package/src/navigation/getStackOptions.js +185 -0
  193. package/src/navigation/getTabHref.js +45 -0
  194. package/src/navigation/getTabIcons.js +43 -0
  195. package/src/navigation/isPageUnderHeader.js +46 -0
  196. package/src/navigation/navigation.test.js +342 -0
  197. package/src/navigation/toRouterHref.js +29 -0
  198. package/src/plugins/appTypes.js +28 -0
  199. package/src/plugins/prefixTypes.js +28 -0
  200. package/src/plugins/shellPlugins.json +14 -0
  201. package/src/plugins/typeManifest.js +649 -0
  202. package/src/plugins/types.js +61 -0
  203. package/src/plugins/types.test.js +173 -0
  204. package/src/polyfills/installGetRandomValues.js +28 -0
  205. package/src/session/createAppSession.js +318 -0
  206. package/src/session/createAppSession.test.js +359 -0
  207. package/src/session/getPageView.js +59 -0
  208. package/src/session/getPageView.test.js +137 -0
  209. package/src/sync/encodePagePath.js +27 -0
  210. package/src/sync/fetchPage.js +49 -0
  211. package/src/sync/fetchRoot.js +38 -0
  212. package/src/sync/fetchUser.js +28 -0
  213. package/src/sync/runLimited.js +36 -0
  214. package/src/sync/toRootConfig.js +35 -0
  215. package/src/test/createFakeNetwork.js +40 -0
  216. package/src/test/createFakeSecureStore.js +40 -0
  217. package/src/test/createFakeServer.js +98 -0
  218. package/src/test/createMemoryCacheStore.js +59 -0
  219. package/src/test/expoImage.js +38 -0
  220. package/src/test/expoSpeechRecognition.js +31 -0
  221. package/src/test/expoUi.js +55 -0
  222. package/src/test/expoUiBottomSheet.js +64 -0
  223. package/src/test/expoUiDateTimePicker.js +26 -0
  224. package/src/test/expoUiMenu.js +46 -0
  225. package/src/test/expoUiSegmentedControl.js +47 -0
  226. package/src/test/expoUiSwiftUi.js +62 -0
  227. package/src/test/expoUiSwiftUiModifiers.js +23 -0
  228. package/src/test/expoWebBrowser.js +23 -0
  229. package/src/test/fixtures.js +98 -0
  230. package/src/test/renderPageScreen.js +109 -0
  231. package/src/types/checkTypeSet.js +39 -0
  232. package/src/types/collectPageTypes.js +49 -0
  233. package/src/types/collectPageTypes.test.js +90 -0
  234. package/src/types/findMissingPageTypes.js +40 -0
  235. package/src/version/compareVersions.js +38 -0
  236. package/src/version/isShellTooOld.js +30 -0
  237. package/src/version/shellVersion.js +21 -0
  238. package/src/version/shellVersion.test.js +32 -0
  239. package/withExcludedNativeModules.js +93 -0
@@ -0,0 +1,342 @@
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 { jest } from '@jest/globals';
18
+
19
+ import { getSlotIndex } from './frameSlots.js';
20
+ import getFrame from './getFrame.js';
21
+ import getPresentation from './getPresentation.js';
22
+ import getSlotRootPath from './getSlotRootPath.js';
23
+ import getStackOptions from './getStackOptions.js';
24
+ import getTabHref from './getTabHref.js';
25
+ import getTabIcons from './getTabIcons.js';
26
+ import isPageUnderHeader from './isPageUnderHeader.js';
27
+ import toRouterHref from './toRouterHref.js';
28
+ import { createRoot } from '../test/fixtures.js';
29
+
30
+ const tabsRoot = createRoot();
31
+
32
+ test('getFrame reads tabs, drawer and stack frames', () => {
33
+ expect(getFrame(tabsRoot.menus).kind).toBe('tabs');
34
+ expect(getFrame({ drawer: [{ pageId: 'today' }] })).toEqual({
35
+ kind: 'drawer',
36
+ items: [{ pageId: 'today' }],
37
+ });
38
+ expect(getFrame({ stack: true })).toEqual({ kind: 'stack', items: [] });
39
+ });
40
+
41
+ test('getFrame falls back to a stack when the caller may open fewer than two tabs', () => {
42
+ expect(getFrame({ tabs: [{ pageId: 'today' }] }).kind).toBe('stack');
43
+ expect(getFrame({ drawer: [] }).kind).toBe('stack');
44
+ });
45
+
46
+ test('getSlotIndex reads the frame group of a route segment', () => {
47
+ expect(getSlotIndex('(t0)')).toBe(0);
48
+ expect(getSlotIndex('(t3)')).toBe(3);
49
+ expect(getSlotIndex('jobs')).toBe(-1);
50
+ });
51
+
52
+ test('getSlotRootPath gives each tab its page and a stack the home page', () => {
53
+ expect(getSlotRootPath({ root: tabsRoot, slotIndex: 0 })).toBe('today');
54
+ expect(getSlotRootPath({ root: tabsRoot, slotIndex: 1 })).toBe('jobs');
55
+ expect(getSlotRootPath({ root: tabsRoot, slotIndex: 4 })).toBeNull();
56
+ const stackRoot = createRoot({ menus: { stack: true }, homePageId: 'jobs' });
57
+ expect(getSlotRootPath({ root: stackRoot, slotIndex: 0 })).toBe('jobs');
58
+ });
59
+
60
+ test('toRouterHref switches tabs for a link to a tab page and keeps the query', () => {
61
+ expect(toRouterHref({ pathname: '/jobs', query: 'a=1', root: tabsRoot })).toBe('/(t1)?a=1');
62
+ expect(toRouterHref({ pathname: '/jobs/12', query: '', root: tabsRoot })).toBe('/jobs/12');
63
+ const stackRoot = createRoot({ menus: { stack: true } });
64
+ expect(toRouterHref({ pathname: '/jobs', query: 'a=1', root: stackRoot })).toBe('/jobs?a=1');
65
+ });
66
+
67
+ test('getTabHref names the tab that shows a page, and no tab for other pages and frames', () => {
68
+ expect(getTabHref({ path: 'today', query: '', root: tabsRoot })).toBe('/(t0)');
69
+ expect(getTabHref({ path: 'jobs', query: 'a=1', root: tabsRoot })).toBe('/(t1)?a=1');
70
+ expect(getTabHref({ path: 'jobs/12', query: '', root: tabsRoot })).toBeNull();
71
+ const stackRoot = createRoot({ menus: { stack: true } });
72
+ expect(getTabHref({ path: 'jobs', query: '', root: stackRoot })).toBeNull();
73
+ });
74
+
75
+ test('getPresentation maps Screen presentations to native stack presentations', () => {
76
+ expect(getPresentation('card')).toBe('card');
77
+ expect(getPresentation('modal')).toBe('modal');
78
+ expect(getPresentation('sheet')).toBe('formSheet');
79
+ expect(getPresentation(undefined)).toBe('card');
80
+ });
81
+
82
+ function headerAction(overrides) {
83
+ return {
84
+ disabled: false,
85
+ id: 'add',
86
+ menu: null,
87
+ onPress: jest.fn(),
88
+ placement: 'right',
89
+ render: () => 'icon button',
90
+ sfSymbol: 'plus',
91
+ title: 'Add',
92
+ ...overrides,
93
+ };
94
+ }
95
+
96
+ const screenOptions = {
97
+ headerActions: [],
98
+ headerLeft: undefined,
99
+ headerRight: undefined,
100
+ headerSearch: undefined,
101
+ largeTitle: false,
102
+ presentation: 'card',
103
+ title: 'Designs',
104
+ };
105
+
106
+ test('getStackOptions gives a large title, the presentation and a native search bar', () => {
107
+ const headerSearch = {
108
+ autoFocus: true,
109
+ hideWhenScrolling: false,
110
+ onCancel: jest.fn(),
111
+ onChangeText: jest.fn(),
112
+ onSearch: jest.fn(),
113
+ placeholder: 'Search designs',
114
+ };
115
+ const searchBarRef = { current: null };
116
+ const options = getStackOptions({
117
+ options: { ...screenOptions, headerSearch, largeTitle: true, presentation: 'sheet' },
118
+ platform: 'ios',
119
+ searchBarRef,
120
+ });
121
+ expect(options).toMatchObject({
122
+ headerLargeTitleEnabled: true,
123
+ presentation: 'formSheet',
124
+ title: 'Designs',
125
+ });
126
+ // No header items: the sides are left as the stack has them (a drawer's menu button).
127
+ expect(options).not.toHaveProperty('headerLeft');
128
+ expect(options).not.toHaveProperty('headerRight');
129
+ const search = options.headerSearchBarOptions;
130
+ expect(search).toMatchObject({ placeholder: 'Search designs', ref: searchBarRef });
131
+ search.onChangeText({ nativeEvent: { text: 'dar' } });
132
+ search.onSearchButtonPress({ nativeEvent: { text: 'dark' } });
133
+ search.onCancelButtonPress({});
134
+ expect(headerSearch.onChangeText).toHaveBeenCalledWith('dar');
135
+ expect(headerSearch.onSearch).toHaveBeenCalledWith('dark');
136
+ expect(headerSearch.onCancel).toHaveBeenCalled();
137
+ });
138
+
139
+ test('getStackOptions draws header actions as native SF Symbol bar buttons on iOS', () => {
140
+ const slot = () => 'slot blocks';
141
+ const add = headerAction();
142
+ const squirrel = headerAction({ id: 'squirrel', placement: 'left', sfSymbol: null });
143
+ const options = getStackOptions({
144
+ options: { ...screenOptions, headerActions: [add, squirrel], headerRight: slot },
145
+ platform: 'ios',
146
+ searchBarRef: { current: null },
147
+ });
148
+ expect(options.headerRight).toBeUndefined();
149
+ expect(options.unstable_headerRightItems()).toEqual([
150
+ { type: 'custom', element: 'slot blocks' },
151
+ {
152
+ type: 'button',
153
+ accessibilityLabel: 'Add',
154
+ disabled: false,
155
+ icon: { type: 'sfSymbol', name: 'plus' },
156
+ identifier: 'add',
157
+ label: 'Add',
158
+ onPress: add.onPress,
159
+ },
160
+ ]);
161
+ // An icon with no SF Symbol draws as its own icon button.
162
+ expect(options.unstable_headerLeftItems()).toEqual([{ type: 'custom', element: 'icon button' }]);
163
+ });
164
+
165
+ test('getStackOptions draws a header action with items as a native menu on iOS', () => {
166
+ const copy = { destructive: false, disabled: false, id: 'copy_link', onPress: jest.fn() };
167
+ const remove = { destructive: true, disabled: false, id: 'delete', onPress: jest.fn() };
168
+ const more = headerAction({
169
+ id: 'more',
170
+ menu: [
171
+ { ...copy, symbol: 'link', title: 'Copy link' },
172
+ { ...remove, symbol: null, title: 'Delete' },
173
+ ],
174
+ sfSymbol: 'ellipsis',
175
+ title: 'More',
176
+ });
177
+ const options = getStackOptions({
178
+ options: { ...screenOptions, headerActions: [more] },
179
+ platform: 'ios',
180
+ searchBarRef: { current: null },
181
+ });
182
+ expect(options.unstable_headerRightItems()).toEqual([
183
+ {
184
+ type: 'menu',
185
+ accessibilityLabel: 'More',
186
+ disabled: false,
187
+ icon: { type: 'sfSymbol', name: 'ellipsis' },
188
+ identifier: 'more',
189
+ label: 'More',
190
+ menu: {
191
+ items: [
192
+ {
193
+ type: 'action',
194
+ destructive: false,
195
+ disabled: false,
196
+ icon: { type: 'sfSymbol', name: 'link' },
197
+ label: 'Copy link',
198
+ onPress: copy.onPress,
199
+ },
200
+ {
201
+ type: 'action',
202
+ destructive: true,
203
+ disabled: false,
204
+ icon: undefined,
205
+ label: 'Delete',
206
+ onPress: remove.onPress,
207
+ },
208
+ ],
209
+ },
210
+ },
211
+ ]);
212
+ // An action with a menu stays a native menu when its icon has no SF Symbol.
213
+ const plain = getStackOptions({
214
+ options: { ...screenOptions, headerActions: [{ ...more, sfSymbol: null }] },
215
+ platform: 'ios',
216
+ searchBarRef: { current: null },
217
+ });
218
+ expect(plain.unstable_headerRightItems()[0]).toMatchObject({ type: 'menu', icon: undefined });
219
+ });
220
+
221
+ test('getStackOptions draws a header slot alone as its custom view on iOS', () => {
222
+ const slot = () => 'slot blocks';
223
+ const options = getStackOptions({
224
+ options: { ...screenOptions, headerRight: slot },
225
+ platform: 'ios',
226
+ searchBarRef: { current: null },
227
+ });
228
+ expect(options.headerRight).toBe(slot);
229
+ expect(options.unstable_headerRightItems).toBeUndefined();
230
+ });
231
+
232
+ test('getStackOptions hides the bar hairline for a headerBottom, which draws its own', () => {
233
+ const options = getStackOptions({
234
+ options: { ...screenOptions, headerBottom: true },
235
+ platform: 'ios',
236
+ searchBarRef: { current: null },
237
+ });
238
+ expect(options.headerShadowVisible).toBe(false);
239
+ // Without one, the stack keeps its own default.
240
+ expect(
241
+ getStackOptions({ options: screenOptions, platform: 'ios', searchBarRef: { current: null } })
242
+ .headerShadowVisible
243
+ ).toBeUndefined();
244
+ });
245
+
246
+ test('getStackOptions makes the iOS bar clear over a Screen with a backgroundTint', () => {
247
+ const tinted = { ...screenOptions, backgroundTint: true, largeTitle: true };
248
+ const searchBarRef = { current: null };
249
+ expect(
250
+ getStackOptions({ options: tinted, platform: 'ios', platformVersion: '26.0', searchBarRef })
251
+ ).toMatchObject({ headerBlurEffect: undefined, headerTransparent: true });
252
+ // Before iOS 26: clear at the scroll edge, the chrome material once content scrolls under it.
253
+ expect(
254
+ getStackOptions({ options: tinted, platform: 'ios', platformVersion: '18.4', searchBarRef })
255
+ ).toMatchObject({
256
+ headerBlurEffect: 'systemChromeMaterial',
257
+ headerLargeStyle: { backgroundColor: 'transparent' },
258
+ headerTransparent: true,
259
+ });
260
+ // Untinted, and off iOS, the stack's own bar: set undefined, as setOptions merges.
261
+ const untinted = getStackOptions({
262
+ options: screenOptions,
263
+ platform: 'ios',
264
+ platformVersion: '26.0',
265
+ searchBarRef,
266
+ });
267
+ expect(untinted).toHaveProperty('headerTransparent', undefined);
268
+ expect(
269
+ getStackOptions({ options: tinted, platform: 'android', platformVersion: 35, searchBarRef })
270
+ .headerTransparent
271
+ ).toBeUndefined();
272
+ });
273
+
274
+ test('isPageUnderHeader: a Screen with a backgroundTint scrolls under its clear bar on iOS', () => {
275
+ expect(
276
+ isPageUnderHeader({
277
+ options: { backgroundTint: true },
278
+ platform: 'ios',
279
+ platformVersion: '18.4',
280
+ })
281
+ ).toBe(true);
282
+ expect(
283
+ isPageUnderHeader({
284
+ options: { backgroundTint: true },
285
+ platform: 'android',
286
+ platformVersion: 35,
287
+ })
288
+ ).toBe(false);
289
+ });
290
+
291
+ test('isPageUnderHeader: iOS 26 pages scroll under the glass bar', () => {
292
+ expect(isPageUnderHeader({ options: {}, platform: 'ios', platformVersion: '26.0' })).toBe(true);
293
+ expect(isPageUnderHeader({ options: {}, platform: 'ios', platformVersion: '27.1' })).toBe(true);
294
+ });
295
+
296
+ test('isPageUnderHeader: before iOS 26 only a large title or search bar makes the bar translucent', () => {
297
+ const ios18 = { platform: 'ios', platformVersion: '18.4' };
298
+ expect(isPageUnderHeader({ ...ios18, options: { largeTitle: false } })).toBe(false);
299
+ expect(isPageUnderHeader({ ...ios18, options: { largeTitle: true } })).toBe(true);
300
+ expect(isPageUnderHeader({ ...ios18, options: { headerSearch: { placeholder: 'Find' } } })).toBe(
301
+ true
302
+ );
303
+ });
304
+
305
+ test('isPageUnderHeader: Android pages sit below the header', () => {
306
+ expect(
307
+ isPageUnderHeader({ options: { largeTitle: true }, platform: 'android', platformVersion: 35 })
308
+ ).toBe(false);
309
+ });
310
+
311
+ test('getStackOptions draws header actions as a row of icon buttons off iOS', () => {
312
+ const options = getStackOptions({
313
+ options: { ...screenOptions, headerActions: [headerAction()] },
314
+ platform: 'android',
315
+ searchBarRef: { current: null },
316
+ });
317
+ expect(options).not.toHaveProperty('unstable_headerRightItems');
318
+ const row = options.headerRight();
319
+ expect(row.props.children).toEqual([null, ['icon button']]);
320
+ });
321
+
322
+ test('getTabIcons gives SF Symbols on iOS and Lucide icon data elsewhere', () => {
323
+ const inbox = { node: [['path', { d: 'M1 1' }]] };
324
+ const squirrel = { node: [['path', { d: 'M2 2' }]] };
325
+ const items = [
326
+ { pageId: 'my-work', icon: 'Inbox', iconData: inbox },
327
+ { pageId: 'designs', icon: 'FileText', iconData: {}, sfSymbol: 'doc.richtext' },
328
+ { pageId: 'pets', icon: 'Squirrel', iconData: squirrel },
329
+ { pageId: 'plain' },
330
+ ];
331
+ expect(getTabIcons({ items, platform: 'ios' })).toEqual([
332
+ { pageId: 'my-work', sf: 'tray', iconData: null },
333
+ { pageId: 'designs', sf: 'doc.richtext', iconData: null },
334
+ { pageId: 'pets', sf: null, iconData: squirrel },
335
+ { pageId: 'plain', sf: null, iconData: null },
336
+ ]);
337
+ expect(getTabIcons({ items, platform: 'android' })[0]).toEqual({
338
+ pageId: 'my-work',
339
+ sf: null,
340
+ iconData: inbox,
341
+ });
342
+ });
@@ -0,0 +1,29 @@
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 getTabHref from './getTabHref.js';
18
+
19
+ // The router href for a link the core follows ({ pathname, query }). A link to a tab's own page
20
+ // switches to that tab, rather than stacking another copy of the page on the current one.
21
+ function toRouterHref({ pathname, query, root }) {
22
+ const tabHref = getTabHref({ path: pathname.replace(/^\//, ''), query, root });
23
+ if (tabHref !== null) {
24
+ return tabHref;
25
+ }
26
+ return `${pathname}${query ? `?${query}` : ''}`;
27
+ }
28
+
29
+ export default toRouterHref;
@@ -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 types of the app's shell plugins (shell.json `plugins`), which src/plugins/types.js
18
+ // registers after the built-in types. The shell package ships it empty; `lowdefy mobile config`
19
+ // rewrites it in the copied shell to import each plugin's `./native`, with its typePrefix applied:
20
+ //
21
+ // import * as plugin0 from '@example/lowdefy-printer/native';
22
+ // import prefixTypes from './prefixTypes.js';
23
+ //
24
+ // export default {
25
+ // actions: { ...prefixTypes({ types: plugin0.actions, typePrefix: 'Printer' }) },
26
+ // blocks: { ...prefixTypes({ types: plugin0.blocks, typePrefix: 'Printer' }) },
27
+ // };
28
+ export default { actions: {}, blocks: {} };
@@ -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
+ // A shell plugin's native types with its typePrefix (shell.json `plugins[].typePrefix`) on each
18
+ // name, as the type manifest names them (scripts/createTypeManifest.mjs).
19
+ function prefixTypes({ types, typePrefix }) {
20
+ return Object.fromEntries(
21
+ Object.entries(types ?? {}).map(([name, implementation]) => [
22
+ `${typePrefix ?? ''}${name}`,
23
+ implementation,
24
+ ])
25
+ );
26
+ }
27
+
28
+ export default prefixTypes;
@@ -0,0 +1,14 @@
1
+ [
2
+ { "category": "actions", "package": "@lowdefy/actions-core" },
3
+ { "category": "blocks", "package": "@lowdefy/blocks-native" },
4
+ { "category": "operators", "package": "@lowdefy/operators-js" },
5
+ { "category": "operators", "package": "@lowdefy/operators-change-case" },
6
+ { "category": "operators", "package": "@lowdefy/operators-cron" },
7
+ { "category": "operators", "package": "@lowdefy/operators-dayjs" },
8
+ { "category": "operators", "package": "@lowdefy/operators-diff" },
9
+ { "category": "operators", "package": "@lowdefy/operators-jsonata" },
10
+ { "category": "operators", "package": "@lowdefy/operators-mql" },
11
+ { "category": "operators", "package": "@lowdefy/operators-nunjucks" },
12
+ { "category": "operators", "package": "@lowdefy/operators-uuid" },
13
+ { "category": "operators", "package": "@lowdefy/operators-yaml" }
14
+ ]