@oxyhq/bloom 0.29.6 → 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/activity-heatmap/ActivityHeatmap.js +304 -0
- package/lib/commonjs/activity-heatmap/ActivityHeatmap.js.map +1 -0
- package/lib/commonjs/activity-heatmap/bucketByDay.js +51 -0
- package/lib/commonjs/activity-heatmap/bucketByDay.js.map +1 -0
- package/lib/commonjs/activity-heatmap/index.js +20 -0
- package/lib/commonjs/activity-heatmap/index.js.map +1 -0
- package/lib/commonjs/activity-heatmap/types.js +6 -0
- package/lib/commonjs/activity-heatmap/types.js.map +1 -0
- package/lib/commonjs/avatar-group/AvatarGroupBase.js +44 -18
- package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/commonjs/composition-bar/CompositionBar.js +159 -0
- package/lib/commonjs/composition-bar/CompositionBar.js.map +1 -0
- package/lib/commonjs/composition-bar/index.js +13 -0
- package/lib/commonjs/composition-bar/index.js.map +1 -0
- package/lib/commonjs/composition-bar/types.js +6 -0
- package/lib/commonjs/composition-bar/types.js.map +1 -0
- package/lib/commonjs/dot-grid-meter/DotGridMeter.js +69 -0
- package/lib/commonjs/dot-grid-meter/DotGridMeter.js.map +1 -0
- package/lib/commonjs/dot-grid-meter/index.js +13 -0
- package/lib/commonjs/dot-grid-meter/index.js.map +1 -0
- package/lib/commonjs/dot-grid-meter/types.js +6 -0
- package/lib/commonjs/dot-grid-meter/types.js.map +1 -0
- package/lib/commonjs/index.js +59 -12
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +59 -12
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/profile-card/ProfileCard.js +311 -0
- package/lib/commonjs/profile-card/ProfileCard.js.map +1 -0
- package/lib/commonjs/profile-card/index.js +13 -0
- package/lib/commonjs/profile-card/index.js.map +1 -0
- package/lib/commonjs/profile-card/types.js +6 -0
- package/lib/commonjs/profile-card/types.js.map +1 -0
- package/lib/commonjs/stat-bar/StatBar.js +213 -0
- package/lib/commonjs/stat-bar/StatBar.js.map +1 -0
- package/lib/commonjs/stat-bar/index.js +13 -0
- package/lib/commonjs/stat-bar/index.js.map +1 -0
- package/lib/commonjs/stat-bar/types.js +6 -0
- package/lib/commonjs/stat-bar/types.js.map +1 -0
- package/lib/module/activity-heatmap/ActivityHeatmap.js +299 -0
- package/lib/module/activity-heatmap/ActivityHeatmap.js.map +1 -0
- package/lib/module/activity-heatmap/bucketByDay.js +47 -0
- package/lib/module/activity-heatmap/bucketByDay.js.map +1 -0
- package/lib/module/activity-heatmap/index.js +5 -0
- package/lib/module/activity-heatmap/index.js.map +1 -0
- package/lib/module/activity-heatmap/types.js +4 -0
- package/lib/module/activity-heatmap/types.js.map +1 -0
- package/lib/module/avatar-group/AvatarGroupBase.js +44 -18
- package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -1
- package/lib/module/composition-bar/CompositionBar.js +154 -0
- package/lib/module/composition-bar/CompositionBar.js.map +1 -0
- package/lib/module/composition-bar/index.js +4 -0
- package/lib/module/composition-bar/index.js.map +1 -0
- package/lib/module/composition-bar/types.js +4 -0
- package/lib/module/composition-bar/types.js.map +1 -0
- package/lib/module/dot-grid-meter/DotGridMeter.js +64 -0
- package/lib/module/dot-grid-meter/DotGridMeter.js.map +1 -0
- package/lib/module/dot-grid-meter/index.js +4 -0
- package/lib/module/dot-grid-meter/index.js.map +1 -0
- package/lib/module/dot-grid-meter/types.js +4 -0
- package/lib/module/dot-grid-meter/types.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +6 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/profile-card/ProfileCard.js +306 -0
- package/lib/module/profile-card/ProfileCard.js.map +1 -0
- package/lib/module/profile-card/index.js +4 -0
- package/lib/module/profile-card/index.js.map +1 -0
- package/lib/module/profile-card/types.js +4 -0
- package/lib/module/profile-card/types.js.map +1 -0
- package/lib/module/stat-bar/StatBar.js +208 -0
- package/lib/module/stat-bar/StatBar.js.map +1 -0
- package/lib/module/stat-bar/index.js +4 -0
- package/lib/module/stat-bar/index.js.map +1 -0
- package/lib/module/stat-bar/types.js +4 -0
- package/lib/module/stat-bar/types.js.map +1 -0
- package/lib/typescript/commonjs/activity-heatmap/ActivityHeatmap.d.ts +4 -0
- package/lib/typescript/commonjs/activity-heatmap/ActivityHeatmap.d.ts.map +1 -0
- package/lib/typescript/commonjs/activity-heatmap/bucketByDay.d.ts +9 -0
- package/lib/typescript/commonjs/activity-heatmap/bucketByDay.d.ts.map +1 -0
- package/lib/typescript/commonjs/activity-heatmap/index.d.ts +4 -0
- package/lib/typescript/commonjs/activity-heatmap/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/activity-heatmap/types.d.ts +73 -0
- package/lib/typescript/commonjs/activity-heatmap/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/avatar-group/types.d.ts +23 -1
- package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts +4 -0
- package/lib/typescript/commonjs/composition-bar/CompositionBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/composition-bar/index.d.ts +3 -0
- package/lib/typescript/commonjs/composition-bar/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/composition-bar/types.d.ts +29 -0
- package/lib/typescript/commonjs/composition-bar/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/dot-grid-meter/DotGridMeter.d.ts +4 -0
- package/lib/typescript/commonjs/dot-grid-meter/DotGridMeter.d.ts.map +1 -0
- package/lib/typescript/commonjs/dot-grid-meter/index.d.ts +3 -0
- package/lib/typescript/commonjs/dot-grid-meter/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/dot-grid-meter/types.d.ts +36 -0
- package/lib/typescript/commonjs/dot-grid-meter/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +10 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +10 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts +4 -0
- package/lib/typescript/commonjs/profile-card/ProfileCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/profile-card/index.d.ts +3 -0
- package/lib/typescript/commonjs/profile-card/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/profile-card/types.d.ts +86 -0
- package/lib/typescript/commonjs/profile-card/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/stat-bar/StatBar.d.ts +4 -0
- package/lib/typescript/commonjs/stat-bar/StatBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/stat-bar/index.d.ts +3 -0
- package/lib/typescript/commonjs/stat-bar/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/stat-bar/types.d.ts +62 -0
- package/lib/typescript/commonjs/stat-bar/types.d.ts.map +1 -0
- package/lib/typescript/module/activity-heatmap/ActivityHeatmap.d.ts +4 -0
- package/lib/typescript/module/activity-heatmap/ActivityHeatmap.d.ts.map +1 -0
- package/lib/typescript/module/activity-heatmap/bucketByDay.d.ts +9 -0
- package/lib/typescript/module/activity-heatmap/bucketByDay.d.ts.map +1 -0
- package/lib/typescript/module/activity-heatmap/index.d.ts +4 -0
- package/lib/typescript/module/activity-heatmap/index.d.ts.map +1 -0
- package/lib/typescript/module/activity-heatmap/types.d.ts +73 -0
- package/lib/typescript/module/activity-heatmap/types.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -1
- package/lib/typescript/module/avatar-group/types.d.ts +23 -1
- package/lib/typescript/module/avatar-group/types.d.ts.map +1 -1
- package/lib/typescript/module/composition-bar/CompositionBar.d.ts +4 -0
- package/lib/typescript/module/composition-bar/CompositionBar.d.ts.map +1 -0
- package/lib/typescript/module/composition-bar/index.d.ts +3 -0
- package/lib/typescript/module/composition-bar/index.d.ts.map +1 -0
- package/lib/typescript/module/composition-bar/types.d.ts +29 -0
- package/lib/typescript/module/composition-bar/types.d.ts.map +1 -0
- package/lib/typescript/module/dot-grid-meter/DotGridMeter.d.ts +4 -0
- package/lib/typescript/module/dot-grid-meter/DotGridMeter.d.ts.map +1 -0
- package/lib/typescript/module/dot-grid-meter/index.d.ts +3 -0
- package/lib/typescript/module/dot-grid-meter/index.d.ts.map +1 -0
- package/lib/typescript/module/dot-grid-meter/types.d.ts +36 -0
- package/lib/typescript/module/dot-grid-meter/types.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +10 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +10 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/profile-card/ProfileCard.d.ts +4 -0
- package/lib/typescript/module/profile-card/ProfileCard.d.ts.map +1 -0
- package/lib/typescript/module/profile-card/index.d.ts +3 -0
- package/lib/typescript/module/profile-card/index.d.ts.map +1 -0
- package/lib/typescript/module/profile-card/types.d.ts +86 -0
- package/lib/typescript/module/profile-card/types.d.ts.map +1 -0
- package/lib/typescript/module/stat-bar/StatBar.d.ts +4 -0
- package/lib/typescript/module/stat-bar/StatBar.d.ts.map +1 -0
- package/lib/typescript/module/stat-bar/index.d.ts +3 -0
- package/lib/typescript/module/stat-bar/index.d.ts.map +1 -0
- package/lib/typescript/module/stat-bar/types.d.ts +62 -0
- package/lib/typescript/module/stat-bar/types.d.ts.map +1 -0
- package/package.json +56 -1
- package/src/__tests__/StatWidgets.test.tsx +184 -0
- package/src/__tests__/public-api-contract.test.ts +15 -0
- package/src/activity-heatmap/ActivityHeatmap.stories.tsx +108 -0
- package/src/activity-heatmap/ActivityHeatmap.tsx +289 -0
- package/src/activity-heatmap/bucketByDay.ts +49 -0
- package/src/activity-heatmap/index.ts +3 -0
- package/src/activity-heatmap/types.ts +74 -0
- package/src/avatar-group/AvatarGroup.stories.tsx +14 -0
- package/src/avatar-group/AvatarGroupBase.tsx +48 -20
- package/src/avatar-group/types.ts +23 -1
- package/src/composition-bar/CompositionBar.stories.tsx +70 -0
- package/src/composition-bar/CompositionBar.tsx +161 -0
- package/src/composition-bar/index.ts +2 -0
- package/src/composition-bar/types.ts +30 -0
- package/src/dot-grid-meter/DotGridMeter.stories.tsx +47 -0
- package/src/dot-grid-meter/DotGridMeter.tsx +71 -0
- package/src/dot-grid-meter/index.ts +2 -0
- package/src/dot-grid-meter/types.ts +36 -0
- package/src/index.ts +24 -0
- package/src/index.web.ts +24 -0
- package/src/profile-card/ProfileCard.stories.tsx +210 -0
- package/src/profile-card/ProfileCard.tsx +269 -0
- package/src/profile-card/index.ts +9 -0
- package/src/profile-card/types.ts +86 -0
- package/src/stat-bar/StatBar.stories.tsx +61 -0
- package/src/stat-bar/StatBar.tsx +126 -0
- package/src/stat-bar/index.ts +7 -0
- package/src/stat-bar/types.ts +65 -0
|
@@ -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,269 @@
|
|
|
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 = 18;
|
|
21
|
+
const CARD_RADIUS = 28;
|
|
22
|
+
const AVATAR_SIZE = 54;
|
|
23
|
+
const FOOTER_AVATAR_SIZE = 30;
|
|
24
|
+
|
|
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 {
|
|
30
|
+
switch (variant) {
|
|
31
|
+
case 'wallet':
|
|
32
|
+
return colors.success;
|
|
33
|
+
case 'shopping':
|
|
34
|
+
return colors.warning;
|
|
35
|
+
case 'social':
|
|
36
|
+
return colors.primary;
|
|
37
|
+
case 'stat':
|
|
38
|
+
default:
|
|
39
|
+
return colors.info;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function AvatarWithBadge({
|
|
44
|
+
source,
|
|
45
|
+
name,
|
|
46
|
+
ring,
|
|
47
|
+
badge,
|
|
48
|
+
haloColor,
|
|
49
|
+
}: {
|
|
50
|
+
source?: string | null;
|
|
51
|
+
name?: string;
|
|
52
|
+
ring: AvatarRingConfig;
|
|
53
|
+
badge?: React.ReactNode;
|
|
54
|
+
haloColor: string;
|
|
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 style={[styles.badgeHalo, { backgroundColor: haloColor }]}>{badge}</View>
|
|
61
|
+
)}
|
|
62
|
+
</View>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function MetricSection({
|
|
67
|
+
metric,
|
|
68
|
+
accent,
|
|
69
|
+
labelColor,
|
|
70
|
+
}: {
|
|
71
|
+
metric: ProfileCardMetric;
|
|
72
|
+
accent: string;
|
|
73
|
+
labelColor: string;
|
|
74
|
+
}) {
|
|
75
|
+
switch (metric.kind) {
|
|
76
|
+
case 'dots':
|
|
77
|
+
return (
|
|
78
|
+
<View style={styles.section}>
|
|
79
|
+
{metric.label != null && (
|
|
80
|
+
<Text style={[styles.sectionLabel, { color: labelColor }]} numberOfLines={1}>
|
|
81
|
+
{metric.label}
|
|
82
|
+
</Text>
|
|
83
|
+
)}
|
|
84
|
+
<DotGridMeter
|
|
85
|
+
filled={metric.filled}
|
|
86
|
+
total={metric.total}
|
|
87
|
+
columns={metric.total}
|
|
88
|
+
dotSize={9}
|
|
89
|
+
gap={6}
|
|
90
|
+
filledColor={metric.filledColor ?? accent}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
case 'progress':
|
|
95
|
+
return (
|
|
96
|
+
<StatBar
|
|
97
|
+
variant="progress"
|
|
98
|
+
label={metric.label}
|
|
99
|
+
value={metric.value}
|
|
100
|
+
max={metric.max}
|
|
101
|
+
minLabel={metric.minLabel}
|
|
102
|
+
maxLabel={metric.maxLabel}
|
|
103
|
+
icon={metric.icon}
|
|
104
|
+
fillColor={metric.fillColor ?? accent}
|
|
105
|
+
/>
|
|
106
|
+
);
|
|
107
|
+
case 'split':
|
|
108
|
+
return (
|
|
109
|
+
<StatBar
|
|
110
|
+
variant="split"
|
|
111
|
+
label={metric.label}
|
|
112
|
+
percent={metric.percent}
|
|
113
|
+
leftValue={metric.leftValue}
|
|
114
|
+
rightValue={metric.rightValue}
|
|
115
|
+
fillColor={metric.fillColor ?? accent}
|
|
116
|
+
/>
|
|
117
|
+
);
|
|
118
|
+
case 'custom':
|
|
119
|
+
default:
|
|
120
|
+
return <>{metric.node}</>;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A composed profile "widget" card — the Apple-Watch-style stat card. Fully
|
|
126
|
+
* app-agnostic: it takes pre-formatted strings, colors, and `ReactNode` icons
|
|
127
|
+
* (no domain/currency logic). Built on {@link Card}, {@link Avatar} (ring +
|
|
128
|
+
* badge), {@link DotGridMeter}/{@link StatBar}, and an {@link AvatarGroup} row.
|
|
129
|
+
*
|
|
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
|
+
*/
|
|
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 accent = variantAccent(variant, colors);
|
|
152
|
+
const isWide = layout === 'wide';
|
|
153
|
+
|
|
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 };
|
|
157
|
+
|
|
158
|
+
const surfaceStyle: ViewStyle = {
|
|
159
|
+
width: isWide ? '100%' : WIDGET_WIDTH,
|
|
160
|
+
padding: CARD_PADDING,
|
|
161
|
+
borderRadius: CARD_RADIUS,
|
|
162
|
+
borderWidth: 1,
|
|
163
|
+
borderColor: colors.border,
|
|
164
|
+
backgroundColor: colors.background,
|
|
165
|
+
borderCurve: 'continuous',
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const headline = (
|
|
169
|
+
<>
|
|
170
|
+
<Text style={[styles.value, { color: colors.text }]} numberOfLines={1}>
|
|
171
|
+
{value}
|
|
172
|
+
</Text>
|
|
173
|
+
{subtitle != null && (
|
|
174
|
+
<Text style={[styles.subtitle, { color: colors.textSecondary }]} numberOfLines={1}>
|
|
175
|
+
{subtitle}
|
|
176
|
+
</Text>
|
|
177
|
+
)}
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const metricNode =
|
|
182
|
+
metric != null ? (
|
|
183
|
+
<MetricSection metric={metric} accent={accent} labelColor={colors.textSecondary} />
|
|
184
|
+
) : null;
|
|
185
|
+
|
|
186
|
+
const footerNode =
|
|
187
|
+
footer != null ? (
|
|
188
|
+
<View style={styles.section}>
|
|
189
|
+
{footer.label != null && (
|
|
190
|
+
<Text style={[styles.sectionLabel, { color: colors.textSecondary }]} numberOfLines={1}>
|
|
191
|
+
{footer.label}
|
|
192
|
+
</Text>
|
|
193
|
+
)}
|
|
194
|
+
<AvatarGroup
|
|
195
|
+
layout="row"
|
|
196
|
+
items={footer.items}
|
|
197
|
+
size={FOOTER_AVATAR_SIZE}
|
|
198
|
+
spacing={7}
|
|
199
|
+
max={footer.max ?? 4}
|
|
200
|
+
showInitials
|
|
201
|
+
/>
|
|
202
|
+
</View>
|
|
203
|
+
) : null;
|
|
204
|
+
|
|
205
|
+
return (
|
|
206
|
+
<Card variant="filled" onPress={onPress} style={[surfaceStyle, style]} testID={testID}>
|
|
207
|
+
<View style={styles.body}>
|
|
208
|
+
{isWide ? (
|
|
209
|
+
<View style={styles.headerRowWide}>
|
|
210
|
+
<AvatarWithBadge
|
|
211
|
+
source={avatar.source}
|
|
212
|
+
name={avatar.name}
|
|
213
|
+
ring={ring}
|
|
214
|
+
badge={avatar.badge}
|
|
215
|
+
haloColor={colors.background}
|
|
216
|
+
/>
|
|
217
|
+
<View style={styles.wideValueRow}>
|
|
218
|
+
<View style={styles.headlineColumn}>{headline}</View>
|
|
219
|
+
{headlineIcon != null && <View style={styles.wideIcon}>{headlineIcon}</View>}
|
|
220
|
+
</View>
|
|
221
|
+
</View>
|
|
222
|
+
) : (
|
|
223
|
+
<View style={styles.headerRow}>
|
|
224
|
+
<AvatarWithBadge
|
|
225
|
+
source={avatar.source}
|
|
226
|
+
name={avatar.name}
|
|
227
|
+
ring={ring}
|
|
228
|
+
badge={avatar.badge}
|
|
229
|
+
haloColor={colors.background}
|
|
230
|
+
/>
|
|
231
|
+
<View style={styles.headlineColumn}>{headline}</View>
|
|
232
|
+
</View>
|
|
233
|
+
)}
|
|
234
|
+
|
|
235
|
+
{metricNode}
|
|
236
|
+
{footerNode}
|
|
237
|
+
</View>
|
|
238
|
+
|
|
239
|
+
{!isWide && headlineIcon != null && <View style={styles.floatingIcon}>{headlineIcon}</View>}
|
|
240
|
+
</Card>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const styles = StyleSheet.create({
|
|
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: {
|
|
257
|
+
position: 'absolute',
|
|
258
|
+
right: -3,
|
|
259
|
+
bottom: -3,
|
|
260
|
+
padding: 2,
|
|
261
|
+
borderRadius: 999,
|
|
262
|
+
alignItems: 'center',
|
|
263
|
+
justifyContent: 'center',
|
|
264
|
+
},
|
|
265
|
+
floatingIcon: { position: 'absolute', top: CARD_PADDING, right: CARD_PADDING },
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export const ProfileCard = memo(ProfileCardComponent);
|
|
269
|
+
ProfileCard.displayName = 'ProfileCard';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import type { AvatarRingConfig } from '../avatar/types';
|
|
5
|
+
import type { AvatarGroupItem } from '../avatar-group/types';
|
|
6
|
+
|
|
7
|
+
export type ProfileCardLayout = 'widget' | 'wide';
|
|
8
|
+
export type ProfileCardVariant = 'wallet' | 'social' | 'shopping' | 'stat';
|
|
9
|
+
|
|
10
|
+
/** The avatar block of a {@link ProfileCard}. All values are consumer-supplied. */
|
|
11
|
+
export interface ProfileCardAvatar {
|
|
12
|
+
/**
|
|
13
|
+
* Avatar value, routed into {@link Avatar}'s `source` prop — a full URL or a
|
|
14
|
+
* resolver-handled id (e.g. an Oxy file ID).
|
|
15
|
+
*/
|
|
16
|
+
source?: string | null;
|
|
17
|
+
/** Decorative ring. Defaults to a solid ring in the variant accent color. */
|
|
18
|
+
ring?: AvatarRingConfig;
|
|
19
|
+
/** Badge overlay pinned to the avatar's bottom-right (e.g. a small logo). */
|
|
20
|
+
badge?: ReactNode;
|
|
21
|
+
/** Name used for the avatar's initial/placeholder and accessibility. */
|
|
22
|
+
name?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The metric visual rendered below the headline. */
|
|
26
|
+
export type ProfileCardMetric =
|
|
27
|
+
| { kind: 'dots'; label?: string; filled: number; total: number; filledColor?: string }
|
|
28
|
+
| {
|
|
29
|
+
kind: 'progress';
|
|
30
|
+
label: string;
|
|
31
|
+
value: number;
|
|
32
|
+
max: number;
|
|
33
|
+
minLabel?: string;
|
|
34
|
+
maxLabel?: string;
|
|
35
|
+
icon?: ReactNode;
|
|
36
|
+
fillColor?: string;
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
kind: 'split';
|
|
40
|
+
label: string;
|
|
41
|
+
percent: number;
|
|
42
|
+
leftValue: string;
|
|
43
|
+
rightValue: string;
|
|
44
|
+
fillColor?: string;
|
|
45
|
+
}
|
|
46
|
+
| { kind: 'custom'; node: ReactNode };
|
|
47
|
+
|
|
48
|
+
/** The trailing facepile row (e.g. "Top tokens"). */
|
|
49
|
+
export interface ProfileCardFooter {
|
|
50
|
+
/** Small label above the row. */
|
|
51
|
+
label?: string;
|
|
52
|
+
/** Avatars rendered adjacent in a row. */
|
|
53
|
+
items: AvatarGroupItem[];
|
|
54
|
+
/** Maximum avatars before collapsing into `+N`. @default 4 */
|
|
55
|
+
max?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface ProfileCardProps {
|
|
59
|
+
/**
|
|
60
|
+
* Card shape. `'widget'` is a compact ~square card; `'wide'` is a full-width
|
|
61
|
+
* card. @default 'widget'
|
|
62
|
+
*/
|
|
63
|
+
layout?: ProfileCardLayout;
|
|
64
|
+
/**
|
|
65
|
+
* Selects sensible accent defaults (ring / metric fill). Every accent stays
|
|
66
|
+
* overridable per-element. @default 'stat'
|
|
67
|
+
*/
|
|
68
|
+
variant?: ProfileCardVariant;
|
|
69
|
+
/** Avatar block. */
|
|
70
|
+
avatar: ProfileCardAvatar;
|
|
71
|
+
/** Pre-formatted headline value (e.g. a balance or count). */
|
|
72
|
+
value: string;
|
|
73
|
+
/** Secondary line under the headline. */
|
|
74
|
+
subtitle?: string;
|
|
75
|
+
/** Node floated at the card's top-right (e.g. a trophy or menu icon). */
|
|
76
|
+
headlineIcon?: ReactNode;
|
|
77
|
+
/** Metric visual below the headline. */
|
|
78
|
+
metric?: ProfileCardMetric;
|
|
79
|
+
/** Trailing facepile row. */
|
|
80
|
+
footer?: ProfileCardFooter;
|
|
81
|
+
/** Press handler — makes the whole card pressable. */
|
|
82
|
+
onPress?: () => void;
|
|
83
|
+
/** Container style override (merged over the card surface). */
|
|
84
|
+
style?: StyleProp<ViewStyle>;
|
|
85
|
+
testID?: string;
|
|
86
|
+
}
|