This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / components / ui / Pagination.svelte
3.4 kB 132 lines
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 &hellip; 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>