@oxyhq/bloom 0.8.5 → 0.9.0
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/AvatarGroup.js +26 -0
- package/lib/commonjs/avatar-group/AvatarGroup.js.map +1 -0
- package/lib/commonjs/avatar-group/AvatarGroup.web.js +116 -0
- package/lib/commonjs/avatar-group/AvatarGroup.web.js.map +1 -0
- package/lib/commonjs/avatar-group/AvatarGroupBase.js +184 -0
- package/lib/commonjs/avatar-group/AvatarGroupBase.js.map +1 -0
- package/lib/commonjs/avatar-group/index.js +13 -0
- package/lib/commonjs/avatar-group/index.js.map +1 -0
- package/lib/commonjs/avatar-group/index.web.js +13 -0
- package/lib/commonjs/avatar-group/index.web.js.map +1 -0
- package/lib/commonjs/avatar-group/types.js +6 -0
- package/lib/commonjs/avatar-group/types.js.map +1 -0
- package/lib/commonjs/index.js +60 -44
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +74 -58
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/theme/color-scope/index.web.js +6 -7
- package/lib/commonjs/theme/color-scope/index.web.js.map +1 -1
- package/lib/commonjs/theme/color-scope/style-builder.js +18 -6
- package/lib/commonjs/theme/color-scope/style-builder.js.map +1 -1
- package/lib/commonjs/user-hover-card/UserHoverCard.js +193 -0
- package/lib/commonjs/user-hover-card/UserHoverCard.js.map +1 -0
- package/lib/commonjs/user-hover-card/index.js +13 -0
- package/lib/commonjs/user-hover-card/index.js.map +1 -0
- package/lib/commonjs/user-hover-card/types.js +6 -0
- package/lib/commonjs/user-hover-card/types.js.map +1 -0
- package/lib/module/avatar-group/AvatarGroup.js +21 -0
- package/lib/module/avatar-group/AvatarGroup.js.map +1 -0
- package/lib/module/avatar-group/AvatarGroup.web.js +111 -0
- package/lib/module/avatar-group/AvatarGroup.web.js.map +1 -0
- package/lib/module/avatar-group/AvatarGroupBase.js +179 -0
- package/lib/module/avatar-group/AvatarGroupBase.js.map +1 -0
- package/lib/module/avatar-group/index.js +4 -0
- package/lib/module/avatar-group/index.js.map +1 -0
- package/lib/module/avatar-group/index.web.js +4 -0
- package/lib/module/avatar-group/index.web.js.map +1 -0
- package/lib/module/avatar-group/types.js +4 -0
- package/lib/module/avatar-group/types.js.map +1 -0
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -0
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/theme/color-scope/index.web.js +6 -7
- package/lib/module/theme/color-scope/index.web.js.map +1 -1
- package/lib/module/theme/color-scope/style-builder.js +19 -7
- package/lib/module/theme/color-scope/style-builder.js.map +1 -1
- package/lib/module/user-hover-card/UserHoverCard.js +188 -0
- package/lib/module/user-hover-card/UserHoverCard.js.map +1 -0
- package/lib/module/user-hover-card/index.js +4 -0
- package/lib/module/user-hover-card/index.js.map +1 -0
- package/lib/module/user-hover-card/types.js +4 -0
- package/lib/module/user-hover-card/types.js.map +1 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts +4 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts +4 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroup.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts +16 -0
- package/lib/typescript/commonjs/avatar-group/AvatarGroupBase.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/index.d.ts +3 -0
- package/lib/typescript/commonjs/avatar-group/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/index.web.d.ts +3 -0
- package/lib/typescript/commonjs/avatar-group/index.web.d.ts.map +1 -0
- package/lib/typescript/commonjs/avatar-group/types.d.ts +63 -0
- package/lib/typescript/commonjs/avatar-group/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +4 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts +4 -0
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts +7 -5
- package/lib/typescript/commonjs/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/commonjs/user-hover-card/UserHoverCard.d.ts +4 -0
- package/lib/typescript/commonjs/user-hover-card/UserHoverCard.d.ts.map +1 -0
- package/lib/typescript/commonjs/user-hover-card/index.d.ts +3 -0
- package/lib/typescript/commonjs/user-hover-card/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/user-hover-card/types.d.ts +37 -0
- package/lib/typescript/commonjs/user-hover-card/types.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/AvatarGroup.d.ts +4 -0
- package/lib/typescript/module/avatar-group/AvatarGroup.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts +4 -0
- package/lib/typescript/module/avatar-group/AvatarGroup.web.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts +16 -0
- package/lib/typescript/module/avatar-group/AvatarGroupBase.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/index.d.ts +3 -0
- package/lib/typescript/module/avatar-group/index.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/index.web.d.ts +3 -0
- package/lib/typescript/module/avatar-group/index.web.d.ts.map +1 -0
- package/lib/typescript/module/avatar-group/types.d.ts +63 -0
- package/lib/typescript/module/avatar-group/types.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +4 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts +4 -0
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/index.web.d.ts.map +1 -1
- package/lib/typescript/module/theme/color-scope/style-builder.d.ts +7 -5
- package/lib/typescript/module/theme/color-scope/style-builder.d.ts.map +1 -1
- package/lib/typescript/module/user-hover-card/UserHoverCard.d.ts +4 -0
- package/lib/typescript/module/user-hover-card/UserHoverCard.d.ts.map +1 -0
- package/lib/typescript/module/user-hover-card/index.d.ts +3 -0
- package/lib/typescript/module/user-hover-card/index.d.ts.map +1 -0
- package/lib/typescript/module/user-hover-card/types.d.ts +37 -0
- package/lib/typescript/module/user-hover-card/types.d.ts.map +1 -0
- package/package.json +30 -1
- package/src/__tests__/BloomColorScope.test.tsx +12 -4
- package/src/avatar-group/AvatarGroup.stories.tsx +135 -0
- package/src/avatar-group/AvatarGroup.tsx +19 -0
- package/src/avatar-group/AvatarGroup.web.tsx +145 -0
- package/src/avatar-group/AvatarGroupBase.tsx +249 -0
- package/src/avatar-group/index.ts +2 -0
- package/src/avatar-group/index.web.tsx +2 -0
- package/src/avatar-group/types.ts +64 -0
- package/src/index.ts +4 -0
- package/src/index.web.ts +4 -0
- package/src/theme/color-scope/index.web.tsx +6 -7
- package/src/theme/color-scope/style-builder.ts +19 -7
- package/src/user-hover-card/UserHoverCard.stories.tsx +113 -0
- package/src/user-hover-card/UserHoverCard.tsx +204 -0
- package/src/user-hover-card/index.ts +2 -0
- package/src/user-hover-card/types.ts +38 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import { Platform, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import { getResolvedTokens } from '../token-registry';
|
|
4
|
+
import { CANONICAL_TOKENS, getResolvedTokens } from '../token-registry';
|
|
5
5
|
import type { AppColorName } from '../color-presets';
|
|
6
6
|
import { lazyRequire } from '../../utils/lazy-require';
|
|
7
7
|
|
|
@@ -25,17 +25,29 @@ const getNativeWindVars = lazyRequire<NativeWindVarsModule>('nativewind');
|
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Build the CSS custom-property map for a preset, ready to be applied to a
|
|
28
|
-
* subtree. Every token is resolved to an sRGB `rgb(...)` string
|
|
29
|
-
* `getResolvedTokens` — the single
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* same
|
|
28
|
+
* subtree. Every canonical `--x` token is resolved to an sRGB `rgb(...)` string
|
|
29
|
+
* via `getResolvedTokens` — the single token pipeline shared by web and native.
|
|
30
|
+
*
|
|
31
|
+
* The returned map also includes Tailwind v4 `--color-x` aliases generated from
|
|
32
|
+
* those same canonical values. Scoped aliases are required on web because
|
|
33
|
+
* inherited custom properties like `--color-primary: var(--primary)` compute at
|
|
34
|
+
* the document root and would otherwise keep pointing at the app-wide preset.
|
|
33
35
|
*/
|
|
34
36
|
export function buildScopeVars(
|
|
35
37
|
colorPreset: AppColorName,
|
|
36
38
|
mode: 'light' | 'dark',
|
|
37
39
|
): Record<string, string> {
|
|
38
|
-
|
|
40
|
+
const tokens = getResolvedTokens(colorPreset, mode);
|
|
41
|
+
const vars: Record<string, string> = { ...tokens };
|
|
42
|
+
|
|
43
|
+
for (const token of CANONICAL_TOKENS) {
|
|
44
|
+
const value = tokens[`--${token}`];
|
|
45
|
+
if (value !== undefined) {
|
|
46
|
+
vars[`--color-${token}`] = value;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return vars;
|
|
39
51
|
}
|
|
40
52
|
|
|
41
53
|
/**
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, Text, View } from 'react-native';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
4
|
+
|
|
5
|
+
import { UserHoverCard } from './UserHoverCard';
|
|
6
|
+
|
|
7
|
+
const SAMPLE_AVATAR =
|
|
8
|
+
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=200&h=200&fit=crop';
|
|
9
|
+
|
|
10
|
+
function DemoFollowButton() {
|
|
11
|
+
return (
|
|
12
|
+
<Pressable
|
|
13
|
+
style={{
|
|
14
|
+
paddingHorizontal: 14,
|
|
15
|
+
paddingVertical: 7,
|
|
16
|
+
borderRadius: 999,
|
|
17
|
+
backgroundColor: '#1A73E8',
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
<Text style={{ color: '#fff', fontWeight: '600', fontSize: 13 }}>
|
|
21
|
+
Follow
|
|
22
|
+
</Text>
|
|
23
|
+
</Pressable>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const meta: Meta<typeof UserHoverCard> = {
|
|
28
|
+
title: 'Components/UserHoverCard',
|
|
29
|
+
component: UserHoverCard,
|
|
30
|
+
argTypes: {
|
|
31
|
+
verified: { control: 'boolean' },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
|
|
37
|
+
type Story = StoryObj<typeof UserHoverCard>;
|
|
38
|
+
|
|
39
|
+
export const Basic: Story = {
|
|
40
|
+
args: {
|
|
41
|
+
avatar: SAMPLE_AVATAR,
|
|
42
|
+
displayName: 'Nate Isern',
|
|
43
|
+
username: 'nate',
|
|
44
|
+
bio: 'Building the Oxy ecosystem. Designer, engineer, and occasional rose grower.',
|
|
45
|
+
stats: [
|
|
46
|
+
{ label: 'Following', value: '312' },
|
|
47
|
+
{ label: 'Followers', value: '4.8K' },
|
|
48
|
+
],
|
|
49
|
+
verified: true,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const WithAction: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
avatar: SAMPLE_AVATAR,
|
|
56
|
+
displayName: 'Ada Lovelace',
|
|
57
|
+
username: 'ada',
|
|
58
|
+
bio: 'Mathematician. Wrote the first algorithm intended for a machine.',
|
|
59
|
+
stats: [
|
|
60
|
+
{ label: 'Following', value: '12' },
|
|
61
|
+
{ label: 'Followers', value: '1.2M' },
|
|
62
|
+
],
|
|
63
|
+
verified: true,
|
|
64
|
+
action: <DemoFollowButton />,
|
|
65
|
+
},
|
|
66
|
+
name: 'With action slot',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export const NoAction: Story = {
|
|
70
|
+
args: {
|
|
71
|
+
avatar: null,
|
|
72
|
+
displayName: 'Grace Hopper',
|
|
73
|
+
username: 'grace',
|
|
74
|
+
bio: 'Computer scientist and US Navy rear admiral.',
|
|
75
|
+
stats: [
|
|
76
|
+
{ label: 'Following', value: '88' },
|
|
77
|
+
{ label: 'Followers', value: '900K' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
name: 'No action slot',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const Minimal: Story = {
|
|
84
|
+
args: {
|
|
85
|
+
displayName: 'Anonymous',
|
|
86
|
+
},
|
|
87
|
+
name: 'Minimal (name only)',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const Pressable_: Story = {
|
|
91
|
+
render: (args) => (
|
|
92
|
+
<View style={{ gap: 8 }}>
|
|
93
|
+
<UserHoverCard
|
|
94
|
+
{...args}
|
|
95
|
+
avatar={SAMPLE_AVATAR}
|
|
96
|
+
displayName="Linus Torvalds"
|
|
97
|
+
username="linus"
|
|
98
|
+
bio="Creator of Linux and Git."
|
|
99
|
+
stats={[
|
|
100
|
+
{ label: 'Following', value: '3' },
|
|
101
|
+
{ label: 'Followers', value: '2.1M' },
|
|
102
|
+
]}
|
|
103
|
+
action={<DemoFollowButton />}
|
|
104
|
+
onPressProfile={() => {
|
|
105
|
+
// eslint-disable-next-line no-alert
|
|
106
|
+
if (typeof window !== 'undefined') window.alert('open profile');
|
|
107
|
+
}}
|
|
108
|
+
/>
|
|
109
|
+
<Text>Click the identity area to open the profile.</Text>
|
|
110
|
+
</View>
|
|
111
|
+
),
|
|
112
|
+
name: 'Pressable identity',
|
|
113
|
+
};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, { memo, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Platform,
|
|
4
|
+
Pressable,
|
|
5
|
+
StyleSheet,
|
|
6
|
+
Text,
|
|
7
|
+
View,
|
|
8
|
+
type ViewStyle,
|
|
9
|
+
} from 'react-native';
|
|
10
|
+
|
|
11
|
+
import { Avatar } from '../avatar';
|
|
12
|
+
import { VerifiedCheck } from '../icons/VerifiedCheck';
|
|
13
|
+
import { useTheme } from '../theme/use-theme';
|
|
14
|
+
import { borderRadius, fontSize, space } from '../styles/tokens';
|
|
15
|
+
import type { UserHoverCardProps } from './types';
|
|
16
|
+
|
|
17
|
+
const AVATAR_SIZE = 48;
|
|
18
|
+
const VERIFIED_SIZE = 16;
|
|
19
|
+
|
|
20
|
+
const UserHoverCardComponent: React.FC<UserHoverCardProps> = ({
|
|
21
|
+
avatar,
|
|
22
|
+
displayName,
|
|
23
|
+
username,
|
|
24
|
+
bio,
|
|
25
|
+
stats,
|
|
26
|
+
verified = false,
|
|
27
|
+
onPressProfile,
|
|
28
|
+
action,
|
|
29
|
+
style,
|
|
30
|
+
testID,
|
|
31
|
+
}) => {
|
|
32
|
+
const theme = useTheme();
|
|
33
|
+
|
|
34
|
+
const cardStyle = useMemo((): ViewStyle => {
|
|
35
|
+
const base: ViewStyle = {
|
|
36
|
+
backgroundColor: theme.colors.card,
|
|
37
|
+
borderRadius: borderRadius.lg,
|
|
38
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
39
|
+
borderColor: theme.colors.border,
|
|
40
|
+
padding: space.lg,
|
|
41
|
+
width: 280,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (Platform.OS === 'web') {
|
|
45
|
+
base.boxShadow = `0px 8px 24px ${theme.colors.shadow}`;
|
|
46
|
+
} else {
|
|
47
|
+
base.shadowColor = theme.colors.shadow;
|
|
48
|
+
base.shadowOffset = { width: 0, height: 8 };
|
|
49
|
+
base.shadowOpacity = 0.18;
|
|
50
|
+
base.shadowRadius = 16;
|
|
51
|
+
base.elevation = 8;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return base;
|
|
55
|
+
}, [theme]);
|
|
56
|
+
|
|
57
|
+
const identity = (
|
|
58
|
+
<View style={styles.identityRow}>
|
|
59
|
+
<Avatar uri={avatar ?? undefined} name={displayName} size={AVATAR_SIZE} />
|
|
60
|
+
<View style={styles.identityText}>
|
|
61
|
+
<View style={styles.nameRow}>
|
|
62
|
+
<Text
|
|
63
|
+
numberOfLines={1}
|
|
64
|
+
style={[styles.displayName, { color: theme.colors.text }]}
|
|
65
|
+
>
|
|
66
|
+
{displayName}
|
|
67
|
+
</Text>
|
|
68
|
+
{verified && (
|
|
69
|
+
<VerifiedCheck
|
|
70
|
+
size="sm"
|
|
71
|
+
fill={theme.colors.primary}
|
|
72
|
+
width={VERIFIED_SIZE}
|
|
73
|
+
height={VERIFIED_SIZE}
|
|
74
|
+
style={styles.verifiedBadge}
|
|
75
|
+
/>
|
|
76
|
+
)}
|
|
77
|
+
</View>
|
|
78
|
+
{username ? (
|
|
79
|
+
<Text
|
|
80
|
+
numberOfLines={1}
|
|
81
|
+
style={[styles.username, { color: theme.colors.textSecondary }]}
|
|
82
|
+
>
|
|
83
|
+
@{username}
|
|
84
|
+
</Text>
|
|
85
|
+
) : null}
|
|
86
|
+
</View>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<View style={[cardStyle, style]} testID={testID}>
|
|
92
|
+
<View style={styles.header}>
|
|
93
|
+
{onPressProfile ? (
|
|
94
|
+
<Pressable
|
|
95
|
+
onPress={onPressProfile}
|
|
96
|
+
accessibilityRole="button"
|
|
97
|
+
accessibilityLabel={
|
|
98
|
+
username ? `${displayName} (@${username})` : displayName
|
|
99
|
+
}
|
|
100
|
+
style={styles.identityPressable}
|
|
101
|
+
>
|
|
102
|
+
{identity}
|
|
103
|
+
</Pressable>
|
|
104
|
+
) : (
|
|
105
|
+
<View style={styles.identityPressable}>{identity}</View>
|
|
106
|
+
)}
|
|
107
|
+
{action != null ? <View style={styles.action}>{action}</View> : null}
|
|
108
|
+
</View>
|
|
109
|
+
|
|
110
|
+
{bio ? (
|
|
111
|
+
<Text
|
|
112
|
+
numberOfLines={3}
|
|
113
|
+
style={[styles.bio, { color: theme.colors.text }]}
|
|
114
|
+
>
|
|
115
|
+
{bio}
|
|
116
|
+
</Text>
|
|
117
|
+
) : null}
|
|
118
|
+
|
|
119
|
+
{stats && stats.length > 0 ? (
|
|
120
|
+
<View style={styles.statsRow}>
|
|
121
|
+
{stats.map((stat) => (
|
|
122
|
+
<View key={stat.label} style={styles.stat}>
|
|
123
|
+
<Text style={[styles.statValue, { color: theme.colors.text }]}>
|
|
124
|
+
{stat.value}
|
|
125
|
+
</Text>
|
|
126
|
+
<Text
|
|
127
|
+
style={[styles.statLabel, { color: theme.colors.textSecondary }]}
|
|
128
|
+
>
|
|
129
|
+
{stat.label}
|
|
130
|
+
</Text>
|
|
131
|
+
</View>
|
|
132
|
+
))}
|
|
133
|
+
</View>
|
|
134
|
+
) : null}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const styles = StyleSheet.create({
|
|
140
|
+
header: {
|
|
141
|
+
flexDirection: 'row',
|
|
142
|
+
alignItems: 'flex-start',
|
|
143
|
+
justifyContent: 'space-between',
|
|
144
|
+
gap: space.sm,
|
|
145
|
+
},
|
|
146
|
+
identityPressable: {
|
|
147
|
+
flexShrink: 1,
|
|
148
|
+
flexGrow: 1,
|
|
149
|
+
},
|
|
150
|
+
identityRow: {
|
|
151
|
+
flexDirection: 'row',
|
|
152
|
+
alignItems: 'center',
|
|
153
|
+
gap: space.sm,
|
|
154
|
+
},
|
|
155
|
+
identityText: {
|
|
156
|
+
flexShrink: 1,
|
|
157
|
+
flexGrow: 1,
|
|
158
|
+
},
|
|
159
|
+
nameRow: {
|
|
160
|
+
flexDirection: 'row',
|
|
161
|
+
alignItems: 'center',
|
|
162
|
+
gap: space.xs,
|
|
163
|
+
},
|
|
164
|
+
displayName: {
|
|
165
|
+
fontSize: fontSize.lg,
|
|
166
|
+
fontWeight: '700',
|
|
167
|
+
flexShrink: 1,
|
|
168
|
+
},
|
|
169
|
+
verifiedBadge: {
|
|
170
|
+
flexShrink: 0,
|
|
171
|
+
},
|
|
172
|
+
username: {
|
|
173
|
+
fontSize: fontSize.sm,
|
|
174
|
+
marginTop: space._2xs,
|
|
175
|
+
},
|
|
176
|
+
action: {
|
|
177
|
+
flexShrink: 0,
|
|
178
|
+
},
|
|
179
|
+
bio: {
|
|
180
|
+
fontSize: fontSize.sm,
|
|
181
|
+
lineHeight: fontSize.sm * 1.4,
|
|
182
|
+
marginTop: space.md,
|
|
183
|
+
},
|
|
184
|
+
statsRow: {
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
gap: space.lg,
|
|
187
|
+
marginTop: space.md,
|
|
188
|
+
},
|
|
189
|
+
stat: {
|
|
190
|
+
flexDirection: 'row',
|
|
191
|
+
alignItems: 'baseline',
|
|
192
|
+
gap: space.xs,
|
|
193
|
+
},
|
|
194
|
+
statValue: {
|
|
195
|
+
fontSize: fontSize.sm,
|
|
196
|
+
fontWeight: '700',
|
|
197
|
+
},
|
|
198
|
+
statLabel: {
|
|
199
|
+
fontSize: fontSize.sm,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
export const UserHoverCard = memo(UserHoverCardComponent);
|
|
204
|
+
UserHoverCard.displayName = 'UserHoverCard';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
|
|
4
|
+
/** A single labeled metric rendered in the {@link UserHoverCardProps.stats} row. */
|
|
5
|
+
export interface UserHoverCardStat {
|
|
6
|
+
/** Human-readable label, e.g. `"Followers"`. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Pre-formatted value, e.g. `"1.2K"` or `42`. */
|
|
9
|
+
value: string | number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UserHoverCardProps {
|
|
13
|
+
/**
|
|
14
|
+
* Avatar image source — a URL string, or null/undefined to fall back to the
|
|
15
|
+
* deterministic name-based placeholder rendered by {@link Avatar}.
|
|
16
|
+
*/
|
|
17
|
+
avatar?: string | null;
|
|
18
|
+
/** Canonical, already-resolved display name (an API contract — render directly). */
|
|
19
|
+
displayName: string;
|
|
20
|
+
/** Handle without the leading `@` (the component prefixes it). */
|
|
21
|
+
username?: string;
|
|
22
|
+
/** Optional short biography; truncated to a few lines. */
|
|
23
|
+
bio?: string;
|
|
24
|
+
/** Optional metrics row (e.g. Following / Followers). */
|
|
25
|
+
stats?: UserHoverCardStat[];
|
|
26
|
+
/** Whether to show the verified badge next to the display name. */
|
|
27
|
+
verified?: boolean;
|
|
28
|
+
/** Makes the identity area (avatar + name + handle) pressable. */
|
|
29
|
+
onPressProfile?: () => void;
|
|
30
|
+
/**
|
|
31
|
+
* Slot for a consumer-injected action — typically the app's SDK FollowButton.
|
|
32
|
+
* Bloom does NOT implement follow logic; it only renders the card.
|
|
33
|
+
*/
|
|
34
|
+
action?: ReactNode;
|
|
35
|
+
/** Container style override. */
|
|
36
|
+
style?: StyleProp<ViewStyle>;
|
|
37
|
+
testID?: string;
|
|
38
|
+
}
|