@studio-piot/native-ui 0.2.5 → 0.17.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 +10 -0
  19. package/dist/components/Choice/Choice.d.ts.map +1 -0
  20. package/dist/components/Choice/Choice.js +66 -0
  21. package/dist/components/Choice/Choice.js.map +1 -0
  22. package/dist/components/Choice/choiceLayout.d.ts +23 -0
  23. package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
  24. package/dist/components/Choice/choiceLayout.js +32 -0
  25. package/dist/components/Choice/choiceLayout.js.map +1 -0
  26. package/dist/components/Choice/choiceThemeRoles.d.ts +9 -0
  27. package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
  28. package/dist/components/Choice/choiceThemeRoles.js +11 -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 +33 -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 +111 -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 +120 -0
  266. package/src/components/Choice/choiceLayout.test.ts +49 -0
  267. package/src/components/Choice/choiceLayout.ts +34 -0
  268. package/src/components/Choice/choiceThemeRoles.test.ts +15 -0
  269. package/src/components/Choice/choiceThemeRoles.ts +9 -0
  270. package/src/components/Choice/index.ts +4 -0
  271. package/src/components/Choice/types.ts +33 -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
@@ -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
+ });
@@ -0,0 +1,70 @@
1
+ import { StyleSheet, View, type LayoutChangeEvent } from "react-native";
2
+ import { KeyboardStickyView } from "react-native-keyboard-controller";
3
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
4
+ import { Button } from "../Button";
5
+ import type { ButtonRadius } from "../Button/buttonRadius";
6
+
7
+ /** Sticky submit footer shared by `Form` and `FormSheet`. */
8
+ export function FormSubmitFooter({
9
+ label,
10
+ onPress,
11
+ submitting,
12
+ disabled,
13
+ radius,
14
+ paddingHorizontal,
15
+ backgroundColor,
16
+ buttonBackgroundColor,
17
+ buttonLabelColor,
18
+ onLayout,
19
+ }: {
20
+ label: string;
21
+ onPress: () => void;
22
+ submitting: boolean;
23
+ disabled: boolean;
24
+ radius: ButtonRadius;
25
+ paddingHorizontal: number;
26
+ backgroundColor: string;
27
+ buttonBackgroundColor?: string;
28
+ buttonLabelColor?: string;
29
+ /** Height of the bar above the home indicator, for scroll clearance. */
30
+ onLayout?: (event: LayoutChangeEvent) => void;
31
+ }) {
32
+ const insets = useSafeAreaInsets();
33
+
34
+ return (
35
+ // The spacer is the home-indicator clearance. `opened` equals that
36
+ // spacer, so once the keyboard is up the spacer sits inside the
37
+ // keyboard and the button rests on whatever height the keyboard is.
38
+ <KeyboardStickyView offset={{ closed: 0, opened: insets.bottom }}>
39
+ <View style={[styles.shell, { backgroundColor }]}>
40
+ <View onLayout={onLayout} style={[styles.bar, { paddingHorizontal }]}>
41
+ <Button
42
+ backgroundColor={buttonBackgroundColor}
43
+ disabled={disabled}
44
+ label={label}
45
+ labelColor={buttonLabelColor}
46
+ loading={submitting}
47
+ onPress={onPress}
48
+ radius={radius}
49
+ />
50
+ </View>
51
+ <View style={{ height: insets.bottom }} />
52
+ </View>
53
+ </KeyboardStickyView>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ shell: {
59
+ zIndex: 2,
60
+ // Cast upward so the sticky footer separates from the scroll body.
61
+ shadowColor: "#000",
62
+ shadowOffset: { width: 0, height: -4 },
63
+ shadowOpacity: 0.08,
64
+ shadowRadius: 12,
65
+ elevation: 8,
66
+ },
67
+ bar: {
68
+ paddingVertical: 12,
69
+ },
70
+ });
@@ -0,0 +1,13 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { FORM_THEME_ROLES } from "./formThemeRoles.ts";
4
+
5
+ describe("FORM_THEME_ROLES", () => {
6
+ it("maps colour props to form-prefixed roles, not the app's own names", () => {
7
+ assert.deepEqual(FORM_THEME_ROLES, {
8
+ backgroundColor: "formBackground",
9
+ footerBackgroundColor: "formFooterBackground",
10
+ closeColor: "formClose",
11
+ });
12
+ });
13
+ });
@@ -0,0 +1,7 @@
1
+ export const FORM_THEME_ROLES = {
2
+ backgroundColor: "formBackground",
3
+ footerBackgroundColor: "formFooterBackground",
4
+ closeColor: "formClose",
5
+ } as const;
6
+
7
+ export type FormThemeColorProp = keyof typeof FORM_THEME_ROLES;
@@ -1,3 +1,4 @@
1
1
  export { Form } from "./Form";
2
2
  export { FormCloseButton } from "./FormCloseButton";
3
+ export { FORM_THEME_ROLES, type FormThemeColorProp } from "./formThemeRoles";
3
4
  export type { FormProps } from "./types";
@@ -2,10 +2,12 @@ import type { ReactNode } from "react";
2
2
  import type { ButtonRadius } from "../Button/buttonRadius";
3
3
  import type { FieldError } from "../ErrorBanner/types";
4
4
 
5
+ /**
6
+ * Set the title and close button in the screen's stack options
7
+ * (`title`, `headerLeft`), not here.
8
+ */
5
9
  export type FormProps = {
6
- title: string;
7
10
  children: ReactNode;
8
- onClose: () => void;
9
11
  onSubmit: () => void;
10
12
  /**
11
13
  * Field errors for the screen banner. Empty hides it.
@@ -16,19 +18,21 @@ export type FormProps = {
16
18
  submitLabel?: string;
17
19
  submitting?: boolean;
18
20
  submitDisabled?: boolean;
19
- /** App-specific Cancel control. Defaults to a plain text Cancel. */
20
- closeButton?: ReactNode;
21
21
  submitRadius?: ButtonRadius;
22
22
  /**
23
- * Horizontal inset for header, scroll content, and footer.
24
- * Defaults to 16. Pass a value to override.
23
+ * Native header height, from `useHeaderHeight()` in
24
+ * `expo-router/react-navigation`. Used to scroll an error's field clear of
25
+ * the header. Defaults to the status bar plus a standard bar.
26
+ */
27
+ headerHeight?: number;
28
+ /**
29
+ * Horizontal inset for the scroll content and footer.
30
+ * Defaults to theme `spacing.screen`. Pass a value to override.
25
31
  */
26
32
  paddingHorizontal?: number;
27
- /** Defaults to theme role `background`. */
33
+ /** Defaults to theme role `formBackground`. */
28
34
  backgroundColor?: string;
29
- /** Defaults to theme role `text`. */
30
- titleColor?: string;
31
- /** Defaults to theme role `surface`. Footer ground. */
35
+ /** Defaults to theme role `formFooterBackground`. Footer ground. */
32
36
  footerBackgroundColor?: string;
33
37
  /** Defaults to theme role `buttonBackground`. */
34
38
  buttonBackgroundColor?: string;