Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function UserAvatar({ user, index }: UserAvatarProps) {
)}
<AvatarFallback
style={{ background: color }}
className='border-0 font-semibold text-[7px] text-white leading-none'
className='border-0 font-semibold text-white leading-none'
>
{initials}
</AvatarFallback>
Expand Down Expand Up @@ -98,7 +98,7 @@ export function PresenceAvatars({
style={{ zIndex: 0 }}
aria-label={`${overflowCount} more ${overflowCount === 1 ? 'user' : 'users'}`}
>
<AvatarFallback className='border-0 bg-gray-700 font-semibold text-[7px] text-white leading-none'>
<AvatarFallback className='border-0 bg-gray-700 font-semibold text-white leading-none'>
+{overflowCount}
</AvatarFallback>
</Avatar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { memo } from 'react'
import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn'
import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource'
import type { WorkspaceMember } from '@/hooks/queries/workspace'

Expand All @@ -13,21 +14,11 @@ export interface OwnerAvatarProps {
* owner/uploaded-by filter options on every list.
*/
export const OwnerAvatar = memo(function OwnerAvatar({ name, image }: OwnerAvatarProps) {
if (image) {
return (
<img
src={image}
alt={name}
referrerPolicy='no-referrer'
className='size-[14px] rounded-full border border-[var(--border)] object-cover'
/>
)
}

return (
<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)]'>
{name.charAt(0).toUpperCase()}
</span>
<Avatar size='xs'>
{image && <AvatarImage src={image} alt={name} referrerPolicy='no-referrer' />}
<AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
)
})

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

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

Expand All @@ -18,21 +18,11 @@ interface MemberAvatarProps {
* the member's name when no image is available.
*/
export function MemberAvatar({ name, image }: MemberAvatarProps) {
if (image) {
return (
<img
src={image}
alt={name}
referrerPolicy='no-referrer'
className='size-[14px] shrink-0 rounded-full border border-[var(--border)] object-cover'
/>
)
}

return (
<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)]'>
{name.charAt(0).toUpperCase()}
</span>
<Avatar size='xs'>
{image && <AvatarImage src={image} alt={name} referrerPolicy='no-referrer' />}
<AvatarFallback>{name.charAt(0).toUpperCase()}</AvatarFallback>
</Avatar>
)
}

Expand Down
49 changes: 49 additions & 0 deletions packages/emcn/src/components/avatar/avatar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/** @vitest-environment jsdom */
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it } from 'vitest'
import { Avatar, AvatarFallback } from './avatar'

let root: Root | undefined
let container: HTMLDivElement | undefined

afterEach(() => {
act(() => root?.unmount())
container?.remove()
})

describe('Avatar fallback sizing', () => {
it('scopes xs sizing to its avatar while preserving explicit overrides', () => {
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true })
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)
act(() =>
root?.render(
<>
<Avatar size='xs'>
<AvatarFallback data-testid='xs'>A</AvatarFallback>
</Avatar>
<Avatar>
<AvatarFallback data-testid='default'>BC</AvatarFallback>
</Avatar>
<Avatar size='xs'>
<AvatarFallback data-testid='override' className='text-[7px]'>
D
</AvatarFallback>
</Avatar>
</>
)
)
const xs = container.querySelector('[data-testid="xs"]')!
const regular = container.querySelector('[data-testid="default"]')!
const overridden = container.querySelector('[data-testid="override"]')!
expect(xs.classList.contains('text-[8px]')).toBe(true)
expect(xs.classList.contains('text-xs')).toBe(false)
expect(regular.classList.contains('text-xs')).toBe(true)
expect(regular.classList.contains('text-[8px]')).toBe(false)
expect(overridden.classList.contains('text-[7px]')).toBe(true)
expect(overridden.classList.contains('text-[8px]')).toBe(false)
expect(overridden.classList.contains('text-xs')).toBe(false)
})
})
61 changes: 35 additions & 26 deletions packages/emcn/src/components/avatar/avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const avatarStatusVariants = cva(
}
)

const AvatarSizeContext = React.createContext<VariantProps<typeof avatarVariants>['size']>('md')

type AvatarStatus = 'online' | 'offline' | 'busy' | 'away'

interface AvatarProps
Expand Down Expand Up @@ -93,22 +95,24 @@ interface AvatarProps
*/
const Avatar = React.forwardRef<React.ElementRef<typeof AvatarPrimitive.Root>, AvatarProps>(
({ className, size, status, children, ...props }, ref) => (
<div className='relative inline-flex'>
<AvatarPrimitive.Root
ref={ref}
className={cn(avatarVariants({ size }), className)}
{...props}
>
{children}
</AvatarPrimitive.Root>
{status && (
<span
data-slot='avatar-status'
className={cn(avatarStatusVariants({ status, size }))}
aria-label={`Status: ${status}`}
/>
)}
</div>
<AvatarSizeContext.Provider value={size ?? 'md'}>
<div className='relative inline-flex'>
<AvatarPrimitive.Root
ref={ref}
className={cn(avatarVariants({ size }), className)}
{...props}
>
{children}
</AvatarPrimitive.Root>
{status && (
<span
data-slot='avatar-status'
className={cn(avatarStatusVariants({ status, size }))}
aria-label={`Status: ${status}`}
/>
)}
</div>
</AvatarSizeContext.Provider>
)
)
Avatar.displayName = 'Avatar'
Expand All @@ -133,6 +137,7 @@ AvatarImage.displayName = 'AvatarImage'

/**
* Fallback component for Avatar. Displays initials or icon when image is unavailable.
* The xs size uses 8px initials; other sizes retain text-xs.
*
* Carries the package's only hardcoded `font-medium`, and deliberately: one or
* two capitals at `text-xs` on a filled disc are a glyph, not running text, and
Expand All @@ -142,16 +147,20 @@ AvatarImage.displayName = 'AvatarImage'
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'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',
className
)}
{...props}
/>
))
>(({ className, ...props }, ref) => {
const size = React.useContext(AvatarSizeContext)
return (
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
'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',
size === 'xs' && 'text-[8px]',
Comment thread
BillLeoutsakosvl346 marked this conversation as resolved.
className
)}
{...props}
/>
)
})
AvatarFallback.displayName = 'AvatarFallback'

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