refactor: Replace Avatar internals with Radix primitives

- Replace custom Avatar implementation with @radix-ui/react-avatar
- Maintain same API and styling for backward compatibility
- Use Radix best practices with Root, Image, and Fallback components
- Fix TypeScript and ESLint issues
- Keep all existing functionality including AvatarWithPresence and GroupAvatar
This commit is contained in:
codegen-sh[bot]
2025-06-01 22:02:19 +00:00
parent 34bdd59f35
commit f96443a941
5 changed files with 77 additions and 28 deletions
+47 -20
View File
@@ -1,6 +1,6 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as React from "react";
import styled from "styled-components";
import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
export enum AvatarSize {
@@ -37,7 +37,7 @@ type Props = {
/** The alt text for the image */
alt?: string;
/** Optional click handler */
onClick?: React.MouseEventHandler<HTMLImageElement>;
onClick?: React.MouseEventHandler<HTMLDivElement>;
/** Optional class name */
className?: string;
/** Optional style */
@@ -50,28 +50,37 @@ function Avatar(props: Props) {
style,
variant = AvatarVariant.Round,
className,
...rest
onClick,
alt,
size,
} = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false);
return (
<Relative
<StyledAvatarRoot
style={style}
$variant={variant}
$size={props.size}
$size={size}
className={className}
onClick={onClick}
>
{src && !error ? (
<Image onError={handleError} src={src} {...rest} />
) : model ? (
<Initials color={model.color} {...rest}>
{model.initial}
</Initials>
) : (
<Initials {...rest} />
{src && (
<StyledAvatarImage
src={src}
alt={alt || (model ? `${model.initial || "User"} avatar` : "Avatar")}
$size={size}
/>
)}
</Relative>
<StyledAvatarFallback $size={size}>
{model ? (
<Initials color={model.color} size={size}>
{model.initial}
</Initials>
) : (
<Initials size={size} />
)}
</StyledAvatarFallback>
</StyledAvatarRoot>
);
}
@@ -79,7 +88,10 @@ Avatar.defaultProps = {
size: AvatarSize.Medium,
};
const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
const StyledAvatarRoot = styled(AvatarPrimitive.Root)<{
$variant: AvatarVariant;
$size: AvatarSize;
}>`
position: relative;
user-select: none;
flex-shrink: 0;
@@ -88,12 +100,27 @@ const Relative = styled.div<{ $variant: AvatarVariant; $size: AvatarSize }>`
overflow: hidden;
width: ${(props) => props.$size}px;
height: ${(props) => props.$size}px;
display: inline-flex;
align-items: center;
justify-content: center;
`;
const Image = styled.img<{ size: number }>`
display: block;
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
const StyledAvatarImage = styled(AvatarPrimitive.Image)<{ $size: number }>`
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
`;
const StyledAvatarFallback = styled(AvatarPrimitive.Fallback)<{
$size: number;
}>`
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
border-radius: inherit;
`;
export default Avatar;
+6 -6
View File
@@ -22,7 +22,7 @@ type Props = {
/** Whether this avatar represents the current user */
isCurrentUser: boolean;
/** Optional click handler for the avatar */
onClick?: React.MouseEventHandler<HTMLImageElement>;
onClick?: React.MouseEventHandler<HTMLDivElement>;
/** Size of the avatar, defaults to AvatarSize.Large */
size?: AvatarSize;
/** Optional inline styles to apply to the avatar wrapper */
@@ -138,11 +138,11 @@ const AvatarPresence = styled.div<AvatarWrapperProps>`
border: 2px solid transparent;
pointer-events: none;
${(props) =>
props.$isObserving &&
${(innerProps) =>
innerProps.$isObserving &&
css`
border: 2px solid ${props.$color};
box-shadow: inset 0 0 0 2px ${props.theme.background};
border: 2px solid ${innerProps.$color};
box-shadow: inset 0 0 0 2px ${innerProps.theme.background};
&:hover {
top: -1px;
@@ -154,7 +154,7 @@ const AvatarPresence = styled.div<AvatarWrapperProps>`
}
&:hover:after {
border: 2px solid ${(props) => props.$color};
border: 2px solid ${(innerProps) => innerProps.$color};
box-shadow: inset 0 0 0 2px ${s("background")};
}
`}
-2
View File
@@ -20,8 +20,6 @@ const Initials = styled(Flex)<{
? s("black50")
: s("white75")};
background-color: ${(props) => props.color ?? props.theme.textTertiary};
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
flex-shrink: 0;
// adjust font size down for each additional character
+1
View File
@@ -86,6 +86,7 @@
"@octokit/auth-app": "^6.1.3",
"@outlinewiki/koa-passport": "^4.2.1",
"@outlinewiki/passport-azure-ad-oauth2": "^0.1.0",
"@radix-ui/react-avatar": "^1.1.10",
"@radix-ui/react-popover": "^1.1.14",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-switch": "^1.2.5",
+23
View File
@@ -3327,6 +3327,17 @@
dependencies:
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-avatar@^1.1.10":
version "1.1.10"
resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz#c58a8800ef3d3ee783b3168fee7c76f6534bfd93"
integrity sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==
dependencies:
"@radix-ui/react-context" "1.1.2"
"@radix-ui/react-primitive" "2.1.3"
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-is-hydrated" "0.1.0"
"@radix-ui/react-use-layout-effect" "1.1.1"
"@radix-ui/react-collection@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.1.tgz#be2c7e01d3508e6d4b6d838f492e7d182f17d3b0"
@@ -3719,6 +3730,13 @@
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.1"
"@radix-ui/react-use-is-hydrated@0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz#544da73369517036c77659d7cdd019dc0f5ff9a0"
integrity sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==
dependencies:
use-sync-external-store "^1.5.0"
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
@@ -15958,6 +15976,11 @@ use-sidecar@^1.1.3:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz#55122e2a3edd2a6c106174c27485e0fd59bcfca0"
integrity sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==
utf8-byte-length@^1.0.1:
version "1.0.4"
resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61"