fix(web): unify workspace avatar usage (#39160)

This commit is contained in:
Jingyi
2026-07-17 01:35:03 +00:00
committed by GitHub
parent 96e34e7b24
commit 50341357b3
8 changed files with 101 additions and 32 deletions
@@ -0,0 +1,43 @@
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
import { WorkspaceAvatar } from '.'
const meta = {
title: 'Base/General/WorkspaceAvatar',
component: WorkspaceAvatar,
parameters: {
docs: {
description: {
component: 'Square workspace avatar with the shared workspace fallback-initial treatment.',
},
},
},
tags: ['autodocs'],
args: {
name: 'Solar Studio',
size: 'sm',
},
} satisfies Meta<typeof WorkspaceAvatar>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const Sizes: Story = {
render: (args) => (
<div className="flex items-end gap-4">
{(['xs', 'sm', 'lg', '2xl'] as const).map((size) => (
<div key={size} className="flex flex-col items-center gap-2">
<WorkspaceAvatar {...args} size={size} />
<span className="text-xs text-text-tertiary uppercase">{size}</span>
</div>
))}
</div>
),
}
export const MissingName: Story = {
args: {
name: undefined,
},
}
@@ -0,0 +1,47 @@
'use client'
import type { AvatarSize } from '@langgenius/dify-ui/avatar'
import { AvatarFallback, AvatarRoot } from '@langgenius/dify-ui/avatar'
import { cn } from '@langgenius/dify-ui/cn'
export type WorkspaceAvatarSize = Extract<AvatarSize, 'xs' | 'sm' | 'lg' | '2xl'>
const workspaceAvatarClasses: Record<WorkspaceAvatarSize, { root: string; text: string }> = {
xs: { root: 'rounded-md', text: 'text-[10px] leading-3' },
sm: { root: 'rounded-md', text: 'text-[13px] leading-4' },
lg: { root: 'rounded-lg', text: 'text-base leading-5' },
'2xl': { root: 'rounded-xl', text: 'text-xl leading-7' },
}
export function WorkspaceAvatar({
name,
size = 'sm',
className,
}: {
name?: string
size?: WorkspaceAvatarSize
className?: string
}) {
return (
<AvatarRoot
size={size}
className={cn('shadow-xs', workspaceAvatarClasses[size].root, className)}
>
<AvatarFallback
size={size}
className={cn(
'border border-divider-regular bg-components-icon-bg-orange-dark-solid bg-linear-to-br from-components-avatar-bg-mask-stop-0 to-components-avatar-bg-mask-stop-100 p-1',
)}
>
<span
className={cn(
'bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold text-shadow-shadow-1 text-transparent uppercase opacity-90',
workspaceAvatarClasses[size].text,
)}
>
{name?.[0]?.toLocaleUpperCase() || '?'}
</span>
</AvatarFallback>
</AvatarRoot>
)
}
@@ -8,6 +8,7 @@ import {
} from '@langgenius/dify-ui/select'
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
import { Plan } from '@/app/components/billing/type'
import { PlanBadge } from '@/app/components/header/plan-badge'
@@ -32,11 +33,7 @@ const WorkplaceSelectorItem = memo(({ workspace }: WorkplaceSelectorItemProps) =
return (
<SelectItem value={workspace.id} className="gap-2 py-1 pr-2 pl-3">
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-components-icon-bg-blue-solid text-[13px]">
<span className="h-6 bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text align-middle leading-6 font-semibold text-shadow-shadow-1 uppercase opacity-90">
{workspaceName[0]?.toLocaleUpperCase()}
</span>
</div>
<WorkspaceAvatar name={workspaceName} size="sm" />
<SelectItemText className="system-md-regular">{workspaceName}</SelectItemText>
<PlanBadge plan={workspacePlan} />
</SelectItem>
@@ -8,6 +8,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
import { NUM_INFINITE } from '@/app/components/billing/config'
import { Plan } from '@/app/components/billing/type'
import UpgradeBtn from '@/app/components/billing/upgrade-btn'
@@ -96,11 +97,7 @@ const MembersPage = () => {
<>
<div className="flex flex-col">
<div className="mb-6 flex items-center gap-3 rounded-xl border-t-[0.5px] border-l-[0.5px] border-divider-subtle bg-linear-to-r from-background-gradient-bg-fill-chat-bg-2 to-background-gradient-bg-fill-chat-bg-1 py-2 pr-5 pl-2">
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-components-icon-bg-blue-solid text-[20px]">
<span className="bg-linear-to-r from-components-avatar-shape-fill-stop-0 to-components-avatar-shape-fill-stop-100 bg-clip-text font-semibold text-shadow-shadow-1 uppercase opacity-90">
{currentWorkspace?.name[0]?.toLocaleUpperCase()}
</span>
</div>
<WorkspaceAvatar name={currentWorkspace.name} size="2xl" />
<div className="grow">
<div className="flex items-center gap-1 system-md-semibold text-text-secondary">
<span>{currentWorkspace?.name}</span>
@@ -8,6 +8,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
import { Plan } from '@/app/components/billing/type'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import LicenseNav from '@/app/components/header/license-env'
@@ -21,7 +22,7 @@ import { consoleQuery } from '@/service/client'
import { hasPermission } from '@/utils/permission'
import { basePath } from '@/utils/var'
import { formatCredits, getRemainingCredits } from '../utils'
import { WorkspaceIcon, WorkspaceMenuItemContent } from './workspace-menu-content'
import { WorkspaceMenuItemContent } from './workspace-menu-content'
import WorkspacePlanBadge from './workspace-plan-badge'
import { WorkspaceSwitcher } from './workspace-switcher'
@@ -116,7 +117,7 @@ function WorkspaceCardTrigger({
open && 'bg-linear-to-b from-background-section-burn to-background-section',
)}
>
<WorkspaceIcon name={name} className="h-6 w-6 rounded-lg" />
<WorkspaceAvatar name={name} size="sm" />
<div className="min-w-0 grow">
<div className="flex min-w-0 items-center gap-1 pr-0.5">
<span
@@ -189,7 +190,7 @@ function WorkspaceMenuHeader({
</PopoverTitle>
{status}
</div>
<WorkspaceIcon name={name} className="h-9 w-9 shrink-0" />
<WorkspaceAvatar name={name} size="lg" />
</div>
<button
type="button"
@@ -1,19 +1,4 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { getWorkspaceInitial } from '../utils'
export function WorkspaceIcon({ name, className }: { name?: string; className?: string }) {
return (
<div
className={cn(
'flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-components-icon-bg-orange-dark-solid text-white shadow-xs',
className,
)}
>
<span className="system-md-semibold">{getWorkspaceInitial(name)}</span>
</div>
)
}
export function WorkspaceMenuItemContent({
icon,
@@ -13,7 +13,8 @@ import {
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { SearchInput } from '@/app/components/base/search-input'
import { WorkspaceIcon, WorkspaceMenuItemContent } from './workspace-menu-content'
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
import { WorkspaceMenuItemContent } from './workspace-menu-content'
const workspaceSwitchActionButtonClassName =
'flex shrink-0 items-center justify-center rounded-md p-0.5 text-text-tertiary outline-hidden hover:bg-state-base-hover hover:text-text-secondary focus-visible:ring-2 focus-visible:ring-state-accent-solid'
@@ -194,7 +195,7 @@ export function WorkspaceSwitcher({ workspaces, onSwitchWorkspace }: WorkspaceSw
}}
>
<WorkspaceMenuItemContent
icon={<WorkspaceIcon name={workspaceName} className="h-5 w-5 rounded-md" />}
icon={<WorkspaceAvatar name={workspaceName} size="xs" />}
label={workspaceName}
trailing={
workspace.current ? (
-2
View File
@@ -1,5 +1,3 @@
export const getWorkspaceInitial = (name?: string) => name?.[0]?.toLocaleUpperCase() || '?'
export const getRemainingCredits = (total: number, used: number) => Math.max(total - used, 0)
export const formatCredits = (value: number) => new Intl.NumberFormat().format(value)