Skip to content
Merged
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
11 changes: 6 additions & 5 deletions .claude/rules/sim-settings-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,12 @@ and — on activatable rows only — the hover band. Never hand-roll any of it,
`RESOURCE_TILE_FILL` for a glyph, `RESOURCE_TILE_PLAIN` for a brand logo or favicon.


**Member avatars are deliberately two components, not one.** `member-list.tsx`
renders a 14px neutral marker for the dense Teammates/Organization roster, where
the email is the primary content; `components/permissions/member-row.tsx` renders
a 36px `getUserColor`-hashed avatar for member *management* rows that carry a name,
an email, and a role control. Same shape, different job — do not merge them.
**One member avatar.** Every member list, owner cell, and ranking renders emcn
`<Avatar size='xs' name={…} src={…} aria-hidden />` — a 14px photo, or the initial
on the neutral disc (`aria-hidden` because the name is always beside it). Never
hand-roll an avatar or give a person a `getUserColor` hash; per-person colors
belong to live collaboration (presence, cursors), where the color matches that
person's cursor.

## Header action order

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/add-settings-page/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ For each page component, confirm the checklist in `.claude/rules/sim-settings-pa
`.claude/rules/sim-settings-pages.md` for the token map and the row
title/subtitle pairing convention):
`git grep -nE "text-\[1[0-8]px\]" -- 'apps/sim/**/settings/' 'apps/sim/ee/'` — should
be 0. Display type above the scale (`text-[40px]` hero headings, the `text-[8px]`
member-avatar initial) is deliberate and out of scope.
be 0. Display type above the scale (`text-[40px]` hero headings) is deliberate
and out of scope.
4. Confirm each page imports `SettingsPanel` and that its `NavigationItem` has an
accurate `description` of consistent length with its peers.
- Editable pages: confirm Save/Discard go through `SaveDiscardActions` and
Expand Down
11 changes: 6 additions & 5 deletions .cursor/rules/sim-settings-pages.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,12 @@ and — on activatable rows only — the hover band. Never hand-roll any of it,
`RESOURCE_TILE_FILL` for a glyph, `RESOURCE_TILE_PLAIN` for a brand logo or favicon.


**Member avatars are deliberately two components, not one.** `member-list.tsx`
renders a 14px neutral marker for the dense Teammates/Organization roster, where
the email is the primary content; `components/permissions/member-row.tsx` renders
a 36px `getUserColor`-hashed avatar for member *management* rows that carry a name,
an email, and a role control. Same shape, different job — do not merge them.
**One member avatar.** Every member list, owner cell, and ranking renders emcn
`<Avatar size='xs' name={…} src={…} aria-hidden />` — a 14px photo, or the initial
on the neutral disc (`aria-hidden` because the name is always beside it). Never
hand-roll an avatar or give a person a `getUserColor` hash; per-person colors
belong to live collaboration (presence, cursors), where the color matches that
person's cursor.

## Header action order

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export function ChangelogTimeline({ initialEntries }: ChangelogTimelineProps) {
title={`@${contributor}`}
className='block size-6 shrink-0'
>
<Avatar className='size-6 ring-2 ring-[var(--bg)]'>
<Avatar size='sm' className='ring-2 ring-[var(--bg)]'>
<AvatarImage
src={`https://avatars.githubusercontent.com/${contributor}`}
alt={`@${contributor}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export function CredentialMembersSection({ credentialId, isAdmin }: CredentialMe
return (
<MemberRow
key={member.id}
member={member}
member={{ ...member, userImage: member.userImage ?? null }}
roleOptions={MEMBER_ROLE_OPTIONS}
lockReason={lockReason}
canManage={isAdmin}
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/app/workspace/[workspaceId]/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ export { MessageActions } from './message-actions'
export type { BulkOutcome } from './resource/bulk-outcome'
export { reportBulkOutcome } from './resource/bulk-outcome'
export { FloatingOverflowText } from './resource/components/floating-overflow-text'
export type { OwnerAvatarProps } from './resource/components/owner-cell'
export { OwnerAvatar, ownerCell } from './resource/components/owner-cell'
export { ownerCell } from './resource/components/owner-cell'
export {
type ChromeActionSpec,
ResourceChromeFallback,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,7 @@ function UserAvatar({ user, index }: UserAvatarProps) {
referrerPolicy='no-referrer'
/>
)}
<AvatarFallback
style={{ background: color }}
className='border-0 font-semibold text-[7px] text-white leading-none'
>
<AvatarFallback style={{ background: color }} className='border-0 font-semibold text-white'>
{initials}
</AvatarFallback>
</Avatar>
Expand Down Expand Up @@ -98,7 +95,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-[7px] text-white'>
+{overflowCount}
</AvatarFallback>
</Avatar>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export type { OwnerAvatarProps } from './owner-cell'
export { OwnerAvatar, ownerCell } from './owner-cell'
export { ownerCell } from './owner-cell'
Original file line number Diff line number Diff line change
@@ -1,36 +1,7 @@
import { memo } from 'react'
import { Avatar } from '@sim/emcn'
import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource'
import type { WorkspaceMember } from '@/hooks/queries/workspace'

export interface OwnerAvatarProps {
name: string
image: string | null
}

/**
* The canonical 14px workspace-member avatar — a photo, or the member's initial on a neutral
* disc. Shared so a member reads identically in a resource row's owner cell and in the
* 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>
)
})

/**
* Resolves a user ID into a ResourceCell with an avatar icon and display name.
* Returns null label while members are still loading to avoid flashing raw IDs.
Expand All @@ -51,7 +22,7 @@ export function ownerCell(
if (!member) return { label: null }

return {
icon: <OwnerAvatar name={member.name} image={member.image} />,
icon: <Avatar size='xs' name={member.name} src={member.image} aria-hidden />,
label: member.name,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ const Pagination = memo(function Pagination({
})

interface CellContentProps {
/** Pre-rendered icon node (svg/img/span avatar); auto-sized to the chip icon size. */
/** Pre-rendered icon node (a glyph or a person's `Avatar`); auto-sized to the chip icon size. */
icon?: ReactNode
label: string
content?: ReactNode
Expand Down
8 changes: 3 additions & 5 deletions apps/sim/app/workspace/[workspaceId]/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import {
Avatar,
Button,
ChipCombobox,
ChipConfirmModal,
Expand Down Expand Up @@ -79,10 +80,7 @@ import {
useFolderRowDragDrop,
} from '@/app/workspace/[workspaceId]/components/folders'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import {
OwnerAvatar,
ownerCell,
} from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import { ownerCell } from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import {
FilesEmptyState,
ResourceNoResults,
Expand Down Expand Up @@ -1938,7 +1936,7 @@ function FilesContent() {
(members ?? []).map((m) => ({
value: m.userId,
label: m.name,
iconElement: <OwnerAvatar name={m.name} image={m.image} />,
iconElement: <Avatar size='xs' name={m.name} src={m.image} aria-hidden />,
})),
[members]
)
Expand Down
9 changes: 3 additions & 6 deletions apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ChipDropdownOption } from '@sim/emcn'
import { Button, ChipConfirmModal, ChipDropdown, Tooltip, toast } from '@sim/emcn'
import { Avatar, Button, ChipConfirmModal, ChipDropdown, Tooltip, toast } from '@sim/emcn'
import { Database, FolderPlus, Pencil, Plus, Trash } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
Expand Down Expand Up @@ -39,10 +39,7 @@ import {
} from '@/app/workspace/[workspaceId]/components/folders'
import { reportBulkOutcome } from '@/app/workspace/[workspaceId]/components/resource/bulk-outcome'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import {
OwnerAvatar,
ownerCell,
} from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import { ownerCell } from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import {
KnowledgeEmptyState,
ResourceNoResults,
Expand Down Expand Up @@ -1331,7 +1328,7 @@ function KnowledgeContent() {
(members ?? []).map((m) => ({
value: m.userId,
label: m.name,
iconElement: <OwnerAvatar name={m.name} image={m.image} />,
iconElement: <Avatar size='xs' name={m.name} src={m.image} aria-hidden />,
})),
[members]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { MemberAvatar, MemberRow, MemberSection } from './member-list'
export { MemberRow, MemberSection } from './member-list'
Original file line number Diff line number Diff line change
@@ -1,41 +1,13 @@
'use client'

import type { ReactNode } from 'react'
import { OverflowText } from '@sim/emcn'
import { Avatar, 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'

const ROW_CLASSES = 'flex items-center gap-2.5 p-2'
const ROW_STATUS_CLASSES = 'shrink-0 text-[var(--text-muted)] text-caption'

interface MemberAvatarProps {
name: string
image: string | null
}

/**
* 14px circular avatar used in member rows. Falls back to the first letter of
* 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>
)
}

interface MemberRowProps {
name: string
email: string
Expand All @@ -56,7 +28,7 @@ interface MemberRowProps {
export function MemberRow({ name, email, image, status, roleControl, menu }: MemberRowProps) {
return (
<div className={ROW_CLASSES}>
<MemberAvatar name={name} image={image} />
<Avatar size='xs' name={name} src={image} />
<OverflowText label={email} className='flex-1 text-[var(--text-body)] text-sm' />
<span className={ROW_STATUS_CLASSES}>{status}</span>
{roleControl}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
import { useMemo, useState } from 'react'
import {
Avatar,
AvatarFallback,
AvatarImage,
Badge,
Banner,
ChipConfirmModal,
Expand All @@ -15,7 +13,6 @@ import {
Skeleton,
} from '@sim/emcn'
import { getErrorMessage } from '@sim/utils/errors'
import { getUserColor } from '@/lib/workspaces/colors'
import type { RosterMember } from '@/hooks/queries/organization'

interface TransferOwnershipDialogProps {
Expand Down Expand Up @@ -167,21 +164,13 @@ export function TransferOwnershipDialog({
aria-pressed={isSelected}
onClick={() => setSelectedUserId(m.userId)}
className={cn(
'flex w-full items-center gap-3 px-3 py-2 text-left transition-colors',
'flex w-full items-center gap-2.5 px-3 py-2 text-left transition-colors',
isSelected
? 'bg-[var(--surface-active)]'
: 'hover-hover:bg-[var(--surface-hover)]'
)}
>
<Avatar className='size-8 shrink-0'>
{m.image && <AvatarImage src={m.image} alt={m.name} />}
<AvatarFallback
style={{ background: getUserColor(m.userId || m.email) }}
className='border-0 text-white'
>
{m.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<Avatar size='xs' name={m.name} src={m.image} aria-hidden />
<div className='min-w-0 flex-1'>
<div className='flex items-center gap-2'>
<OverflowText
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export function SkillEditorsCard({ editors, canEdit }: SkillEditorsCardProps) {
userId: editor.userId,
userName: editor.userName,
userEmail: editor.userEmail,
userImage: editor.userImage,
role: 'editor',
}}
roleOptions={SKILL_EDITOR_ROLE_OPTIONS}
Expand Down
9 changes: 3 additions & 6 deletions apps/sim/app/workspace/[workspaceId]/tables/tables.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import type { ComboboxOption } from '@sim/emcn'
import { ChipCombobox, ChipConfirmModal, Plus, toast, Upload } from '@sim/emcn'
import { Avatar, ChipCombobox, ChipConfirmModal, Plus, toast, Upload } from '@sim/emcn'
import { Columns3, FolderPlus, Pencil, Rows3, Table as TableIcon, Trash } from '@sim/emcn/icons'
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
Expand Down Expand Up @@ -39,10 +39,7 @@ import {
} from '@/app/workspace/[workspaceId]/components/folders'
import { reportBulkOutcome } from '@/app/workspace/[workspaceId]/components/resource/bulk-outcome'
import { ResourceActionBar } from '@/app/workspace/[workspaceId]/components/resource/components/action-bar'
import {
OwnerAvatar,
ownerCell,
} from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import { ownerCell } from '@/app/workspace/[workspaceId]/components/resource/components/owner-cell'
import {
ResourceNoResults,
TablesEmptyState,
Expand Down Expand Up @@ -721,7 +718,7 @@ function TablesContent() {
(members ?? []).map((m) => ({
value: m.userId,
label: m.name,
iconElement: <OwnerAvatar name={m.name} image={m.image} />,
iconElement: <Avatar size='xs' name={m.name} src={m.image} aria-hidden />,
})),
[members]
)
Expand Down
18 changes: 8 additions & 10 deletions apps/sim/components/permissions/member-row.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
'use client'

import { Avatar, AvatarFallback, Chip, ChipDropdown, cn, OverflowText } from '@sim/emcn'
import { getUserColor } from '@/lib/workspaces/colors'
import { Avatar, Chip, ChipDropdown, cn, OverflowText } from '@sim/emcn'
import type { MemberRole } from './member-role-options'
import { RoleLockTooltip } from './role-lock'

export interface MemberRowMember<TRole extends string = MemberRole> {
userId: string
userName: string | null
userEmail: string | null
userImage: string | null
role: TRole
}

Expand Down Expand Up @@ -54,14 +54,12 @@ export function MemberRow<TRole extends string = MemberRole>({
)}
>
<div className='flex min-w-0 items-center gap-2.5'>
<Avatar className='size-9 shrink-0'>
<AvatarFallback
style={{ background: getUserColor(member.userId || member.userEmail || '') }}
className='border border-[var(--border-1)] text-small text-white'
>
{(member.userName || member.userEmail || '?').charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<Avatar
size='xs'
name={member.userName || member.userEmail || '?'}
src={member.userImage}
aria-hidden
/>
<div className='flex min-w-0 flex-col'>
<OverflowText
label={member.userName || member.userEmail || member.userId}
Expand Down
Loading
Loading