@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.
- 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 +65 -55
- 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 +65 -55
- 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 +63 -97
- 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,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
|
|
128
|
+
* badge), {@link DotGridMeter}/{@link StatBar}, and an {@link AvatarGroup} row.
|
|
133
129
|
*
|
|
134
|
-
* `
|
|
135
|
-
*
|
|
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
|
|
151
|
+
const accent = variantAccent(variant, colors);
|
|
152
152
|
const isWide = layout === 'wide';
|
|
153
153
|
|
|
154
|
-
|
|
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.
|
|
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}
|
|
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.
|
|
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
|
-
|
|
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);
|