Skip to content

Commit

Permalink
Badge in avatars (#988)
Browse files Browse the repository at this point in the history
* Display Badge in avatars

* avatar size

* update badge
  • Loading branch information
dani-moreno authored Dec 19, 2024
1 parent 83616fa commit 646cf35
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 25 deletions.
63 changes: 45 additions & 18 deletions lib/experimental/Information/Avatars/BaseAvatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { Badge, BadgeProps } from "@/experimental/Information/Badge"
import {
Avatar as AvatarComponent,
AvatarFallback,
AvatarImage,
} from "@/ui/avatar"
import { ComponentProps, forwardRef } from "react"
import { getAvatarColor, getInitials } from "./utils"
import { getAvatarColor, getInitials, getMask } from "./utils"

const getBadgeSize = (size: ShadAvatarProps["size"]) => {
if (size === "xlarge") return "lg"
if (size === "large") return "md"
return "sm"
}

type ShadAvatarProps = ComponentProps<typeof AvatarComponent>

Expand All @@ -14,6 +21,7 @@ type Props = {
src?: string
size?: ShadAvatarProps["size"]
color?: ShadAvatarProps["color"] | "random"
badge?: BadgeProps
} & Pick<ShadAvatarProps, "aria-label" | "aria-labelledby">

export const BaseAvatar = forwardRef<HTMLDivElement, Props>(
Expand All @@ -22,10 +30,11 @@ export const BaseAvatar = forwardRef<HTMLDivElement, Props>(
src,
name,
size,
type,
type = "base",
color = "random",
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
badge,
},
ref
) => {
Expand All @@ -38,22 +47,40 @@ export const BaseAvatar = forwardRef<HTMLDivElement, Props>(
const hasAria = Boolean(ariaLabel || ariaLabelledby)

return (
<AvatarComponent
size={size}
type={type}
color={avatarColor}
ref={ref}
role="img"
aria-hidden={!hasAria}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
className={
src ? "bg-f1-background dark:bg-f1-background-inverse-secondary" : ""
}
>
<AvatarImage src={src} alt={initials} />
<AvatarFallback>{initials}</AvatarFallback>
</AvatarComponent>
<div className="relative inline-flex">
<div
className="h-fit w-fit"
style={badge ? { clipPath: getMask.get(type, size) } : undefined}
>
<AvatarComponent
size={size}
type={type}
color={avatarColor}
ref={ref}
role="img"
aria-hidden={!hasAria}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
className={
src
? "bg-f1-background dark:bg-f1-background-inverse-secondary"
: ""
}
>
<AvatarImage src={src} alt={initials} />
<AvatarFallback>{initials}</AvatarFallback>
</AvatarComponent>
</div>
{badge && (
<div className="absolute -bottom-0.5 -right-0.5">
<Badge
type={badge.type}
icon={badge.icon}
size={getBadgeSize(size)}
/>
</div>
)}
</div>
)
}
)
Expand Down
38 changes: 37 additions & 1 deletion lib/experimental/Information/Avatars/BaseAvatar/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { color as AvatarColors, Avatar as AvatarComponent } from "@/ui/avatar"
import {
color as AvatarColors,
Avatar as AvatarComponent,
type as avatarType,
sizes,
} from "@/ui/avatar"
import { type ComponentProps } from "react"

type ShadAvatarProps = ComponentProps<typeof AvatarComponent>
Expand Down Expand Up @@ -33,3 +38,34 @@ export function getAvatarColor(text: string): ShadAvatarProps["color"] {

return AvatarColors[index]
}

export const getMask = {
base: {
xlarge:
"path('M72 0H0V72H52.202C49.6089 69.459 48 65.9174 48 62C48 54.268 54.268 48 62 48C65.9174 48 69.459 49.6089 72 52.202V0ZM72 71.798C71.9333 71.866 71.866 71.9333 71.798 72H72V71.798Z')",
large:
"path('M56 0H0V56H39.0556C37.1554 53.877 36 51.0734 36 48C36 41.3726 41.3726 36 48 36C51.0734 36 53.877 37.1554 56 39.0556V0Z')",
medium:
"path('M32 0H0V32H22.2547C21.4638 30.8662 21 29.4872 21 28C21 24.134 24.134 21 28 21C29.4872 21 30.8662 21.4638 32 22.2547V0Z')",
small:
"path('M24 0H0V24H14.2547C13.4638 22.8662 13 21.4872 13 20C13 16.134 16.134 13 20 13C21.4872 13 22.8662 13.4638 24 14.2547V0Z')",
xsmall:
"path('M20 0H0V20H10.2547C9.46381 18.8662 9 17.4872 9 16C9 12.134 12.134 9 16 9C17.4872 9 18.8662 9.46381 20 10.2547V0Z')",
},
rounded: {
xlarge:
"path('M69.1842 49.9814C70.9975 45.6828 72 40.9585 72 36C72 16.1177 55.8823 0 36 0C16.1177 0 0 16.1177 0 36C0 55.8823 16.1177 72 36 72C40.9585 72 45.6828 70.9975 49.9814 69.1842C48.7232 67.0839 48 64.6264 48 62C48 54.268 54.268 48 62 48C64.6264 48 67.0839 48.7232 69.1842 49.9814Z')",
large:
"path('M54.2534 37.7562C55.3828 34.7182 56 31.4312 56 28C56 12.536 43.464 0 28 0C12.536 0 0 12.536 0 28C0 43.464 12.536 56 28 56C31.4312 56 34.7182 55.3828 37.7562 54.2534C36.6421 52.4324 36 50.2912 36 48C36 41.3726 41.3726 36 48 36C50.2912 36 52.4324 36.6421 54.2534 37.7562Z')",
medium:
"path('M30.9702 21.6596C31.6358 19.9001 32 17.9926 32 16C32 7.16344 24.8366 0 16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32C17.9926 32 19.9001 31.6358 21.6596 30.9702C21.2365 30.0686 21 29.0619 21 28C21 24.134 24.134 21 28 21C29.0619 21 30.0686 21.2365 30.9702 21.6596Z')",
small:
"path('M23.8119 14.128C23.9355 13.4373 24 12.7262 24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C12.7262 24 13.4373 23.9355 14.128 23.8119C13.4145 22.7151 13 21.406 13 20C13 16.134 16.134 13 20 13C21.406 13 22.7151 13.4145 23.8119 14.128Z')",
xsmall:
"path('M19.9969 10.2525C19.999 10.1686 20 10.0844 20 10C20 4.47715 15.5228 0 10 0C4.47715 0 0 4.47715 0 10C0 15.5228 4.47715 20 10 20C10.0844 20 10.1686 19.999 10.2525 19.9969C9.46297 18.8636 9 17.4859 9 16C9 12.134 12.134 9 16 9C17.4859 9 18.8636 9.46297 19.9969 10.2525Z')",
},
get: (
type: (typeof avatarType)[number] = "base",
size: (typeof sizes)[number] = "medium"
) => getMask[type][size],
} as const
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Check } from "@/icons/app"
import { sizes } from "@/ui/avatar"
import type { Meta, StoryObj } from "@storybook/react"
import { CompanyAvatar } from "."
Expand Down Expand Up @@ -41,3 +42,13 @@ export const WithImage: Story = {
src: "https://avatars.githubusercontent.com/u/21041797?s=48&v=4",
},
}

export const WithBadge: Story = {
args: {
src: "https://avatars.githubusercontent.com/u/21041797?s=48&v=4",
badge: {
type: "positive",
icon: Check,
},
},
}
4 changes: 4 additions & 0 deletions lib/experimental/Information/Avatars/CompanyAvatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BadgeProps } from "@/experimental/Information/Badge"
import { ComponentProps } from "react"
import { BaseAvatar } from "../BaseAvatar"

Expand All @@ -7,6 +8,7 @@ type Props = {
name: string
src?: string
size?: BaseAvatarProps["size"]
badge?: BadgeProps
} & Pick<BaseAvatarProps, "aria-label" | "aria-labelledby">

export const CompanyAvatar = ({
Expand All @@ -15,6 +17,7 @@ export const CompanyAvatar = ({
size,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
badge,
}: Props) => {
return (
<BaseAvatar
Expand All @@ -25,6 +28,7 @@ export const CompanyAvatar = ({
color="viridian"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
badge={badge}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Check } from "@/icons/app"
import { sizes } from "@/ui/avatar"
import type { Meta, StoryObj } from "@storybook/react"
import { PersonAvatar } from "."
Expand Down Expand Up @@ -41,3 +42,12 @@ export const WithImage: Story = {
src: "https://github.com/dani-moreno.png",
},
}

export const WithBadge: Story = {
args: {
badge: {
type: "positive",
icon: Check,
},
},
}
4 changes: 4 additions & 0 deletions lib/experimental/Information/Avatars/PersonAvatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BadgeProps } from "@/experimental/Information/Badge"
import { ComponentProps } from "react"
import { BaseAvatar } from "../BaseAvatar"

Expand All @@ -8,6 +9,7 @@ type Props = {
lastName: string
src?: string
size?: BaseAvatarProps["size"]
badge?: BadgeProps
} & Pick<BaseAvatarProps, "aria-label" | "aria-labelledby">

export const PersonAvatar = ({
Expand All @@ -17,6 +19,7 @@ export const PersonAvatar = ({
size,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
badge,
}: Props) => {
return (
<BaseAvatar
Expand All @@ -27,6 +30,7 @@ export const PersonAvatar = ({
color="random"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
badge={badge}
/>
)
}
Expand Down
10 changes: 10 additions & 0 deletions lib/experimental/Information/Avatars/TeamAvatar/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Check } from "@/icons/app"
import { sizes } from "@/ui/avatar"
import type { Meta, StoryObj } from "@storybook/react"
import { TeamAvatar } from "."
Expand Down Expand Up @@ -40,3 +41,12 @@ export const WithImage: Story = {
src: "https://avatars.githubusercontent.com/u/21041797?s=48&v=4",
},
}

export const WithBadge: Story = {
args: {
badge: {
type: "positive",
icon: Check,
},
},
}
4 changes: 4 additions & 0 deletions lib/experimental/Information/Avatars/TeamAvatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { BadgeProps } from "@/experimental/Information/Badge"
import { ComponentProps } from "react"
import { BaseAvatar } from "../BaseAvatar"

Expand All @@ -7,6 +8,7 @@ type Props = {
name: string
src?: string
size?: BaseAvatarProps["size"]
badge?: BadgeProps
} & Pick<BaseAvatarProps, "aria-label" | "aria-labelledby">

export const TeamAvatar = ({
Expand All @@ -15,6 +17,7 @@ export const TeamAvatar = ({
size,
"aria-label": ariaLabel,
"aria-labelledby": ariaLabelledby,
badge,
}: Props) => {
return (
<BaseAvatar
Expand All @@ -25,6 +28,7 @@ export const TeamAvatar = ({
color="random"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledby}
badge={badge}
/>
)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/experimental/Information/Badge/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ const iconSizes = {
lg: "md",
} as const

interface BadgeProps extends VariantProps<typeof badgeVariants> {
export interface BadgeProps extends VariantProps<typeof badgeVariants> {
icon: IconType
size: keyof typeof iconSizes
size?: keyof typeof iconSizes
}

export const Badge = ({ type, size = "md", icon }: BadgeProps) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@ export function CelebrationAvatar({
src={src}
firstName={firstName}
lastName={lastName}
size="large"
size="xlarge"
/>
</div>
{canReact && (
<div className="absolute -bottom-[3px] -right-0.5">
<div className="absolute -right-0.5 bottom-0.5">
<Button
label="React"
hideLabel
Expand Down
5 changes: 3 additions & 2 deletions lib/ui/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar"
import { cva } from "class-variance-authority"
import * as React from "react"

export const sizes = ["xsmall", "small", "medium", "large"] as const
export const sizes = ["xsmall", "small", "medium", "large", "xlarge"] as const

export const type = ["base", "rounded"] as const

Expand All @@ -32,7 +32,8 @@ const avatarVariants = cva(
xsmall: "size-5 rounded-xs text-sm",
small: "size-6 rounded-sm text-sm",
medium: "size-8 rounded",
large: "size-18 rounded-xl text-2xl",
large: "size-14 rounded-xl text-xl",
xlarge: "size-18 rounded-[20px] text-2xl",
} satisfies Record<(typeof sizes)[number], string>,
type: {
base: "",
Expand Down

0 comments on commit 646cf35

Please sign in to comment.