@oxyhq/bloom 0.29.5 → 0.30.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 (199) hide show
  1. package/lib/commonjs/activity-heatmap/ActivityHeatmap.js +304 -0
  2. package/lib/commonjs/activity-heatmap/ActivityHeatmap.js.map +1 -0
  3. package/lib/commonjs/activity-heatmap/bucketByDay.js +51 -0
  4. package/lib/commonjs/activity-heatmap/bucketByDay.js.map +1 -0
  5. package/lib/commonjs/activity-heatmap/index.js +20 -0
  6. package/lib/commonjs/activity-heatmap/index.js.map +1 -0
  7. package/lib/commonjs/activity-heatmap/types.js +6 -0
  8. package/lib/commonjs/activity-heatmap/types.js.map +1 -0
  9. package/lib/commonjs/avatar-group/AvatarGroupBase.js +42 -18
  10. package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
  11. package/lib/commonjs/composition-bar/CompositionBar.js +159 -0
  12. package/lib/commonjs/composition-bar/CompositionBar.js.map +1 -0
  13. package/lib/commonjs/composition-bar/index.js +13 -0
  14. package/lib/commonjs/composition-bar/index.js.map +1 -0
  15. package/lib/commonjs/composition-bar/types.js +6 -0
  16. package/lib/commonjs/composition-bar/types.js.map +1 -0
  17. package/lib/commonjs/dot-grid-meter/DotGridMeter.js +69 -0
  18. package/lib/commonjs/dot-grid-meter/DotGridMeter.js.map +1 -0
  19. package/lib/commonjs/dot-grid-meter/index.js +13 -0
  20. package/lib/commonjs/dot-grid-meter/index.js.map +1 -0
  21. package/lib/commonjs/dot-grid-meter/types.js +6 -0
  22. package/lib/commonjs/dot-grid-meter/types.js.map +1 -0
  23. package/lib/commonjs/index.js +59 -12
  24. package/lib/commonjs/index.js.map +1 -1
  25. package/lib/commonjs/index.web.js +59 -12
  26. package/lib/commonjs/index.web.js.map +1 -1
  27. package/lib/commonjs/profile-card/ProfileCard.js +301 -0
  28. package/lib/commonjs/profile-card/ProfileCard.js.map +1 -0
  29. package/lib/commonjs/profile-card/index.js +13 -0
  30. package/lib/commonjs/profile-card/index.js.map +1 -0
  31. package/lib/commonjs/profile-card/types.js +6 -0
  32. package/lib/commonjs/profile-card/types.js.map +1 -0
  33. package/lib/commonjs/stat-bar/StatBar.js +210 -0
  34. package/lib/commonjs/stat-bar/StatBar.js.map +1 -0
  35. package/lib/commonjs/stat-bar/index.js +13 -0
  36. package/lib/commonjs/stat-bar/index.js.map +1 -0
  37. package/lib/commonjs/stat-bar/types.js +6 -0
  38. package/lib/commonjs/stat-bar/types.js.map +1 -0
  39. package/lib/commonjs/typography/index.js +4 -5
  40. package/lib/commonjs/typography/index.js.map +1 -1
  41. package/lib/commonjs/typography/styled-text.js +12 -19
  42. package/lib/commonjs/typography/styled-text.js.map +1 -1
  43. package/lib/module/activity-heatmap/ActivityHeatmap.js +299 -0
  44. package/lib/module/activity-heatmap/ActivityHeatmap.js.map +1 -0
  45. package/lib/module/activity-heatmap/bucketByDay.js +47 -0
  46. package/lib/module/activity-heatmap/bucketByDay.js.map +1 -0
  47. package/lib/module/activity-heatmap/index.js +5 -0
  48. package/lib/module/activity-heatmap/index.js.map +1 -0
  49. package/lib/module/activity-heatmap/types.js +4 -0
  50. package/lib/module/activity-heatmap/types.js.map +1 -0
  51. package/lib/module/avatar-group/AvatarGroupBase.js +42 -18
  52. package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
  53. package/lib/module/composition-bar/CompositionBar.js +154 -0
  54. package/lib/module/composition-bar/CompositionBar.js.map +1 -0
  55. package/lib/module/composition-bar/index.js +4 -0
  56. package/lib/module/composition-bar/index.js.map +1 -0
  57. package/lib/module/composition-bar/types.js +4 -0
  58. package/lib/module/composition-bar/types.js.map +1 -0
  59. package/lib/module/dot-grid-meter/DotGridMeter.js +64 -0
  60. package/lib/module/dot-grid-meter/DotGridMeter.js.map +1 -0
  61. package/lib/module/dot-grid-meter/index.js +4 -0
  62. package/lib/module/dot-grid-meter/index.js.map +1 -0
  63. package/lib/module/dot-grid-meter/types.js +4 -0
  64. package/lib/module/dot-grid-meter/types.js.map +1 -0
  65. package/lib/module/index.js +6 -0
  66. package/lib/module/index.js.map +1 -1
  67. package/lib/module/index.web.js +6 -0
  68. package/lib/module/index.web.js.map +1 -1
  69. package/lib/module/profile-card/ProfileCard.js +296 -0
  70. package/lib/module/profile-card/ProfileCard.js.map +1 -0
  71. package/lib/module/profile-card/index.js +4 -0
  72. package/lib/module/profile-card/index.js.map +1 -0
  73. package/lib/module/profile-card/types.js +4 -0
  74. package/lib/module/profile-card/types.js.map +1 -0
  75. package/lib/module/stat-bar/StatBar.js +205 -0
  76. package/lib/module/stat-bar/StatBar.js.map +1 -0
  77. package/lib/module/stat-bar/index.js +4 -0
  78. package/lib/module/stat-bar/index.js.map +1 -0
  79. package/lib/module/stat-bar/types.js +4 -0
  80. package/lib/module/stat-bar/types.js.map +1 -0
  81. package/lib/module/typography/index.js +5 -6
  82. package/lib/module/typography/index.js.map +1 -1
  83. package/lib/module/typography/styled-text.js +12 -19
  84. package/lib/module/typography/styled-text.js.map +1 -1
  85. package/lib/typescript/commonjs/activity-heatmap/ActivityHeatmap.d.ts +4 -0
  86. package/lib/typescript/commonjs/activity-heatmap/ActivityHeatmap.d.ts.map +1 -0
  87. package/lib/typescript/commonjs/activity-heatmap/bucketByDay.d.ts +9 -0
  88. package/lib/typescript/commonjs/activity-heatmap/bucketByDay.d.ts.map +1 -0
  89. package/lib/typescript/commonjs/activity-heatmap/index.d.ts +4 -0
  90. package/lib/typescript/commonjs/activity-heatmap/index.d.ts.map +1 -0
  91. package/lib/typescript/commonjs/activity-heatmap/types.d.ts +73 -0
  92. package/lib/typescript/commonjs/activity-heatmap/types.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/avatar-group/types.d.ts +15 -1
  95. package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts +4 -0
  97. package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/composition-bar/index.d.ts +3 -0
  99. package/lib/typescript/commonjs/composition-bar/index.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/composition-bar/types.d.ts +29 -0
  101. package/lib/typescript/commonjs/composition-bar/types.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/dot-grid-meter/DotGridMeter.d.ts +4 -0
  103. package/lib/typescript/commonjs/dot-grid-meter/DotGridMeter.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/dot-grid-meter/index.d.ts +3 -0
  105. package/lib/typescript/commonjs/dot-grid-meter/index.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/dot-grid-meter/types.d.ts +36 -0
  107. package/lib/typescript/commonjs/dot-grid-meter/types.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/index.d.ts +10 -0
  109. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/index.web.d.ts +10 -0
  111. package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts +4 -0
  113. package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/profile-card/index.d.ts +3 -0
  115. package/lib/typescript/commonjs/profile-card/index.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/profile-card/types.d.ts +86 -0
  117. package/lib/typescript/commonjs/profile-card/types.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/stat-bar/StatBar.d.ts +4 -0
  119. package/lib/typescript/commonjs/stat-bar/StatBar.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/stat-bar/index.d.ts +3 -0
  121. package/lib/typescript/commonjs/stat-bar/index.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/stat-bar/types.d.ts +62 -0
  123. package/lib/typescript/commonjs/stat-bar/types.d.ts.map +1 -0
  124. package/lib/typescript/commonjs/typography/index.d.ts.map +1 -1
  125. package/lib/typescript/commonjs/typography/styled-text.d.ts +1 -6
  126. package/lib/typescript/commonjs/typography/styled-text.d.ts.map +1 -1
  127. package/lib/typescript/module/activity-heatmap/ActivityHeatmap.d.ts +4 -0
  128. package/lib/typescript/module/activity-heatmap/ActivityHeatmap.d.ts.map +1 -0
  129. package/lib/typescript/module/activity-heatmap/bucketByDay.d.ts +9 -0
  130. package/lib/typescript/module/activity-heatmap/bucketByDay.d.ts.map +1 -0
  131. package/lib/typescript/module/activity-heatmap/index.d.ts +4 -0
  132. package/lib/typescript/module/activity-heatmap/index.d.ts.map +1 -0
  133. package/lib/typescript/module/activity-heatmap/types.d.ts +73 -0
  134. package/lib/typescript/module/activity-heatmap/types.d.ts.map +1 -0
  135. package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  136. package/lib/typescript/module/avatar-group/types.d.ts +15 -1
  137. package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
  138. package/lib/typescript/module/composition-bar/CompositionBar.d.ts +4 -0
  139. package/lib/typescript/module/composition-bar/CompositionBar.d.ts.map +1 -0
  140. package/lib/typescript/module/composition-bar/index.d.ts +3 -0
  141. package/lib/typescript/module/composition-bar/index.d.ts.map +1 -0
  142. package/lib/typescript/module/composition-bar/types.d.ts +29 -0
  143. package/lib/typescript/module/composition-bar/types.d.ts.map +1 -0
  144. package/lib/typescript/module/dot-grid-meter/DotGridMeter.d.ts +4 -0
  145. package/lib/typescript/module/dot-grid-meter/DotGridMeter.d.ts.map +1 -0
  146. package/lib/typescript/module/dot-grid-meter/index.d.ts +3 -0
  147. package/lib/typescript/module/dot-grid-meter/index.d.ts.map +1 -0
  148. package/lib/typescript/module/dot-grid-meter/types.d.ts +36 -0
  149. package/lib/typescript/module/dot-grid-meter/types.d.ts.map +1 -0
  150. package/lib/typescript/module/index.d.ts +10 -0
  151. package/lib/typescript/module/index.d.ts.map +1 -1
  152. package/lib/typescript/module/index.web.d.ts +10 -0
  153. package/lib/typescript/module/index.web.d.ts.map +1 -1
  154. package/lib/typescript/module/profile-card/ProfileCard.d.ts +4 -0
  155. package/lib/typescript/module/profile-card/ProfileCard.d.ts.map +1 -0
  156. package/lib/typescript/module/profile-card/index.d.ts +3 -0
  157. package/lib/typescript/module/profile-card/index.d.ts.map +1 -0
  158. package/lib/typescript/module/profile-card/types.d.ts +86 -0
  159. package/lib/typescript/module/profile-card/types.d.ts.map +1 -0
  160. package/lib/typescript/module/stat-bar/StatBar.d.ts +4 -0
  161. package/lib/typescript/module/stat-bar/StatBar.d.ts.map +1 -0
  162. package/lib/typescript/module/stat-bar/index.d.ts +3 -0
  163. package/lib/typescript/module/stat-bar/index.d.ts.map +1 -0
  164. package/lib/typescript/module/stat-bar/types.d.ts +62 -0
  165. package/lib/typescript/module/stat-bar/types.d.ts.map +1 -0
  166. package/lib/typescript/module/typography/index.d.ts.map +1 -1
  167. package/lib/typescript/module/typography/styled-text.d.ts +1 -6
  168. package/lib/typescript/module/typography/styled-text.d.ts.map +1 -1
  169. package/package.json +56 -1
  170. package/src/__tests__/StatWidgets.test.tsx +184 -0
  171. package/src/__tests__/public-api-contract.test.ts +15 -0
  172. package/src/activity-heatmap/ActivityHeatmap.stories.tsx +108 -0
  173. package/src/activity-heatmap/ActivityHeatmap.tsx +289 -0
  174. package/src/activity-heatmap/bucketByDay.ts +49 -0
  175. package/src/activity-heatmap/index.ts +3 -0
  176. package/src/activity-heatmap/types.ts +74 -0
  177. package/src/avatar-group/AvatarGroup.stories.tsx +14 -0
  178. package/src/avatar-group/AvatarGroupBase.tsx +39 -18
  179. package/src/avatar-group/types.ts +15 -1
  180. package/src/composition-bar/CompositionBar.stories.tsx +70 -0
  181. package/src/composition-bar/CompositionBar.tsx +161 -0
  182. package/src/composition-bar/index.ts +2 -0
  183. package/src/composition-bar/types.ts +30 -0
  184. package/src/dot-grid-meter/DotGridMeter.stories.tsx +47 -0
  185. package/src/dot-grid-meter/DotGridMeter.tsx +71 -0
  186. package/src/dot-grid-meter/index.ts +2 -0
  187. package/src/dot-grid-meter/types.ts +36 -0
  188. package/src/index.ts +24 -0
  189. package/src/index.web.ts +24 -0
  190. package/src/profile-card/ProfileCard.stories.tsx +210 -0
  191. package/src/profile-card/ProfileCard.tsx +303 -0
  192. package/src/profile-card/index.ts +9 -0
  193. package/src/profile-card/types.ts +86 -0
  194. package/src/stat-bar/StatBar.stories.tsx +61 -0
  195. package/src/stat-bar/StatBar.tsx +156 -0
  196. package/src/stat-bar/index.ts +7 -0
  197. package/src/stat-bar/types.ts +65 -0
  198. package/src/typography/index.tsx +4 -6
  199. package/src/typography/styled-text.ts +11 -28
@@ -0,0 +1,71 @@
1
+ import React, { memo, useMemo } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { useTheme } from '../theme/use-theme';
5
+ import type { DotGridMeterProps } from './types';
6
+
7
+ /**
8
+ * A data-driven grid of small circular dots — the "diversity meter" visual. The
9
+ * first `filled` dots use `filledColor`, the rest use `emptyColor`, laid out in
10
+ * a flex-wrapped grid of `columns` per row. Pure `<View>` composition (no SVG),
11
+ * so it renders identically on web and native.
12
+ */
13
+ const DotGridMeterComponent: React.FC<DotGridMeterProps> = ({
14
+ filled,
15
+ total,
16
+ columns = 10,
17
+ dotSize = 10,
18
+ gap = 6,
19
+ filledColor,
20
+ emptyColor,
21
+ style,
22
+ testID,
23
+ }) => {
24
+ const { colors } = useTheme();
25
+ const onColor = filledColor ?? colors.success;
26
+ const offColor = emptyColor ?? colors.backgroundSecondary;
27
+
28
+ const safeTotal = Math.max(0, Math.floor(total));
29
+ const safeFilled = Math.min(safeTotal, Math.max(0, Math.floor(filled)));
30
+ // A row can hold at most `columns` dots; a grid narrower than one full row
31
+ // should still wrap tightly, so the container width follows the smaller count.
32
+ const perRow = Math.max(1, Math.floor(columns));
33
+ const rowWidth = Math.min(safeTotal, perRow);
34
+
35
+ const dots = useMemo(
36
+ () => Array.from({ length: safeTotal }, (_, index) => index < safeFilled),
37
+ [safeTotal, safeFilled],
38
+ );
39
+
40
+ return (
41
+ <View
42
+ style={[
43
+ {
44
+ flexDirection: 'row',
45
+ flexWrap: 'wrap',
46
+ gap,
47
+ width: rowWidth * dotSize + Math.max(0, rowWidth - 1) * gap,
48
+ },
49
+ style,
50
+ ]}
51
+ accessibilityRole="progressbar"
52
+ accessibilityValue={{ min: 0, max: safeTotal, now: safeFilled }}
53
+ testID={testID}
54
+ >
55
+ {dots.map((isFilled, index) => (
56
+ <View
57
+ key={index}
58
+ style={{
59
+ width: dotSize,
60
+ height: dotSize,
61
+ borderRadius: dotSize / 2,
62
+ backgroundColor: isFilled ? onColor : offColor,
63
+ }}
64
+ />
65
+ ))}
66
+ </View>
67
+ );
68
+ };
69
+
70
+ export const DotGridMeter = memo(DotGridMeterComponent);
71
+ DotGridMeter.displayName = 'DotGridMeter';
@@ -0,0 +1,2 @@
1
+ export { DotGridMeter } from './DotGridMeter';
2
+ export type { DotGridMeterProps } from './types';
@@ -0,0 +1,36 @@
1
+ import type { StyleProp, ViewStyle } from 'react-native';
2
+
3
+ export interface DotGridMeterProps {
4
+ /** Number of filled dots (clamped to `[0, total]`). */
5
+ filled: number;
6
+ /** Total number of dots in the grid. */
7
+ total: number;
8
+ /**
9
+ * Dots per row before wrapping.
10
+ * @default 10
11
+ */
12
+ columns?: number;
13
+ /**
14
+ * Diameter of each dot in px.
15
+ * @default 10
16
+ */
17
+ dotSize?: number;
18
+ /**
19
+ * Gap between dots in px (both axes).
20
+ * @default 6
21
+ */
22
+ gap?: number;
23
+ /**
24
+ * Color of a filled dot.
25
+ * @default theme.colors.success
26
+ */
27
+ filledColor?: string;
28
+ /**
29
+ * Color of an empty dot.
30
+ * @default theme.colors.backgroundSecondary
31
+ */
32
+ emptyColor?: string;
33
+ /** Container style override. */
34
+ style?: StyleProp<ViewStyle>;
35
+ testID?: string;
36
+ }
package/src/index.ts CHANGED
@@ -152,6 +152,30 @@ export * from './accordion';
152
152
  export { LinkPreviewCard } from './link-preview';
153
153
  export type { LinkPreviewCardProps } from './link-preview';
154
154
 
155
+ // Stat / profile widgets
156
+ export { CompositionBar } from './composition-bar';
157
+ export type { CompositionBarProps, CompositionCategory } from './composition-bar';
158
+ export { DotGridMeter } from './dot-grid-meter';
159
+ export type { DotGridMeterProps } from './dot-grid-meter';
160
+ export { StatBar } from './stat-bar';
161
+ export type {
162
+ StatBarProps,
163
+ StatBarProgressProps,
164
+ StatBarSplitProps,
165
+ StatBarVariant,
166
+ } from './stat-bar';
167
+ export { ActivityHeatmap, bucketByDay } from './activity-heatmap';
168
+ export type { ActivityHeatmapProps, ActivityHeatmapDay } from './activity-heatmap';
169
+ export { ProfileCard } from './profile-card';
170
+ export type {
171
+ ProfileCardProps,
172
+ ProfileCardLayout,
173
+ ProfileCardVariant,
174
+ ProfileCardAvatar,
175
+ ProfileCardMetric,
176
+ ProfileCardFooter,
177
+ } from './profile-card';
178
+
155
179
  // Settings / Grouped list
156
180
  export * from './settings-list';
157
181
  export { Item } from './item';
package/src/index.web.ts CHANGED
@@ -157,6 +157,30 @@ export * from './accordion';
157
157
  export { LinkPreviewCard } from './link-preview';
158
158
  export type { LinkPreviewCardProps } from './link-preview';
159
159
 
160
+ // Stat / profile widgets
161
+ export { CompositionBar } from './composition-bar';
162
+ export type { CompositionBarProps, CompositionCategory } from './composition-bar';
163
+ export { DotGridMeter } from './dot-grid-meter';
164
+ export type { DotGridMeterProps } from './dot-grid-meter';
165
+ export { StatBar } from './stat-bar';
166
+ export type {
167
+ StatBarProps,
168
+ StatBarProgressProps,
169
+ StatBarSplitProps,
170
+ StatBarVariant,
171
+ } from './stat-bar';
172
+ export { ActivityHeatmap, bucketByDay } from './activity-heatmap';
173
+ export type { ActivityHeatmapProps, ActivityHeatmapDay } from './activity-heatmap';
174
+ export { ProfileCard } from './profile-card';
175
+ export type {
176
+ ProfileCardProps,
177
+ ProfileCardLayout,
178
+ ProfileCardVariant,
179
+ ProfileCardAvatar,
180
+ ProfileCardMetric,
181
+ ProfileCardFooter,
182
+ } from './profile-card';
183
+
160
184
  // Settings / Grouped list
161
185
  export * from './settings-list';
162
186
  export { Item } from './item';
@@ -0,0 +1,210 @@
1
+ import React from 'react';
2
+ import { View } from 'react-native';
3
+ import type { Meta, StoryObj } from '@storybook/react-vite';
4
+
5
+ import { ProfileCard } from './index';
6
+ import type { AvatarGroupItem } from '../avatar-group';
7
+ import { BloomThemeProvider } from '../theme';
8
+ import { Star_Filled_Corner0_Rounded as StarIcon } from '../icons/Star';
9
+
10
+ /** Small colored square as a data-URI so stories are self-contained. */
11
+ function token(color: string): string {
12
+ return `data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'><rect width='40' height='40' rx='20' fill='${color}'/></svg>`;
13
+ }
14
+
15
+ const TOP_TOKENS: AvatarGroupItem[] = [
16
+ { id: 'a', uri: token('gold') },
17
+ { id: 'b', uri: token('tomato') },
18
+ { id: 'c', uri: token('mediumseagreen') },
19
+ { id: 'd', uri: token('royalblue') },
20
+ { id: 'e', uri: token('orchid') },
21
+ ];
22
+
23
+ const HERO = token('slateblue');
24
+
25
+ function Badge() {
26
+ return (
27
+ <View
28
+ style={{
29
+ width: 18,
30
+ height: 18,
31
+ borderRadius: 9,
32
+ backgroundColor: 'gold',
33
+ borderWidth: 2,
34
+ borderColor: '#111',
35
+ }}
36
+ />
37
+ );
38
+ }
39
+
40
+ const trophy = <StarIcon size="sm" style={{ color: '#F5B301' }} />;
41
+
42
+ function Gallery() {
43
+ return (
44
+ <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 20, maxWidth: 780 }}>
45
+ {/* wallet — dot-grid meter */}
46
+ <ProfileCard
47
+ variant="wallet"
48
+ avatar={{ source: HERO, badge: <Badge />, name: 'Wallet' }}
49
+ value="$167,395"
50
+ subtitle="*5bF5"
51
+ headlineIcon={trophy}
52
+ metric={{ kind: 'dots', label: 'Token diversity', filled: 34, total: 50 }}
53
+ footer={{ label: 'Top tokens', items: TOP_TOKENS }}
54
+ />
55
+
56
+ {/* wallet — progress + trophy */}
57
+ <ProfileCard
58
+ variant="wallet"
59
+ avatar={{ source: HERO, badge: <Badge />, name: 'Wallet' }}
60
+ value="$167,395"
61
+ subtitle="*5bF5"
62
+ metric={{
63
+ kind: 'progress',
64
+ label: 'TX count 24h',
65
+ value: 192,
66
+ max: 350,
67
+ minLabel: '32',
68
+ maxLabel: '350',
69
+ icon: trophy,
70
+ }}
71
+ footer={{ label: 'Top tokens', items: TOP_TOKENS }}
72
+ />
73
+
74
+ {/* wallet — split bar % */}
75
+ <ProfileCard
76
+ variant="wallet"
77
+ avatar={{ source: HERO, badge: <Badge />, name: 'Wallet' }}
78
+ value="$167,395"
79
+ subtitle="*5bF5"
80
+ metric={{
81
+ kind: 'split',
82
+ label: 'Net flow 24h',
83
+ percent: 62,
84
+ leftValue: '+$4,210',
85
+ rightValue: '-$2,560',
86
+ }}
87
+ footer={{ label: 'Top tokens', items: TOP_TOKENS }}
88
+ />
89
+
90
+ {/* social */}
91
+ <ProfileCard
92
+ variant="social"
93
+ avatar={{ source: HERO, badge: <Badge />, name: 'Ada' }}
94
+ value="12,480"
95
+ subtitle="@ada"
96
+ metric={{ kind: 'progress', label: 'Weekly goal', value: 5, max: 7 }}
97
+ footer={{ label: 'Mutuals', items: TOP_TOKENS }}
98
+ />
99
+
100
+ {/* shopping */}
101
+ <ProfileCard
102
+ variant="shopping"
103
+ avatar={{ source: HERO, name: 'Store' }}
104
+ value="48 orders"
105
+ subtitle="This month"
106
+ metric={{ kind: 'dots', label: 'Categories', filled: 6, total: 12 }}
107
+ footer={{ label: 'Recent buyers', items: TOP_TOKENS }}
108
+ />
109
+
110
+ {/* stat (default) */}
111
+ <ProfileCard
112
+ variant="stat"
113
+ avatar={{ source: HERO, name: 'Score' }}
114
+ value="820"
115
+ subtitle="Trust score"
116
+ headlineIcon={trophy}
117
+ metric={{
118
+ kind: 'split',
119
+ label: 'Positive vs negative',
120
+ percent: 88,
121
+ leftValue: '880',
122
+ rightValue: '60',
123
+ }}
124
+ />
125
+ </View>
126
+ );
127
+ }
128
+
129
+ function WideGallery() {
130
+ return (
131
+ <View style={{ gap: 16, width: 420 }}>
132
+ <ProfileCard
133
+ layout="wide"
134
+ variant="wallet"
135
+ avatar={{ source: HERO, badge: <Badge />, name: 'Wallet' }}
136
+ value="$167,395"
137
+ subtitle="*5bF5"
138
+ headlineIcon={trophy}
139
+ metric={{ kind: 'dots', label: 'Token diversity', filled: 34, total: 50 }}
140
+ footer={{ label: 'Top tokens', items: TOP_TOKENS }}
141
+ />
142
+ <ProfileCard
143
+ layout="wide"
144
+ variant="social"
145
+ avatar={{ source: HERO, badge: <Badge />, name: 'Ada' }}
146
+ value="12,480"
147
+ subtitle="@ada"
148
+ metric={{
149
+ kind: 'progress',
150
+ label: 'TX count 24h',
151
+ value: 192,
152
+ max: 350,
153
+ minLabel: '32',
154
+ maxLabel: '350',
155
+ icon: trophy,
156
+ }}
157
+ footer={{ label: 'Mutuals', items: TOP_TOKENS }}
158
+ />
159
+ </View>
160
+ );
161
+ }
162
+
163
+ const meta: Meta<typeof ProfileCard> = {
164
+ title: 'Data Display/ProfileCard',
165
+ component: ProfileCard,
166
+ };
167
+
168
+ export default meta;
169
+
170
+ type Story = StoryObj<typeof ProfileCard>;
171
+
172
+ export const WidgetLight: Story = {
173
+ render: () => (
174
+ <BloomThemeProvider mode="light">
175
+ <View style={{ padding: 24 }}>
176
+ <Gallery />
177
+ </View>
178
+ </BloomThemeProvider>
179
+ ),
180
+ };
181
+
182
+ export const WidgetDark: Story = {
183
+ render: () => (
184
+ <BloomThemeProvider mode="dark">
185
+ <View style={{ padding: 24, backgroundColor: '#000' }}>
186
+ <Gallery />
187
+ </View>
188
+ </BloomThemeProvider>
189
+ ),
190
+ };
191
+
192
+ export const WideLight: Story = {
193
+ render: () => (
194
+ <BloomThemeProvider mode="light">
195
+ <View style={{ padding: 24 }}>
196
+ <WideGallery />
197
+ </View>
198
+ </BloomThemeProvider>
199
+ ),
200
+ };
201
+
202
+ export const WideDark: Story = {
203
+ render: () => (
204
+ <BloomThemeProvider mode="dark">
205
+ <View style={{ padding: 24, backgroundColor: '#000' }}>
206
+ <WideGallery />
207
+ </View>
208
+ </BloomThemeProvider>
209
+ ),
210
+ };
@@ -0,0 +1,303 @@
1
+ import React, { memo } from 'react';
2
+ import { View, StyleSheet, type ViewStyle } from 'react-native';
3
+
4
+ import { Card } from '../card';
5
+ import { Avatar } from '../avatar';
6
+ import { AvatarGroup } from '../avatar-group';
7
+ import { DotGridMeter } from '../dot-grid-meter';
8
+ import { StatBar } from '../stat-bar';
9
+ import { Text } from '../typography';
10
+ import { useTheme } from '../theme/use-theme';
11
+ import type { ThemeColors } from '../theme/types';
12
+ import type { AvatarRingConfig } from '../avatar/types';
13
+ import type {
14
+ ProfileCardMetric,
15
+ ProfileCardProps,
16
+ ProfileCardVariant,
17
+ } from './types';
18
+
19
+ const WIDGET_WIDTH = 240;
20
+ const CARD_PADDING = 16;
21
+ const CARD_RADIUS = 24;
22
+ const AVATAR_SIZE = 52;
23
+ const FOOTER_AVATAR_SIZE = 28;
24
+
25
+ interface VariantAccents {
26
+ ring: string;
27
+ fill: string;
28
+ dots: string;
29
+ }
30
+
31
+ function variantAccents(variant: ProfileCardVariant, colors: ThemeColors): VariantAccents {
32
+ switch (variant) {
33
+ case 'wallet':
34
+ return { ring: colors.success, fill: colors.primary, dots: colors.success };
35
+ case 'shopping':
36
+ return { ring: colors.info, fill: colors.info, dots: colors.info };
37
+ case 'social':
38
+ return { ring: colors.primary, fill: colors.primary, dots: colors.primary };
39
+ case 'stat':
40
+ default:
41
+ return { ring: colors.primary, fill: colors.primary, dots: colors.primary };
42
+ }
43
+ }
44
+
45
+ function AvatarWithBadge({
46
+ source,
47
+ name,
48
+ ring,
49
+ badge,
50
+ }: {
51
+ source?: string | null;
52
+ name?: string;
53
+ ring: AvatarRingConfig;
54
+ badge?: React.ReactNode;
55
+ }) {
56
+ return (
57
+ <View style={styles.avatarWrap}>
58
+ <Avatar source={source ?? undefined} name={name} size={AVATAR_SIZE} ring={ring} />
59
+ {badge != null && (
60
+ <View
61
+ style={[
62
+ styles.badge,
63
+ { width: Math.round(AVATAR_SIZE * 0.34), height: Math.round(AVATAR_SIZE * 0.34) },
64
+ ]}
65
+ >
66
+ {badge}
67
+ </View>
68
+ )}
69
+ </View>
70
+ );
71
+ }
72
+
73
+ function MetricSection({
74
+ metric,
75
+ accents,
76
+ labelColor,
77
+ }: {
78
+ metric: ProfileCardMetric;
79
+ accents: VariantAccents;
80
+ labelColor: string;
81
+ }) {
82
+ switch (metric.kind) {
83
+ case 'dots':
84
+ return (
85
+ <View style={styles.section}>
86
+ {metric.label != null && (
87
+ <Text style={[styles.sectionLabel, { color: labelColor }]} numberOfLines={1}>
88
+ {metric.label}
89
+ </Text>
90
+ )}
91
+ <DotGridMeter
92
+ filled={metric.filled}
93
+ total={metric.total}
94
+ filledColor={metric.filledColor ?? accents.dots}
95
+ />
96
+ </View>
97
+ );
98
+ case 'progress':
99
+ return (
100
+ <StatBar
101
+ variant="progress"
102
+ label={metric.label}
103
+ value={metric.value}
104
+ max={metric.max}
105
+ minLabel={metric.minLabel}
106
+ maxLabel={metric.maxLabel}
107
+ icon={metric.icon}
108
+ fillColor={metric.fillColor ?? accents.fill}
109
+ />
110
+ );
111
+ case 'split':
112
+ return (
113
+ <StatBar
114
+ variant="split"
115
+ label={metric.label}
116
+ percent={metric.percent}
117
+ leftValue={metric.leftValue}
118
+ rightValue={metric.rightValue}
119
+ fillColor={metric.fillColor ?? accents.fill}
120
+ />
121
+ );
122
+ case 'custom':
123
+ default:
124
+ return <>{metric.node}</>;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * A composed profile "widget" card — the Apple-Watch-style stat card. Fully
130
+ * app-agnostic: it takes pre-formatted strings, colors, and `ReactNode` icons
131
+ * (no domain/currency logic). Built on {@link Card}, {@link Avatar} (ring +
132
+ * badge), {@link DotGridMeter}/{@link StatBar}, and a {@link AvatarGroup} row.
133
+ *
134
+ * `variant` only picks sensible accent defaults (ring + metric fill); every
135
+ * accent stays overridable via the individual props.
136
+ */
137
+ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
138
+ layout = 'widget',
139
+ variant = 'stat',
140
+ avatar,
141
+ value,
142
+ subtitle,
143
+ headlineIcon,
144
+ metric,
145
+ footer,
146
+ onPress,
147
+ style,
148
+ testID,
149
+ }) => {
150
+ const { colors } = useTheme();
151
+ const accents = variantAccents(variant, colors);
152
+ const isWide = layout === 'wide';
153
+
154
+ const ring: AvatarRingConfig = avatar.ring ?? { colors: accents.ring, width: 2, gap: 2 };
155
+
156
+ const surfaceStyle: ViewStyle = {
157
+ width: isWide ? '100%' : WIDGET_WIDTH,
158
+ padding: CARD_PADDING,
159
+ borderRadius: CARD_RADIUS,
160
+ borderCurve: 'continuous',
161
+ };
162
+
163
+ const headline = (
164
+ <>
165
+ <Text style={[styles.value, { color: colors.text }]} numberOfLines={1}>
166
+ {value}
167
+ </Text>
168
+ {subtitle != null && (
169
+ <Text style={[styles.subtitle, { color: colors.textTertiary }]} numberOfLines={1}>
170
+ {subtitle}
171
+ </Text>
172
+ )}
173
+ </>
174
+ );
175
+
176
+ const metricNode =
177
+ metric != null ? (
178
+ <MetricSection metric={metric} accents={accents} labelColor={colors.textTertiary} />
179
+ ) : null;
180
+
181
+ const footerNode =
182
+ footer != null ? (
183
+ <View style={styles.section}>
184
+ {footer.label != null && (
185
+ <Text style={[styles.sectionLabel, { color: colors.textTertiary }]} numberOfLines={1}>
186
+ {footer.label}
187
+ </Text>
188
+ )}
189
+ <AvatarGroup
190
+ layout="row"
191
+ items={footer.items}
192
+ size={FOOTER_AVATAR_SIZE}
193
+ max={footer.max ?? 4}
194
+ />
195
+ </View>
196
+ ) : null;
197
+
198
+ return (
199
+ <Card
200
+ variant="filled"
201
+ onPress={onPress}
202
+ style={[surfaceStyle, style]}
203
+ testID={testID}
204
+ >
205
+ <View style={styles.body}>
206
+ {isWide ? (
207
+ <View style={styles.headerRowWide}>
208
+ <AvatarWithBadge
209
+ source={avatar.source}
210
+ name={avatar.name}
211
+ ring={ring}
212
+ badge={avatar.badge}
213
+ />
214
+ <View style={styles.wideValueRow}>
215
+ <View style={styles.headlineColumn}>{headline}</View>
216
+ {headlineIcon != null && <View style={styles.wideIcon}>{headlineIcon}</View>}
217
+ </View>
218
+ </View>
219
+ ) : (
220
+ <View style={styles.headerRow}>
221
+ <AvatarWithBadge
222
+ source={avatar.source}
223
+ name={avatar.name}
224
+ ring={ring}
225
+ badge={avatar.badge}
226
+ />
227
+ <View style={styles.headlineColumn}>{headline}</View>
228
+ </View>
229
+ )}
230
+
231
+ {metricNode}
232
+ {footerNode}
233
+ </View>
234
+
235
+ {!isWide && headlineIcon != null && <View style={styles.floatingIcon}>{headlineIcon}</View>}
236
+ </Card>
237
+ );
238
+ };
239
+
240
+ const styles = StyleSheet.create({
241
+ body: {
242
+ gap: 16,
243
+ },
244
+ headerRow: {
245
+ flexDirection: 'row',
246
+ alignItems: 'center',
247
+ gap: 12,
248
+ },
249
+ headerRowWide: {
250
+ flexDirection: 'row',
251
+ alignItems: 'center',
252
+ gap: 16,
253
+ },
254
+ headlineColumn: {
255
+ flex: 1,
256
+ minWidth: 0,
257
+ },
258
+ wideValueRow: {
259
+ flex: 1,
260
+ flexDirection: 'row',
261
+ alignItems: 'center',
262
+ gap: 8,
263
+ },
264
+ wideIcon: {
265
+ marginLeft: 8,
266
+ },
267
+ value: {
268
+ fontSize: 26,
269
+ fontWeight: '800',
270
+ letterSpacing: -0.5,
271
+ fontVariant: ['tabular-nums'],
272
+ },
273
+ subtitle: {
274
+ marginTop: 2,
275
+ fontSize: 13,
276
+ fontVariant: ['tabular-nums'],
277
+ },
278
+ section: {
279
+ gap: 8,
280
+ },
281
+ sectionLabel: {
282
+ fontSize: 12,
283
+ fontWeight: '500',
284
+ },
285
+ avatarWrap: {
286
+ position: 'relative',
287
+ },
288
+ badge: {
289
+ position: 'absolute',
290
+ right: -2,
291
+ bottom: -2,
292
+ alignItems: 'center',
293
+ justifyContent: 'center',
294
+ },
295
+ floatingIcon: {
296
+ position: 'absolute',
297
+ top: CARD_PADDING,
298
+ right: CARD_PADDING,
299
+ },
300
+ });
301
+
302
+ export const ProfileCard = memo(ProfileCardComponent);
303
+ ProfileCard.displayName = 'ProfileCard';
@@ -0,0 +1,9 @@
1
+ export { ProfileCard } from './ProfileCard';
2
+ export type {
3
+ ProfileCardProps,
4
+ ProfileCardLayout,
5
+ ProfileCardVariant,
6
+ ProfileCardAvatar,
7
+ ProfileCardMetric,
8
+ ProfileCardFooter,
9
+ } from './types';