Skip to content

Commit 4491848

Browse files
BillLeoutsakosvl346Bill Leoutsakos
andauthored
improvement(ui): reuse avatars for owners and members (#8079)
Co-authored-by: Bill Leoutsakos <billleoutsakos@Bills-MacBook-Pro.local>
1 parent c5a5c75 commit 4491848

5 files changed

Lines changed: 96 additions & 57 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/presence/presence-avatars.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ function UserAvatar({ user, index }: UserAvatarProps) {
3737
)}
3838
<AvatarFallback
3939
style={{ background: color }}
40-
className='border-0 font-semibold text-[7px] text-white leading-none'
40+
className='border-0 font-semibold text-white leading-none'
4141
>
4242
{initials}
4343
</AvatarFallback>
@@ -98,7 +98,7 @@ export function PresenceAvatars({
9898
style={{ zIndex: 0 }}
9999
aria-label={`${overflowCount} more ${overflowCount === 1 ? 'user' : 'users'}`}
100100
>
101-
<AvatarFallback className='border-0 bg-gray-700 font-semibold text-[7px] text-white leading-none'>
101+
<AvatarFallback className='border-0 bg-gray-700 font-semibold text-white leading-none'>
102102
+{overflowCount}
103103
</AvatarFallback>
104104
</Avatar>

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx‎

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { memo } from 'react'
2+
import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn'
23
import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource'
34
import type { WorkspaceMember } from '@/hooks/queries/workspace'
45

@@ -13,21 +14,11 @@ export interface OwnerAvatarProps {
1314
* owner/uploaded-by filter options on every list.
1415
*/
1516
export const OwnerAvatar = memo(function OwnerAvatar({ name, image }: OwnerAvatarProps) {
16-
if (image) {
17-
return (
18-
<img
19-
src={image}
20-
alt={name}
21-
referrerPolicy='no-referrer'
22-
className='size-[14px] rounded-full border border-[var(--border)] object-cover'
23-
/>
24-
)
25-
}
26-
2717
return (
28-
<span className='flex size-[14px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
29-
{name.charAt(0).toUpperCase()}
30-
</span>
18+
<Avatar size='xs'>
19+
{image && <AvatarImage src={image} alt={name} referrerPolicy='no-referrer' />}
20+
<AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
21+
</Avatar>
3122
)
3223
})
3324

‎apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import type { ReactNode } from 'react'
4-
import { OverflowText } from '@sim/emcn'
4+
import { Avatar, AvatarFallback, AvatarImage, OverflowText } from '@sim/emcn'
55
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
66
import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section'
77

@@ -18,21 +18,11 @@ interface MemberAvatarProps {
1818
* the member's name when no image is available.
1919
*/
2020
export function MemberAvatar({ name, image }: MemberAvatarProps) {
21-
if (image) {
22-
return (
23-
<img
24-
src={image}
25-
alt={name}
26-
referrerPolicy='no-referrer'
27-
className='size-[14px] shrink-0 rounded-full border border-[var(--border)] object-cover'
28-
/>
29-
)
30-
}
31-
3221
return (
33-
<span className='flex size-[14px] shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
34-
{name.charAt(0).toUpperCase()}
35-
</span>
22+
<Avatar size='xs'>
23+
{image && <AvatarImage src={image} alt={name} referrerPolicy='no-referrer' />}
24+
<AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
25+
</Avatar>
3626
)
3727
}
3828

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @vitest-environment jsdom */
2+
import { act } from 'react'
3+
import { createRoot, type Root } from 'react-dom/client'
4+
import { afterEach, describe, expect, it } from 'vitest'
5+
import { Avatar, AvatarFallback } from './avatar'
6+
7+
let root: Root | undefined
8+
let container: HTMLDivElement | undefined
9+
10+
afterEach(() => {
11+
act(() => root?.unmount())
12+
container?.remove()
13+
})
14+
15+
describe('Avatar fallback sizing', () => {
16+
it('scopes xs sizing to its avatar while preserving explicit overrides', () => {
17+
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
18+
container = document.createElement('div')
19+
document.body.appendChild(container)
20+
root = createRoot(container)
21+
act(() =>
22+
root?.render(
23+
<>
24+
<Avatar size='xs'>
25+
<AvatarFallback data-testid='xs'>A</AvatarFallback>
26+
</Avatar>
27+
<Avatar>
28+
<AvatarFallback data-testid='default'>BC</AvatarFallback>
29+
</Avatar>
30+
<Avatar size='xs'>
31+
<AvatarFallback data-testid='override' className='text-[7px]'>
32+
D
33+
</AvatarFallback>
34+
</Avatar>
35+
</>
36+
)
37+
)
38+
const xs = container.querySelector('[data-testid="xs"]')!
39+
const regular = container.querySelector('[data-testid="default"]')!
40+
const overridden = container.querySelector('[data-testid="override"]')!
41+
expect(xs.classList.contains('text-[8px]')).toBe(true)
42+
expect(xs.classList.contains('text-xs')).toBe(false)
43+
expect(regular.classList.contains('text-xs')).toBe(true)
44+
expect(regular.classList.contains('text-[8px]')).toBe(false)
45+
expect(overridden.classList.contains('text-[7px]')).toBe(true)
46+
expect(overridden.classList.contains('text-[8px]')).toBe(false)
47+
expect(overridden.classList.contains('text-xs')).toBe(false)
48+
})
49+
})

‎packages/emcn/src/components/avatar/avatar.tsx‎

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ const avatarStatusVariants = cva(
5050
}
5151
)
5252

53+
const AvatarSizeContext = React.createContext<VariantProps<typeof avatarVariants>['size']>('md')
54+
5355
type AvatarStatus = 'online' | 'offline' | 'busy' | 'away'
5456

5557
interface AvatarProps
@@ -93,22 +95,24 @@ interface AvatarProps
9395
*/
9496
const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, AvatarProps>(
9597
({ className, size, status, children, ...props }, ref) => (
96-
<div className='relative inline-flex'>
97-
<AvatarPrimitive.Root
98-
ref={ref}
99-
className={cn(avatarVariants({ size }), className)}
100-
{...props}
101-
>
102-
{children}
103-
</AvatarPrimitive.Root>
104-
{status && (
105-
<span
106-
data-slot='avatar-status'
107-
className={cn(avatarStatusVariants({ status, size }))}
108-
aria-label={`Status: ${status}`}
109-
/>
110-
)}
111-
</div>
98+
<AvatarSizeContext.Provider value={size ?? 'md'}>
99+
<div className='relative inline-flex'>
100+
<AvatarPrimitive.Root
101+
ref={ref}
102+
className={cn(avatarVariants({ size }), className)}
103+
{...props}
104+
>
105+
{children}
106+
</AvatarPrimitive.Root>
107+
{status && (
108+
<span
109+
data-slot='avatar-status'
110+
className={cn(avatarStatusVariants({ status, size }))}
111+
aria-label={`Status: ${status}`}
112+
/>
113+
)}
114+
</div>
115+
</AvatarSizeContext.Provider>
112116
)
113117
)
114118
Avatar.displayName = 'Avatar'
@@ -133,6 +137,7 @@ AvatarImage.displayName = 'AvatarImage'
133137

134138
/**
135139
* Fallback component for Avatar. Displays initials or icon when image is unavailable.
140+
* The xs size uses 8px initials; other sizes retain text-xs.
136141
*
137142
* Carries the package's only hardcoded `font-medium`, and deliberately: one or
138143
* two capitals at `text-xs` on a filled disc are a glyph, not running text, and
@@ -142,16 +147,20 @@ AvatarImage.displayName = 'AvatarImage'
142147
const AvatarFallback = React.forwardRef<
143148
React.ElementRef<typeof AvatarPrimitive.Fallback>,
144149
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
145-
>(({ className, ...props }, ref) => (
146-
<AvatarPrimitive.Fallback
147-
ref={ref}
148-
className={cn(
149-
'flex h-full w-full items-center justify-center rounded-full border border-[var(--border-1)] bg-[var(--surface-4)] font-medium text-[var(--text-secondary)] text-xs',
150-
className
151-
)}
152-
{...props}
153-
/>
154-
))
150+
>(({ className, ...props }, ref) => {
151+
const size = React.useContext(AvatarSizeContext)
152+
return (
153+
<AvatarPrimitive.Fallback
154+
ref={ref}
155+
className={cn(
156+
'flex h-full w-full items-center justify-center rounded-full border border-[var(--border-1)] bg-[var(--surface-4)] font-medium text-[var(--text-secondary)] text-xs',
157+
size === 'xs' && 'text-[8px]',
158+
className
159+
)}
160+
{...props}
161+
/>
162+
)
163+
})
155164
AvatarFallback.displayName = 'AvatarFallback'
156165

157166
export { Avatar, AvatarImage, AvatarFallback, avatarVariants, avatarStatusVariants }

0 commit comments

Comments
 (0)