pagination · neumorphic · 灵感来源:Neumorphism(同色页码,悬停压成凹槽)
多页:首尾固定,中间收成省略号
当前第 4 页
少页:全部铺开,首尾按钮到端禁用
当前第 1 页
Pagination · 拟态分页 · 分页 / Neumorphic
做一个 Pagination(拟态分页):
- 同一份 props(page / pageCount / onChange)
- 无边框,悬停压一层内双阴影;当前页仍是墨色实心,不再压槽
- 页码窗口与禁用规则同玻璃分页
用这些 CSS 变量:--color-bg --color-text --color-text-muted --color-text-subtle --neuro-bg --neuro-dark --neuro-light
灵感参考:Neumorphism(同色页码,悬停压成凹槽)--color-bg --color-text --color-text-muted --color-text-subtle --neuro-bg --neuro-dark --neuro-light
焦点 / 禁用
(尚未接进整页场景)
/r/pagination--neumorphic.json
免外壳的 markdown 版:/llms/components/pagination--neumorphic.mdaria-current、首尾禁用规则同玻璃分页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 NeumorphicPagination({ 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-transparent bg-[var(--neuro-bg)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:shadow-[inset_1px_1px_2px_var(--neuro-dark),inset_-1px_-1px_2px_var(--neuro-light)]`}
>
<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-transparent bg-[var(--neuro-bg)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:shadow-[inset_1px_1px_2px_var(--neuro-dark),inset_-1px_-1px_2px_var(--neuro-light)]'}
`}
>
{item}
</button>
</li>
),
)}
<li>
<button
type="button"
onClick={() => go(page + 1)}
disabled={page >= pageCount}
aria-label="下一页"
className={`${base} border-transparent bg-[var(--neuro-bg)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] hover:shadow-[inset_1px_1px_2px_var(--neuro-dark),inset_-1px_-1px_2px_var(--neuro-light)]`}
>
<Icon icon={ChevronRight} size={13} />
</button>
</li>
</ul>
</nav>
);
}
src/components/pagination/neumorphic/