First Release v1.0.0
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
---
|
||||
/**
|
||||
* Pagination Component
|
||||
* Page navigation with prev/next and numbered pages.
|
||||
*/
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
import { cn } from '@/lib/cn';
|
||||
import { paginationItemVariants } from './pagination.variants';
|
||||
import Icon from '../../primitives/Icon/Icon.astro';
|
||||
|
||||
interface Props extends HTMLAttributes<'nav'> {
|
||||
/** Current active page (1-indexed) */
|
||||
currentPage: number;
|
||||
/** Total number of pages */
|
||||
totalPages: number;
|
||||
/** Base URL for page links (page number appended) */
|
||||
baseUrl?: string;
|
||||
/** Maximum number of visible page buttons */
|
||||
maxVisible?: number;
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
const {
|
||||
currentPage,
|
||||
totalPages,
|
||||
baseUrl = '?page=',
|
||||
maxVisible = 5,
|
||||
size = 'md',
|
||||
class: className,
|
||||
...attrs
|
||||
} = Astro.props;
|
||||
|
||||
/**
|
||||
* Build the array of page numbers and ellipsis markers to render.
|
||||
*
|
||||
* @param current - The 1-indexed active page.
|
||||
* @param total - Total number of pages.
|
||||
* @param max - Size of the central sliding window. When `total <= max`,
|
||||
* every page is returned directly. Otherwise a window of
|
||||
* `max` pages is centered around `current`, and first/last
|
||||
* pages plus `'...'` ellipsis markers are added outside the
|
||||
* window as needed — so the returned array can contain more
|
||||
* than `max` entries.
|
||||
* @returns An array of page numbers and `'...'` separators.
|
||||
*/
|
||||
function getPageRange(current: number, total: number, max: number): (number | '...')[] {
|
||||
if (total <= max) {
|
||||
return Array.from({ length: total }, (_, i) => i + 1);
|
||||
}
|
||||
|
||||
const pages: (number | '...')[] = [];
|
||||
const half = Math.floor(max / 2);
|
||||
let start = Math.max(1, current - half);
|
||||
let end = Math.min(total, start + max - 1);
|
||||
|
||||
if (end - start < max - 1) {
|
||||
start = Math.max(1, end - max + 1);
|
||||
}
|
||||
|
||||
if (start > 1) {
|
||||
pages.push(1);
|
||||
if (start > 2) pages.push('...');
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
if (end < total) {
|
||||
if (end < total - 1) pages.push('...');
|
||||
pages.push(total);
|
||||
}
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
const pages = getPageRange(currentPage, totalPages, maxVisible);
|
||||
const prevUrl = currentPage > 1 ? `${baseUrl}${currentPage - 1}` : undefined;
|
||||
const nextUrl = currentPage < totalPages ? `${baseUrl}${currentPage + 1}` : undefined;
|
||||
---
|
||||
|
||||
<nav class={cn('flex items-center gap-1', className)} aria-label="Pagination" {...attrs}>
|
||||
{/* Previous */}
|
||||
{prevUrl ? (
|
||||
<a
|
||||
href={prevUrl}
|
||||
class={cn(paginationItemVariants({ variant: 'default', size }))}
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<Icon name="chevron-left" size="sm" />
|
||||
</a>
|
||||
) : (
|
||||
<span class={cn(paginationItemVariants({ variant: 'disabled', size }))} aria-disabled="true">
|
||||
<Icon name="chevron-left" size="sm" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Page Numbers */}
|
||||
{pages.map((page) =>
|
||||
page === '...' ? (
|
||||
<span class={cn(paginationItemVariants({ variant: 'default', size }), 'cursor-default hover:bg-transparent')} role="separator" aria-label="More pages">
|
||||
<span aria-hidden="true">...</span>
|
||||
</span>
|
||||
) : page === currentPage ? (
|
||||
<span
|
||||
class={cn(paginationItemVariants({ variant: 'active', size }))}
|
||||
aria-current="page"
|
||||
>
|
||||
{page}
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
href={`${baseUrl}${page}`}
|
||||
class={cn(paginationItemVariants({ variant: 'default', size }))}
|
||||
>
|
||||
{page}
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* Next */}
|
||||
{nextUrl ? (
|
||||
<a
|
||||
href={nextUrl}
|
||||
class={cn(paginationItemVariants({ variant: 'default', size }))}
|
||||
aria-label="Next page"
|
||||
>
|
||||
<Icon name="chevron-right" size="sm" />
|
||||
</a>
|
||||
) : (
|
||||
<span class={cn(paginationItemVariants({ variant: 'disabled', size }))} aria-disabled="true">
|
||||
<Icon name="chevron-right" size="sm" />
|
||||
</span>
|
||||
)}
|
||||
</nav>
|
||||
Reference in New Issue
Block a user