# 玻璃头像（Avatar with fallback）

- 类型 / 材质：`avatar` / `glass-apple`（玻璃）
- 状态：done ｜ 最后提交：2026-11-01
- 灵感来源：iOS 联系人头像 / Linear 成员头像（图 + 首字母回落 + 状态点）
- 依据标准：`/design#interaction` ｜ 组件页：`/showcase/component/avatar/glass-apple` ｜ 条目：`/r/avatar--glass-apple.json`
- 必缺状态：（无必缺状态）
- 整页场景：
  - 发布记录（`/r/scene--record.json`）

## 规格

- 无图或 onError 回落首字母：中文取前两字，西文取首字母缩写，不重试加载
- size 三档 sm 24 / md 32 / lg 44，字号随档位变化
- status 画 2px 圆点，另有 sr-only 文案（在线 / 忙碌 / 离线）
- 头像本身不可聚焦；可点入口由消费方包链接并把焦点环加在外框

## 参数（CSS 变量）

`--color-success-fg` `--color-surface` `--color-text-muted` `--color-text-subtle` `--color-warning-fg` `--glass-bg` `--glass-border` `--text-2xs` `--text-xs`

## Props 契约

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `name` | `string` | 必填 | — |
| `src` | `string` | 可选 | — |
| `size` | `'sm' | 'md' | 'lg'` | 可选 | — |
| `status` | `'online' | 'busy' | 'offline'` | 可选 | — |
| `className` | `string` | 可选 | — |

## 提示词（可直接粘给智能体）

```
复现「玻璃头像」— Avatar with fallback（头像 / Glass）
灵感来源：iOS 联系人头像 / Linear 成员头像（图 + 首字母回落 + 状态点）
规格：
- 无图或 onError 回落首字母：中文取前两字，西文取首字母缩写，不重试加载
- size 三档 sm 24 / md 32 / lg 44，字号随档位变化
- status 画 2px 圆点，另有 sr-only 文案（在线 / 忙碌 / 离线）
- 头像本身不可聚焦；可点入口由消费方包链接并把焦点环加在外框
参数（CSS 变量）：--color-success-fg --color-surface --color-text-muted --color-text-subtle --color-warning-fg --glass-bg --glass-border --text-2xs --text-xs
必缺状态：（无必缺状态）
整页场景：发布记录
无障碍：焦点环 ≥2px 等效周长（SC 2.4.13）；指针目标 ≥24×24（SC 2.5.8）；动效带 prefers-reduced-motion 降级
参考：本站实样 /showcase/component/avatar/glass-apple · 标准 interaction /design#interaction · 调研笔记 /showcase/component/avatar/glass-apple#notes
```

## 调研笔记

import { useState } from 'react';

interface Props {
  name: string;
  src?: string;
  size?: 'sm' | 'md' | 'lg';
  status?: 'online' | 'busy' | 'offline';
  className?: string;
}

const SIZE = {
  sm: 'size-6 text-[length:var(--text-2xs)]',
  md: 'size-8 text-[length:var(--text-xs)]',
  lg: 'size-11 text-sm',
};

const STATUS = {
  online: 'bg-[var(--color-success-fg)]',
  busy: 'bg-[var(--color-warning-fg)]',
  offline: 'bg-[var(--color-text-subtle)]',
};

const STATUS_LABEL = {
  online: '在线',
  busy: '忙碌',
  offline: '离线',
};

/** 中文在小尺寸只留姓，lg 才放两个字；西文取首字母缩写 */
function initials(name: string, size: 'sm' | 'md' | 'lg') {
  const parts = name.trim().split(/\s+/);
  if (parts.length > 1) return (parts[0][0] + parts[1][0]).toUpperCase();
  if (/[\u4e00-\u9fa5]/.test(name)) return size === 'lg' ? name.slice(0, 2) : name.slice(0, 1);
  return name.slice(0, 2).toUpperCase();
}

export function GlassAppleAvatar({ name, src, size = 'md', status, className = '' }: Props) {
  const [broken, setBroken] = useState(false);
  const showImage = src && !broken;

  return (
    <span className={`relative inline-flex shrink-0 ${className}`}>
      <span
        className={`
          ${SIZE[size]} rounded-full overflow-hidden relative
          inline-flex items-center justify-center
          bg-[var(--glass-bg)] border border-[var(--glass-border)] backdrop-blur-xl
          text-[var(--color-text-muted)] font-medium
        `}
      >
        <span aria-hidden="true" className="absolute inset-0 grid place-items-center">
          {initials(name, size)}
        </span>
        {showImage && (
          <img
            src={src}
            alt=""
            className="absolute inset-0 size-full object-cover"
            onError={() => setBroken(true)}
          />
        )}
        <span className="sr-only">{name}</span>
      </span>

      {status && (
        <>
          <span
            className={`
              absolute -bottom-0 -right-0 size-2 rounded-full
              ring-2 ring-[var(--color-surface)]
              ${STATUS[status]}
            `}
            aria-hidden="true"
          />
          <span className="sr-only">{STATUS_LABEL[status]}</span>
        </>
      )}
    </span>
  );
}
