This repository has no description
1<script module lang="ts">
2 import { tv } from "tailwind-variants";
3
4 export const pagination = tv({
5 base: "flex items-center gap-1"
6 });
7
8 type Item = number | "start-ellipsis" | "end-ellipsis";
9
10 function range(start: number, end: number): number[] {
11 return Array.from({ length: Math.max(end - start + 1, 0) }, (_, i) => start + i);
12 }
13
14 // Windowing per MUI's usePagination: always show the boundary pages
15 // (first/last) plus a sibling window around the current page, collapsing
16 // any gap into a single ellipsis.
17 function getPageItems(current: number, count: number): Item[] {
18 const siblingCount = 1;
19 const boundaryCount = 1;
20
21 const startPages = range(1, Math.min(boundaryCount, count));
22 const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count);
23
24 const siblingsStart = Math.max(
25 Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1),
26 boundaryCount + 2
27 );
28 const siblingsEnd = Math.min(
29 Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2),
30 endPages.length > 0 ? endPages[0] - 2 : count - 1
31 );
32
33 return [
34 ...startPages,
35 ...(siblingsStart > boundaryCount + 2
36 ? (["start-ellipsis"] as const)
37 : boundaryCount + 1 < count - boundaryCount
38 ? [boundaryCount + 1]
39 : []),
40 ...range(siblingsStart, siblingsEnd),
41 ...(siblingsEnd < count - boundaryCount - 1
42 ? (["end-ellipsis"] as const)
43 : count - boundaryCount > boundaryCount
44 ? [count - boundaryCount]
45 : []),
46 ...endPages
47 ];
48 }
49</script>
50
51<script lang="ts">
52 import ChevronLeft from "$icon/chevron-left";
53 import ChevronRight from "$icon/chevron-right";
54 import Button from "./Button.svelte";
55
56 interface Props {
57 page: number;
58 total: number;
59 onchange?: (page: number) => void;
60 class?: string;
61 labels?: boolean;
62 }
63
64 let { page = $bindable(1), total, onchange, class: className, labels }: Props = $props();
65
66 const count = $derived(Math.max(total, 1));
67 const items = $derived(getPageItems(Math.min(Math.max(page, 1), count), count));
68
69 function goTo(target: number) {
70 const next = Math.min(Math.max(target, 1), count);
71 if (next === page) return;
72 page = next;
73 onchange?.(next);
74 }
75</script>
76
77<nav aria-label="Pagination" class={pagination({ class: className })}>
78 <Button
79 variant="ghost"
80 icon={ChevronLeft}
81 disabled={page <= 1}
82 aria-label="Previous page"
83 onclick={() => goTo(page - 1)}
84 >
85 <span class="hidden sm:inline">{labels ? "Previous" : ""}</span>
86 </Button>
87
88 {#each items as item, i (typeof item === "number" ? item : `${item}-${i}`)}
89 {#if typeof item === "number"}
90 <Button
91 variant={item === page ? "default" : "ghost"}
92 aria-current={item === page ? "page" : undefined}
93 aria-label={`Page ${item}`}
94 onclick={() => goTo(item)}
95 >
96 {item}
97 </Button>
98 {:else}
99 <span
100 class="pointer-events-none flex size-8 items-center justify-center text-foreground-subtle select-none"
101 aria-hidden="true"
102 >
103 …
104 </span>
105 {/if}
106 {/each}
107
108 <Button
109 variant="ghost"
110 icon={ChevronRight}
111 iconSide="right"
112 disabled={page >= count}
113 aria-label="Next page"
114 onclick={() => goTo(page + 1)}
115 >
116 <span class="hidden sm:inline">{labels ? "Next" : ""}</span>
117 </Button>
118</nav>