@studio-piot/native-ui 0.2.4 → 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 (341) 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/SegmentedControl/types.d.ts +0 -1
  176. package/dist/components/SegmentedControl/types.d.ts.map +1 -1
  177. package/dist/components/Select/Select.d.ts +1 -1
  178. package/dist/components/Select/Select.d.ts.map +1 -1
  179. package/dist/components/Select/Select.js +8 -9
  180. package/dist/components/Select/Select.js.map +1 -1
  181. package/dist/components/Select/selectThemeRoles.d.ts +2 -0
  182. package/dist/components/Select/selectThemeRoles.d.ts.map +1 -1
  183. package/dist/components/Select/selectThemeRoles.js +2 -0
  184. package/dist/components/Select/selectThemeRoles.js.map +1 -1
  185. package/dist/components/Select/types.d.ts +4 -0
  186. package/dist/components/Select/types.d.ts.map +1 -1
  187. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts +8 -0
  188. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts.map +1 -0
  189. package/dist/components/TabAccessoryButton/TabAccessoryButton.js +77 -0
  190. package/dist/components/TabAccessoryButton/TabAccessoryButton.js.map +1 -0
  191. package/dist/components/TabAccessoryButton/index.d.ts +5 -0
  192. package/dist/components/TabAccessoryButton/index.d.ts.map +1 -0
  193. package/dist/components/TabAccessoryButton/index.js +11 -0
  194. package/dist/components/TabAccessoryButton/index.js.map +1 -0
  195. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts +27 -0
  196. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts.map +1 -0
  197. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js +38 -0
  198. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js.map +1 -0
  199. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts +14 -0
  200. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts.map +1 -0
  201. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js +19 -0
  202. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js.map +1 -0
  203. package/dist/components/TabAccessoryButton/types.d.ts +27 -0
  204. package/dist/components/TabAccessoryButton/types.d.ts.map +1 -0
  205. package/dist/components/TabAccessoryButton/types.js +3 -0
  206. package/dist/components/TabAccessoryButton/types.js.map +1 -0
  207. package/dist/components/index.d.ts +9 -3
  208. package/dist/components/index.d.ts.map +1 -1
  209. package/dist/components/index.js +30 -1
  210. package/dist/components/index.js.map +1 -1
  211. package/dist/index.d.ts +11 -4
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +34 -2
  214. package/dist/index.js.map +1 -1
  215. package/dist/layout/HeaderInsetProbe.d.ts +15 -0
  216. package/dist/layout/HeaderInsetProbe.d.ts.map +1 -0
  217. package/dist/layout/HeaderInsetProbe.js +28 -0
  218. package/dist/layout/HeaderInsetProbe.js.map +1 -0
  219. package/dist/layout/headerInset.d.ts +26 -0
  220. package/dist/layout/headerInset.d.ts.map +1 -0
  221. package/dist/layout/headerInset.js +29 -0
  222. package/dist/layout/headerInset.js.map +1 -0
  223. package/dist/layout/index.d.ts +1 -0
  224. package/dist/layout/index.d.ts.map +1 -1
  225. package/dist/layout/index.js +3 -1
  226. package/dist/layout/index.js.map +1 -1
  227. package/dist/layout/sheet.d.ts +3 -0
  228. package/dist/layout/sheet.d.ts.map +1 -0
  229. package/dist/layout/sheet.js +6 -0
  230. package/dist/layout/sheet.js.map +1 -0
  231. package/dist/theme/ThemeProvider.d.ts +4 -1
  232. package/dist/theme/ThemeProvider.d.ts.map +1 -1
  233. package/dist/theme/ThemeProvider.js +5 -2
  234. package/dist/theme/ThemeProvider.js.map +1 -1
  235. package/dist/theme/index.d.ts +2 -0
  236. package/dist/theme/index.d.ts.map +1 -1
  237. package/dist/theme/index.js +6 -1
  238. package/dist/theme/index.js.map +1 -1
  239. package/dist/theme/resolveThemeSpacing.d.ts +12 -0
  240. package/dist/theme/resolveThemeSpacing.d.ts.map +1 -0
  241. package/dist/theme/resolveThemeSpacing.js +14 -0
  242. package/dist/theme/resolveThemeSpacing.js.map +1 -0
  243. package/dist/theme/types.d.ts +2 -0
  244. package/dist/theme/types.d.ts.map +1 -1
  245. package/dist/theme/useThemeSpacing.d.ts +4 -0
  246. package/dist/theme/useThemeSpacing.d.ts.map +1 -0
  247. package/dist/theme/useThemeSpacing.js +11 -0
  248. package/dist/theme/useThemeSpacing.js.map +1 -0
  249. package/docs/components/button.md +92 -38
  250. package/docs/components/choice.md +88 -0
  251. package/docs/components/error-banner.md +46 -3
  252. package/docs/components/form-sheet.md +140 -0
  253. package/docs/components/form.md +80 -45
  254. package/docs/components/header-button.md +310 -0
  255. package/docs/components/header-wash.md +40 -0
  256. package/docs/components/page.md +183 -0
  257. package/docs/components/select.md +2 -2
  258. package/docs/components/tab-accessory-button.md +146 -0
  259. package/docs/specifications/Liquid Glass header buttons in Expo Router.md +327 -0
  260. package/package.json +1 -1
  261. package/src/components/Button/Button.tsx +82 -38
  262. package/src/components/Button/buttonAppearance.test.ts +44 -0
  263. package/src/components/Button/buttonAppearance.ts +38 -0
  264. package/src/components/Button/buttonThemeRoles.test.ts +10 -0
  265. package/src/components/Button/buttonThemeRoles.ts +6 -0
  266. package/src/components/Button/index.ts +12 -1
  267. package/src/components/Choice/Choice.tsx +109 -0
  268. package/src/components/Choice/choiceLayout.test.ts +14 -0
  269. package/src/components/Choice/choiceLayout.ts +9 -0
  270. package/src/components/Choice/choiceThemeRoles.test.ts +14 -0
  271. package/src/components/Choice/choiceThemeRoles.ts +8 -0
  272. package/src/components/Choice/index.ts +4 -0
  273. package/src/components/Choice/types.ts +26 -0
  274. package/src/components/ErrorBanner/ErrorBanner.tsx +32 -22
  275. package/src/components/ErrorBanner/errorBannerContext.tsx +31 -6
  276. package/src/components/ErrorBanner/errorBannerLayout.test.ts +49 -0
  277. package/src/components/ErrorBanner/errorBannerLayout.ts +59 -0
  278. package/src/components/ErrorBanner/icons.tsx +25 -6
  279. package/src/components/ErrorBanner/index.ts +1 -0
  280. package/src/components/Form/Form.tsx +77 -173
  281. package/src/components/Form/FormCloseButton.tsx +12 -4
  282. package/src/components/Form/FormErrorBanner.tsx +57 -0
  283. package/src/components/Form/FormSubmitFooter.tsx +70 -0
  284. package/src/components/Form/formThemeRoles.test.ts +13 -0
  285. package/src/components/Form/formThemeRoles.ts +7 -0
  286. package/src/components/Form/index.ts +1 -0
  287. package/src/components/Form/types.ts +14 -10
  288. package/src/components/FormSheet/FormSheet.tsx +279 -0
  289. package/src/components/FormSheet/formSheetLayout.test.ts +66 -0
  290. package/src/components/FormSheet/formSheetLayout.ts +55 -0
  291. package/src/components/FormSheet/formSheetThemeRoles.test.ts +14 -0
  292. package/src/components/FormSheet/formSheetThemeRoles.ts +8 -0
  293. package/src/components/FormSheet/index.ts +7 -0
  294. package/src/components/FormSheet/types.ts +66 -0
  295. package/src/components/HeaderButton/HeaderCapsule.tsx +63 -0
  296. package/src/components/HeaderButton/HeaderGroup.tsx +36 -0
  297. package/src/components/HeaderButton/HeaderIcon.tsx +56 -0
  298. package/src/components/HeaderButton/headerButtonLayout.test.ts +47 -0
  299. package/src/components/HeaderButton/headerButtonLayout.ts +36 -0
  300. package/src/components/HeaderButton/headerButtonThemeRoles.test.ts +9 -0
  301. package/src/components/HeaderButton/headerButtonThemeRoles.ts +5 -0
  302. package/src/components/HeaderButton/index.ts +14 -0
  303. package/src/components/HeaderButton/types.ts +40 -0
  304. package/src/components/HeaderWash/HeaderWash.tsx +20 -0
  305. package/src/components/HeaderWash/headerWashGradient.test.ts +22 -0
  306. package/src/components/{Form/withAlpha.ts → HeaderWash/headerWashGradient.ts} +9 -0
  307. package/src/components/HeaderWash/index.ts +1 -0
  308. package/src/components/Page/Page.tsx +80 -0
  309. package/src/components/Page/PageBleed.tsx +14 -0
  310. package/src/components/Page/index.ts +4 -0
  311. package/src/components/Page/pageContext.ts +8 -0
  312. package/src/components/Page/pageInsets.test.ts +73 -0
  313. package/src/components/Page/pageInsets.ts +52 -0
  314. package/src/components/Page/pageThemeRoles.test.ts +9 -0
  315. package/src/components/Page/pageThemeRoles.ts +5 -0
  316. package/src/components/Page/types.ts +26 -0
  317. package/src/components/SegmentedControl/types.ts +0 -1
  318. package/src/components/Select/Select.tsx +11 -4
  319. package/src/components/Select/selectThemeRoles.test.ts +5 -0
  320. package/src/components/Select/selectThemeRoles.ts +2 -0
  321. package/src/components/Select/types.ts +4 -0
  322. package/src/components/TabAccessoryButton/TabAccessoryButton.tsx +150 -0
  323. package/src/components/TabAccessoryButton/index.ts +12 -0
  324. package/src/components/TabAccessoryButton/tabAccessoryLayout.test.ts +42 -0
  325. package/src/components/TabAccessoryButton/tabAccessoryLayout.ts +43 -0
  326. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.test.ts +28 -0
  327. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.ts +22 -0
  328. package/src/components/TabAccessoryButton/types.ts +27 -0
  329. package/src/components/index.ts +64 -2
  330. package/src/index.ts +61 -2
  331. package/src/layout/headerInset.test.ts +60 -0
  332. package/src/layout/headerInset.ts +41 -0
  333. package/src/layout/index.ts +1 -0
  334. package/src/layout/sheet.ts +2 -0
  335. package/src/theme/ThemeProvider.tsx +9 -1
  336. package/src/theme/index.ts +6 -0
  337. package/src/theme/resolveThemeSpacing.test.ts +29 -0
  338. package/src/theme/resolveThemeSpacing.ts +21 -0
  339. package/src/theme/types.ts +3 -0
  340. package/src/theme/useThemeSpacing.ts +8 -0
  341. package/src/components/Form/withAlpha.test.ts +0 -13
@@ -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
+ });
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ HEADER_BUTTON_SIZE,
5
+ headerFallbackStyle,
6
+ supportsLiquidGlass,
7
+ } from "./headerButtonLayout.ts";
8
+
9
+ describe("supportsLiquidGlass", () => {
10
+ it("is true on iOS 26 and later", () => {
11
+ assert.equal(supportsLiquidGlass("ios", "26.0"), true);
12
+ assert.equal(supportsLiquidGlass("ios", "26.1.2"), true);
13
+ assert.equal(supportsLiquidGlass("ios", "27"), true);
14
+ });
15
+
16
+ it("is false on iOS 18 and earlier", () => {
17
+ assert.equal(supportsLiquidGlass("ios", "18.6"), false);
18
+ assert.equal(supportsLiquidGlass("ios", "17.0"), false);
19
+ });
20
+
21
+ it("is false on Android and web whatever the version", () => {
22
+ assert.equal(supportsLiquidGlass("android", 35), false);
23
+ assert.equal(supportsLiquidGlass("web", "26"), false);
24
+ });
25
+
26
+ it("is false when the version cannot be parsed", () => {
27
+ assert.equal(supportsLiquidGlass("ios", ""), false);
28
+ assert.equal(supportsLiquidGlass("ios", "unknown"), false);
29
+ });
30
+ });
31
+
32
+ describe("headerFallbackStyle", () => {
33
+ it("adds nothing on glass platforms, even with a colour", () => {
34
+ assert.equal(headerFallbackStyle(true, "#eee"), undefined);
35
+ });
36
+
37
+ it("adds nothing on other platforms without a colour", () => {
38
+ assert.equal(headerFallbackStyle(false, undefined), undefined);
39
+ });
40
+
41
+ it("draws a pill background on other platforms when a colour is passed", () => {
42
+ assert.deepEqual(headerFallbackStyle(false, "#eee"), {
43
+ backgroundColor: "#eee",
44
+ borderRadius: HEADER_BUTTON_SIZE / 2,
45
+ });
46
+ });
47
+ });
@@ -0,0 +1,36 @@
1
+ /** Height of every header button; a square of this size becomes a glass circle. */
2
+ export const HEADER_BUTTON_SIZE = 36;
3
+ export const HEADER_ICON_SIZE = 17;
4
+ export const HEADER_CAPSULE_ICON_SIZE = 15;
5
+ export const HEADER_CAPSULE_FONT_SIZE = 16;
6
+ export const HEADER_GROUP_GAP = 4;
7
+ export const HEADER_HIT_SLOP = 6;
8
+
9
+ /**
10
+ * Liquid Glass bar button backgrounds exist on iOS 26 and later only.
11
+ * Pass `Platform.OS` and `Platform.Version`. Pure so it can be unit-tested.
12
+ */
13
+ export function supportsLiquidGlass(
14
+ os: string,
15
+ version: string | number,
16
+ ): boolean {
17
+ if (os !== "ios") {
18
+ return false;
19
+ }
20
+ const major = Number.parseInt(String(version), 10);
21
+ return Number.isFinite(major) && major >= 26;
22
+ }
23
+
24
+ /**
25
+ * Background drawn on platforms without glass. On iOS 26+ the system draws
26
+ * the glass, so a background there would give two shapes, one inside the other.
27
+ */
28
+ export function headerFallbackStyle(
29
+ glass: boolean,
30
+ backgroundColor: string | undefined,
31
+ ): { backgroundColor: string; borderRadius: number } | undefined {
32
+ if (glass || backgroundColor == null) {
33
+ return undefined;
34
+ }
35
+ return { backgroundColor, borderRadius: HEADER_BUTTON_SIZE / 2 };
36
+ }
@@ -0,0 +1,9 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { HEADER_BUTTON_THEME_ROLES } from "./headerButtonThemeRoles.ts";
4
+
5
+ describe("HEADER_BUTTON_THEME_ROLES", () => {
6
+ it("maps the colour prop to a dedicated header button role", () => {
7
+ assert.equal(HEADER_BUTTON_THEME_ROLES.color, "headerButton");
8
+ });
9
+ });
@@ -0,0 +1,5 @@
1
+ export const HEADER_BUTTON_THEME_ROLES = {
2
+ color: "headerButton",
3
+ } as const;
4
+
5
+ export type HeaderButtonThemeColorProp = keyof typeof HEADER_BUTTON_THEME_ROLES;
@@ -0,0 +1,14 @@
1
+ export { HeaderCapsule } from "./HeaderCapsule";
2
+ export { HeaderGroup } from "./HeaderGroup";
3
+ export { HeaderIcon } from "./HeaderIcon";
4
+ export { supportsLiquidGlass } from "./headerButtonLayout";
5
+ export {
6
+ HEADER_BUTTON_THEME_ROLES,
7
+ type HeaderButtonThemeColorProp,
8
+ } from "./headerButtonThemeRoles";
9
+ export type {
10
+ HeaderButtonIcon,
11
+ HeaderCapsuleProps,
12
+ HeaderGroupProps,
13
+ HeaderIconProps,
14
+ } from "./types";
@@ -0,0 +1,40 @@
1
+ import type { ReactNode } from "react";
2
+ import type { StyleProp, ViewStyle } from "react-native";
3
+
4
+ /** Renders the icon with the resolved tint colour and size, e.g. an expo-symbols `SymbolView`. */
5
+ export type HeaderButtonIcon = (props: { color: string; size: number }) => ReactNode;
6
+
7
+ type HeaderButtonSharedProps = {
8
+ onPress: () => void;
9
+ disabled?: boolean;
10
+ /** Icon and label colour. Defaults to the `headerButton` theme role. */
11
+ color?: string;
12
+ /** Background on Android and iOS 18 and earlier only. iOS 26+ draws glass instead. */
13
+ fallbackBackgroundColor?: string;
14
+ hitSlop?: number;
15
+ style?: StyleProp<ViewStyle>;
16
+ };
17
+
18
+ export type HeaderIconProps = HeaderButtonSharedProps & {
19
+ icon: HeaderButtonIcon;
20
+ /** Accessibility label. Required: icon-only buttons have no text for VoiceOver. */
21
+ label: string;
22
+ /** Icon box size. Default 17 (range 15–20). */
23
+ size?: number;
24
+ };
25
+
26
+ export type HeaderCapsuleProps = HeaderButtonSharedProps & {
27
+ icon?: HeaderButtonIcon;
28
+ label: string;
29
+ /** Default 15, close to the height of the 16pt label. */
30
+ iconSize?: number;
31
+ /** Default 16. */
32
+ fontSize?: number;
33
+ };
34
+
35
+ export type HeaderGroupProps = {
36
+ children: ReactNode;
37
+ /** Shared background on Android and iOS 18 and earlier only. */
38
+ fallbackBackgroundColor?: string;
39
+ style?: StyleProp<ViewStyle>;
40
+ };
@@ -0,0 +1,20 @@
1
+ import { StyleSheet, View } from "react-native";
2
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
3
+ import { FORM_THEME_ROLES } from "../Form/formThemeRoles";
4
+ import { headerWashGradient } from "./headerWashGradient";
5
+
6
+ /**
7
+ * Background for a transparent native header: a wash from `color` to clear.
8
+ * Use it as the stack's `headerBackground` with `headerTransparent: true`.
9
+ * The stack sizes it to the header and draws it above the screen content,
10
+ * so content fades as it scrolls under the bar.
11
+ */
12
+ export function HeaderWash({ color: colorProp }: { color?: string }) {
13
+ const color = useResolvedFieldColor(colorProp, FORM_THEME_ROLES.backgroundColor);
14
+ return (
15
+ <View
16
+ pointerEvents="none"
17
+ style={[StyleSheet.absoluteFill, { experimental_backgroundImage: headerWashGradient(color) }]}
18
+ />
19
+ );
20
+ }
@@ -0,0 +1,22 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { headerWashGradient, withAlpha } from "./headerWashGradient.ts";
4
+
5
+ describe("headerWashGradient", () => {
6
+ it("runs from the colour, nearly opaque, to clear at the header's bottom", () => {
7
+ assert.equal(
8
+ headerWashGradient("#F9FAFB"),
9
+ "linear-gradient(to bottom, rgba(249, 250, 251, 0.98) 0%, rgba(249, 250, 251, 0.92) 55%, rgba(249, 250, 251, 0) 100%)",
10
+ );
11
+ });
12
+ });
13
+
14
+ describe("withAlpha", () => {
15
+ it("converts a hex colour to rgba", () => {
16
+ assert.equal(withAlpha("#F9FAFB", 0.98), "rgba(249, 250, 251, 0.98)");
17
+ });
18
+
19
+ it("expands three-digit hex", () => {
20
+ assert.equal(withAlpha("#abc", 0.5), "rgba(170, 187, 204, 0.5)");
21
+ });
22
+ });
@@ -7,3 +7,12 @@ export function withAlpha(hex: string, alpha: number): string {
7
7
  const b = Number.parseInt(full.slice(4, 6), 16);
8
8
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
9
9
  }
10
+
11
+ /**
12
+ * The header wash: `color` nearly opaque at the top of the header, fading to
13
+ * clear at its bottom, so content fades out as it scrolls under the bar.
14
+ * Pure so it can be unit-tested.
15
+ */
16
+ export function headerWashGradient(color: string): string {
17
+ return `linear-gradient(to bottom, ${withAlpha(color, 0.98)} 0%, ${withAlpha(color, 0.92)} 55%, ${withAlpha(color, 0)} 100%)`;
18
+ }
@@ -0,0 +1 @@
1
+ export { HeaderWash } from "./HeaderWash";
@@ -0,0 +1,80 @@
1
+ import { Platform, ScrollView, StyleSheet } from "react-native";
2
+ import { useResolvedFieldColor } from "../Field/useResolvedFieldColor";
3
+ import { supportsLiquidGlass } from "../HeaderButton/headerButtonLayout";
4
+ import { TabAccessoryButton } from "../TabAccessoryButton";
5
+ import { tabAccessoryScreenClearance } from "../TabAccessoryButton/tabAccessoryLayout";
6
+ import { useThemeSpacing } from "../../theme/useThemeSpacing";
7
+ import { PageInsetContext } from "./pageContext";
8
+ import { pageContentInset, pagePaddingTop, pageSideInset } from "./pageInsets";
9
+ import { PAGE_THEME_ROLES } from "./pageThemeRoles";
10
+ import type { PageProps } from "./types";
11
+
12
+ const GLASS = supportsLiquidGlass(Platform.OS, Platform.Version);
13
+
14
+ /**
15
+ * Scrolling screen body for a native stack inside native tabs. iOS insets
16
+ * the content for the header (including the large title) and the tab bar,
17
+ * so it starts just below the header, scrolls behind it, and clears the
18
+ * tabs. The title belongs to the header: set it in the stack options.
19
+ *
20
+ * The ScrollView must be a direct child of the screen: react-native-screens
21
+ * only links the header (large title, insets) to a direct child, so there is
22
+ * no wrapper view. Render `Page` as the screen's root element.
23
+ */
24
+ export function Page({
25
+ children,
26
+ tabAccessory,
27
+ backgroundColor: backgroundColorProp,
28
+ padded = true,
29
+ contentUnderHeader,
30
+ contentInset,
31
+ contentContainerStyle,
32
+ style,
33
+ ...scrollProps
34
+ }: PageProps) {
35
+ const backgroundColor = useResolvedFieldColor(
36
+ backgroundColorProp,
37
+ PAGE_THEME_ROLES.backgroundColor,
38
+ );
39
+ const spacing = useThemeSpacing();
40
+ const sideInset = pageSideInset(padded, spacing.screen);
41
+ const paddingBottom =
42
+ (padded ? spacing.pageBottom : 0) +
43
+ (tabAccessory ? tabAccessoryScreenClearance(GLASS, spacing.screen) : 0);
44
+
45
+ return (
46
+ <>
47
+ <ScrollView
48
+ {...scrollProps}
49
+ contentInsetAdjustmentBehavior="automatic"
50
+ contentInset={pageContentInset(Platform.OS, contentUnderHeader, contentInset)}
51
+ style={[styles.screen, { backgroundColor }, style]}
52
+ contentContainerStyle={[
53
+ { paddingHorizontal: sideInset },
54
+ {
55
+ paddingTop: pagePaddingTop({
56
+ padded,
57
+ underHeader: contentUnderHeader != null,
58
+ pageTop: spacing.pageTop,
59
+ }),
60
+ },
61
+ { paddingBottom },
62
+ contentContainerStyle,
63
+ ]}
64
+ >
65
+ <PageInsetContext.Provider value={sideInset}>
66
+ {children}
67
+ </PageInsetContext.Provider>
68
+ </ScrollView>
69
+ {tabAccessory ? (
70
+ <TabAccessoryButton placement="screen" {...tabAccessory} />
71
+ ) : null}
72
+ </>
73
+ );
74
+ }
75
+
76
+ const styles = StyleSheet.create({
77
+ screen: {
78
+ flex: 1,
79
+ },
80
+ });
@@ -0,0 +1,14 @@
1
+ import { View, type StyleProp, type ViewStyle } from "react-native";
2
+ import type { ReactNode } from "react";
3
+ import { usePageSideInset } from "./pageContext";
4
+ import { pageBleedStyle } from "./pageInsets";
5
+
6
+ export type PageBleedProps = {
7
+ children?: ReactNode;
8
+ style?: StyleProp<ViewStyle>;
9
+ };
10
+
11
+ /** Takes one section of a `Page` out to the screen edges, e.g. a full-width image or map. */
12
+ export function PageBleed({ children, style }: PageBleedProps) {
13
+ return <View style={[pageBleedStyle(usePageSideInset()), style]}>{children}</View>;
14
+ }
@@ -0,0 +1,4 @@
1
+ export { Page } from "./Page";
2
+ export { PageBleed, type PageBleedProps } from "./PageBleed";
3
+ export { PAGE_THEME_ROLES, type PageThemeColorProp } from "./pageThemeRoles";
4
+ export type { PageProps, PageTabAccessory } from "./types";
@@ -0,0 +1,8 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ /** Side inset of the nearest `Page` content, for `PageBleed`. */
4
+ export const PageInsetContext = createContext(0);
5
+
6
+ export function usePageSideInset(): number {
7
+ return useContext(PageInsetContext);
8
+ }
@@ -0,0 +1,73 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import {
4
+ pageBleedStyle,
5
+ pageContentInset,
6
+ pagePaddingTop,
7
+ pageSideInset,
8
+ } from "./pageInsets.ts";
9
+
10
+ describe("pageSideInset", () => {
11
+ it("uses the theme screen spacing when padded", () => {
12
+ assert.equal(pageSideInset(true, 16), 16);
13
+ assert.equal(pageSideInset(true, 20), 20);
14
+ });
15
+
16
+ it("is zero when the page is not padded", () => {
17
+ assert.equal(pageSideInset(false, 20), 0);
18
+ });
19
+ });
20
+
21
+ describe("pageBleedStyle", () => {
22
+ it("cancels the page side inset exactly", () => {
23
+ assert.deepEqual(pageBleedStyle(20), { marginHorizontal: -20 });
24
+ });
25
+
26
+ it("adds no margin when there is no inset", () => {
27
+ assert.deepEqual(pageBleedStyle(0), { marginHorizontal: 0 });
28
+ });
29
+ });
30
+
31
+ describe("pageContentInset", () => {
32
+ it("leaves the inset alone without contentUnderHeader", () => {
33
+ assert.equal(pageContentInset("ios", undefined, undefined), undefined);
34
+ const own = { top: 4, bottom: 8 };
35
+ assert.equal(pageContentInset("ios", undefined, own), own);
36
+ });
37
+
38
+ it("cancels only the header clearance on iOS", () => {
39
+ assert.deepEqual(pageContentInset("ios", 96, undefined), {
40
+ top: -96,
41
+ left: 0,
42
+ bottom: 0,
43
+ right: 0,
44
+ });
45
+ });
46
+
47
+ it("adds to an inset the app already set", () => {
48
+ assert.deepEqual(pageContentInset("ios", 96, { top: 10, bottom: 20 }), {
49
+ top: -86,
50
+ left: 0,
51
+ bottom: 20,
52
+ right: 0,
53
+ });
54
+ });
55
+
56
+ it("does nothing on Android, where the header never adds an inset", () => {
57
+ assert.equal(pageContentInset("android", 96, undefined), undefined);
58
+ });
59
+ });
60
+
61
+ describe("pagePaddingTop", () => {
62
+ it("uses the theme gap under the header", () => {
63
+ assert.equal(pagePaddingTop({ padded: true, underHeader: false, pageTop: 16 }), 16);
64
+ });
65
+
66
+ it("starts at the very top when content goes under the header", () => {
67
+ assert.equal(pagePaddingTop({ padded: true, underHeader: true, pageTop: 16 }), 0);
68
+ });
69
+
70
+ it("is zero when not padded", () => {
71
+ assert.equal(pagePaddingTop({ padded: false, underHeader: false, pageTop: 16 }), 0);
72
+ });
73
+ });
@@ -0,0 +1,52 @@
1
+ /** Side padding the page content actually has. Pure so it can be unit-tested. */
2
+ export function pageSideInset(padded: boolean, screen: number): number {
3
+ return padded ? screen : 0;
4
+ }
5
+
6
+ /** Pulls a section out to the screen edges by cancelling the page side inset. */
7
+ export function pageBleedStyle(sideInset: number): { marginHorizontal: number } {
8
+ // `0 - 0` keeps +0 rather than -0, so equality checks stay simple.
9
+ return { marginHorizontal: sideInset === 0 ? 0 : -sideInset };
10
+ }
11
+
12
+ export type PageInsets = {
13
+ top?: number;
14
+ left?: number;
15
+ bottom?: number;
16
+ right?: number;
17
+ };
18
+
19
+ /**
20
+ * iOS adds `contentInset` on top of the automatic inset, so a negative top
21
+ * equal to the header height cancels only the header clearance; the tab
22
+ * bar clearance stays automatic. Android has no automatic inset, so there
23
+ * is nothing to cancel. Pure so it can be unit-tested.
24
+ */
25
+ export function pageContentInset(
26
+ os: string,
27
+ contentUnderHeader: number | undefined,
28
+ contentInset: PageInsets | undefined,
29
+ ): PageInsets | undefined {
30
+ if (contentUnderHeader == null || os !== "ios") {
31
+ return contentInset;
32
+ }
33
+ return {
34
+ top: (contentInset?.top ?? 0) - contentUnderHeader,
35
+ left: contentInset?.left ?? 0,
36
+ bottom: contentInset?.bottom ?? 0,
37
+ right: contentInset?.right ?? 0,
38
+ };
39
+ }
40
+
41
+ /** Gap above the first item. None when the content starts under the header. */
42
+ export function pagePaddingTop({
43
+ padded,
44
+ underHeader,
45
+ pageTop,
46
+ }: {
47
+ padded: boolean;
48
+ underHeader: boolean;
49
+ pageTop: number;
50
+ }): number {
51
+ return padded && !underHeader ? pageTop : 0;
52
+ }
@@ -0,0 +1,9 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { PAGE_THEME_ROLES } from "./pageThemeRoles.ts";
4
+
5
+ describe("PAGE_THEME_ROLES", () => {
6
+ it("maps the background to a page-prefixed role, not the app's own name", () => {
7
+ assert.equal(PAGE_THEME_ROLES.backgroundColor, "pageBackground");
8
+ });
9
+ });
@@ -0,0 +1,5 @@
1
+ export const PAGE_THEME_ROLES = {
2
+ backgroundColor: "pageBackground",
3
+ } as const;
4
+
5
+ export type PageThemeColorProp = keyof typeof PAGE_THEME_ROLES;
@@ -0,0 +1,26 @@
1
+ import type { ReactNode, Ref } from "react";
2
+ import type { ScrollView, ScrollViewProps } from "react-native";
3
+ import type { TabAccessoryButtonProps } from "../TabAccessoryButton/types";
4
+
5
+ /** The in-screen `TabAccessoryButton` (Android and iOS 18 and earlier). */
6
+ export type PageTabAccessory = Omit<
7
+ TabAccessoryButtonProps,
8
+ "placement" | "variant" | "color"
9
+ >;
10
+
11
+ export type PageProps = Omit<ScrollViewProps, "contentInsetAdjustmentBehavior"> & {
12
+ children?: ReactNode;
13
+ ref?: Ref<ScrollView>;
14
+ /** Renders the floating fallback pill and adds bottom room for it. */
15
+ tabAccessory?: PageTabAccessory;
16
+ /** Screen fill. Defaults to theme role `background`. */
17
+ backgroundColor?: string;
18
+ /** Screen-edge, top and bottom padding on the content. Default `true`. */
19
+ padded?: boolean;
20
+ /**
21
+ * Start the content at the very top, behind the header, e.g. for a hero
22
+ * image. Pass the header height: `useHeaderHeight()` from
23
+ * `expo-router/react-navigation`. The tab bar clearance is kept.
24
+ */
25
+ contentUnderHeader?: number;
26
+ };
@@ -15,7 +15,6 @@ export type SegmentedControlProps<T extends string = string> = {
15
15
  radius?: ButtonRadius;
16
16
  disabled?: boolean;
17
17
  backgroundColor?: string;
18
- borderColor?: string;
19
18
  selectedBackgroundColor?: string;
20
19
  labelColor?: string;
21
20
  selectedLabelColor?: string;