# 玻璃勾选（Checkbox & Radio Group (APG)）

- 类型 / 材质：`choice` / `glass-apple`（玻璃）
- 状态：done ｜ 最后提交：2026-11-01
- 灵感来源：APG Checkbox / Radio Group + iOS 设置勾选
- 依据标准：`/design#interaction` ｜ 组件页：`/showcase/component/choice/glass-apple` ｜ 条目：`/r/choice--glass-apple.json`
- 必缺状态：焦点 / 禁用
- 整页场景：尚未接入

## 规格

- 原生 checkbox / radio + fieldset / legend；单选组同 name，方向键与空格交给浏览器
- input sr-only，focus-visible 环用 peer 画在可视框上，读屏与键盘都不丢
- values 数组同时兼容多选与单选；组级 fieldset[disabled] 与单项禁用互不干扰
- 未选半透明 + 发丝边 + inset；选中 accent 实底 + 反色勾 / 点 + 轻浮起

## 参数（CSS 变量）

`--color-accent` `--color-bg` `--color-text` `--color-text-muted` `--glass-bg` `--glass-border` `--glass-inset` `--shadow-sm` `--text-xs`

## Props 契约

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `kind` | `'checkbox' | 'radio'` | 必填 | — |
| `options` | `ChoiceOption[]` | 必填 | — |
| `values` | `string[]` | 必填 | — |
| `onChange` | `(values: string[]) => void` | 可选 | — |
| `label` | `string` | 可选 | — |
| `disabled` | `boolean` | 可选 | — |
| `className` | `string` | 可选 | — |

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

```
复现「玻璃勾选」— Checkbox & Radio Group (APG)（勾选 / Glass）
灵感来源：APG Checkbox / Radio Group + iOS 设置勾选
规格：
- 原生 checkbox / radio + fieldset / legend；单选组同 name，方向键与空格交给浏览器
- input sr-only，focus-visible 环用 peer 画在可视框上，读屏与键盘都不丢
- values 数组同时兼容多选与单选；组级 fieldset[disabled] 与单项禁用互不干扰
- 未选半透明 + 发丝边 + inset；选中 accent 实底 + 反色勾 / 点 + 轻浮起
参数（CSS 变量）：--color-accent --color-bg --color-text --color-text-muted --glass-bg --glass-border --glass-inset --shadow-sm --text-xs
必缺状态：焦点 / 禁用
整页场景：（尚未接进整页场景）
无障碍：焦点环 ≥2px 等效周长（SC 2.4.13）；指针目标 ≥24×24（SC 2.5.8）；动效带 prefers-reduced-motion 降级
参考：本站实样 /showcase/component/choice/glass-apple · 标准 interaction /design#interaction · 调研笔记 /showcase/component/choice/glass-apple#notes
```

## 调研笔记

import { useId } from 'react';
import { Check } from 'lucide-react';
import { Icon } from '@/components/ui-echo/Icon';

export interface ChoiceOption {
  value: string;
  label: string;
  disabled?: boolean;
}

interface Props {
  kind: 'checkbox' | 'radio';
  options: ChoiceOption[];
  values: string[];
  onChange?: (values: string[]) => void;
  label?: string;
  disabled?: boolean;
  className?: string;
}

/**
 * 玻璃勾选：复选框与单选框共用一套行结构，只有可视框形状不同。
 * 用原生 input 保留键盘与读屏行为；焦点环画在可视框上（input 本身 sr-only）。
 */
export function GlassAppleChoice({
  kind,
  options,
  values,
  onChange,
  label,
  disabled = false,
  className = '',
}: Props) {
  const groupId = useId();

  const toggle = (value: string) => {
    if (kind === 'radio') {
      onChange?.([value]);
      return;
    }
    const next = new Set(values);
    if (next.has(value)) next.delete(value);
    else next.add(value);
    onChange?.([...next]);
  };

  return (
    <fieldset
      disabled={disabled || undefined}
      aria-disabled={disabled || undefined}
      className={`border-0 p-0 m-0 min-w-0 ${disabled ? 'opacity-60' : ''} ${className}`}
    >
      {label && (
        <legend className="text-xs uppercase tracking-[0.18em] text-[var(--color-text-muted)] mb-2.5">
          {label}
        </legend>
      )}
      <div className="space-y-2.5">
        {options.map((option) => {
          const isOn = values.includes(option.value);
          const isDisabled = disabled || option.disabled;
          return (
            <label
              key={option.value}
              className={`flex items-center gap-3 text-sm ${
                isDisabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'
              }`}
            >
              <input
                type={kind}
                name={groupId}
                value={option.value}
                checked={isOn}
                disabled={isDisabled}
                onChange={() => toggle(option.value)}
                className="peer sr-only"
              />
              <span
                aria-hidden="true"
                className={`
                  grid place-items-center shrink-0 size-5
                  ${kind === 'checkbox' ? 'rounded-md' : 'rounded-full'}
                  ${isOn
                    ? 'bg-[var(--color-accent)] border border-[var(--color-accent)] text-[var(--color-bg)] shadow-[var(--shadow-sm)]'
                    : 'bg-[var(--glass-bg)] border border-[var(--glass-border)] shadow-[var(--glass-inset)]'}
                  peer-focus-visible:outline peer-focus-visible:outline-2
                  peer-focus-visible:outline-offset-2 peer-focus-visible:outline-[var(--color-accent)]
                `}
              >
                {isOn && kind === 'checkbox' && (
                  <Icon icon={Check} size={12} strokeWidth={3} />
                )}
                {isOn && kind === 'radio' && (
                  <span className="size-2 rounded-full bg-current" />
                )}
              </span>
              <span>{option.label}</span>
            </label>
          );
        })}
      </div>
    </fieldset>
  );
}
