@oxyhq/bloom 0.30.0 → 0.30.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 (27) hide show
  1. package/lib/commonjs/avatar-group/AvatarGroupBase.js +2 -0
  2. package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
  3. package/lib/commonjs/profile-card/ProfileCard.js +65 -55
  4. package/lib/commonjs/profile-card/ProfileCard.js.map +1 -1
  5. package/lib/commonjs/stat-bar/StatBar.js +4 -1
  6. package/lib/commonjs/stat-bar/StatBar.js.map +1 -1
  7. package/lib/module/avatar-group/AvatarGroupBase.js +2 -0
  8. package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
  9. package/lib/module/profile-card/ProfileCard.js +65 -55
  10. package/lib/module/profile-card/ProfileCard.js.map +1 -1
  11. package/lib/module/stat-bar/StatBar.js +4 -1
  12. package/lib/module/stat-bar/StatBar.js.map +1 -1
  13. package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  14. package/lib/typescript/commonjs/avatar-group/types.d.ts +8 -0
  15. package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
  16. package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/stat-bar/StatBar.d.ts.map +1 -1
  18. package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
  19. package/lib/typescript/module/avatar-group/types.d.ts +8 -0
  20. package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
  21. package/lib/typescript/module/profile-card/ProfileCard.d.ts.map +1 -1
  22. package/lib/typescript/module/stat-bar/StatBar.d.ts.map +1 -1
  23. package/package.json +1 -1
  24. package/src/avatar-group/AvatarGroupBase.tsx +9 -2
  25. package/src/avatar-group/types.ts +8 -0
  26. package/src/profile-card/ProfileCard.tsx +63 -97
  27. package/src/stat-bar/StatBar.tsx +12 -42
@@ -17,28 +17,26 @@ import type {
17
17
  } from './types';
18
18
 
19
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;
20
+ const CARD_PADDING = 18;
21
+ const CARD_RADIUS = 28;
22
+ const AVATAR_SIZE = 54;
23
+ const FOOTER_AVATAR_SIZE = 30;
24
24
 
25
- interface VariantAccents {
26
- ring: string;
27
- fill: string;
28
- dots: string;
29
- }
30
-
31
- function variantAccents(variant: ProfileCardVariant, colors: ThemeColors): VariantAccents {
25
+ /**
26
+ * Vivid per-variant accent, drawn from the theme's semantic tokens so it tracks
27
+ * the active Bloom preset. Every accent stays overridable per-prop.
28
+ */
29
+ function variantAccent(variant: ProfileCardVariant, colors: ThemeColors): string {
32
30
  switch (variant) {
33
31
  case 'wallet':
34
- return { ring: colors.success, fill: colors.primary, dots: colors.success };
32
+ return colors.success;
35
33
  case 'shopping':
36
- return { ring: colors.info, fill: colors.info, dots: colors.info };
34
+ return colors.warning;
37
35
  case 'social':
38
- return { ring: colors.primary, fill: colors.primary, dots: colors.primary };
36
+ return colors.primary;
39
37
  case 'stat':
40
38
  default:
41
- return { ring: colors.primary, fill: colors.primary, dots: colors.primary };
39
+ return colors.info;
42
40
  }
43
41
  }
44
42
 
@@ -47,24 +45,19 @@ function AvatarWithBadge({
47
45
  name,
48
46
  ring,
49
47
  badge,
48
+ haloColor,
50
49
  }: {
51
50
  source?: string | null;
52
51
  name?: string;
53
52
  ring: AvatarRingConfig;
54
53
  badge?: React.ReactNode;
54
+ haloColor: string;
55
55
  }) {
56
56
  return (
57
57
  <View style={styles.avatarWrap}>
58
58
  <Avatar source={source ?? undefined} name={name} size={AVATAR_SIZE} ring={ring} />
59
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>
60
+ <View style={[styles.badgeHalo, { backgroundColor: haloColor }]}>{badge}</View>
68
61
  )}
69
62
  </View>
70
63
  );
@@ -72,11 +65,11 @@ function AvatarWithBadge({
72
65
 
73
66
  function MetricSection({
74
67
  metric,
75
- accents,
68
+ accent,
76
69
  labelColor,
77
70
  }: {
78
71
  metric: ProfileCardMetric;
79
- accents: VariantAccents;
72
+ accent: string;
80
73
  labelColor: string;
81
74
  }) {
82
75
  switch (metric.kind) {
@@ -91,7 +84,10 @@ function MetricSection({
91
84
  <DotGridMeter
92
85
  filled={metric.filled}
93
86
  total={metric.total}
94
- filledColor={metric.filledColor ?? accents.dots}
87
+ columns={metric.total}
88
+ dotSize={9}
89
+ gap={6}
90
+ filledColor={metric.filledColor ?? accent}
95
91
  />
96
92
  </View>
97
93
  );
@@ -105,7 +101,7 @@ function MetricSection({
105
101
  minLabel={metric.minLabel}
106
102
  maxLabel={metric.maxLabel}
107
103
  icon={metric.icon}
108
- fillColor={metric.fillColor ?? accents.fill}
104
+ fillColor={metric.fillColor ?? accent}
109
105
  />
110
106
  );
111
107
  case 'split':
@@ -116,7 +112,7 @@ function MetricSection({
116
112
  percent={metric.percent}
117
113
  leftValue={metric.leftValue}
118
114
  rightValue={metric.rightValue}
119
- fillColor={metric.fillColor ?? accents.fill}
115
+ fillColor={metric.fillColor ?? accent}
120
116
  />
121
117
  );
122
118
  case 'custom':
@@ -129,10 +125,14 @@ function MetricSection({
129
125
  * A composed profile "widget" card — the Apple-Watch-style stat card. Fully
130
126
  * app-agnostic: it takes pre-formatted strings, colors, and `ReactNode` icons
131
127
  * (no domain/currency logic). Built on {@link Card}, {@link Avatar} (ring +
132
- * badge), {@link DotGridMeter}/{@link StatBar}, and a {@link AvatarGroup} row.
128
+ * badge), {@link DotGridMeter}/{@link StatBar}, and an {@link AvatarGroup} row.
133
129
  *
134
- * `variant` only picks sensible accent defaults (ring + metric fill); every
135
- * accent stays overridable via the individual props.
130
+ * Theme-driven (Bloom's `useTheme()` reads the same NativeWind token pipeline
131
+ * that backs the `--*` vars): the surface is the theme's `background` — the
132
+ * darkest token, a near-black in dark mode — with a hairline `border` so it
133
+ * separates on any page, and the metric tracks read from `backgroundSecondary`,
134
+ * a step lighter, so they always show. `variant` picks a vivid semantic accent
135
+ * (success/warning/info/primary), overridable per-prop.
136
136
  */
137
137
  const ProfileCardComponent: React.FC<ProfileCardProps> = ({
138
138
  layout = 'widget',
@@ -148,15 +148,20 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
148
148
  testID,
149
149
  }) => {
150
150
  const { colors } = useTheme();
151
- const accents = variantAccents(variant, colors);
151
+ const accent = variantAccent(variant, colors);
152
152
  const isWide = layout === 'wide';
153
153
 
154
- const ring: AvatarRingConfig = avatar.ring ?? { colors: accents.ring, width: 2, gap: 2 };
154
+ // Consumer ring wins for color; default to the accent, always with a small
155
+ // gap so the ring reads as a distinct halo (as in the reference).
156
+ const ring: AvatarRingConfig = { colors: accent, width: 2, gap: 2, ...avatar.ring };
155
157
 
156
158
  const surfaceStyle: ViewStyle = {
157
159
  width: isWide ? '100%' : WIDGET_WIDTH,
158
160
  padding: CARD_PADDING,
159
161
  borderRadius: CARD_RADIUS,
162
+ borderWidth: 1,
163
+ borderColor: colors.border,
164
+ backgroundColor: colors.background,
160
165
  borderCurve: 'continuous',
161
166
  };
162
167
 
@@ -166,7 +171,7 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
166
171
  {value}
167
172
  </Text>
168
173
  {subtitle != null && (
169
- <Text style={[styles.subtitle, { color: colors.textTertiary }]} numberOfLines={1}>
174
+ <Text style={[styles.subtitle, { color: colors.textSecondary }]} numberOfLines={1}>
170
175
  {subtitle}
171
176
  </Text>
172
177
  )}
@@ -175,14 +180,14 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
175
180
 
176
181
  const metricNode =
177
182
  metric != null ? (
178
- <MetricSection metric={metric} accents={accents} labelColor={colors.textTertiary} />
183
+ <MetricSection metric={metric} accent={accent} labelColor={colors.textSecondary} />
179
184
  ) : null;
180
185
 
181
186
  const footerNode =
182
187
  footer != null ? (
183
188
  <View style={styles.section}>
184
189
  {footer.label != null && (
185
- <Text style={[styles.sectionLabel, { color: colors.textTertiary }]} numberOfLines={1}>
190
+ <Text style={[styles.sectionLabel, { color: colors.textSecondary }]} numberOfLines={1}>
186
191
  {footer.label}
187
192
  </Text>
188
193
  )}
@@ -190,18 +195,15 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
190
195
  layout="row"
191
196
  items={footer.items}
192
197
  size={FOOTER_AVATAR_SIZE}
198
+ spacing={7}
193
199
  max={footer.max ?? 4}
200
+ showInitials
194
201
  />
195
202
  </View>
196
203
  ) : null;
197
204
 
198
205
  return (
199
- <Card
200
- variant="filled"
201
- onPress={onPress}
202
- style={[surfaceStyle, style]}
203
- testID={testID}
204
- >
206
+ <Card variant="filled" onPress={onPress} style={[surfaceStyle, style]} testID={testID}>
205
207
  <View style={styles.body}>
206
208
  {isWide ? (
207
209
  <View style={styles.headerRowWide}>
@@ -210,6 +212,7 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
210
212
  name={avatar.name}
211
213
  ring={ring}
212
214
  badge={avatar.badge}
215
+ haloColor={colors.background}
213
216
  />
214
217
  <View style={styles.wideValueRow}>
215
218
  <View style={styles.headlineColumn}>{headline}</View>
@@ -223,6 +226,7 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
223
226
  name={avatar.name}
224
227
  ring={ring}
225
228
  badge={avatar.badge}
229
+ haloColor={colors.background}
226
230
  />
227
231
  <View style={styles.headlineColumn}>{headline}</View>
228
232
  </View>
@@ -238,65 +242,27 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
238
242
  };
239
243
 
240
244
  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: {
245
+ body: { gap: 16 },
246
+ headerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
247
+ headerRowWide: { flexDirection: 'row', alignItems: 'center', gap: 14 },
248
+ headlineColumn: { flex: 1, minWidth: 0 },
249
+ wideValueRow: { flex: 1, flexDirection: 'row', alignItems: 'center', gap: 8 },
250
+ wideIcon: { marginLeft: 8 },
251
+ value: { fontSize: 27, fontWeight: '800', letterSpacing: -0.6, fontVariant: ['tabular-nums'] },
252
+ subtitle: { marginTop: 2, fontSize: 13, fontWeight: '500', fontVariant: ['tabular-nums'] },
253
+ section: { gap: 10 },
254
+ sectionLabel: { fontSize: 13, fontWeight: '500' },
255
+ avatarWrap: { position: 'relative' },
256
+ badgeHalo: {
289
257
  position: 'absolute',
290
- right: -2,
291
- bottom: -2,
258
+ right: -3,
259
+ bottom: -3,
260
+ padding: 2,
261
+ borderRadius: 999,
292
262
  alignItems: 'center',
293
263
  justifyContent: 'center',
294
264
  },
295
- floatingIcon: {
296
- position: 'absolute',
297
- top: CARD_PADDING,
298
- right: CARD_PADDING,
299
- },
265
+ floatingIcon: { position: 'absolute', top: CARD_PADDING, right: CARD_PADDING },
300
266
  });
301
267
 
302
268
  export const ProfileCard = memo(ProfileCardComponent);
@@ -19,7 +19,10 @@ function clampPercent(value: number): number {
19
19
  * track color), a bold percentage right of the label, and a two-sided footer
20
20
  * of opposing values (inflow/outflow style).
21
21
  *
22
- * Pure `<View>` composition, so it renders identically on web and native.
22
+ * Colors come from the theme (the same NativeWind token pipeline that backs the
23
+ * `--*` vars) — text via `text`/`textSecondary`/`textTertiary`, the track from
24
+ * `backgroundSecondary` — with the fill overridable per-prop. Pure `<View>`
25
+ * composition, so it renders identically on web and native.
23
26
  */
24
27
  const StatBarComponent: React.FC<StatBarProps> = (props) => {
25
28
  const { colors } = useTheme();
@@ -47,12 +50,7 @@ const StatBarComponent: React.FC<StatBarProps> = (props) => {
47
50
 
48
51
  <View style={[styles.track, { height, borderRadius: radius, backgroundColor: remainder }]}>
49
52
  <View
50
- style={{
51
- width: `${leftPercent}%`,
52
- height,
53
- borderRadius: radius,
54
- backgroundColor: activeLeft,
55
- }}
53
+ style={{ width: `${leftPercent}%`, height, borderRadius: radius, backgroundColor: activeLeft }}
56
54
  />
57
55
  </View>
58
56
 
@@ -86,14 +84,7 @@ const StatBarComponent: React.FC<StatBarProps> = (props) => {
86
84
  accessibilityRole="progressbar"
87
85
  accessibilityValue={{ min: 0, max, now: value }}
88
86
  >
89
- <View
90
- style={{
91
- width: `${ratio}%`,
92
- height,
93
- borderRadius: radius,
94
- backgroundColor: fill,
95
- }}
96
- />
87
+ <View style={{ width: `${ratio}%`, height, borderRadius: radius, backgroundColor: fill }} />
97
88
  </View>
98
89
 
99
90
  {hasFooter && (
@@ -111,45 +102,24 @@ const StatBarComponent: React.FC<StatBarProps> = (props) => {
111
102
  };
112
103
 
113
104
  const styles = StyleSheet.create({
114
- wrap: {
115
- gap: 8,
116
- },
105
+ wrap: { gap: 8 },
117
106
  labelRow: {
118
107
  flexDirection: 'row',
119
108
  alignItems: 'center',
120
109
  justifyContent: 'space-between',
121
110
  gap: 8,
122
111
  },
123
- label: {
124
- flexShrink: 1,
125
- fontSize: 13,
126
- fontWeight: '500',
127
- },
128
- percent: {
129
- fontSize: 14,
130
- fontWeight: '700',
131
- letterSpacing: -0.2,
132
- fontVariant: ['tabular-nums'],
133
- },
134
- icon: {
135
- marginLeft: 8,
136
- },
137
- track: {
138
- width: '100%',
139
- overflow: 'hidden',
140
- flexDirection: 'row',
141
- borderCurve: 'continuous',
142
- },
112
+ label: { flexShrink: 1, fontSize: 13, fontWeight: '500' },
113
+ percent: { fontSize: 14, fontWeight: '700', letterSpacing: -0.2, fontVariant: ['tabular-nums'] },
114
+ icon: { marginLeft: 8 },
115
+ track: { width: '100%', overflow: 'hidden', flexDirection: 'row', borderCurve: 'continuous' },
143
116
  footerRow: {
144
117
  flexDirection: 'row',
145
118
  alignItems: 'center',
146
119
  justifyContent: 'space-between',
147
120
  gap: 8,
148
121
  },
149
- footerValue: {
150
- fontSize: 12,
151
- fontVariant: ['tabular-nums'],
152
- },
122
+ footerValue: { fontSize: 12, fontVariant: ['tabular-nums'] },
153
123
  });
154
124
 
155
125
  export const StatBar = memo(StatBarComponent);