Files
ss-tools/frontend/src/lib/ui/Pagination.svelte

120 lines
3.8 KiB
Svelte

<!-- #region UI.Pagination [C:2] [TYPE Component] [SEMANTICS pagination, paging, navigation, ui, atom] -->
<!-- @ingroup UI -->
<!-- @BRIEF Reusable pagination controls with prev/next, page numbers, and "Showing X-Y of Z" summary. -->
<!-- @LAYER UI -->
<!-- @UX_STATE First -> Prev disabled, page 1 highlighted. -->
<!-- @UX_STATE Middle -> Prev/Next enabled, current page highlighted. -->
<!-- @UX_STATE Last -> Next disabled, last page highlighted. -->
<!-- @UX_STATE Single -> Only one page, both buttons disabled, no page numbers shown. -->
<script lang="ts">
import { t } from '$lib/i18n/index.svelte.js';
import { Button } from "$lib/ui";
let {
page = $bindable(0),
pageSize = 20,
total = 0,
showingText = '',
pageSizeOptions = [] as number[],
onPageSizeChange,
class: className = "",
} = $props();
let totalPages = $derived(Math.max(1, Math.ceil(total / pageSize)));
let start = $derived(total === 0 ? 0 : page * pageSize + 1);
let end = $derived(Math.min((page + 1) * pageSize, total));
function goToPage(p: number) {
if (p >= 0 && p < totalPages) page = p;
}
// Generate page number range (max 7 buttons with ellipsis)
let pageNumbers = $derived.by(() => {
const pages: (number | "...")[] = [];
if (totalPages <= 7) {
for (let i = 0; i < totalPages; i++) pages.push(i);
} else {
pages.push(0);
if (page > 2) pages.push("...");
const start = Math.max(1, page - 1);
const end = Math.min(totalPages - 2, page + 1);
for (let i = start; i <= end; i++) pages.push(i);
if (page < totalPages - 3) pages.push("...");
pages.push(totalPages - 1);
}
return pages;
});
</script>
<div class="flex min-w-0 flex-col gap-3 sm:flex-row sm:items-center sm:justify-between {className}">
<!-- Summary -->
<div class="min-w-0 text-sm text-text-muted">
{#if total > 0}
{(showingText || $t.dashboard?.showing || "Showing {start}-{end} of {total}")
.replace("{start}", String(start))
.replace("{end}", String(end))
.replace("{total}", String(total))}
{:else}
{$t.common?.no_data || "No items"}
{/if}
</div>
<div class="flex min-w-0 flex-wrap items-center gap-2">
<!-- Page size selector -->
{#if pageSizeOptions.length > 0}
<select
class="text-sm border border-border rounded px-2 py-1 bg-surface-card text-text"
value={pageSize}
onchange={(e) => {
const val = Number(e.currentTarget.value);
if (onPageSizeChange) onPageSizeChange(val);
}}
>
{#each pageSizeOptions as opt (opt)}
<option value={opt}>{opt}</option>
{/each}
</select>
{/if}
<!-- Page number buttons (only when > 1 page) -->
{#if totalPages > 1}
<div class="flex max-w-full flex-wrap gap-1">
{#each pageNumbers as p, i (String(p) + "-" + i)}
{#if p === "..."}
<span class="px-2 py-1 text-sm text-text-muted"></span>
{:else}
<button
class="px-2 py-1 text-sm rounded transition-colors
{p === page
? 'bg-primary text-white'
: 'text-text hover:bg-surface-muted'}"
onclick={() => goToPage(p)}
>
{p + 1}
</button>
{/if}
{/each}
</div>
{/if}
<!-- Prev / Next -->
<Button
variant="secondary"
size="sm"
disabled={page === 0}
onclick={() => goToPage(page - 1)}
>
{$t.dashboard?.previous || "Prev"}
</Button>
<Button
variant="secondary"
size="sm"
disabled={page >= totalPages - 1}
onclick={() => goToPage(page + 1)}
>
{$t.dashboard?.next || "Next"}
</Button>
</div>
</div>
<!-- #endregion UI.Pagination -->