UserProfile
Avatar plus name and optional meta in one block. Account headers and people cards.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Account header block:
```tsx import { UserProfile } from '@/components/atoms/UserProfile';
<UserProfile name="Ada Lovelace" org="Analytical Engines" avatar={{ type: 'initials', size: 's' }} /> ```
## Accesibilidad
- `name` is required. Initials default from the name; when using image avatars, pass meaningful `alt` via `avatar` props. - Treat as a presentational cluster — interactive menus belong on a surrounding button/menu, not only on the faces.
## Cuándo no usar
- Single avatar without name/org → `Avatar`. - Dense assignee stacks → `AvatarGroup`.
## Criterio de uso
- Usa UserProfile como identidad, no como menu: si necesita acciones, envuelvelo en `DropdownMenu`. - El nombre es lo minimo indispensable; `org` solo aporta cuando el usuario pertenece a varias. - En sidebar colapsado se reduce a avatar: la inicial o imagen debe seguir identificando sin el texto.
## Gotchas
- Sin imagen cae a iniciales: nombres de una sola palabra dan una inicial, y con pocos usuarios eso deja de distinguir. - El avatar no es un boton: si es clickeable, el elemento interactivo lo pone el contenedor.
Uso
import { UserProfile } from '@/components/atoms/UserProfile';
<UserProfile name="Ada Lovelace" org="Analytical Engines" avatar={{ type: 'initials', size: 's' }} />Props
name: string (required)org: stringavatar: Omit<AvatarProps, 'className'>
Gotchas
- react
name is required. avatar spreads Avatar props; initials default from name words. org is optional subtitle.
- a11y
Ensure Avatar gets a meaningful alt/aria via avatar props when using image type.
Anatomía CSS
<div class="user-profile"> <span class="user-profile__info"></span> <span class="user-profile__name"></span> <span class="user-profile__org"></span> </div>
| Clase | Propósito |
|---|---|
user-profile | root |
user-profile__info | element |
user-profile__name | element |
user-profile__org | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | hover-bg | #f5f5f5 | muted |
| all | fg | #525252 | muted.foreground |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
background-color | 0.15s | cubic-bezier(0.19 |
1 | | |
0.22 | | |
1) | | |
opacity | 0.2s | ease |
width | 0.2s | ease |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
User Profile (Molecule)
Avatar + name + org/role in a row. Collapses to avatar-only
when parent constrains width (e.g., sidebar collapsed).
Composes: Avatar atom.
Used in: sidebar footer, dropdowns, settings pages.
------------------------------------------------------------------------- */
.user-profile {
display: flex;
align-items: center;
gap: 8px;
overflow: hidden;
cursor: pointer;
padding: 4px;
border-radius: 8px;
text-decoration: none;
transition: background-color 0.15s cubic-bezier(0.19, 1, 0.22, 1);
}
.user-profile:hover {
background-color: #f5f5f5;
}
.user-profile__info {
display: flex;
flex-direction: column;
gap: 2px;
overflow: hidden;
min-width: 0;
transition: opacity 0.2s ease, width 0.2s ease;
}
.user-profile__name {
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
font-weight: 500;
line-height: 1;
color: #0a0a0a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-profile__org {
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 10.24px;
font-weight: 400;
line-height: 1;
color: #525252;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Collapses in sidebar collapsed state */
.sidebar--collapsed .user-profile__info {
opacity: 0;
width: 0;
overflow: hidden;
}
.sidebar--collapsed .user-profile {
padding: 4px;
justify-content: center;
overflow: visible;
}
Codigo fuente
import { Avatar, type AvatarProps } from '../atoms/Avatar'; export type UserProfileProps = { name: string; org?: string; avatar?: Omit<AvatarProps, 'className'>; className?: string; }; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export function UserProfile({ name, org, avatar, className, }: UserProfileProps) { return ( <div className={cn('user-profile', className)}> <Avatar type="initials" shape="square" size="s" status {...avatar} initials={avatar?.initials || name.split(' ').map((w) => w[0]).join('').slice(0, 2).toUpperCase()} /> <div className="user-profile__info" data-sidebar-user-info=""> <span className="user-profile__name">{name}</span> {org && <span className="user-profile__org">{org}</span>} </div> </div> ); }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / UserProfile) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.sidebar--collapsed .user-profile__info— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.sidebar--collapsed .user-profile— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
Tras pegar: Create component → nombre Atom / UserProfile → Publish.