Skip to content

Commit

Permalink
react-ui: Update Avatar styles to match figma design
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinwhereby committed Jun 6, 2024
1 parent 75ca4d7 commit 2d9c9b2
Show file tree
Hide file tree
Showing 9 changed files with 278 additions and 147 deletions.
1 change: 1 addition & 0 deletions packages/react-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"dependencies": {
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"react": "^18.2.0",
Expand Down
69 changes: 54 additions & 15 deletions packages/react-ui/src/Avatar/__tests__/avatar.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from "react";

import { Avatar } from "../index";
import { Avatar, AvatarWrapper, AvatarImage } from "../index";
import { CSSObject } from "@emotion/react";

Check failure on line 4 in packages/react-ui/src/Avatar/__tests__/avatar.stories.tsx

View workflow job for this annotation

GitHub Actions / Test

'CSSObject' is defined but never used

const randomAvatar = "https://avatar.iran.liara.run/public";
const randomAvatar = "https://api.dicebear.com/8.x/adventurer/svg?backgroundColor=b6e3f4,c0aede,d1d4f9";

const names = [
"Atle Antonsen",
Expand Down Expand Up @@ -31,35 +32,73 @@ export default {
title: "Avatar",
};

export const Default = () => <Avatar />;
export const Default = {
render: ({ size, withAvatarUrlSet, name }: { size: number; withAvatarUrlSet: boolean; name?: string }) => (
<Avatar size={size} avatarUrl={withAvatarUrlSet ? randomAvatar : undefined} name={name} />
),
argTypes: {
size: { control: "range", min: 0, max: 200 },
withAvatarUrlSet: { control: "boolean" },
name: { control: "text" },
},
args: { size: 40, withAvatarUrlSet: false, name: names[0] },
};

export const WithAvatarUrlSet = () => <Avatar avatarUrl={randomAvatar} name={"John Doe"} />;
WithAvatarUrlSet.storyName = "with avatarUrl set";

export const WithAvatarUrlSetSquare = () => <Avatar avatarUrl={randomAvatar} name={"John Doe"} variant={"square"} />;
WithAvatarUrlSetSquare.storyName = "with avatarUrl set - square";

export const WithAvatarNameSetSquare = () => <Avatar variant={"square"} name={"John Doe"} />;
WithAvatarNameSetSquare.storyName = "with name set - square";

export const WithNameSetRound = () => <Avatar variant={"round"} name={"John Doe"} />;
WithNameSetRound.storyName = "with name set - round";

export const WithNameSetOutline = () => <Avatar variant={"outline"} name={"John Doe"} />;
WithNameSetOutline.storyName = "with name set - outline";
export const WithAvatarNameSet = () => <Avatar name={"John Doe"} />;
WithAvatarNameSet.storyName = "with name set";

export const WhenSizeIs60 = () => <Avatar avatarUrl={randomAvatar} size={60} name={"John Doe"} />;
WhenSizeIs60.storyName = "when size is 60";

export const WhenSizeIs200 = () => <Avatar avatarUrl={randomAvatar} size={200} name={"John Doe"} />;
WhenSizeIs200.storyName = "when size is 200";

export const WithCustomStyling = {
render: ({ name, withAvatarUrlSet }: { name?: string; withAvatarUrlSet: boolean }) => {
const customCss = `
.avatarWrapper {
width: 150px;
height: 150px;
border-radius: 50% 25%
}
.avatarImage {
fill: red;
background-color: blue;
}
`;
return (
<>
<style>{customCss}</style>
<AvatarWrapper name={name} className={"avatarWrapper"}>
<AvatarImage
avatarUrl={withAvatarUrlSet ? randomAvatar : undefined}
name={name}
className={"avatarImage"}
/>
</AvatarWrapper>
</>
);
},
name: "with custom styling",
argTypes: {
withAvatarUrlSet: { control: "boolean" },
name: { control: "text" },
},
args: {
withAvatarUrlSet: false,
name: names[0],
},
};

export const WithNameSet = () => (
<>
{([32, 36, 40, 60, 80] as const).map((size) =>
names.map((name) => (
<div key={size + name} style={{ display: "inline-block", margin: 4 }}>
<Avatar name={name} size={size} variant={"square"} />
<Avatar name={name} size={size} />
</div>
)),
)}
Expand Down
130 changes: 78 additions & 52 deletions packages/react-ui/src/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as React from "react";
import React, { PropsWithChildren } from "react";

import { css } from "@emotion/react";
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import runes from "runes";
import { borderRadius, colors } from "../theme";
import { useTheme } from "../theme";
import { ProfileIcon } from "../icons";

export const getInitialsFromName = (name = "") => {
name = name.trim();
Expand All @@ -29,73 +30,98 @@ const Initials = ({ name }: { name?: string }) => {

return (
<svg viewBox={"-60 -60 120 120"} aria-hidden={"true"} css={initialsStyles}>
<text x={0} y={0} textAnchor={"middle"} dominantBaseline={"central"} fontSize={fontSize}>
<text
fill="currentcolor"
x={0}
y={0}
textAnchor={"middle"}
dominantBaseline={"central"}
fontSize={fontSize}
>
{initialsStr}
</text>
</svg>
);
};

const avatarStyles = css({
display: "flex",
position: "relative",
objectFit: "cover",
maxWidth: "initial",
flexShrink: 0,
overflow: "hidden",
});

const variantStyles = {
outline: css({
boxShadow: "inset 0 0 0 1px rgba(0, 0, 0, 0.12)",
}),
round: css({
borderRadius: "50%",
}),
square: css({
borderRadius: borderRadius.extraSmall,
}),
};

const imageStyles = css({
height: "100%",
width: "100%",
maxWidth: "initial",
objectFit: "cover",
});

const fallbackStyles = css({
backgroundColor: colors.backgroundYellow,
overflow: "hidden",
userSelect: "none",
});

interface AvatarProps {
type AvatarWrapperProps = PropsWithChildren<{
avatarUrl?: string;
className?: string;
variant?: "outline" | "round" | "square";
name?: string;
size?: 32 | 36 | 40 | 60 | 80 | 200;
}
size?: number;
}>;

function Avatar({ avatarUrl, className, size = 40, name, variant = "round", ...rest }: AvatarProps) {
function AvatarWrapper({ className, size = 40, name, children }: AvatarWrapperProps) {
return (
<AvatarPrimitive.Root
css={[avatarStyles, variantStyles[variant]]}
className={className}
title={name}
style={{
css={{
display: "flex",
position: "relative",
objectFit: "cover",
maxWidth: "initial",
flexShrink: 0,
overflow: "hidden",
height: `${size}px`,
width: `${size}px`,
borderRadius: "15%",
}}
{...rest}
className={className}
title={name}
>
<AvatarPrimitive.Image css={imageStyles} src={avatarUrl} alt={name} />
<AvatarPrimitive.Fallback css={fallbackStyles}>
<Initials name={name} />
</AvatarPrimitive.Fallback>
{children}
</AvatarPrimitive.Root>
);
}

export { Avatar };
interface AvatarImageProps {
className?: string;
name?: string;
avatarUrl?: string;
}

function AvatarImage({ className, name, avatarUrl }: AvatarImageProps) {
const theme = useTheme();
return (
<>
<AvatarPrimitive.Image
css={{
height: "100%",
width: "100%",
maxWidth: "initial",
objectFit: "cover",
}}
src={avatarUrl}
alt={name}
className={className}
/>
<AvatarPrimitive.Fallback
css={{
height: "100%",
width: "100%",
backgroundColor: theme.color.brand1000,
color: theme.color.brand300,
overflow: "hidden",
userSelect: "none",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
className={className}
>
{name ? <Initials name={name} /> : <ProfileIcon css={{ height: "50%", width: "50%" }} />}
</AvatarPrimitive.Fallback>
</>
);
}

function Avatar({ className, name, avatarUrl, size }: AvatarWrapperProps & AvatarImageProps) {
return (
<div>
<AvatarWrapper size={size} name={name} className={className}>
<AvatarImage avatarUrl={avatarUrl} name={name} />
</AvatarWrapper>
</div>
);
}

export { Avatar, AvatarImage, AvatarWrapper };
77 changes: 8 additions & 69 deletions packages/react-ui/src/icons/base-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +1,20 @@
import * as React from "react";

import { css } from "@emotion/react";
import { colors } from "../theme";

const style = css({
fill: "currentColor",
});

const sizeStyles = {
small: css({
width: "16px",
height: "16px",
}),
medium: css({
width: "24px",
height: "24px",
}),
};

const variantStyles = {
dark: css({
fill: colors.black,
}),
darkGrey: css({
fill: colors.greyExtraDark,
}),
midGrey: css({
fill: colors.grey,
}),
grey: css({
fill: colors.greyExtraLight,
}),
light: css({
fill: "#fff",
}),
lightBlue: css({
fill: colors.illustrationBlue,
}),
lightGreen: css({
fill: colors.illustrationGreen,
}),
primary: css({
fill: colors.primary,
}),
secondary: css({
fill: colors.secondary,
}),
negative: css({
fill: colors.negative,
}),
meetingRed: css({
fill: colors.meetingRed,
}),
};

export interface IconProps extends React.SVGProps<SVGSVGElement> {
size?: "small" | "medium";
variant?:
| "dark"
| "darkGrey"
| "midGrey"
| "grey"
| "light"
| "lightBlue"
| "lightGreen"
| "primary"
| "secondary"
| "negative"
| "meetingRed";
size?: number;
}

function BaseIcon({ children, size = "medium", variant = "primary", ...props }: IconProps) {
function BaseIcon({ children, size = 24, ...props }: IconProps) {
return (
<svg
xmlns={"http://www.w3.org/2000/svg"}
viewBox={"0 0 24 24"}
css={[style, sizeStyles[size], variantStyles[variant]]}
css={{
fill: "currentcolor",
stroke: "currentcolor",
height: size,
width: size,
}}
{...props}
>
{children}
Expand Down
1 change: 1 addition & 0 deletions packages/react-ui/src/icons/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Ellipsis as EllipsisIcon } from "./ellipsis";
export { MaximizeOn as MaximizeOnIcon } from "./maximize-on";
export { Spotlight as SpotlightIcon } from "./spotlight";
export { Profile as ProfileIcon } from "./profile";
17 changes: 17 additions & 0 deletions packages/react-ui/src/icons/profile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as React from "react";

import { BaseIcon, IconProps } from "./base-icon";

function Profile(props: IconProps) {
return (
<BaseIcon {...props} css={{ fill: "none" }}>
<path
d="M3.23779 19.5C4.5632 17.2892 7.46807 15.7762 12.0001 15.7762C16.5321 15.7762 19.4369 17.2892 20.7623 19.5M15.6001 8.1C15.6001 10.0882 13.9883 11.7 12.0001 11.7C10.0118 11.7 8.40007 10.0882 8.40007 8.1C8.40007 6.11177 10.0118 4.5 12.0001 4.5C13.9883 4.5 15.6001 6.11177 15.6001 8.1Z"
stroke-width="2"
stroke-linecap="round"
/>
</BaseIcon>
);
}

export { Profile };
4 changes: 2 additions & 2 deletions packages/react-ui/src/icons/stories/icons.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const icons = Object.entries(Icons)
return 0;
});

function IconGrid({ variant, size, light }: IconProps & { light?: boolean }) {
function IconGrid({ size, light }: IconProps & { light?: boolean }) {
return (
<div css={[iconGridStyles]}>
{icons.map(({ name, component: Component }) => (
Expand All @@ -63,7 +63,7 @@ function IconGrid({ variant, size, light }: IconProps & { light?: boolean }) {
: undefined
}
>
<Component variant={variant} size={size} />
<Component size={size} />
</IconCell>
))}
</div>
Expand Down
Loading

0 comments on commit 2d9c9b2

Please sign in to comment.