pagination · glass-apple · 灵感来源:Carbon Pagination / GOV.UK pagination
多页:首尾固定,中间收成省略号
当前第 4 页
少页:全部铺开,首尾按钮到端禁用
当前第 1 页
Pagination · 玻璃分页 · 分页 / Glass
做一个 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
灵感参考:Carbon Pagination / GOV.UK pagination--color-bg --color-surface --color-text --color-text-muted --color-text-subtle --glass-border --glass-highlight
焦点 / 禁用
/r/pagination--glass-apple.json
免外壳的 markdown 版:/llms/components/pagination--glass-apple.mdnav aria-label="分页";当前页 aria-current="page";每个页码有 aria-label="第 n 页"import { ChevronLeft, ChevronRight } from 'lucide-react';
import { Icon } from '~/components/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>
);
}
src/components/pagination/glass-apple/