@oxyhq/bloom 0.26.1 → 0.28.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 (300) hide show
  1. package/lib/commonjs/animated-check/AnimatedCheck.js +162 -0
  2. package/lib/commonjs/animated-check/AnimatedCheck.js.map +1 -0
  3. package/lib/commonjs/animated-check/index.js +13 -0
  4. package/lib/commonjs/animated-check/index.js.map +1 -0
  5. package/lib/commonjs/avatar/Avatar.js +67 -96
  6. package/lib/commonjs/avatar/Avatar.js.map +1 -1
  7. package/lib/commonjs/avatar/AvatarRing.js +183 -0
  8. package/lib/commonjs/avatar/AvatarRing.js.map +1 -0
  9. package/lib/commonjs/avatar/index.js.map +1 -1
  10. package/lib/commonjs/avatar/squircle-path.js +14 -0
  11. package/lib/commonjs/avatar/squircle-path.js.map +1 -0
  12. package/lib/commonjs/avatar/svg-module.js +15 -0
  13. package/lib/commonjs/avatar/svg-module.js.map +1 -0
  14. package/lib/commonjs/dialog/placement.js +9 -7
  15. package/lib/commonjs/dialog/placement.js.map +1 -1
  16. package/lib/commonjs/fill/index.js.map +1 -1
  17. package/lib/commonjs/hooks/index.js +20 -0
  18. package/lib/commonjs/hooks/index.js.map +1 -1
  19. package/lib/commonjs/hooks/useGutters.js +106 -0
  20. package/lib/commonjs/hooks/useGutters.js.map +1 -0
  21. package/lib/commonjs/hooks/useHaptics.js +69 -0
  22. package/lib/commonjs/hooks/useHaptics.js.map +1 -0
  23. package/lib/commonjs/hooks/useInteractionState.js +8 -2
  24. package/lib/commonjs/hooks/useInteractionState.js.map +1 -1
  25. package/lib/commonjs/index.js +155 -63
  26. package/lib/commonjs/index.js.map +1 -1
  27. package/lib/commonjs/index.web.js +155 -63
  28. package/lib/commonjs/index.web.js.map +1 -1
  29. package/lib/commonjs/media-inset-border/MediaInsetBorder.js +50 -0
  30. package/lib/commonjs/media-inset-border/MediaInsetBorder.js.map +1 -0
  31. package/lib/commonjs/media-inset-border/index.js +13 -0
  32. package/lib/commonjs/media-inset-border/index.js.map +1 -0
  33. package/lib/commonjs/motion/ScreenTransition.js +46 -0
  34. package/lib/commonjs/motion/ScreenTransition.js.map +1 -0
  35. package/lib/commonjs/motion/index.js +32 -0
  36. package/lib/commonjs/motion/index.js.map +1 -0
  37. package/lib/commonjs/motion/motion.js +93 -0
  38. package/lib/commonjs/motion/motion.js.map +1 -0
  39. package/lib/commonjs/pressable-scale/PressableScale.js +75 -0
  40. package/lib/commonjs/pressable-scale/PressableScale.js.map +1 -0
  41. package/lib/commonjs/pressable-scale/index.js +13 -0
  42. package/lib/commonjs/pressable-scale/index.js.map +1 -0
  43. package/lib/commonjs/pressable-with-hover/PressableWithHover.js +36 -0
  44. package/lib/commonjs/pressable-with-hover/PressableWithHover.js.map +1 -0
  45. package/lib/commonjs/pressable-with-hover/index.js +13 -0
  46. package/lib/commonjs/pressable-with-hover/index.js.map +1 -0
  47. package/lib/commonjs/styles/breakpoints.js +24 -0
  48. package/lib/commonjs/styles/breakpoints.js.map +1 -0
  49. package/lib/commonjs/styles/index.js +7 -0
  50. package/lib/commonjs/styles/index.js.map +1 -1
  51. package/lib/commonjs/subtle-hover/SubtleHover.js +69 -0
  52. package/lib/commonjs/subtle-hover/SubtleHover.js.map +1 -0
  53. package/lib/commonjs/subtle-hover/index.js +13 -0
  54. package/lib/commonjs/subtle-hover/index.js.map +1 -0
  55. package/lib/commonjs/theme/build-theme.js +2 -0
  56. package/lib/commonjs/theme/build-theme.js.map +1 -1
  57. package/lib/commonjs/theme/gradients.js +39 -0
  58. package/lib/commonjs/theme/gradients.js.map +1 -0
  59. package/lib/commonjs/theme/index.js +7 -0
  60. package/lib/commonjs/theme/index.js.map +1 -1
  61. package/lib/module/animated-check/AnimatedCheck.js +157 -0
  62. package/lib/module/animated-check/AnimatedCheck.js.map +1 -0
  63. package/lib/module/animated-check/index.js +4 -0
  64. package/lib/module/animated-check/index.js.map +1 -0
  65. package/lib/module/avatar/Avatar.js +66 -96
  66. package/lib/module/avatar/Avatar.js.map +1 -1
  67. package/lib/module/avatar/AvatarRing.js +177 -0
  68. package/lib/module/avatar/AvatarRing.js.map +1 -0
  69. package/lib/module/avatar/index.js.map +1 -1
  70. package/lib/module/avatar/squircle-path.js +10 -0
  71. package/lib/module/avatar/squircle-path.js.map +1 -0
  72. package/lib/module/avatar/svg-module.js +11 -0
  73. package/lib/module/avatar/svg-module.js.map +1 -0
  74. package/lib/module/dialog/placement.js +9 -7
  75. package/lib/module/dialog/placement.js.map +1 -1
  76. package/lib/module/fill/index.js.map +1 -1
  77. package/lib/module/hooks/index.js +2 -0
  78. package/lib/module/hooks/index.js.map +1 -1
  79. package/lib/module/hooks/useGutters.js +103 -0
  80. package/lib/module/hooks/useGutters.js.map +1 -0
  81. package/lib/module/hooks/useHaptics.js +65 -0
  82. package/lib/module/hooks/useHaptics.js.map +1 -0
  83. package/lib/module/hooks/useInteractionState.js +10 -3
  84. package/lib/module/hooks/useInteractionState.js.map +1 -1
  85. package/lib/module/index.js +10 -1
  86. package/lib/module/index.js.map +1 -1
  87. package/lib/module/index.web.js +10 -1
  88. package/lib/module/index.web.js.map +1 -1
  89. package/lib/module/media-inset-border/MediaInsetBorder.js +47 -0
  90. package/lib/module/media-inset-border/MediaInsetBorder.js.map +1 -0
  91. package/lib/module/media-inset-border/index.js +4 -0
  92. package/lib/module/media-inset-border/index.js.map +1 -0
  93. package/lib/module/motion/ScreenTransition.js +40 -0
  94. package/lib/module/motion/ScreenTransition.js.map +1 -0
  95. package/lib/module/motion/index.js +5 -0
  96. package/lib/module/motion/index.js.map +1 -0
  97. package/lib/module/motion/motion.js +87 -0
  98. package/lib/module/motion/motion.js.map +1 -0
  99. package/lib/module/pressable-scale/PressableScale.js +72 -0
  100. package/lib/module/pressable-scale/PressableScale.js.map +1 -0
  101. package/lib/module/pressable-scale/index.js +4 -0
  102. package/lib/module/pressable-scale/index.js.map +1 -0
  103. package/lib/module/pressable-with-hover/PressableWithHover.js +31 -0
  104. package/lib/module/pressable-with-hover/PressableWithHover.js.map +1 -0
  105. package/lib/module/pressable-with-hover/index.js +4 -0
  106. package/lib/module/pressable-with-hover/index.js.map +1 -0
  107. package/lib/module/styles/breakpoints.js +20 -0
  108. package/lib/module/styles/breakpoints.js.map +1 -0
  109. package/lib/module/styles/index.js +1 -0
  110. package/lib/module/styles/index.js.map +1 -1
  111. package/lib/module/subtle-hover/SubtleHover.js +66 -0
  112. package/lib/module/subtle-hover/SubtleHover.js.map +1 -0
  113. package/lib/module/subtle-hover/index.js +4 -0
  114. package/lib/module/subtle-hover/index.js.map +1 -0
  115. package/lib/module/theme/build-theme.js +2 -0
  116. package/lib/module/theme/build-theme.js.map +1 -1
  117. package/lib/module/theme/gradients.js +35 -0
  118. package/lib/module/theme/gradients.js.map +1 -0
  119. package/lib/module/theme/index.js +1 -0
  120. package/lib/module/theme/index.js.map +1 -1
  121. package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts +18 -0
  122. package/lib/typescript/commonjs/animated-check/AnimatedCheck.d.ts.map +1 -0
  123. package/lib/typescript/commonjs/animated-check/index.d.ts +3 -0
  124. package/lib/typescript/commonjs/animated-check/index.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/avatar/Avatar.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/avatar/AvatarRing.d.ts +27 -0
  127. package/lib/typescript/commonjs/avatar/AvatarRing.d.ts.map +1 -0
  128. package/lib/typescript/commonjs/avatar/index.d.ts +1 -1
  129. package/lib/typescript/commonjs/avatar/index.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/avatar/squircle-path.d.ts +8 -0
  131. package/lib/typescript/commonjs/avatar/squircle-path.d.ts.map +1 -0
  132. package/lib/typescript/commonjs/avatar/svg-module.d.ts +8 -0
  133. package/lib/typescript/commonjs/avatar/svg-module.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/avatar/types.d.ts +30 -0
  135. package/lib/typescript/commonjs/avatar/types.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/dialog/placement.d.ts +8 -7
  137. package/lib/typescript/commonjs/dialog/placement.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/fill/index.d.ts +2 -2
  139. package/lib/typescript/commonjs/fill/index.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
  141. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/hooks/useGutters.d.ts +32 -0
  143. package/lib/typescript/commonjs/hooks/useGutters.d.ts.map +1 -0
  144. package/lib/typescript/commonjs/hooks/useHaptics.d.ts +28 -0
  145. package/lib/typescript/commonjs/hooks/useHaptics.d.ts.map +1 -0
  146. package/lib/typescript/commonjs/hooks/useInteractionState.d.ts +6 -0
  147. package/lib/typescript/commonjs/hooks/useInteractionState.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/index.d.ts +16 -0
  149. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/index.web.d.ts +16 -0
  151. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts +26 -0
  153. package/lib/typescript/commonjs/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
  154. package/lib/typescript/commonjs/media-inset-border/index.d.ts +3 -0
  155. package/lib/typescript/commonjs/media-inset-border/index.d.ts.map +1 -0
  156. package/lib/typescript/commonjs/motion/ScreenTransition.d.ts +33 -0
  157. package/lib/typescript/commonjs/motion/ScreenTransition.d.ts.map +1 -0
  158. package/lib/typescript/commonjs/motion/index.d.ts +4 -0
  159. package/lib/typescript/commonjs/motion/index.d.ts.map +1 -0
  160. package/lib/typescript/commonjs/motion/motion.d.ts +24 -0
  161. package/lib/typescript/commonjs/motion/motion.d.ts.map +1 -0
  162. package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts +21 -0
  163. package/lib/typescript/commonjs/pressable-scale/PressableScale.d.ts.map +1 -0
  164. package/lib/typescript/commonjs/pressable-scale/index.d.ts +3 -0
  165. package/lib/typescript/commonjs/pressable-scale/index.d.ts.map +1 -0
  166. package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts +18 -0
  167. package/lib/typescript/commonjs/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
  168. package/lib/typescript/commonjs/pressable-with-hover/index.d.ts +3 -0
  169. package/lib/typescript/commonjs/pressable-with-hover/index.d.ts.map +1 -0
  170. package/lib/typescript/commonjs/styles/breakpoints.d.ts +18 -0
  171. package/lib/typescript/commonjs/styles/breakpoints.d.ts.map +1 -0
  172. package/lib/typescript/commonjs/styles/index.d.ts +2 -0
  173. package/lib/typescript/commonjs/styles/index.d.ts.map +1 -1
  174. package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts +32 -0
  175. package/lib/typescript/commonjs/subtle-hover/SubtleHover.d.ts.map +1 -0
  176. package/lib/typescript/commonjs/subtle-hover/index.d.ts +3 -0
  177. package/lib/typescript/commonjs/subtle-hover/index.d.ts.map +1 -0
  178. package/lib/typescript/commonjs/theme/build-theme.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/theme/gradients.d.ts +12 -0
  180. package/lib/typescript/commonjs/theme/gradients.d.ts.map +1 -0
  181. package/lib/typescript/commonjs/theme/index.d.ts +2 -1
  182. package/lib/typescript/commonjs/theme/index.d.ts.map +1 -1
  183. package/lib/typescript/commonjs/theme/types.d.ts +11 -0
  184. package/lib/typescript/commonjs/theme/types.d.ts.map +1 -1
  185. package/lib/typescript/module/animated-check/AnimatedCheck.d.ts +18 -0
  186. package/lib/typescript/module/animated-check/AnimatedCheck.d.ts.map +1 -0
  187. package/lib/typescript/module/animated-check/index.d.ts +3 -0
  188. package/lib/typescript/module/animated-check/index.d.ts.map +1 -0
  189. package/lib/typescript/module/avatar/Avatar.d.ts.map +1 -1
  190. package/lib/typescript/module/avatar/AvatarRing.d.ts +27 -0
  191. package/lib/typescript/module/avatar/AvatarRing.d.ts.map +1 -0
  192. package/lib/typescript/module/avatar/index.d.ts +1 -1
  193. package/lib/typescript/module/avatar/index.d.ts.map +1 -1
  194. package/lib/typescript/module/avatar/squircle-path.d.ts +8 -0
  195. package/lib/typescript/module/avatar/squircle-path.d.ts.map +1 -0
  196. package/lib/typescript/module/avatar/svg-module.d.ts +8 -0
  197. package/lib/typescript/module/avatar/svg-module.d.ts.map +1 -0
  198. package/lib/typescript/module/avatar/types.d.ts +30 -0
  199. package/lib/typescript/module/avatar/types.d.ts.map +1 -1
  200. package/lib/typescript/module/dialog/placement.d.ts +8 -7
  201. package/lib/typescript/module/dialog/placement.d.ts.map +1 -1
  202. package/lib/typescript/module/fill/index.d.ts +2 -2
  203. package/lib/typescript/module/fill/index.d.ts.map +1 -1
  204. package/lib/typescript/module/hooks/index.d.ts +4 -0
  205. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  206. package/lib/typescript/module/hooks/useGutters.d.ts +32 -0
  207. package/lib/typescript/module/hooks/useGutters.d.ts.map +1 -0
  208. package/lib/typescript/module/hooks/useHaptics.d.ts +28 -0
  209. package/lib/typescript/module/hooks/useHaptics.d.ts.map +1 -0
  210. package/lib/typescript/module/hooks/useInteractionState.d.ts +6 -0
  211. package/lib/typescript/module/hooks/useInteractionState.d.ts.map +1 -1
  212. package/lib/typescript/module/index.d.ts +16 -0
  213. package/lib/typescript/module/index.d.ts.map +1 -1
  214. package/lib/typescript/module/index.web.d.ts +16 -0
  215. package/lib/typescript/module/index.web.d.ts.map +1 -1
  216. package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts +26 -0
  217. package/lib/typescript/module/media-inset-border/MediaInsetBorder.d.ts.map +1 -0
  218. package/lib/typescript/module/media-inset-border/index.d.ts +3 -0
  219. package/lib/typescript/module/media-inset-border/index.d.ts.map +1 -0
  220. package/lib/typescript/module/motion/ScreenTransition.d.ts +33 -0
  221. package/lib/typescript/module/motion/ScreenTransition.d.ts.map +1 -0
  222. package/lib/typescript/module/motion/index.d.ts +4 -0
  223. package/lib/typescript/module/motion/index.d.ts.map +1 -0
  224. package/lib/typescript/module/motion/motion.d.ts +24 -0
  225. package/lib/typescript/module/motion/motion.d.ts.map +1 -0
  226. package/lib/typescript/module/pressable-scale/PressableScale.d.ts +21 -0
  227. package/lib/typescript/module/pressable-scale/PressableScale.d.ts.map +1 -0
  228. package/lib/typescript/module/pressable-scale/index.d.ts +3 -0
  229. package/lib/typescript/module/pressable-scale/index.d.ts.map +1 -0
  230. package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts +18 -0
  231. package/lib/typescript/module/pressable-with-hover/PressableWithHover.d.ts.map +1 -0
  232. package/lib/typescript/module/pressable-with-hover/index.d.ts +3 -0
  233. package/lib/typescript/module/pressable-with-hover/index.d.ts.map +1 -0
  234. package/lib/typescript/module/styles/breakpoints.d.ts +18 -0
  235. package/lib/typescript/module/styles/breakpoints.d.ts.map +1 -0
  236. package/lib/typescript/module/styles/index.d.ts +2 -0
  237. package/lib/typescript/module/styles/index.d.ts.map +1 -1
  238. package/lib/typescript/module/subtle-hover/SubtleHover.d.ts +32 -0
  239. package/lib/typescript/module/subtle-hover/SubtleHover.d.ts.map +1 -0
  240. package/lib/typescript/module/subtle-hover/index.d.ts +3 -0
  241. package/lib/typescript/module/subtle-hover/index.d.ts.map +1 -0
  242. package/lib/typescript/module/theme/build-theme.d.ts.map +1 -1
  243. package/lib/typescript/module/theme/gradients.d.ts +12 -0
  244. package/lib/typescript/module/theme/gradients.d.ts.map +1 -0
  245. package/lib/typescript/module/theme/index.d.ts +2 -1
  246. package/lib/typescript/module/theme/index.d.ts.map +1 -1
  247. package/lib/typescript/module/theme/types.d.ts +11 -0
  248. package/lib/typescript/module/theme/types.d.ts.map +1 -1
  249. package/package.json +73 -1
  250. package/src/__tests__/AnimatedCheck.test.tsx +31 -0
  251. package/src/__tests__/MediaInsetBorder.test.tsx +36 -0
  252. package/src/__tests__/Motion.test.tsx +58 -0
  253. package/src/__tests__/PressableScale.test.tsx +29 -0
  254. package/src/__tests__/PressableWithHover.test.tsx +48 -0
  255. package/src/__tests__/SubtleHover.test.tsx +42 -0
  256. package/src/__tests__/theme-gradients.test.ts +31 -0
  257. package/src/__tests__/theme.test.ts +3 -0
  258. package/src/__tests__/useGutters.test.ts +58 -0
  259. package/src/__tests__/useHaptics.test.tsx +58 -0
  260. package/src/__tests__/useInteractionState.test.ts +27 -0
  261. package/src/animated-check/AnimatedCheck.stories.tsx +60 -0
  262. package/src/animated-check/AnimatedCheck.tsx +173 -0
  263. package/src/animated-check/index.ts +2 -0
  264. package/src/avatar/Avatar.stories.tsx +95 -0
  265. package/src/avatar/Avatar.tsx +61 -110
  266. package/src/avatar/AvatarRing.tsx +170 -0
  267. package/src/avatar/index.ts +6 -1
  268. package/src/avatar/squircle-path.ts +11 -0
  269. package/src/avatar/svg-module.ts +11 -0
  270. package/src/avatar/types.ts +32 -0
  271. package/src/dialog/placement.ts +10 -7
  272. package/src/fill/index.tsx +2 -2
  273. package/src/hooks/index.ts +4 -0
  274. package/src/hooks/useGutters.ts +99 -0
  275. package/src/hooks/useHaptics.tsx +69 -0
  276. package/src/hooks/useInteractionState.ts +8 -2
  277. package/src/index.ts +20 -0
  278. package/src/index.web.ts +20 -0
  279. package/src/media-inset-border/MediaInsetBorder.stories.tsx +60 -0
  280. package/src/media-inset-border/MediaInsetBorder.tsx +72 -0
  281. package/src/media-inset-border/index.ts +2 -0
  282. package/src/motion/Motion.stories.tsx +99 -0
  283. package/src/motion/ScreenTransition.tsx +71 -0
  284. package/src/motion/index.ts +3 -0
  285. package/src/motion/motion.ts +75 -0
  286. package/src/pressable-scale/PressableScale.stories.tsx +71 -0
  287. package/src/pressable-scale/PressableScale.tsx +103 -0
  288. package/src/pressable-scale/index.ts +2 -0
  289. package/src/pressable-with-hover/PressableWithHover.stories.tsx +43 -0
  290. package/src/pressable-with-hover/PressableWithHover.tsx +43 -0
  291. package/src/pressable-with-hover/index.ts +2 -0
  292. package/src/styles/breakpoints.ts +18 -0
  293. package/src/styles/index.ts +2 -0
  294. package/src/subtle-hover/SubtleHover.stories.tsx +72 -0
  295. package/src/subtle-hover/SubtleHover.tsx +90 -0
  296. package/src/subtle-hover/index.ts +2 -0
  297. package/src/theme/build-theme.ts +2 -0
  298. package/src/theme/gradients.ts +61 -0
  299. package/src/theme/index.ts +2 -1
  300. package/src/theme/types.ts +13 -0
@@ -5,10 +5,12 @@ import type { TextStyle } from 'react-native';
5
5
  import { useTheme } from '../theme/use-theme';
6
6
  import { useImageResolver } from '../image-resolver/context';
7
7
  import { Z_INDEX } from '../styles/z-index';
8
- import { lazyRequire } from '../utils/lazy-require';
9
8
  import { useAvatarPlaceholder } from './placeholder-context';
10
9
  import { LiveBadge } from './LiveBadge';
11
- import type { AvatarProps } from './types';
10
+ import { AvatarRing, getRingOuterSize } from './AvatarRing';
11
+ import { getSvgModule } from './svg-module';
12
+ import { SQUIRCLE_PATH } from './squircle-path';
13
+ import type { AvatarProps, AvatarRingConfig } from './types';
12
14
 
13
15
  // Google Contacts-inspired palette used to pick a deterministic background
14
16
  // color for name-based placeholder avatars.
@@ -40,32 +42,8 @@ function getNameColor(name: string): string {
40
42
  // consumers compiling Bloom's source files directly.
41
43
  import DEFAULT_AVATAR_IMAGE from './default-avatar';
42
44
 
43
- // Squircle clip path normalized to 0–1 coordinate space (viewBox="0 0 1 1").
44
- const SQUIRCLE_PATH =
45
- 'M0 0.5 L0.00122 0.31674 L0.00489 0.25123 L0.01103 0.20331 L0.01969 0.16478 L0.03097 0.13257 L0.04495 0.10518 L0.0618 0.08177 L0.08177 0.0618 L0.10518 0.04495 L0.13257 0.03097 L0.16478 0.01969 L0.20331 0.01103 L0.25123 0.00489 L0.31674 0.00122 L0.5 0' +
46
- ' L0.68895 0.0014 L0.7564 0.00561 L0.80559 0.01267 L0.84499 0.02264 L0.87771 0.03564 L0.9053 0.05181 L0.92862 0.07138 L0.94819 0.0947 L0.96436 0.12228 L0.97736 0.15501 L0.98733 0.19441 L0.99439 0.2436 L0.9986 0.31105 L1 0.5' +
47
- ' L0.9986 0.68895 L0.99439 0.7564 L0.98733 0.80559 L0.97736 0.84499 L0.96436 0.87771 L0.94819 0.9053 L0.92862 0.92862 L0.9053 0.94819 L0.87771 0.96436 L0.84499 0.97736 L0.80559 0.98733 L0.7564 0.99439 L0.68895 0.9986 L0.5 1' +
48
- ' L0.31105 0.9986 L0.2436 0.99439 L0.19441 0.98733 L0.15501 0.97736 L0.12228 0.96436 L0.0947 0.94819 L0.07138 0.92862 L0.05181 0.9053 L0.03564 0.87771 L0.02264 0.84499 L0.01267 0.80559 L0.00561 0.7564 L0.0014 0.68895 L0 0.5Z';
49
-
50
45
  let clipIdCounter = 0;
51
46
 
52
- // Lazy-loaded SVG components for squircle shape.
53
- // Returns null if react-native-svg is not installed.
54
- type SvgModuleType = typeof import('react-native-svg');
55
- const getSvgModule = lazyRequire<SvgModuleType>('react-native-svg');
56
-
57
- /**
58
- * Live-ring drawing config passed down to shape renderers.
59
- * - `color`: resolved ring color (theme `negative` or `liveColor` override).
60
- * - `strokeWidth`: stroke width in the squircle's 0–1 path coordinate space.
61
- * - `borderWidth`: ring border width in pixels for the circle fallback.
62
- */
63
- interface LiveRingConfig {
64
- color: string;
65
- strokeWidth: number;
66
- borderWidth: number;
67
- }
68
-
69
47
  function SquircleImage({
70
48
  uri,
71
49
  fallbackSource,
@@ -74,7 +52,6 @@ function SquircleImage({
74
52
  placeholderIcon,
75
53
  name,
76
54
  onError,
77
- liveRing,
78
55
  }: {
79
56
  uri?: string;
80
57
  fallbackSource?: AvatarProps['fallbackSource'];
@@ -83,34 +60,23 @@ function SquircleImage({
83
60
  placeholderIcon?: React.ReactNode;
84
61
  name?: string;
85
62
  onError: () => void;
86
- liveRing?: LiveRingConfig;
87
63
  }) {
64
+ const clipId = useMemo(() => `bloom-sqc${clipIdCounter++}`, []);
88
65
  const svg = getSvgModule();
89
66
  if (!svg) {
90
- // Fallback to circle if react-native-svg is not installed. The circle
91
- // fallback also gets a circle live ring (matching its rendered shape).
67
+ // Fallback to a circle if react-native-svg is not installed. The ring (if
68
+ // any) is drawn separately by AvatarRing, which also degrades to a circle.
92
69
  return (
93
- <>
94
- <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
95
- {liveRing && (
96
- <CircleLiveRing radius={size / 2} color={liveRing.color} borderWidth={liveRing.borderWidth} />
97
- )}
98
- </>
70
+ <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
99
71
  );
100
72
  }
101
73
 
102
74
  const { default: Svg, Defs, ClipPath, Path, Image: SvgImage } = svg;
103
- const clipId = useMemo(() => `bloom-sqc${clipIdCounter++}`, []);
104
75
 
105
76
  const href = uri ? { uri } : fallbackSource;
106
77
  if (!href) {
107
78
  return (
108
- <>
109
- <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
110
- {liveRing && (
111
- <CircleLiveRing radius={size / 2} color={liveRing.color} borderWidth={liveRing.borderWidth} />
112
- )}
113
- </>
79
+ <CircleFallback size={size} fallbackColor={fallbackColor} icon={placeholderIcon} name={name} />
114
80
  );
115
81
  }
116
82
 
@@ -137,18 +103,6 @@ function SquircleImage({
137
103
  preserveAspectRatio="xMidYMid slice"
138
104
  clipPath={`url(#${clipId})`}
139
105
  />
140
- {/* Live ring: the squircle outline stroked in the ring color. The
141
- path lives in 0–1 space, so the stroke width is expressed in the
142
- same units; the outer half is clipped by the SVG viewport, leaving
143
- a ~borderWidth-px inset ring on the squircle edge. */}
144
- {liveRing && (
145
- <Path
146
- d={SQUIRCLE_PATH}
147
- fill="none"
148
- stroke={liveRing.color}
149
- strokeWidth={liveRing.strokeWidth}
150
- />
151
- )}
152
106
  </Svg>
153
107
  </>
154
108
  );
@@ -208,37 +162,6 @@ function CircleFallback({
208
162
  );
209
163
  }
210
164
 
211
- /**
212
- * Solid live ring for circular avatars — an absolutely-positioned inset-0
213
- * overlay whose border overlays the avatar edge (drawn on top of the image,
214
- * never a border on the image itself). Also used as the fallback ring for a
215
- * squircle avatar when react-native-svg is unavailable.
216
- */
217
- function CircleLiveRing({
218
- radius,
219
- color,
220
- borderWidth,
221
- }: {
222
- radius: number;
223
- color: string;
224
- borderWidth: number;
225
- }) {
226
- return (
227
- <View
228
- style={[
229
- StyleSheet.absoluteFill,
230
- {
231
- borderRadius: radius,
232
- borderColor: color,
233
- borderWidth,
234
- zIndex: Z_INDEX.raised,
235
- pointerEvents: 'none',
236
- },
237
- ]}
238
- />
239
- );
240
- }
241
-
242
165
  const AvatarComponent: React.FC<AvatarProps> = ({
243
166
  source,
244
167
  uri,
@@ -258,6 +181,7 @@ const AvatarComponent: React.FC<AvatarProps> = ({
258
181
  hideLiveBadge = false,
259
182
  liveLabel = 'LIVE',
260
183
  liveColor,
184
+ ring,
261
185
  testID,
262
186
  }) => {
263
187
  const [errored, setErrored] = useState(false);
@@ -323,25 +247,40 @@ const AvatarComponent: React.FC<AvatarProps> = ({
323
247
  return fallbackSource;
324
248
  }, [effectiveUri, resolvedImageSource, fallbackSource]);
325
249
 
326
- // Live indicator (modeled on Bluesky): a solid ring around the avatar plus,
327
- // unless suppressed, a "LIVE" pill hanging off the bottom-center. Colors come
328
- // from the theme (`negative` / `negativeForeground`) with an optional
329
- // `liveColor` override. The badge is only shown for `size > 16`.
330
- const liveRingColor = liveColor ?? theme.colors.negative;
331
- const liveBorderWidth = size > 16 ? 2 : 1;
332
- const liveRing: LiveRingConfig | undefined = live
333
- ? {
334
- color: liveRingColor,
335
- // The squircle path is in 0–1 space; center-stroking it and letting the
336
- // viewport clip the outer half yields a ~`liveBorderWidth`-px inset ring.
337
- strokeWidth: (liveBorderWidth / size) * 2,
338
- borderWidth: liveBorderWidth,
339
- }
340
- : undefined;
250
+ // Resolve a SINGLE ring config shared by `live` and the public `ring` prop.
251
+ // An explicit `ring` wins for geometry and colors; otherwise `live` derives a
252
+ // solid theme-`negative` ring (modeled on Bluesky's live indicator). The
253
+ // "LIVE" badge stays independently controlled by `live` / `hideLiveBadge` and
254
+ // uses the theme `negative` color regardless of the ring's colors.
255
+ const liveBadgeColor = liveColor ?? theme.colors.negative;
256
+ const resolvedRing: AvatarRingConfig | undefined = ring
257
+ ? ring
258
+ : live
259
+ ? { colors: liveBadgeColor, width: size > 16 ? 2 : 1, gap: 0 }
260
+ : undefined;
261
+ const ringWidth = resolvedRing?.width ?? (size > 16 ? 2 : 1);
262
+ const ringGap = resolvedRing?.gap ?? 0;
263
+ // When gap > 0 the ring sits outside the avatar and the footprint grows so the
264
+ // avatar can be centered inside; gap 0 overlays the edge (footprint unchanged).
265
+ const outer = resolvedRing ? getRingOuterSize(size, ringWidth, ringGap) : size;
341
266
  const showLiveBadge = live && size > 16 && !hideLiveBadge;
342
267
 
343
- const content = (
344
- <View style={[styles.container, { width: size, height: size }, style]} testID={testID}>
268
+ const ringElement = resolvedRing ? (
269
+ <AvatarRing
270
+ size={size}
271
+ shape={shape}
272
+ colors={resolvedRing.colors}
273
+ width={ringWidth}
274
+ gap={ringGap}
275
+ gradientDirection={resolvedRing.gradientDirection ?? 'diagonal'}
276
+ />
277
+ ) : null;
278
+
279
+ // The avatar box holds the image and the badges (which hug the avatar). When
280
+ // gap > 0 this box is centered inside the larger container; when gap is 0 it
281
+ // fills the container exactly.
282
+ const avatarBox = (
283
+ <View style={[styles.avatarBox, { width: size, height: size }]}>
345
284
  {shape === 'squircle' ? (
346
285
  <SquircleImage
347
286
  uri={effectiveUri}
@@ -351,7 +290,6 @@ const AvatarComponent: React.FC<AvatarProps> = ({
351
290
  placeholderIcon={resolvedPlaceholderIcon}
352
291
  name={name}
353
292
  onError={() => setErrored(true)}
354
- liveRing={liveRing}
355
293
  />
356
294
  ) : (
357
295
  <View style={[styles.imageContainer, { width: size, height: size, borderRadius: radius }]}>
@@ -373,11 +311,9 @@ const AvatarComponent: React.FC<AvatarProps> = ({
373
311
  </View>
374
312
  )}
375
313
 
376
- {/* Circle live ring overlays the avatar edge. The squircle shape draws
377
- its own ring inside SquircleImage (SVG path or circle fallback). */}
378
- {live && shape !== 'squircle' && (
379
- <CircleLiveRing radius={radius} color={liveRingColor} borderWidth={liveBorderWidth} />
380
- )}
314
+ {/* Overlay ring (gap 0) overlays the avatar edge, above the image but
315
+ below the badges. */}
316
+ {ringGap === 0 ? ringElement : null}
381
317
 
382
318
  {verified && verifiedIcon && (
383
319
  <View
@@ -397,13 +333,22 @@ const AvatarComponent: React.FC<AvatarProps> = ({
397
333
  <LiveBadge
398
334
  variant={size > 32 ? 'small' : 'tiny'}
399
335
  label={liveLabel}
400
- backgroundColor={liveRingColor}
336
+ backgroundColor={liveBadgeColor}
401
337
  textColor={theme.colors.negativeForeground}
402
338
  />
403
339
  )}
404
340
  </View>
405
341
  );
406
342
 
343
+ const content = (
344
+ <View style={[styles.container, { width: outer, height: outer }, style]} testID={testID}>
345
+ {/* Outside ring (gap > 0) sits on the reserved perimeter, behind the
346
+ avatar box so the avatar and its badges paint on top. */}
347
+ {ringGap > 0 ? ringElement : null}
348
+ {avatarBox}
349
+ </View>
350
+ );
351
+
407
352
  if (onPress) {
408
353
  return <TouchableOpacity onPress={onPress}>{content}</TouchableOpacity>;
409
354
  }
@@ -418,6 +363,12 @@ const styles = StyleSheet.create({
418
363
  justifyContent: 'center',
419
364
  alignItems: 'center',
420
365
  },
366
+ avatarBox: {
367
+ position: 'relative',
368
+ overflow: 'visible',
369
+ justifyContent: 'center',
370
+ alignItems: 'center',
371
+ },
421
372
  imageContainer: {
422
373
  overflow: 'hidden',
423
374
  justifyContent: 'center',
@@ -0,0 +1,170 @@
1
+ import React, { memo, useMemo } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+
4
+ import { Z_INDEX } from '../styles/z-index';
5
+ import { getSvgModule } from './svg-module';
6
+ import { SQUIRCLE_PATH } from './squircle-path';
7
+ import type { AvatarRingGradientDirection, AvatarShape } from './types';
8
+
9
+ // Module counter for unique gradient element ids — mirrors the `clipIdCounter`
10
+ // pattern in `Avatar.tsx`. Deterministic (no `Math.random()`) so ids are stable
11
+ // per mounted instance.
12
+ let ringGradientIdCounter = 0;
13
+
14
+ /**
15
+ * Size of the box the ring occupies. With `gap === 0` the ring overlays the
16
+ * avatar edge and the box equals the avatar size (footprint unchanged). With
17
+ * `gap > 0` the ring sits OUTSIDE the avatar, so the box grows and the avatar is
18
+ * centered inside it. Exported so `Avatar` reserves the same footprint with a
19
+ * single source for the geometry formula.
20
+ */
21
+ export function getRingOuterSize(size: number, width: number, gap: number): number {
22
+ return gap > 0 ? size + 2 * (width + gap) : size;
23
+ }
24
+
25
+ interface AvatarRingProps {
26
+ /** Avatar diameter in px (the inner content size, before any gap). */
27
+ size: number;
28
+ /** Matches the avatar shape. */
29
+ shape: AvatarShape;
30
+ /** Solid ring: one color. Gradient ring: 2+ colors. */
31
+ colors: string | string[];
32
+ /** Ring stroke width in px. */
33
+ width: number;
34
+ /** Gap between the avatar edge and the ring (0 = overlay the edge). */
35
+ gap: number;
36
+ /** Gradient sweep direction for multi-color rings. */
37
+ gradientDirection: AvatarRingGradientDirection;
38
+ }
39
+
40
+ function gradientCoords(direction: AvatarRingGradientDirection) {
41
+ switch (direction) {
42
+ case 'horizontal':
43
+ return { x1: '0', y1: '0', x2: '1', y2: '0' };
44
+ case 'vertical':
45
+ return { x1: '0', y1: '0', x2: '0', y2: '1' };
46
+ case 'diagonal':
47
+ default:
48
+ return { x1: '0', y1: '0', x2: '1', y2: '1' };
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Single ring primitive shared by the `live` indicator and the public `ring`
54
+ * prop. Renders either a solid or a gradient ring, for both `circle` and
55
+ * `squircle` avatars.
56
+ *
57
+ * - Solid circle → a plain bordered `<View>` (no `react-native-svg` needed).
58
+ * - Solid squircle → an SVG `<Path>` stroking the squircle outline.
59
+ * - Gradient (either shape) → an SVG `<LinearGradient>` stroke; degrades to a
60
+ * solid ring of the first color when `react-native-svg` is unavailable (a
61
+ * squircle then degrades to a circle border, matching the avatar's own no-SVG
62
+ * circle fallback).
63
+ *
64
+ * Internal to the avatar family — not exported from the package.
65
+ */
66
+ const AvatarRingComponent: React.FC<AvatarRingProps> = ({
67
+ size,
68
+ shape,
69
+ colors,
70
+ width,
71
+ gap,
72
+ gradientDirection,
73
+ }) => {
74
+ const gradientId = useMemo(() => `bloom-ring-grad${ringGradientIdCounter++}`, []);
75
+
76
+ const colorList = Array.isArray(colors) ? colors : [colors];
77
+ const wantsGradient = colorList.length >= 2;
78
+ const solidColor = colorList[0] ?? '#000000';
79
+
80
+ const outer = getRingOuterSize(size, width, gap);
81
+ // The ring's visible band is always the outermost `width` px inside the outer
82
+ // box; when it degrades to a circle border it is a full circle → radius
83
+ // `outer / 2`.
84
+ const ringRadius = outer / 2;
85
+
86
+ // `react-native-svg` is required for gradients (both shapes) and for the
87
+ // squircle outline. A solid circle ring is a plain border and never needs it.
88
+ const needsSvg = wantsGradient || shape === 'squircle';
89
+ const svg = needsSvg ? getSvgModule() : null;
90
+
91
+ // No SVG available (or a solid circle, which never needs SVG): draw a plain
92
+ // bordered View. A gradient degrades to its first color; a squircle degrades
93
+ // to a circle border.
94
+ if (!svg) {
95
+ return (
96
+ <View
97
+ pointerEvents="none"
98
+ style={{
99
+ position: 'absolute',
100
+ top: 0,
101
+ left: 0,
102
+ width: outer,
103
+ height: outer,
104
+ borderRadius: ringRadius,
105
+ borderColor: solidColor,
106
+ borderWidth: width,
107
+ zIndex: gap > 0 ? Z_INDEX.base : Z_INDEX.raised,
108
+ }}
109
+ />
110
+ );
111
+ }
112
+
113
+ const { default: Svg, Defs, LinearGradient, Stop, Circle, Path } = svg;
114
+ const stroke = wantsGradient ? `url(#${gradientId})` : solidColor;
115
+ const coords = gradientCoords(gradientDirection);
116
+ const gradientDefs = wantsGradient ? (
117
+ <Defs>
118
+ <LinearGradient id={gradientId} x1={coords.x1} y1={coords.y1} x2={coords.x2} y2={coords.y2}>
119
+ {colorList.map((color, index) => (
120
+ <Stop
121
+ key={`${gradientId}-${index}`}
122
+ offset={colorList.length === 1 ? 0 : index / (colorList.length - 1)}
123
+ stopColor={color}
124
+ />
125
+ ))}
126
+ </LinearGradient>
127
+ </Defs>
128
+ ) : null;
129
+
130
+ // The overlay fills its parent; the SVG within is sized to the ring box so a
131
+ // `gap === 0` ring overlays the avatar edge exactly and a `gap > 0` ring sits
132
+ // on the reserved outer perimeter.
133
+ const overlayStyle = [
134
+ StyleSheet.absoluteFill,
135
+ { zIndex: gap > 0 ? Z_INDEX.base : Z_INDEX.raised },
136
+ ];
137
+
138
+ if (shape === 'squircle') {
139
+ // The path lives in 0–1 space, so the stroke width is expressed in the same
140
+ // units; center-stroking it and letting the viewport clip the outer half
141
+ // yields a ~`width`-px band on the inner edge of the outer box.
142
+ return (
143
+ <View pointerEvents="none" style={overlayStyle}>
144
+ <Svg width={outer} height={outer} viewBox="0 0 1 1">
145
+ {gradientDefs}
146
+ <Path d={SQUIRCLE_PATH} fill="none" stroke={stroke} strokeWidth={(width / outer) * 2} />
147
+ </Svg>
148
+ </View>
149
+ );
150
+ }
151
+
152
+ return (
153
+ <View pointerEvents="none" style={overlayStyle}>
154
+ <Svg width={outer} height={outer} viewBox={`0 0 ${outer} ${outer}`}>
155
+ {gradientDefs}
156
+ <Circle
157
+ cx={outer / 2}
158
+ cy={outer / 2}
159
+ r={(outer - width) / 2}
160
+ fill="none"
161
+ stroke={stroke}
162
+ strokeWidth={width}
163
+ />
164
+ </Svg>
165
+ </View>
166
+ );
167
+ };
168
+
169
+ export const AvatarRing = memo(AvatarRingComponent);
170
+ AvatarRing.displayName = 'AvatarRing';
@@ -1,7 +1,12 @@
1
1
  export { Avatar } from './Avatar';
2
2
  export { AvatarPlaceholderProvider } from './placeholder-context';
3
3
  export type { AvatarPlaceholderConfig } from './placeholder-context';
4
- export type { AvatarProps, AvatarShape } from './types';
4
+ export type {
5
+ AvatarProps,
6
+ AvatarShape,
7
+ AvatarRingConfig,
8
+ AvatarRingGradientDirection,
9
+ } from './types';
5
10
 
6
11
  // Inlined as a base64 data URI so consumers compiling Bloom's source directly
7
12
  // do not need ambient `*.jpg` module declarations or a `.jpg` asset loader.
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Squircle clip/outline path normalized to a 0–1 coordinate space
3
+ * (viewBox="0 0 1 1"). Shared by the squircle avatar image clip in `Avatar.tsx`
4
+ * and the squircle outline in `AvatarRing.tsx` so the geometry has a single
5
+ * source of truth.
6
+ */
7
+ export const SQUIRCLE_PATH =
8
+ 'M0 0.5 L0.00122 0.31674 L0.00489 0.25123 L0.01103 0.20331 L0.01969 0.16478 L0.03097 0.13257 L0.04495 0.10518 L0.0618 0.08177 L0.08177 0.0618 L0.10518 0.04495 L0.13257 0.03097 L0.16478 0.01969 L0.20331 0.01103 L0.25123 0.00489 L0.31674 0.00122 L0.5 0' +
9
+ ' L0.68895 0.0014 L0.7564 0.00561 L0.80559 0.01267 L0.84499 0.02264 L0.87771 0.03564 L0.9053 0.05181 L0.92862 0.07138 L0.94819 0.0947 L0.96436 0.12228 L0.97736 0.15501 L0.98733 0.19441 L0.99439 0.2436 L0.9986 0.31105 L1 0.5' +
10
+ ' L0.9986 0.68895 L0.99439 0.7564 L0.98733 0.80559 L0.97736 0.84499 L0.96436 0.87771 L0.94819 0.9053 L0.92862 0.92862 L0.9053 0.94819 L0.87771 0.96436 L0.84499 0.97736 L0.80559 0.98733 L0.7564 0.99439 L0.68895 0.9986 L0.5 1' +
11
+ ' L0.31105 0.9986 L0.2436 0.99439 L0.19441 0.98733 L0.15501 0.97736 L0.12228 0.96436 L0.0947 0.94819 L0.07138 0.92862 L0.05181 0.9053 L0.03564 0.87771 L0.02264 0.84499 L0.01267 0.80559 L0.00561 0.7564 L0.0014 0.68895 L0 0.5Z';
@@ -0,0 +1,11 @@
1
+ import { lazyRequire } from '../utils/lazy-require';
2
+
3
+ type SvgModuleType = typeof import('react-native-svg');
4
+
5
+ /**
6
+ * Lazily loads `react-native-svg`, returning `null` when it is not installed
7
+ * (e.g. a web bundle that does not ship the optional peer). Shared by the
8
+ * squircle avatar image clip in `Avatar.tsx` and the `AvatarRing` so there is a
9
+ * single loader rather than one per call site.
10
+ */
11
+ export const getSvgModule = lazyRequire<SvgModuleType>('react-native-svg');
@@ -5,6 +5,20 @@ import type { ImageResolver } from '../image-resolver/context';
5
5
 
6
6
  export type AvatarShape = 'circle' | 'squircle';
7
7
 
8
+ /** Gradient sweep direction for a multi-color {@link AvatarRingConfig}. */
9
+ export type AvatarRingGradientDirection = 'diagonal' | 'horizontal' | 'vertical';
10
+
11
+ export interface AvatarRingConfig {
12
+ /** Solid ring: one color. Gradient ring: 2+ colors. */
13
+ colors: string | string[];
14
+ /** Ring stroke width in px. Default: size > 16 ? 2 : 1. */
15
+ width?: number;
16
+ /** Gap between avatar edge and ring. Default 0 (ring overlays the edge). */
17
+ gap?: number;
18
+ /** Gradient sweep direction for multi-color rings. Default 'diagonal'. */
19
+ gradientDirection?: AvatarRingGradientDirection;
20
+ }
21
+
8
22
  export interface AvatarProps {
9
23
  /**
10
24
  * Flexible image source — accepts a URL string, an ImageSourcePropType
@@ -70,5 +84,23 @@ export interface AvatarProps {
70
84
  * live treatments.
71
85
  */
72
86
  liveColor?: string;
87
+ /**
88
+ * Decorative ring drawn around the avatar. Pass a single color string for a
89
+ * solid ring, or an array of 2+ colors for a gradient ring (e.g. an
90
+ * Instagram-stories ring). The gradient form requires `react-native-svg`
91
+ * (falls back to a solid ring of the first color if it is not installed).
92
+ *
93
+ * When `gap` is 0 (default) the ring overlays the avatar edge and the
94
+ * component's footprint is unchanged. When `gap` > 0 the ring is drawn OUTSIDE
95
+ * the avatar and the rendered footprint grows to `size + 2*(width + gap)` with
96
+ * the avatar centered inside (matching how Instagram-stories rings sit outside
97
+ * the avatar).
98
+ *
99
+ * `live` is sugar over this same primitive: when set (and no explicit `ring`
100
+ * is passed) it renders a solid theme-`negative` ring plus the "LIVE" badge.
101
+ * An explicit `ring` always wins for geometry and colors; the badge stays
102
+ * controlled by `live`/`hideLiveBadge`.
103
+ */
104
+ ring?: AvatarRingConfig;
73
105
  testID?: string;
74
106
  }
@@ -1,6 +1,8 @@
1
1
  import { useMemo } from 'react';
2
2
  import { useWindowDimensions } from 'react-native';
3
3
 
4
+ import { BREAKPOINTS } from '../styles/breakpoints';
5
+
4
6
  /**
5
7
  * Anchor a `<Dialog>` can render against.
6
8
  *
@@ -31,14 +33,15 @@ export type ResponsiveDialogPlacement =
31
33
  };
32
34
 
33
35
  /**
34
- * Tailwind-default breakpoints (px). A responsive `placement` map resolves to
35
- * the value for the largest key whose min-width is `<=` the current viewport
36
- * width, falling back to `base`.
36
+ * Tailwind-default breakpoints (px), re-exported from the canonical
37
+ * {@link BREAKPOINTS} source. A responsive `placement` map resolves to the value
38
+ * for the largest key whose min-width is `<=` the current viewport width,
39
+ * falling back to `base`.
37
40
  */
38
- export const BREAKPOINT_SM = 640;
39
- export const BREAKPOINT_MD = 768;
40
- export const BREAKPOINT_LG = 1024;
41
- export const BREAKPOINT_XL = 1280;
41
+ export const BREAKPOINT_SM = BREAKPOINTS.sm;
42
+ export const BREAKPOINT_MD = BREAKPOINTS.md;
43
+ export const BREAKPOINT_LG = BREAKPOINTS.lg;
44
+ export const BREAKPOINT_XL = BREAKPOINTS.xl;
42
45
 
43
46
  /** Ordered breakpoint table, widest first, so the first match wins. */
44
47
  const RESPONSIVE_KEYS = [
@@ -1,12 +1,12 @@
1
1
  import React, { memo } from 'react';
2
- import { StyleSheet, View, type ViewStyle } from 'react-native';
2
+ import { StyleSheet, View, type StyleProp, type ViewStyle } from 'react-native';
3
3
 
4
4
  const FillComponent = function Fill({
5
5
  children,
6
6
  style,
7
7
  }: {
8
8
  children?: React.ReactNode;
9
- style?: ViewStyle | ViewStyle[];
9
+ style?: StyleProp<ViewStyle>;
10
10
  }) {
11
11
  return (
12
12
  <View style={[fillStyles.base, style]}>
@@ -3,3 +3,7 @@ export { useDelayedLoading } from './useDelayedLoading';
3
3
  export { useThrottledValue } from './useThrottledValue';
4
4
  export { usePressAnimation } from './usePressAnimation';
5
5
  export { mergeRefs } from './mergeRefs';
6
+ export { useHaptics, BloomHapticsProvider } from './useHaptics';
7
+ export type { HapticStrength, BloomHapticsProviderProps } from './useHaptics';
8
+ export { useGutters } from './useGutters';
9
+ export type { Gutter, Gutters } from './useGutters';