diff --git a/apps/sim/app/workspace/[workspaceId]/components/presence/presence-avatars.tsx b/apps/sim/app/workspace/[workspaceId]/components/presence/presence-avatars.tsx index ff53ee14325..386c026a551 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/presence/presence-avatars.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/presence/presence-avatars.tsx @@ -37,7 +37,7 @@ function UserAvatar({ user, index }: UserAvatarProps) { )} {initials} @@ -98,7 +98,7 @@ export function PresenceAvatars({ style={{ zIndex: 0 }} aria-label={`${overflowCount} more ${overflowCount === 1 ? 'user' : 'users'}`} > - + +{overflowCount} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx index ebbf3bb9e9b..ad14698ae52 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx @@ -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' @@ -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 ( - {name} - ) - } - return ( - - {name.charAt(0).toUpperCase()} - + + {image && } + {name.charAt(0).toUpperCase()} + ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx index aa11d37b50f..97c5be6db21 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx @@ -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' @@ -18,21 +18,11 @@ interface MemberAvatarProps { * the member's name when no image is available. */ export function MemberAvatar({ name, image }: MemberAvatarProps) { - if (image) { - return ( - {name} - ) - } - return ( - - {name.charAt(0).toUpperCase()} - + + {image && } + {name.charAt(0).toUpperCase()} + ) } diff --git a/packages/emcn/src/components/avatar/avatar.test.tsx b/packages/emcn/src/components/avatar/avatar.test.tsx new file mode 100644 index 00000000000..474d9242536 --- /dev/null +++ b/packages/emcn/src/components/avatar/avatar.test.tsx @@ -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( + <> + + A + + + BC + + + + D + + + + ) + ) + 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) + }) +}) diff --git a/packages/emcn/src/components/avatar/avatar.tsx b/packages/emcn/src/components/avatar/avatar.tsx index 85ebaa522c8..f2dc07acc56 100644 --- a/packages/emcn/src/components/avatar/avatar.tsx +++ b/packages/emcn/src/components/avatar/avatar.tsx @@ -50,6 +50,8 @@ const avatarStatusVariants = cva( } ) +const AvatarSizeContext = React.createContext['size']>('md') + type AvatarStatus = 'online' | 'offline' | 'busy' | 'away' interface AvatarProps @@ -93,22 +95,24 @@ interface AvatarProps */ const Avatar = React.forwardRef, AvatarProps>( ({ className, size, status, children, ...props }, ref) => ( -
- - {children} - - {status && ( - - )} -
+ +
+ + {children} + + {status && ( + + )} +
+
) ) Avatar.displayName = 'Avatar' @@ -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 @@ -142,16 +147,20 @@ AvatarImage.displayName = 'AvatarImage' const AvatarFallback = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) +>(({ className, ...props }, ref) => { + const size = React.useContext(AvatarSizeContext) + return ( + + ) +}) AvatarFallback.displayName = 'AvatarFallback' export { Avatar, AvatarImage, AvatarFallback, avatarVariants, avatarStatusVariants }