@sproutsocial/seeds-react-profile 0.1.3 → 0.3.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.
@@ -0,0 +1,186 @@
1
+ import styled from "styled-components";
2
+ import { PartnerLogo } from "@sproutsocial/seeds-react-partner-logo";
3
+ import { Avatar } from "@sproutsocial/seeds-react-avatar";
4
+ import {
5
+ Card,
6
+ CardHeader,
7
+ CardContent,
8
+ CardFooter,
9
+ } from "@sproutsocial/seeds-react-card";
10
+ import { Icon } from "@sproutsocial/seeds-react-icon";
11
+ import { Link } from "@sproutsocial/seeds-react-link";
12
+ import { Text } from "@sproutsocial/seeds-react-text";
13
+ import type { TypeProfileCardProps } from "./types";
14
+ import { VerifiedProfileIcon } from "./VerifiedProfileIcon";
15
+
16
+ // Styled Components
17
+ const StyledProfileCard = styled(Card)`
18
+ max-width: 300px;
19
+ `;
20
+
21
+ const StyledCardHeader = styled(CardHeader)<{
22
+ $bannerColor?: string;
23
+ $partnerName?: string;
24
+ }>`
25
+ position: relative;
26
+ height: 80px;
27
+ padding: ${(props) => props.theme.space[400]}
28
+ ${(props) => props.theme.space[400]} 0 ${(props) => props.theme.space[400]};
29
+ margin-bottom: 0;
30
+ color: ${(props) => props.theme.colors.text.inverse};
31
+ border-radius: ${(props) => props.theme.radii[500]}
32
+ ${(props) => props.theme.radii[500]} 0 0;
33
+ ${(props) => {
34
+ // Use explicit bannerColor if provided
35
+ if (props.$bannerColor) {
36
+ return `background: ${props.$bannerColor};`;
37
+ }
38
+ // Fall back to theme.colors.network[partnerName] if partnerName is available
39
+ if (props.$partnerName && props.theme.colors.network) {
40
+ const networkColor =
41
+ props.theme.colors.network[
42
+ props.$partnerName as keyof typeof props.theme.colors.network
43
+ ];
44
+ return networkColor ? `background: ${networkColor};` : "";
45
+ }
46
+ return "";
47
+ }}
48
+ `;
49
+
50
+ const BannerContent = styled.div`
51
+ display: flex;
52
+ justify-content: flex-end;
53
+ width: 100%;
54
+ `;
55
+
56
+ const ActionsSection = styled.div`
57
+ display: flex;
58
+ justify-content: flex-end;
59
+ gap: ${(props) => props.theme.space[100]};
60
+ padding: 0 ${(props) => props.theme.space[300]};
61
+
62
+ &:empty {
63
+ min-height: 44px;
64
+ }
65
+ `;
66
+
67
+ const StyledCardContent = styled(CardContent)`
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: ${(props) => props.theme.space[300]};
71
+ padding: 0 ${(props) => props.theme.space[400]}
72
+ ${(props) => props.theme.space[400]};
73
+ `;
74
+
75
+ const ProfileNameSection = styled.div`
76
+ display: flex;
77
+ align-items: center;
78
+ gap: ${(props) => props.theme.space[200]};
79
+ `;
80
+
81
+ const InfoRow = styled.div`
82
+ display: flex;
83
+ align-items: center;
84
+ gap: ${(props) => props.theme.space[300]};
85
+ `;
86
+
87
+ /**
88
+ * A flexible profile component that can display user profiles in various formats
89
+ * across different social networks with consistent styling and behavior.
90
+ */
91
+ export const ProfileCard = ({
92
+ partnerName,
93
+ name,
94
+ secondaryName,
95
+ avatarUrl,
96
+ subtext,
97
+ description,
98
+ verificationType,
99
+ profileUrl,
100
+ location,
101
+ bannerColor,
102
+ bannerContent,
103
+ metadata,
104
+ profileActions,
105
+ footer,
106
+ cardProps,
107
+ }: TypeProfileCardProps) => {
108
+ return (
109
+ <StyledProfileCard role="presentation" {...cardProps}>
110
+ <StyledCardHeader $bannerColor={bannerColor} $partnerName={partnerName}>
111
+ <Avatar src={avatarUrl} name={name} size="60px" mt={450} />
112
+ {bannerContent && <BannerContent>{bannerContent}</BannerContent>}
113
+ </StyledCardHeader>
114
+
115
+ <ActionsSection>{profileActions}</ActionsSection>
116
+
117
+ <StyledCardContent>
118
+ <ProfileNameSection>
119
+ {partnerName && (
120
+ <PartnerLogo
121
+ partnerName={partnerName}
122
+ aria-label={`${partnerName} profile`}
123
+ size="small"
124
+ />
125
+ )}
126
+ <Text.SubHeadline as="h2">{name}</Text.SubHeadline>
127
+ </ProfileNameSection>
128
+
129
+ {subtext && (
130
+ <Text fontSize={200} color="text.subtext">
131
+ {subtext}
132
+ </Text>
133
+ )}
134
+
135
+ {secondaryName && (
136
+ <InfoRow>
137
+ {profileUrl ? (
138
+ <Link href={profileUrl} external>
139
+ <Text fontSize={200}>{secondaryName}</Text>
140
+ <Icon
141
+ name="arrow-right-up-outline"
142
+ aria-hidden
143
+ ml={100}
144
+ size="small"
145
+ />
146
+ </Link>
147
+ ) : (
148
+ <Text.SmallByline color="text.subtext">
149
+ {secondaryName}
150
+ </Text.SmallByline>
151
+ )}
152
+ {verificationType && (
153
+ <VerifiedProfileIcon
154
+ partnerName={partnerName}
155
+ verificationType={verificationType}
156
+ />
157
+ )}
158
+ </InfoRow>
159
+ )}
160
+
161
+ {description && <Text fontSize={200}>{description}</Text>}
162
+
163
+ {location && (
164
+ <InfoRow>
165
+ <Icon name="location-pin-outline" aria-hidden size="small" />
166
+ <Text fontSize={200}>{location}</Text>
167
+ </InfoRow>
168
+ )}
169
+
170
+ {metadata && metadata.length > 0 && (
171
+ <InfoRow>
172
+ {metadata.map((info, index) => (
173
+ <Text fontSize={200} key={index}>
174
+ {info}
175
+ </Text>
176
+ ))}
177
+ </InfoRow>
178
+ )}
179
+ </StyledCardContent>
180
+
181
+ {footer && <CardFooter>{footer}</CardFooter>}
182
+ </StyledProfileCard>
183
+ );
184
+ };
185
+
186
+ export default ProfileCard;
@@ -1,14 +1,5 @@
1
- import React from "react";
2
1
  import type { Meta, StoryObj } from "@storybook/react";
3
2
  import { ProfileToken } from "./ProfileToken";
4
- import { Icon } from "@sproutsocial/seeds-react-icon";
5
-
6
- const avatarUrl =
7
- "https://d672eyudr6aq1.cloudfront.net/avatar/cede1373e17c05542b1cc60f427067f2?s=30&amp;d=404";
8
-
9
- const VerificationBadge = () => (
10
- <Icon name="check-solid" style={{ color: "#1da1f2" }} />
11
- );
12
3
 
13
4
  const meta: Meta<typeof ProfileToken> = {
14
5
  title: "Components/Profile/ProfileToken",
@@ -17,7 +8,25 @@ const meta: Meta<typeof ProfileToken> = {
17
8
  layout: "centered",
18
9
  },
19
10
  tags: ["autodocs"],
20
- argTypes: {},
11
+ argTypes: {
12
+ partnerName: {
13
+ control: { type: "select" },
14
+ options: [
15
+ "twitter",
16
+ "facebook",
17
+ "instagram",
18
+ "linkedin",
19
+ "youtube",
20
+ "tiktok",
21
+ "threads",
22
+ "bluesky",
23
+ ],
24
+ },
25
+ verificationType: {
26
+ control: { type: "select" },
27
+ options: ["verified", "blue_verified", "gray_verified", "not_verified"],
28
+ },
29
+ },
21
30
  };
22
31
 
23
32
  export default meta;
@@ -26,8 +35,6 @@ type Story = StoryObj<typeof meta>;
26
35
  export const Default: Story = {
27
36
  args: {
28
37
  name: "John Doe",
29
- secondaryName: "@johndoe",
30
- img: avatarUrl,
31
38
  partnerName: "twitter",
32
39
  },
33
40
  };
@@ -35,18 +42,14 @@ export const Default: Story = {
35
42
  export const WithVerification: Story = {
36
43
  args: {
37
44
  name: "Elon Musk",
38
- secondaryName: "@elonmusk",
39
- img: avatarUrl,
40
45
  partnerName: "twitter",
41
- children: <VerificationBadge />,
46
+ verificationType: "verified",
42
47
  },
43
48
  };
44
49
 
45
50
  export const FacebookUser: Story = {
46
51
  args: {
47
52
  name: "Facebook User",
48
- secondaryName: "/facebookuser",
49
- img: avatarUrl,
50
53
  partnerName: "facebook",
51
54
  },
52
55
  };
@@ -54,8 +57,6 @@ export const FacebookUser: Story = {
54
57
  export const InstagramUser: Story = {
55
58
  args: {
56
59
  name: "Instagram User",
57
- secondaryName: "@instagramuser",
58
- img: avatarUrl,
59
60
  partnerName: "instagram",
60
61
  },
61
62
  };
@@ -63,8 +64,6 @@ export const InstagramUser: Story = {
63
64
  export const LinkedInUser: Story = {
64
65
  args: {
65
66
  name: "LinkedIn User",
66
- secondaryName: "linkedinuser",
67
- img: avatarUrl,
68
67
  partnerName: "linkedin",
69
68
  },
70
69
  };
@@ -72,8 +71,6 @@ export const LinkedInUser: Story = {
72
71
  export const YouTubeUser: Story = {
73
72
  args: {
74
73
  name: "YouTube Creator",
75
- secondaryName: "@youtubecreator",
76
- img: avatarUrl,
77
74
  partnerName: "youtube",
78
75
  },
79
76
  };
@@ -81,8 +78,6 @@ export const YouTubeUser: Story = {
81
78
  export const TikTokUser: Story = {
82
79
  args: {
83
80
  name: "TikTok Creator",
84
- secondaryName: "@tiktokcreator",
85
- img: avatarUrl,
86
81
  partnerName: "tiktok",
87
82
  },
88
83
  };
@@ -90,8 +85,6 @@ export const TikTokUser: Story = {
90
85
  export const ThreadsUser: Story = {
91
86
  args: {
92
87
  name: "Threads User",
93
- secondaryName: "@threadsuser",
94
- img: avatarUrl,
95
88
  partnerName: "threads",
96
89
  },
97
90
  };
@@ -99,59 +92,27 @@ export const ThreadsUser: Story = {
99
92
  export const BlueskyUser: Story = {
100
93
  args: {
101
94
  name: "Bluesky User",
102
- secondaryName: "@blueskyuser",
103
- img: avatarUrl,
104
95
  partnerName: "bluesky",
105
96
  },
106
97
  };
107
98
 
108
- export const WithoutAvatar: Story = {
99
+ export const BlueVerifiedUser: Story = {
109
100
  args: {
110
- name: "No Avatar User",
111
- secondaryName: "@noavat,ar",
101
+ name: "Blue Verified User",
112
102
  partnerName: "twitter",
103
+ verificationType: "blue_verified",
113
104
  },
114
105
  };
115
106
 
116
107
  export const DifferentNetworks: Story = {
117
108
  render: () => (
118
109
  <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
119
- <ProfileToken
120
- name="Twitter User"
121
- secondaryName="@twitteruser"
122
- img={avatarUrl}
123
- partnerName="twitter"
124
- />
125
- <ProfileToken
126
- name="Facebook User"
127
- secondaryName="/facebookuser"
128
- img={avatarUrl}
129
- partnerName="facebook"
130
- />
131
- <ProfileToken
132
- name="Instagram User"
133
- secondaryName="@instagramuser"
134
- img={avatarUrl}
135
- partnerName="instagram"
136
- />
137
- <ProfileToken
138
- name="LinkedIn User"
139
- secondaryName="linkedinuser"
140
- img={avatarUrl}
141
- partnerName="linkedin"
142
- />
143
- <ProfileToken
144
- name="YouTube Creator"
145
- secondaryName="@youtubecreator"
146
- img={avatarUrl}
147
- partnerName="youtube"
148
- />
149
- <ProfileToken
150
- name="TikTok Creator"
151
- secondaryName="@tiktokcreator"
152
- img={avatarUrl}
153
- partnerName="tiktok"
154
- />
110
+ <ProfileToken name="Twitter User" partnerName="twitter" />
111
+ <ProfileToken name="Facebook User" partnerName="facebook" />
112
+ <ProfileToken name="Instagram User" partnerName="instagram" />
113
+ <ProfileToken name="LinkedIn User" partnerName="linkedin" />
114
+ <ProfileToken name="YouTube Creator" partnerName="youtube" />
115
+ <ProfileToken name="TikTok Creator" partnerName="tiktok" />
155
116
  </div>
156
117
  ),
157
118
  };
@@ -161,42 +122,39 @@ export const TokenVariations: Story = {
161
122
  <div style={{ display: "flex", flexWrap: "wrap", gap: "8px" }}>
162
123
  <ProfileToken
163
124
  name="Verified User"
164
- secondaryName="@verified"
165
- img={avatarUrl}
166
- partnerName="twitter"
167
- >
168
- <VerificationBadge />
169
- </ProfileToken>
170
- <ProfileToken
171
- name="Regular User"
172
- secondaryName="@regular"
173
- img={avatarUrl}
174
125
  partnerName="twitter"
126
+ verificationType="verified"
175
127
  />
176
- <ProfileToken
177
- name="Facebook User"
178
- secondaryName="/facebookuser"
179
- img={avatarUrl}
180
- partnerName="facebook"
181
- />
182
- <ProfileToken
183
- name="Instagram User"
184
- secondaryName="@instagramuser"
185
- img={avatarUrl}
186
- partnerName="instagram"
187
- />
188
- <ProfileToken
189
- name="LinkedIn User"
190
- secondaryName="linkedinuser"
191
- img={avatarUrl}
192
- partnerName="linkedin"
193
- />
194
- <ProfileToken
195
- name="YouTube Creator"
196
- secondaryName="@youtubecreator"
197
- img={avatarUrl}
198
- partnerName="youtube"
199
- />
128
+ <ProfileToken name="Regular User" partnerName="twitter" />
129
+ <ProfileToken name="Facebook User" partnerName="facebook" />
130
+ <ProfileToken name="Instagram User" partnerName="instagram" />
131
+ <ProfileToken name="LinkedIn User" partnerName="linkedin" />
132
+ <ProfileToken name="YouTube Creator" partnerName="youtube" />
133
+ </div>
134
+ ),
135
+ };
136
+
137
+ export const LongNamesTruncation: Story = {
138
+ render: () => (
139
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
140
+ <div
141
+ style={{ maxWidth: "250px", border: "1px solid #ccc", padding: "8px" }}
142
+ >
143
+ <ProfileToken
144
+ name="A Very Long User Name That Should Truncate"
145
+ partnerName="twitter"
146
+ />
147
+ </div>
148
+ <div
149
+ style={{ maxWidth: "180px", border: "1px solid #ccc", padding: "8px" }}
150
+ >
151
+ <ProfileToken name="Another Long Name Token" partnerName="facebook" />
152
+ </div>
153
+ <div
154
+ style={{ maxWidth: "120px", border: "1px solid #ccc", padding: "8px" }}
155
+ >
156
+ <ProfileToken name="Very Narrow Token" partnerName="instagram" />
157
+ </div>
200
158
  </div>
201
159
  ),
202
160
  };
@@ -1,20 +1,40 @@
1
- import React from "react";
1
+ import styled from "styled-components";
2
2
  import { Token } from "@sproutsocial/seeds-react-token";
3
3
  import { InlineProfile } from "./InlineProfile";
4
4
  import type { ProfileTokenProps } from "./types";
5
5
 
6
+ const StyledProfileToken = styled(Token)`
7
+ box-sizing: border-box;
8
+ display: inline-flex;
9
+ max-width: 100%;
10
+ vertical-align: middle;
11
+ padding: ${(props) => props.theme.space[100]}
12
+ ${(props) => props.theme.space[200]};
13
+ margin: ${(props) => props.theme.space[100]} 0;
14
+ `;
15
+
6
16
  /**
7
- * A ProfileToken component that wraps CompactProfile in a Seeds Token component.
17
+ * A ProfileToken component that wraps InlineProfile in a Seeds Token component.
8
18
  * This token is not closable and provides a compact way to display profile information inline.
19
+ *
20
+ * ProfileToken enforces design standards by only showing the profile name and network logo.
21
+ * It does not support avatars, secondary names (handles), or custom avatar sizes.
22
+ * Use InlineProfile directly if you need more flexibility.
9
23
  */
10
- export const ProfileToken: React.FC<ProfileTokenProps> = ({
11
- tokenProps,
24
+ export const ProfileToken = ({
12
25
  className,
26
+ onClick,
27
+ tokenProps,
13
28
  ...props
14
- }) => (
15
- <Token className={className} closeable={false} {...tokenProps}>
16
- <InlineProfile {...props} />
17
- </Token>
29
+ }: ProfileTokenProps) => (
30
+ <StyledProfileToken
31
+ className={className}
32
+ closeable={false}
33
+ onClick={onClick}
34
+ {...tokenProps}
35
+ >
36
+ <InlineProfile size="small" {...props} />
37
+ </StyledProfileToken>
18
38
  );
19
39
 
20
40
  export default ProfileToken;
@@ -0,0 +1,64 @@
1
+ import { Icon } from "@sproutsocial/seeds-react-icon";
2
+ import type { TypeProfileNetwork, TypeProfileVerification } from "./types";
3
+
4
+ // TODO: Move these to a more appropriate location
5
+ const VERIFICATION_COLORS = {
6
+ twitter: "#000000",
7
+ facebook: "#1877F2",
8
+ x: "#000000",
9
+ linkedin: "#0A66C2",
10
+ instagram: "#e4405f",
11
+ gray: "#515e5f",
12
+ } as const;
13
+
14
+ /**
15
+ * Gets the verification color based on partnerName and verification type
16
+ */
17
+ export const getVerificationColor = (
18
+ partnerName?: TypeProfileNetwork,
19
+ verificationType?: TypeProfileVerification
20
+ ): string => {
21
+ if (!verificationType || verificationType === "not_verified") return "";
22
+
23
+ switch (verificationType) {
24
+ case "blue_verified":
25
+ // Use partner-specific blue color
26
+ return partnerName === "facebook"
27
+ ? VERIFICATION_COLORS.facebook
28
+ : VERIFICATION_COLORS.twitter;
29
+ case "gray_verified":
30
+ return VERIFICATION_COLORS.gray;
31
+ case "verified":
32
+ default:
33
+ // Default to partner color or Twitter blue
34
+ if (partnerName && partnerName in VERIFICATION_COLORS) {
35
+ return VERIFICATION_COLORS[
36
+ partnerName as keyof typeof VERIFICATION_COLORS
37
+ ];
38
+ }
39
+ return VERIFICATION_COLORS.twitter;
40
+ }
41
+ };
42
+
43
+ export interface TypeVerifiedProfileIconProps {
44
+ partnerName?: TypeProfileNetwork;
45
+ verificationType: TypeProfileVerification;
46
+ }
47
+
48
+ export const VerifiedProfileIcon = ({
49
+ partnerName,
50
+ verificationType,
51
+ }: TypeVerifiedProfileIconProps) => {
52
+ if (!verificationType || verificationType === "not_verified") return;
53
+
54
+ const verificationColor = getVerificationColor(partnerName, verificationType);
55
+ return (
56
+ <Icon
57
+ title="Verified"
58
+ aria-label="Verified account"
59
+ color={verificationColor}
60
+ name="verified"
61
+ ml="150"
62
+ />
63
+ );
64
+ };
@@ -1,4 +1,3 @@
1
- import React from "react";
2
1
  import { render, screen } from "@sproutsocial/seeds-react-testing-library";
3
2
  import { InlineProfile } from "../InlineProfile";
4
3
 
@@ -9,7 +8,7 @@ describe("InlineProfile", () => {
9
8
  name="John Doe"
10
9
  secondaryName="@johndoe"
11
10
  partnerName="twitter"
12
- img="https://example.com/avatar.jpg"
11
+ avatarUrl="https://example.com/avatar.jpg"
13
12
  />
14
13
  );
15
14
 
@@ -17,21 +16,6 @@ describe("InlineProfile", () => {
17
16
  expect(screen.getByText("@johndoe")).toBeInTheDocument();
18
17
  });
19
18
 
20
- it("renders with children", () => {
21
- render(
22
- <InlineProfile
23
- name="John Doe"
24
- secondaryName="@johndoe"
25
- partnerName="twitter"
26
- img="https://example.com/avatar.jpg"
27
- >
28
- Children
29
- </InlineProfile>
30
- );
31
-
32
- expect(screen.getByText("Children")).toBeInTheDocument();
33
- });
34
-
35
19
  it("renders network logo", () => {
36
20
  render(
37
21
  <InlineProfile
@@ -39,7 +23,7 @@ describe("InlineProfile", () => {
39
23
  secondaryName="@johndoe"
40
24
  partnerName="twitter"
41
25
  partnerLogoLabel="Twitter Logo"
42
- img="https://example.com/avatar.jpg"
26
+ avatarUrl="https://example.com/avatar.jpg"
43
27
  />
44
28
  );
45
29
 
@@ -52,7 +36,7 @@ describe("InlineProfile", () => {
52
36
  <InlineProfile
53
37
  name="John Doe"
54
38
  secondaryName="@johndoe"
55
- img="https://example.com/avatar.jpg"
39
+ avatarUrl="https://example.com/avatar.jpg"
56
40
  />
57
41
  );
58
42
 
@@ -66,7 +50,7 @@ describe("InlineProfile", () => {
66
50
  <InlineProfile
67
51
  name="John Doe"
68
52
  partnerName="twitter"
69
- img="https://example.com/avatar.jpg"
53
+ avatarUrl="https://example.com/avatar.jpg"
70
54
  />
71
55
  );
72
56
 
@@ -74,10 +58,26 @@ describe("InlineProfile", () => {
74
58
  expect(screen.queryByText(/@/)).not.toBeInTheDocument();
75
59
  });
76
60
 
77
- it("renders avatar with name fallback when img prop is not provided", () => {
61
+ it("hides avatar when avatarUrl prop is not provided", () => {
78
62
  render(<InlineProfile name="John Doe" />);
79
63
 
80
- const avatar = screen.getByText("JD");
81
- expect(avatar).toBeInTheDocument();
64
+ // Avatar should not be rendered when no URL provided
65
+ expect(screen.queryByText("JD")).not.toBeInTheDocument();
66
+ // But name should still be visible
67
+ expect(screen.getByText("John Doe")).toBeInTheDocument();
68
+ });
69
+
70
+ it("renders with verification badge when verified", () => {
71
+ render(
72
+ <InlineProfile
73
+ name="John Doe"
74
+ secondaryName="@johndoe"
75
+ partnerName="twitter"
76
+ avatarUrl="https://example.com/avatar.jpg"
77
+ verificationType="verified"
78
+ />
79
+ );
80
+
81
+ expect(screen.getByLabelText("Verified account")).toBeInTheDocument();
82
82
  });
83
83
  });