Avatar

Identity Row

Строка личности: аватар, имя, роль и статус словом. Текст режется многоточием, чтобы строки списка были одной высоты.

  • avatar
  • identity
  • list
  • status

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/avatar-007

Мария ГуроваПродуктовый дизайнерКоманда «Каталог» · Москвав сети
Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «avatar-007» (Identity Row) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/avatar-007.json

Registry item: https://vibeui.ru/r/avatar-007.json
Ставится в: components/vibeui/avatar-007.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Строка личности: аватар, имя, роль и статус словом. Текст режется многоточием, чтобы строки списка были одной высоты.

Строка человека для списков: аватар с инициалами, имя-ссылка на всю площадь карточки, роль, служебная строка и статус словом. Ноль зависимостей, один файл, клиентского JS нет.

## 3. Как использовать
import { Avatar007 } from "@/components/vibeui/avatar-007"

<Avatar007
  name="Мария Гурова"
  role="Продуктовый дизайнер"
  status="online"
  href="/people/maria"
/>

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-avatar-007-* и не подменять её токенами вашей темы
- статус словом рядом с точкой: по одному цвету состояние не прочитать
- обрезку многоточием вместо переноса — строки списка обязаны быть одной высоты
- растянутую ссылку с обводкой на всей карточке, а не только на имени
- порядок полей по убыванию важности: имя, роль, служебная строка
- min-width: 0 на текстовой колонке — без него многоточие не сработает

## 6. Можно менять
- name, role, meta и href
- status — online, away или offline
- фотографию через src
- ширину карточки через max-width

## 7. Правила
- Карточка рассчитана на список: в ряд из трёх колонок ставьте её без максимальной ширины.
- Статус — снимок на момент отрисовки: живое обновление делает вызывающий код.
- Если ссылка не нужна, передайте пустой href — карточка останется просто строкой.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/avatar-007.json
https://vibeui.ru/r/avatar-007.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-avatar-007-*. Серверный: хуков нет. Ссылка растянута на всю карточку псевдоэлементом ::after, а видимая обводка фокуса ставится на корень через :has(a:focus-visible) — иначе подсвечивалось бы только имя. Три текстовых поля режутся многоточием. Статус подписан словом, цвет только дублирует подпись.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Avatar007Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  name?: string
  role?: string
  meta?: string
  src?: string
  href?: string
  status?: "online" | "away" | "offline"
}

// Идея компонента: строка личности — аватар и три поля текста. Имя, роль и
// служебная строка стоят в порядке убывания важности и режутся многоточием, а
// не переносятся: в списке из тридцати человек строки обязаны быть одной
// высоты. Статус подписан словом, а не только точкой — цвет не для всех.
const STYLES = `
:where([data-vibeui-block="avatar-007"]){
--vibeui-avatar-007-size:2.75rem;
--vibeui-avatar-007-bg:oklch(1 0 0);
--vibeui-avatar-007-fg:oklch(0.22 0.014 265);
--vibeui-avatar-007-muted:oklch(0.52 0.014 265);
--vibeui-avatar-007-border:oklch(0.9 0.006 265);
--vibeui-avatar-007-hue:250;
--vibeui-avatar-007-shape:oklch(0.92 0.05 var(--vibeui-avatar-007-hue));
--vibeui-avatar-007-initials:oklch(0.38 0.09 var(--vibeui-avatar-007-hue));
--vibeui-avatar-007-status:oklch(0.62 0.17 152);
--vibeui-avatar-007-accent:oklch(0.55 0.17 265);
--vibeui-avatar-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="avatar-007"]{
position:relative;display:flex;align-items:center;gap:0.75rem;
box-sizing:border-box;width:100%;max-width:22rem;padding:0.75rem;
background:var(--vibeui-avatar-007-bg);
border:1px solid var(--vibeui-avatar-007-border);border-radius:0.75rem;
color:var(--vibeui-avatar-007-fg);font-family:var(--vibeui-avatar-007-font);
}
[data-vibeui-block="avatar-007"][data-status="away"]{--vibeui-avatar-007-status:oklch(0.75 0.16 75)}
[data-vibeui-block="avatar-007"][data-status="offline"]{--vibeui-avatar-007-status:oklch(0.72 0.012 265)}
[data-vibeui-block="avatar-007"] [data-part="shape"]{
position:relative;display:flex;align-items:center;justify-content:center;flex:none;
width:var(--vibeui-avatar-007-size);height:var(--vibeui-avatar-007-size);
overflow:hidden;border-radius:9999px;
background:var(--vibeui-avatar-007-shape);color:var(--vibeui-avatar-007-initials);
font-size:calc(var(--vibeui-avatar-007-size) * 0.34);font-weight:650;line-height:1;user-select:none;
}
[data-vibeui-block="avatar-007"] img{width:100%;height:100%;object-fit:cover;display:block}
[data-vibeui-block="avatar-007"] [data-part="text"]{display:flex;flex-direction:column;gap:0.125rem;min-width:0}
/* Обрезка вместо переноса: строки списка обязаны быть одной высоты. */
[data-vibeui-block="avatar-007"] [data-part="name"],
[data-vibeui-block="avatar-007"] [data-part="role"],
[data-vibeui-block="avatar-007"] [data-part="meta"]{
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="avatar-007"] [data-part="name"]{font-size:0.9375rem;font-weight:600;line-height:1.25}
[data-vibeui-block="avatar-007"] [data-part="role"]{font-size:0.8125rem;line-height:1.3;color:var(--vibeui-avatar-007-muted)}
[data-vibeui-block="avatar-007"] [data-part="meta"]{font-size:0.75rem;line-height:1.3;color:var(--vibeui-avatar-007-muted)}
[data-vibeui-block="avatar-007"] a{color:inherit;text-decoration:none}
/* Ссылка растянута на всю карточку: попадать надо в строку, а не в имя. */
[data-vibeui-block="avatar-007"] a::after{content:"";position:absolute;inset:0;border-radius:inherit}
[data-vibeui-block="avatar-007"] a:focus-visible{outline:none}
[data-vibeui-block="avatar-007"]:has(a:focus-visible){outline:2px solid var(--vibeui-avatar-007-accent);outline-offset:2px}
[data-vibeui-block="avatar-007"] [data-part="state"]{
display:inline-flex;align-items:center;gap:0.3125rem;margin-left:auto;flex:none;
font-size:0.75rem;color:var(--vibeui-avatar-007-muted);
}
[data-vibeui-block="avatar-007"] [data-part="dot"]{
width:0.4375rem;height:0.4375rem;border-radius:9999px;background:var(--vibeui-avatar-007-status);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="avatar-007"] *{animation:none!important;transition:none!important}}
`

const STATUS_TEXT = {
  online: "в сети",
  away: "отошёл",
  offline: "не в сети",
}

// Оттенок из имени: FNV-1a, разложенный по двенадцати ступеням круга.
// Сумма кодов символов не годится — кириллические имена ложатся в один
// розовый сектор; ступени в 30° дают заведомо различимые цвета.
function hue(name: string) {
  let hash = 2166136261

  for (const symbol of name) {
    hash ^= symbol.codePointAt(0)!
    hash = Math.imul(hash, 16777619)
  }

  return ((hash >>> 0) % 12) * 30
}

function initials(name: string) {
  return name
    .split(" ")
    .slice(0, 2)
    .map((part) => part[0]?.toUpperCase() ?? "")
    .join("")
}

/**
 * Строка личности: аватар, имя, роль и подписанный словом статус.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Avatar007({
  name = "Мария Гурова",
  role = "Продуктовый дизайнер",
  meta = "Команда «Каталог» · Москва",
  src = "",
  href = "#",
  status = "online",
  className,
  style,
  ...props
}: Avatar007Props) {
  const palette = {
    "--vibeui-avatar-007-hue": hue(name),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-avatar-007" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="avatar-007"
        data-status={status}
        className={className}
        style={palette}
      >
        <span data-part="shape" aria-hidden="true">
          {src ? (
            <img src={src} alt="" loading="lazy" decoding="async" />
          ) : (
            initials(name)
          )}
        </span>
        <span data-part="text">
          <span data-part="name">
            {href ? <a href={href}>{name}</a> : name}
          </span>
          {role ? <span data-part="role">{role}</span> : null}
          {meta ? <span data-part="meta">{meta}</span> : null}
        </span>
        <span data-part="state">
          <span data-part="dot" aria-hidden="true" />
          {STATUS_TEXT[status]}
        </span>
      </div>
    </>
  )
}