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 disabled?: boolean;
63 }
64
65 let {
66 page = $bindable(1),
67 total,
68 onchange,
69 class: className,
70 labels,
71 disabled = false
72 }: Props = $props();
73
74 const count = $derived(Math.max(total, 1));
75 const items = $derived(getPageItems(Math.min(Math.max(page, 1), count), count));
76
77 function goTo(target: number) {
78 if (disabled) return;
79 const next = Math.min(Math.max(target, 1), count);
80 if (next === page) return;
81 page = next;
82 onchange?.(next);
83 }
84</script>
85
86<nav
87 aria-label="Pagination"
88 aria-busy={disabled || undefined}
89 class={pagination({ class: className })}
90>
91 <Button
92 variant="ghost"
93 icon={ChevronLeft}
94 disabled={disabled || page <= 1}
95 aria-label="Previous page"
96 onclick={() => goTo(page - 1)}
97 >
98 <span class="hidden sm:inline">{labels ? "Previous" : ""}</span>
99 </Button>
100
101 {#each items as item, i (typeof item === "number" ? item : `${item}-${i}`)}
102 {#if typeof item === "number"}
103 <Button
104 variant={item === page ? "default" : "ghost"}
105 {disabled}
106 aria-current={item === page ? "page" : undefined}
107 aria-label={`Page ${item}`}
108 onclick={() => goTo(item)}
109 >
110 {item}
111 </Button>
112 {:else}
113 <span
114 class="pointer-events-none flex size-8 items-center justify-center text-foreground-subtle select-none"
115 aria-hidden="true"
116 >
117 …
118 </span>
119 {/if}
120 {/each}
121
122 <Button
123 variant="ghost"
124 icon={ChevronRight}
125 iconSide="right"
126 disabled={disabled || page >= count}
127 aria-label="Next page"
128 onclick={() => goTo(page + 1)}
129 >
130 <span class="hidden sm:inline">{labels ? "Next" : ""}</span>
131 </Button>
132</nav>