@studio-piot/native-ui 0.2.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (338) hide show
  1. package/README.md +22 -18
  2. package/dist/components/Button/Button.d.ts +20 -4
  3. package/dist/components/Button/Button.d.ts.map +1 -1
  4. package/dist/components/Button/Button.js +34 -21
  5. package/dist/components/Button/Button.js.map +1 -1
  6. package/dist/components/Button/buttonAppearance.d.ts +20 -0
  7. package/dist/components/Button/buttonAppearance.d.ts.map +1 -0
  8. package/dist/components/Button/buttonAppearance.js +28 -0
  9. package/dist/components/Button/buttonAppearance.js.map +1 -0
  10. package/dist/components/Button/buttonThemeRoles.d.ts +6 -0
  11. package/dist/components/Button/buttonThemeRoles.d.ts.map +1 -0
  12. package/dist/components/Button/buttonThemeRoles.js +8 -0
  13. package/dist/components/Button/buttonThemeRoles.js.map +1 -0
  14. package/dist/components/Button/index.d.ts +3 -1
  15. package/dist/components/Button/index.d.ts.map +1 -1
  16. package/dist/components/Button/index.js +4 -1
  17. package/dist/components/Button/index.js.map +1 -1
  18. package/dist/components/Choice/Choice.d.ts +8 -0
  19. package/dist/components/Choice/Choice.d.ts.map +1 -0
  20. package/dist/components/Choice/Choice.js +60 -0
  21. package/dist/components/Choice/Choice.js.map +1 -0
  22. package/dist/components/Choice/choiceLayout.d.ts +7 -0
  23. package/dist/components/Choice/choiceLayout.d.ts.map +1 -0
  24. package/dist/components/Choice/choiceLayout.js +13 -0
  25. package/dist/components/Choice/choiceLayout.js.map +1 -0
  26. package/dist/components/Choice/choiceThemeRoles.d.ts +8 -0
  27. package/dist/components/Choice/choiceThemeRoles.d.ts.map +1 -0
  28. package/dist/components/Choice/choiceThemeRoles.js +10 -0
  29. package/dist/components/Choice/choiceThemeRoles.js.map +1 -0
  30. package/dist/components/Choice/index.d.ts +5 -0
  31. package/dist/components/Choice/index.d.ts.map +1 -0
  32. package/dist/components/Choice/index.js +10 -0
  33. package/dist/components/Choice/index.js.map +1 -0
  34. package/dist/components/Choice/types.d.ts +26 -0
  35. package/dist/components/Choice/types.d.ts.map +1 -0
  36. package/dist/components/Choice/types.js +3 -0
  37. package/dist/components/Choice/types.js.map +1 -0
  38. package/dist/components/ErrorBanner/ErrorBanner.d.ts +1 -1
  39. package/dist/components/ErrorBanner/ErrorBanner.d.ts.map +1 -1
  40. package/dist/components/ErrorBanner/ErrorBanner.js +19 -19
  41. package/dist/components/ErrorBanner/ErrorBanner.js.map +1 -1
  42. package/dist/components/ErrorBanner/errorBannerContext.d.ts +23 -3
  43. package/dist/components/ErrorBanner/errorBannerContext.d.ts.map +1 -1
  44. package/dist/components/ErrorBanner/errorBannerContext.js +9 -5
  45. package/dist/components/ErrorBanner/errorBannerContext.js.map +1 -1
  46. package/dist/components/ErrorBanner/errorBannerLayout.d.ts +18 -0
  47. package/dist/components/ErrorBanner/errorBannerLayout.d.ts.map +1 -0
  48. package/dist/components/ErrorBanner/errorBannerLayout.js +54 -0
  49. package/dist/components/ErrorBanner/errorBannerLayout.js.map +1 -0
  50. package/dist/components/ErrorBanner/icons.d.ts +7 -5
  51. package/dist/components/ErrorBanner/icons.d.ts.map +1 -1
  52. package/dist/components/ErrorBanner/icons.js +8 -4
  53. package/dist/components/ErrorBanner/icons.js.map +1 -1
  54. package/dist/components/ErrorBanner/index.d.ts +1 -1
  55. package/dist/components/ErrorBanner/index.d.ts.map +1 -1
  56. package/dist/components/ErrorBanner/index.js.map +1 -1
  57. package/dist/components/Form/Form.d.ts +9 -1
  58. package/dist/components/Form/Form.d.ts.map +1 -1
  59. package/dist/components/Form/Form.js +56 -78
  60. package/dist/components/Form/Form.js.map +1 -1
  61. package/dist/components/Form/FormCloseButton.d.ts +3 -1
  62. package/dist/components/Form/FormCloseButton.d.ts.map +1 -1
  63. package/dist/components/Form/FormCloseButton.js +5 -4
  64. package/dist/components/Form/FormCloseButton.js.map +1 -1
  65. package/dist/components/Form/FormErrorBanner.d.ts +15 -0
  66. package/dist/components/Form/FormErrorBanner.d.ts.map +1 -0
  67. package/dist/components/Form/FormErrorBanner.js +30 -0
  68. package/dist/components/Form/FormErrorBanner.js.map +1 -0
  69. package/dist/components/Form/FormSubmitFooter.d.ts +17 -0
  70. package/dist/components/Form/FormSubmitFooter.d.ts.map +1 -0
  71. package/dist/components/Form/FormSubmitFooter.js +32 -0
  72. package/dist/components/Form/FormSubmitFooter.js.map +1 -0
  73. package/dist/components/Form/formThemeRoles.d.ts +7 -0
  74. package/dist/components/Form/formThemeRoles.d.ts.map +1 -0
  75. package/dist/components/Form/formThemeRoles.js +9 -0
  76. package/dist/components/Form/formThemeRoles.js.map +1 -0
  77. package/dist/components/Form/index.d.ts +1 -0
  78. package/dist/components/Form/index.d.ts.map +1 -1
  79. package/dist/components/Form/index.js +3 -1
  80. package/dist/components/Form/index.js.map +1 -1
  81. package/dist/components/Form/types.d.ts +14 -10
  82. package/dist/components/Form/types.d.ts.map +1 -1
  83. package/dist/components/FormSheet/FormSheet.d.ts +10 -0
  84. package/dist/components/FormSheet/FormSheet.d.ts.map +1 -0
  85. package/dist/components/FormSheet/FormSheet.js +153 -0
  86. package/dist/components/FormSheet/FormSheet.js.map +1 -0
  87. package/dist/components/FormSheet/formSheetLayout.d.ts +32 -0
  88. package/dist/components/FormSheet/formSheetLayout.d.ts.map +1 -0
  89. package/dist/components/FormSheet/formSheetLayout.js +43 -0
  90. package/dist/components/FormSheet/formSheetLayout.js.map +1 -0
  91. package/dist/components/FormSheet/formSheetThemeRoles.d.ts +8 -0
  92. package/dist/components/FormSheet/formSheetThemeRoles.d.ts.map +1 -0
  93. package/dist/components/FormSheet/formSheetThemeRoles.js +10 -0
  94. package/dist/components/FormSheet/formSheetThemeRoles.js.map +1 -0
  95. package/dist/components/FormSheet/index.d.ts +5 -0
  96. package/dist/components/FormSheet/index.d.ts.map +1 -0
  97. package/dist/components/FormSheet/index.js +10 -0
  98. package/dist/components/FormSheet/index.js.map +1 -0
  99. package/dist/components/FormSheet/types.d.ts +64 -0
  100. package/dist/components/FormSheet/types.d.ts.map +1 -0
  101. package/dist/components/FormSheet/types.js +3 -0
  102. package/dist/components/FormSheet/types.js.map +1 -0
  103. package/dist/components/HeaderButton/HeaderCapsule.d.ts +4 -0
  104. package/dist/components/HeaderButton/HeaderCapsule.d.ts.map +1 -0
  105. package/dist/components/HeaderButton/HeaderCapsule.js +32 -0
  106. package/dist/components/HeaderButton/HeaderCapsule.js.map +1 -0
  107. package/dist/components/HeaderButton/HeaderGroup.d.ts +4 -0
  108. package/dist/components/HeaderButton/HeaderGroup.d.ts.map +1 -0
  109. package/dist/components/HeaderButton/HeaderGroup.js +23 -0
  110. package/dist/components/HeaderButton/HeaderGroup.js.map +1 -0
  111. package/dist/components/HeaderButton/HeaderIcon.d.ts +4 -0
  112. package/dist/components/HeaderButton/HeaderIcon.d.ts.map +1 -0
  113. package/dist/components/HeaderButton/HeaderIcon.js +28 -0
  114. package/dist/components/HeaderButton/HeaderIcon.js.map +1 -0
  115. package/dist/components/HeaderButton/headerButtonLayout.d.ts +21 -0
  116. package/dist/components/HeaderButton/headerButtonLayout.d.ts.map +1 -0
  117. package/dist/components/HeaderButton/headerButtonLayout.js +34 -0
  118. package/dist/components/HeaderButton/headerButtonLayout.js.map +1 -0
  119. package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts +5 -0
  120. package/dist/components/HeaderButton/headerButtonThemeRoles.d.ts.map +1 -0
  121. package/dist/components/HeaderButton/headerButtonThemeRoles.js +7 -0
  122. package/dist/components/HeaderButton/headerButtonThemeRoles.js.map +1 -0
  123. package/dist/components/HeaderButton/index.d.ts +7 -0
  124. package/dist/components/HeaderButton/index.d.ts.map +1 -0
  125. package/dist/components/HeaderButton/index.js +14 -0
  126. package/dist/components/HeaderButton/index.js.map +1 -0
  127. package/dist/components/HeaderButton/types.d.ts +40 -0
  128. package/dist/components/HeaderButton/types.d.ts.map +1 -0
  129. package/dist/components/HeaderButton/types.js +3 -0
  130. package/dist/components/HeaderButton/types.js.map +1 -0
  131. package/dist/components/HeaderWash/HeaderWash.d.ts +10 -0
  132. package/dist/components/HeaderWash/HeaderWash.d.ts.map +1 -0
  133. package/dist/components/HeaderWash/HeaderWash.js +19 -0
  134. package/dist/components/HeaderWash/HeaderWash.js.map +1 -0
  135. package/dist/components/HeaderWash/headerWashGradient.d.ts +8 -0
  136. package/dist/components/HeaderWash/headerWashGradient.d.ts.map +1 -0
  137. package/dist/components/HeaderWash/headerWashGradient.js +21 -0
  138. package/dist/components/HeaderWash/headerWashGradient.js.map +1 -0
  139. package/dist/components/HeaderWash/index.d.ts +2 -0
  140. package/dist/components/HeaderWash/index.d.ts.map +1 -0
  141. package/dist/components/HeaderWash/index.js +6 -0
  142. package/dist/components/HeaderWash/index.js.map +1 -0
  143. package/dist/components/Page/Page.d.ts +13 -0
  144. package/dist/components/Page/Page.d.ts.map +1 -0
  145. package/dist/components/Page/Page.js +49 -0
  146. package/dist/components/Page/Page.js.map +1 -0
  147. package/dist/components/Page/PageBleed.d.ts +9 -0
  148. package/dist/components/Page/PageBleed.d.ts.map +1 -0
  149. package/dist/components/Page/PageBleed.js +12 -0
  150. package/dist/components/Page/PageBleed.js.map +1 -0
  151. package/dist/components/Page/index.d.ts +5 -0
  152. package/dist/components/Page/index.d.ts.map +1 -0
  153. package/dist/components/Page/index.js +10 -0
  154. package/dist/components/Page/index.js.map +1 -0
  155. package/dist/components/Page/pageContext.d.ts +4 -0
  156. package/dist/components/Page/pageContext.d.ts.map +1 -0
  157. package/dist/components/Page/pageContext.js +11 -0
  158. package/dist/components/Page/pageContext.js.map +1 -0
  159. package/dist/components/Page/pageInsets.d.ts +26 -0
  160. package/dist/components/Page/pageInsets.d.ts.map +1 -0
  161. package/dist/components/Page/pageInsets.js +37 -0
  162. package/dist/components/Page/pageInsets.js.map +1 -0
  163. package/dist/components/Page/pageLayout.d.ts +5 -0
  164. package/dist/components/Page/pageLayout.d.ts.map +1 -0
  165. package/dist/components/Page/pageLayout.js +8 -0
  166. package/dist/components/Page/pageLayout.js.map +1 -0
  167. package/dist/components/Page/pageThemeRoles.d.ts +5 -0
  168. package/dist/components/Page/pageThemeRoles.d.ts.map +1 -0
  169. package/dist/components/Page/pageThemeRoles.js +7 -0
  170. package/dist/components/Page/pageThemeRoles.js.map +1 -0
  171. package/dist/components/Page/types.d.ts +22 -0
  172. package/dist/components/Page/types.d.ts.map +1 -0
  173. package/dist/components/Page/types.js +3 -0
  174. package/dist/components/Page/types.js.map +1 -0
  175. package/dist/components/Select/Select.d.ts +1 -1
  176. package/dist/components/Select/Select.d.ts.map +1 -1
  177. package/dist/components/Select/Select.js +8 -9
  178. package/dist/components/Select/Select.js.map +1 -1
  179. package/dist/components/Select/selectThemeRoles.d.ts +2 -0
  180. package/dist/components/Select/selectThemeRoles.d.ts.map +1 -1
  181. package/dist/components/Select/selectThemeRoles.js +2 -0
  182. package/dist/components/Select/selectThemeRoles.js.map +1 -1
  183. package/dist/components/Select/types.d.ts +4 -0
  184. package/dist/components/Select/types.d.ts.map +1 -1
  185. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts +8 -0
  186. package/dist/components/TabAccessoryButton/TabAccessoryButton.d.ts.map +1 -0
  187. package/dist/components/TabAccessoryButton/TabAccessoryButton.js +77 -0
  188. package/dist/components/TabAccessoryButton/TabAccessoryButton.js.map +1 -0
  189. package/dist/components/TabAccessoryButton/index.d.ts +5 -0
  190. package/dist/components/TabAccessoryButton/index.d.ts.map +1 -0
  191. package/dist/components/TabAccessoryButton/index.js +11 -0
  192. package/dist/components/TabAccessoryButton/index.js.map +1 -0
  193. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts +27 -0
  194. package/dist/components/TabAccessoryButton/tabAccessoryLayout.d.ts.map +1 -0
  195. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js +38 -0
  196. package/dist/components/TabAccessoryButton/tabAccessoryLayout.js.map +1 -0
  197. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts +14 -0
  198. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.d.ts.map +1 -0
  199. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js +19 -0
  200. package/dist/components/TabAccessoryButton/tabAccessoryThemeRoles.js.map +1 -0
  201. package/dist/components/TabAccessoryButton/types.d.ts +27 -0
  202. package/dist/components/TabAccessoryButton/types.d.ts.map +1 -0
  203. package/dist/components/TabAccessoryButton/types.js +3 -0
  204. package/dist/components/TabAccessoryButton/types.js.map +1 -0
  205. package/dist/components/index.d.ts +9 -3
  206. package/dist/components/index.d.ts.map +1 -1
  207. package/dist/components/index.js +30 -1
  208. package/dist/components/index.js.map +1 -1
  209. package/dist/index.d.ts +11 -4
  210. package/dist/index.d.ts.map +1 -1
  211. package/dist/index.js +34 -2
  212. package/dist/index.js.map +1 -1
  213. package/dist/layout/HeaderInsetProbe.d.ts +15 -0
  214. package/dist/layout/HeaderInsetProbe.d.ts.map +1 -0
  215. package/dist/layout/HeaderInsetProbe.js +28 -0
  216. package/dist/layout/HeaderInsetProbe.js.map +1 -0
  217. package/dist/layout/headerInset.d.ts +26 -0
  218. package/dist/layout/headerInset.d.ts.map +1 -0
  219. package/dist/layout/headerInset.js +29 -0
  220. package/dist/layout/headerInset.js.map +1 -0
  221. package/dist/layout/index.d.ts +1 -0
  222. package/dist/layout/index.d.ts.map +1 -1
  223. package/dist/layout/index.js +3 -1
  224. package/dist/layout/index.js.map +1 -1
  225. package/dist/layout/sheet.d.ts +3 -0
  226. package/dist/layout/sheet.d.ts.map +1 -0
  227. package/dist/layout/sheet.js +6 -0
  228. package/dist/layout/sheet.js.map +1 -0
  229. package/dist/theme/ThemeProvider.d.ts +4 -1
  230. package/dist/theme/ThemeProvider.d.ts.map +1 -1
  231. package/dist/theme/ThemeProvider.js +5 -2
  232. package/dist/theme/ThemeProvider.js.map +1 -1
  233. package/dist/theme/index.d.ts +2 -0
  234. package/dist/theme/index.d.ts.map +1 -1
  235. package/dist/theme/index.js +6 -1
  236. package/dist/theme/index.js.map +1 -1
  237. package/dist/theme/resolveThemeSpacing.d.ts +12 -0
  238. package/dist/theme/resolveThemeSpacing.d.ts.map +1 -0
  239. package/dist/theme/resolveThemeSpacing.js +14 -0
  240. package/dist/theme/resolveThemeSpacing.js.map +1 -0
  241. package/dist/theme/types.d.ts +2 -0
  242. package/dist/theme/types.d.ts.map +1 -1
  243. package/dist/theme/useThemeSpacing.d.ts +4 -0
  244. package/dist/theme/useThemeSpacing.d.ts.map +1 -0
  245. package/dist/theme/useThemeSpacing.js +11 -0
  246. package/dist/theme/useThemeSpacing.js.map +1 -0
  247. package/docs/components/button.md +92 -38
  248. package/docs/components/choice.md +88 -0
  249. package/docs/components/error-banner.md +46 -3
  250. package/docs/components/form-sheet.md +140 -0
  251. package/docs/components/form.md +80 -45
  252. package/docs/components/header-button.md +310 -0
  253. package/docs/components/header-wash.md +40 -0
  254. package/docs/components/page.md +183 -0
  255. package/docs/components/select.md +2 -2
  256. package/docs/components/tab-accessory-button.md +146 -0
  257. package/docs/specifications/Liquid Glass header buttons in Expo Router.md +327 -0
  258. package/package.json +1 -1
  259. package/src/components/Button/Button.tsx +82 -38
  260. package/src/components/Button/buttonAppearance.test.ts +44 -0
  261. package/src/components/Button/buttonAppearance.ts +38 -0
  262. package/src/components/Button/buttonThemeRoles.test.ts +10 -0
  263. package/src/components/Button/buttonThemeRoles.ts +6 -0
  264. package/src/components/Button/index.ts +12 -1
  265. package/src/components/Choice/Choice.tsx +109 -0
  266. package/src/components/Choice/choiceLayout.test.ts +14 -0
  267. package/src/components/Choice/choiceLayout.ts +9 -0
  268. package/src/components/Choice/choiceThemeRoles.test.ts +14 -0
  269. package/src/components/Choice/choiceThemeRoles.ts +8 -0
  270. package/src/components/Choice/index.ts +4 -0
  271. package/src/components/Choice/types.ts +26 -0
  272. package/src/components/ErrorBanner/ErrorBanner.tsx +32 -22
  273. package/src/components/ErrorBanner/errorBannerContext.tsx +31 -6
  274. package/src/components/ErrorBanner/errorBannerLayout.test.ts +49 -0
  275. package/src/components/ErrorBanner/errorBannerLayout.ts +59 -0
  276. package/src/components/ErrorBanner/icons.tsx +25 -6
  277. package/src/components/ErrorBanner/index.ts +1 -0
  278. package/src/components/Form/Form.tsx +77 -173
  279. package/src/components/Form/FormCloseButton.tsx +12 -4
  280. package/src/components/Form/FormErrorBanner.tsx +57 -0
  281. package/src/components/Form/FormSubmitFooter.tsx +70 -0
  282. package/src/components/Form/formThemeRoles.test.ts +13 -0
  283. package/src/components/Form/formThemeRoles.ts +7 -0
  284. package/src/components/Form/index.ts +1 -0
  285. package/src/components/Form/types.ts +14 -10
  286. package/src/components/FormSheet/FormSheet.tsx +279 -0
  287. package/src/components/FormSheet/formSheetLayout.test.ts +66 -0
  288. package/src/components/FormSheet/formSheetLayout.ts +55 -0
  289. package/src/components/FormSheet/formSheetThemeRoles.test.ts +14 -0
  290. package/src/components/FormSheet/formSheetThemeRoles.ts +8 -0
  291. package/src/components/FormSheet/index.ts +7 -0
  292. package/src/components/FormSheet/types.ts +66 -0
  293. package/src/components/HeaderButton/HeaderCapsule.tsx +63 -0
  294. package/src/components/HeaderButton/HeaderGroup.tsx +36 -0
  295. package/src/components/HeaderButton/HeaderIcon.tsx +56 -0
  296. package/src/components/HeaderButton/headerButtonLayout.test.ts +47 -0
  297. package/src/components/HeaderButton/headerButtonLayout.ts +36 -0
  298. package/src/components/HeaderButton/headerButtonThemeRoles.test.ts +9 -0
  299. package/src/components/HeaderButton/headerButtonThemeRoles.ts +5 -0
  300. package/src/components/HeaderButton/index.ts +14 -0
  301. package/src/components/HeaderButton/types.ts +40 -0
  302. package/src/components/HeaderWash/HeaderWash.tsx +20 -0
  303. package/src/components/HeaderWash/headerWashGradient.test.ts +22 -0
  304. package/src/components/{Form/withAlpha.ts → HeaderWash/headerWashGradient.ts} +9 -0
  305. package/src/components/HeaderWash/index.ts +1 -0
  306. package/src/components/Page/Page.tsx +80 -0
  307. package/src/components/Page/PageBleed.tsx +14 -0
  308. package/src/components/Page/index.ts +4 -0
  309. package/src/components/Page/pageContext.ts +8 -0
  310. package/src/components/Page/pageInsets.test.ts +73 -0
  311. package/src/components/Page/pageInsets.ts +52 -0
  312. package/src/components/Page/pageThemeRoles.test.ts +9 -0
  313. package/src/components/Page/pageThemeRoles.ts +5 -0
  314. package/src/components/Page/types.ts +26 -0
  315. package/src/components/Select/Select.tsx +11 -4
  316. package/src/components/Select/selectThemeRoles.test.ts +5 -0
  317. package/src/components/Select/selectThemeRoles.ts +2 -0
  318. package/src/components/Select/types.ts +4 -0
  319. package/src/components/TabAccessoryButton/TabAccessoryButton.tsx +150 -0
  320. package/src/components/TabAccessoryButton/index.ts +12 -0
  321. package/src/components/TabAccessoryButton/tabAccessoryLayout.test.ts +42 -0
  322. package/src/components/TabAccessoryButton/tabAccessoryLayout.ts +43 -0
  323. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.test.ts +28 -0
  324. package/src/components/TabAccessoryButton/tabAccessoryThemeRoles.ts +22 -0
  325. package/src/components/TabAccessoryButton/types.ts +27 -0
  326. package/src/components/index.ts +64 -2
  327. package/src/index.ts +61 -2
  328. package/src/layout/headerInset.test.ts +60 -0
  329. package/src/layout/headerInset.ts +41 -0
  330. package/src/layout/index.ts +1 -0
  331. package/src/layout/sheet.ts +2 -0
  332. package/src/theme/ThemeProvider.tsx +9 -1
  333. package/src/theme/index.ts +6 -0
  334. package/src/theme/resolveThemeSpacing.test.ts +29 -0
  335. package/src/theme/resolveThemeSpacing.ts +21 -0
  336. package/src/theme/types.ts +3 -0
  337. package/src/theme/useThemeSpacing.ts +8 -0
  338. package/src/components/Form/withAlpha.test.ts +0 -13
@@ -0,0 +1,183 @@
1
+ # Page — usage
2
+
3
+ The standard scrolling screen for a tab: a native stack screen inside `NativeTabs`, such as Today.
4
+
5
+ - **Header clearance.** At rest, the content sits just below the header, including the large title. When you scroll, it moves behind the header. When the large title collapses, the content follows it. On iOS this comes from UIKit through `contentInsetAdjustmentBehavior="automatic"`, which `Page` always sets. On Android the native stack header is laid out above the screen, so nothing overlaps.
6
+ - **Tab bar clearance.** The same automatic inset keeps the last item clear of the tab bar and home indicator on iOS. On Android the tab bar sits outside the screen.
7
+ - **Tab accessory clearance.** Pass `tabAccessory` and `Page` renders the floating fallback pill (`TabAccessoryButton placement="screen"`) and adds bottom room for it. On iOS 26+ the accessory belongs to the native tab bar, so the in-screen pill and the extra room are left out.
8
+ - **Padding.** Content gets the theme spacing: `spacing.screen` at the sides, `spacing.pageTop` at the top and `spacing.pageBottom` at the bottom (defaults 16, 16 and 24; see [Spacing](./button.md#spacing)). Turn it off with `padded={false}`.
9
+
10
+ ## Title
11
+
12
+ The title belongs to the header, not the page. Set it in the stack options and turn on the large title to get the iOS collapsing title:
13
+
14
+ ```tsx
15
+ // app/(tabs)/today/_layout.tsx
16
+ <Stack screenOptions={{ headerLargeTitle: true }}>
17
+ <Stack.Screen name="index" options={{ title: "Today" }} />
18
+ </Stack>
19
+ ```
20
+
21
+ For a page with no title, leave `title` empty or set `headerShown: false`. `Page` needs no change either way.
22
+
23
+ ## Basic
24
+
25
+ ```tsx
26
+ // app/(tabs)/today/index.tsx
27
+ import { Page } from "@studio-piot/native-ui";
28
+
29
+ export default function TodayScreen() {
30
+ return (
31
+ <Page>
32
+ <SummaryCard />
33
+ <RecentActivities />
34
+ </Page>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ## With the tab accessory
40
+
41
+ Mount the iOS 26 copy in the tabs layout as usual (see [TabAccessoryButton](./tab-accessory-button.md)). `Page` handles the in-screen copy:
42
+
43
+ ```tsx
44
+ import { Page } from "@studio-piot/native-ui";
45
+ import { router } from "expo-router";
46
+ import { SymbolView } from "expo-symbols";
47
+
48
+ export default function TodayScreen() {
49
+ return (
50
+ <Page
51
+ tabAccessory={{
52
+ label: "New activity",
53
+ icon: ({ color, size }) => (
54
+ <SymbolView name={{ ios: "plus", android: "add" }} size={size} tintColor={color} />
55
+ ),
56
+ onPress: () => router.push("/new-activity"),
57
+ }}
58
+ >
59
+ {/* content */}
60
+ </Page>
61
+ );
62
+ }
63
+ ```
64
+
65
+ `tabAccessory` takes the same props as `TabAccessoryButton`, except `placement`, `variant` and `color`, which only apply to the iOS 26 copy.
66
+
67
+ ## Full-width sections
68
+
69
+ To take one section out to the screen edges, such as an image, a map or a horizontal carousel, wrap it in `PageBleed`. It cancels exactly the side padding the page has: `spacing.screen` normally, or nothing with `padded={false}`.
70
+
71
+ ```tsx
72
+ import { Page, PageBleed } from "@studio-piot/native-ui";
73
+
74
+ <Page>
75
+ <Text>Padded text</Text>
76
+ <PageBleed>
77
+ <Image source={hero} style={{ width: "100%", aspectRatio: 16 / 9 }} />
78
+ </PageBleed>
79
+ <Text>Padded again</Text>
80
+ </Page>
81
+ ```
82
+
83
+ For a page where everything is full width, use `padded={false}` instead.
84
+
85
+ `PageBleed` only knows the padding `Page` itself applies. If you change the side padding with `contentContainerStyle`, it won't follow; set `spacing.screen` on the theme instead.
86
+
87
+ ## Content under the header
88
+
89
+ For a hero image or map that should start at the very top, behind the header, pass the header height as `contentUnderHeader`. Expo Router provides it through `useHeaderHeight()`:
90
+
91
+ ```tsx
92
+ // app/(tabs)/today/_layout.tsx
93
+ <Stack.Screen
94
+ name="index"
95
+ options={{ title: "", headerTransparent: true, headerLargeTitle: false }}
96
+ />
97
+ ```
98
+
99
+ ```tsx
100
+ // app/(tabs)/today/index.tsx
101
+ import { Page, PageBleed } from "@studio-piot/native-ui";
102
+ import { useHeaderHeight } from "expo-router/react-navigation";
103
+
104
+ export default function TodayScreen() {
105
+ const headerHeight = useHeaderHeight();
106
+
107
+ return (
108
+ <Page contentUnderHeader={headerHeight}>
109
+ <PageBleed>
110
+ <Image source={hero} style={{ width: "100%", height: 320 }} />
111
+ </PageBleed>
112
+ <Text>Padded content below the hero</Text>
113
+ </Page>
114
+ );
115
+ }
116
+ ```
117
+
118
+ - **What changes.** iOS adds `contentInset` on top of its automatic inset. `Page` sets a negative top inset equal to the header height, which cancels only the header clearance. The tab bar and accessory clearance stay automatic, and `spacing.pageTop` is dropped so the first item touches the top.
119
+ - **Use a small, transparent header.** A large title collapses as you scroll, so its height changes and no single value cancels it. Set `headerLargeTitle: false` and `headerTransparent: true` on hero screens.
120
+ - **Wrap the hero in `PageBleed`** so it also reaches the side edges.
121
+ - **Android.** The native header adds no inset, so `contentUnderHeader` does nothing. With `headerTransparent: true` the content already starts under the header. With an opaque header, content can't go under it.
122
+
123
+ ## ScrollView props
124
+
125
+ `Page` passes everything else to its `ScrollView`: `ref`, `refreshControl`, `onScroll`, `keyboardShouldPersistTaps` and so on. `contentContainerStyle` is merged after the built-in padding, so it can override it.
126
+
127
+ ```tsx
128
+ const scrollRef = useRef<ScrollView>(null);
129
+
130
+ <Page ref={scrollRef} refreshControl={<RefreshControl refreshing={loading} onRefresh={reload} />}>
131
+ …
132
+ </Page>
133
+ ```
134
+
135
+ `contentInsetAdjustmentBehavior` can't be changed. It is what gives the header and tab bar clearance. Your own `contentInset` is kept, and `contentUnderHeader` is added to it.
136
+
137
+ ## Theme roles
138
+
139
+ | Usage | Role |
140
+ |---|---|
141
+ | Screen fill | `pageBackground` |
142
+
143
+ When `tabAccessory` is set, the fallback pill also reads `buttonBackground` and `buttonLabel`.
144
+
145
+ ## Props
146
+
147
+ | Prop | Type | Default | Notes |
148
+ |---|---|---|---|
149
+ | `children` | `ReactNode` | — | Page content |
150
+ | `tabAccessory` | `PageTabAccessory` | — | Floating fallback pill and its bottom room |
151
+ | `backgroundColor` | `string` | theme `pageBackground` | Optional override |
152
+ | `padded` | `boolean` | `true` | Side, top and bottom padding from the theme spacing |
153
+ | `contentUnderHeader` | `number` | — | Header height. Content starts at the very top, behind the header (iOS) |
154
+ | `ref` | `Ref<ScrollView>` | — | Forwarded to the scroll view |
155
+ | `style` | view style | — | Scroll view style, merged after `flex: 1` and the background |
156
+ | …`ScrollViewProps` | | | Everything except `contentInsetAdjustmentBehavior` |
157
+
158
+ ### PageBleed
159
+
160
+ | Prop | Type | Notes |
161
+ |---|---|---|
162
+ | `children` | `ReactNode` | The full-width section |
163
+ | `style` | view style | Merged after the bleed margin |
164
+
165
+ ## Gotchas
166
+
167
+ - **Make `Page` the screen's root element.** react-native-screens only links the header (large title collapse and insets) to a `ScrollView` that is a direct child of the screen. `Page` renders its `ScrollView` as that direct child, with no wrapper view. If you wrap `Page` in a `View`, content starts at the very top, under the header. `<Stack.Screen options>` next to it in a fragment is fine, because it renders nothing.
168
+ - **Don't wrap `Page` in a `SafeAreaView` or add `insets.top` padding.** That clears the header twice, leaving a gap under it.
169
+ - **The header must be translucent for content to scroll behind it.** That is the default on iOS. An opaque `headerStyle: { backgroundColor }` still works, but content then scrolls under a solid bar.
170
+ - **On iOS 26, check that the last item clears the native accessory on a device.** If it doesn't, add room with `contentContainerStyle={{ paddingBottom: … }}`.
171
+
172
+ ## Imports
173
+
174
+ ```ts
175
+ import {
176
+ Page,
177
+ PageBleed,
178
+ PAGE_THEME_ROLES,
179
+ type PageBleedProps,
180
+ type PageProps,
181
+ type PageTabAccessory,
182
+ } from "@studio-piot/native-ui";
183
+ ```
@@ -36,7 +36,7 @@ Import `react-native-gesture-handler` at the top of the entry layout (before oth
36
36
 
37
37
  ## Theme roles
38
38
 
39
- Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus **`fieldSelectSelected`** for the selected option check / label tint.
39
+ Uses Field roles: `fieldBackground`, `fieldText`, `fieldPlaceholder`, `fieldLabel`, `fieldDescription`, `fieldBorder`, `fieldError`, plus **`fieldSelectSelected`** for the selected option check / label tint, and **`selectCloseBackground`** / **`selectCloseIcon`** for the default sheet close disc and its cross. Each has a matching optional prop (`closeBackgroundColor`, `closeIconColor`).
40
40
 
41
41
  ## Basic usage
42
42
 
@@ -58,7 +58,7 @@ import { Select } from "@studio-piot/native-ui";
58
58
 
59
59
  ## Close control
60
60
 
61
- Default is a dark disc with a white × (`SelectCloseButton`). Override:
61
+ The default is a filled disc with a cross (`SelectCloseButton`), coloured by `selectCloseBackground` and `selectCloseIcon`. Override:
62
62
 
63
63
  ```tsx
64
64
  closeButton={(dismiss) => <YourClose onPress={dismiss} />}
@@ -0,0 +1,146 @@
1
+ # TabAccessoryButton — usage
2
+
3
+ A primary action that floats above the bottom tabs, such as "New activity" on a Today tab.
4
+
5
+ - **iOS 26 and later.** Expo Router's `NativeTabs.BottomAccessory` is UIKit's tab bar accessory: the glass strip above the tabs that Apple Music uses for its mini player. iOS draws the glass, sets the position and handles safe areas. `TabAccessoryButton` supplies the content only: an icon and a label, no fill. A filled button inside the accessory would give two shapes, one inside the other.
6
+ - **Android and iOS 18 and earlier.** There is no accessory. The button floats a filled pill (a `Button` with `radius="full"`) at the bottom of the tab screen, just above the tabs.
7
+
8
+ You mount it twice, and `placement` makes each copy render only where it belongs. You don't need a platform check in the app.
9
+
10
+ | `placement` | Mount it in | Renders on |
11
+ |---|---|---|
12
+ | `"accessory"` | `NativeTabs.BottomAccessory` | iOS 26+ |
13
+ | `"screen"` | The tab screen, after the scroll view | Android, iOS 18 and earlier |
14
+
15
+ ## Tabs layout
16
+
17
+ ```tsx
18
+ // app/(tabs)/_layout.tsx
19
+ import { TabAccessoryButton } from "@studio-piot/native-ui";
20
+ import { router, useSegments } from "expo-router";
21
+ import { NativeTabs } from "expo-router/unstable-native-tabs";
22
+ import { SymbolView } from "expo-symbols";
23
+
24
+ export default function TabsLayout() {
25
+ const onToday = useSegments().some((segment) => segment === "today");
26
+
27
+ return (
28
+ <NativeTabs minimizeBehavior="never">
29
+ {onToday ? (
30
+ <NativeTabs.BottomAccessory>
31
+ <TabAccessoryButton
32
+ placement="accessory"
33
+ label="New activity"
34
+ icon={({ color, size }) => (
35
+ <SymbolView name="plus" size={size} weight="semibold" tintColor={color} />
36
+ )}
37
+ onPress={() => router.push("/new-activity")}
38
+ />
39
+ </NativeTabs.BottomAccessory>
40
+ ) : null}
41
+ <NativeTabs.Trigger name="today">{/* … */}</NativeTabs.Trigger>
42
+ </NativeTabs>
43
+ );
44
+ }
45
+ ```
46
+
47
+ The accessory belongs to the tab bar, not to a tab, so render it conditionally to show it on one tab only. Without that check it shows on every tab.
48
+
49
+ ## Tab screen
50
+
51
+ ```tsx
52
+ // app/(tabs)/today/index.tsx
53
+ import {
54
+ TabAccessoryButton,
55
+ tabAccessoryScreenInset,
56
+ useThemeSpacing,
57
+ } from "@studio-piot/native-ui";
58
+
59
+ export default function TodayScreen() {
60
+ const { screen } = useThemeSpacing();
61
+
62
+ return (
63
+ <View style={{ flex: 1 }}>
64
+ <ScrollView contentContainerStyle={{ paddingBottom: tabAccessoryScreenInset(screen) }}>
65
+ {/* content */}
66
+ </ScrollView>
67
+ <TabAccessoryButton
68
+ placement="screen"
69
+ label="New activity"
70
+ icon={plusIcon}
71
+ onPress={() => router.push("/new-activity")}
72
+ />
73
+ </View>
74
+ );
75
+ }
76
+ ```
77
+
78
+ The screen's root `View` needs `flex: 1`, because the pill is positioned absolutely against it. It sits `spacing.screen` from the theme (default 16) from each side and from the bottom, so the gap around it is even. `tabAccessoryScreenInset(spacing.screen)` keeps the last item of the scroll view clear of the pill: the pill height plus that gap below and above it. `TAB_ACCESSORY_SCREEN_INSET` (84) is the same value with the default spacing. On iOS 26 the in-screen copy renders nothing, so the padding is only extra space at the end of the scroll.
79
+
80
+ `Page` does all of this for you with its `tabAccessory` prop. See [Page](./page.md).
81
+
82
+ ## Filled (brand colour)
83
+
84
+ The accessory takes no tint, so `variant="filled"` covers the glass with a pill in the button colours instead (`buttonBackground` and `buttonLabel`). The pill fills the accessory edge to edge and is fully rounded, so you see one coloured capsule above the tabs, not a shape inside the glass. It matches the fallback pill on other platforms.
85
+
86
+ ```tsx
87
+ <NativeTabs.BottomAccessory>
88
+ <TabAccessoryButton
89
+ placement="accessory"
90
+ variant="filled"
91
+ label="New activity"
92
+ icon={plusIcon}
93
+ onPress={() => router.push("/new-activity")}
94
+ />
95
+ </NativeTabs.BottomAccessory>
96
+ ```
97
+
98
+ Use it for one primary brand action. For anything secondary, keep the default glass so the tab bar area stays calm.
99
+
100
+ ## Icons
101
+
102
+ `icon` is a `ButtonIcon` render function, the same one `Button` takes: `({ color, size }) => ReactNode`. The colour is `tabAccessoryLabel` for the glass variant and `buttonLabel` for the filled variant and the fallback pill. Icon size is 20. SF Symbols are iOS only; on Android use a platform name (`name={{ ios: "plus", android: "add" }}` with recent expo-symbols) or another icon set.
103
+
104
+ ## Theme roles
105
+
106
+ | Usage | Role |
107
+ |---|---|
108
+ | Glass label and icon (iOS 26+) | `tabAccessoryLabel` |
109
+ | Filled variant and fallback pill: fill | `buttonBackground` |
110
+ | Filled variant and fallback pill: label and icon | `buttonLabel` |
111
+
112
+ Only the roles for the platform you are on are read, but add all three to both light and dark maps. `tabAccessoryLabel` is usually your `text` colour, since it sits on glass like the tab labels.
113
+
114
+ ## Props
115
+
116
+ | Prop | Type | Default | Notes |
117
+ |---|---|---|---|
118
+ | `placement` | `"accessory" \| "screen"` | — | Required. See above |
119
+ | `label` | `string` | — | Required. Shown text and accessibility label |
120
+ | `onPress` | `() => void` | — | Required |
121
+ | `icon` | `ButtonIcon` | — | Before the label |
122
+ | `variant` | `"glass" \| "filled"` | `"glass"` | iOS 26+ look. The fallback is always filled |
123
+ | `disabled` | `boolean` | `false` | 40% opacity; not pressable |
124
+ | `color` | `string` | theme `tabAccessoryLabel` | Glass content override |
125
+ | `backgroundColor` | `string` | theme `buttonBackground` | Filled and fallback pill fill override |
126
+ | `labelColor` | `string` | theme `buttonLabel` | Filled and fallback pill label override |
127
+
128
+ ## Gotchas
129
+
130
+ - **Don't put a `Button` in the accessory.** Its padding leaves a smaller shape inside the system glass. For a coloured accessory, use `variant="filled"`, which fills it edge to edge.
131
+ - **The accessory is full width.** iOS sets its size. If you need a small centred pill instead, that is a custom `GlassView` from expo-glass-effect, not this component.
132
+ - **`minimizeBehavior`.** With `"onScrollDown"`, iOS moves the accessory inline with the collapsed tab bar, and the content gets less room. Keep the label short.
133
+
134
+ ## Imports
135
+
136
+ ```ts
137
+ import {
138
+ TabAccessoryButton,
139
+ TAB_ACCESSORY_SCREEN_INSET,
140
+ TAB_ACCESSORY_THEME_ROLES,
141
+ tabAccessoryScreenInset,
142
+ type TabAccessoryButtonProps,
143
+ type TabAccessoryPlacement,
144
+ type TabAccessoryVariant,
145
+ } from "@studio-piot/native-ui";
146
+ ```
@@ -0,0 +1,327 @@
1
+ # Liquid Glass header buttons in Expo Router
2
+
3
+ Oct 2, 2026 · @Guillaume
4
+
5
+ ## How it works
6
+
7
+ On iOS 26, the native stack puts whatever `headerLeft` or `headerRight` returns inside a native `UIBarButtonItem`. iOS draws the Liquid Glass background around it automatically. Your job is to supply the content only: an icon, or an icon and a label.
8
+
9
+ - **Never add your own background or border.** A styled button inside the glass gives you two shapes, one inside the other.
10
+ - **The shape follows the content.** A square (36×36) gives a circle. Wider content gives a capsule.
11
+ - **One function, one glass shape.** Everything returned by `headerRight` shares one capsule, even when it contains several buttons.
12
+ - **Android and iOS 18 and earlier have no glass.** The same code shows plain icons there.
13
+
14
+ ## Icon only (round)
15
+
16
+ A 36×36 `Pressable` holding one SF Symbol becomes a glass circle. `size` sets the icon size and defaults to 17. The other examples reuse this component.
17
+
18
+ ```tsx
19
+ // components/header/HeaderIcon.tsx
20
+ import { useThemeColor } from "@/hooks/useThemeColor";
21
+ import { SFSymbol, SymbolView } from "expo-symbols";
22
+ import { Pressable } from "react-native";
23
+
24
+ type HeaderIconProps = {
25
+ name: SFSymbol;
26
+ label: string; // accessibility label, required for icon-only buttons
27
+ onPress: () => void;
28
+ size?: number; // icon size, default 17
29
+ weight?: "regular" | "medium" | "semibold" | "bold";
30
+ };
31
+
32
+ export function HeaderIcon({
33
+ name,
34
+ label,
35
+ onPress,
36
+ size = 17,
37
+ weight = "semibold",
38
+ }: HeaderIconProps) {
39
+ const text = useThemeColor("text");
40
+
41
+ return (
42
+ <Pressable
43
+ onPress={onPress}
44
+ hitSlop={6}
45
+ accessibilityRole="button"
46
+ accessibilityLabel={label}
47
+ style={({ pressed }) => ({
48
+ width: 36,
49
+ height: 36,
50
+ alignItems: "center",
51
+ justifyContent: "center",
52
+ opacity: pressed ? 0.5 : 1,
53
+ })}
54
+ >
55
+ <SymbolView name={name} size={size} weight={weight} tintColor={text} />
56
+ </Pressable>
57
+ );
58
+ }
59
+ ```
60
+
61
+ Usage:
62
+
63
+ ```tsx
64
+ <Stack.Screen
65
+ name="index"
66
+ options={{
67
+ headerLeft: () => (
68
+ <HeaderIcon name="xmark" label="Close" size={18} onPress={() => router.back()} />
69
+ ),
70
+ }}
71
+ />
72
+ ```
73
+
74
+ ## Capsule (icon and label)
75
+
76
+ An icon, a gap, a label and horizontal padding make content wider than it is tall, so iOS draws a pill. The icon defaults to 15 so it sits close to the height of the 16pt label.
77
+
78
+ ```tsx
79
+ // components/header/HeaderCapsule.tsx
80
+ import { useThemeColor } from "@/hooks/useThemeColor";
81
+ import { SFSymbol, SymbolView } from "expo-symbols";
82
+ import { Pressable, Text } from "react-native";
83
+
84
+ type HeaderCapsuleProps = {
85
+ icon: SFSymbol;
86
+ label: string;
87
+ onPress: () => void;
88
+ iconSize?: number; // default 15
89
+ fontSize?: number; // default 16
90
+ };
91
+
92
+ export function HeaderCapsule({
93
+ icon,
94
+ label,
95
+ onPress,
96
+ iconSize = 15,
97
+ fontSize = 16,
98
+ }: HeaderCapsuleProps) {
99
+ const text = useThemeColor("text");
100
+
101
+ return (
102
+ <Pressable
103
+ onPress={onPress}
104
+ accessibilityRole="button"
105
+ accessibilityLabel={label}
106
+ style={({ pressed }) => ({
107
+ flexDirection: "row",
108
+ alignItems: "center",
109
+ gap: 6,
110
+ height: 36,
111
+ paddingHorizontal: 12,
112
+ opacity: pressed ? 0.5 : 1,
113
+ })}
114
+ >
115
+ <SymbolView name={icon} size={iconSize} weight="semibold" tintColor={text} />
116
+ <Text style={{ color: text, fontSize, fontWeight: "600" }}>{label}</Text>
117
+ </Pressable>
118
+ );
119
+ }
120
+ ```
121
+
122
+ Usage:
123
+
124
+ ```tsx
125
+ headerLeft: () => (
126
+ <HeaderCapsule icon="xmark" label="Close" iconSize={16} onPress={() => router.back()} />
127
+ ),
128
+ ```
129
+
130
+ ## Multiple icons (bundled)
131
+
132
+ Put several `HeaderIcon`s in one row `View` and they share a single glass capsule. Each icon keeps its own size and action.
133
+
134
+ ```tsx
135
+ // components/header/HeaderGroup.tsx
136
+ import { ReactNode } from "react";
137
+ import { View } from "react-native";
138
+
139
+ export function HeaderGroup({ children }: { children: ReactNode }) {
140
+ return (
141
+ <View style={{ flexDirection: "row", alignItems: "center", gap: 4 }}>
142
+ {children}
143
+ </View>
144
+ );
145
+ }
146
+ ```
147
+
148
+ Usage:
149
+
150
+ ```tsx
151
+ headerRight: () => (
152
+ <HeaderGroup>
153
+ <HeaderIcon
154
+ name="bookmark"
155
+ label="Saved routes"
156
+ size={19}
157
+ onPress={() => router.push("/new-activity/saved-routes")}
158
+ />
159
+ <HeaderIcon
160
+ name="gearshape"
161
+ label="Settings"
162
+ onPress={() => router.push("/settings")}
163
+ />
164
+ </HeaderGroup>
165
+ ),
166
+ ```
167
+
168
+ A custom-view `headerRight` gives you one shared capsule. To get two separate circles, each button has to be its own native bar button item, which needs the native bar-button-items API in your Expo Router or react-native-screens version.
169
+
170
+ ## Header left and right
171
+
172
+ The full `_layout.tsx` for the New activity flow has a Close capsule on the left and a bundled pair of icons on the right. Child screens keep the default back button.
173
+
174
+ ```tsx
175
+ // app/new-activity/_layout.tsx
176
+ import { HeaderCapsule } from "@/components/header/HeaderCapsule";
177
+ import { HeaderGroup } from "@/components/header/HeaderGroup";
178
+ import { HeaderIcon } from "@/components/header/HeaderIcon";
179
+ import { useThemeColor } from "@/hooks/useThemeColor";
180
+ import { router, Stack } from "expo-router";
181
+
182
+ export default function NewActivityLayout() {
183
+ const background = useThemeColor("background");
184
+ const text = useThemeColor("text");
185
+
186
+ return (
187
+ <Stack
188
+ screenOptions={{
189
+ headerLargeTitle: false,
190
+ headerTintColor: text,
191
+ headerTitleStyle: { color: text },
192
+ headerStyle: { backgroundColor: background },
193
+ headerShadowVisible: false,
194
+ contentStyle: { backgroundColor: background },
195
+ }}
196
+ >
197
+ <Stack.Screen
198
+ name="index"
199
+ options={{
200
+ title: "New activity",
201
+ headerBackVisible: false,
202
+ headerLeft: () => (
203
+ <HeaderCapsule icon="xmark" label="Close" onPress={() => router.back()} />
204
+ ),
205
+ headerRight: () => (
206
+ <HeaderGroup>
207
+ <HeaderIcon
208
+ name="bookmark"
209
+ label="Saved routes"
210
+ size={19}
211
+ onPress={() => router.push("/new-activity/saved-routes")}
212
+ />
213
+ <HeaderIcon
214
+ name="gearshape"
215
+ label="Settings"
216
+ onPress={() => router.push("/settings")}
217
+ />
218
+ </HeaderGroup>
219
+ ),
220
+ }}
221
+ />
222
+ <Stack.Screen name="running" options={{ title: "Running" }} />
223
+ <Stack.Screen name="saved-routes" options={{ title: "Saved routes" }} />
224
+ <Stack.Screen name="hiit" options={{ title: "HIIT" }} />
225
+ </Stack>
226
+ );
227
+ }
228
+ ```
229
+
230
+ Route paths such as `/new-activity/saved-routes` and `/settings` are placeholders. Change them to match your app.
231
+
232
+ **Setting buttons from inside a screen.** When a button needs the screen's state, set the options in the screen file with `<Stack.Screen options={{ headerRight: … }} />` instead of in the layout:
233
+
234
+ ```tsx
235
+ // app/new-activity/running.tsx
236
+ export default function RunningScreen() {
237
+ const [favourite, setFavourite] = useState(false);
238
+
239
+ return (
240
+ <>
241
+ <Stack.Screen
242
+ options={{
243
+ headerRight: () => (
244
+ <HeaderIcon
245
+ name={favourite ? "heart.fill" : "heart"}
246
+ label={favourite ? "Remove favourite" : "Add favourite"}
247
+ onPress={() => setFavourite((f) => !f)}
248
+ />
249
+ ),
250
+ }}
251
+ />
252
+ {/* screen content */}
253
+ </>
254
+ );
255
+ }
256
+ ```
257
+
258
+ ## Actions
259
+
260
+ Every button takes an `onPress`, so any function works. These are the common ones.
261
+
262
+ | Goal | Call | Notes |
263
+ | --- | --- | --- |
264
+ | Go back one screen | `router.back()` | From the first screen of a nested stack, this closes the modal |
265
+ | Close the modal | `router.dismiss()` | Use when the stack is presented with `presentation: "modal"` |
266
+ | Close the whole flow from a deep screen | `router.dismissAll()` | Returns to the first screen of the stack. Follow with `router.back()` to leave the modal |
267
+ | Open another screen | `router.push("/path")` | Adds to the history |
268
+ | Swap the current screen | `router.replace("/path")` | No back step to the old screen |
269
+ | Open a sheet | `router.push("/sheet-route")` | Set `presentation: "formSheet"` on that route |
270
+ | Share | `Share.share({ message })` | From `react-native` |
271
+ | Toggle state | `setState(...)` | Define the button in the screen file (see above) |
272
+
273
+ **Safe close.** This closes the modal whether or not there is something to go back to:
274
+
275
+ ```tsx
276
+ import { router } from "expo-router";
277
+
278
+ export function closeFlow() {
279
+ if (router.canDismiss()) {
280
+ router.dismissAll();
281
+ }
282
+ router.back();
283
+ }
284
+ ```
285
+
286
+ **Confirm before discarding.** Ask before closing when the user has unsaved changes:
287
+
288
+ ```tsx
289
+ import { Alert } from "react-native";
290
+
291
+ function confirmClose(hasChanges: boolean) {
292
+ if (!hasChanges) return router.back();
293
+
294
+ Alert.alert("Discard activity?", "Your changes will be lost.", [
295
+ { text: "Keep editing", style: "cancel" },
296
+ { text: "Discard", style: "destructive", onPress: () => router.back() },
297
+ ]);
298
+ }
299
+ ```
300
+
301
+ **Haptic tap.** A light haptic makes the button feel native. Add it inside `HeaderIcon`'s `onPress`:
302
+
303
+ ```tsx
304
+ import * as Haptics from "expo-haptics";
305
+
306
+ onPress={() => {
307
+ Haptics.selectionAsync();
308
+ onPress();
309
+ }}
310
+ ```
311
+
312
+ ## Sizing and gotchas
313
+
314
+ | Element | Value | Why |
315
+ | --- | --- | --- |
316
+ | Round button box | 36×36 | Gives a circle that matches system buttons |
317
+ | Icon in a round button | 17 (range 15–20) | Above about 24 the icon fills the circle and looks cramped |
318
+ | Icon in a capsule | 15–16 | Close to the height of a 16pt label |
319
+ | Capsule label | 16pt, weight 600 | Matches system bar button text |
320
+ | Gap between bundled icons | 4 | Keeps the capsule tight |
321
+ | `hitSlop` | 6–8 | A bigger tap area without changing the glass size |
322
+
323
+ - **Thin symbols look smaller.** `xmark` fills less of its box than `gearshape` at the same size. Going about 1pt bigger on thin symbols makes up for it.
324
+ - **`SymbolView` `size` is a box, not a font size.** Wide or tall symbols are shrunk to fit, so check alignment on a device.
325
+ - **An empty `View` still shows glass.** `headerLeft: () => <View />` draws an empty circle. Hide the button with `headerLeft: undefined` instead.
326
+ - **Always set `accessibilityLabel`.** VoiceOver has no text to read on icon-only buttons otherwise.
327
+ - **No glass outside iOS 26.** If you want a visible circle on Android or older iOS, add a background only on those platforms.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@studio-piot/native-ui",
3
- "version": "0.2.5",
3
+ "version": "0.16.0",
4
4
  "description": "Shared React Native components for Studio Piot apps.",
5
5
  "homepage": "https://github.com/Studio-Piot/studio-native-ui#readme",
6
6
  "bugs": {