mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-09-04 07:19:35 +00:00
fix(web): densify table rows and soften user avatars
Keep truncated cells single-line across data tables, compact oversized quota figures, and replace loud avatar gradients with identity-tint styling that matches badges. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -59,8 +59,10 @@ function DataTableRowInner<TData>({
|
||||
'max-w-full min-w-0',
|
||||
contentMode === 'full' &&
|
||||
'max-w-none overflow-visible [&_.truncate]:overflow-visible [&_.truncate]:text-clip',
|
||||
// Keep `.truncate` children single-line (ellipsis + tooltip)
|
||||
// even in wrap mode, so table rows stay at most ~2 lines tall.
|
||||
contentMode === 'wrap' &&
|
||||
'whitespace-normal break-words [overflow-wrap:anywhere] [&_.truncate]:overflow-visible [&_.truncate]:text-clip [&_.truncate]:whitespace-normal',
|
||||
'whitespace-normal break-words [overflow-wrap:anywhere]',
|
||||
contentMode === 'summary' &&
|
||||
'whitespace-normal break-words [overflow-wrap:anywhere]',
|
||||
getColumnClassName?.(cell.column.id, 'cell')
|
||||
|
||||
+8
-8
@@ -34,11 +34,11 @@ import {
|
||||
import useDialogState from '@/hooks/use-dialog'
|
||||
import { useIsSidebarModuleVisible } from '@/hooks/use-sidebar-config'
|
||||
import { useUserDisplay } from '@/hooks/use-user-display'
|
||||
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { getUserAvatarFallback, getUserAvatarProps } from '@/lib/avatar'
|
||||
import { ROLE } from '@/lib/roles'
|
||||
import { useAuthStore } from '@/stores/auth-store'
|
||||
|
||||
const avatarFallbackClassName = 'font-semibold text-white'
|
||||
const avatarFallbackClassName = 'font-semibold'
|
||||
|
||||
export function ProfileDropdown() {
|
||||
const { t } = useTranslation()
|
||||
@@ -50,8 +50,8 @@ export function ProfileDropdown() {
|
||||
const isWalletVisible = useIsSidebarModuleVisible('/wallet')
|
||||
const avatarName = user?.username || displayName
|
||||
const avatarFallback = getUserAvatarFallback(avatarName)
|
||||
const avatarFallbackStyle = useMemo(
|
||||
() => getUserAvatarStyle(avatarName),
|
||||
const avatarProps = useMemo(
|
||||
() => getUserAvatarProps(avatarName),
|
||||
[avatarName]
|
||||
)
|
||||
|
||||
@@ -63,8 +63,8 @@ export function ProfileDropdown() {
|
||||
>
|
||||
<Avatar className='size-6'>
|
||||
<AvatarFallback
|
||||
className={`${avatarFallbackClassName} text-xs`}
|
||||
style={avatarFallbackStyle}
|
||||
className={`${avatarFallbackClassName} ${avatarProps.className} text-xs`}
|
||||
style={avatarProps.style}
|
||||
>
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
@@ -74,8 +74,8 @@ export function ProfileDropdown() {
|
||||
<div className='flex items-center gap-2 px-1.5 py-1.5'>
|
||||
<Avatar className='size-8'>
|
||||
<AvatarFallback
|
||||
className={`${avatarFallbackClassName} text-xs`}
|
||||
style={avatarFallbackStyle}
|
||||
className={`${avatarFallbackClassName} ${avatarProps.className} text-xs`}
|
||||
style={avatarProps.style}
|
||||
>
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
|
||||
@@ -585,7 +585,9 @@ export function useChannelsColumns(
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
// 'summary' keeps the name on a single truncated line (full name in
|
||||
// tooltip), matching the Groups column instead of wrapping rows tall.
|
||||
contentMode: 'summary',
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isTagRow = isTagAggregateRow(row.original)
|
||||
@@ -671,7 +673,7 @@ export function useChannelsColumns(
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<span className='text-muted-foreground text-xs' />
|
||||
<span className='text-muted-foreground max-w-full truncate text-xs' />
|
||||
}
|
||||
>
|
||||
{truncateText(channel.remark, 40)}
|
||||
|
||||
@@ -30,6 +30,8 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useGroupRatios } from '@/hooks/use-group-ratios'
|
||||
import { toIntlLocale } from '@/i18n/languages'
|
||||
import { formatQuotaWithCurrency } from '@/lib/currency'
|
||||
import { formatQuota, formatTimestampToDate } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -42,6 +44,12 @@ import {
|
||||
} from './api-keys-cells'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
|
||||
/**
|
||||
* Inline quota values longer than this switch to locale-aware compact
|
||||
* notation (e.g. "¥4.8万"); precise values stay available in the tooltip.
|
||||
*/
|
||||
const MAX_INLINE_QUOTA_CHARS = 8
|
||||
|
||||
function getQuotaProgressColor(percentage: number): string {
|
||||
if (percentage <= 10) {
|
||||
return '[&_[data-slot=progress-indicator]]:bg-destructive'
|
||||
@@ -51,8 +59,9 @@ function getQuotaProgressColor(percentage: number): string {
|
||||
}
|
||||
|
||||
export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const groupRatios = useGroupRatios()
|
||||
const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language)
|
||||
return [
|
||||
{
|
||||
id: 'select',
|
||||
@@ -81,9 +90,14 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: t('Name'),
|
||||
cell: ({ row }) => (
|
||||
<span className='font-medium'>{row.getValue('name')}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('name') as string
|
||||
return (
|
||||
<span className='block max-w-full truncate font-medium' title={name}>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
meta: {
|
||||
cardRole: 'title',
|
||||
@@ -140,15 +154,23 @@ export function useApiKeysColumns(): ColumnDef<ApiKey>[] {
|
||||
const total = used + remaining
|
||||
const percentage = total > 0 ? (remaining / total) * 100 : 0
|
||||
|
||||
const toInlineQuota = (value: number) => {
|
||||
const full = formatQuota(value)
|
||||
if (full.length <= MAX_INLINE_QUOTA_CHARS) {
|
||||
return full
|
||||
}
|
||||
return formatQuotaWithCurrency(value, { compact: true, locale })
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<div className='w-[150px] space-y-1' />}>
|
||||
<div className='flex justify-between text-xs'>
|
||||
<span className='font-medium tabular-nums'>
|
||||
{formatQuota(remaining)}
|
||||
<div className='flex justify-between gap-2 text-xs'>
|
||||
<span className='truncate font-medium tabular-nums'>
|
||||
{toInlineQuota(remaining)}
|
||||
</span>
|
||||
<span className='text-muted-foreground tabular-nums'>
|
||||
{formatQuota(total)}
|
||||
<span className='text-muted-foreground truncate tabular-nums'>
|
||||
{toInlineQuota(total)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
|
||||
@@ -22,7 +22,7 @@ import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { getUserAvatarFallback, getUserAvatarProps } from '@/lib/avatar'
|
||||
import { formatCompactNumber, formatQuota } from '@/lib/format'
|
||||
import { getRoleLabel } from '@/lib/roles'
|
||||
|
||||
@@ -68,7 +68,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
const displayName = getDisplayName(profile)
|
||||
const avatarName = profile.username || displayName
|
||||
const avatarFallback = getUserAvatarFallback(avatarName)
|
||||
const avatarFallbackStyle = getUserAvatarStyle(avatarName)
|
||||
const avatarProps = getUserAvatarProps(avatarName)
|
||||
const roleLabel = getRoleLabel(profile.role)
|
||||
const stats = [
|
||||
{
|
||||
@@ -91,8 +91,8 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
|
||||
<div className='flex items-center gap-3 sm:gap-4'>
|
||||
<Avatar className='size-12 rounded-xl text-base sm:size-14 sm:text-lg'>
|
||||
<AvatarFallback
|
||||
className='rounded-xl font-semibold text-white'
|
||||
style={avatarFallbackStyle}
|
||||
className={`rounded-xl font-semibold ${avatarProps.className}`}
|
||||
style={avatarProps.style}
|
||||
>
|
||||
{avatarFallback}
|
||||
</AvatarFallback>
|
||||
|
||||
+8
-3
@@ -85,9 +85,14 @@ export function useRedemptionsColumns(): ColumnDef<Redemption>[] {
|
||||
cardSpan: 2,
|
||||
contentMode: 'wrap',
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<span className='font-medium'>{row.getValue('name')}</span>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
const name = row.getValue('name') as string
|
||||
return (
|
||||
<span className='block max-w-full truncate font-medium' title={name}>
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
|
||||
+7
-2
@@ -61,9 +61,14 @@ export function useSubscriptionsColumns(): ColumnDef<PlanRecord>[] {
|
||||
const plan = row.original.plan
|
||||
return (
|
||||
<div className='max-w-full min-w-0'>
|
||||
<div className='font-medium'>{plan.title}</div>
|
||||
<div className='truncate font-medium' title={plan.title}>
|
||||
{plan.title}
|
||||
</div>
|
||||
{plan.subtitle && (
|
||||
<div className='text-muted-foreground text-xs'>
|
||||
<div
|
||||
className='text-muted-foreground truncate text-xs'
|
||||
title={plan.subtitle}
|
||||
>
|
||||
{plan.subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
+15
-10
@@ -35,7 +35,7 @@ import {
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { useGroupRatios } from '@/hooks/use-group-ratios'
|
||||
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { getUserAvatarFallback, getUserAvatarProps } from '@/lib/avatar'
|
||||
import { getIdentityTextColorClass } from '@/lib/colors'
|
||||
import { formatBillingCurrencyFromUSD } from '@/lib/currency'
|
||||
import {
|
||||
@@ -505,6 +505,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
|
||||
if (!log.username) return null
|
||||
|
||||
const avatarProps = sensitiveVisible
|
||||
? getUserAvatarProps(log.username)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
@@ -515,17 +519,13 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
setUserInfoDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||
<Avatar className='size-6 max-sm:hidden'>
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
'text-xs font-semibold',
|
||||
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
||||
avatarProps?.className
|
||||
)}
|
||||
style={
|
||||
sensitiveVisible
|
||||
? getUserAvatarStyle(log.username)
|
||||
: undefined
|
||||
}
|
||||
style={avatarProps?.style}
|
||||
>
|
||||
{sensitiveVisible ? getUserAvatarFallback(log.username) : '•'}
|
||||
</AvatarFallback>
|
||||
@@ -609,7 +609,10 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
{metaParts.length > 0 && (
|
||||
<span className='text-subtle-foreground text-xs [overflow-wrap:anywhere] break-words'>
|
||||
<span
|
||||
className='text-subtle-foreground max-w-[150px] truncate text-xs'
|
||||
title={sensitiveVisible ? metaParts.join(' · ') : undefined}
|
||||
>
|
||||
{metaParts.join(' · ')}
|
||||
</span>
|
||||
)}
|
||||
@@ -621,7 +624,9 @@ export function useCommonLogsColumns(isAdmin: boolean): ColumnDef<UsageLog>[] {
|
||||
cardRole: 'primary',
|
||||
cardOrder: 40,
|
||||
cardSpan: 2,
|
||||
contentMode: 'full',
|
||||
// 'summary' (not 'full') so the group/ratio meta line can truncate
|
||||
// instead of overflowing or wrapping to extra rows.
|
||||
contentMode: 'summary',
|
||||
},
|
||||
})
|
||||
columns.push(
|
||||
|
||||
+12
-11
@@ -23,7 +23,7 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { CopyableStatusBadge, StatusBadge } from '@/components/status-badge'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { getUserAvatarFallback, getUserAvatarStyle } from '@/lib/avatar'
|
||||
import { getUserAvatarFallback, getUserAvatarProps } from '@/lib/avatar'
|
||||
import { formatTimestampToDate } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -85,6 +85,9 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
useUsageLogsContext()
|
||||
const log = row.original
|
||||
const displayName = log.username || String(log.user_id || '?')
|
||||
const avatarProps = sensitiveVisible
|
||||
? getUserAvatarProps(displayName)
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -96,20 +99,18 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
setUserInfoDialogOpen(true)
|
||||
}}
|
||||
>
|
||||
<Avatar className='ring-border/60 size-6 ring-1 max-sm:hidden'>
|
||||
<Avatar className='size-6 max-sm:hidden'>
|
||||
<AvatarFallback
|
||||
className={cn(
|
||||
'text-xs font-semibold',
|
||||
!sensitiveVisible && 'bg-muted text-muted-foreground'
|
||||
)}
|
||||
style={
|
||||
sensitiveVisible ? getUserAvatarStyle(displayName) : undefined
|
||||
}
|
||||
className={cn('text-xs font-semibold', avatarProps?.className)}
|
||||
style={avatarProps?.style}
|
||||
>
|
||||
{sensitiveVisible ? getUserAvatarFallback(displayName) : '•'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className='text-muted-foreground text-sm [overflow-wrap:anywhere] break-words hover:underline'>
|
||||
<span
|
||||
className='text-muted-foreground max-w-[100px] truncate text-sm hover:underline'
|
||||
title={sensitiveVisible ? displayName : undefined}
|
||||
>
|
||||
{sensitiveVisible ? displayName : '••••'}
|
||||
</span>
|
||||
</button>
|
||||
@@ -143,7 +144,7 @@ export function useTaskLogsColumns(isAdmin: boolean): ColumnDef<TaskLog>[] {
|
||||
>
|
||||
{taskId}
|
||||
</CopyableStatusBadge>
|
||||
<span className='text-subtle-foreground text-xs [overflow-wrap:anywhere] break-words'>
|
||||
<span className='text-subtle-foreground max-w-[200px] truncate text-xs'>
|
||||
{getTaskPlatformName(log.platform)} ·{' '}
|
||||
{t(taskActionMapper.getLabel(log.action))}
|
||||
</span>
|
||||
|
||||
+36
-9
@@ -31,6 +31,8 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { toIntlLocale } from '@/i18n/languages'
|
||||
import { formatQuotaWithCurrency } from '@/lib/currency'
|
||||
import { formatQuota, formatTimestamp } from '@/lib/format'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
@@ -43,6 +45,12 @@ import {
|
||||
import type { User } from '../types'
|
||||
import { DataTableRowActions } from './data-table-row-actions'
|
||||
|
||||
/**
|
||||
* Inline quota values longer than this switch to locale-aware compact
|
||||
* notation (e.g. "¥4.8万"); precise values stay available in the tooltip.
|
||||
*/
|
||||
const MAX_INLINE_QUOTA_CHARS = 8
|
||||
|
||||
function getQuotaProgressColor(percentage: number): string {
|
||||
if (percentage <= 10) {
|
||||
return '[&_[data-slot=progress-indicator]]:bg-destructive'
|
||||
@@ -54,7 +62,8 @@ function getQuotaProgressColor(percentage: number): string {
|
||||
}
|
||||
|
||||
export function useUsersColumns(): ColumnDef<User>[] {
|
||||
const { t } = useTranslation()
|
||||
const { t, i18n } = useTranslation()
|
||||
const locale = toIntlLocale(i18n.resolvedLanguage || i18n.language)
|
||||
return [
|
||||
{
|
||||
id: 'select',
|
||||
@@ -108,13 +117,23 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
|
||||
return (
|
||||
<div className='flex min-w-0 flex-col gap-1 sm:min-w-[160px]'>
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<LongText className='max-w-full font-medium sm:max-w-[140px]'>
|
||||
<div className='flex min-w-0 items-center gap-2'>
|
||||
<LongText className='max-w-full shrink-0 font-medium sm:max-w-[140px]'>
|
||||
{username}
|
||||
</LongText>
|
||||
{remark && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger render={<StatusBadge variant='success' />}>
|
||||
<TooltipTrigger
|
||||
render={
|
||||
<StatusBadge
|
||||
variant='success'
|
||||
// Keep the remark on the username line: allow the badge
|
||||
// to shrink and ellipsize instead of wrapping to a new
|
||||
// row (full text stays in the tooltip).
|
||||
className='min-w-0 shrink overflow-hidden [&_[data-slot=status-badge-label]]:max-w-full [&_[data-slot=status-badge-label]]:min-w-0 [&_[data-slot=status-badge-label]]:shrink [&_[data-slot=status-badge-label]]:overflow-hidden [&_[data-slot=status-badge-label]]:text-ellipsis'
|
||||
/>
|
||||
}
|
||||
>
|
||||
{remark}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -191,6 +210,14 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
return <StatusBadge variant='neutral'>{t('No Quota')}</StatusBadge>
|
||||
}
|
||||
|
||||
const toInlineQuota = (value: number) => {
|
||||
const full = formatQuota(value)
|
||||
if (full.length <= MAX_INLINE_QUOTA_CHARS) {
|
||||
return full
|
||||
}
|
||||
return formatQuotaWithCurrency(value, { compact: true, locale })
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
@@ -198,12 +225,12 @@ export function useUsersColumns(): ColumnDef<User>[] {
|
||||
<div className='w-full cursor-help space-y-1 sm:w-[150px]' />
|
||||
}
|
||||
>
|
||||
<div className='flex justify-between text-xs'>
|
||||
<span className='font-medium tabular-nums'>
|
||||
{formatQuota(remaining)}
|
||||
<div className='flex justify-between gap-2 text-xs'>
|
||||
<span className='truncate font-medium tabular-nums'>
|
||||
{toInlineQuota(remaining)}
|
||||
</span>
|
||||
<span className='text-muted-foreground tabular-nums'>
|
||||
{formatQuota(total)}
|
||||
<span className='text-muted-foreground truncate tabular-nums'>
|
||||
{toInlineQuota(total)}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
|
||||
Vendored
+21
-30
@@ -18,40 +18,31 @@ For commercial licensing, please contact support@quantumnous.com
|
||||
*/
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
export type UserAvatarStyle = Pick<CSSProperties, 'backgroundImage' | 'color'>
|
||||
import { getIdentityTextColorClass } from '@/lib/colors'
|
||||
|
||||
/*
|
||||
* Curated duotone gradients (avatar.vercel.sh style). A raw hash-to-hue
|
||||
* mapping lands on murky olive/mustard tones for many names; a hand-picked
|
||||
* palette keeps every user identity vivid and readable (white initial) in
|
||||
* both light and dark themes.
|
||||
*/
|
||||
const AVATAR_GRADIENTS: ReadonlyArray<readonly [string, string]> = [
|
||||
['#4f46e5', '#7c3aed'], // indigo -> violet
|
||||
['#2563eb', '#0891b2'], // blue -> cyan
|
||||
['#7c3aed', '#db2777'], // violet -> pink
|
||||
['#e11d48', '#ea580c'], // rose -> orange
|
||||
['#059669', '#0d9488'], // emerald -> teal
|
||||
['#0284c7', '#4f46e5'], // sky -> indigo
|
||||
['#db2777', '#e11d48'], // pink -> rose
|
||||
['#0d9488', '#0284c7'], // teal -> sky
|
||||
]
|
||||
|
||||
function hashString(value: string): number {
|
||||
let hash = 0
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
hash = (hash * 31 + value.charCodeAt(i)) >>> 0
|
||||
}
|
||||
return hash
|
||||
export interface UserAvatarProps {
|
||||
className: string
|
||||
style: CSSProperties
|
||||
}
|
||||
|
||||
export function getUserAvatarStyle(name: string): UserAvatarStyle {
|
||||
const [from, to] =
|
||||
AVATAR_GRADIENTS[hashString(name) % AVATAR_GRADIENTS.length]
|
||||
|
||||
/**
|
||||
* Soft identity tint (Linear/Notion style): a low-opacity wash of the user's
|
||||
* stable identity hue, the initial in the full-strength hue, and a faint
|
||||
* matching inner ring. The class sets the identity text color and the inline
|
||||
* style derives background/ring from `currentColor` with the same mix ratios
|
||||
* as identity badges, so avatars match group/model badges and adapt to
|
||||
* light/dark themes automatically. Inline styles also win over the fallback's
|
||||
* default `bg-muted` deterministically.
|
||||
*/
|
||||
export function getUserAvatarProps(name: string): UserAvatarProps {
|
||||
return {
|
||||
backgroundImage: `linear-gradient(135deg, ${from} 0%, ${to} 100%)`,
|
||||
color: 'white',
|
||||
className: getIdentityTextColorClass(name),
|
||||
style: {
|
||||
backgroundColor:
|
||||
'color-mix(in oklch, currentColor var(--identity-surface-mix), transparent)',
|
||||
boxShadow:
|
||||
'inset 0 0 0 1px color-mix(in oklch, currentColor var(--identity-border-mix), transparent)',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user