Skip to content

Commit 1e5876f

Browse files
committed
feat(ui-avatar,shared-types): add new ai variant
INSTUI-4538
1 parent 58d311c commit 1e5876f

File tree

6 files changed

+178
-55
lines changed

6 files changed

+178
-55
lines changed

packages/shared-types/src/ComponentThemeVariables.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,21 +66,24 @@ export type AlertTheme = {
6666
}
6767

6868
export type AvatarTheme = {
69-
background: Colors['contrasts']['white1010']
69+
background: string
7070
borderWidthSmall: Border['widthSmall']
7171
borderWidthMedium: Border['widthMedium']
72-
borderColor: Colors['contrasts']['grey1214']
72+
borderColor: string
7373
boxShadowColor: string
7474
boxShadowBlur: string
7575
fontFamily: Typography['fontFamily']
7676
fontWeight: Typography['fontWeightBold']
77-
color: Colors['contrasts']['blue4570']
78-
colorShamrock: Colors['contrasts']['green4570']
79-
colorBarney: Colors['contrasts']['blue4570']
80-
colorCrimson: Colors['contrasts']['orange4570']
81-
colorFire: Colors['contrasts']['red4570']
82-
colorLicorice: Colors['contrasts']['grey125125']
83-
colorAsh: Colors['contrasts']['grey4570']
77+
color: string
78+
colorShamrock: string
79+
colorBarney: string
80+
colorCrimson: string
81+
colorFire: string
82+
colorLicorice: string
83+
colorAsh: string
84+
85+
aiTopGradientColor: string
86+
aiBottomGradientColor: string
8487
}
8588

8689
export type BadgeTheme = {

packages/ui-avatar/src/Avatar/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,34 @@ describes: Avatar
44

55
The avatar component can be used to display a user's avatar. When an image src is not supplied the user's initials will display.
66

7+
## Variant
8+
9+
Avatar has a variant prop, which currently only toggles it between `ai` and `default` behavior. `ai` is a preset where you can only change the `size` and `margin` visual props, all others are preset. In the following example, there are the `ai` variants.
10+
11+
### ai
12+
13+
```js
14+
---
15+
type: example
16+
readonly: true
17+
---
18+
<div>
19+
<View display="block" padding="small medium">
20+
<Avatar variant="ai" size="xx-small" margin="0 small 0 0" />
21+
<Avatar variant="ai" size="x-small" margin="0 small 0 0" />
22+
<Avatar variant="ai" size="small" margin="0 small 0 0" />
23+
<Avatar variant="ai" size="medium" margin="0 small 0 0" />
24+
<Avatar variant="ai" size="large" margin="0 small 0 0" />
25+
<Avatar variant="ai" size="x-large" margin="0 small 0 0" />
26+
<Avatar variant="ai" size="xx-large" />
27+
</View>
28+
</div>
29+
```
30+
31+
### default
32+
33+
Most avatar's features are accessible through the `default` variant
34+
735
Instead of the initials, an SVG icon can be displayed with the `renderIcon` property.
836

937
The avatar can be `circle` _(default)_ or `rectangle`. Use the `margin` prop to add space between Avatar and other content.

packages/ui-avatar/src/Avatar/index.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from 'react'
3333

3434
import { View } from '@instructure/ui-view'
35+
import { IconAiSolid } from '@instructure/ui-icons'
3536
import { callRenderProp, passthroughProps } from '@instructure/ui-react-utils'
3637
import type { AvatarProps } from './props'
3738

@@ -52,7 +53,8 @@ const Avatar = forwardRef(
5253
showBorder = 'auto',
5354
shape = 'circle',
5455
display = 'inline-block',
55-
onImageLoaded = (_event: SyntheticEvent) => {},
56+
variant = 'default',
57+
onImageLoaded = (_event: SyntheticEvent) => { },
5658
src,
5759
name,
5860
renderIcon,
@@ -78,7 +80,8 @@ const Avatar = forwardRef(
7880
shape,
7981
src,
8082
showBorder,
81-
themeOverride
83+
themeOverride,
84+
variant
8285
},
8386
componentId: 'Avatar',
8487
displayName: 'Avatar'
@@ -122,11 +125,13 @@ const Avatar = forwardRef(
122125
}
123126

124127
const renderContent = () => {
125-
if (!renderIcon) {
128+
const calcedRenderIcon = variant === 'ai' ? <IconAiSolid /> : renderIcon
129+
130+
if (!calcedRenderIcon) {
126131
return renderInitials()
127132
}
128133

129-
return <span css={styles?.iconSVG}>{callRenderProp(renderIcon)}</span>
134+
return <span css={styles?.iconSVG}>{callRenderProp(calcedRenderIcon)}</span>
130135
}
131136

132137
return (

packages/ui-avatar/src/Avatar/props.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ type AvatarOwnProps = {
100100
* An icon, or function that returns an icon that gets displayed. If the `src` prop is provided, `src` will have priority.
101101
*/
102102
renderIcon?: Renderable
103+
104+
/**
105+
* if "ai", it will ignore most props (e.g.: shape, showBorder, onImageLoaded, hasInverseColor, color, name) and render an "ai avatar"
106+
*/
107+
variant?: 'default' | 'ai'
103108
}
104109

105110
export type AvatarState = {
@@ -149,7 +154,8 @@ const propTypes: PropValidators<PropKeys> = {
149154
onImageLoaded: PropTypes.func,
150155
as: PropTypes.elementType,
151156
elementRef: PropTypes.func,
152-
renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func])
157+
renderIcon: PropTypes.oneOfType([PropTypes.node, PropTypes.func]),
158+
variant: PropTypes.oneOf(['default', 'ai'])
153159
}
154160

155161
const allowedProps: AllowedPropKeys = [

packages/ui-avatar/src/Avatar/styles.ts

Lines changed: 118 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type StyleParams = {
3434
src: AvatarProps['src']
3535
showBorder: AvatarProps['showBorder']
3636
themeOverride: AvatarProps['themeOverride']
37+
variant: AvatarProps['variant']
3738
}
3839
/**
3940
* ---
@@ -48,47 +49,108 @@ const generateStyle = (
4849
componentTheme: AvatarTheme,
4950
params: StyleParams
5051
): AvatarStyle => {
51-
const { loaded, size, color, hasInverseColor, shape, src, showBorder } =
52-
params
52+
const {
53+
loaded,
54+
size,
55+
color,
56+
hasInverseColor,
57+
shape,
58+
src,
59+
showBorder,
60+
variant
61+
} = params
62+
63+
// NOTE: this is needed due to design changes. The size of the component is calculated from "em" which means it is
64+
// tied to the fontSize. The font sizes changed for the icons, which meant that the container (component) size would've
65+
// changed too without additional calculations
66+
const calcNewScaler = (
67+
originalFontSize: number,
68+
newFontSize: number,
69+
originalScaler: number
70+
) => {
71+
return `${(originalFontSize * originalScaler) / newFontSize}em`
72+
}
5373

5474
const sizeStyles = {
5575
auto: {
5676
fontSize: 'inherit',
57-
borderWidth: componentTheme.borderWidthSmall
77+
borderWidth: componentTheme.borderWidthSmall,
78+
width: '2.5em',
79+
height: '2.5em'
5880
},
5981
'xx-small': {
60-
fontSize: '0.5rem',
61-
borderWidth: componentTheme.borderWidthSmall
82+
fontSize: '0.625rem',
83+
borderWidth: componentTheme.borderWidthSmall,
84+
width: calcNewScaler(0.5, 0.625, shape === 'circle' ? 2.5 : 3),
85+
height: calcNewScaler(0.5, 0.625, 2.5)
6286
},
6387
'x-small': {
64-
fontSize: '0.75rem',
65-
borderWidth: componentTheme.borderWidthSmall
88+
fontSize: '0.875rem',
89+
borderWidth: componentTheme.borderWidthSmall,
90+
width: calcNewScaler(0.75, 0.875, shape === 'circle' ? 2.5 : 3),
91+
height: calcNewScaler(0.75, 0.875, 2.5)
6692
},
6793
small: {
68-
fontSize: '1rem',
69-
borderWidth: componentTheme.borderWidthSmall
94+
fontSize: '1.25rem',
95+
borderWidth: componentTheme.borderWidthSmall,
96+
width: calcNewScaler(1, 1.25, shape === 'circle' ? 2.5 : 3),
97+
height: calcNewScaler(1, 1.25, 2.5)
7098
},
7199
medium: {
72-
fontSize: '1.25rem',
73-
borderWidth: componentTheme.borderWidthMedium
100+
fontSize: '1.5rem',
101+
borderWidth: componentTheme.borderWidthMedium,
102+
width: calcNewScaler(1.25, 1.5, shape === 'circle' ? 2.5 : 3),
103+
height: calcNewScaler(1.25, 1.5, 2.5)
74104
},
75105
large: {
76-
fontSize: '1.5rem',
77-
borderWidth: componentTheme.borderWidthMedium
106+
fontSize: '1.75rem',
107+
borderWidth: componentTheme.borderWidthMedium,
108+
width: calcNewScaler(1.5, 1.75, shape === 'circle' ? 2.5 : 3),
109+
height: calcNewScaler(1.5, 1.75, 2.5)
78110
},
79111
'x-large': {
80-
fontSize: '1.75rem',
81-
borderWidth: componentTheme.borderWidthMedium
112+
fontSize: '2rem',
113+
borderWidth: componentTheme.borderWidthMedium,
114+
width: calcNewScaler(1.75, 2, shape === 'circle' ? 2.5 : 3),
115+
height: calcNewScaler(1.75, 2, 2.5)
82116
},
83117
'xx-large': {
84-
fontSize: '2rem',
85-
borderWidth: componentTheme.borderWidthMedium
118+
fontSize: '2.25rem',
119+
borderWidth: componentTheme.borderWidthMedium,
120+
width: calcNewScaler(2, 2.25, shape === 'circle' ? 2.5 : 3),
121+
height: calcNewScaler(2, 2.25, 2.5)
86122
}
87123
}
88124

89-
const variantStyles = {
125+
const initialSizeStyles = {
126+
auto: {
127+
fontSize: 'inherit'
128+
},
129+
'xx-small': {
130+
fontSize: '0.5rem'
131+
},
132+
'x-small': {
133+
fontSize: '0.75rem'
134+
},
135+
small: {
136+
fontSize: '1rem'
137+
},
138+
medium: {
139+
fontSize: '1.25rem'
140+
},
141+
large: {
142+
fontSize: '1.5rem'
143+
},
144+
'x-large': {
145+
fontSize: '1.75rem'
146+
},
147+
'xx-large': {
148+
fontSize: '2rem'
149+
}
150+
}
151+
152+
const shapeStyles = {
90153
circle: {
91-
width: '2.5em',
92154
position: 'relative',
93155
borderRadius: '100%',
94156
overflow: 'hidden'
@@ -112,27 +174,23 @@ const generateStyle = (
112174
? colorVariants[color!]
113175
: componentTheme.background
114176

115-
const contentColor = hasInverseColor
116-
? componentTheme.background
117-
: colorVariants[color!]
177+
const contentColor =
178+
variant === 'ai'
179+
? 'white'
180+
: hasInverseColor
181+
? componentTheme.background
182+
: colorVariants[color!]
118183

119-
return {
120-
avatar: {
121-
label: 'avatar',
122-
height: '2.5em',
123-
boxSizing: 'border-box',
124-
backgroundColor: backgroundColor,
125-
backgroundPosition: 'center',
126-
backgroundSize: 'cover',
127-
backgroundClip: 'content-box',
128-
backgroundRepeat: 'no-repeat',
129-
overflow: 'hidden',
130-
lineHeight: 0,
131-
textAlign: 'center',
132-
borderStyle: 'solid',
133-
borderColor: componentTheme.borderColor,
134-
...sizeStyles[size!],
135-
...variantStyles[shape!],
184+
const variantStyles = {
185+
ai: {
186+
background: `
187+
linear-gradient(to bottom, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) padding-box,
188+
linear-gradient(to bottom right, ${componentTheme.aiTopGradientColor} 0%, ${componentTheme.aiBottomGradientColor} 100%) border-box`,
189+
border: 'solid transparent',
190+
...shapeStyles['circle']
191+
},
192+
default: {
193+
...shapeStyles[shape!],
136194
...(loaded
137195
? {
138196
backgroundImage: `url('${src}')`,
@@ -152,14 +210,34 @@ const generateStyle = (
152210
...(showBorder === 'never' && {
153211
border: 0
154212
})
213+
}
214+
}
215+
216+
return {
217+
avatar: {
218+
label: 'avatar',
219+
boxSizing: 'border-box',
220+
backgroundColor: backgroundColor,
221+
backgroundPosition: 'center',
222+
backgroundSize: 'cover',
223+
backgroundClip: 'content-box',
224+
backgroundRepeat: 'no-repeat',
225+
overflow: 'hidden',
226+
lineHeight: 0,
227+
textAlign: 'center',
228+
borderStyle: 'solid',
229+
borderColor: componentTheme.borderColor,
230+
...sizeStyles[size!],
231+
...variantStyles[variant!]
155232
},
156233
initials: {
157234
label: 'avatar__initials',
158235
color: contentColor,
159236
lineHeight: '2.375em',
160237
fontFamily: componentTheme.fontFamily,
161238
fontWeight: componentTheme.fontWeight,
162-
letterSpacing: '0.0313em'
239+
letterSpacing: '0.0313em',
240+
...initialSizeStyles[size!]
163241
},
164242
loadImage: {
165243
label: 'avatar__loadImage',

packages/ui-avatar/src/Avatar/theme.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ const generateComponentTheme = (theme: Theme): AvatarTheme => {
5252
colorCrimson: colors?.contrasts?.red4570,
5353
colorFire: colors?.contrasts?.orange4570,
5454
colorLicorice: colors?.contrasts?.grey125125,
55-
colorAsh: colors?.contrasts?.grey4570
55+
colorAsh: colors?.contrasts?.grey4570,
56+
57+
aiTopGradientColor: colors?.contrasts?.violet4570,
58+
aiBottomGradientColor: colors?.contrasts?.sea4570
5659
}
5760

5861
return {

0 commit comments

Comments
 (0)