react-native-terra-ui 0.1.0 → 0.2.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 (304) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/lib/module/components/avatar/Avatar.js +118 -35
  3. package/lib/module/components/avatar/Avatar.js.map +1 -1
  4. package/lib/module/components/avatar/index.js.map +1 -1
  5. package/lib/module/components/box/Box.js +15 -2
  6. package/lib/module/components/box/Box.js.map +1 -1
  7. package/lib/module/components/box/index.js.map +1 -1
  8. package/lib/module/components/box/utils.js +5 -2
  9. package/lib/module/components/box/utils.js.map +1 -1
  10. package/lib/module/components/button/Button.js +7 -14
  11. package/lib/module/components/button/Button.js.map +1 -1
  12. package/lib/module/components/button/index.js.map +1 -1
  13. package/lib/module/components/chip/Chip.js +372 -0
  14. package/lib/module/components/chip/Chip.js.map +1 -0
  15. package/lib/module/components/chip/index.js +4 -0
  16. package/lib/module/components/chip/index.js.map +1 -0
  17. package/lib/module/components/chip/types.js +2 -0
  18. package/lib/module/components/chip/types.js.map +1 -0
  19. package/lib/module/components/chip/utils.js +36 -0
  20. package/lib/module/components/chip/utils.js.map +1 -0
  21. package/lib/module/components/empty-state/EmptyState.js +200 -0
  22. package/lib/module/components/empty-state/EmptyState.js.map +1 -0
  23. package/lib/module/components/empty-state/index.js +4 -0
  24. package/lib/module/components/empty-state/index.js.map +1 -0
  25. package/lib/module/components/empty-state/types.js +2 -0
  26. package/lib/module/components/empty-state/types.js.map +1 -0
  27. package/lib/module/components/header/Header.js.map +1 -1
  28. package/lib/module/components/header/header-buttons.js +31 -36
  29. package/lib/module/components/header/header-buttons.js.map +1 -1
  30. package/lib/module/components/header/index.js +1 -1
  31. package/lib/module/components/header/index.js.map +1 -1
  32. package/lib/module/components/header/parts/HeaderGradientOverlay.js +38 -0
  33. package/lib/module/components/header/parts/HeaderGradientOverlay.js.map +1 -0
  34. package/lib/module/components/header/variants/LargeTitleHeader/index.js +71 -101
  35. package/lib/module/components/header/variants/LargeTitleHeader/index.js.map +1 -1
  36. package/lib/module/components/header/variants/TitleHeader/index.js +34 -65
  37. package/lib/module/components/header/variants/TitleHeader/index.js.map +1 -1
  38. package/lib/module/components/icon/Icon.js +21 -9
  39. package/lib/module/components/icon/Icon.js.map +1 -1
  40. package/lib/module/components/icon/index.js.map +1 -1
  41. package/lib/module/components/image/Image.js +54 -0
  42. package/lib/module/components/image/Image.js.map +1 -0
  43. package/lib/module/components/image/index.js +4 -0
  44. package/lib/module/components/image/index.js.map +1 -0
  45. package/lib/module/components/image/types.js +4 -0
  46. package/lib/module/components/image/types.js.map +1 -0
  47. package/lib/module/components/index.js +4 -0
  48. package/lib/module/components/index.js.map +1 -1
  49. package/lib/module/components/page-indicator/PageIndicator.js.map +1 -1
  50. package/lib/module/components/page-indicator/PageIndicatorRoot.js.map +1 -1
  51. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js +1 -1
  52. package/lib/module/components/page-indicator/hooks/use-page-indicator-colors.js.map +1 -1
  53. package/lib/module/components/page-indicator/hooks/use-page-indicator-core.js.map +1 -1
  54. package/lib/module/components/page-indicator/hooks/use-page-indicator-progress.js.map +1 -1
  55. package/lib/module/components/page-indicator/index.js.map +1 -1
  56. package/lib/module/components/page-indicator/variants/DotIndicator/InactiveDot.js.map +1 -1
  57. package/lib/module/components/page-indicator/variants/DotIndicator/LoadingOrbit.js.map +1 -1
  58. package/lib/module/components/page-indicator/variants/DotIndicator/TravelingPill.js.map +1 -1
  59. package/lib/module/components/page-indicator/variants/DotIndicator/index.js.map +1 -1
  60. package/lib/module/components/page-indicator/variants/PillIndicator/PillIndicatorDot.js.map +1 -1
  61. package/lib/module/components/page-indicator/variants/PillIndicator/index.js.map +1 -1
  62. package/lib/module/components/portal/Portal.js +42 -0
  63. package/lib/module/components/portal/Portal.js.map +1 -0
  64. package/lib/module/components/portal/PortalContext.js +1 -16
  65. package/lib/module/components/portal/PortalContext.js.map +1 -1
  66. package/lib/module/components/portal/PortalHost.js +16 -14
  67. package/lib/module/components/portal/PortalHost.js.map +1 -1
  68. package/lib/module/components/portal/PortalProvider.js +51 -19
  69. package/lib/module/components/portal/PortalProvider.js.map +1 -1
  70. package/lib/module/components/portal/index.js +1 -1
  71. package/lib/module/components/portal/index.js.map +1 -1
  72. package/lib/module/components/screen/Screen.js +26 -29
  73. package/lib/module/components/screen/Screen.js.map +1 -1
  74. package/lib/module/components/screen/ScreenContext.js +7 -5
  75. package/lib/module/components/screen/ScreenContext.js.map +1 -1
  76. package/lib/module/components/screen/ScreenFlashList.js +144 -0
  77. package/lib/module/components/screen/ScreenFlashList.js.map +1 -0
  78. package/lib/module/components/screen/ScreenFlatList.js +88 -27
  79. package/lib/module/components/screen/ScreenFlatList.js.map +1 -1
  80. package/lib/module/components/screen/ScreenScrollView.js +101 -34
  81. package/lib/module/components/screen/ScreenScrollView.js.map +1 -1
  82. package/lib/module/components/screen/index.js +1 -0
  83. package/lib/module/components/screen/index.js.map +1 -1
  84. package/lib/module/components/screen/parts/BottomSafeArea.js +62 -0
  85. package/lib/module/components/screen/parts/BottomSafeArea.js.map +1 -0
  86. package/lib/module/components/screen/parts/ScreenPlaceholder.js +50 -0
  87. package/lib/module/components/screen/parts/ScreenPlaceholder.js.map +1 -0
  88. package/lib/module/components/screen/types.js +4 -0
  89. package/lib/module/components/screen/types.js.map +1 -0
  90. package/lib/module/components/screen/utils.js +12 -9
  91. package/lib/module/components/screen/utils.js.map +1 -1
  92. package/lib/module/components/slot/Slot.js.map +1 -1
  93. package/lib/module/components/slot/index.js.map +1 -1
  94. package/lib/module/components/spinner/Spinner.js +1 -1
  95. package/lib/module/components/spinner/Spinner.js.map +1 -1
  96. package/lib/module/components/spinner/index.js.map +1 -1
  97. package/lib/module/components/surface/Surface.js +16 -9
  98. package/lib/module/components/surface/Surface.js.map +1 -1
  99. package/lib/module/components/surface/index.js.map +1 -1
  100. package/lib/module/components/text/Text.js +25 -7
  101. package/lib/module/components/text/Text.js.map +1 -1
  102. package/lib/module/components/text/index.js.map +1 -1
  103. package/lib/module/components/toast/Toast.js +8 -8
  104. package/lib/module/components/toast/Toast.js.map +1 -1
  105. package/lib/module/components/toast/ToastProvider.js.map +1 -1
  106. package/lib/module/components/toast/index.js.map +1 -1
  107. package/lib/module/components/toolbar/Toolbar.js +268 -0
  108. package/lib/module/components/toolbar/Toolbar.js.map +1 -0
  109. package/lib/module/components/toolbar/index.js +4 -0
  110. package/lib/module/components/toolbar/index.js.map +1 -0
  111. package/lib/module/context/ThemeProvider.js +5 -2
  112. package/lib/module/context/ThemeProvider.js.map +1 -1
  113. package/lib/module/context/index.js.map +1 -1
  114. package/lib/module/index.js.map +1 -1
  115. package/lib/module/{components/icon/utils.js → theme/defaults/icons.js} +1 -1
  116. package/lib/module/theme/defaults/icons.js.map +1 -0
  117. package/lib/module/theme/defaults/image.js +30 -0
  118. package/lib/module/theme/defaults/image.js.map +1 -0
  119. package/lib/module/theme/index.js +6 -6
  120. package/lib/module/theme/index.js.map +1 -1
  121. package/lib/module/theme/{configure.js → registry.js} +23 -30
  122. package/lib/module/theme/registry.js.map +1 -0
  123. package/lib/module/theme/runtime.js +1 -1
  124. package/lib/module/theme/runtime.js.map +1 -1
  125. package/lib/module/theme/theme.js +92 -11
  126. package/lib/module/theme/theme.js.map +1 -1
  127. package/lib/module/theme/tokens/dark.js +59 -68
  128. package/lib/module/theme/tokens/dark.js.map +1 -1
  129. package/lib/module/theme/tokens/light.js +49 -67
  130. package/lib/module/theme/tokens/light.js.map +1 -1
  131. package/lib/module/theme/tokens/primitives.js +241 -20
  132. package/lib/module/theme/tokens/primitives.js.map +1 -1
  133. package/lib/module/utils/accent-utils.js +4 -4
  134. package/lib/module/utils/accent-utils.js.map +1 -1
  135. package/lib/typescript/src/components/avatar/Avatar.d.ts.map +1 -1
  136. package/lib/typescript/src/components/avatar/types.d.ts +5 -5
  137. package/lib/typescript/src/components/avatar/types.d.ts.map +1 -1
  138. package/lib/typescript/src/components/box/Box.d.ts.map +1 -1
  139. package/lib/typescript/src/components/box/utils.d.ts +6 -1
  140. package/lib/typescript/src/components/box/utils.d.ts.map +1 -1
  141. package/lib/typescript/src/components/button/Button.d.ts +2 -2
  142. package/lib/typescript/src/components/button/Button.d.ts.map +1 -1
  143. package/lib/typescript/src/components/chip/Chip.d.ts +24 -0
  144. package/lib/typescript/src/components/chip/Chip.d.ts.map +1 -0
  145. package/lib/typescript/src/components/chip/index.d.ts +4 -0
  146. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  147. package/lib/typescript/src/components/chip/types.d.ts +8 -0
  148. package/lib/typescript/src/components/chip/types.d.ts.map +1 -0
  149. package/lib/typescript/src/components/chip/utils.d.ts +8 -0
  150. package/lib/typescript/src/components/chip/utils.d.ts.map +1 -0
  151. package/lib/typescript/src/components/empty-state/EmptyState.d.ts +45 -0
  152. package/lib/typescript/src/components/empty-state/EmptyState.d.ts.map +1 -0
  153. package/lib/typescript/src/components/empty-state/index.d.ts +4 -0
  154. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  155. package/lib/typescript/src/components/empty-state/types.d.ts +6 -0
  156. package/lib/typescript/src/components/empty-state/types.d.ts.map +1 -0
  157. package/lib/typescript/src/components/header/header-buttons.d.ts +27 -0
  158. package/lib/typescript/src/components/header/header-buttons.d.ts.map +1 -1
  159. package/lib/typescript/src/components/header/index.d.ts +1 -1
  160. package/lib/typescript/src/components/header/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts +7 -0
  162. package/lib/typescript/src/components/header/parts/HeaderGradientOverlay.d.ts.map +1 -0
  163. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts +1 -1
  164. package/lib/typescript/src/components/header/variants/LargeTitleHeader/index.d.ts.map +1 -1
  165. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts +19 -8
  166. package/lib/typescript/src/components/header/variants/TitleHeader/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/icon/Icon.d.ts +1 -1
  168. package/lib/typescript/src/components/icon/Icon.d.ts.map +1 -1
  169. package/lib/typescript/src/components/image/Image.d.ts +16 -0
  170. package/lib/typescript/src/components/image/Image.d.ts.map +1 -0
  171. package/lib/typescript/src/components/image/index.d.ts +3 -0
  172. package/lib/typescript/src/components/image/index.d.ts.map +1 -0
  173. package/lib/typescript/src/components/image/types.d.ts +7 -0
  174. package/lib/typescript/src/components/image/types.d.ts.map +1 -0
  175. package/lib/typescript/src/components/index.d.ts +4 -0
  176. package/lib/typescript/src/components/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/page-indicator/PageIndicator.d.ts +1 -1
  178. package/lib/typescript/src/components/page-indicator/hooks/use-page-indicator-colors.d.ts +1 -1
  179. package/lib/typescript/src/components/page-indicator/variants/DotIndicator/index.d.ts +1 -1
  180. package/lib/typescript/src/components/page-indicator/variants/PillIndicator/index.d.ts +1 -1
  181. package/lib/typescript/src/components/portal/Portal.d.ts +26 -0
  182. package/lib/typescript/src/components/portal/Portal.d.ts.map +1 -0
  183. package/lib/typescript/src/components/portal/PortalContext.d.ts +11 -14
  184. package/lib/typescript/src/components/portal/PortalContext.d.ts.map +1 -1
  185. package/lib/typescript/src/components/portal/PortalHost.d.ts +9 -4
  186. package/lib/typescript/src/components/portal/PortalHost.d.ts.map +1 -1
  187. package/lib/typescript/src/components/portal/PortalProvider.d.ts +3 -3
  188. package/lib/typescript/src/components/portal/PortalProvider.d.ts.map +1 -1
  189. package/lib/typescript/src/components/portal/index.d.ts +4 -2
  190. package/lib/typescript/src/components/portal/index.d.ts.map +1 -1
  191. package/lib/typescript/src/components/screen/Screen.d.ts +19 -14
  192. package/lib/typescript/src/components/screen/Screen.d.ts.map +1 -1
  193. package/lib/typescript/src/components/screen/ScreenContext.d.ts +12 -9
  194. package/lib/typescript/src/components/screen/ScreenContext.d.ts.map +1 -1
  195. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts +43 -0
  196. package/lib/typescript/src/components/screen/ScreenFlashList.d.ts.map +1 -0
  197. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts +17 -7
  198. package/lib/typescript/src/components/screen/ScreenFlatList.d.ts.map +1 -1
  199. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts +51 -16
  200. package/lib/typescript/src/components/screen/ScreenScrollView.d.ts.map +1 -1
  201. package/lib/typescript/src/components/screen/index.d.ts +3 -0
  202. package/lib/typescript/src/components/screen/index.d.ts.map +1 -1
  203. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts +30 -0
  204. package/lib/typescript/src/components/screen/parts/BottomSafeArea.d.ts.map +1 -0
  205. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts +22 -0
  206. package/lib/typescript/src/components/screen/parts/ScreenPlaceholder.d.ts.map +1 -0
  207. package/lib/typescript/src/components/screen/types.d.ts +42 -0
  208. package/lib/typescript/src/components/screen/types.d.ts.map +1 -0
  209. package/lib/typescript/src/components/screen/utils.d.ts +9 -9
  210. package/lib/typescript/src/components/screen/utils.d.ts.map +1 -1
  211. package/lib/typescript/src/components/spinner/Spinner.d.ts +1 -1
  212. package/lib/typescript/src/components/surface/Surface.d.ts +8 -1
  213. package/lib/typescript/src/components/surface/Surface.d.ts.map +1 -1
  214. package/lib/typescript/src/components/text/Text.d.ts +1 -1
  215. package/lib/typescript/src/components/text/Text.d.ts.map +1 -1
  216. package/lib/typescript/src/components/toast/Toast.d.ts +1 -1
  217. package/lib/typescript/src/components/toolbar/Toolbar.d.ts +115 -0
  218. package/lib/typescript/src/components/toolbar/Toolbar.d.ts.map +1 -0
  219. package/lib/typescript/src/components/toolbar/index.d.ts +3 -0
  220. package/lib/typescript/src/components/toolbar/index.d.ts.map +1 -0
  221. package/lib/typescript/src/context/ThemeProvider.d.ts.map +1 -1
  222. package/lib/typescript/src/{components/icon/utils.d.ts → theme/defaults/icons.d.ts} +2 -2
  223. package/lib/typescript/src/theme/defaults/icons.d.ts.map +1 -0
  224. package/lib/typescript/src/theme/defaults/image.d.ts +4 -0
  225. package/lib/typescript/src/theme/defaults/image.d.ts.map +1 -0
  226. package/lib/typescript/src/theme/index.d.ts +7 -7
  227. package/lib/typescript/src/theme/index.d.ts.map +1 -1
  228. package/lib/typescript/src/theme/{configure.d.ts → registry.d.ts} +1 -1
  229. package/lib/typescript/src/theme/registry.d.ts.map +1 -0
  230. package/lib/typescript/src/theme/theme.d.ts +21 -1
  231. package/lib/typescript/src/theme/theme.d.ts.map +1 -1
  232. package/lib/typescript/src/theme/tokens/dark.d.ts +41 -64
  233. package/lib/typescript/src/theme/tokens/dark.d.ts.map +1 -1
  234. package/lib/typescript/src/theme/tokens/light.d.ts +44 -65
  235. package/lib/typescript/src/theme/tokens/light.d.ts.map +1 -1
  236. package/lib/typescript/src/theme/tokens/primitives.d.ts +228 -17
  237. package/lib/typescript/src/theme/tokens/primitives.d.ts.map +1 -1
  238. package/lib/typescript/src/theme/types.d.ts +66 -36
  239. package/lib/typescript/src/theme/types.d.ts.map +1 -1
  240. package/lib/typescript/src/utils/accent-utils.d.ts +1 -1
  241. package/lib/typescript/src/utils/resolve-theme-color.d.ts +1 -1
  242. package/lib/typescript/src/utils/typography.d.ts +1 -1
  243. package/package.json +28 -12
  244. package/skills/terra-ui/SKILL.md +193 -0
  245. package/skills/terra-ui/references/recipes.md +198 -0
  246. package/skills/terra-ui/references/troubleshooting.md +112 -0
  247. package/src/components/avatar/Avatar.tsx +128 -38
  248. package/src/components/avatar/types.ts +5 -5
  249. package/src/components/box/Box.tsx +13 -1
  250. package/src/components/box/utils.ts +10 -0
  251. package/src/components/button/Button.tsx +7 -15
  252. package/src/components/chip/Chip.tsx +444 -0
  253. package/src/components/chip/index.ts +3 -0
  254. package/src/components/chip/types.ts +15 -0
  255. package/src/components/chip/utils.ts +34 -0
  256. package/src/components/empty-state/EmptyState.tsx +209 -0
  257. package/src/components/empty-state/index.ts +3 -0
  258. package/src/components/empty-state/types.ts +5 -0
  259. package/src/components/header/header-buttons.tsx +33 -45
  260. package/src/components/header/index.ts +2 -0
  261. package/src/components/header/parts/HeaderGradientOverlay.tsx +29 -0
  262. package/src/components/header/variants/LargeTitleHeader/index.tsx +68 -102
  263. package/src/components/header/variants/TitleHeader/index.tsx +47 -56
  264. package/src/components/icon/Icon.tsx +20 -15
  265. package/src/components/image/Image.tsx +48 -0
  266. package/src/components/image/index.ts +2 -0
  267. package/src/components/image/types.ts +7 -0
  268. package/src/components/index.ts +4 -0
  269. package/src/components/portal/Portal.tsx +55 -0
  270. package/src/components/portal/PortalContext.tsx +14 -22
  271. package/src/components/portal/PortalHost.tsx +17 -10
  272. package/src/components/portal/PortalProvider.tsx +58 -21
  273. package/src/components/portal/index.ts +4 -2
  274. package/src/components/screen/Screen.tsx +45 -59
  275. package/src/components/screen/ScreenContext.tsx +19 -12
  276. package/src/components/screen/ScreenFlashList.tsx +196 -0
  277. package/src/components/screen/ScreenFlatList.tsx +100 -39
  278. package/src/components/screen/ScreenScrollView.tsx +114 -53
  279. package/src/components/screen/index.ts +3 -0
  280. package/src/components/screen/parts/BottomSafeArea.tsx +60 -0
  281. package/src/components/screen/parts/ScreenPlaceholder.tsx +55 -0
  282. package/src/components/screen/types.ts +43 -0
  283. package/src/components/screen/utils.ts +29 -15
  284. package/src/components/surface/Surface.tsx +17 -20
  285. package/src/components/text/Text.tsx +23 -5
  286. package/src/components/toast/Toast.tsx +8 -8
  287. package/src/components/toolbar/Toolbar.tsx +353 -0
  288. package/src/components/toolbar/index.ts +6 -0
  289. package/src/context/ThemeProvider.tsx +5 -4
  290. package/src/{components/icon/utils.tsx → theme/defaults/icons.tsx} +1 -1
  291. package/src/theme/defaults/image.tsx +34 -0
  292. package/src/theme/index.ts +1 -1
  293. package/src/theme/{configure.ts → registry.ts} +19 -34
  294. package/src/theme/runtime.ts +1 -1
  295. package/src/theme/theme.ts +127 -13
  296. package/src/theme/tokens/dark.ts +60 -69
  297. package/src/theme/tokens/light.ts +50 -68
  298. package/src/theme/tokens/primitives.ts +248 -20
  299. package/src/theme/types.ts +67 -35
  300. package/src/utils/accent-utils.ts +4 -4
  301. package/lib/module/components/icon/utils.js.map +0 -1
  302. package/lib/module/theme/configure.js.map +0 -1
  303. package/lib/typescript/src/components/icon/utils.d.ts.map +0 -1
  304. package/lib/typescript/src/theme/configure.d.ts.map +0 -1
@@ -0,0 +1,353 @@
1
+ import {
2
+ Children,
3
+ type ComponentRef,
4
+ createContext,
5
+ forwardRef,
6
+ type ReactNode,
7
+ useContext,
8
+ useMemo,
9
+ } from 'react';
10
+ import {
11
+ Pressable,
12
+ type PressableProps,
13
+ View,
14
+ type ViewProps,
15
+ } from 'react-native';
16
+
17
+ import { StyleSheet } from 'react-native-unistyles';
18
+
19
+ import type { ColorToken, SpacingKey, TerraIconName } from '#theme/types';
20
+ import { resolveThemeColor } from '#utils/resolve-theme-color';
21
+
22
+ import { Icon } from '../icon';
23
+ import { Text } from '../text';
24
+
25
+ // ─── Context (lets Toolbar.Button inherit the group's paint) ──────────────────
26
+ //
27
+ // Both colors are unresolved **tokens**, never resolved values — the button and
28
+ // its `Text`/`Icon` resolve them against the live theme, so each leaf owns its
29
+ // own subscription and keeps repainting on a scheme switch however the group
30
+ // above it was rendered. Same contract as `ChipContext`.
31
+
32
+ interface ToolbarGroupContextValue {
33
+ /** Fill token for a button's own capsule. A group sets `'transparent'`. */
34
+ background: ColorToken;
35
+ /** Foreground token inherited by each button's icon and label. */
36
+ tint: ColorToken;
37
+ /**
38
+ * True inside a {@link ToolbarGroup}. Adjacent buttons share a cell edge, so
39
+ * a horizontal `hitSlop` would reach into a neighbour's bounds and steal
40
+ * taps from it — grouped buttons expand vertically only.
41
+ */
42
+ isGrouped: boolean;
43
+ }
44
+
45
+ /** Non-null default, so a bare `Toolbar.Button` still paints its own circle. */
46
+ const ToolbarGroupContext = createContext<ToolbarGroupContextValue>({
47
+ background: 'surface.default',
48
+ tint: 'text.default',
49
+ isGrouped: false,
50
+ });
51
+
52
+ /** Square cell of an icon-only item, in dp. */
53
+ const ITEM_SIZE = 40;
54
+
55
+ /** Touch expansion outside the cell, in dp. */
56
+ const HIT_SLOP = 8;
57
+
58
+ /** Typographic role of a text item's label. */
59
+ const LABEL_VARIANT = 'label-md';
60
+
61
+ // ─── Button ───────────────────────────────────────────────────────────────────
62
+
63
+ interface ToolbarButtonBaseProps
64
+ extends Omit<PressableProps, 'children' | 'style'> {
65
+ /** Icon to render, before the label when both are given. */
66
+ icon?: TerraIconName;
67
+ /** Icon size in density-independent pixels. Defaults to `20`. */
68
+ iconSize?: number;
69
+ /**
70
+ * Foreground token for the icon and label. Defaults to the group's `tint`,
71
+ * or `text.default` outside a group.
72
+ */
73
+ tint?: ColorToken;
74
+ /**
75
+ * Fill. Defaults to `surface.default` standalone, and to `transparent`
76
+ * inside a {@link ToolbarGroup}, which paints the shared capsule instead.
77
+ * Set it explicitly to opt back out of the group's fill.
78
+ */
79
+ background?: ColorToken;
80
+ /**
81
+ * Label content. A `string` or `number` renders as themed text in the
82
+ * button's `tint`; anything else renders as given. The cell grows
83
+ * horizontally to fit, so a text item is a pill rather than a circle.
84
+ */
85
+ children?: ReactNode;
86
+ /**
87
+ * Merged over the base style, so a single item can be repositioned (e.g.
88
+ * pinned over a hero image) without a wrapper view. Supports `Pressable`'s
89
+ * function form for press-state styling.
90
+ */
91
+ style?: PressableProps['style'];
92
+ }
93
+
94
+ /**
95
+ * An icon-only button has no visible text, so it must carry an
96
+ * `accessibilityLabel`; one with children may rely on them instead.
97
+ */
98
+ export type ToolbarButtonProps = ToolbarButtonBaseProps &
99
+ (
100
+ | { children: ReactNode; accessibilityLabel?: string }
101
+ | { icon: TerraIconName; accessibilityLabel: string }
102
+ );
103
+
104
+ /**
105
+ * An action inside a toolbar — a circular icon button, a text pill, or both.
106
+ * Paints its own capsule standalone, and goes transparent inside a
107
+ * {@link ToolbarGroup} so the group's shared capsule shows through.
108
+ *
109
+ * @example
110
+ * ```tsx
111
+ * <Toolbar.Button icon="share" accessibilityLabel="Share" onPress={share} />
112
+ * <Toolbar.Button onPress={save}>Save</Toolbar.Button>
113
+ * ```
114
+ */
115
+ export function ToolbarButton({
116
+ icon,
117
+ iconSize = 20,
118
+ tint,
119
+ background,
120
+ children,
121
+ accessibilityLabel,
122
+ onPress,
123
+ hitSlop = HIT_SLOP,
124
+ style,
125
+ ...rest
126
+ }: ToolbarButtonProps) {
127
+ const {
128
+ background: groupBackground,
129
+ tint: groupTint,
130
+ isGrouped,
131
+ } = useContext(ToolbarGroupContext);
132
+
133
+ // Tokens, not resolved values: `Icon` and `Text` each resolve against the
134
+ // live theme, so they keep their own subscriptions.
135
+ const fg = tint ?? groupTint;
136
+ const bg = background ?? groupBackground;
137
+ const shape = Children.count(children) > 0 ? 'label' : 'icon';
138
+
139
+ styles.useVariants({ shape, grouped: isGrouped });
140
+
141
+ // Read during render rather than inside the `style` callback below. The
142
+ // callback can run after sibling buttons have rendered, and `useVariants` is
143
+ // set per render pass, so resolving in there risks picking up whichever
144
+ // sibling called it last. Reading here pins this instance's variants. (The
145
+ // Jest mock resolves both spellings identically, so the difference only shows
146
+ // on a device — hence the comment rather than a test.)
147
+ const cell = styles.button;
148
+
149
+ return (
150
+ <Pressable
151
+ accessibilityLabel={accessibilityLabel}
152
+ accessibilityRole="button"
153
+ hitSlop={hitSlop}
154
+ onPress={onPress}
155
+ {...rest}
156
+ style={(state) => [
157
+ cell,
158
+ styles.fill(bg, state.pressed),
159
+ typeof style === 'function' ? style(state) : style,
160
+ ]}
161
+ >
162
+ {icon !== undefined && <Icon size={iconSize} color={fg} name={icon} />}
163
+ {renderLabel(children, fg)}
164
+ </Pressable>
165
+ );
166
+ }
167
+
168
+ /** Auto-wraps text children in the button's tint, as `Chip` does its label. */
169
+ function renderLabel(children: ReactNode, tint: ColorToken): ReactNode {
170
+ return Children.map(children, (child) => {
171
+ if (typeof child === 'string') {
172
+ return child.trim().length > 0 ? (
173
+ <Text variant={LABEL_VARIANT} color={tint} weight="bold">
174
+ {child}
175
+ </Text>
176
+ ) : null;
177
+ }
178
+ if (typeof child === 'number') {
179
+ return (
180
+ <Text variant={LABEL_VARIANT} color={tint} weight="bold">
181
+ {child}
182
+ </Text>
183
+ );
184
+ }
185
+ return child;
186
+ });
187
+ }
188
+
189
+ // ─── Group ────────────────────────────────────────────────────────────────────
190
+
191
+ export interface ToolbarGroupProps extends ViewProps {
192
+ /** Capsule fill. Defaults to `surface.default`. */
193
+ background?: ColorToken;
194
+ /**
195
+ * Foreground token inherited by every item's icon and label. Defaults to
196
+ * `text.default`; a per-item `tint` still wins.
197
+ */
198
+ tint?: ColorToken;
199
+
200
+ children?: ReactNode;
201
+ }
202
+
203
+ /**
204
+ * Wraps several {@link ToolbarButton}s in one shared capsule — the iOS
205
+ * `ToolbarItemGroup` look. Children drop their individual circular fill and
206
+ * inherit the group's, so the row reads as a single segmented pill.
207
+ *
208
+ * No height of its own: the capsule takes the height of its tallest child, and
209
+ * its full radius clamps to half of that for an exact stadium.
210
+ *
211
+ * @example
212
+ * ```tsx
213
+ * <Toolbar.Group>
214
+ * <Toolbar.Button icon="add" accessibilityLabel="Add" onPress={add} />
215
+ * <Toolbar.Button icon="download" accessibilityLabel="Save" onPress={save} />
216
+ * </Toolbar.Group>
217
+ * ```
218
+ */
219
+ export const ToolbarGroup = forwardRef<
220
+ ComponentRef<typeof View>,
221
+ ToolbarGroupProps
222
+ >(function ToolbarGroup(
223
+ {
224
+ background = 'surface.default',
225
+ tint = 'text.default',
226
+ children,
227
+ style,
228
+ ...rest
229
+ },
230
+ ref
231
+ ) {
232
+ const context = useMemo<ToolbarGroupContextValue>(
233
+ () => ({ background: 'transparent', tint, isGrouped: true }),
234
+ [tint]
235
+ );
236
+
237
+ return (
238
+ <ToolbarGroupContext.Provider value={context}>
239
+ <View ref={ref} style={[styles.capsule(background), style]} {...rest}>
240
+ {children}
241
+ </View>
242
+ </ToolbarGroupContext.Provider>
243
+ );
244
+ });
245
+
246
+ // ─── Root ─────────────────────────────────────────────────────────────────────
247
+
248
+ export interface ToolbarProps extends ViewProps {
249
+ /**
250
+ * Space between groups and standalone items. Defaults to `'2'` (8dp) — the
251
+ * separator between a capsule and an adjacent standalone item.
252
+ */
253
+ gap?: SpacingKey;
254
+ children?: ReactNode;
255
+ }
256
+
257
+ /**
258
+ * A row of {@link ToolbarGroup}s and standalone {@link ToolbarButton}s. The
259
+ * header's `LeftToolbar` / `RightToolbar` slots supply one automatically, so
260
+ * you only need to render it yourself when placing a toolbar elsewhere — over
261
+ * a hero image, or floating above content.
262
+ *
263
+ * @example
264
+ * ```tsx
265
+ * <Toolbar style={{ position: 'absolute', right: 16, bottom: 24 }}>
266
+ * <Toolbar.Group>
267
+ * <Toolbar.Button icon="add" accessibilityLabel="Add" onPress={add} />
268
+ * <Toolbar.Button icon="download" accessibilityLabel="Save" onPress={save} />
269
+ * </Toolbar.Group>
270
+ * <Toolbar.Button icon="share" accessibilityLabel="Share" onPress={share} />
271
+ * </Toolbar>
272
+ * ```
273
+ */
274
+ const ToolbarRoot = forwardRef<ComponentRef<typeof View>, ToolbarProps>(
275
+ function Toolbar({ gap = '2', children, style, ...rest }, ref) {
276
+ return (
277
+ <View ref={ref} style={[styles.row(gap), style]} {...rest}>
278
+ {children}
279
+ </View>
280
+ );
281
+ }
282
+ );
283
+
284
+ type ToolbarComponent = typeof ToolbarRoot & {
285
+ Group: typeof ToolbarGroup;
286
+ Button: typeof ToolbarButton;
287
+ };
288
+
289
+ export const Toolbar = ToolbarRoot as ToolbarComponent;
290
+ Toolbar.Group = ToolbarGroup;
291
+ Toolbar.Button = ToolbarButton;
292
+
293
+ // ─── Styles ───────────────────────────────────────────────────────────────────
294
+
295
+ const styles = StyleSheet.create((theme) => ({
296
+ row: (gap: SpacingKey) => ({
297
+ flexDirection: 'row',
298
+ alignItems: 'center',
299
+ gap: theme.spacing[gap],
300
+ }),
301
+ // `background` is an open `ColorToken` — it accepts raw literals, so it cannot
302
+ // be enumerated as a variant. Resolving inside the factory keeps the fill tied
303
+ // to the live theme.
304
+ capsule: (background: ColorToken) => ({
305
+ flexDirection: 'row',
306
+ alignItems: 'center',
307
+ gap: theme.spacing['5'],
308
+ paddingHorizontal: 10,
309
+ borderRadius: theme.radius.full,
310
+ backgroundColor: resolveThemeColor(background, theme),
311
+ borderWidth: StyleSheet.hairlineWidth,
312
+ borderColor: theme.color['border.subtle'],
313
+ }),
314
+ // Geometry is two finite axes, so it belongs in `variants` rather than in
315
+ // conditionals: an icon-only cell is an exact square, a labelled one keeps the
316
+ // height and grows; a grouped cell drops its own padding because the capsule
317
+ // supplies it.
318
+ button: {
319
+ height: ITEM_SIZE,
320
+ flexDirection: 'row',
321
+ alignItems: 'center',
322
+ justifyContent: 'center',
323
+ borderRadius: theme.radius.full,
324
+ variants: {
325
+ shape: {
326
+ icon: { width: ITEM_SIZE },
327
+ label: { gap: theme.spacing['1'] },
328
+ },
329
+ grouped: {
330
+ true: { paddingHorizontal: 0 },
331
+ false: {
332
+ minWidth: ITEM_SIZE,
333
+ paddingHorizontal: 10,
334
+ borderWidth: StyleSheet.hairlineWidth,
335
+ borderColor: theme.color['border.subtle'],
336
+ },
337
+ },
338
+ },
339
+ compoundVariants: [
340
+ {
341
+ shape: 'icon',
342
+ grouped: true,
343
+ styles: { width: undefined },
344
+ },
345
+ ],
346
+ },
347
+ // Split from `button` so the open-ended fill and the press state stay in the
348
+ // stylesheet without turning the geometry into a dynamic function too.
349
+ fill: (background: ColorToken, pressed: boolean) => ({
350
+ backgroundColor: resolveThemeColor(background, theme),
351
+ opacity: pressed ? theme.opacity.pressed : 1,
352
+ }),
353
+ }));
@@ -0,0 +1,6 @@
1
+ export type {
2
+ ToolbarButtonProps,
3
+ ToolbarGroupProps,
4
+ ToolbarProps,
5
+ } from './Toolbar';
6
+ export { Toolbar } from './Toolbar';
@@ -6,10 +6,9 @@ import {
6
6
  useMemo,
7
7
  useState,
8
8
  } from 'react';
9
-
9
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
10
10
  import { useUnistyles } from 'react-native-unistyles';
11
-
12
- import { configureTerraUI, getIsConfigured } from '../theme/configure';
11
+ import { configureTerraUI, getIsConfigured } from '../theme/registry';
13
12
  import { applyAccent, applyScheme, getCurrentAccent } from '../theme/runtime';
14
13
  import type { Scheme, TerraTheme } from '../theme/types';
15
14
 
@@ -50,7 +49,9 @@ export const TerraUIProvider = ({ children }: PropsWithChildren) => {
50
49
  );
51
50
 
52
51
  return (
53
- <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
52
+ <ThemeContext.Provider value={value}>
53
+ <SafeAreaProvider>{children}</SafeAreaProvider>
54
+ </ThemeContext.Provider>
54
55
  );
55
56
  };
56
57
 
@@ -4,7 +4,7 @@ import type {
4
4
  TerraIconComponent,
5
5
  TerraIconProps,
6
6
  TerraSemanticIconName,
7
- } from '#theme/types';
7
+ } from '../types';
8
8
 
9
9
  const DEFAULT_SIZE = 20;
10
10
  const DEFAULT_COLOR = '#000000';
@@ -0,0 +1,34 @@
1
+ import { createElement } from 'react';
2
+ import { Image } from 'react-native';
3
+
4
+ import type { TerraImageComponent, TerraImageContentFit } from '../types';
5
+
6
+ const CONTENT_FIT_MAP = {
7
+ cover: 'cover',
8
+ contain: 'contain',
9
+ fill: 'stretch',
10
+ } as const satisfies Record<
11
+ TerraImageContentFit,
12
+ 'cover' | 'contain' | 'stretch'
13
+ >;
14
+
15
+ /** Default image implementation, backed by React Native's `Image`. */
16
+ export const defaultImage: TerraImageComponent = ({
17
+ contentFit = 'cover',
18
+ source,
19
+ style,
20
+ accessibilityLabel,
21
+ placeholder,
22
+ onError,
23
+ }) =>
24
+ createElement(Image, {
25
+ source,
26
+ style,
27
+ accessibilityLabel,
28
+ // RN splits "show this until loaded" across two platform-specific props:
29
+ // `defaultSource` (iOS only) and `loadingIndicatorSource` (Android only).
30
+ defaultSource: placeholder,
31
+ loadingIndicatorSource: placeholder,
32
+ resizeMode: CONTENT_FIT_MAP[contentFit],
33
+ onError: onError ? () => onError() : undefined,
34
+ });
@@ -23,7 +23,7 @@ export {
23
23
  getRequiredIcon,
24
24
  getSurfaceElevation,
25
25
  resolveTheme,
26
- } from './configure';
26
+ } from './registry';
27
27
  export { applyAccent, applyScheme, getCurrentAccent } from './runtime';
28
28
  // ─── Default themes ───────────────────────────────────────────────────────────
29
29
  export { defaultDarkTheme, defaultLightTheme } from './theme';
@@ -1,12 +1,16 @@
1
- import { createElement } from 'react';
2
- import { Image } from 'react-native';
3
1
  import { StyleSheet } from 'react-native-unistyles';
4
2
 
5
- import { defaultIcons } from '#components/icon/utils';
6
3
  import { normalizeAccent } from '#utils/accent-utils';
7
4
  import { deepMerge } from '#utils/deep-merge';
8
5
 
9
- import { buildRadiusScale, defaultDarkTheme, defaultLightTheme } from './theme';
6
+ import { defaultIcons } from './defaults/icons';
7
+ import { defaultImage } from './defaults/image';
8
+ import {
9
+ buildRadiusScale,
10
+ buildSpacingScale,
11
+ defaultDarkTheme,
12
+ defaultLightTheme,
13
+ } from './theme';
10
14
  import type {
11
15
  ComponentDefaults,
12
16
  DefaultRadiusToken,
@@ -17,38 +21,11 @@ import type {
17
21
  TerraConfig,
18
22
  TerraIconComponent,
19
23
  TerraImageComponent,
20
- TerraImageContentFit,
21
24
  TerraSemanticIconName,
22
25
  TerraTheme,
23
26
  TerraThemeOverride,
24
27
  } from './types';
25
28
 
26
- // ─── Default image component ──────────────────────────────────────────────────
27
-
28
- const CONTENT_FIT_MAP = {
29
- cover: 'cover',
30
- contain: 'contain',
31
- fill: 'stretch',
32
- } as const satisfies Record<
33
- TerraImageContentFit,
34
- 'cover' | 'contain' | 'stretch'
35
- >;
36
-
37
- const DefaultImage: TerraImageComponent = ({
38
- contentFit = 'cover',
39
- source,
40
- style,
41
- accessibilityLabel,
42
- onError,
43
- }) =>
44
- createElement(Image, {
45
- source,
46
- style,
47
- accessibilityLabel,
48
- resizeMode: CONTENT_FIT_MAP[contentFit],
49
- onError: onError ? () => onError() : undefined,
50
- });
51
-
52
29
  // ─── Radius / elevation defaults ─────────────────────────────────────────────
53
30
 
54
31
  /** Fallback when a component's radius is not configured. */
@@ -94,7 +71,7 @@ const registry: Registry = {
94
71
  defaultRadius: resolveDefaultRadius(),
95
72
  surfaceElevation: DEFAULT_ELEVATION,
96
73
  icons: {},
97
- imageComponent: DefaultImage,
74
+ imageComponent: defaultImage,
98
75
  configured: false,
99
76
  };
100
77
 
@@ -143,7 +120,7 @@ const mergeThemeOverride = (
143
120
  ): TerraTheme => {
144
121
  if (!override) return theme;
145
122
 
146
- const { radius, ...rest } = override;
123
+ const { radius, spacing, ...rest } = override;
147
124
  let next = deepMerge(theme, rest);
148
125
 
149
126
  if (radius) {
@@ -154,6 +131,14 @@ const mergeThemeOverride = (
154
131
  next = deepMerge(next, { radius: radiusTokens });
155
132
  }
156
133
 
134
+ if (spacing) {
135
+ const { base, ...spacingTokens } = spacing;
136
+ if (base != null) {
137
+ next = deepMerge(next, { spacing: buildSpacingScale(base) });
138
+ }
139
+ next = deepMerge(next, { spacing: spacingTokens });
140
+ }
141
+
157
142
  return next;
158
143
  };
159
144
 
@@ -187,7 +172,7 @@ export function configureTerraUI(config: TerraConfig = {}): void {
187
172
  registry.surfaceElevation =
188
173
  config.components?.surface?.elevation ?? DEFAULT_ELEVATION;
189
174
  registry.icons = config.icons ?? {};
190
- registry.imageComponent = config.image ?? DefaultImage;
175
+ registry.imageComponent = config.image ?? defaultImage;
191
176
 
192
177
  registry.accents = {};
193
178
  for (const [name, accent] of Object.entries(config.accents ?? {})) {
@@ -1,6 +1,6 @@
1
1
  import { UnistylesRuntime } from 'react-native-unistyles';
2
2
 
3
- import { getRegistry, resolveTheme } from './configure';
3
+ import { getRegistry, resolveTheme } from './registry';
4
4
  import type { Scheme } from './types';
5
5
 
6
6
  /** Apply a named accent at runtime by updating both registered themes. */