@sproutsocial/seeds-react-avatar 1.0.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/.eslintignore ADDED
@@ -0,0 +1,6 @@
1
+ # Node modules
2
+ node_modules/
3
+
4
+ # Build output
5
+ dist/
6
+ coverage/
package/.eslintrc.js ADDED
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ["eslint-config-seeds/racine"],
4
+ };
@@ -0,0 +1,21 @@
1
+ yarn run v1.22.22
2
+ $ tsup --dts
3
+ CLI Building entry: src/index.ts
4
+ CLI Using tsconfig: tsconfig.json
5
+ CLI tsup v8.0.2
6
+ CLI Using tsup config: /home/runner/work/seeds/seeds/seeds-react/seeds-react-avatar/tsup.config.ts
7
+ CLI Target: es2022
8
+ CLI Cleaning output folder
9
+ CJS Build start
10
+ ESM Build start
11
+ CJS dist/index.js 5.61 KB
12
+ CJS dist/index.js.map 7.10 KB
13
+ CJS ⚡️ Build success in 69ms
14
+ ESM dist/esm/index.js 3.51 KB
15
+ ESM dist/esm/index.js.map 7.00 KB
16
+ ESM ⚡️ Build success in 75ms
17
+ DTS Build start
18
+ DTS ⚡️ Build success in 10367ms
19
+ DTS dist/index.d.ts 1.53 KB
20
+ DTS dist/index.d.mts 1.53 KB
21
+ Done in 12.80s.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # @sproutsocial/seeds-react-avatar
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - 426f572: Migrated Avatar and Image components into their own packages
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [426f572]
12
+ - @sproutsocial/seeds-react-image@1.0.0
@@ -0,0 +1,132 @@
1
+ // src/Avatar.tsx
2
+ import { useState, useCallback, useMemo, memo, useEffect } from "react";
3
+ import styled, { css } from "styled-components";
4
+ import Box from "@sproutsocial/seeds-react-box";
5
+ import Image from "@sproutsocial/seeds-react-image";
6
+ import Text from "@sproutsocial/seeds-react-text";
7
+ import { BORDER } from "@sproutsocial/seeds-react-system-props";
8
+ import { jsx } from "react/jsx-runtime";
9
+ var defaultType = "neutral";
10
+ var AvatarText = styled(({ fontSize, ...rest }) => /* @__PURE__ */ jsx(Text, { ...rest }))`
11
+ font-size: ${(props) => props.fontSize}px;
12
+ color: ${({ theme, type, color }) => color ? color : theme.colors.text.decorative[type]};
13
+ `;
14
+ var Container = styled(Box)`
15
+ ${({ theme, $type, bg, borderColor, $displayFallback }) => css`
16
+ background: ${$displayFallback ? bg ? bg : theme.colors.container.background.decorative[$type] : "none"};
17
+ border: ${$displayFallback ? `1px solid` : "none"};
18
+ border-color: ${borderColor ? borderColor : theme.colors.container.border.decorative[$type]};
19
+ color: ${theme.colors.text.decorative[$type]};
20
+ `}
21
+
22
+ ${BORDER}
23
+ `;
24
+ var getInitials = (name, fallback = "?") => {
25
+ if (!name || typeof name !== "string")
26
+ return fallback;
27
+ return name.replace(/\s+/, " ").split(" ").slice(0, 2).map((v) => v && v[0]?.toUpperCase()).join("");
28
+ };
29
+ var getAvatarColor = (name, type) => {
30
+ if (type !== "auto") {
31
+ return type;
32
+ }
33
+ const colors = [
34
+ "purple",
35
+ "green",
36
+ "blue",
37
+ "yellow",
38
+ "red",
39
+ "orange"
40
+ ];
41
+ const seed = name.split("").reduce((seed2, char) => {
42
+ return seed2 + char.charCodeAt(0);
43
+ }, 0);
44
+ return colors[seed % colors.length] || "neutral";
45
+ };
46
+ var Avatar = ({
47
+ appearance = "circle",
48
+ name = "",
49
+ src,
50
+ type = defaultType,
51
+ size = "40px",
52
+ bg,
53
+ color,
54
+ initials,
55
+ ...rest
56
+ }) => {
57
+ const colorType = getAvatarColor(name, type);
58
+ const [imageFailedLoading, setImageFailedLoading] = useState(false);
59
+ useEffect(() => {
60
+ setImageFailedLoading(false);
61
+ }, [src]);
62
+ const displayInitials = useMemo(
63
+ () => initials || getInitials(name),
64
+ [initials, name]
65
+ );
66
+ const handleError = useCallback(() => {
67
+ setImageFailedLoading(true);
68
+ }, [setImageFailedLoading]);
69
+ const fontSize = Math.floor(Number(size.replace("px", "")) * 0.4);
70
+ return /* @__PURE__ */ jsx(
71
+ Container,
72
+ {
73
+ size,
74
+ overflow: "hidden",
75
+ borderRadius: appearance === "leaf" ? "40% 0 40% 0" : "50%",
76
+ position: "relative",
77
+ display: "flex",
78
+ flexShrink: 0,
79
+ justifyContent: "center",
80
+ alignItems: "center",
81
+ title: name,
82
+ bg,
83
+ $type: colorType,
84
+ "data-qa-user-avatar": name,
85
+ $displayFallback: !src || imageFailedLoading,
86
+ ...rest,
87
+ children: !src || imageFailedLoading ? /* @__PURE__ */ jsx(
88
+ AvatarText,
89
+ {
90
+ lineHeight: size,
91
+ fontWeight: "semibold",
92
+ fontSize,
93
+ type: colorType,
94
+ color,
95
+ children: displayInitials
96
+ }
97
+ ) : /* @__PURE__ */ jsx(
98
+ Image,
99
+ {
100
+ alt: name,
101
+ width: "auto",
102
+ height: "100%",
103
+ src,
104
+ onError: handleError,
105
+ m: 0
106
+ }
107
+ )
108
+ }
109
+ );
110
+ };
111
+ var Avatar_default = memo(Avatar);
112
+
113
+ // src/constants.ts
114
+ var AvatarColorOptions = {
115
+ auto: "auto",
116
+ neutral: "neutral",
117
+ purple: "purple",
118
+ green: "green",
119
+ blue: "blue",
120
+ yellow: "yellow",
121
+ red: "red",
122
+ orange: "orange"
123
+ };
124
+
125
+ // src/index.ts
126
+ var src_default = Avatar_default;
127
+ export {
128
+ Avatar_default as Avatar,
129
+ AvatarColorOptions,
130
+ src_default as default
131
+ };
132
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/Avatar.tsx","../../src/constants.ts","../../src/index.ts"],"sourcesContent":["import { useState, useCallback, useMemo, memo, useEffect } from \"react\";\nimport styled, { css } from \"styled-components\";\n\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport Image from \"@sproutsocial/seeds-react-image\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport type {\n TypeAvatarProps,\n TypeAvatarContainerProps,\n TypeAvatarType,\n} from \"./AvatarTypes\";\n\nimport { BORDER } from \"@sproutsocial/seeds-react-system-props\";\n\nconst defaultType = \"neutral\";\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst AvatarText = styled(({ fontSize, ...rest }) => <Text {...rest} />)`\n font-size: ${(props) => props.fontSize}px;\n color: ${({ theme, type, color }) =>\n color ? color : theme.colors.text.decorative[type]};\n`;\n\nconst Container = styled(Box)<TypeAvatarContainerProps>`\n ${({ theme, $type, bg, borderColor, $displayFallback }) => css`\n background: ${$displayFallback\n ? bg\n ? bg\n : theme.colors.container.background.decorative[$type]\n : \"none\"};\n border: ${$displayFallback ? `1px solid` : \"none\"};\n border-color: ${borderColor\n ? borderColor\n : theme.colors.container.border.decorative[$type]};\n color: ${theme.colors.text.decorative[$type]};\n `}\n\n ${BORDER}\n`;\n\nconst getInitials = (name: string, fallback = \"?\"): string => {\n if (!name || typeof name !== \"string\") return fallback;\n return name\n .replace(/\\s+/, \" \")\n .split(\" \") // Repeated spaces results in empty strings\n .slice(0, 2)\n .map((v) => v && v[0]?.toUpperCase()) // Watch out for empty strings\n .join(\"\");\n};\n\nexport const getAvatarColor = (\n name: string,\n type: TypeAvatarType\n): TypeAvatarType => {\n if (type !== \"auto\") {\n return type;\n }\n\n const colors: Array<TypeAvatarType> = [\n \"purple\",\n \"green\",\n \"blue\",\n \"yellow\",\n \"red\",\n \"orange\",\n ];\n\n // Condense the avatar name down into a number\n const seed = name.split(\"\").reduce((seed, char) => {\n return seed + char.charCodeAt(0);\n }, 0);\n\n // Use that seed modulo the number of available colors to generate\n // a \"random\" color value which will always be consistent\n // for a given string. As a failsafe, return neutral (this should never happen).\n return colors[seed % colors.length] || \"neutral\";\n};\n\nexport const Avatar = ({\n appearance = \"circle\",\n name = \"\",\n src,\n type = defaultType,\n size = \"40px\",\n bg,\n color,\n initials,\n ...rest\n}: TypeAvatarProps) => {\n const colorType = getAvatarColor(name, type);\n const [imageFailedLoading, setImageFailedLoading] = useState(false);\n\n useEffect(() => {\n // If the src changes, we need to invalidate the image failed to load flag\n setImageFailedLoading(false);\n }, [src]);\n\n const displayInitials = useMemo(\n () => initials || getInitials(name),\n [initials, name]\n );\n const handleError = useCallback(() => {\n setImageFailedLoading(true);\n }, [setImageFailedLoading]);\n\n // Font size for initials is half the size of the avatar, rounded down.\n const fontSize = Math.floor(Number(size.replace(\"px\", \"\")) * 0.4);\n\n return (\n <Container\n size={size}\n overflow=\"hidden\"\n borderRadius={appearance === \"leaf\" ? \"40% 0 40% 0\" : \"50%\"}\n position=\"relative\"\n display=\"flex\"\n flexShrink={0}\n justifyContent=\"center\"\n alignItems=\"center\"\n title={name}\n bg={bg}\n $type={colorType}\n data-qa-user-avatar={name}\n $displayFallback={!src || imageFailedLoading}\n {...rest}\n >\n {!src || imageFailedLoading ? (\n <AvatarText\n lineHeight={size}\n fontWeight=\"semibold\"\n fontSize={fontSize}\n type={colorType}\n color={color}\n >\n {displayInitials}\n </AvatarText>\n ) : (\n <Image\n alt={name}\n width=\"auto\"\n height=\"100%\"\n src={src}\n onError={handleError}\n m={0}\n />\n )}\n </Container>\n );\n};\nexport default memo(Avatar);\n","export const AvatarColorOptions = {\n auto: \"auto\",\n neutral: \"neutral\",\n purple: \"purple\",\n green: \"green\",\n blue: \"blue\",\n yellow: \"yellow\",\n red: \"red\",\n orange: \"orange\",\n} as const;\n","import Avatar from \"./Avatar\";\n\nexport default Avatar;\nexport { Avatar };\nexport * from \"./AvatarTypes\";\nexport { AvatarColorOptions } from \"./constants\";\n"],"mappings":"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","names":["seed"]}
@@ -0,0 +1,37 @@
1
+ import * as react from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { TypeContainerProps } from '@sproutsocial/seeds-react-box';
4
+
5
+ declare const AvatarColorOptions: {
6
+ readonly auto: "auto";
7
+ readonly neutral: "neutral";
8
+ readonly purple: "purple";
9
+ readonly green: "green";
10
+ readonly blue: "blue";
11
+ readonly yellow: "yellow";
12
+ readonly red: "red";
13
+ readonly orange: "orange";
14
+ };
15
+
16
+ type TypeAvatarType = keyof typeof AvatarColorOptions;
17
+ interface TypeAvatarProps extends TypeContainerProps {
18
+ /** Circles are used for social profile avatars, leaf is for internal Sprout user avatars */
19
+ appearance?: "circle" | "leaf";
20
+ /** The name of the user that the avatar represents */
21
+ name?: string;
22
+ /** The initials of the user that the avatar represents.
23
+ * If not provided, the first letter of the first 1-2 words of the `name` prop will be used. */
24
+ initials?: string;
25
+ /** URL of the avatar image. If a URL is not provided, the component will fall back to showing the user's initials */
26
+ src?: string;
27
+ type?: TypeAvatarType;
28
+ size?: string;
29
+ }
30
+ interface TypeAvatarContainerProps extends TypeContainerProps {
31
+ $type: TypeAvatarType;
32
+ $displayFallback: boolean;
33
+ }
34
+
35
+ declare const _default: react.MemoExoticComponent<({ appearance, name, src, type, size, bg, color, initials, ...rest }: TypeAvatarProps) => react_jsx_runtime.JSX.Element>;
36
+
37
+ export { _default as Avatar, AvatarColorOptions, type TypeAvatarContainerProps, type TypeAvatarProps, type TypeAvatarType, _default as default };
@@ -0,0 +1,37 @@
1
+ import * as react from 'react';
2
+ import * as react_jsx_runtime from 'react/jsx-runtime';
3
+ import { TypeContainerProps } from '@sproutsocial/seeds-react-box';
4
+
5
+ declare const AvatarColorOptions: {
6
+ readonly auto: "auto";
7
+ readonly neutral: "neutral";
8
+ readonly purple: "purple";
9
+ readonly green: "green";
10
+ readonly blue: "blue";
11
+ readonly yellow: "yellow";
12
+ readonly red: "red";
13
+ readonly orange: "orange";
14
+ };
15
+
16
+ type TypeAvatarType = keyof typeof AvatarColorOptions;
17
+ interface TypeAvatarProps extends TypeContainerProps {
18
+ /** Circles are used for social profile avatars, leaf is for internal Sprout user avatars */
19
+ appearance?: "circle" | "leaf";
20
+ /** The name of the user that the avatar represents */
21
+ name?: string;
22
+ /** The initials of the user that the avatar represents.
23
+ * If not provided, the first letter of the first 1-2 words of the `name` prop will be used. */
24
+ initials?: string;
25
+ /** URL of the avatar image. If a URL is not provided, the component will fall back to showing the user's initials */
26
+ src?: string;
27
+ type?: TypeAvatarType;
28
+ size?: string;
29
+ }
30
+ interface TypeAvatarContainerProps extends TypeContainerProps {
31
+ $type: TypeAvatarType;
32
+ $displayFallback: boolean;
33
+ }
34
+
35
+ declare const _default: react.MemoExoticComponent<({ appearance, name, src, type, size, bg, color, initials, ...rest }: TypeAvatarProps) => react_jsx_runtime.JSX.Element>;
36
+
37
+ export { _default as Avatar, AvatarColorOptions, type TypeAvatarContainerProps, type TypeAvatarProps, type TypeAvatarType, _default as default };
package/dist/index.js ADDED
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var src_exports = {};
32
+ __export(src_exports, {
33
+ Avatar: () => Avatar_default,
34
+ AvatarColorOptions: () => AvatarColorOptions,
35
+ default: () => src_default
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+
39
+ // src/Avatar.tsx
40
+ var import_react = require("react");
41
+ var import_styled_components = __toESM(require("styled-components"));
42
+ var import_seeds_react_box = __toESM(require("@sproutsocial/seeds-react-box"));
43
+ var import_seeds_react_image = __toESM(require("@sproutsocial/seeds-react-image"));
44
+ var import_seeds_react_text = __toESM(require("@sproutsocial/seeds-react-text"));
45
+ var import_seeds_react_system_props = require("@sproutsocial/seeds-react-system-props");
46
+ var import_jsx_runtime = require("react/jsx-runtime");
47
+ var defaultType = "neutral";
48
+ var AvatarText = (0, import_styled_components.default)(({ fontSize, ...rest }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_seeds_react_text.default, { ...rest }))`
49
+ font-size: ${(props) => props.fontSize}px;
50
+ color: ${({ theme, type, color }) => color ? color : theme.colors.text.decorative[type]};
51
+ `;
52
+ var Container = (0, import_styled_components.default)(import_seeds_react_box.default)`
53
+ ${({ theme, $type, bg, borderColor, $displayFallback }) => import_styled_components.css`
54
+ background: ${$displayFallback ? bg ? bg : theme.colors.container.background.decorative[$type] : "none"};
55
+ border: ${$displayFallback ? `1px solid` : "none"};
56
+ border-color: ${borderColor ? borderColor : theme.colors.container.border.decorative[$type]};
57
+ color: ${theme.colors.text.decorative[$type]};
58
+ `}
59
+
60
+ ${import_seeds_react_system_props.BORDER}
61
+ `;
62
+ var getInitials = (name, fallback = "?") => {
63
+ if (!name || typeof name !== "string")
64
+ return fallback;
65
+ return name.replace(/\s+/, " ").split(" ").slice(0, 2).map((v) => v && v[0]?.toUpperCase()).join("");
66
+ };
67
+ var getAvatarColor = (name, type) => {
68
+ if (type !== "auto") {
69
+ return type;
70
+ }
71
+ const colors = [
72
+ "purple",
73
+ "green",
74
+ "blue",
75
+ "yellow",
76
+ "red",
77
+ "orange"
78
+ ];
79
+ const seed = name.split("").reduce((seed2, char) => {
80
+ return seed2 + char.charCodeAt(0);
81
+ }, 0);
82
+ return colors[seed % colors.length] || "neutral";
83
+ };
84
+ var Avatar = ({
85
+ appearance = "circle",
86
+ name = "",
87
+ src,
88
+ type = defaultType,
89
+ size = "40px",
90
+ bg,
91
+ color,
92
+ initials,
93
+ ...rest
94
+ }) => {
95
+ const colorType = getAvatarColor(name, type);
96
+ const [imageFailedLoading, setImageFailedLoading] = (0, import_react.useState)(false);
97
+ (0, import_react.useEffect)(() => {
98
+ setImageFailedLoading(false);
99
+ }, [src]);
100
+ const displayInitials = (0, import_react.useMemo)(
101
+ () => initials || getInitials(name),
102
+ [initials, name]
103
+ );
104
+ const handleError = (0, import_react.useCallback)(() => {
105
+ setImageFailedLoading(true);
106
+ }, [setImageFailedLoading]);
107
+ const fontSize = Math.floor(Number(size.replace("px", "")) * 0.4);
108
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
109
+ Container,
110
+ {
111
+ size,
112
+ overflow: "hidden",
113
+ borderRadius: appearance === "leaf" ? "40% 0 40% 0" : "50%",
114
+ position: "relative",
115
+ display: "flex",
116
+ flexShrink: 0,
117
+ justifyContent: "center",
118
+ alignItems: "center",
119
+ title: name,
120
+ bg,
121
+ $type: colorType,
122
+ "data-qa-user-avatar": name,
123
+ $displayFallback: !src || imageFailedLoading,
124
+ ...rest,
125
+ children: !src || imageFailedLoading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
126
+ AvatarText,
127
+ {
128
+ lineHeight: size,
129
+ fontWeight: "semibold",
130
+ fontSize,
131
+ type: colorType,
132
+ color,
133
+ children: displayInitials
134
+ }
135
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
136
+ import_seeds_react_image.default,
137
+ {
138
+ alt: name,
139
+ width: "auto",
140
+ height: "100%",
141
+ src,
142
+ onError: handleError,
143
+ m: 0
144
+ }
145
+ )
146
+ }
147
+ );
148
+ };
149
+ var Avatar_default = (0, import_react.memo)(Avatar);
150
+
151
+ // src/constants.ts
152
+ var AvatarColorOptions = {
153
+ auto: "auto",
154
+ neutral: "neutral",
155
+ purple: "purple",
156
+ green: "green",
157
+ blue: "blue",
158
+ yellow: "yellow",
159
+ red: "red",
160
+ orange: "orange"
161
+ };
162
+
163
+ // src/index.ts
164
+ var src_default = Avatar_default;
165
+ // Annotate the CommonJS export names for ESM import in node:
166
+ 0 && (module.exports = {
167
+ Avatar,
168
+ AvatarColorOptions
169
+ });
170
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/Avatar.tsx","../src/constants.ts"],"sourcesContent":["import Avatar from \"./Avatar\";\n\nexport default Avatar;\nexport { Avatar };\nexport * from \"./AvatarTypes\";\nexport { AvatarColorOptions } from \"./constants\";\n","import { useState, useCallback, useMemo, memo, useEffect } from \"react\";\nimport styled, { css } from \"styled-components\";\n\nimport Box from \"@sproutsocial/seeds-react-box\";\nimport Image from \"@sproutsocial/seeds-react-image\";\nimport Text from \"@sproutsocial/seeds-react-text\";\nimport type {\n TypeAvatarProps,\n TypeAvatarContainerProps,\n TypeAvatarType,\n} from \"./AvatarTypes\";\n\nimport { BORDER } from \"@sproutsocial/seeds-react-system-props\";\n\nconst defaultType = \"neutral\";\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst AvatarText = styled(({ fontSize, ...rest }) => <Text {...rest} />)`\n font-size: ${(props) => props.fontSize}px;\n color: ${({ theme, type, color }) =>\n color ? color : theme.colors.text.decorative[type]};\n`;\n\nconst Container = styled(Box)<TypeAvatarContainerProps>`\n ${({ theme, $type, bg, borderColor, $displayFallback }) => css`\n background: ${$displayFallback\n ? bg\n ? bg\n : theme.colors.container.background.decorative[$type]\n : \"none\"};\n border: ${$displayFallback ? `1px solid` : \"none\"};\n border-color: ${borderColor\n ? borderColor\n : theme.colors.container.border.decorative[$type]};\n color: ${theme.colors.text.decorative[$type]};\n `}\n\n ${BORDER}\n`;\n\nconst getInitials = (name: string, fallback = \"?\"): string => {\n if (!name || typeof name !== \"string\") return fallback;\n return name\n .replace(/\\s+/, \" \")\n .split(\" \") // Repeated spaces results in empty strings\n .slice(0, 2)\n .map((v) => v && v[0]?.toUpperCase()) // Watch out for empty strings\n .join(\"\");\n};\n\nexport const getAvatarColor = (\n name: string,\n type: TypeAvatarType\n): TypeAvatarType => {\n if (type !== \"auto\") {\n return type;\n }\n\n const colors: Array<TypeAvatarType> = [\n \"purple\",\n \"green\",\n \"blue\",\n \"yellow\",\n \"red\",\n \"orange\",\n ];\n\n // Condense the avatar name down into a number\n const seed = name.split(\"\").reduce((seed, char) => {\n return seed + char.charCodeAt(0);\n }, 0);\n\n // Use that seed modulo the number of available colors to generate\n // a \"random\" color value which will always be consistent\n // for a given string. As a failsafe, return neutral (this should never happen).\n return colors[seed % colors.length] || \"neutral\";\n};\n\nexport const Avatar = ({\n appearance = \"circle\",\n name = \"\",\n src,\n type = defaultType,\n size = \"40px\",\n bg,\n color,\n initials,\n ...rest\n}: TypeAvatarProps) => {\n const colorType = getAvatarColor(name, type);\n const [imageFailedLoading, setImageFailedLoading] = useState(false);\n\n useEffect(() => {\n // If the src changes, we need to invalidate the image failed to load flag\n setImageFailedLoading(false);\n }, [src]);\n\n const displayInitials = useMemo(\n () => initials || getInitials(name),\n [initials, name]\n );\n const handleError = useCallback(() => {\n setImageFailedLoading(true);\n }, [setImageFailedLoading]);\n\n // Font size for initials is half the size of the avatar, rounded down.\n const fontSize = Math.floor(Number(size.replace(\"px\", \"\")) * 0.4);\n\n return (\n <Container\n size={size}\n overflow=\"hidden\"\n borderRadius={appearance === \"leaf\" ? \"40% 0 40% 0\" : \"50%\"}\n position=\"relative\"\n display=\"flex\"\n flexShrink={0}\n justifyContent=\"center\"\n alignItems=\"center\"\n title={name}\n bg={bg}\n $type={colorType}\n data-qa-user-avatar={name}\n $displayFallback={!src || imageFailedLoading}\n {...rest}\n >\n {!src || imageFailedLoading ? (\n <AvatarText\n lineHeight={size}\n fontWeight=\"semibold\"\n fontSize={fontSize}\n type={colorType}\n color={color}\n >\n {displayInitials}\n </AvatarText>\n ) : (\n <Image\n alt={name}\n width=\"auto\"\n height=\"100%\"\n src={src}\n onError={handleError}\n m={0}\n />\n )}\n </Container>\n );\n};\nexport default memo(Avatar);\n","export const AvatarColorOptions = {\n auto: \"auto\",\n neutral: \"neutral\",\n purple: \"purple\",\n green: \"green\",\n blue: \"blue\",\n yellow: \"yellow\",\n red: \"red\",\n orange: \"orange\",\n} as const;\n"],"mappings":"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","names":["styled","Text","Box","seed","Image"]}
package/jest.config.js ADDED
@@ -0,0 +1,9 @@
1
+ const baseConfig = require("@sproutsocial/seeds-testing");
2
+
3
+ /** * @type {import('jest').Config} */
4
+ const config = {
5
+ ...baseConfig,
6
+ displayName: "seeds-react-avatar",
7
+ };
8
+
9
+ module.exports = config;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@sproutsocial/seeds-react-avatar",
3
+ "version": "1.0.0",
4
+ "description": "Seeds React Avatar",
5
+ "author": "Sprout Social, Inc.",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "module": "dist/esm/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "scripts": {
11
+ "build": "tsup --dts",
12
+ "build:debug": "tsup --dts --metafile",
13
+ "dev": "tsup --watch --dts",
14
+ "clean": "rm -rf .turbo dist",
15
+ "clean:modules": "rm -rf node_modules",
16
+ "typecheck": "tsc --noEmit",
17
+ "test": "jest",
18
+ "test:watch": "jest --watch --coverage=false"
19
+ },
20
+ "dependencies": {
21
+ "@sproutsocial/seeds-react-theme": "^*",
22
+ "@sproutsocial/seeds-react-system-props": "^*",
23
+ "@sproutsocial/seeds-react-box": "*",
24
+ "@sproutsocial/seeds-react-image": "*",
25
+ "@sproutsocial/seeds-react-text": "*"
26
+ },
27
+ "devDependencies": {
28
+ "@types/react": "^18.0.0",
29
+ "@types/styled-components": "^5.1.26",
30
+ "@sproutsocial/eslint-config-seeds": "*",
31
+ "react": "^18.0.0",
32
+ "styled-components": "^5.2.3",
33
+ "tsup": "^8.0.2",
34
+ "typescript": "^5.6.2",
35
+ "@sproutsocial/seeds-tsconfig": "*",
36
+ "@sproutsocial/seeds-testing": "*",
37
+ "@sproutsocial/seeds-react-testing-library": "*"
38
+ },
39
+ "peerDependencies": {
40
+ "styled-components": "^5.2.3"
41
+ },
42
+ "engines": {
43
+ "node": ">=18"
44
+ }
45
+ }
@@ -0,0 +1,55 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import Avatar from "./Avatar";
3
+
4
+ const meta: Meta<typeof Avatar> = {
5
+ title: "Components/Avatar",
6
+ component: Avatar,
7
+ };
8
+ export default meta;
9
+
10
+ type Story = StoryObj<typeof Avatar>;
11
+
12
+ export const Default: Story = {
13
+ args: {
14
+ name: "Kent C. Dodds",
15
+ },
16
+ };
17
+
18
+ export const CustomColors: Story = {
19
+ args: {
20
+ ...Default.args,
21
+ bg: "purple.500",
22
+ color: "red.200",
23
+ borderColor: "green",
24
+ },
25
+ };
26
+
27
+ export const WithImage: Story = {
28
+ args: {
29
+ ...Default.args,
30
+ src: "https://github.com/kentcdodds.png",
31
+ },
32
+ };
33
+
34
+ export const Leaf: Story = {
35
+ args: {
36
+ ...Default.args,
37
+ ...WithImage.args,
38
+ appearance: "leaf",
39
+ },
40
+ };
41
+
42
+ export const WithAutoColors: Story = {
43
+ args: {
44
+ type: "auto",
45
+ appearance: "leaf",
46
+ },
47
+ render: (args) => (
48
+ <>
49
+ <Avatar mt={400} name="Justyn Howard" {...args} />
50
+ <Avatar mt={400} name="Aaron Rankin" {...args} />
51
+ <Avatar mt={400} name="Gil Lara" {...args} />
52
+ <Avatar mt={400} name="Pete Soung" {...args} />
53
+ </>
54
+ ),
55
+ };
package/src/Avatar.tsx ADDED
@@ -0,0 +1,149 @@
1
+ import { useState, useCallback, useMemo, memo, useEffect } from "react";
2
+ import styled, { css } from "styled-components";
3
+
4
+ import Box from "@sproutsocial/seeds-react-box";
5
+ import Image from "@sproutsocial/seeds-react-image";
6
+ import Text from "@sproutsocial/seeds-react-text";
7
+ import type {
8
+ TypeAvatarProps,
9
+ TypeAvatarContainerProps,
10
+ TypeAvatarType,
11
+ } from "./AvatarTypes";
12
+
13
+ import { BORDER } from "@sproutsocial/seeds-react-system-props";
14
+
15
+ const defaultType = "neutral";
16
+
17
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
18
+ const AvatarText = styled(({ fontSize, ...rest }) => <Text {...rest} />)`
19
+ font-size: ${(props) => props.fontSize}px;
20
+ color: ${({ theme, type, color }) =>
21
+ color ? color : theme.colors.text.decorative[type]};
22
+ `;
23
+
24
+ const Container = styled(Box)<TypeAvatarContainerProps>`
25
+ ${({ theme, $type, bg, borderColor, $displayFallback }) => css`
26
+ background: ${$displayFallback
27
+ ? bg
28
+ ? bg
29
+ : theme.colors.container.background.decorative[$type]
30
+ : "none"};
31
+ border: ${$displayFallback ? `1px solid` : "none"};
32
+ border-color: ${borderColor
33
+ ? borderColor
34
+ : theme.colors.container.border.decorative[$type]};
35
+ color: ${theme.colors.text.decorative[$type]};
36
+ `}
37
+
38
+ ${BORDER}
39
+ `;
40
+
41
+ const getInitials = (name: string, fallback = "?"): string => {
42
+ if (!name || typeof name !== "string") return fallback;
43
+ return name
44
+ .replace(/\s+/, " ")
45
+ .split(" ") // Repeated spaces results in empty strings
46
+ .slice(0, 2)
47
+ .map((v) => v && v[0]?.toUpperCase()) // Watch out for empty strings
48
+ .join("");
49
+ };
50
+
51
+ export const getAvatarColor = (
52
+ name: string,
53
+ type: TypeAvatarType
54
+ ): TypeAvatarType => {
55
+ if (type !== "auto") {
56
+ return type;
57
+ }
58
+
59
+ const colors: Array<TypeAvatarType> = [
60
+ "purple",
61
+ "green",
62
+ "blue",
63
+ "yellow",
64
+ "red",
65
+ "orange",
66
+ ];
67
+
68
+ // Condense the avatar name down into a number
69
+ const seed = name.split("").reduce((seed, char) => {
70
+ return seed + char.charCodeAt(0);
71
+ }, 0);
72
+
73
+ // Use that seed modulo the number of available colors to generate
74
+ // a "random" color value which will always be consistent
75
+ // for a given string. As a failsafe, return neutral (this should never happen).
76
+ return colors[seed % colors.length] || "neutral";
77
+ };
78
+
79
+ export const Avatar = ({
80
+ appearance = "circle",
81
+ name = "",
82
+ src,
83
+ type = defaultType,
84
+ size = "40px",
85
+ bg,
86
+ color,
87
+ initials,
88
+ ...rest
89
+ }: TypeAvatarProps) => {
90
+ const colorType = getAvatarColor(name, type);
91
+ const [imageFailedLoading, setImageFailedLoading] = useState(false);
92
+
93
+ useEffect(() => {
94
+ // If the src changes, we need to invalidate the image failed to load flag
95
+ setImageFailedLoading(false);
96
+ }, [src]);
97
+
98
+ const displayInitials = useMemo(
99
+ () => initials || getInitials(name),
100
+ [initials, name]
101
+ );
102
+ const handleError = useCallback(() => {
103
+ setImageFailedLoading(true);
104
+ }, [setImageFailedLoading]);
105
+
106
+ // Font size for initials is half the size of the avatar, rounded down.
107
+ const fontSize = Math.floor(Number(size.replace("px", "")) * 0.4);
108
+
109
+ return (
110
+ <Container
111
+ size={size}
112
+ overflow="hidden"
113
+ borderRadius={appearance === "leaf" ? "40% 0 40% 0" : "50%"}
114
+ position="relative"
115
+ display="flex"
116
+ flexShrink={0}
117
+ justifyContent="center"
118
+ alignItems="center"
119
+ title={name}
120
+ bg={bg}
121
+ $type={colorType}
122
+ data-qa-user-avatar={name}
123
+ $displayFallback={!src || imageFailedLoading}
124
+ {...rest}
125
+ >
126
+ {!src || imageFailedLoading ? (
127
+ <AvatarText
128
+ lineHeight={size}
129
+ fontWeight="semibold"
130
+ fontSize={fontSize}
131
+ type={colorType}
132
+ color={color}
133
+ >
134
+ {displayInitials}
135
+ </AvatarText>
136
+ ) : (
137
+ <Image
138
+ alt={name}
139
+ width="auto"
140
+ height="100%"
141
+ src={src}
142
+ onError={handleError}
143
+ m={0}
144
+ />
145
+ )}
146
+ </Container>
147
+ );
148
+ };
149
+ export default memo(Avatar);
@@ -0,0 +1,26 @@
1
+ import type { TypeContainerProps as TypeBoxContainerProps } from "@sproutsocial/seeds-react-box";
2
+ import { AvatarColorOptions } from "./constants";
3
+
4
+ export type TypeAvatarType = keyof typeof AvatarColorOptions;
5
+
6
+ // @ts-note: Docs indicate that only common props are accepted, but the Avatar component uses a Box component. We may want to update the docs to accurately reflect the real functionality.
7
+ export interface TypeAvatarProps extends TypeBoxContainerProps {
8
+ /** Circles are used for social profile avatars, leaf is for internal Sprout user avatars */
9
+ appearance?: "circle" | "leaf";
10
+
11
+ /** The name of the user that the avatar represents */
12
+ name?: string;
13
+ /** The initials of the user that the avatar represents.
14
+ * If not provided, the first letter of the first 1-2 words of the `name` prop will be used. */
15
+ initials?: string;
16
+
17
+ /** URL of the avatar image. If a URL is not provided, the component will fall back to showing the user's initials */
18
+ src?: string;
19
+ type?: TypeAvatarType;
20
+ size?: string;
21
+ }
22
+
23
+ export interface TypeAvatarContainerProps extends TypeBoxContainerProps {
24
+ $type: TypeAvatarType;
25
+ $displayFallback: boolean;
26
+ }
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import Avatar from "../Avatar";
3
+
4
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
5
+ function AvatarTypes() {
6
+ const imgSrc = "https://github.com/kentcdodds.png";
7
+ return (
8
+ <>
9
+ <Avatar name="" />
10
+ <Avatar name="John Doe" />
11
+ <Avatar name="John Doe" src="" />
12
+ <Avatar name="John Doe" src={imgSrc} />
13
+ <Avatar name="John Doe" appearance="leaf" />
14
+ <Avatar name="John Doe" size="100px" />
15
+ <Avatar name="John Doe" type="blue" />
16
+ <Avatar name="John Doe" initials="JDD" />
17
+ {/* @ts-expect-error - test that invalid type is rejected */}
18
+ <Avatar name="John Doe" type="invalid" />
19
+ {/* @ts-expect-error - test that invalid src is rejected */}
20
+ <Avatar name="John Doe" src={123} />
21
+ <Avatar
22
+ name="John Doe"
23
+ // @ts-expect-error - test that invalid appearance is rejected
24
+ appearance="invalid"
25
+ />
26
+ <Avatar
27
+ name="John Doe"
28
+ // @ts-expect-error - test that invalid size is rejected
29
+ size={123}
30
+ />
31
+ </>
32
+ );
33
+ }
@@ -0,0 +1,161 @@
1
+ import { useState } from "react";
2
+ import {
3
+ render,
4
+ screen,
5
+ fireEvent,
6
+ act,
7
+ } from "@sproutsocial/seeds-react-testing-library";
8
+ import Avatar from "../Avatar";
9
+
10
+ // This top level describe should be used to describe the component and what we are testing
11
+ describe("Avatar/features", () => {
12
+ // Each describe should describe an intentional feature of the component
13
+
14
+ describe("When an image is provided", () => {
15
+ // Each test should describe the expected behavior of the feature and its fallbacks
16
+ it("should render an image correctly", () => {
17
+ const imgSrc = "https://github.com/kentcdodds.png";
18
+ render(<Avatar name="John Doe" src={imgSrc} />);
19
+ const image = screen.getByAltText("John Doe");
20
+ expect(image).toHaveAttribute("src", imgSrc);
21
+ });
22
+
23
+ it("should not render initials", () => {
24
+ const imgSrc = "https://github.com/kentcdodds.png";
25
+ render(<Avatar name="John Doe" src={imgSrc} />);
26
+ const initials = screen.queryByText("JD");
27
+ expect(initials).not.toBeInTheDocument();
28
+ });
29
+
30
+ it("should render initials when image fails to load", () => {
31
+ render(<Avatar name="John Doe" src="" />);
32
+ // TODO: figure out a better way to mock image failures...
33
+ const image = screen.queryByRole("img");
34
+ const initials = screen.getByText("JD");
35
+ expect(image).not.toBeInTheDocument();
36
+ expect(initials).toBeInTheDocument();
37
+ });
38
+
39
+ it("should render custom initials when provided + image fails to load", () => {
40
+ render(<Avatar name="John Doe" src="" initials="JRD" />);
41
+ const image = screen.queryByRole("img");
42
+ const initials = screen.getByText("JRD");
43
+ const defaultInitials = screen.queryByText("JD");
44
+ expect(image).not.toBeInTheDocument();
45
+ expect(initials).toBeInTheDocument();
46
+ expect(defaultInitials).not.toBeInTheDocument();
47
+ });
48
+
49
+ it("should render as circle by default", () => {
50
+ render(<Avatar name="John Doe" />);
51
+ const avatar = screen.getByTitle("John Doe");
52
+ expect(avatar).toHaveStyleRule("border-radius", "50%");
53
+ });
54
+
55
+ it('should render as a leaf when appearance="leaf" ', () => {
56
+ render(<Avatar name="John Doe" appearance="leaf" />);
57
+ const avatar = screen.getByTitle("John Doe");
58
+ expect(avatar).toHaveStyleRule("border-radius", "40% 0 40% 0");
59
+ });
60
+
61
+ it("should render 40px by default", () => {
62
+ render(<Avatar name="John Doe" />);
63
+ const avatar = screen.getByTitle("John Doe");
64
+ expect(avatar).toHaveStyleRule("width", "40px");
65
+ expect(avatar).toHaveStyleRule("height", "40px");
66
+ });
67
+
68
+ it("should render an image correctly after an image fails to load", async () => {
69
+ const imgSrc =
70
+ "https://static01.nyt.com/images/2022/10/24/arts/24taylor-notebook3/24taylor-notebook3-superJumbo.jpg";
71
+ const name = "Taylor Swift";
72
+ const buttonName = "My Button";
73
+ const TestComponent = () => {
74
+ const [srcString, setSrcString] = useState("mybadimageurl.coco");
75
+ return (
76
+ <div>
77
+ <Avatar name={name} src={srcString} />
78
+ <button onClick={() => setSrcString(imgSrc)}>{buttonName}</button>
79
+ </div>
80
+ );
81
+ };
82
+ const { user } = render(<TestComponent />);
83
+
84
+ const firstImage = screen.getByDataQaLabel({ image: name });
85
+ fireEvent.error(firstImage);
86
+ const initials = await screen.findByText("TS");
87
+ expect(initials).toBeInTheDocument();
88
+
89
+ const myButton = screen.getByText(buttonName);
90
+ await act(async () => {
91
+ await user.click(myButton);
92
+ });
93
+ const secondImage = screen.getByAltText("Taylor Swift");
94
+ expect(secondImage).toHaveAttribute("src", imgSrc);
95
+ });
96
+ });
97
+
98
+ describe("When no image is provided", () => {
99
+ it("should render initials correctly", () => {
100
+ render(<Avatar name="John Doe" />);
101
+ const initials = screen.getByText("JD");
102
+ expect(initials).toBeInTheDocument();
103
+ });
104
+
105
+ it("should render single initial if only 1 name given", () => {
106
+ render(<Avatar name="John" />);
107
+ const singleInitial = screen.getByText("J");
108
+ expect(singleInitial).toBeInTheDocument();
109
+ });
110
+
111
+ it("should display first 2 initials if >3 names are provided", () => {
112
+ render(<Avatar name="John Doe Smith" />);
113
+ const initials = screen.getByText("JD");
114
+ expect(initials).toBeInTheDocument();
115
+ });
116
+
117
+ it('should render a "?" as a fallback if no name is provided', () => {
118
+ render(<Avatar name="" />);
119
+ const fallback = screen.getByText("?");
120
+ const initials = screen.queryByText("JD");
121
+ expect(fallback).toBeInTheDocument();
122
+ expect(initials).not.toBeInTheDocument();
123
+ });
124
+
125
+ it("should render custom initials even if no name is provided", () => {
126
+ render(<Avatar name="" initials="SU" />);
127
+ const fallback = screen.queryByText("?");
128
+ const initials = screen.getByText("SU");
129
+ expect(fallback).not.toBeInTheDocument();
130
+ expect(initials).toBeInTheDocument();
131
+ });
132
+ });
133
+
134
+ describe("When a user wants to customize Avatar", () => {
135
+ it("should be able to the size of Avatar", () => {
136
+ render(<Avatar name="John Doe" size="100px" />);
137
+ const avatar = screen.getByTitle("John Doe");
138
+ expect(avatar).toHaveStyleRule("width", "100px");
139
+ expect(avatar).toHaveStyleRule("height", "100px");
140
+ });
141
+
142
+ // Should we even test stuff like this? What's the best way for us to test intentional overrides we provide that are not a default behavior?
143
+ it("should render different color background when type prop is used", () => {
144
+ render(<Avatar name="John Doe" type="blue" />);
145
+ const avatar = screen.getByTitle("John Doe");
146
+ expect(avatar).toHaveStyleRule("background", `#deebfe`);
147
+ });
148
+ });
149
+
150
+ it("should allow border thickness to changed to 2px", () => {
151
+ render(<Avatar name="John Doe" border={600} />);
152
+ const avatar = screen.getByTitle("John Doe");
153
+ expect(avatar).toHaveStyleRule("border", "2px solid");
154
+ });
155
+
156
+ it("should allow border thickness to changed to 3px", () => {
157
+ render(<Avatar name="John Doe" border={700} />);
158
+ const avatar = screen.getByTitle("John Doe");
159
+ expect(avatar).toHaveStyleRule("border", "3px solid");
160
+ });
161
+ });
@@ -0,0 +1,10 @@
1
+ export const AvatarColorOptions = {
2
+ auto: "auto",
3
+ neutral: "neutral",
4
+ purple: "purple",
5
+ green: "green",
6
+ blue: "blue",
7
+ yellow: "yellow",
8
+ red: "red",
9
+ orange: "orange",
10
+ } as const;
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import Avatar from "./Avatar";
2
+
3
+ export default Avatar;
4
+ export { Avatar };
5
+ export * from "./AvatarTypes";
6
+ export { AvatarColorOptions } from "./constants";
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@sproutsocial/seeds-tsconfig/bundler/dom/library-monorepo",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "module": "esnext"
6
+ },
7
+ "include": ["src/**/*"],
8
+ "exclude": ["node_modules", "dist", "coverage"]
9
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "tsup";
2
+
3
+ export default defineConfig((options) => ({
4
+ entry: ["src/index.ts"],
5
+ format: ["cjs", "esm"],
6
+ clean: true,
7
+ legacyOutput: true,
8
+ dts: options.dts,
9
+ external: ["react"],
10
+ sourcemap: true,
11
+ metafile: options.metafile,
12
+ }));