# 玻璃分页（Pagination）

- 类型 / 材质：`pagination` / `glass-apple`（玻璃）
- 状态：done ｜ 最后提交：2026-11-01
- 灵感来源：Carbon Pagination / GOV.UK pagination
- 依据标准：`/design#interaction` ｜ 组件页：`/showcase/component/pagination/glass-apple` ｜ 条目：`/r/pagination--glass-apple.json`
- 必缺状态：焦点 / 禁用
- 整页场景：
  - 组件目录（`/r/scene--list.json`）

## 规格

- nav aria-label + ul/li；当前页 aria-current=page；每个页码 aria-label=第 n 页
- 页码窗口：≤7 页全显示，更多时固定首尾、当前左右各 1，中间省略号不进 Tab 序列
- 首/末页时上/下一页禁用而不隐藏，按钮位置稳定
- 不做每页条数选择器——那是数据表的职责

## 参数（CSS 变量）

`--color-bg` `--color-surface` `--color-text` `--color-text-muted` `--color-text-subtle` `--glass-border` `--glass-highlight`

## Props 契约

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `page` | `number` | 必填 | 从 1 开始 |
| `pageCount` | `number` | 必填 | — |
| `onChange` | `(page: number) => void` | 必填 | — |
| `className` | `string` | 可选 | — |

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

```
复现「玻璃分页」— Pagination（分页 / Glass）
灵感来源：Carbon Pagination / GOV.UK pagination
规格：
- nav aria-label + ul/li；当前页 aria-current=page；每个页码 aria-label=第 n 页
- 页码窗口：≤7 页全显示，更多时固定首尾、当前左右各 1，中间省略号不进 Tab 序列
- 首/末页时上/下一页禁用而不隐藏，按钮位置稳定
- 不做每页条数选择器——那是数据表的职责
参数（CSS 变量）：--color-bg --color-surface --color-text --color-text-muted --color-text-subtle --glass-border --glass-highlight
必缺状态：焦点 / 禁用
整页场景：组件目录
无障碍：焦点环 ≥2px 等效周长（SC 2.4.13）；指针目标 ≥24×24（SC 2.5.8）；动效带 prefers-reduced-motion 降级
参考：本站实样 /showcase/component/pagination/glass-apple · 标准 interaction /design#interaction · 调研笔记 /showcase/component/pagination/glass-apple#notes
```

## 调研笔记

import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Icon } from '@/components/ui-echo/Icon';

interface Props {
  /** 从 1 开始 */
  page: number;
  pageCount: number;
  onChange: (page: number) => void;
  className?: string;
}

/** 页码窗口：总页数 ≤7 全显示，否则首尾 + 当前左右各 1，中间用省略号 */
function pageItems(page: number, pageCount: number): (number | 'gap')[] {
  if (pageCount <= 7) return Array.from({ length: pageCount }, (_, i) => i + 1);
  const items: (number | 'gap')[] = [1];
  const start = Math.max(2, page - 1);
  const end = Math.min(pageCount - 1, page + 1);
  if (start > 2) items.push('gap');
  for (let p = start; p <= end; p += 1) items.push(p);
  if (end < pageCount - 1) items.push('gap');
  items.push(pageCount);
  return items;
}

export function GlassApplePagination({ page, pageCount, onChange, className = '' }: Props) {
  const go = (next: number) => {
    const target = Math.min(Math.max(1, next), pageCount);
    if (target !== page) onChange(target);
  };

  const base = `
    press-feedback grid place-items-center min-w-8 h-8 px-2 rounded-lg text-xs tabular-nums
    border transition-colors cursor-pointer
    disabled:opacity-40 disabled:cursor-not-allowed
  `;

  return (
    <nav aria-label="分页" className={className}>
      <ul className="flex items-center gap-1">
        <li>
          <button
            type="button"
            onClick={() => go(page - 1)}
            disabled={page <= 1}
            aria-label="上一页"
            className={`${base} border-[var(--glass-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:bg-[var(--glass-highlight)]`}
          >
            <Icon icon={ChevronLeft} size={13} />
          </button>
        </li>

        {pageItems(page, pageCount).map((item, i) =>
          item === 'gap' ? (
            <li key={`gap-${i}`} aria-hidden="true" className="px-1 text-xs text-[var(--color-text-subtle)]">
              …
            </li>
          ) : (
            <li key={item}>
              <button
                type="button"
                onClick={() => go(item)}
                aria-current={item === page ? 'page' : undefined}
                aria-label={`第 ${item} 页`}
                className={`
                  ${base}
                  ${item === page
                    ? 'border-transparent bg-[var(--color-text)] text-[var(--color-bg)]'
                    : 'border-[var(--glass-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:bg-[var(--glass-highlight)]'}
                `}
              >
                {item}
              </button>
            </li>
          ),
        )}

        <li>
          <button
            type="button"
            onClick={() => go(page + 1)}
            disabled={page >= pageCount}
            aria-label="下一页"
            className={`${base} border-[var(--glass-border)] bg-[var(--color-surface)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:bg-[var(--glass-highlight)]`}
          >
            <Icon icon={ChevronRight} size={13} />
          </button>
        </li>
      </ul>
    </nav>
  );
}
