# 玻璃按钮（Liquid Glass Button）

- 类型 / 材质：`button` / `glass-apple`（玻璃）
- 状态：done ｜ 最后提交：2026-10-31
- 灵感来源：Apple Vision Pro / iOS 26 Liquid Glass
- 依据标准：`/design#material` ｜ 组件页：`/showcase/component/button/glass-apple` ｜ 条目：`/r/button--glass-apple.json`
- 必缺状态：焦点 / 按压 / 禁用
- 整页场景：
  - 两步验证（`/r/scene--auth.json`）

## 规格

- 半透明底 + 1px 细边框 + 背景模糊，做出玻璃厚度
- 双阴影：外阴影抬起，内高光做顶部反光
- sm / md / lg 三档尺寸 + pill 变体 + disabled
- 按压缩放 0.97、悬停放大 1.02（全站统一值）

## 参数（CSS 变量）

`--color-text` `--color-text-muted` `--color-text-subtle` `--dur-fast` `--ease-standard` `--glass-bg` `--glass-border` `--glass-highlight` `--glass-inset` `--glass-recess` `--shadow-md` `--text-base` `--text-xs`

## Props 契约

继承原生属性接口：`ButtonHTMLAttributes<HTMLButtonElement>`（原生属性都接受，自有字段如下）

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `children` | `ReactNode` | 必填 | — |
| `variant` | `Variant` | 可选 | — |
| `size` | `Size` | 可选 | — |
| `loading` | `boolean` | 可选 | 加载中：转圈 + 禁用 + aria-busy（宽度不变，内容变淡） |

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

```
复现「玻璃按钮」— Liquid Glass Button（按钮 / Glass）
灵感来源：Apple Vision Pro / iOS 26 Liquid Glass
规格：
- 半透明底 + 1px 细边框 + 背景模糊，做出玻璃厚度
- 双阴影：外阴影抬起，内高光做顶部反光
- sm / md / lg 三档尺寸 + pill 变体 + disabled
- 按压缩放 0.97、悬停放大 1.02（全站统一值）
参数（CSS 变量）：--color-text --color-text-muted --color-text-subtle --dur-fast --ease-standard --glass-bg --glass-border --glass-highlight --glass-inset --glass-recess --shadow-md --text-base --text-xs
必缺状态：焦点 / 按压 / 禁用
整页场景：两步验证
无障碍：焦点环 ≥2px 等效周长（SC 2.4.13）；指针目标 ≥24×24（SC 2.5.8）；动效带 prefers-reduced-motion 降级
参考：本站实样 /showcase/component/button/glass-apple · 标准 material /design#material · 调研笔记 /showcase/component/button/glass-apple#notes
```

## 调研笔记

import { type ButtonHTMLAttributes, type ReactNode } from 'react';
import { Spinner } from '@/components/ui-echo/Feedback';

type Variant = 'default' | 'pill';
type Size = 'sm' | 'md' | 'lg';

interface GlassAppleButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: ReactNode;
  variant?: Variant;
  size?: Size;
  /** 加载中：转圈 + 禁用 + aria-busy（宽度不变，内容变淡） */
  loading?: boolean;
}

const sizeClasses: Record<Size, string> = {
  sm: 'px-4 py-1.5 text-sm',
  md: 'px-6 py-2.5 text-base',
  lg: 'px-8 py-3.5 text-lg',
};

const variantClasses: Record<Variant, string> = {
  default: 'rounded-2xl',
  pill: 'rounded-full',
};

export function GlassAppleButton({
  children,
  variant = 'default',
  size = 'md',
  loading = false,
  className = '',
  ...rest
}: GlassAppleButtonProps) {
  const spinnerSize = size === 'lg' ? 16 : size === 'md' ? 14 : 12;
  return (
    <button
      {...rest}
      aria-busy={loading || undefined}
      disabled={loading || rest.disabled}
      className={`
        relative
        ${sizeClasses[size]}
        ${variantClasses[variant]}
        font-medium
        text-[var(--color-text)]
        bg-[var(--glass-bg)]
        border border-[var(--glass-border)]
        backdrop-blur-xl
        shadow-[var(--shadow-md),var(--glass-inset)]
        press-feedback press-hover
        transition-[background-color,border-color,box-shadow,transform,opacity] duration-[var(--dur-fast)] ease-[var(--ease-standard)]
        hover:bg-[var(--glass-highlight)]
        hover:border-[rgba(29,31,38,0.14)]
        disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:scale-100
        cursor-pointer
        ${className}
      `}
    >
      {loading && <Spinner size={spinnerSize} className="mr-2 align-[-0.125em]" label="加载中" />}
      <span className={loading ? 'opacity-60' : ''}>{children}</span>
    </button>
  );
}
