@oxy.so/bloom 6.1.0 → 6.2.1

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 (244) hide show
  1. package/README.md +1 -1
  2. package/docs/adoption-matrix.mdx +4 -3
  3. package/docs/bottom-sheet.mdx +1 -1
  4. package/docs/chat-composer.mdx +5 -1
  5. package/docs/cover-header.mdx +56 -0
  6. package/lib/commonjs/ai-profile-card/AiProfileCard.js +186 -263
  7. package/lib/commonjs/ai-profile-card/AiProfileCard.js.map +1 -1
  8. package/lib/commonjs/bottom-sheet/BottomSheetBase.js +33 -49
  9. package/lib/commonjs/bottom-sheet/BottomSheetBase.js.map +1 -1
  10. package/lib/commonjs/chat-composer/SuggestionList.js +3 -1
  11. package/lib/commonjs/chat-composer/SuggestionList.js.map +1 -1
  12. package/lib/commonjs/chat-composer/SuggestionList.web.js +3 -1
  13. package/lib/commonjs/chat-composer/SuggestionList.web.js.map +1 -1
  14. package/lib/commonjs/chat-composer/create-suggestion-list.js +64 -10
  15. package/lib/commonjs/chat-composer/create-suggestion-list.js.map +1 -1
  16. package/lib/commonjs/chat-composer/messages.js +7 -0
  17. package/lib/commonjs/chat-composer/messages.js.map +1 -1
  18. package/lib/commonjs/cover-header/CoverHeader.js +83 -0
  19. package/lib/commonjs/cover-header/CoverHeader.js.map +1 -0
  20. package/lib/commonjs/cover-header/CoverImage.js +72 -0
  21. package/lib/commonjs/cover-header/CoverImage.js.map +1 -0
  22. package/lib/commonjs/cover-header/index.js +13 -0
  23. package/lib/commonjs/cover-header/index.js.map +1 -0
  24. package/lib/commonjs/cover-header/types.js +6 -0
  25. package/lib/commonjs/cover-header/types.js.map +1 -0
  26. package/lib/commonjs/dialog/DialogBottomSheet.js +1 -1
  27. package/lib/commonjs/dialog/DialogBottomSheet.js.map +1 -1
  28. package/lib/commonjs/index.js +951 -939
  29. package/lib/commonjs/index.js.map +1 -1
  30. package/lib/commonjs/index.web.js +951 -939
  31. package/lib/commonjs/index.web.js.map +1 -1
  32. package/lib/commonjs/locale/translations/ar.js +7 -0
  33. package/lib/commonjs/locale/translations/ar.js.map +1 -1
  34. package/lib/commonjs/locale/translations/bn.js +7 -0
  35. package/lib/commonjs/locale/translations/bn.js.map +1 -1
  36. package/lib/commonjs/locale/translations/ca.js +7 -0
  37. package/lib/commonjs/locale/translations/ca.js.map +1 -1
  38. package/lib/commonjs/locale/translations/de.js +7 -0
  39. package/lib/commonjs/locale/translations/de.js.map +1 -1
  40. package/lib/commonjs/locale/translations/es.js +7 -0
  41. package/lib/commonjs/locale/translations/es.js.map +1 -1
  42. package/lib/commonjs/locale/translations/fr.js +7 -0
  43. package/lib/commonjs/locale/translations/fr.js.map +1 -1
  44. package/lib/commonjs/locale/translations/hi.js +7 -0
  45. package/lib/commonjs/locale/translations/hi.js.map +1 -1
  46. package/lib/commonjs/locale/translations/id.js +7 -0
  47. package/lib/commonjs/locale/translations/id.js.map +1 -1
  48. package/lib/commonjs/locale/translations/it.js +7 -0
  49. package/lib/commonjs/locale/translations/it.js.map +1 -1
  50. package/lib/commonjs/locale/translations/ja.js +7 -0
  51. package/lib/commonjs/locale/translations/ja.js.map +1 -1
  52. package/lib/commonjs/locale/translations/pt.js +7 -0
  53. package/lib/commonjs/locale/translations/pt.js.map +1 -1
  54. package/lib/commonjs/locale/translations/ru.js +7 -0
  55. package/lib/commonjs/locale/translations/ru.js.map +1 -1
  56. package/lib/commonjs/locale/translations/tr.js +7 -0
  57. package/lib/commonjs/locale/translations/tr.js.map +1 -1
  58. package/lib/commonjs/locale/translations/zh.js +7 -0
  59. package/lib/commonjs/locale/translations/zh.js.map +1 -1
  60. package/lib/commonjs/surface/SurfacePaint.js +17 -4
  61. package/lib/commonjs/surface/SurfacePaint.js.map +1 -1
  62. package/lib/module/ai-profile-card/AiProfileCard.js +185 -262
  63. package/lib/module/ai-profile-card/AiProfileCard.js.map +1 -1
  64. package/lib/module/bottom-sheet/BottomSheetBase.js +33 -49
  65. package/lib/module/bottom-sheet/BottomSheetBase.js.map +1 -1
  66. package/lib/module/chat-composer/SuggestionList.js +3 -1
  67. package/lib/module/chat-composer/SuggestionList.js.map +1 -1
  68. package/lib/module/chat-composer/SuggestionList.web.js +3 -1
  69. package/lib/module/chat-composer/SuggestionList.web.js.map +1 -1
  70. package/lib/module/chat-composer/create-suggestion-list.js +64 -10
  71. package/lib/module/chat-composer/create-suggestion-list.js.map +1 -1
  72. package/lib/module/chat-composer/messages.js +7 -0
  73. package/lib/module/chat-composer/messages.js.map +1 -1
  74. package/lib/module/cover-header/CoverHeader.js +78 -0
  75. package/lib/module/cover-header/CoverHeader.js.map +1 -0
  76. package/lib/module/cover-header/CoverImage.js +68 -0
  77. package/lib/module/cover-header/CoverImage.js.map +1 -0
  78. package/lib/module/cover-header/index.js +4 -0
  79. package/lib/module/cover-header/index.js.map +1 -0
  80. package/lib/module/cover-header/types.js +4 -0
  81. package/lib/module/cover-header/types.js.map +1 -0
  82. package/lib/module/dialog/DialogBottomSheet.js +1 -1
  83. package/lib/module/dialog/DialogBottomSheet.js.map +1 -1
  84. package/lib/module/index.js +1 -0
  85. package/lib/module/index.js.map +1 -1
  86. package/lib/module/index.web.js +1 -0
  87. package/lib/module/index.web.js.map +1 -1
  88. package/lib/module/locale/translations/ar.js +7 -0
  89. package/lib/module/locale/translations/ar.js.map +1 -1
  90. package/lib/module/locale/translations/bn.js +7 -0
  91. package/lib/module/locale/translations/bn.js.map +1 -1
  92. package/lib/module/locale/translations/ca.js +7 -0
  93. package/lib/module/locale/translations/ca.js.map +1 -1
  94. package/lib/module/locale/translations/de.js +7 -0
  95. package/lib/module/locale/translations/de.js.map +1 -1
  96. package/lib/module/locale/translations/es.js +7 -0
  97. package/lib/module/locale/translations/es.js.map +1 -1
  98. package/lib/module/locale/translations/fr.js +7 -0
  99. package/lib/module/locale/translations/fr.js.map +1 -1
  100. package/lib/module/locale/translations/hi.js +7 -0
  101. package/lib/module/locale/translations/hi.js.map +1 -1
  102. package/lib/module/locale/translations/id.js +7 -0
  103. package/lib/module/locale/translations/id.js.map +1 -1
  104. package/lib/module/locale/translations/it.js +7 -0
  105. package/lib/module/locale/translations/it.js.map +1 -1
  106. package/lib/module/locale/translations/ja.js +7 -0
  107. package/lib/module/locale/translations/ja.js.map +1 -1
  108. package/lib/module/locale/translations/pt.js +7 -0
  109. package/lib/module/locale/translations/pt.js.map +1 -1
  110. package/lib/module/locale/translations/ru.js +7 -0
  111. package/lib/module/locale/translations/ru.js.map +1 -1
  112. package/lib/module/locale/translations/tr.js +7 -0
  113. package/lib/module/locale/translations/tr.js.map +1 -1
  114. package/lib/module/locale/translations/zh.js +7 -0
  115. package/lib/module/locale/translations/zh.js.map +1 -1
  116. package/lib/module/surface/SurfacePaint.js +18 -5
  117. package/lib/module/surface/SurfacePaint.js.map +1 -1
  118. package/lib/typescript/commonjs/ai-profile-card/AiProfileCard.d.ts.map +1 -1
  119. package/lib/typescript/commonjs/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  120. package/lib/typescript/commonjs/bottom-sheet/types.d.ts +1 -0
  121. package/lib/typescript/commonjs/bottom-sheet/types.d.ts.map +1 -1
  122. package/lib/typescript/commonjs/chat-composer/SuggestionList.d.ts +1 -1
  123. package/lib/typescript/commonjs/chat-composer/SuggestionList.d.ts.map +1 -1
  124. package/lib/typescript/commonjs/chat-composer/SuggestionList.web.d.ts +1 -1
  125. package/lib/typescript/commonjs/chat-composer/SuggestionList.web.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/chat-composer/create-suggestion-list.d.ts +4 -2
  127. package/lib/typescript/commonjs/chat-composer/create-suggestion-list.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/chat-composer/messages.d.ts +6 -0
  129. package/lib/typescript/commonjs/chat-composer/messages.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/chat-composer/types.d.ts +17 -0
  131. package/lib/typescript/commonjs/chat-composer/types.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/cover-header/CoverHeader.d.ts +21 -0
  133. package/lib/typescript/commonjs/cover-header/CoverHeader.d.ts.map +1 -0
  134. package/lib/typescript/commonjs/cover-header/CoverImage.d.ts +11 -0
  135. package/lib/typescript/commonjs/cover-header/CoverImage.d.ts.map +1 -0
  136. package/lib/typescript/commonjs/cover-header/index.d.ts +3 -0
  137. package/lib/typescript/commonjs/cover-header/index.d.ts.map +1 -0
  138. package/lib/typescript/commonjs/cover-header/types.d.ts +33 -0
  139. package/lib/typescript/commonjs/cover-header/types.d.ts.map +1 -0
  140. package/lib/typescript/commonjs/index.d.ts +1 -0
  141. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  142. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  143. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  144. package/lib/typescript/commonjs/locale/translations/ar.d.ts.map +1 -1
  145. package/lib/typescript/commonjs/locale/translations/bn.d.ts.map +1 -1
  146. package/lib/typescript/commonjs/locale/translations/ca.d.ts.map +1 -1
  147. package/lib/typescript/commonjs/locale/translations/de.d.ts.map +1 -1
  148. package/lib/typescript/commonjs/locale/translations/es.d.ts.map +1 -1
  149. package/lib/typescript/commonjs/locale/translations/fr.d.ts.map +1 -1
  150. package/lib/typescript/commonjs/locale/translations/hi.d.ts.map +1 -1
  151. package/lib/typescript/commonjs/locale/translations/id.d.ts.map +1 -1
  152. package/lib/typescript/commonjs/locale/translations/it.d.ts.map +1 -1
  153. package/lib/typescript/commonjs/locale/translations/ja.d.ts.map +1 -1
  154. package/lib/typescript/commonjs/locale/translations/pt.d.ts.map +1 -1
  155. package/lib/typescript/commonjs/locale/translations/ru.d.ts.map +1 -1
  156. package/lib/typescript/commonjs/locale/translations/tr.d.ts.map +1 -1
  157. package/lib/typescript/commonjs/locale/translations/zh.d.ts.map +1 -1
  158. package/lib/typescript/commonjs/surface/SurfacePaint.d.ts.map +1 -1
  159. package/lib/typescript/module/ai-profile-card/AiProfileCard.d.ts.map +1 -1
  160. package/lib/typescript/module/bottom-sheet/BottomSheetBase.d.ts.map +1 -1
  161. package/lib/typescript/module/bottom-sheet/types.d.ts +1 -0
  162. package/lib/typescript/module/bottom-sheet/types.d.ts.map +1 -1
  163. package/lib/typescript/module/chat-composer/SuggestionList.d.ts +1 -1
  164. package/lib/typescript/module/chat-composer/SuggestionList.d.ts.map +1 -1
  165. package/lib/typescript/module/chat-composer/SuggestionList.web.d.ts +1 -1
  166. package/lib/typescript/module/chat-composer/SuggestionList.web.d.ts.map +1 -1
  167. package/lib/typescript/module/chat-composer/create-suggestion-list.d.ts +4 -2
  168. package/lib/typescript/module/chat-composer/create-suggestion-list.d.ts.map +1 -1
  169. package/lib/typescript/module/chat-composer/messages.d.ts +6 -0
  170. package/lib/typescript/module/chat-composer/messages.d.ts.map +1 -1
  171. package/lib/typescript/module/chat-composer/types.d.ts +17 -0
  172. package/lib/typescript/module/chat-composer/types.d.ts.map +1 -1
  173. package/lib/typescript/module/cover-header/CoverHeader.d.ts +21 -0
  174. package/lib/typescript/module/cover-header/CoverHeader.d.ts.map +1 -0
  175. package/lib/typescript/module/cover-header/CoverImage.d.ts +11 -0
  176. package/lib/typescript/module/cover-header/CoverImage.d.ts.map +1 -0
  177. package/lib/typescript/module/cover-header/index.d.ts +3 -0
  178. package/lib/typescript/module/cover-header/index.d.ts.map +1 -0
  179. package/lib/typescript/module/cover-header/types.d.ts +33 -0
  180. package/lib/typescript/module/cover-header/types.d.ts.map +1 -0
  181. package/lib/typescript/module/index.d.ts +1 -0
  182. package/lib/typescript/module/index.d.ts.map +1 -1
  183. package/lib/typescript/module/index.web.d.ts +1 -0
  184. package/lib/typescript/module/index.web.d.ts.map +1 -1
  185. package/lib/typescript/module/locale/translations/ar.d.ts.map +1 -1
  186. package/lib/typescript/module/locale/translations/bn.d.ts.map +1 -1
  187. package/lib/typescript/module/locale/translations/ca.d.ts.map +1 -1
  188. package/lib/typescript/module/locale/translations/de.d.ts.map +1 -1
  189. package/lib/typescript/module/locale/translations/es.d.ts.map +1 -1
  190. package/lib/typescript/module/locale/translations/fr.d.ts.map +1 -1
  191. package/lib/typescript/module/locale/translations/hi.d.ts.map +1 -1
  192. package/lib/typescript/module/locale/translations/id.d.ts.map +1 -1
  193. package/lib/typescript/module/locale/translations/it.d.ts.map +1 -1
  194. package/lib/typescript/module/locale/translations/ja.d.ts.map +1 -1
  195. package/lib/typescript/module/locale/translations/pt.d.ts.map +1 -1
  196. package/lib/typescript/module/locale/translations/ru.d.ts.map +1 -1
  197. package/lib/typescript/module/locale/translations/tr.d.ts.map +1 -1
  198. package/lib/typescript/module/locale/translations/zh.d.ts.map +1 -1
  199. package/lib/typescript/module/surface/SurfacePaint.d.ts.map +1 -1
  200. package/package.json +18 -1
  201. package/src/__tests__/support/adoption-matrix.ts +683 -0
  202. package/src/__tests__/support/card-surface.ts +25 -0
  203. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  204. package/src/__tests__/support/commerce-harness.tsx +97 -0
  205. package/src/__tests__/support/composite.ts +68 -0
  206. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  207. package/src/__tests__/support/messages-in.ts +10 -0
  208. package/src/__tests__/support/press-host.ts +30 -0
  209. package/src/__tests__/support/rendered-style.ts +99 -0
  210. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  211. package/src/__tests__/support/worklet-capture-fixture.tsx +28 -0
  212. package/src/ai-profile-card/AiProfileCard.tsx +159 -241
  213. package/src/bottom-sheet/BottomSheetBase.tsx +24 -34
  214. package/src/bottom-sheet/types.ts +1 -0
  215. package/src/chat-composer/SuggestionList.tsx +2 -1
  216. package/src/chat-composer/SuggestionList.web.tsx +2 -1
  217. package/src/chat-composer/create-suggestion-list.tsx +54 -8
  218. package/src/chat-composer/messages.ts +9 -0
  219. package/src/chat-composer/types.ts +17 -0
  220. package/src/cover-header/CoverHeader.tsx +75 -0
  221. package/src/cover-header/CoverImage.tsx +60 -0
  222. package/src/cover-header/index.ts +3 -0
  223. package/src/cover-header/types.ts +30 -0
  224. package/src/dialog/DialogBottomSheet.tsx +1 -1
  225. package/src/index.ts +1 -0
  226. package/src/index.web.ts +1 -0
  227. package/src/locale/translations/ar.ts +3 -0
  228. package/src/locale/translations/bn.ts +3 -0
  229. package/src/locale/translations/ca.ts +3 -0
  230. package/src/locale/translations/de.ts +3 -0
  231. package/src/locale/translations/es.ts +3 -0
  232. package/src/locale/translations/fr.ts +3 -0
  233. package/src/locale/translations/hi.ts +3 -0
  234. package/src/locale/translations/id.ts +3 -0
  235. package/src/locale/translations/it.ts +3 -0
  236. package/src/locale/translations/ja.ts +3 -0
  237. package/src/locale/translations/pt.ts +3 -0
  238. package/src/locale/translations/ru.ts +3 -0
  239. package/src/locale/translations/tr.ts +3 -0
  240. package/src/locale/translations/zh.ts +3 -0
  241. package/src/surface/SurfacePaint.tsx +14 -4
  242. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
  243. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
  244. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
@@ -1,15 +1,5 @@
1
- import React, { memo, useCallback, useEffect, useMemo, useState } from 'react';
2
- import {
3
- Image,
4
- ScrollView,
5
- StyleSheet,
6
- View,
7
- useWindowDimensions,
8
- type ImageLoadEventData,
9
- type ImageSourcePropType,
10
- type LayoutChangeEvent,
11
- type NativeSyntheticEvent,
12
- } from 'react-native';
1
+ import React, { memo, useEffect, useMemo, useState } from 'react';
2
+ import { ScrollView, StyleSheet, View, useWindowDimensions } from 'react-native';
13
3
 
14
4
  import { Avatar } from '../avatar';
15
5
  import { Badge } from '../badge';
@@ -19,6 +9,7 @@ import { TABULAR } from '../chart-cards/primitives/ChartHeader';
19
9
  import { groupThousands } from '../chart-cards/primitives/format';
20
10
  import { useCountUp } from '../chart-cards/use-count-up';
21
11
  import { Chip } from '../chip';
12
+ import { CoverHeader } from '../cover-header';
22
13
  import { useControllableState } from '../hooks/use-controllable-state';
23
14
  import { useMessages } from '../locale/messages';
24
15
  import { SegmentedControl, SegmentedControlItem, SegmentedControlItemText } from '../segmented-control';
@@ -37,7 +28,7 @@ import { formatCurrency } from '../locale/format-number';
37
28
  * card radius 24, 1px borderLight, no
38
29
  * fill, clips its children
39
30
  * cover absolute over the top 165px, top corners 23, background-tertiary
40
- * under the photo, `object-position: 50% 45%`
31
+ * under the photo, `object-position: 50% 45%` — a `CoverHeader`
41
32
  * content column 15 apart, padding 124 top / 16 sides / 16 bottom
42
33
  * avatar 80 disc, background-tertiary, initials 30 / 42.5 medium
43
34
  * text-secondary — overlapping the cover's bottom edge by 41
@@ -59,56 +50,8 @@ import { formatCurrency } from '../locale/format-number';
59
50
 
60
51
  const COVER_HEIGHT = 165;
61
52
  const AVATAR_SIZE = 80;
62
-
63
- /** `object-fit: cover` with an `object-position`: the image sized to cover the box, offset by the fractions. */
64
- function CoverImage({
65
- source,
66
- position,
67
- testID,
68
- }: {
69
- source: string | ImageSourcePropType;
70
- position: { x: number; y: number };
71
- testID?: string;
72
- }) {
73
- const [box, setBox] = useState<{ width: number; height: number } | null>(null);
74
- const [natural, setNatural] = useState<{ width: number; height: number } | null>(null);
75
- const resolved: ImageSourcePropType = typeof source === 'string' ? { uri: source } : source;
76
-
77
- const onLayout = useCallback((event: LayoutChangeEvent) => {
78
- const { width, height } = event.nativeEvent.layout;
79
- setBox((prev) => (prev && prev.width === width && prev.height === height ? prev : { width, height }));
80
- }, []);
81
- const onLoad = useCallback((event: NativeSyntheticEvent<ImageLoadEventData>) => {
82
- const s = event.nativeEvent?.source;
83
- if (s && s.width > 0 && s.height > 0) setNatural({ width: s.width, height: s.height });
84
- }, []);
85
-
86
- const frame = useMemo(() => {
87
- if (!box || !natural) return null;
88
- const scale = Math.max(box.width / natural.width, box.height / natural.height);
89
- const width = natural.width * scale;
90
- const height = natural.height * scale;
91
- return { width, height, left: (box.width - width) * position.x, top: (box.height - height) * position.y };
92
- }, [box, natural, position.x, position.y]);
93
-
94
- return (
95
- <View style={StyleSheet.absoluteFill} onLayout={onLayout} testID={testID ? `${testID}-frame` : undefined}>
96
- <Image
97
- testID={testID}
98
- source={resolved}
99
- onLoad={onLoad}
100
- accessibilityIgnoresInvertColors
101
- aria-hidden
102
- resizeMode="cover"
103
- style={
104
- frame
105
- ? { position: 'absolute', left: frame.left, top: frame.top, width: frame.width, height: frame.height }
106
- : StyleSheet.absoluteFill
107
- }
108
- />
109
- </View>
110
- );
111
- }
53
+ /** Where the content starts: the avatar overlaps the cover's bottom edge by 41. */
54
+ const CONTENT_TOP = 124;
112
55
 
113
56
  /**
114
57
  * The count-up headline, in its own component: the roll re-renders every frame
@@ -223,12 +166,14 @@ function AiProfileCardComponent({
223
166
  );
224
167
 
225
168
  return (
226
- <View
169
+ <CoverHeader
227
170
  testID={testID}
171
+ coverSource={coverSource}
172
+ coverPosition={coverPosition}
173
+ coverHeight={COVER_HEIGHT}
174
+ overlap={COVER_HEIGHT - CONTENT_TOP}
228
175
  style={[
229
176
  {
230
- position: 'relative',
231
- width: '100%',
232
177
  minWidth: 0,
233
178
  overflow: 'hidden',
234
179
  borderRadius: 24,
@@ -236,195 +181,168 @@ function AiProfileCardComponent({
236
181
  borderColor: colors.border,
237
182
  },
238
183
  style,
239
- ]}>
240
- <View
241
- testID={testID ? `${testID}-cover` : undefined}
242
- style={{
243
- position: 'absolute',
244
- top: 0,
245
- left: 0,
246
- right: 0,
247
- height: COVER_HEIGHT,
248
- overflow: 'hidden',
249
- borderTopLeftRadius: 23,
250
- borderTopRightRadius: 23,
251
- backgroundColor: colors.tertiary,
252
- }}>
253
- {coverSource ? (
254
- <CoverImage
255
- source={coverSource}
256
- position={coverPosition}
257
- testID={testID ? `${testID}-cover-image` : undefined}
258
- />
259
- ) : null}
260
- </View>
261
-
262
- <View
263
- testID={testID ? `${testID}-content` : undefined}
264
- style={{
265
- position: 'relative',
266
- width: '100%',
267
- flexDirection: 'column',
268
- gap: 15,
269
- paddingTop: 124,
270
- paddingBottom: 16,
271
- paddingLeft: 16,
272
- paddingRight: 16,
273
- }}>
274
- <Avatar
275
- testID={testID ? `${testID}-avatar` : undefined}
276
- size={AVATAR_SIZE}
277
- source={avatarSource ?? undefined}
278
- initials={letters}
279
- color="neutral"
280
- alt={avatarSource ? name : undefined}
281
- placeholderColor={colors.tertiary}
282
- placeholderIcon={
283
- <Text
284
- allowFontScaling={false}
285
- numberOfLines={1}
286
- style={[styles.initials, { color: colors.textSecondary }]}>
287
- {letters}
288
- </Text>
289
- }
290
- />
291
-
292
- <View style={{ position: 'relative', width: '100%', flexDirection: 'row', alignItems: 'flex-start', gap: 15 }}>
293
- <View style={{ minWidth: 0, flex: 1, flexDirection: 'column', gap: 4 }}>
294
- <Text variant="title-2-medium" numberOfLines={1} style={{ color: colors.text }}>
295
- {name}
296
- </Text>
297
- {handle != null || badge != null ? (
298
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
299
- {handle != null ? (
300
- <Text variant="headline-medium" numberOfLines={1} style={{ color: colors.textSecondary }}>
301
- {handle}
302
- </Text>
303
- ) : null}
304
- {badge != null ? (
305
- <Badge
306
- testID={testID ? `${testID}-badge` : undefined}
307
- content={badge}
308
- style={{ backgroundColor: colors.tertiary }}
309
- textStyle={{ color: colors.textSecondary }}
310
- />
311
- ) : null}
312
- </View>
313
- ) : null}
314
- </View>
315
- {actions ? (
316
- <View
317
- testID={testID ? `${testID}-actions` : undefined}
318
- style={{
319
- position: 'absolute',
320
- top: -34,
321
- right: 4,
322
- flexDirection: 'row',
323
- alignItems: 'center',
324
- justifyContent: 'flex-end',
325
- gap: 10,
326
- }}>
327
- {actions}
328
- </View>
329
- ) : null}
330
- </View>
184
+ ]}
185
+ coverStyle={{ borderTopLeftRadius: 23, borderTopRightRadius: 23 }}
186
+ contentStyle={{
187
+ flexDirection: 'column',
188
+ gap: 15,
189
+ paddingBottom: 16,
190
+ paddingLeft: 16,
191
+ paddingRight: 16,
192
+ }}>
193
+ <Avatar
194
+ testID={testID ? `${testID}-avatar` : undefined}
195
+ size={AVATAR_SIZE}
196
+ source={avatarSource ?? undefined}
197
+ initials={letters}
198
+ color="neutral"
199
+ alt={avatarSource ? name : undefined}
200
+ placeholderColor={colors.tertiary}
201
+ placeholderIcon={
202
+ <Text
203
+ allowFontScaling={false}
204
+ numberOfLines={1}
205
+ style={[styles.initials, { color: colors.textSecondary }]}>
206
+ {letters}
207
+ </Text>
208
+ }
209
+ />
331
210
 
332
- <View style={{ width: '100%', flexDirection: 'column', gap: 8 }}>
333
- <View style={{ flexDirection: 'column', gap: 2 }}>
334
- <Text variant="body-medium" numberOfLines={1} style={{ color: colors.textSecondary }}>
335
- {contributionsLabel}
336
- </Text>
337
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
338
- <RollingHeadline
339
- value={contributions}
340
- duration={countUpDuration}
341
- format={format}
342
- color={colors.text}
343
- testID={testID ? `${testID}-headline` : undefined}
344
- />
345
- {delta != null ? (
346
- <Chip
347
- size="md"
348
- testID={testID ? `${testID}-delta` : undefined}
349
- style={{ alignSelf: 'center', backgroundColor: colors.chip.background }}
350
- textStyle={{ color: colors.chip.foreground }}>
351
- {delta}
352
- </Chip>
211
+ <View style={{ position: 'relative', width: '100%', flexDirection: 'row', alignItems: 'flex-start', gap: 15 }}>
212
+ <View style={{ minWidth: 0, flex: 1, flexDirection: 'column', gap: 4 }}>
213
+ <Text variant="title-2-medium" numberOfLines={1} style={{ color: colors.text }}>
214
+ {name}
215
+ </Text>
216
+ {handle != null || badge != null ? (
217
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
218
+ {handle != null ? (
219
+ <Text variant="headline-medium" numberOfLines={1} style={{ color: colors.textSecondary }}>
220
+ {handle}
221
+ </Text>
222
+ ) : null}
223
+ {badge != null ? (
224
+ <Badge
225
+ testID={testID ? `${testID}-badge` : undefined}
226
+ content={badge}
227
+ style={{ backgroundColor: colors.tertiary }}
228
+ textStyle={{ color: colors.textSecondary }}
229
+ />
353
230
  ) : null}
354
- </View>
355
- </View>
356
-
357
- {stats && stats.length > 0 ? (
358
- <View testID={testID ? `${testID}-stats` : undefined} style={{ flexDirection: 'column', gap: 8 }}>
359
- {(wide ? [stats.map((_, i) => i)] : pairs(stats.length)).map((row) => (
360
- <View key={row[0]} style={{ flexDirection: 'row', alignItems: 'stretch', gap: 8 }}>
361
- {row.map((i) => {
362
- const stat = stats[i]!;
363
- return (
364
- <View
365
- key={`${i}-${stat.label}`}
366
- testID={testID ? `${testID}-stat-${i}` : undefined}
367
- style={[styles.tile, { backgroundColor: colors.secondary }]}>
368
- <Text variant="body-medium" numberOfLines={1} style={{ width: '100%', color: colors.text }}>
369
- {stat.value}
370
- </Text>
371
- <Text
372
- variant="body-2-medium"
373
- numberOfLines={1}
374
- style={{ width: '100%', color: colors.textSecondary }}>
375
- {stat.label}
376
- </Text>
377
- </View>
378
- );
379
- })}
380
- {/* An odd tile keeps its half of the two-column grid. */}
381
- {!wide && row.length === 1 ? <View style={styles.tileSpacer} /> : null}
382
- </View>
383
- ))}
384
231
  </View>
385
232
  ) : null}
386
-
233
+ </View>
234
+ {actions ? (
387
235
  <View
236
+ testID={testID ? `${testID}-actions` : undefined}
388
237
  style={{
389
- width: '100%',
238
+ position: 'absolute',
239
+ top: -34,
240
+ right: 4,
390
241
  flexDirection: 'row',
391
242
  alignItems: 'center',
392
- justifyContent: 'space-between',
393
- paddingTop: 6,
394
- paddingLeft: 2,
243
+ justifyContent: 'flex-end',
244
+ gap: 10,
395
245
  }}>
396
- <Text variant="body-2-medium" style={{ color: colors.textSecondary }}>
397
- {activityLabel}
398
- </Text>
399
- {periods.length > 0 ? (
400
- <SegmentedControl
401
- label={messages.periodGroup(activityLabel)}
402
- type="radio"
403
- variant="plain"
404
- value={selectedPeriod}
405
- onValueChange={setPeriod}>
406
- {periods.map((p) => (
407
- <SegmentedControlItem
408
- key={p.id}
409
- value={p.id}
410
- testID={testID ? `${testID}-period-${p.id}` : undefined}>
411
- <SegmentedControlItemText>{p.label}</SegmentedControlItemText>
412
- </SegmentedControlItem>
413
- ))}
414
- </SegmentedControl>
246
+ {actions}
247
+ </View>
248
+ ) : null}
249
+ </View>
250
+
251
+ <View style={{ width: '100%', flexDirection: 'column', gap: 8 }}>
252
+ <View style={{ flexDirection: 'column', gap: 2 }}>
253
+ <Text variant="body-medium" numberOfLines={1} style={{ color: colors.textSecondary }}>
254
+ {contributionsLabel}
255
+ </Text>
256
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
257
+ <RollingHeadline
258
+ value={contributions}
259
+ duration={countUpDuration}
260
+ format={format}
261
+ color={colors.text}
262
+ testID={testID ? `${testID}-headline` : undefined}
263
+ />
264
+ {delta != null ? (
265
+ <Chip
266
+ size="md"
267
+ testID={testID ? `${testID}-delta` : undefined}
268
+ style={{ alignSelf: 'center', backgroundColor: colors.chip.background }}
269
+ textStyle={{ color: colors.chip.foreground }}>
270
+ {delta}
271
+ </Chip>
415
272
  ) : null}
416
273
  </View>
274
+ </View>
417
275
 
418
- {wide ? (
419
- grid
420
- ) : (
421
- <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ flexGrow: 0 }}>
422
- {grid}
423
- </ScrollView>
424
- )}
276
+ {stats && stats.length > 0 ? (
277
+ <View testID={testID ? `${testID}-stats` : undefined} style={{ flexDirection: 'column', gap: 8 }}>
278
+ {(wide ? [stats.map((_, i) => i)] : pairs(stats.length)).map((row) => (
279
+ <View key={row[0]} style={{ flexDirection: 'row', alignItems: 'stretch', gap: 8 }}>
280
+ {row.map((i) => {
281
+ const stat = stats[i]!;
282
+ return (
283
+ <View
284
+ key={`${i}-${stat.label}`}
285
+ testID={testID ? `${testID}-stat-${i}` : undefined}
286
+ style={[styles.tile, { backgroundColor: colors.secondary }]}>
287
+ <Text variant="body-medium" numberOfLines={1} style={{ width: '100%', color: colors.text }}>
288
+ {stat.value}
289
+ </Text>
290
+ <Text
291
+ variant="body-2-medium"
292
+ numberOfLines={1}
293
+ style={{ width: '100%', color: colors.textSecondary }}>
294
+ {stat.label}
295
+ </Text>
296
+ </View>
297
+ );
298
+ })}
299
+ {/* An odd tile keeps its half of the two-column grid. */}
300
+ {!wide && row.length === 1 ? <View style={styles.tileSpacer} /> : null}
301
+ </View>
302
+ ))}
303
+ </View>
304
+ ) : null}
305
+
306
+ <View
307
+ style={{
308
+ width: '100%',
309
+ flexDirection: 'row',
310
+ alignItems: 'center',
311
+ justifyContent: 'space-between',
312
+ paddingTop: 6,
313
+ paddingLeft: 2,
314
+ }}>
315
+ <Text variant="body-2-medium" style={{ color: colors.textSecondary }}>
316
+ {activityLabel}
317
+ </Text>
318
+ {periods.length > 0 ? (
319
+ <SegmentedControl
320
+ label={messages.periodGroup(activityLabel)}
321
+ type="radio"
322
+ variant="plain"
323
+ value={selectedPeriod}
324
+ onValueChange={setPeriod}>
325
+ {periods.map((p) => (
326
+ <SegmentedControlItem
327
+ key={p.id}
328
+ value={p.id}
329
+ testID={testID ? `${testID}-period-${p.id}` : undefined}>
330
+ <SegmentedControlItemText>{p.label}</SegmentedControlItemText>
331
+ </SegmentedControlItem>
332
+ ))}
333
+ </SegmentedControl>
334
+ ) : null}
425
335
  </View>
336
+
337
+ {wide ? (
338
+ grid
339
+ ) : (
340
+ <ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ flexGrow: 0 }}>
341
+ {grid}
342
+ </ScrollView>
343
+ )}
426
344
  </View>
427
- </View>
345
+ </CoverHeader>
428
346
  );
429
347
  }
430
348
 
@@ -133,14 +133,11 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
133
133
  const bodyPanRef = useRef<GestureType | undefined>(undefined);
134
134
  const handlePanRef = useRef<GestureType | undefined>(undefined);
135
135
 
136
- // Dismiss callbacks
136
+ // Completion is not a user request: once closed, every owner must hear it.
137
+ // Vetoes are checked before a user-triggered close starts, never after it.
137
138
  const safeClose = useCallback(() => {
138
- if (onDismissAttempt?.()) {
139
- onDismiss?.();
140
- } else if (!onDismissAttempt) {
141
- onDismiss?.();
142
- }
143
- }, [onDismissAttempt, onDismiss]);
139
+ onDismiss?.();
140
+ }, [onDismiss]);
144
141
 
145
142
  // Mirror `safeClose` and `rendered` into refs so the unmount cleanup can
146
143
  // fire the latest dismiss callback when needed, without re-binding the
@@ -251,6 +248,20 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
251
248
  };
252
249
  }, [colors.border]);
253
250
 
251
+ // Gesture callbacks cross to JS before consulting the consumer's veto.
252
+ // A refused drag springs back; it never hides the Modal or strands its owner.
253
+ const requestPanDismiss = useCallback((generation: number, velocity: number) => {
254
+ if (closeGenerationRef.current !== generation || hasClosedRef.current) return;
255
+ if (onDismissAttempt && !onDismissAttempt()) {
256
+ translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
257
+ return;
258
+ }
259
+ translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
260
+ opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
261
+ if (finished) runOnJS(finishClose)(generation);
262
+ });
263
+ }, [onDismissAttempt, translateY, screenHeightSV, opacity, finishClose]);
264
+
254
265
  const present = useCallback(() => {
255
266
  setRendered(true);
256
267
  setVisible(true);
@@ -336,15 +347,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
336
347
  (distance > closeThreshold && velocity > -300);
337
348
 
338
349
  if (shouldClose) {
339
- // Snapshot the generation on the UI thread at the
340
- // moment the close gesture commits. The completion
341
- // callback only fires `finishClose` if no reopen
342
- // bumped the generation in between.
343
- const generation = closeGeneration.value;
344
- translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
345
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
346
- if (finished) runOnJS(finishClose)(generation);
347
- });
350
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
348
351
  } else {
349
352
  translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
350
353
  }
@@ -396,16 +399,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
396
399
  (distance > closeThreshold && velocity > -300);
397
400
 
398
401
  if (shouldClose) {
399
- const generation = closeGeneration.value;
400
- translateY.value = withSpring(screenHeightSV.value, {
401
- ...SPRING_CONFIG,
402
- velocity: velocity,
403
- });
404
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
405
- if (finished) {
406
- runOnJS(finishClose)(generation);
407
- }
408
- });
402
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
409
403
  } else {
410
404
  translateY.value = withSpring(0, {
411
405
  ...SPRING_CONFIG,
@@ -417,7 +411,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
417
411
  // values that change the gesture's behavior. `finishClose` is stable
418
412
  // (useCallback with stable deps).
419
413
  // eslint-disable-next-line react-hooks/exhaustive-deps
420
- }, [enablePanDownToClose, detached, manualActivation, nativeGesture, finishClose]);
414
+ }, [enablePanDownToClose, detached, manualActivation, nativeGesture, requestPanDismiss]);
421
415
 
422
416
  // Dedicated handle pan — only built in `manualActivation` mode. Always
423
417
  // active so users can drag the handle even while content is mid-scroll.
@@ -456,17 +450,13 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
456
450
  (distance > closeThreshold && velocity > -300);
457
451
 
458
452
  if (shouldClose) {
459
- const generation = closeGeneration.value;
460
- translateY.value = withSpring(screenHeightSV.value, { ...SPRING_CONFIG, velocity });
461
- opacity.value = withTiming(0, { ...MOTION_RECIPES.dismiss, duration: 250 }, (finished) => {
462
- if (finished) runOnJS(finishClose)(generation);
463
- });
453
+ runOnJS(requestPanDismiss)(closeGeneration.value, velocity);
464
454
  } else {
465
455
  translateY.value = withSpring(0, { ...SPRING_CONFIG, velocity });
466
456
  }
467
457
  });
468
458
  // eslint-disable-next-line react-hooks/exhaustive-deps
469
- }, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, finishClose]);
459
+ }, [manualActivation, enablePanDownToClose, enableHandlePanningGesture, detached, requestPanDismiss]);
470
460
 
471
461
  // CRITICAL — the shared values each `useAnimatedStyle` READS (translateY,
472
462
  // opacity, screenHeightSV, keyboardHeight) MUST be listed in its dependency
@@ -698,7 +688,7 @@ export const BottomSheetBase = forwardRef((props: BottomSheetBaseProps, ref: Rea
698
688
  : <View style={[styles.nonScrollableContent, surfaceCorners, { overflow: 'hidden' }]}>{surfaceChildren}</View>;
699
689
 
700
690
  return (
701
- <Shell visible={rendered} onRequestClose={dismiss} keyboardHeight={keyboardHeight}>
691
+ <Shell visible={rendered} onRequestClose={handleBackdropPress} keyboardHeight={keyboardHeight}>
702
692
  {/* Web keyboard and focus: in on open, back on close, Tab kept in
703
693
  the sheet, Escape a USER dismissal — through `onDismissAttempt`,
704
694
  exactly like the backdrop. The sheet had no keyboard path at
@@ -38,6 +38,7 @@ export interface BottomSheetProps {
38
38
  */
39
39
  style?: StyleProp<AnimatedStyle<ViewStyle>>;
40
40
  enableHandlePanningGesture?: boolean;
41
+ /** Veto user Back/backdrop/Escape/pan requests before closing; imperative dismiss bypasses it. */
41
42
  onDismissAttempt?: () => boolean;
42
43
  detached?: boolean; // If true, shows with margins and rounded corners. If false, full width with rounded top only.
43
44
  /**
@@ -1,4 +1,5 @@
1
1
  import { Surface } from '../surface';
2
+ import { Loading } from '../loading';
2
3
  import { createSuggestionList } from './create-suggestion-list';
3
4
 
4
- export const SuggestionList = createSuggestionList({ Surface });
5
+ export const SuggestionList = createSuggestionList({ Surface, Loading });
@@ -1,4 +1,5 @@
1
1
  import { Surface } from '../surface/index.web';
2
+ import { Loading } from '../loading/index.web';
2
3
  import { createSuggestionList } from './create-suggestion-list';
3
4
 
4
- export const SuggestionList = createSuggestionList({ Surface });
5
+ export const SuggestionList = createSuggestionList({ Surface, Loading });