@oxyhq/bloom 0.30.0 → 0.30.2

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 +66 -56
  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 +66 -56
  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 +64 -98
  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,15 @@ 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}
129
+ * facepile (overlapping stack).
133
130
  *
134
- * `variant` only picks sensible accent defaults (ring + metric fill); every
135
- * accent stays overridable via the individual props.
131
+ * Theme-driven (Bloom's `useTheme()` reads the same NativeWind token pipeline
132
+ * that backs the `--*` vars): the surface is the theme's `background` — the
133
+ * darkest token, a near-black in dark mode — with a hairline `border` so it
134
+ * separates on any page, and the metric tracks read from `backgroundSecondary`,
135
+ * a step lighter, so they always show. `variant` picks a vivid semantic accent
136
+ * (success/warning/info/primary), overridable per-prop.
136
137
  */
137
138
  const ProfileCardComponent: React.FC<ProfileCardProps> = ({
138
139
  layout = 'widget',
@@ -148,15 +149,20 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
148
149
  testID,
149
150
  }) => {
150
151
  const { colors } = useTheme();
151
- const accents = variantAccents(variant, colors);
152
+ const accent = variantAccent(variant, colors);
152
153
  const isWide = layout === 'wide';
153
154
 
154
- const ring: AvatarRingConfig = avatar.ring ?? { colors: accents.ring, width: 2, gap: 2 };
155
+ // Consumer ring wins for color; default to the accent, always with a small
156
+ // gap so the ring reads as a distinct halo (as in the reference).
157
+ const ring: AvatarRingConfig = { colors: accent, width: 2, gap: 2, ...avatar.ring };
155
158
 
156
159
  const surfaceStyle: ViewStyle = {
157
160
  width: isWide ? '100%' : WIDGET_WIDTH,
158
161
  padding: CARD_PADDING,
159
162
  borderRadius: CARD_RADIUS,
163
+ borderWidth: 1,
164
+ borderColor: colors.border,
165
+ backgroundColor: colors.background,
160
166
  borderCurve: 'continuous',
161
167
  };
162
168
 
@@ -166,7 +172,7 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
166
172
  {value}
167
173
  </Text>
168
174
  {subtitle != null && (
169
- <Text style={[styles.subtitle, { color: colors.textTertiary }]} numberOfLines={1}>
175
+ <Text style={[styles.subtitle, { color: colors.textSecondary }]} numberOfLines={1}>
170
176
  {subtitle}
171
177
  </Text>
172
178
  )}
@@ -175,33 +181,29 @@ const ProfileCardComponent: React.FC<ProfileCardProps> = ({
175
181
 
176
182
  const metricNode =
177
183
  metric != null ? (
178
- <MetricSection metric={metric} accents={accents} labelColor={colors.textTertiary} />
184
+ <MetricSection metric={metric} accent={accent} labelColor={colors.textSecondary} />
179
185
  ) : null;
180
186
 
181
187
  const footerNode =
182
188
  footer != null ? (
183
189
  <View style={styles.section}>
184
190
  {footer.label != null && (
185
- <Text style={[styles.sectionLabel, { color: colors.textTertiary }]} numberOfLines={1}>
191
+ <Text style={[styles.sectionLabel, { color: colors.textSecondary }]} numberOfLines={1}>
186
192
  {footer.label}
187
193
  </Text>
188
194
  )}
189
195
  <AvatarGroup
190
- layout="row"
196
+ layout="stack"
191
197
  items={footer.items}
192
198
  size={FOOTER_AVATAR_SIZE}
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);