@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.
- package/lib/commonjs/avatar-group/AvatarGroupBase.js +2 -0
- package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/commonjs/profile-card/ProfileCard.js +66 -56
- package/lib/commonjs/profile-card/ProfileCard.js.map +1 -1
- package/lib/commonjs/stat-bar/StatBar.js +4 -1
- package/lib/commonjs/stat-bar/StatBar.js.map +1 -1
- package/lib/module/avatar-group/AvatarGroupBase.js +2 -0
- package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/module/profile-card/ProfileCard.js +66 -56
- package/lib/module/profile-card/ProfileCard.js.map +1 -1
- package/lib/module/stat-bar/StatBar.js +4 -1
- package/lib/module/stat-bar/StatBar.js.map +1 -1
- package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar-group/types.d.ts +8 -0
- package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/stat-bar/StatBar.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/types.d.ts +8 -0
- package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
- package/lib/typescript/module/profile-card/ProfileCard.d.ts.map +1 -1
- package/lib/typescript/module/stat-bar/StatBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/avatar-group/AvatarGroupBase.tsx +9 -2
- package/src/avatar-group/types.ts +8 -0
- package/src/profile-card/ProfileCard.tsx +64 -98
- 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 =
|
|
21
|
-
const CARD_RADIUS =
|
|
22
|
-
const AVATAR_SIZE =
|
|
23
|
-
const FOOTER_AVATAR_SIZE =
|
|
20
|
+
const CARD_PADDING = 18;
|
|
21
|
+
const CARD_RADIUS = 28;
|
|
22
|
+
const AVATAR_SIZE = 54;
|
|
23
|
+
const FOOTER_AVATAR_SIZE = 30;
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
32
|
+
return colors.success;
|
|
35
33
|
case 'shopping':
|
|
36
|
-
return
|
|
34
|
+
return colors.warning;
|
|
37
35
|
case 'social':
|
|
38
|
-
return
|
|
36
|
+
return colors.primary;
|
|
39
37
|
case 'stat':
|
|
40
38
|
default:
|
|
41
|
-
return
|
|
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
|
-
|
|
68
|
+
accent,
|
|
76
69
|
labelColor,
|
|
77
70
|
}: {
|
|
78
71
|
metric: ProfileCardMetric;
|
|
79
|
-
|
|
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
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
|
128
|
+
* badge), {@link DotGridMeter}/{@link StatBar}, and an {@link AvatarGroup}
|
|
129
|
+
* facepile (overlapping stack).
|
|
133
130
|
*
|
|
134
|
-
* `
|
|
135
|
-
*
|
|
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
|
|
152
|
+
const accent = variantAccent(variant, colors);
|
|
152
153
|
const isWide = layout === 'wide';
|
|
153
154
|
|
|
154
|
-
|
|
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.
|
|
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}
|
|
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.
|
|
191
|
+
<Text style={[styles.sectionLabel, { color: colors.textSecondary }]} numberOfLines={1}>
|
|
186
192
|
{footer.label}
|
|
187
193
|
</Text>
|
|
188
194
|
)}
|
|
189
195
|
<AvatarGroup
|
|
190
|
-
layout="
|
|
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
|
-
|
|
243
|
-
},
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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: -
|
|
291
|
-
bottom: -
|
|
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);
|
package/src/stat-bar/StatBar.tsx
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|