@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
@@ -0,0 +1,279 @@
1
+ import BottomSheet, {
2
+ BottomSheetHandle,
3
+ BottomSheetScrollView,
4
+ type BottomSheetHandleProps,
5
+ type BottomSheetScrollViewMethods,
6
+ } from "@gorhom/bottom-sheet";
7
+ import { useCallback, useMemo, useRef, useState } from "react";
8
+ import {
9
+ Platform,
10
+ StyleSheet,
11
+ Text,
12
+ useWindowDimensions,
13
+ View,
14
+ type LayoutChangeEvent,
15
+ } from "react-native";
16
+ import { useKeyboardState } from "react-native-keyboard-controller";
17
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
18
+ import { estimateHeaderHeight } from "../../layout/headerInset";
19
+ import { SHEET_TOP_RADIUS } from "../../layout/sheet";
20
+ import { useThemeSpacing } from "../../theme/useThemeSpacing";
21
+ import {
22
+ ErrorBannerProvider,
23
+ useErrorBanner,
24
+ } from "../ErrorBanner";
25
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
26
+ import { FormErrorBanner } from "../Form/FormErrorBanner";
27
+ import { FormSubmitFooter } from "../Form/FormSubmitFooter";
28
+ import {
29
+ formSheetHandleFallback,
30
+ formSheetSnapPoints,
31
+ sheetBannerOverlap,
32
+ } from "./formSheetLayout";
33
+ import { FORM_SHEET_THEME_ROLES } from "./formSheetThemeRoles";
34
+ import type { FormSheetInset, FormSheetProps, FormSheetSnapIndex } from "./types";
35
+
36
+ const EXPANDED: FormSheetSnapIndex = 2;
37
+
38
+ function useMeasuredHeight(initial = 0) {
39
+ const [height, setHeight] = useState(initial);
40
+ const onLayout = useCallback((event: LayoutChangeEvent) => {
41
+ const next = Math.round(event.nativeEvent.layout.height);
42
+ setHeight((current) => (current === next ? current : next));
43
+ }, []);
44
+ return [height, onLayout] as const;
45
+ }
46
+
47
+ /**
48
+ * A form in a draggable sheet over a full-screen background, usually a map.
49
+ * Three stops: the handle and title, half the screen, and up to the native
50
+ * header. The submit button stays above the keyboard. Set the close button
51
+ * in the screen's stack options, with `headerTransparent: true` so the
52
+ * background shows behind the header.
53
+ */
54
+ export function FormSheet({
55
+ title,
56
+ children,
57
+ background,
58
+ onSubmit,
59
+ errors = [],
60
+ submitLabel = "Save",
61
+ submitting = false,
62
+ submitDisabled = false,
63
+ submitRadius = "lg",
64
+ initialIndex = 0,
65
+ headerHeight: headerHeightProp,
66
+ paddingHorizontal: paddingHorizontalProp,
67
+ backgroundColor: backgroundColorProp,
68
+ titleColor: titleColorProp,
69
+ footerBackgroundColor: footerBackgroundColorProp,
70
+ handleColor: handleColorProp,
71
+ buttonBackgroundColor,
72
+ buttonLabelColor,
73
+ }: FormSheetProps) {
74
+ const insets = useSafeAreaInsets();
75
+ const spacing = useThemeSpacing();
76
+ const paddingHorizontal = paddingHorizontalProp ?? spacing.screen;
77
+ const { height: windowHeight } = useWindowDimensions();
78
+ const backgroundColor = useResolvedFieldColor(
79
+ backgroundColorProp,
80
+ FORM_SHEET_THEME_ROLES.backgroundColor,
81
+ );
82
+ const titleColor = useResolvedFieldColor(
83
+ titleColorProp,
84
+ FORM_SHEET_THEME_ROLES.titleColor,
85
+ );
86
+ const footerBackground = useResolvedFieldColor(
87
+ footerBackgroundColorProp,
88
+ FORM_SHEET_THEME_ROLES.footerBackgroundColor,
89
+ );
90
+ const handleColor = useResolvedFieldColor(
91
+ handleColorProp,
92
+ FORM_SHEET_THEME_ROLES.handleColor,
93
+ );
94
+
95
+ const keyboardHeight = useKeyboardState((state) => state.height);
96
+ const keyboardLift = Math.max(0, keyboardHeight - insets.bottom);
97
+
98
+ const [footerHeight, onFooterLayout] = useMeasuredHeight();
99
+ const [handleHeight, onHandleLayout] = useMeasuredHeight(
100
+ formSheetHandleFallback(Boolean(title)),
101
+ );
102
+ const [sheetAreaHeight, onSheetAreaLayout] = useMeasuredHeight();
103
+ const [snapIndex, setSnapIndex] = useState<FormSheetSnapIndex>(initialIndex);
104
+
105
+ // Estimates until the first layout pass reports real sizes.
106
+ const resolvedHeader =
107
+ headerHeightProp ?? estimateHeaderHeight(Platform.OS, insets.top);
108
+ const resolvedFooter = footerHeight || insets.bottom + 76;
109
+ const resolvedArea =
110
+ sheetAreaHeight || Math.max(0, windowHeight - resolvedFooter - keyboardLift);
111
+
112
+ const snapPoints = useMemo(
113
+ () =>
114
+ formSheetSnapPoints({
115
+ windowHeight,
116
+ sheetAreaHeight: resolvedArea,
117
+ headerHeight: resolvedHeader,
118
+ handleHeight,
119
+ }),
120
+ [handleHeight, resolvedArea, resolvedHeader, windowHeight],
121
+ );
122
+
123
+ const sheetRef = useRef<BottomSheet>(null);
124
+ const scrollRef = useRef<BottomSheetScrollViewMethods>(null);
125
+ const contentRef = useRef<View>(null);
126
+ const bannerRawHeightRef = useRef(0);
127
+ const bannerOverlapRef = useRef(0);
128
+ const scrollTopRef = useRef(0);
129
+ scrollTopRef.current = resolvedHeader + handleHeight;
130
+ const banner = useErrorBanner(errors);
131
+
132
+ // A banner row expands the sheet first, so the field has room to show.
133
+ const expandForJump = useCallback(() => {
134
+ sheetRef.current?.snapToIndex(EXPANDED);
135
+ bannerOverlapRef.current = sheetBannerOverlap(
136
+ bannerRawHeightRef.current,
137
+ scrollTopRef.current,
138
+ );
139
+ }, []);
140
+
141
+ const renderHandle = useCallback(
142
+ (props: BottomSheetHandleProps) => (
143
+ <View
144
+ onLayout={onHandleLayout}
145
+ style={[styles.handle, { backgroundColor, paddingHorizontal }]}
146
+ >
147
+ <BottomSheetHandle
148
+ {...props}
149
+ indicatorStyle={{ backgroundColor: handleColor }}
150
+ />
151
+ {title ? (
152
+ <Text
153
+ accessibilityRole="header"
154
+ numberOfLines={1}
155
+ style={[styles.title, { color: titleColor }]}
156
+ >
157
+ {title}
158
+ </Text>
159
+ ) : null}
160
+ </View>
161
+ ),
162
+ [backgroundColor, handleColor, onHandleLayout, paddingHorizontal, title, titleColor],
163
+ );
164
+
165
+ const inset: FormSheetInset = {
166
+ top: resolvedHeader,
167
+ bottom: resolvedFooter + snapPoints[snapIndex],
168
+ bottomAtHalf: resolvedFooter + snapPoints[1],
169
+ };
170
+
171
+ return (
172
+ <ErrorBannerProvider
173
+ bannerHeightRef={bannerOverlapRef}
174
+ errors={errors}
175
+ measureRef={contentRef}
176
+ onBeforeJump={expandForJump}
177
+ onDismiss={banner.dismiss}
178
+ scrollRef={scrollRef}
179
+ >
180
+ <View style={[styles.screen, { backgroundColor }]}>
181
+ <View pointerEvents="box-none" style={StyleSheet.absoluteFill}>
182
+ {background(inset)}
183
+ </View>
184
+
185
+ <View
186
+ onLayout={onSheetAreaLayout}
187
+ pointerEvents="box-none"
188
+ style={[styles.sheetArea, { marginBottom: keyboardLift }]}
189
+ >
190
+ <BottomSheet
191
+ ref={sheetRef}
192
+ animateOnMount={false}
193
+ index={initialIndex}
194
+ backgroundStyle={[styles.sheet, { backgroundColor }]}
195
+ enableContentPanningGesture
196
+ enableDynamicSizing={false}
197
+ keyboardBehavior="extend"
198
+ enablePanDownToClose={false}
199
+ handleComponent={renderHandle}
200
+ onChange={(next: number) => {
201
+ // -1 is closed; the sheet only has three stops.
202
+ if (next === 0 || next === 1 || next === 2) {
203
+ setSnapIndex(next);
204
+ }
205
+ }}
206
+ snapPoints={snapPoints}
207
+ >
208
+ <BottomSheetScrollView
209
+ ref={scrollRef}
210
+ contentContainerStyle={[styles.content, { paddingHorizontal }]}
211
+ keyboardShouldPersistTaps="handled"
212
+ >
213
+ {/* Field positions are measured from here: the top of the content. */}
214
+ <View ref={contentRef} collapsable={false} style={styles.contentTop}>
215
+ {children}
216
+ </View>
217
+ </BottomSheetScrollView>
218
+ </BottomSheet>
219
+ </View>
220
+
221
+ <FormSubmitFooter
222
+ backgroundColor={footerBackground}
223
+ buttonBackgroundColor={buttonBackgroundColor}
224
+ buttonLabelColor={buttonLabelColor}
225
+ disabled={submitDisabled}
226
+ label={submitLabel}
227
+ onLayout={onFooterLayout}
228
+ onPress={() => {
229
+ banner.present();
230
+ onSubmit();
231
+ }}
232
+ paddingHorizontal={paddingHorizontal}
233
+ radius={submitRadius}
234
+ submitting={submitting}
235
+ />
236
+
237
+ <FormErrorBanner
238
+ androidTop={resolvedHeader}
239
+ errors={errors}
240
+ onDismiss={banner.dismiss}
241
+ onHeightChange={(height) => {
242
+ bannerRawHeightRef.current = height;
243
+ }}
244
+ open={banner.open}
245
+ />
246
+ </View>
247
+ </ErrorBannerProvider>
248
+ );
249
+ }
250
+
251
+ const styles = StyleSheet.create({
252
+ screen: {
253
+ flex: 1,
254
+ },
255
+ sheetArea: {
256
+ flex: 1,
257
+ },
258
+ sheet: {
259
+ borderTopLeftRadius: SHEET_TOP_RADIUS,
260
+ borderTopRightRadius: SHEET_TOP_RADIUS,
261
+ },
262
+ handle: {
263
+ alignItems: "center",
264
+ paddingBottom: 12,
265
+ borderTopLeftRadius: SHEET_TOP_RADIUS,
266
+ borderTopRightRadius: SHEET_TOP_RADIUS,
267
+ },
268
+ title: {
269
+ fontSize: 17,
270
+ fontWeight: "600",
271
+ textAlign: "center",
272
+ },
273
+ content: {
274
+ paddingBottom: 24,
275
+ },
276
+ contentTop: {
277
+ paddingTop: 8,
278
+ },
279
+ });
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ FORM_SHEET_HANDLE_FALLBACK,
5
+ formSheetHandleFallback,
6
+ formSheetSnapPoints,
7
+ sheetBannerOverlap,
8
+ } from "./formSheetLayout.ts";
9
+
10
+ describe("formSheetSnapPoints", () => {
11
+ it("returns the handle, half the screen, and up to the header", () => {
12
+ const [collapsed, half, full] = formSheetSnapPoints({
13
+ windowHeight: 800,
14
+ sheetAreaHeight: 720,
15
+ headerHeight: 100,
16
+ handleHeight: 72,
17
+ });
18
+ assert.equal(collapsed, 72);
19
+ assert.equal(half, 320);
20
+ assert.equal(full, 620);
21
+ });
22
+
23
+ it("keeps half between the handle and the header when space is tight", () => {
24
+ const [collapsed, half, full] = formSheetSnapPoints({
25
+ windowHeight: 800,
26
+ sheetAreaHeight: 420,
27
+ headerHeight: 100,
28
+ handleHeight: 72,
29
+ });
30
+ assert.equal(collapsed, 72);
31
+ assert.ok(half > collapsed);
32
+ assert.ok(half < full);
33
+ assert.equal(full, 320);
34
+ });
35
+
36
+ it("always returns three increasing heights, even with no room", () => {
37
+ const [collapsed, half, full] = formSheetSnapPoints({
38
+ windowHeight: 800,
39
+ sheetAreaHeight: 80,
40
+ headerHeight: 100,
41
+ handleHeight: 72,
42
+ });
43
+ assert.ok(collapsed < half && half < full);
44
+ });
45
+ });
46
+
47
+ describe("sheetBannerOverlap", () => {
48
+ it("is how far the banner reaches into the expanded sheet's scroll area", () => {
49
+ assert.equal(sheetBannerOverlap(180, 150), 30);
50
+ });
51
+
52
+ it("is zero when the banner ends above the scroll area", () => {
53
+ assert.equal(sheetBannerOverlap(120, 150), 0);
54
+ });
55
+ });
56
+
57
+ describe("formSheetHandleFallback", () => {
58
+ it("leaves room for the title line before the handle is measured", () => {
59
+ assert.equal(formSheetHandleFallback(true), FORM_SHEET_HANDLE_FALLBACK.withTitle);
60
+ assert.ok(FORM_SHEET_HANDLE_FALLBACK.withTitle > FORM_SHEET_HANDLE_FALLBACK.indicatorOnly);
61
+ });
62
+
63
+ it("is just the indicator without a title", () => {
64
+ assert.equal(formSheetHandleFallback(false), FORM_SHEET_HANDLE_FALLBACK.indicatorOnly);
65
+ });
66
+ });
@@ -0,0 +1,55 @@
1
+ export type FormSheetSnapInput = {
2
+ /** Full window height, used to place the middle stop at half the screen. */
3
+ windowHeight: number;
4
+ /** Height of the sheet's parent: the screen above the footer and keyboard. */
5
+ sheetAreaHeight: number;
6
+ /** Header block the expanded sheet stops beneath. */
7
+ headerHeight: number;
8
+ /** Collapsed sheet: drag handle plus title. */
9
+ handleHeight: number;
10
+ };
11
+
12
+ /**
13
+ * Three sheet heights, shortest first. Minimum is the handle. Middle puts the
14
+ * sheet top at half the screen. Maximum stops at the bottom of the header.
15
+ * Pure so it can be unit-tested.
16
+ */
17
+ export function formSheetSnapPoints({
18
+ windowHeight,
19
+ sheetAreaHeight,
20
+ headerHeight,
21
+ handleHeight,
22
+ }: FormSheetSnapInput): [number, number, number] {
23
+ const collapsed = Math.max(1, Math.round(handleHeight));
24
+ const full = Math.max(collapsed, Math.round(sheetAreaHeight - headerHeight));
25
+ const halfRaw = Math.round(sheetAreaHeight - windowHeight / 2);
26
+ // The sheet needs three distinct, increasing stops even when there is
27
+ // almost no room, so nudge each one above the last.
28
+ const half = Math.max(collapsed + 1, Math.min(full, halfRaw));
29
+ const top = Math.max(half + 1, full);
30
+ return [collapsed, half, top];
31
+ }
32
+
33
+ /**
34
+ * How far the error banner reaches into the expanded sheet's scroll area,
35
+ * whose top sits `scrollTop` below the screen top (header plus handle).
36
+ * A banner row scrolls its field this much further so it is not hidden.
37
+ */
38
+ export function sheetBannerOverlap(bannerHeight: number, scrollTop: number): number {
39
+ return Math.max(0, bannerHeight - scrollTop);
40
+ }
41
+
42
+ /** Handle heights before the first layout pass measures the real one. */
43
+ export const FORM_SHEET_HANDLE_FALLBACK = {
44
+ /** Drag indicator and padding. */
45
+ indicatorOnly: 36,
46
+ /** Indicator plus one title line. */
47
+ withTitle: 64,
48
+ } as const;
49
+
50
+ /** The collapsed stop is the handle, so it must fit the title when there is one. */
51
+ export function formSheetHandleFallback(hasTitle: boolean): number {
52
+ return hasTitle
53
+ ? FORM_SHEET_HANDLE_FALLBACK.withTitle
54
+ : FORM_SHEET_HANDLE_FALLBACK.indicatorOnly;
55
+ }
@@ -0,0 +1,14 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { FORM_SHEET_THEME_ROLES } from "./formSheetThemeRoles.ts";
4
+
5
+ describe("FORM_SHEET_THEME_ROLES", () => {
6
+ it("maps colour props to formSheet-prefixed roles", () => {
7
+ assert.deepEqual(FORM_SHEET_THEME_ROLES, {
8
+ backgroundColor: "formSheetBackground",
9
+ titleColor: "formSheetTitle",
10
+ footerBackgroundColor: "formSheetFooterBackground",
11
+ handleColor: "formSheetHandle",
12
+ });
13
+ });
14
+ });
@@ -0,0 +1,8 @@
1
+ export const FORM_SHEET_THEME_ROLES = {
2
+ backgroundColor: "formSheetBackground",
3
+ titleColor: "formSheetTitle",
4
+ footerBackgroundColor: "formSheetFooterBackground",
5
+ handleColor: "formSheetHandle",
6
+ } as const;
7
+
8
+ export type FormSheetThemeColorProp = keyof typeof FORM_SHEET_THEME_ROLES;
@@ -0,0 +1,7 @@
1
+ export { FormSheet } from "./FormSheet";
2
+ export { formSheetSnapPoints, type FormSheetSnapInput } from "./formSheetLayout";
3
+ export {
4
+ FORM_SHEET_THEME_ROLES,
5
+ type FormSheetThemeColorProp,
6
+ } from "./formSheetThemeRoles";
7
+ export type { FormSheetInset, FormSheetProps, FormSheetSnapIndex } from "./types";
@@ -0,0 +1,66 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ButtonRadius } from "../Button/buttonRadius";
3
+ import type { FieldError } from "../ErrorBanner/types";
4
+
5
+ /** Where the chrome sits over the background layer, in points from the screen edges. */
6
+ export type FormSheetInset = {
7
+ /** Bottom of the native header. */
8
+ top: number;
9
+ /** Top of the sheet at its current stop, measured from the screen bottom. */
10
+ bottom: number;
11
+ /** Top of the sheet at its middle stop. Steady while dragging, e.g. for map camera padding. */
12
+ bottomAtHalf: number;
13
+ };
14
+
15
+ export type FormSheetSnapIndex = 0 | 1 | 2;
16
+
17
+ /**
18
+ * Set the close button in the screen's stack options (`headerLeft`), with
19
+ * `headerTransparent: true`. The form's title lives on the sheet (`title`).
20
+ */
21
+ export type FormSheetProps = {
22
+ /**
23
+ * Shown at the top of the sheet, under the drag indicator. The lowest stop
24
+ * always shows it.
25
+ */
26
+ title?: string;
27
+ children: ReactNode;
28
+ /**
29
+ * Full-screen layer behind the sheet, usually a map. Receives the chrome
30
+ * insets so it can keep its own controls clear of the header and sheet.
31
+ */
32
+ background: (inset: FormSheetInset) => ReactNode;
33
+ onSubmit: () => void;
34
+ /**
35
+ * Field errors for the screen banner. Empty hides it. A banner row
36
+ * expands the sheet and scrolls to the field. Editing a field dismisses
37
+ * the banner; the next submit shows it again.
38
+ */
39
+ errors?: readonly FieldError[];
40
+ submitLabel?: string;
41
+ submitting?: boolean;
42
+ submitDisabled?: boolean;
43
+ submitRadius?: ButtonRadius;
44
+ /** Stop on first paint: `0` the handle and title, `1` half the screen, `2` up to the header. Default `0`. */
45
+ initialIndex?: FormSheetSnapIndex;
46
+ /**
47
+ * Native header height, from `useHeaderHeight()` in
48
+ * `expo-router/react-navigation`. The top stop ends here. Defaults to the
49
+ * status bar plus a standard bar.
50
+ */
51
+ headerHeight?: number;
52
+ /** Side inset for the sheet content and footer. Defaults to theme `spacing.screen`. */
53
+ paddingHorizontal?: number;
54
+ /** Sheet fill. Defaults to theme role `formSheetBackground`. */
55
+ backgroundColor?: string;
56
+ /** Sheet title. Defaults to theme role `formSheetTitle`. */
57
+ titleColor?: string;
58
+ /** Footer ground. Defaults to theme role `formSheetFooterBackground`. */
59
+ footerBackgroundColor?: string;
60
+ /** Drag indicator. Defaults to theme role `formSheetHandle`. */
61
+ handleColor?: string;
62
+ /** Defaults to theme role `buttonBackground`. */
63
+ buttonBackgroundColor?: string;
64
+ /** Defaults to theme role `buttonLabel`. */
65
+ buttonLabelColor?: string;
66
+ };
@@ -0,0 +1,63 @@
1
+ import { Platform, Pressable, StyleSheet, Text } from "react-native";
2
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
3
+ import {
4
+ HEADER_BUTTON_SIZE,
5
+ HEADER_CAPSULE_FONT_SIZE,
6
+ HEADER_CAPSULE_ICON_SIZE,
7
+ HEADER_HIT_SLOP,
8
+ headerFallbackStyle,
9
+ supportsLiquidGlass,
10
+ } from "./headerButtonLayout";
11
+ import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles";
12
+ import type { HeaderCapsuleProps } from "./types";
13
+
14
+ const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
15
+
16
+ /** Header button with a label (and optional icon). Wider than tall, so iOS 26+ draws a glass pill. */
17
+ export function HeaderCapsule({
18
+ icon,
19
+ label,
20
+ onPress,
21
+ iconSize = HEADER_CAPSULE_ICON_SIZE,
22
+ fontSize = HEADER_CAPSULE_FONT_SIZE,
23
+ disabled = false,
24
+ color: colorProp,
25
+ fallbackBackgroundColor,
26
+ hitSlop = HEADER_HIT_SLOP,
27
+ style,
28
+ }: HeaderCapsuleProps) {
29
+ const color = useResolvedFieldColor(colorProp, HEADER_BUTTON_THEME_ROLES.color);
30
+
31
+ return (
32
+ <Pressable
33
+ onPress={onPress}
34
+ disabled={disabled}
35
+ hitSlop={hitSlop}
36
+ accessibilityRole="button"
37
+ accessibilityLabel={label}
38
+ accessibilityState={{ disabled }}
39
+ style={({ pressed }) => [
40
+ styles.capsule,
41
+ headerFallbackStyle(GLASS, fallbackBackgroundColor),
42
+ { opacity: disabled ? 0.4 : pressed ? 0.5 : 1 },
43
+ style,
44
+ ]}
45
+ >
46
+ {icon?.({ color, size: iconSize })}
47
+ <Text style={[styles.label, { color, fontSize }]}>{label}</Text>
48
+ </Pressable>
49
+ );
50
+ }
51
+
52
+ const styles = StyleSheet.create({
53
+ capsule: {
54
+ flexDirection: "row",
55
+ alignItems: "center",
56
+ gap: 6,
57
+ height: HEADER_BUTTON_SIZE,
58
+ paddingHorizontal: 12,
59
+ },
60
+ label: {
61
+ fontWeight: "600",
62
+ },
63
+ });
@@ -0,0 +1,36 @@
1
+ import { Platform, StyleSheet, View } from "react-native";
2
+ import {
3
+ HEADER_GROUP_GAP,
4
+ headerFallbackStyle,
5
+ supportsLiquidGlass,
6
+ } from "./headerButtonLayout";
7
+ import type { HeaderGroupProps } from "./types";
8
+
9
+ const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
10
+
11
+ /** Row of header buttons that share one glass capsule on iOS 26+. */
12
+ export function HeaderGroup({
13
+ children,
14
+ fallbackBackgroundColor,
15
+ style,
16
+ }: HeaderGroupProps) {
17
+ return (
18
+ <View
19
+ style={[
20
+ styles.row,
21
+ headerFallbackStyle(GLASS, fallbackBackgroundColor),
22
+ style,
23
+ ]}
24
+ >
25
+ {children}
26
+ </View>
27
+ );
28
+ }
29
+
30
+ const styles = StyleSheet.create({
31
+ row: {
32
+ flexDirection: "row",
33
+ alignItems: "center",
34
+ gap: HEADER_GROUP_GAP,
35
+ },
36
+ });
@@ -0,0 +1,56 @@
1
+ import { Platform, Pressable, StyleSheet } from "react-native";
2
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
3
+ import {
4
+ HEADER_BUTTON_SIZE,
5
+ HEADER_HIT_SLOP,
6
+ HEADER_ICON_SIZE,
7
+ headerFallbackStyle,
8
+ supportsLiquidGlass,
9
+ } from "./headerButtonLayout";
10
+ import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles";
11
+ import type { HeaderIconProps } from "./types";
12
+
13
+ const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
14
+
15
+ /** Icon-only header button. A 36×36 box becomes a glass circle on iOS 26+. */
16
+ export function HeaderIcon({
17
+ icon,
18
+ label,
19
+ onPress,
20
+ size = HEADER_ICON_SIZE,
21
+ disabled = false,
22
+ color: colorProp,
23
+ fallbackBackgroundColor,
24
+ hitSlop = HEADER_HIT_SLOP,
25
+ style,
26
+ }: HeaderIconProps) {
27
+ const color = useResolvedFieldColor(colorProp, HEADER_BUTTON_THEME_ROLES.color);
28
+
29
+ return (
30
+ <Pressable
31
+ onPress={onPress}
32
+ disabled={disabled}
33
+ hitSlop={hitSlop}
34
+ accessibilityRole="button"
35
+ accessibilityLabel={label}
36
+ accessibilityState={{ disabled }}
37
+ style={({ pressed }) => [
38
+ styles.box,
39
+ headerFallbackStyle(GLASS, fallbackBackgroundColor),
40
+ { opacity: disabled ? 0.4 : pressed ? 0.5 : 1 },
41
+ style,
42
+ ]}
43
+ >
44
+ {icon({ color, size })}
45
+ </Pressable>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ box: {
51
+ width: HEADER_BUTTON_SIZE,
52
+ height: HEADER_BUTTON_SIZE,
53
+ alignItems: "center",
54
+ justifyContent: "center",
55
+ },
56
+ });