From beeb07bc70c452118734e32bdd2930672a62d293 Mon Sep 17 00:00:00 2001 From: yyh Date: Wed, 25 Mar 2026 13:53:54 +0800 Subject: [PATCH] refactor(web): simplify avatar composition api --- web/app/components/base/avatar/index.tsx | 74 +++++++++++++++---- .../base/user-avatar-list/index.tsx | 10 +-- .../components/workflow/comment/thread.tsx | 9 +-- .../workflow/header/online-users.tsx | 9 +-- 4 files changed, 67 insertions(+), 35 deletions(-) diff --git a/web/app/components/base/avatar/index.tsx b/web/app/components/base/avatar/index.tsx index 3d651cff00..f53e1f8985 100644 --- a/web/app/components/base/avatar/index.tsx +++ b/web/app/components/base/avatar/index.tsx @@ -1,8 +1,9 @@ import type { ImageLoadingStatus } from '@base-ui/react/avatar' +import type * as React from 'react' import { Avatar as BaseAvatar } from '@base-ui/react/avatar' import { cn } from '@/utils/classnames' -export const avatarSizeClasses = { +const avatarSizeClasses = { 'xxs': { root: 'size-4', text: 'text-[7px]' }, 'xs': { root: 'size-5', text: 'text-[8px]' }, 'sm': { root: 'size-6', text: 'text-[10px]' }, @@ -15,8 +16,6 @@ export const avatarSizeClasses = { export type AvatarSize = keyof typeof avatarSizeClasses -export const getAvatarSizeClassNames = (size: AvatarSize) => avatarSizeClasses[size] - export type AvatarProps = { name: string avatar: string | null @@ -25,15 +24,61 @@ export type AvatarProps = { onLoadingStatusChange?: (status: ImageLoadingStatus) => void } -export const AvatarRoot = BaseAvatar.Root -export const AvatarImage = BaseAvatar.Image -export const AvatarFallback = BaseAvatar.Fallback +export type AvatarRootProps = React.ComponentPropsWithRef & { + size?: AvatarSize +} -export const avatarPartClassNames = { - root: 'relative inline-flex shrink-0 select-none items-center justify-center overflow-hidden rounded-full bg-primary-600', - image: 'absolute inset-0 size-full object-cover', - fallback: 'flex size-full items-center justify-center font-medium text-white', -} as const +export function AvatarRoot({ + size = 'md', + className, + ...props +}: AvatarRootProps) { + return ( + + ) +} + +export type AvatarImageProps = React.ComponentPropsWithRef + +export function AvatarImage({ + className, + ...props +}: AvatarImageProps) { + return ( + + ) +} + +export type AvatarFallbackProps = React.ComponentPropsWithRef & { + size?: AvatarSize +} + +export function AvatarFallback({ + size = 'md', + className, + ...props +}: AvatarFallbackProps) { + return ( + + ) +} export const Avatar = ({ name, @@ -42,19 +87,16 @@ export const Avatar = ({ className, onLoadingStatusChange, }: AvatarProps) => { - const sizeClassNames = getAvatarSizeClassNames(size) - return ( - + {avatar && ( )} - + {name?.[0]?.toLocaleUpperCase()} diff --git a/web/app/components/base/user-avatar-list/index.tsx b/web/app/components/base/user-avatar-list/index.tsx index 03d71c3c4d..04ff5bd988 100644 --- a/web/app/components/base/user-avatar-list/index.tsx +++ b/web/app/components/base/user-avatar-list/index.tsx @@ -1,10 +1,9 @@ import type { FC } from 'react' import type { AvatarSize } from '@/app/components/base/avatar' import { memo } from 'react' -import { AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } from '@/app/components/base/avatar' +import { AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/avatar' import { getUserColor } from '@/app/components/workflow/collaboration/utils/user-color' import { useAppContext } from '@/context/app-context' -import { cn } from '@/utils/classnames' type User = { id: string @@ -12,7 +11,6 @@ type User = { avatar_url?: string | null } -/** Map legacy pixel size to Avatar token (closest; ties favor smaller px, e.g. 28 -> sm). */ function numericPxToAvatarSize(px: number): AvatarSize { const candidates: { px: number, size: AvatarSize }[] = [ { px: 16, size: 'xxs' }, @@ -62,7 +60,6 @@ export const UserAvatarList: FC = memo(({ const currentUserId = userProfile?.id const avatarSize = numericPxToAvatarSize(size) - const avatarSizeClassNames = getAvatarSizeClassNames(avatarSize) return (
@@ -75,16 +72,15 @@ export const UserAvatarList: FC = memo(({ className="relative" style={{ zIndex: visibleUsers.length - index }} > - + {user.avatar_url && ( )} {user.name?.[0]?.toLocaleUpperCase()} diff --git a/web/app/components/workflow/comment/thread.tsx b/web/app/components/workflow/comment/thread.tsx index 2b415dda19..96251e1764 100644 --- a/web/app/components/workflow/comment/thread.tsx +++ b/web/app/components/workflow/comment/thread.tsx @@ -6,7 +6,7 @@ import { RiArrowDownSLine, RiArrowUpSLine, RiCheckboxCircleFill, RiCheckboxCircl import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useReactFlow, useViewport } from 'reactflow' -import { Avatar, AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } from '@/app/components/base/avatar' +import { Avatar, AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/avatar' import Divider from '@/app/components/base/divider' import InlineDeleteConfirm from '@/app/components/base/inline-delete-confirm' import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem' @@ -19,8 +19,6 @@ import { cn } from '@/utils/classnames' import { useStore } from '../store' import { MentionInput } from './mention-input' -const threadAvatarSizeClassNames = getAvatarSizeClassNames('sm') - type CommentThreadProps = { comment: WorkflowCommentDetail loading?: boolean @@ -122,16 +120,15 @@ const ThreadMessage: FC<{ return (
- + {avatarUrl && ( )} {authorName?.[0]?.toLocaleUpperCase()} diff --git a/web/app/components/workflow/header/online-users.tsx b/web/app/components/workflow/header/online-users.tsx index d2dcccb990..5b80ea0e90 100644 --- a/web/app/components/workflow/header/online-users.tsx +++ b/web/app/components/workflow/header/online-users.tsx @@ -4,7 +4,7 @@ import { ChevronDownIcon } from '@heroicons/react/20/solid' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import { useReactFlow } from 'reactflow' -import { AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } from '@/app/components/base/avatar' +import { AvatarFallback, AvatarImage, AvatarRoot } from '@/app/components/base/avatar' import Divider from '@/app/components/base/divider' import { PortalToFollowElem, @@ -51,8 +51,6 @@ const useAvatarUrls = (users: OnlineUser[]) => { return avatarUrls } -const onlineUserAvatarSizeClassNames = getAvatarSizeClassNames('sm') - type OnlineUserAvatarProps = { name: string avatar: string | null @@ -66,16 +64,15 @@ const OnlineUserAvatar = ({ className, fallbackColor, }: OnlineUserAvatarProps) => ( - + {avatar && ( )} {name?.[0]?.toLocaleUpperCase()}