@studio-piot/native-ui 0.2.5 → 0.16.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 (338) hide show
  1. package/README.md +22 -18
  2. package/dist/components/Button/Button.d.ts +20 -4
  3. package/dist/components/Button/Button.d.ts.map +1 -1
  4. package/dist/components/Button/Button.js +34 -21
  5. package/dist/components/Button/Button.js.map +1 -1
  6. package/dist/components/Button/buttonAppearance.d.ts +20 -0
  7. package/dist/components/Button/buttonAppearance.d.ts.map +1 -0
  8. package/dist/components/Button/buttonAppearance.js +28 -0
  9. package/dist/components/Button/buttonAppearance.js.map +1 -0
  10. package/dist/components/Button/buttonThemeRoles.d.ts +6 -0
  11. package/dist/components/Button/buttonThemeRoles.d.ts.map +1 -0
  12. package/dist/components/Button/buttonThemeRoles.js +8 -0
  13. package/dist/components/Button/buttonThemeRoles.js.map +1 -0
  14. package/dist/components/Button/index.d.ts +3 -1
  15. package/dist/components/Button/index.d.ts.map +1 -1
  16. package/dist/components/Button/index.js +4 -1
  17. package/dist/components/Button/index.js.map +1 -1
  18. package/dist/components/Choice/Choice.d.ts +8 -0
  19. package/dist/components/Choice/Choice.d.ts.map +1 -0
  20. package/dist/components/Choice/Choice.js +60 -0
  21. package/dist/components/Choice/Choice.js.map +1 -0
  22. package/dist/components/Choice/choiceLayout.d.ts +7 -0
  23. package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
  24. package/dist/components/Choice/choiceLayout.js +13 -0
  25. package/dist/components/Choice/choiceLayout.js.map +1 -0
  26. package/dist/components/Choice/choiceThemeRoles.d.ts +8 -0
  27. package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
  28. package/dist/components/Choice/choiceThemeRoles.js +10 -0
  29. package/dist/components/Choice/choiceThemeRoles.js.map +1 -0
  30. package/dist/components/Choice/index.d.ts +5 -0
  31. package/dist/components/Choice/index.d.ts.map +1 -0
  32. package/dist/components/Choice/index.js +10 -0
  33. package/dist/components/Choice/index.js.map +1 -0
  34. package/dist/components/Choice/types.d.ts +26 -0
  35. package/dist/components/Choice/types.d.ts.map +1 -0
  36. package/dist/components/Choice/types.js +3 -0
  37. package/dist/components/Choice/types.js.map +1 -0
  38. package/dist/components/ErrorBanner/ErrorBanner.d.ts +1 -1
  39. package/dist/components/ErrorBanner/ErrorBanner.d.ts.map +1 -1
  40. package/dist/components/ErrorBanner/ErrorBanner.js +19 -19
  41. package/dist/components/ErrorBanner/ErrorBanner.js.map +1 -1
  42. package/dist/components/ErrorBanner/errorBannerContext.d.ts +23 -3
  43. package/dist/components/ErrorBanner/errorBannerContext.d.ts.map +1 -1
  44. package/dist/components/ErrorBanner/errorBannerContext.js +9 -5
  45. package/dist/components/ErrorBanner/errorBannerContext.js.map +1 -1
  46. package/dist/components/ErrorBanner/errorBannerLayout.d.ts +18 -0
  47. package/dist/components/ErrorBanner/errorBannerLayout.d.ts.map +1 -0
  48. package/dist/components/ErrorBanner/errorBannerLayout.js +54 -0
  49. package/dist/components/ErrorBanner/errorBannerLayout.js.map +1 -0
  50. package/dist/components/ErrorBanner/icons.d.ts +7 -5
  51. package/dist/components/ErrorBanner/icons.d.ts.map +1 -1
  52. package/dist/components/ErrorBanner/icons.js +8 -4
  53. package/dist/components/ErrorBanner/icons.js.map +1 -1
  54. package/dist/components/ErrorBanner/index.d.ts +1 -1
  55. package/dist/components/ErrorBanner/index.d.ts.map +1 -1
  56. package/dist/components/ErrorBanner/index.js.map +1 -1
  57. package/dist/components/Form/Form.d.ts +9 -1
  58. package/dist/components/Form/Form.d.ts.map +1 -1
  59. package/dist/components/Form/Form.js +56 -78
  60. package/dist/components/Form/Form.js.map +1 -1
  61. package/dist/components/Form/FormCloseButton.d.ts +3 -1
  62. package/dist/components/Form/FormCloseButton.d.ts.map +1 -1
  63. package/dist/components/Form/FormCloseButton.js +5 -4
  64. package/dist/components/Form/FormCloseButton.js.map +1 -1
  65. package/dist/components/Form/FormErrorBanner.d.ts +15 -0
  66. package/dist/components/Form/FormErrorBanner.d.ts.map +1 -0
  67. package/dist/components/Form/FormErrorBanner.js +30 -0
  68. package/dist/components/Form/FormErrorBanner.js.map +1 -0
  69. package/dist/components/Form/FormSubmitFooter.d.ts +17 -0
  70. package/dist/components/Form/FormSubmitFooter.d.ts.map +1 -0
  71. package/dist/components/Form/FormSubmitFooter.js +32 -0
  72. package/dist/components/Form/FormSubmitFooter.js.map +1 -0
  73. package/dist/components/Form/formThemeRoles.d.ts +7 -0
  74. package/dist/components/Form/formThemeRoles.d.ts.map +1 -0
  75. package/dist/components/Form/formThemeRoles.js +9 -0
  76. package/dist/components/Form/formThemeRoles.js.map +1 -0
  77. package/dist/components/Form/index.d.ts +1 -0
  78. package/dist/components/Form/index.d.ts.map +1 -1
  79. package/dist/components/Form/index.js +3 -1
  80. package/dist/components/Form/index.js.map +1 -1
  81. package/dist/components/Form/types.d.ts +14 -10
  82. package/dist/components/Form/types.d.ts.map +1 -1
  83. package/dist/components/FormSheet/FormSheet.d.ts +10 -0
  84. package/dist/components/FormSheet/FormSheet.d.ts.map +1 -0
  85. package/dist/components/FormSheet/FormSheet.js +153 -0
  86. package/dist/components/FormSheet/FormSheet.js.map +1 -0
  87. package/dist/components/FormSheet/formSheetLayout.d.ts +32 -0
  88. package/dist/components/FormSheet/formSheetLayout.d.ts.map +1 -0
  89. package/dist/components/FormSheet/formSheetLayout.js +43 -0
  90. package/dist/components/FormSheet/formSheetLayout.js.map +1 -0
  91. package/dist/components/FormSheet/formSheetThemeRoles.d.ts +8 -0
  92. package/dist/components/FormSheet/formSheetThemeRoles.d.ts.map +1 -0
  93. package/dist/components/FormSheet/formSheetThemeRoles.js +10 -0
  94. package/dist/components/FormSheet/formSheetThemeRoles.js.map +1 -0
  95. package/dist/components/FormSheet/index.d.ts +5 -0
  96. package/dist/components/FormSheet/index.d.ts.map +1 -0
  97. package/dist/components/FormSheet/index.js +10 -0
  98. package/dist/components/FormSheet/index.js.map +1 -0
  99. package/dist/components/FormSheet/types.d.ts +64 -0
  100. package/dist/components/FormSheet/types.d.ts.map +1 -0
  101. package/dist/components/FormSheet/types.js +3 -0
  102. package/dist/components/FormSheet/types.js.map +1 -0
  103. package/dist/components/HeaderButton/HeaderCapsule.d.ts +4 -0
  104. package/dist/components/HeaderButton/HeaderCapsule.d.ts.map +1 -0
  105. package/dist/components/HeaderButton/HeaderCapsule.js +32 -0
  106. package/dist/components/HeaderButton/HeaderCapsule.js.map +1 -0
  107. package/dist/components/HeaderButton/HeaderGroup.d.ts +4 -0
  108. package/dist/components/HeaderButton/HeaderGroup.d.ts.map +1 -0
  109. package/dist/components/HeaderButton/HeaderGroup.js +23 -0
  110. package/dist/components/HeaderButton/HeaderGroup.js.map +1 -0
  111. package/dist/components/HeaderButton/HeaderIcon.d.ts +4 -0
  112. package/dist/components/HeaderButton/HeaderIcon.d.ts.map +1 -0
  113. package/dist/components/HeaderButton/HeaderIcon.js +28 -0
  114. package/dist/components/HeaderButton/HeaderIcon.js.map +1 -0
  115. package/dist/components/HeaderButton/headerButtonLayout.d.ts +21 -0
  116. package/dist/components/HeaderButton/headerButtonLayout.d.ts.map +1 -0
  117. package/dist/components/HeaderButton/headerButtonLayout.js +34 -0
  118. package/dist/components/HeaderButton/headerButtonLayout.js.map +1 -0
  119. package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts +5 -0
  120. package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts.map +1 -0
  121. package/dist/components/HeaderButton/headerButtonThemeRoles.js +7 -0
  122. package/dist/components/HeaderButton/headerButtonThemeRoles.js.map +1 -0
  123. package/dist/components/HeaderButton/index.d.ts +7 -0
  124. package/dist/components/HeaderButton/index.d.ts.map +1 -0
  125. package/dist/components/HeaderButton/index.js +14 -0
  126. package/dist/components/HeaderButton/index.js.map +1 -0
  127. package/dist/components/HeaderButton/types.d.ts +40 -0
  128. package/dist/components/HeaderButton/types.d.ts.map +1 -0
  129. package/dist/components/HeaderButton/types.js +3 -0
  130. package/dist/components/HeaderButton/types.js.map +1 -0
  131. package/dist/components/HeaderWash/HeaderWash.d.ts +10 -0
  132. package/dist/components/HeaderWash/HeaderWash.d.ts.map +1 -0
  133. package/dist/components/HeaderWash/HeaderWash.js +19 -0
  134. package/dist/components/HeaderWash/HeaderWash.js.map +1 -0
  135. package/dist/components/HeaderWash/headerWashGradient.d.ts +8 -0
  136. package/dist/components/HeaderWash/headerWashGradient.d.ts.map +1 -0
  137. package/dist/components/HeaderWash/headerWashGradient.js +21 -0
  138. package/dist/components/HeaderWash/headerWashGradient.js.map +1 -0
  139. package/dist/components/HeaderWash/index.d.ts +2 -0
  140. package/dist/components/HeaderWash/index.d.ts.map +1 -0
  141. package/dist/components/HeaderWash/index.js +6 -0
  142. package/dist/components/HeaderWash/index.js.map +1 -0
  143. package/dist/components/Page/Page.d.ts +13 -0
  144. package/dist/components/Page/Page.d.ts.map +1 -0
  145. package/dist/components/Page/Page.js +49 -0
  146. package/dist/components/Page/Page.js.map +1 -0
  147. package/dist/components/Page/PageBleed.d.ts +9 -0
  148. package/dist/components/Page/PageBleed.d.ts.map +1 -0
  149. package/dist/components/Page/PageBleed.js +12 -0
  150. package/dist/components/Page/PageBleed.js.map +1 -0
  151. package/dist/components/Page/index.d.ts +5 -0
  152. package/dist/components/Page/index.d.ts.map +1 -0
  153. package/dist/components/Page/index.js +10 -0
  154. package/dist/components/Page/index.js.map +1 -0
  155. package/dist/components/Page/pageContext.d.ts +4 -0
  156. package/dist/components/Page/pageContext.d.ts.map +1 -0
  157. package/dist/components/Page/pageContext.js +11 -0
  158. package/dist/components/Page/pageContext.js.map +1 -0
  159. package/dist/components/Page/pageInsets.d.ts +26 -0
  160. package/dist/components/Page/pageInsets.d.ts.map +1 -0
  161. package/dist/components/Page/pageInsets.js +37 -0
  162. package/dist/components/Page/pageInsets.js.map +1 -0
  163. package/dist/components/Page/pageLayout.d.ts +5 -0
  164. package/dist/components/Page/pageLayout.d.ts.map +1 -0
  165. package/dist/components/Page/pageLayout.js +8 -0
  166. package/dist/components/Page/pageLayout.js.map +1 -0
  167. package/dist/components/Page/pageThemeRoles.d.ts +5 -0
  168. package/dist/components/Page/pageThemeRoles.d.ts.map +1 -0
  169. package/dist/components/Page/pageThemeRoles.js +7 -0
  170. package/dist/components/Page/pageThemeRoles.js.map +1 -0
  171. package/dist/components/Page/types.d.ts +22 -0
  172. package/dist/components/Page/types.d.ts.map +1 -0
  173. package/dist/components/Page/types.js +3 -0
  174. package/dist/components/Page/types.js.map +1 -0
  175. package/dist/components/Select/Select.d.ts +1 -1
  176. package/dist/components/Select/Select.d.ts.map +1 -1
  177. package/dist/components/Select/Select.js +8 -9
  178. package/dist/components/Select/Select.js.map +1 -1
  179. package/dist/components/Select/selectThemeRoles.d.ts +2 -0
  180. package/dist/components/Select/selectThemeRoles.d.ts.map +1 -1
  181. package/dist/components/Select/selectThemeRoles.js +2 -0
  182. package/dist/components/Select/selectThemeRoles.js.map +1 -1
  183. package/dist/components/Select/types.d.ts +4 -0
  184. package/dist/components/Select/types.d.ts.map +1 -1
  185. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts +8 -0
  186. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts.map +1 -0
  187. package/dist/components/TabAccessoryButton/TabAccessoryButton.js +77 -0
  188. package/dist/components/TabAccessoryButton/TabAccessoryButton.js.map +1 -0
  189. package/dist/components/TabAccessoryButton/index.d.ts +5 -0
  190. package/dist/components/TabAccessoryButton/index.d.ts.map +1 -0
  191. package/dist/components/TabAccessoryButton/index.js +11 -0
  192. package/dist/components/TabAccessoryButton/index.js.map +1 -0
  193. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts +27 -0
  194. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts.map +1 -0
  195. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js +38 -0
  196. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js.map +1 -0
  197. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts +14 -0
  198. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts.map +1 -0
  199. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js +19 -0
  200. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js.map +1 -0
  201. package/dist/components/TabAccessoryButton/types.d.ts +27 -0
  202. package/dist/components/TabAccessoryButton/types.d.ts.map +1 -0
  203. package/dist/components/TabAccessoryButton/types.js +3 -0
  204. package/dist/components/TabAccessoryButton/types.js.map +1 -0
  205. package/dist/components/index.d.ts +9 -3
  206. package/dist/components/index.d.ts.map +1 -1
  207. package/dist/components/index.js +30 -1
  208. package/dist/components/index.js.map +1 -1
  209. package/dist/index.d.ts +11 -4
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +34 -2
  212. package/dist/index.js.map +1 -1
  213. package/dist/layout/HeaderInsetProbe.d.ts +15 -0
  214. package/dist/layout/HeaderInsetProbe.d.ts.map +1 -0
  215. package/dist/layout/HeaderInsetProbe.js +28 -0
  216. package/dist/layout/HeaderInsetProbe.js.map +1 -0
  217. package/dist/layout/headerInset.d.ts +26 -0
  218. package/dist/layout/headerInset.d.ts.map +1 -0
  219. package/dist/layout/headerInset.js +29 -0
  220. package/dist/layout/headerInset.js.map +1 -0
  221. package/dist/layout/index.d.ts +1 -0
  222. package/dist/layout/index.d.ts.map +1 -1
  223. package/dist/layout/index.js +3 -1
  224. package/dist/layout/index.js.map +1 -1
  225. package/dist/layout/sheet.d.ts +3 -0
  226. package/dist/layout/sheet.d.ts.map +1 -0
  227. package/dist/layout/sheet.js +6 -0
  228. package/dist/layout/sheet.js.map +1 -0
  229. package/dist/theme/ThemeProvider.d.ts +4 -1
  230. package/dist/theme/ThemeProvider.d.ts.map +1 -1
  231. package/dist/theme/ThemeProvider.js +5 -2
  232. package/dist/theme/ThemeProvider.js.map +1 -1
  233. package/dist/theme/index.d.ts +2 -0
  234. package/dist/theme/index.d.ts.map +1 -1
  235. package/dist/theme/index.js +6 -1
  236. package/dist/theme/index.js.map +1 -1
  237. package/dist/theme/resolveThemeSpacing.d.ts +12 -0
  238. package/dist/theme/resolveThemeSpacing.d.ts.map +1 -0
  239. package/dist/theme/resolveThemeSpacing.js +14 -0
  240. package/dist/theme/resolveThemeSpacing.js.map +1 -0
  241. package/dist/theme/types.d.ts +2 -0
  242. package/dist/theme/types.d.ts.map +1 -1
  243. package/dist/theme/useThemeSpacing.d.ts +4 -0
  244. package/dist/theme/useThemeSpacing.d.ts.map +1 -0
  245. package/dist/theme/useThemeSpacing.js +11 -0
  246. package/dist/theme/useThemeSpacing.js.map +1 -0
  247. package/docs/components/button.md +92 -38
  248. package/docs/components/choice.md +88 -0
  249. package/docs/components/error-banner.md +46 -3
  250. package/docs/components/form-sheet.md +140 -0
  251. package/docs/components/form.md +80 -45
  252. package/docs/components/header-button.md +310 -0
  253. package/docs/components/header-wash.md +40 -0
  254. package/docs/components/page.md +183 -0
  255. package/docs/components/select.md +2 -2
  256. package/docs/components/tab-accessory-button.md +146 -0
  257. package/docs/specifications/Liquid Glass header buttons in Expo Router.md +327 -0
  258. package/package.json +1 -1
  259. package/src/components/Button/Button.tsx +82 -38
  260. package/src/components/Button/buttonAppearance.test.ts +44 -0
  261. package/src/components/Button/buttonAppearance.ts +38 -0
  262. package/src/components/Button/buttonThemeRoles.test.ts +10 -0
  263. package/src/components/Button/buttonThemeRoles.ts +6 -0
  264. package/src/components/Button/index.ts +12 -1
  265. package/src/components/Choice/Choice.tsx +109 -0
  266. package/src/components/Choice/choiceLayout.test.ts +14 -0
  267. package/src/components/Choice/choiceLayout.ts +9 -0
  268. package/src/components/Choice/choiceThemeRoles.test.ts +14 -0
  269. package/src/components/Choice/choiceThemeRoles.ts +8 -0
  270. package/src/components/Choice/index.ts +4 -0
  271. package/src/components/Choice/types.ts +26 -0
  272. package/src/components/ErrorBanner/ErrorBanner.tsx +32 -22
  273. package/src/components/ErrorBanner/errorBannerContext.tsx +31 -6
  274. package/src/components/ErrorBanner/errorBannerLayout.test.ts +49 -0
  275. package/src/components/ErrorBanner/errorBannerLayout.ts +59 -0
  276. package/src/components/ErrorBanner/icons.tsx +25 -6
  277. package/src/components/ErrorBanner/index.ts +1 -0
  278. package/src/components/Form/Form.tsx +77 -173
  279. package/src/components/Form/FormCloseButton.tsx +12 -4
  280. package/src/components/Form/FormErrorBanner.tsx +57 -0
  281. package/src/components/Form/FormSubmitFooter.tsx +70 -0
  282. package/src/components/Form/formThemeRoles.test.ts +13 -0
  283. package/src/components/Form/formThemeRoles.ts +7 -0
  284. package/src/components/Form/index.ts +1 -0
  285. package/src/components/Form/types.ts +14 -10
  286. package/src/components/FormSheet/FormSheet.tsx +279 -0
  287. package/src/components/FormSheet/formSheetLayout.test.ts +66 -0
  288. package/src/components/FormSheet/formSheetLayout.ts +55 -0
  289. package/src/components/FormSheet/formSheetThemeRoles.test.ts +14 -0
  290. package/src/components/FormSheet/formSheetThemeRoles.ts +8 -0
  291. package/src/components/FormSheet/index.ts +7 -0
  292. package/src/components/FormSheet/types.ts +66 -0
  293. package/src/components/HeaderButton/HeaderCapsule.tsx +63 -0
  294. package/src/components/HeaderButton/HeaderGroup.tsx +36 -0
  295. package/src/components/HeaderButton/HeaderIcon.tsx +56 -0
  296. package/src/components/HeaderButton/headerButtonLayout.test.ts +47 -0
  297. package/src/components/HeaderButton/headerButtonLayout.ts +36 -0
  298. package/src/components/HeaderButton/headerButtonThemeRoles.test.ts +9 -0
  299. package/src/components/HeaderButton/headerButtonThemeRoles.ts +5 -0
  300. package/src/components/HeaderButton/index.ts +14 -0
  301. package/src/components/HeaderButton/types.ts +40 -0
  302. package/src/components/HeaderWash/HeaderWash.tsx +20 -0
  303. package/src/components/HeaderWash/headerWashGradient.test.ts +22 -0
  304. package/src/components/{Form/withAlpha.ts → HeaderWash/headerWashGradient.ts} +9 -0
  305. package/src/components/HeaderWash/index.ts +1 -0
  306. package/src/components/Page/Page.tsx +80 -0
  307. package/src/components/Page/PageBleed.tsx +14 -0
  308. package/src/components/Page/index.ts +4 -0
  309. package/src/components/Page/pageContext.ts +8 -0
  310. package/src/components/Page/pageInsets.test.ts +73 -0
  311. package/src/components/Page/pageInsets.ts +52 -0
  312. package/src/components/Page/pageThemeRoles.test.ts +9 -0
  313. package/src/components/Page/pageThemeRoles.ts +5 -0
  314. package/src/components/Page/types.ts +26 -0
  315. package/src/components/Select/Select.tsx +11 -4
  316. package/src/components/Select/selectThemeRoles.test.ts +5 -0
  317. package/src/components/Select/selectThemeRoles.ts +2 -0
  318. package/src/components/Select/types.ts +4 -0
  319. package/src/components/TabAccessoryButton/TabAccessoryButton.tsx +150 -0
  320. package/src/components/TabAccessoryButton/index.ts +12 -0
  321. package/src/components/TabAccessoryButton/tabAccessoryLayout.test.ts +42 -0
  322. package/src/components/TabAccessoryButton/tabAccessoryLayout.ts +43 -0
  323. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.test.ts +28 -0
  324. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.ts +22 -0
  325. package/src/components/TabAccessoryButton/types.ts +27 -0
  326. package/src/components/index.ts +64 -2
  327. package/src/index.ts +61 -2
  328. package/src/layout/headerInset.test.ts +60 -0
  329. package/src/layout/headerInset.ts +41 -0
  330. package/src/layout/index.ts +1 -0
  331. package/src/layout/sheet.ts +2 -0
  332. package/src/theme/ThemeProvider.tsx +9 -1
  333. package/src/theme/index.ts +6 -0
  334. package/src/theme/resolveThemeSpacing.test.ts +29 -0
  335. package/src/theme/resolveThemeSpacing.ts +21 -0
  336. package/src/theme/types.ts +3 -0
  337. package/src/theme/useThemeSpacing.ts +8 -0
  338. package/src/components/Form/withAlpha.test.ts +0 -13
@@ -8,10 +8,15 @@ import {
8
8
  type ReactNode,
9
9
  type RefObject,
10
10
  } from "react";
11
- import { View, type ReactNativeElement, type ScrollView } from "react-native";
11
+ import { View, type ReactNativeElement } from "react-native";
12
12
  import { fieldScrollY } from "./errorBannerCopy";
13
13
  import type { FieldError } from "./types";
14
14
 
15
+ /** Anything that scrolls: a ScrollView, a KeyboardAwareScrollView, a BottomSheetScrollView. */
16
+ export type ErrorBannerScrollTarget = {
17
+ scrollTo(options: { y: number; animated?: boolean }): void;
18
+ };
19
+
15
20
  export type ErrorBannerAnchor = {
16
21
  view: View;
17
22
  focus?: () => void;
@@ -76,12 +81,28 @@ export function ErrorBannerProvider({
76
81
  errors,
77
82
  onDismiss,
78
83
  bannerHeightRef,
84
+ measureRef,
85
+ onBeforeJump,
86
+ scrollYForField,
79
87
  }: {
80
88
  children: ReactNode;
81
- scrollRef: RefObject<ScrollView | null>;
89
+ scrollRef: RefObject<ErrorBannerScrollTarget | null>;
82
90
  errors: readonly FieldError[];
83
91
  onDismiss: () => void;
92
+ /** How far the banner covers the top of the scroll area. */
84
93
  bannerHeightRef: RefObject<number>;
94
+ /**
95
+ * View that field positions are measured against: the scroll content's
96
+ * top. Defaults to `scrollRef`, which works for a plain ScrollView.
97
+ */
98
+ measureRef?: RefObject<View | null>;
99
+ /** Runs before scrolling to a field, e.g. to expand a sheet. */
100
+ onBeforeJump?: () => void;
101
+ /**
102
+ * Scroll offset for a field at content y `fieldY`. Defaults to clearing
103
+ * `bannerHeightRef` from the top of the scroll area.
104
+ */
105
+ scrollYForField?: (fieldY: number) => number;
85
106
  }) {
86
107
  const anchors = useRef(new Map<string, ErrorBannerAnchor>());
87
108
 
@@ -96,21 +117,25 @@ export function ErrorBannerProvider({
96
117
  const jump = useCallback((field: string) => {
97
118
  const anchor = anchors.current.get(field);
98
119
  const scroll = scrollRef.current;
99
- if (!anchor || !scroll) {
120
+ const relativeTo = measureRef?.current ?? scroll;
121
+ if (!anchor || !scroll || !relativeTo) {
100
122
  return;
101
123
  }
124
+ onBeforeJump?.();
102
125
  anchor.view.measureLayout(
103
- scroll as unknown as ReactNativeElement,
126
+ relativeTo as unknown as ReactNativeElement,
104
127
  (_x, y) => {
105
128
  scroll.scrollTo({
106
- y: fieldScrollY(y, bannerHeightRef.current ?? 0),
129
+ y: scrollYForField
130
+ ? scrollYForField(y)
131
+ : fieldScrollY(y, bannerHeightRef.current ?? 0),
107
132
  animated: true,
108
133
  });
109
134
  anchor.focus?.();
110
135
  },
111
136
  () => undefined,
112
137
  );
113
- }, [bannerHeightRef, scrollRef]);
138
+ }, [bannerHeightRef, measureRef, onBeforeJump, scrollRef, scrollYForField]);
114
139
 
115
140
  const value = useMemo(
116
141
  () => ({ errors, register, dismiss: onDismiss, jump }),
@@ -0,0 +1,49 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ ERROR_BANNER_DISC_SIZE,
5
+ ERROR_BANNER_FONT_SIZE,
6
+ ERROR_BANNER_HEADER_GAP,
7
+ ERROR_BANNER_ROW_INSET,
8
+ ERROR_BANNER_ROW_MIN_HEIGHT,
9
+ errorBannerBarStyle,
10
+ } from "./errorBannerLayout.ts";
11
+
12
+ describe("errorBannerBarStyle", () => {
13
+ it("uses light status bar text on a dark banner", () => {
14
+ assert.equal(errorBannerBarStyle("#1F2937"), "light-content");
15
+ assert.equal(errorBannerBarStyle("#000"), "light-content");
16
+ });
17
+
18
+ it("uses dark status bar text on a light banner", () => {
19
+ assert.equal(errorBannerBarStyle("#F9FAFB"), "dark-content");
20
+ assert.equal(errorBannerBarStyle("#fff"), "dark-content");
21
+ });
22
+
23
+ it("reads rgb() and 8-digit hex colours", () => {
24
+ assert.equal(errorBannerBarStyle("rgb(255, 255, 255)"), "dark-content");
25
+ assert.equal(errorBannerBarStyle("rgba(31, 41, 55, 0.9)"), "light-content");
26
+ assert.equal(errorBannerBarStyle("#FFFFFFCC"), "dark-content");
27
+ });
28
+
29
+ it("falls back to the system default when the colour cannot be read", () => {
30
+ assert.equal(errorBannerBarStyle("red"), "default");
31
+ });
32
+ });
33
+
34
+ describe("error banner sizes", () => {
35
+ it("matches the 17pt body text used by the other components", () => {
36
+ assert.equal(ERROR_BANNER_FONT_SIZE, 17);
37
+ });
38
+
39
+ it("gives each row the 44pt minimum tap target", () => {
40
+ assert.ok(ERROR_BANNER_ROW_MIN_HEIGHT >= 44);
41
+ });
42
+
43
+ it("lines rows up with the title, after the disc and gap", () => {
44
+ assert.equal(
45
+ ERROR_BANNER_ROW_INSET,
46
+ ERROR_BANNER_DISC_SIZE + ERROR_BANNER_HEADER_GAP,
47
+ );
48
+ });
49
+ });
@@ -0,0 +1,59 @@
1
+ /** Title and rows, matching the 17pt body text of Button, Field and Select. */
2
+ export const ERROR_BANNER_FONT_SIZE = 17;
3
+ export const ERROR_BANNER_LINE_HEIGHT = 22;
4
+ export const ERROR_BANNER_DISC_SIZE = 32;
5
+ export const ERROR_BANNER_MEGAPHONE_SIZE = 18;
6
+ export const ERROR_BANNER_ARROW_SIZE = 20;
7
+ export const ERROR_BANNER_DISMISS_SIZE = 18;
8
+ export const ERROR_BANNER_HEADER_GAP = 12;
9
+ export const ERROR_BANNER_ROW_MIN_HEIGHT = 44;
10
+ /** Rows start under the title, past the disc and gap. */
11
+ export const ERROR_BANNER_ROW_INSET =
12
+ ERROR_BANNER_DISC_SIZE + ERROR_BANNER_HEADER_GAP;
13
+
14
+ export type ErrorBannerBarStyle = "light-content" | "dark-content" | "default";
15
+
16
+ function parseRgb(color: string): [number, number, number] | null {
17
+ const value = color.trim();
18
+ const hex = /^#([0-9a-f]{3,8})$/i.exec(value)?.[1];
19
+ if (hex) {
20
+ const full =
21
+ hex.length === 3 || hex.length === 4
22
+ ? hex
23
+ .slice(0, 3)
24
+ .split("")
25
+ .map((c) => c + c)
26
+ .join("")
27
+ : hex.slice(0, 6);
28
+ if (full.length !== 6) {
29
+ return null;
30
+ }
31
+ return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16)) as [
32
+ number,
33
+ number,
34
+ number,
35
+ ];
36
+ }
37
+ const rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value);
38
+ if (rgb) {
39
+ return [Number(rgb[1]), Number(rgb[2]), Number(rgb[3])];
40
+ }
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Status bar text that stays readable over the banner fill: light text on a
46
+ * dark banner, dark text on a light one. Pure so it can be unit-tested.
47
+ */
48
+ export function errorBannerBarStyle(backgroundColor: string): ErrorBannerBarStyle {
49
+ const rgb = parseRgb(backgroundColor);
50
+ if (!rgb) {
51
+ return "default";
52
+ }
53
+ const [r, g, b] = rgb.map((c) => {
54
+ const s = c / 255;
55
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
56
+ });
57
+ const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;
58
+ return luminance > 0.179 ? "dark-content" : "light-content";
59
+ }
@@ -1,10 +1,12 @@
1
1
  import Svg, { Line, Path, Polyline } from "react-native-svg";
2
2
 
3
- export function Megaphone({ color }: { color: string }) {
3
+ type IconProps = { color: string; size: number };
4
+
5
+ export function Megaphone({ color, size }: IconProps) {
4
6
  return (
5
7
  <Svg
6
- width={16}
7
- height={16}
8
+ width={size}
9
+ height={size}
8
10
  viewBox="0 0 24 24"
9
11
  fill="none"
10
12
  stroke={color}
@@ -19,11 +21,11 @@ export function Megaphone({ color }: { color: string }) {
19
21
  );
20
22
  }
21
23
 
22
- export function ArrowRight({ color }: { color: string }) {
24
+ export function ArrowRight({ color, size }: IconProps) {
23
25
  return (
24
26
  <Svg
25
- width={18}
26
- height={18}
27
+ width={size}
28
+ height={size}
27
29
  viewBox="0 0 24 24"
28
30
  fill="none"
29
31
  stroke={color}
@@ -36,3 +38,20 @@ export function ArrowRight({ color }: { color: string }) {
36
38
  </Svg>
37
39
  );
38
40
  }
41
+
42
+ export function Cross({ color, size }: IconProps) {
43
+ return (
44
+ <Svg
45
+ width={size}
46
+ height={size}
47
+ viewBox="0 0 24 24"
48
+ fill="none"
49
+ stroke={color}
50
+ strokeWidth={2.25}
51
+ strokeLinecap="round"
52
+ >
53
+ <Line x1={6} y1={6} x2={18} y2={18} />
54
+ <Line x1={18} y1={6} x2={6} y2={18} />
55
+ </Svg>
56
+ );
57
+ }
@@ -5,6 +5,7 @@ export {
5
5
  useErrorBannerContext,
6
6
  useErrorBannerDismiss,
7
7
  useErrorBannerField,
8
+ type ErrorBannerScrollTarget,
8
9
  } from "./errorBannerContext";
9
10
  export {
10
11
  ERROR_BANNER_THEME_ROLES,
@@ -1,55 +1,20 @@
1
- import { useRef, useState, type ReactNode } from "react";
2
- import {
3
- StyleSheet,
4
- Text,
5
- View,
6
- type LayoutChangeEvent,
7
- } from "react-native";
1
+ import { useCallback, useRef, useState } from "react";
2
+ import { Platform, StyleSheet, View, type LayoutChangeEvent } from "react-native";
8
3
  import {
9
4
  KeyboardAwareScrollView,
10
- KeyboardStickyView,
11
5
  type KeyboardAwareScrollViewRef,
12
6
  } from "react-native-keyboard-controller";
13
- import {
14
- ErrorBanner,
15
- ErrorBannerProvider,
16
- useErrorBanner,
17
- useErrorBannerContext,
18
- type FieldError,
19
- } from "../ErrorBanner";
20
7
  import { useSafeAreaInsets } from "react-native-safe-area-context";
21
- import { screenPaddingHorizontal } from "../../layout/screenPadding";
22
- import { Button } from "../Button/Button";
23
- import { FormCloseButton } from "./FormCloseButton";
8
+ import { estimateHeaderHeight, formFieldScrollY } from "../../layout/headerInset";
9
+ import { useThemeSpacing } from "../../theme/useThemeSpacing";
10
+ import { ErrorBannerProvider, useErrorBanner } from "../ErrorBanner";
11
+ import { ERROR_BANNER_SCROLL_GAP } from "../ErrorBanner/errorBannerCopy";
12
+ import { FormErrorBanner } from "./FormErrorBanner";
13
+ import { FormSubmitFooter } from "./FormSubmitFooter";
14
+ import { FORM_THEME_ROLES } from "./formThemeRoles";
24
15
  import type { FormProps } from "./types";
25
16
  import { useResolvedThemeColor } from "./useResolvedThemeColor";
26
- import { withAlpha } from "./withAlpha";
27
17
 
28
- /** How far the header wash continues below the buttons. */
29
- const FADE_TAIL = 32;
30
-
31
- function FormErrorBanner({
32
- open,
33
- errors,
34
- onDismiss,
35
- onHeightChange,
36
- }: {
37
- open: boolean;
38
- errors: readonly FieldError[];
39
- onDismiss: () => void;
40
- onHeightChange: (height: number) => void;
41
- }) {
42
- const ctx = useErrorBannerContext();
43
- return (
44
- <ErrorBanner
45
- errors={errors}
46
- onDismiss={onDismiss}
47
- onHeightChange={onHeightChange}
48
- onJump={(field) => ctx?.jump(field)}
49
- open={open}
50
- />
51
- );
52
- }
53
18
 
54
19
  /** Space kept between the focused field and the top of the footer. */
55
20
  const FIELD_GAP = 12;
@@ -60,55 +25,79 @@ const FIELD_GAP = 12;
60
25
  */
61
26
  const FIELD_BELOW_CARET = 36;
62
27
 
28
+ /**
29
+ * A full-screen form under the native stack header. Set the title and the
30
+ * close button in the screen's stack options. The scroll view is the
31
+ * screen's first child, so iOS insets it for the header (including a large
32
+ * title) and content scrolls behind the bar. Render `Form` as the screen's
33
+ * root element. For a wash behind a transparent header, use `HeaderWash` as
34
+ * the stack's `headerBackground`.
35
+ */
63
36
  export function Form({
64
- title,
65
37
  children,
66
- onClose,
67
38
  onSubmit,
68
39
  errors = [],
69
40
  submitLabel = "Save",
70
41
  submitting = false,
71
42
  submitDisabled = false,
72
- closeButton,
73
43
  submitRadius = "lg",
74
- paddingHorizontal = screenPaddingHorizontal,
44
+ headerHeight: headerHeightProp,
45
+ paddingHorizontal: paddingHorizontalProp,
75
46
  backgroundColor: backgroundColorProp,
76
- titleColor: titleColorProp,
77
47
  footerBackgroundColor: footerBackgroundColorProp,
78
- buttonBackgroundColor: buttonBackgroundColorProp,
79
- buttonLabelColor: buttonLabelColorProp,
48
+ buttonBackgroundColor,
49
+ buttonLabelColor,
80
50
  }: FormProps) {
51
+ const spacing = useThemeSpacing();
81
52
  const insets = useSafeAreaInsets();
82
- const background = useResolvedThemeColor(backgroundColorProp, "background");
83
- const titleColor = useResolvedThemeColor(titleColorProp, "text");
53
+ const paddingHorizontal = paddingHorizontalProp ?? spacing.screen;
54
+ const background = useResolvedThemeColor(
55
+ backgroundColorProp,
56
+ FORM_THEME_ROLES.backgroundColor,
57
+ );
84
58
  const footerBackground = useResolvedThemeColor(
85
59
  footerBackgroundColorProp,
86
- "surface",
87
- );
88
- const buttonBackground = useResolvedThemeColor(
89
- buttonBackgroundColorProp,
90
- "buttonBackground",
91
- );
92
- const buttonLabel = useResolvedThemeColor(
93
- buttonLabelColorProp,
94
- "buttonLabel",
60
+ FORM_THEME_ROLES.footerBackgroundColor,
95
61
  );
96
62
  const scrollRef = useRef<KeyboardAwareScrollViewRef>(null);
97
63
  const bannerHeightRef = useRef(0);
64
+ const scrollTopRef = useRef(0);
65
+ const headerHeight =
66
+ headerHeightProp ?? estimateHeaderHeight(Platform.OS, insets.top);
67
+ const headerHeightRef = useRef(headerHeight);
68
+ headerHeightRef.current = headerHeight;
98
69
  const banner = useErrorBanner(errors);
99
70
  const [footerHeight, setFooterHeight] = useState(0);
100
- const [cancelWidth, setCancelWidth] = useState(0);
101
- const [barHeight, setBarHeight] = useState(0);
102
- const resolvedBar = barHeight || insets.top + 52;
103
71
 
104
72
  const onFooterLayout = (event: LayoutChangeEvent) => {
105
73
  const next = Math.round(event.nativeEvent.layout.height);
106
74
  setFooterHeight((current) => (current === next ? current : next));
107
75
  };
108
76
 
109
- const resolvedClose: ReactNode = closeButton ?? (
110
- <FormCloseButton onPress={onClose} />
111
- );
77
+ // Window y of the scroll view: 0 under a transparent header, the header's
78
+ // bottom under an opaque one.
79
+ const onScrollLayout = (event: LayoutChangeEvent) => {
80
+ (event.currentTarget as unknown as View).measureInWindow((_x, y) => {
81
+ scrollTopRef.current = Math.round(y);
82
+ });
83
+ };
84
+
85
+ // A banner row scrolls its field clear of the header and the banner.
86
+ const scrollYForField = useCallback((fieldY: number) => {
87
+ const scrollTop = scrollTopRef.current;
88
+ const header = headerHeightRef.current;
89
+ // iOS draws the banner over the window top; Android inside the screen.
90
+ const bannerTop = Platform.OS === "ios" ? 0 : scrollTop;
91
+ return formFieldScrollY({
92
+ fieldY,
93
+ scrollTop,
94
+ visibleTop: Math.max(scrollTop, header),
95
+ bannerBottom: bannerTop + bannerHeightRef.current,
96
+ // Under a transparent header iOS rests the content at -(header).
97
+ minScrollY: Math.min(0, scrollTop - header),
98
+ gap: ERROR_BANNER_SCROLL_GAP,
99
+ });
100
+ }, []);
112
101
 
113
102
  return (
114
103
  <ErrorBannerProvider
@@ -116,96 +105,42 @@ export function Form({
116
105
  errors={errors}
117
106
  onDismiss={banner.dismiss}
118
107
  scrollRef={scrollRef}
108
+ scrollYForField={scrollYForField}
119
109
  >
120
- <View style={[styles.screen, { backgroundColor: background }]}>
121
110
  <KeyboardAwareScrollView
122
111
  ref={scrollRef}
123
112
  automaticallyAdjustKeyboardInsets={false}
124
113
  bottomOffset={footerHeight + FIELD_GAP + FIELD_BELOW_CARET}
125
114
  contentContainerStyle={[
126
115
  styles.content,
127
- {
128
- paddingTop: resolvedBar + FADE_TAIL,
129
- paddingHorizontal,
130
- },
116
+ { paddingTop: spacing.pageTop, paddingHorizontal },
131
117
  ]}
132
- contentInsetAdjustmentBehavior="never"
118
+ contentInsetAdjustmentBehavior="automatic"
133
119
  keyboardDismissMode="interactive"
134
120
  keyboardShouldPersistTaps="handled"
121
+ onLayout={onScrollLayout}
135
122
  showsVerticalScrollIndicator={false}
136
- style={styles.scroll}
123
+ style={[styles.scroll, { backgroundColor: background }]}
137
124
  >
138
125
  {children}
139
126
  </KeyboardAwareScrollView>
140
127
 
141
- <View
142
- pointerEvents="box-none"
143
- style={[
144
- styles.fade,
145
- {
146
- height: resolvedBar + FADE_TAIL,
147
- experimental_backgroundImage: `linear-gradient(to bottom, ${withAlpha(background, 0.98)} 0%, ${withAlpha(background, 0.92)} 55%, ${withAlpha(background, 0)} 100%)`,
148
- },
149
- ]}
150
- >
151
- <View
152
- onLayout={(event) => {
153
- const next = Math.round(event.nativeEvent.layout.height);
154
- setBarHeight((current) => (current === next ? current : next));
155
- }}
156
- pointerEvents="box-none"
157
- style={[
158
- styles.header,
159
- { paddingTop: insets.top, paddingHorizontal },
160
- ]}
161
- >
162
- <View
163
- onLayout={(event) => {
164
- const next = Math.round(event.nativeEvent.layout.width);
165
- setCancelWidth((current) => (current === next ? current : next));
166
- }}
167
- >
168
- {resolvedClose}
169
- </View>
170
- <Text
171
- accessibilityRole="header"
172
- numberOfLines={1}
173
- pointerEvents="none"
174
- style={[styles.title, { flex: 1, color: titleColor }]}
175
- >
176
- {title}
177
- </Text>
178
- <View style={{ width: cancelWidth }} />
179
- </View>
180
- </View>
128
+ <FormSubmitFooter
129
+ backgroundColor={footerBackground}
130
+ buttonBackgroundColor={buttonBackgroundColor}
131
+ buttonLabelColor={buttonLabelColor}
132
+ disabled={submitDisabled}
133
+ label={submitLabel}
134
+ onLayout={onFooterLayout}
135
+ onPress={() => {
136
+ banner.present();
137
+ onSubmit();
138
+ }}
139
+ paddingHorizontal={paddingHorizontal}
140
+ radius={submitRadius}
141
+ submitting={submitting}
142
+ />
181
143
 
182
- {/*
183
- The spacer is the home-indicator clearance. `opened` equals that
184
- spacer, so once the keyboard is up the spacer sits inside the
185
- keyboard and the button rests on whatever height the keyboard is.
186
- */}
187
- <KeyboardStickyView offset={{ closed: 0, opened: insets.bottom }}>
188
- <View style={[styles.footerShell, { backgroundColor: footerBackground }]}>
189
- <View
190
- onLayout={onFooterLayout}
191
- style={[styles.footer, { paddingHorizontal }]}
192
- >
193
- <Button
194
- backgroundColor={buttonBackground}
195
- disabled={submitDisabled}
196
- label={submitLabel}
197
- labelColor={buttonLabel}
198
- loading={submitting}
199
- onPress={() => {
200
- banner.present();
201
- onSubmit();
202
- }}
203
- radius={submitRadius}
204
- />
205
- </View>
206
- <View style={{ height: insets.bottom }} />
207
- </View>
208
- </KeyboardStickyView>
209
144
  <FormErrorBanner
210
145
  errors={errors}
211
146
  onDismiss={banner.dismiss}
@@ -214,46 +149,15 @@ export function Form({
214
149
  }}
215
150
  open={banner.open}
216
151
  />
217
- </View>
218
152
  </ErrorBannerProvider>
219
153
  );
220
154
  }
221
155
 
222
156
  const styles = StyleSheet.create({
223
- screen: {
224
- flex: 1,
225
- },
226
157
  scroll: {
227
158
  flex: 1,
228
159
  },
229
160
  content: {
230
- paddingTop: 20,
231
161
  paddingBottom: 24,
232
162
  },
233
- fade: {
234
- position: "absolute",
235
- top: 0,
236
- left: 0,
237
- right: 0,
238
- },
239
- header: {
240
- flexDirection: "row",
241
- alignItems: "center",
242
- },
243
- title: {
244
- fontSize: 17,
245
- fontWeight: "600",
246
- textAlign: "center",
247
- },
248
- footerShell: {
249
- // Cast upward so the sticky footer separates from the scroll body.
250
- shadowColor: "#000",
251
- shadowOffset: { width: 0, height: -4 },
252
- shadowOpacity: 0.08,
253
- shadowRadius: 12,
254
- elevation: 8,
255
- },
256
- footer: {
257
- paddingVertical: 12,
258
- },
259
163
  });
@@ -1,12 +1,20 @@
1
1
  import { Pressable, StyleSheet, Text } from "react-native";
2
- import { useThemeColor } from "../../theme/useThemeColor";
2
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
3
+ import { FORM_THEME_ROLES } from "./formThemeRoles";
3
4
 
4
- export function FormCloseButton({ onPress }: { onPress: () => void }) {
5
- const text = useThemeColor("text");
5
+ export function FormCloseButton({
6
+ onPress,
7
+ color: colorProp,
8
+ }: {
9
+ onPress: () => void;
10
+ /** Defaults to theme role `formClose`. */
11
+ color?: string;
12
+ }) {
13
+ const color = useResolvedFieldColor(colorProp, FORM_THEME_ROLES.closeColor);
6
14
 
7
15
  return (
8
16
  <Pressable accessibilityRole="button" hitSlop={8} onPress={onPress}>
9
- <Text style={[styles.label, { color: text }]}>Cancel</Text>
17
+ <Text style={[styles.label, { color }]}>Cancel</Text>
10
18
  </Pressable>
11
19
  );
12
20
  }
@@ -0,0 +1,57 @@
1
+ import { Platform, StyleSheet, View } from "react-native";
2
+ import { FullWindowOverlay } from "react-native-screens";
3
+ import {
4
+ ErrorBanner,
5
+ useErrorBannerContext,
6
+ type FieldError,
7
+ } from "../ErrorBanner";
8
+
9
+ /**
10
+ * The screen banner wired to the nearest `ErrorBannerProvider`. Shared by
11
+ * `Form` and `FormSheet`. On iOS it renders in a native overlay window so it
12
+ * covers the native header. Android native headers do not overlap the
13
+ * screen, so there it sits `androidTop` below the screen top, under the header.
14
+ */
15
+ export function FormErrorBanner({
16
+ open,
17
+ errors,
18
+ onDismiss,
19
+ onHeightChange,
20
+ androidTop = 0,
21
+ }: {
22
+ open: boolean;
23
+ errors: readonly FieldError[];
24
+ onDismiss: () => void;
25
+ onHeightChange: (height: number) => void;
26
+ androidTop?: number;
27
+ }) {
28
+ const ctx = useErrorBannerContext();
29
+ const banner = (
30
+ <ErrorBanner
31
+ errors={errors}
32
+ onDismiss={onDismiss}
33
+ onHeightChange={onHeightChange}
34
+ onJump={(field) => ctx?.jump(field)}
35
+ open={open}
36
+ safeAreaTop={Platform.OS === "ios" ? undefined : 0}
37
+ />
38
+ );
39
+
40
+ if (Platform.OS === "ios") {
41
+ return <FullWindowOverlay>{banner}</FullWindowOverlay>;
42
+ }
43
+ return (
44
+ <View pointerEvents="box-none" style={[styles.below, { top: androidTop }]}>
45
+ {banner}
46
+ </View>
47
+ );
48
+ }
49
+
50
+ const styles = StyleSheet.create({
51
+ below: {
52
+ position: "absolute",
53
+ left: 0,
54
+ right: 0,
55
+ bottom: 0,
56
+ },
57
+ });