<script>
import { formatCount } from '$lib/bib-utils.js'
// `href` turns each page into a real link: a pager whose page lives in the
// URL (the works list) can then be opened in a new tab, shared, and stepped
// through with the browser's back button. A pager over data the page already
// holds (references, citations, relations) passes none and stays buttons.
let { page = 1, totalPages = 1, onpage, href = null, locale = 'en-US' } = $props()
function go(n) {
if (n >= 1 && n <= totalPages) {
const y = window.scrollY
onpage?.(n)
requestAnimationFrame(() => requestAnimationFrame(() => window.scrollTo({ top: y, behavior: 'instant' })))
}
}
// Follow the link ourselves: the page is swapped in place, and whoever owns
// the pager writes the URL. A modified click (new tab, new window) is left
// to the browser.
function nav(e, n) {
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button !== 0) return
e.preventDefault()
go(n)
}
function noFocusScroll(e) {
e.preventDefault()
}
const CELL = 'inline-flex items-center justify-center h-9 min-w-10 px-2 text-xs transition-colors'
const HOVER = 'hover:bg-accent hover:text-accent-foreground'
// The first page, the last page, and the current one with two either side:
// both ends of the list stay one click away wherever the reader is, and the
// run around the current page is wide enough to step through. A gap of
// exactly one page shows that page rather than an ellipsis standing in for it.
let pages = $derived.by(() => {
const wanted = new Set()
for (const n of [1, totalPages, page - 2, page - 1, page, page + 1, page + 2]) {
if (n >= 1 && n <= totalPages) wanted.add(n)
}
const out = []
for (const n of [...wanted].sort((a, b) => a - b)) {
const prev = out[out.length - 1]
if (typeof prev === 'number') {
if (n === prev + 2) out.push(prev + 1)
else if (n > prev + 2) out.push('…')
}
out.push(n)
}
return out
})
</script>
{#snippet prevIcon()}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
{/snippet}
{#snippet nextIcon()}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
{/snippet}
<nav aria-label="Pagination" class="mt-4 overflow-x-auto">
<!-- `w-max` + `mx-auto` centres the row while it fits and lets it scroll from
the left once it does not; `justify-center` would clip its first pages. -->
<div class="w-max mx-auto flex items-center border border-input rounded-lg bg-background overflow-hidden divide-x divide-input shadow-sm">
{#if href && page > 1}
<a
href={href(page - 1)}
onmousedown={noFocusScroll}
onclick={e => nav(e, page - 1)}
aria-label="Previous page"
class="{CELL} {HOVER}"
>{@render prevIcon()}</a>
{:else}
<button
onmousedown={noFocusScroll}
onclick={() => go(page - 1)}
disabled={page === 1}
aria-label="Previous page"
class="{CELL} {HOVER} disabled:pointer-events-none disabled:opacity-40"
>{@render prevIcon()}</button>
{/if}
{#each pages as p}
{#if p === '…'}
<span class="{CELL} text-muted-foreground select-none">…</span>
{:else if href}
<a
href={href(p)}
onmousedown={noFocusScroll}
onclick={e => nav(e, p)}
aria-current={p === page ? 'page' : undefined}
class="{CELL} tabular-nums {p === page ? 'bg-muted font-medium' : HOVER}"
>{formatCount(p, locale)}</a>
{:else}
<button
onmousedown={noFocusScroll}
onclick={() => go(p)}
aria-current={p === page ? 'page' : undefined}
class="{CELL} tabular-nums {p === page ? 'bg-muted font-medium' : HOVER}"
>{formatCount(p, locale)}</button>
{/if}
{/each}
{#if href && page < totalPages}
<a
href={href(page + 1)}
onmousedown={noFocusScroll}
onclick={e => nav(e, page + 1)}
aria-label="Next page"
class="{CELL} {HOVER}"
>{@render nextIcon()}</a>
{:else}
<button
onmousedown={noFocusScroll}
onclick={() => go(page + 1)}
disabled={page === totalPages}
aria-label="Next page"
class="{CELL} {HOVER} disabled:pointer-events-none disabled:opacity-40"
>{@render nextIcon()}</button>
{/if}
</div>
</nav>