refactor(web): compose avatar fallbacks from primitives

This commit is contained in:
yyh
2026-03-25 13:47:59 +08:00
parent 76b8f7ee4b
commit 1a2baa431e
3 changed files with 74 additions and 23 deletions

View File

@ -1,9 +1,10 @@
import type { FC } from 'react'
import type { AvatarSize } from '@/app/components/base/avatar'
import { memo } from 'react'
import { Avatar } from '@/app/components/base/avatar'
import { AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } 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
@ -60,6 +61,8 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
const remainingCount = users.length - actualMaxVisible
const currentUserId = userProfile?.id
const avatarSize = numericPxToAvatarSize(size)
const avatarSizeClassNames = getAvatarSizeClassNames(avatarSize)
return (
<div className={`flex items-center -space-x-1 ${className}`}>
@ -72,13 +75,21 @@ export const UserAvatarList: FC<UserAvatarListProps> = memo(({
className="relative"
style={{ zIndex: visibleUsers.length - index }}
>
<Avatar
name={user.name}
avatar={user.avatar_url || null}
size={numericPxToAvatarSize(size)}
className="ring-2 ring-components-panel-bg"
backgroundColor={userColor}
/>
<AvatarRoot className={cn(avatarPartClassNames.root, avatarSizeClassNames.root, 'ring-2 ring-components-panel-bg')}>
{user.avatar_url && (
<AvatarImage
src={user.avatar_url}
alt={user.name}
className={avatarPartClassNames.image}
/>
)}
<AvatarFallback
className={cn(avatarPartClassNames.fallback, avatarSizeClassNames.text)}
style={userColor ? { backgroundColor: userColor } : undefined}
>
{user.name?.[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
</div>
)
},

View File

@ -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 } from '@/app/components/base/avatar'
import { Avatar, AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } 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,6 +19,8 @@ import { cn } from '@/utils/classnames'
import { useStore } from '../store'
import { MentionInput } from './mention-input'
const threadAvatarSizeClassNames = getAvatarSizeClassNames('sm')
type CommentThreadProps = {
comment: WorkflowCommentDetail
loading?: boolean
@ -120,13 +122,21 @@ const ThreadMessage: FC<{
return (
<div className={cn('flex gap-3 pt-1', className)}>
<div className="shrink-0">
<Avatar
name={authorName}
avatar={avatarUrl || null}
size="sm"
className={cn('h-8 w-8 rounded-full')}
backgroundColor={userColor}
/>
<AvatarRoot className={cn(avatarPartClassNames.root, threadAvatarSizeClassNames.root, 'h-8 w-8 rounded-full')}>
{avatarUrl && (
<AvatarImage
src={avatarUrl}
alt={authorName}
className={avatarPartClassNames.image}
/>
)}
<AvatarFallback
className={cn(avatarPartClassNames.fallback, threadAvatarSizeClassNames.text)}
style={userColor ? { backgroundColor: userColor } : undefined}
>
{authorName?.[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
</div>
<div className="min-w-0 flex-1 pb-4 text-text-primary last:pb-0">
<div className="flex flex-wrap items-center gap-x-2 gap-y-1">

View File

@ -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 { Avatar } from '@/app/components/base/avatar'
import { AvatarFallback, AvatarImage, avatarPartClassNames, AvatarRoot, getAvatarSizeClassNames } from '@/app/components/base/avatar'
import Divider from '@/app/components/base/divider'
import {
PortalToFollowElem,
@ -51,6 +51,38 @@ const useAvatarUrls = (users: OnlineUser[]) => {
return avatarUrls
}
const onlineUserAvatarSizeClassNames = getAvatarSizeClassNames('sm')
type OnlineUserAvatarProps = {
name: string
avatar: string | null
className?: string
fallbackColor?: string
}
const OnlineUserAvatar = ({
name,
avatar,
className,
fallbackColor,
}: OnlineUserAvatarProps) => (
<AvatarRoot className={cn(avatarPartClassNames.root, onlineUserAvatarSizeClassNames.root, className)}>
{avatar && (
<AvatarImage
src={avatar}
alt={name}
className={avatarPartClassNames.image}
/>
)}
<AvatarFallback
className={cn(avatarPartClassNames.fallback, onlineUserAvatarSizeClassNames.text)}
style={fallbackColor ? { backgroundColor: fallbackColor } : undefined}
>
{name?.[0]?.toLocaleUpperCase()}
</AvatarFallback>
</AvatarRoot>
)
const OnlineUsers = () => {
const { t } = useTranslation()
const appId = useStore(s => s.appId)
@ -135,12 +167,11 @@ const OnlineUsers = () => {
style={{ zIndex: visibleUsers.length - index }}
onClick={() => !isCurrentUser && jumpToUserCursor(user.user_id)}
>
<Avatar
<OnlineUserAvatar
name={user.username || t('comments.fallback.user', { ns: 'workflow' })}
avatar={getAvatarUrl(user) ?? null}
size="sm"
className="ring-1 ring-components-panel-bg"
backgroundColor={userColor}
fallbackColor={userColor}
/>
</TooltipTrigger>
<TooltipContent
@ -216,11 +247,10 @@ const OnlineUsers = () => {
}}
>
<div className="relative">
<Avatar
<OnlineUserAvatar
name={user.username || t('comments.fallback.user', { ns: 'workflow' })}
avatar={getAvatarUrl(user) ?? null}
size="sm"
backgroundColor={userColor}
fallbackColor={userColor}
/>
</div>
{renderDisplayName(