@oxy.so/bloom 6.1.0 → 6.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 (205) hide show
  1. package/README.md +1 -1
  2. package/docs/adoption-matrix.mdx +3 -2
  3. package/docs/chat-composer.mdx +5 -1
  4. package/docs/cover-header.mdx +56 -0
  5. package/lib/commonjs/ai-profile-card/AiProfileCard.js +186 -263
  6. package/lib/commonjs/ai-profile-card/AiProfileCard.js.map +1 -1
  7. package/lib/commonjs/chat-composer/SuggestionList.js +3 -1
  8. package/lib/commonjs/chat-composer/SuggestionList.js.map +1 -1
  9. package/lib/commonjs/chat-composer/SuggestionList.web.js +3 -1
  10. package/lib/commonjs/chat-composer/SuggestionList.web.js.map +1 -1
  11. package/lib/commonjs/chat-composer/create-suggestion-list.js +64 -10
  12. package/lib/commonjs/chat-composer/create-suggestion-list.js.map +1 -1
  13. package/lib/commonjs/chat-composer/messages.js +7 -0
  14. package/lib/commonjs/chat-composer/messages.js.map +1 -1
  15. package/lib/commonjs/cover-header/CoverHeader.js +83 -0
  16. package/lib/commonjs/cover-header/CoverHeader.js.map +1 -0
  17. package/lib/commonjs/cover-header/CoverImage.js +72 -0
  18. package/lib/commonjs/cover-header/CoverImage.js.map +1 -0
  19. package/lib/commonjs/cover-header/index.js +13 -0
  20. package/lib/commonjs/cover-header/index.js.map +1 -0
  21. package/lib/commonjs/cover-header/types.js +6 -0
  22. package/lib/commonjs/cover-header/types.js.map +1 -0
  23. package/lib/commonjs/index.js +951 -939
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/index.web.js +951 -939
  26. package/lib/commonjs/index.web.js.map +1 -1
  27. package/lib/commonjs/locale/translations/ar.js +7 -0
  28. package/lib/commonjs/locale/translations/ar.js.map +1 -1
  29. package/lib/commonjs/locale/translations/bn.js +7 -0
  30. package/lib/commonjs/locale/translations/bn.js.map +1 -1
  31. package/lib/commonjs/locale/translations/ca.js +7 -0
  32. package/lib/commonjs/locale/translations/ca.js.map +1 -1
  33. package/lib/commonjs/locale/translations/de.js +7 -0
  34. package/lib/commonjs/locale/translations/de.js.map +1 -1
  35. package/lib/commonjs/locale/translations/es.js +7 -0
  36. package/lib/commonjs/locale/translations/es.js.map +1 -1
  37. package/lib/commonjs/locale/translations/fr.js +7 -0
  38. package/lib/commonjs/locale/translations/fr.js.map +1 -1
  39. package/lib/commonjs/locale/translations/hi.js +7 -0
  40. package/lib/commonjs/locale/translations/hi.js.map +1 -1
  41. package/lib/commonjs/locale/translations/id.js +7 -0
  42. package/lib/commonjs/locale/translations/id.js.map +1 -1
  43. package/lib/commonjs/locale/translations/it.js +7 -0
  44. package/lib/commonjs/locale/translations/it.js.map +1 -1
  45. package/lib/commonjs/locale/translations/ja.js +7 -0
  46. package/lib/commonjs/locale/translations/ja.js.map +1 -1
  47. package/lib/commonjs/locale/translations/pt.js +7 -0
  48. package/lib/commonjs/locale/translations/pt.js.map +1 -1
  49. package/lib/commonjs/locale/translations/ru.js +7 -0
  50. package/lib/commonjs/locale/translations/ru.js.map +1 -1
  51. package/lib/commonjs/locale/translations/tr.js +7 -0
  52. package/lib/commonjs/locale/translations/tr.js.map +1 -1
  53. package/lib/commonjs/locale/translations/zh.js +7 -0
  54. package/lib/commonjs/locale/translations/zh.js.map +1 -1
  55. package/lib/module/ai-profile-card/AiProfileCard.js +185 -262
  56. package/lib/module/ai-profile-card/AiProfileCard.js.map +1 -1
  57. package/lib/module/chat-composer/SuggestionList.js +3 -1
  58. package/lib/module/chat-composer/SuggestionList.js.map +1 -1
  59. package/lib/module/chat-composer/SuggestionList.web.js +3 -1
  60. package/lib/module/chat-composer/SuggestionList.web.js.map +1 -1
  61. package/lib/module/chat-composer/create-suggestion-list.js +64 -10
  62. package/lib/module/chat-composer/create-suggestion-list.js.map +1 -1
  63. package/lib/module/chat-composer/messages.js +7 -0
  64. package/lib/module/chat-composer/messages.js.map +1 -1
  65. package/lib/module/cover-header/CoverHeader.js +78 -0
  66. package/lib/module/cover-header/CoverHeader.js.map +1 -0
  67. package/lib/module/cover-header/CoverImage.js +68 -0
  68. package/lib/module/cover-header/CoverImage.js.map +1 -0
  69. package/lib/module/cover-header/index.js +4 -0
  70. package/lib/module/cover-header/index.js.map +1 -0
  71. package/lib/module/cover-header/types.js +4 -0
  72. package/lib/module/cover-header/types.js.map +1 -0
  73. package/lib/module/index.js +1 -0
  74. package/lib/module/index.js.map +1 -1
  75. package/lib/module/index.web.js +1 -0
  76. package/lib/module/index.web.js.map +1 -1
  77. package/lib/module/locale/translations/ar.js +7 -0
  78. package/lib/module/locale/translations/ar.js.map +1 -1
  79. package/lib/module/locale/translations/bn.js +7 -0
  80. package/lib/module/locale/translations/bn.js.map +1 -1
  81. package/lib/module/locale/translations/ca.js +7 -0
  82. package/lib/module/locale/translations/ca.js.map +1 -1
  83. package/lib/module/locale/translations/de.js +7 -0
  84. package/lib/module/locale/translations/de.js.map +1 -1
  85. package/lib/module/locale/translations/es.js +7 -0
  86. package/lib/module/locale/translations/es.js.map +1 -1
  87. package/lib/module/locale/translations/fr.js +7 -0
  88. package/lib/module/locale/translations/fr.js.map +1 -1
  89. package/lib/module/locale/translations/hi.js +7 -0
  90. package/lib/module/locale/translations/hi.js.map +1 -1
  91. package/lib/module/locale/translations/id.js +7 -0
  92. package/lib/module/locale/translations/id.js.map +1 -1
  93. package/lib/module/locale/translations/it.js +7 -0
  94. package/lib/module/locale/translations/it.js.map +1 -1
  95. package/lib/module/locale/translations/ja.js +7 -0
  96. package/lib/module/locale/translations/ja.js.map +1 -1
  97. package/lib/module/locale/translations/pt.js +7 -0
  98. package/lib/module/locale/translations/pt.js.map +1 -1
  99. package/lib/module/locale/translations/ru.js +7 -0
  100. package/lib/module/locale/translations/ru.js.map +1 -1
  101. package/lib/module/locale/translations/tr.js +7 -0
  102. package/lib/module/locale/translations/tr.js.map +1 -1
  103. package/lib/module/locale/translations/zh.js +7 -0
  104. package/lib/module/locale/translations/zh.js.map +1 -1
  105. package/lib/typescript/commonjs/ai-profile-card/AiProfileCard.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/chat-composer/SuggestionList.d.ts +1 -1
  107. package/lib/typescript/commonjs/chat-composer/SuggestionList.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/chat-composer/SuggestionList.web.d.ts +1 -1
  109. package/lib/typescript/commonjs/chat-composer/SuggestionList.web.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/chat-composer/create-suggestion-list.d.ts +4 -2
  111. package/lib/typescript/commonjs/chat-composer/create-suggestion-list.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/chat-composer/messages.d.ts +6 -0
  113. package/lib/typescript/commonjs/chat-composer/messages.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/chat-composer/types.d.ts +17 -0
  115. package/lib/typescript/commonjs/chat-composer/types.d.ts.map +1 -1
  116. package/lib/typescript/commonjs/cover-header/CoverHeader.d.ts +21 -0
  117. package/lib/typescript/commonjs/cover-header/CoverHeader.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/cover-header/CoverImage.d.ts +11 -0
  119. package/lib/typescript/commonjs/cover-header/CoverImage.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/cover-header/index.d.ts +3 -0
  121. package/lib/typescript/commonjs/cover-header/index.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/cover-header/types.d.ts +33 -0
  123. package/lib/typescript/commonjs/cover-header/types.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/index.d.ts +1 -0
  125. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  126. package/lib/typescript/commonjs/index.web.d.ts +1 -0
  127. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  128. package/lib/typescript/commonjs/locale/translations/ar.d.ts.map +1 -1
  129. package/lib/typescript/commonjs/locale/translations/bn.d.ts.map +1 -1
  130. package/lib/typescript/commonjs/locale/translations/ca.d.ts.map +1 -1
  131. package/lib/typescript/commonjs/locale/translations/de.d.ts.map +1 -1
  132. package/lib/typescript/commonjs/locale/translations/es.d.ts.map +1 -1
  133. package/lib/typescript/commonjs/locale/translations/fr.d.ts.map +1 -1
  134. package/lib/typescript/commonjs/locale/translations/hi.d.ts.map +1 -1
  135. package/lib/typescript/commonjs/locale/translations/id.d.ts.map +1 -1
  136. package/lib/typescript/commonjs/locale/translations/it.d.ts.map +1 -1
  137. package/lib/typescript/commonjs/locale/translations/ja.d.ts.map +1 -1
  138. package/lib/typescript/commonjs/locale/translations/pt.d.ts.map +1 -1
  139. package/lib/typescript/commonjs/locale/translations/ru.d.ts.map +1 -1
  140. package/lib/typescript/commonjs/locale/translations/tr.d.ts.map +1 -1
  141. package/lib/typescript/commonjs/locale/translations/zh.d.ts.map +1 -1
  142. package/lib/typescript/module/ai-profile-card/AiProfileCard.d.ts.map +1 -1
  143. package/lib/typescript/module/chat-composer/SuggestionList.d.ts +1 -1
  144. package/lib/typescript/module/chat-composer/SuggestionList.d.ts.map +1 -1
  145. package/lib/typescript/module/chat-composer/SuggestionList.web.d.ts +1 -1
  146. package/lib/typescript/module/chat-composer/SuggestionList.web.d.ts.map +1 -1
  147. package/lib/typescript/module/chat-composer/create-suggestion-list.d.ts +4 -2
  148. package/lib/typescript/module/chat-composer/create-suggestion-list.d.ts.map +1 -1
  149. package/lib/typescript/module/chat-composer/messages.d.ts +6 -0
  150. package/lib/typescript/module/chat-composer/messages.d.ts.map +1 -1
  151. package/lib/typescript/module/chat-composer/types.d.ts +17 -0
  152. package/lib/typescript/module/chat-composer/types.d.ts.map +1 -1
  153. package/lib/typescript/module/cover-header/CoverHeader.d.ts +21 -0
  154. package/lib/typescript/module/cover-header/CoverHeader.d.ts.map +1 -0
  155. package/lib/typescript/module/cover-header/CoverImage.d.ts +11 -0
  156. package/lib/typescript/module/cover-header/CoverImage.d.ts.map +1 -0
  157. package/lib/typescript/module/cover-header/index.d.ts +3 -0
  158. package/lib/typescript/module/cover-header/index.d.ts.map +1 -0
  159. package/lib/typescript/module/cover-header/types.d.ts +33 -0
  160. package/lib/typescript/module/cover-header/types.d.ts.map +1 -0
  161. package/lib/typescript/module/index.d.ts +1 -0
  162. package/lib/typescript/module/index.d.ts.map +1 -1
  163. package/lib/typescript/module/index.web.d.ts +1 -0
  164. package/lib/typescript/module/index.web.d.ts.map +1 -1
  165. package/lib/typescript/module/locale/translations/ar.d.ts.map +1 -1
  166. package/lib/typescript/module/locale/translations/bn.d.ts.map +1 -1
  167. package/lib/typescript/module/locale/translations/ca.d.ts.map +1 -1
  168. package/lib/typescript/module/locale/translations/de.d.ts.map +1 -1
  169. package/lib/typescript/module/locale/translations/es.d.ts.map +1 -1
  170. package/lib/typescript/module/locale/translations/fr.d.ts.map +1 -1
  171. package/lib/typescript/module/locale/translations/hi.d.ts.map +1 -1
  172. package/lib/typescript/module/locale/translations/id.d.ts.map +1 -1
  173. package/lib/typescript/module/locale/translations/it.d.ts.map +1 -1
  174. package/lib/typescript/module/locale/translations/ja.d.ts.map +1 -1
  175. package/lib/typescript/module/locale/translations/pt.d.ts.map +1 -1
  176. package/lib/typescript/module/locale/translations/ru.d.ts.map +1 -1
  177. package/lib/typescript/module/locale/translations/tr.d.ts.map +1 -1
  178. package/lib/typescript/module/locale/translations/zh.d.ts.map +1 -1
  179. package/package.json +18 -1
  180. package/src/ai-profile-card/AiProfileCard.tsx +159 -241
  181. package/src/chat-composer/SuggestionList.tsx +2 -1
  182. package/src/chat-composer/SuggestionList.web.tsx +2 -1
  183. package/src/chat-composer/create-suggestion-list.tsx +54 -8
  184. package/src/chat-composer/messages.ts +9 -0
  185. package/src/chat-composer/types.ts +17 -0
  186. package/src/cover-header/CoverHeader.tsx +75 -0
  187. package/src/cover-header/CoverImage.tsx +60 -0
  188. package/src/cover-header/index.ts +3 -0
  189. package/src/cover-header/types.ts +30 -0
  190. package/src/index.ts +1 -0
  191. package/src/index.web.ts +1 -0
  192. package/src/locale/translations/ar.ts +3 -0
  193. package/src/locale/translations/bn.ts +3 -0
  194. package/src/locale/translations/ca.ts +3 -0
  195. package/src/locale/translations/de.ts +3 -0
  196. package/src/locale/translations/es.ts +3 -0
  197. package/src/locale/translations/fr.ts +3 -0
  198. package/src/locale/translations/hi.ts +3 -0
  199. package/src/locale/translations/id.ts +3 -0
  200. package/src/locale/translations/it.ts +3 -0
  201. package/src/locale/translations/ja.ts +3 -0
  202. package/src/locale/translations/pt.ts +3 -0
  203. package/src/locale/translations/ru.ts +3 -0
  204. package/src/locale/translations/tr.ts +3 -0
  205. package/src/locale/translations/zh.ts +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/bloom",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "packageManager": "bun@1.3.14",
5
5
  "description": "Bloom UI — Oxy ecosystem component library for React Native + Expo + Web",
6
6
  "main": "lib/commonjs/index.js",
@@ -713,6 +713,20 @@
713
713
  "default": "./lib/commonjs/page-header/index.js"
714
714
  }
715
715
  },
716
+ "./cover-header": {
717
+ "react-native": {
718
+ "types": "./lib/typescript/module/cover-header/index.d.ts",
719
+ "default": "./src/cover-header/index.ts"
720
+ },
721
+ "import": {
722
+ "types": "./lib/typescript/module/cover-header/index.d.ts",
723
+ "default": "./lib/module/cover-header/index.js"
724
+ },
725
+ "require": {
726
+ "types": "./lib/typescript/commonjs/cover-header/index.d.ts",
727
+ "default": "./lib/commonjs/cover-header/index.js"
728
+ }
729
+ },
716
730
  "./notification-center": {
717
731
  "react-native": {
718
732
  "types": "./lib/typescript/module/notification-center/index.d.ts",
@@ -3689,6 +3703,9 @@
3689
3703
  "page-header": [
3690
3704
  "./lib/typescript/module/page-header/index.d.ts"
3691
3705
  ],
3706
+ "cover-header": [
3707
+ "./lib/typescript/module/cover-header/index.d.ts"
3708
+ ],
3692
3709
  "notification-center": [
3693
3710
  "./lib/typescript/module/notification-center/index.d.ts"
3694
3711
  ],
@@ -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
 
@@ -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 });
@@ -1,6 +1,7 @@
1
1
  import { useSurfaceFill } from '../styles/surface-levels';
2
2
  import { useCardFill } from '../card/use-card-fill';
3
3
  import type { Surface as SurfaceComponent } from '../surface';
4
+ import type { Loading as LoadingComponent } from '../loading';
4
5
  /**
5
6
  * The anchored list above the composer: `@mention`, `/command` and `:shortcode`
6
7
  * are ONE part with a `kind`, because the row geometry, the keyboard model and
@@ -14,6 +15,7 @@ import React from 'react';
14
15
  import { Image, Pressable, ScrollView, View } from 'react-native';
15
16
 
16
17
  import { RiHashtag } from '../icons/remix/RiHashtag';
18
+ import { RiVerifiedBadgeFill } from '../icons/remix/RiVerifiedBadgeFill';
17
19
  import { useImageResolver } from '../image-resolver/context';
18
20
  import { useMessages } from '../locale/messages';
19
21
  import { useTheme } from '../theme/use-theme';
@@ -30,8 +32,9 @@ import type { ChatComposerSuggestion, SuggestionKind, SuggestionListProps } from
30
32
  import { dataHook, IS_WEB } from './web-hooks';
31
33
 
32
34
  /** Platform dependencies are bound once; shared rendering adds no wrapper. */
33
- export function createSuggestionList({ Surface }: {
35
+ export function createSuggestionList({ Surface, Loading }: {
34
36
  Surface: typeof SurfaceComponent;
37
+ Loading: typeof LoadingComponent;
35
38
  }) {
36
39
 
37
40
  function isUrl(value: string): boolean {
@@ -111,11 +114,15 @@ export function createSuggestionList({ Surface }: {
111
114
  style,
112
115
  testID,
113
116
  accessibilityLabel,
117
+ loading = false,
118
+ showEmpty = false,
119
+ emptyLabel,
114
120
  }: SuggestionListProps) {
115
121
  const theme = useTheme();
116
122
  const palette = resolveChatComposerPalette(theme, useCardFill(style));
117
123
  const { messages } = useMessages(CHAT_COMPOSER_MESSAGES);
118
- if (suggestions.length === 0) return null;
124
+ const empty = !loading && suggestions.length === 0;
125
+ if (empty && !showEmpty) return null;
119
126
 
120
127
  // React Native's `Role` union has no `listbox`; react-native-web passes the
121
128
  // DOM `role` straight through, so it travels as a web-only prop.
@@ -144,6 +151,34 @@ export function createSuggestionList({ Surface }: {
144
151
  {header}
145
152
  </Text>
146
153
  ) : null}
154
+ {loading ? (
155
+ <View style={{ minHeight: SUGGESTION_ROW_HEIGHT, justifyContent: 'center', paddingLeft: 8, paddingRight: 8 }}>
156
+ <Loading
157
+ variant="inline"
158
+ size="sm"
159
+ color={palette.textSecondary}
160
+ text={messages.searchingSuggestions}
161
+ textStyle={{ color: palette.textSecondary }}
162
+ accessibilityLabel={messages.searchingSuggestions}
163
+ testID={testID ? `${testID}-loading` : undefined}
164
+ />
165
+ </View>
166
+ ) : empty ? (
167
+ <Text
168
+ variant="body-2-regular"
169
+ accessibilityRole="text"
170
+ style={{
171
+ color: palette.textSecondary,
172
+ minHeight: SUGGESTION_ROW_HEIGHT,
173
+ paddingLeft: 8,
174
+ paddingRight: 8,
175
+ paddingTop: 10,
176
+ paddingBottom: 10,
177
+ }}
178
+ testID={testID ? `${testID}-empty` : undefined}>
179
+ {emptyLabel ?? messages.noSuggestions[kind]}
180
+ </Text>
181
+ ) : (
147
182
  <ScrollView
148
183
  {...dataHook('bloomChatComposerScroll')}
149
184
  {...listRole}
@@ -167,9 +202,12 @@ export function createSuggestionList({ Surface }: {
167
202
  cursor: suggestion.disabled ? 'auto' : 'pointer',
168
203
  '--bloom-chat-composer-ring': palette.focusRing,
169
204
  };
170
- const name = suggestion.handle
171
- ? `${suggestion.label} ${suggestion.handle}`
172
- : suggestion.label;
205
+ const verified = kind === 'mention' && suggestion.verified === true;
206
+ const name = [
207
+ suggestion.label,
208
+ verified ? messages.suggestionVerified : undefined,
209
+ suggestion.handle,
210
+ ].filter(Boolean).join(verified ? ', ' : ' ');
173
211
  return (
174
212
  <Pressable
175
213
  key={suggestion.id}
@@ -186,9 +224,16 @@ export function createSuggestionList({ Surface }: {
186
224
  <Leading kind={kind} suggestion={suggestion} />
187
225
  <View style={{ flexShrink: 1, flexGrow: 1 }}>
188
226
  <View style={{ flexDirection: 'row', alignItems: 'baseline', gap: 6 }}>
189
- <Text variant="body-medium" numberOfLines={1} style={{ color: palette.text }}>
190
- {suggestion.label}
191
- </Text>
227
+ <View style={{ flexDirection: 'row', alignItems: 'center', gap: 3, flexShrink: 1 }}>
228
+ <Text variant="body-medium" numberOfLines={1} style={{ color: palette.text, flexShrink: 1 }}>
229
+ {suggestion.label}
230
+ </Text>
231
+ {verified ? (
232
+ <View testID={testID ? `${testID}-verified-${suggestion.id}` : undefined}>
233
+ <RiVerifiedBadgeFill width={14} height={14} fill={palette.accent} />
234
+ </View>
235
+ ) : null}
236
+ </View>
192
237
  {suggestion.handle ? (
193
238
  <Text
194
239
  variant="body-2-regular"
@@ -211,6 +256,7 @@ export function createSuggestionList({ Surface }: {
211
256
  );
212
257
  })}
213
258
  </ScrollView>
259
+ )}
214
260
  </Surface>
215
261
  );
216
262
  }
@@ -46,6 +46,12 @@ export interface ChatComposerMessages {
46
46
  removeAttachment: (name: string) => string;
47
47
  /** `SuggestionList`'s name per kind. */
48
48
  suggestions: Record<SuggestionKind, string>;
49
+ /** A suggestion row's verified marker, as announced. */
50
+ suggestionVerified: string;
51
+ /** `SuggestionList` while its caller is still searching. */
52
+ searchingSuggestions: string;
53
+ /** `SuggestionList` when a finished search found nothing, per kind. */
54
+ noSuggestions: Record<SuggestionKind, string>;
49
55
  /** `AttachmentMenu`'s default rows. */
50
56
  attachmentItems: Record<AttachmentMenuItemId, string>;
51
57
  }
@@ -77,5 +83,8 @@ export const CHAT_COMPOSER_MESSAGES: MessageCatalog<ChatComposerMessages> = defi
77
83
  attachments: 'Attachments',
78
84
  removeAttachment: (name) => `Remove ${name}`,
79
85
  suggestions: { mention: 'People', command: 'Commands', emoji: 'Emoji' },
86
+ suggestionVerified: 'Verified',
87
+ searchingSuggestions: 'Searching…',
88
+ noSuggestions: { mention: 'No people found', command: 'No commands found', emoji: 'No emoji found' },
80
89
  attachmentItems: { gallery: 'Gallery', camera: 'Camera', file: 'File', location: 'Location', contact: 'Contact', poll: 'Poll', music: 'Music' },
81
90
  });