Avatar

Last Seen Row

An avatar with a last-seen date: the human caption lives in a <time> element with a machine stamp, and recency differs by mark shape rather than shade of grey.

  • avatar
  • last-seen
  • time
  • list

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/avatar-034?lang=en

Мария Лоза
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "avatar-034" (Last Seen Row) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/avatar-034.json

Registry item: https://vibeui.ru/r/avatar-034.json
Installs to: components/vibeui/avatar-034.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
An avatar with a last-seen date: the human caption lives in a <time> element with a machine stamp, and recency differs by mark shape rather than shade of grey.

A person row with a last-seen date: the caption sits in a <time> element carrying a machine stamp, and recency differs by the shape of the mark. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Avatar034 } from "@/components/vibeui/avatar-034"

<Avatar034
  name="Maria Loza"
  when="yesterday at 18:40"
  dateTime="2026-08-30T18:40"
  freshness="week"
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-avatar-034-* palette — do not swap it for your theme tokens
- the <time> element with dateTime: the human caption must be machine-readable too
- the ready-made caption as a prop instead of recomputing relative time on the client — that is a straight road to a hydration mismatch
- recency by mark shape: three shades of grey are indistinguishable
- the aria-label on the mark — a filled dot is mute on its own
- the row's own light surface: without it the dark text disappears on a dark background

## 6. You may change
- the name and the time caption through when
- the recency step through freshness — today, week or long
- the machine stamp through dateTime
- the fresh and stale colours in the variables

## 7. Rules
- The caption does not refresh itself: "yesterday" stays "yesterday" tomorrow, so update it on the server.
- An exact last-seen time is sensitive: messengers hide it for a reason.
- The dateTime format must be valid HTML, otherwise the <time> element stops meaning anything.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

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

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-avatar-034-*. Серверный: хуков нет — относительное время не пересчитывается на клиенте и не тянет за собой расхождение гидрации. Подпись живёт в <time> с машинным dateTime: человек читает «вчера в 18:40», а разметка несёт точную отметку, которую можно разобрать программно. Давность продублирована формой метки — залитая, полая, пунктирная, — потому что три оттенка серого между собой не различаются, а метка объявлена role="img" с подписью.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Avatar034Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  name?: string
  when?: string
  dateTime?: string
  freshness?: "today" | "week" | "long"
}

// Идея компонента: аватар с датой последнего входа. «Был в сети недавно» —
// текст без опоры: браузер не подскажет точную дату, а пересчитывать её на
// клиенте значит уехать в гидрацию. Поэтому подпись живёт в <time> с машинным
// dateTime: человек читает «вчера в 18:40», а разметка несёт точную отметку.
// Давность продублирована формой метки — залитая, полая, пунктирная — потому
// что три оттенка серого между собой не различаются.
const STYLES = `
:where([data-vibeui-block="avatar-034"]){
--vibeui-avatar-034-size:2.5rem;
--vibeui-avatar-034-bg:oklch(1 0 0);
--vibeui-avatar-034-fg:oklch(0.24 0.014 265);
--vibeui-avatar-034-muted:oklch(0.55 0.014 265);
--vibeui-avatar-034-border:oklch(0.91 0.006 265);
--vibeui-avatar-034-fresh:oklch(0.62 0.15 152);
--vibeui-avatar-034-stale:oklch(0.66 0.02 265);
--vibeui-avatar-034-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: тёмный текст обязан читаться на любом фоне. */
[data-vibeui-block="avatar-034"]{
display:flex;align-items:center;gap:0.75rem;
box-sizing:border-box;width:100%;max-width:19rem;padding:0.5rem 0.75rem;
background:var(--vibeui-avatar-034-bg);
border:1px solid var(--vibeui-avatar-034-border);border-radius:0.75rem;
font-family:var(--vibeui-avatar-034-font);color:var(--vibeui-avatar-034-fg);
}
[data-vibeui-block="avatar-034"] *{box-sizing:border-box}
[data-vibeui-block="avatar-034"] [data-part="face"]{
display:grid;place-items:center;flex:none;
width:var(--vibeui-avatar-034-size);height:var(--vibeui-avatar-034-size);
border-radius:9999px;
background:oklch(0.9 0.06 var(--vibeui-avatar-034-hue,265));
color:oklch(0.36 0.12 var(--vibeui-avatar-034-hue,265));
font-size:calc(var(--vibeui-avatar-034-size) * 0.34);font-weight:700;line-height:1;
}
[data-vibeui-block="avatar-034"] [data-part="text"]{display:flex;flex-direction:column;gap:0.0625rem;min-width:0;flex:1 1 auto}
[data-vibeui-block="avatar-034"] [data-part="name"]{
font-size:0.875rem;font-weight:650;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="avatar-034"] [data-part="seen"]{
display:flex;align-items:center;gap:0.375rem;
font-size:0.75rem;color:var(--vibeui-avatar-034-muted);
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
/* Форма, а не оттенок серого: три градации давности различимы. */
[data-vibeui-block="avatar-034"] [data-part="mark"]{
flex:none;width:0.5rem;height:0.5rem;border-radius:9999px;
background:var(--vibeui-avatar-034-fresh);
}
[data-vibeui-block="avatar-034"][data-freshness="week"] [data-part="mark"]{
background:none;box-shadow:inset 0 0 0 2px var(--vibeui-avatar-034-stale);
}
[data-vibeui-block="avatar-034"][data-freshness="long"] [data-part="mark"]{
background:none;border:1px dashed var(--vibeui-avatar-034-stale);
}
[data-vibeui-block="avatar-034"] [data-part="time"]{color:inherit;text-decoration:none}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="avatar-034"] *{animation:none!important;transition:none!important}}
`

const FRESHNESS_LABEL = {
  today: "заходил сегодня",
  week: "заходил на этой неделе",
  long: "давно не заходил",
} as const

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.charAt(0).toUpperCase())
    .join("")
}

/**
 * Аватар с датой последнего входа: подпись в <time>, давность различается формой метки.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Avatar034({
  name = "Мария Лоза",
  when = "вчера в 18:40",
  dateTime = "2026-08-30T18:40",
  freshness = "week",
  className,
  style,
  ...props
}: Avatar034Props) {
  const palette = {
    "--vibeui-avatar-034-hue": hue(name),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-avatar-034" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="avatar-034"
        data-freshness={freshness}
        className={className}
        style={palette}
      >
        <span data-part="face" aria-hidden="true">
          {initials(name)}
        </span>
        <span data-part="text">
          <span data-part="name">{name}</span>
          <span data-part="seen">
            <span
              data-part="mark"
              role="img"
              aria-label={FRESHNESS_LABEL[freshness]}
            />
            {/* Машинная отметка рядом с человеческой: считать её нечем. */}
            <time data-part="time" dateTime={dateTime}>
              был в сети {when}
            </time>
          </span>
        </span>
      </div>
    </>
  )
}