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
4.1 kB 135 lines
1<script module lang="ts"> 2 import { tv, type VariantProps } from "tailwind-variants"; 3 4 export const pagination = tv({ 5 base: "flex items-center gap-1" 6 }); 7 8 export const paginationNav = tv({ 9 base: "flex size-8 items-center justify-center rounded text-foreground-muted transition-colors duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] hover:bg-background-subtle focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus disabled:pointer-events-none disabled:opacity-50" 10 }); 11 12 export const paginationItem = tv({ 13 base: "flex size-8 items-center justify-center rounded border text-sm transition-colors duration-150 ease-[cubic-bezier(0.4,0,0.2,1)] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-border-focus", 14 variants: { 15 active: { 16 true: "border-border-strong bg-background-inset font-medium text-foreground-default", 17 false: "border-border-default text-foreground-muted hover:bg-background-subtle" 18 } 19 }, 20 defaultVariants: { 21 active: false 22 } 23 }); 24 25 export type PaginationItemVariants = VariantProps<typeof paginationItem>; 26 27 type Item = number | "start-ellipsis" | "end-ellipsis"; 28 29 function range(start: number, end: number): number[] { 30 return Array.from({ length: Math.max(end - start + 1, 0) }, (_, i) => start + i); 31 } 32 33 // Windowing per MUI's usePagination: always show the boundary pages 34 // (first/last) plus a sibling window around the current page, collapsing 35 // any gap into a single ellipsis. 36 function getPageItems(current: number, count: number): Item[] { 37 const siblingCount = 1; 38 const boundaryCount = 1; 39 40 const startPages = range(1, Math.min(boundaryCount, count)); 41 const endPages = range(Math.max(count - boundaryCount + 1, boundaryCount + 1), count); 42 43 const siblingsStart = Math.max( 44 Math.min(current - siblingCount, count - boundaryCount - siblingCount * 2 - 1), 45 boundaryCount + 2 46 ); 47 const siblingsEnd = Math.min( 48 Math.max(current + siblingCount, boundaryCount + siblingCount * 2 + 2), 49 endPages.length > 0 ? endPages[0] - 2 : count - 1 50 ); 51 52 return [ 53 ...startPages, 54 ...(siblingsStart > boundaryCount + 2 55 ? (["start-ellipsis"] as const) 56 : boundaryCount + 1 < count - boundaryCount 57 ? [boundaryCount + 1] 58 : []), 59 ...range(siblingsStart, siblingsEnd), 60 ...(siblingsEnd < count - boundaryCount - 1 61 ? (["end-ellipsis"] as const) 62 : count - boundaryCount > boundaryCount 63 ? [count - boundaryCount] 64 : []), 65 ...endPages 66 ]; 67 } 68</script> 69 70<script lang="ts"> 71 import ChevronLeft from "$icon/chevron-left"; 72 import ChevronRight from "$icon/chevron-right"; 73 74 interface Props { 75 page: number; 76 total: number; 77 onchange?: (page: number) => void; 78 class?: string; 79 } 80 81 let { page = $bindable(1), total, onchange, class: className }: Props = $props(); 82 83 const count = $derived(Math.max(total, 1)); 84 const items = $derived(getPageItems(Math.min(Math.max(page, 1), count), count)); 85 86 function goTo(target: number) { 87 const next = Math.min(Math.max(target, 1), count); 88 if (next === page) return; 89 page = next; 90 onchange?.(next); 91 } 92</script> 93 94<nav aria-label="Pagination" class={pagination({ class: className })}> 95 <button 96 type="button" 97 class={paginationNav()} 98 disabled={page <= 1} 99 aria-label="Previous page" 100 onclick={() => goTo(page - 1)} 101 > 102 <ChevronLeft class="size-4" aria-hidden="true" /> 103 </button> 104 105 {#each items as item, i (typeof item === "number" ? item : `${item}-${i}`)} 106 {#if typeof item === "number"} 107 <button 108 type="button" 109 class={paginationItem({ active: item === page })} 110 aria-current={item === page ? "page" : undefined} 111 aria-label={`Page ${item}`} 112 onclick={() => goTo(item)} 113 > 114 {item} 115 </button> 116 {:else} 117 <span 118 class="flex size-8 items-center justify-center text-foreground-subtle" 119 aria-hidden="true" 120 > 121 &hellip; 122 </span> 123 {/if} 124 {/each} 125 126 <button 127 type="button" 128 class={paginationNav()} 129 disabled={page >= count} 130 aria-label="Next page" 131 onclick={() => goTo(page + 1)} 132 > 133 <ChevronRight class="size-4" aria-hidden="true" /> 134 </button> 135</nav>