Files
Mangalord/frontend/src/lib/components/Pager.svelte
MechaCat02 5c22dfdb41 feat: paginate list views, fix stale page titles, tidy admin filter bar
Bundle of small UI/UX fixes plus a build hygiene tweak.

* List pagination — Home (`/`) and `/authors/[id]` silently capped at
  the backend default of 50 with no UI to advance. New reusable
  `Pager.svelte` (Prev/Next + numbered with ellipsis), URL-synced
  `?page=N`, and filter/search/sort reset to page 1 so users aren't
  stranded on an out-of-range page. Count label now shows a range
  ("Showing 51–100 of 237").

* Stale page title — Pages without a `<svelte:head><title>` left the
  document title at whatever the last manga / author / collection page
  set it to. Move static-route titles into a route-id → title map in
  the root layout and invert every dynamic title to brand-first
  (`Mangalord | {X}`) for consistency.

* Admin filter bar — `/admin/mangas` search input had `flex: 1` and
  ballooned across the row, shoving the sync-state select + Search
  button to the far right. Cap at 24rem, vertical-align the row, and
  promote the previously aria-only "Sync state" label to visible text.

* Build hygiene — `backend/target` had grown to 68 GiB. Cleaned and
  added `[profile.dev] debug = "line-tables-only"` (and `[profile.test]`
  too) to cut future dev builds by ~50–70% while keeping line numbers
  in backtraces.

Also: configure vitest to resolve Svelte's browser entry so
`@testing-library/svelte` can mount components in jsdom — needed for
the new `Pager.svelte.test.ts`.

Bump 0.48.0 -> 0.49.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 21:18:53 +02:00

129 lines
3.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
type Props = {
page: number;
totalPages: number;
onChange: (page: number) => void;
testid?: string;
};
let { page, totalPages, onChange, testid }: Props = $props();
type Slot = number | 'ellipsis';
// Compact layout: always show first + last, surround the current page with
// its direct neighbours, and use "…" to elide the rest. Keeps the bar to
// at most 7 buttons regardless of totalPages.
function buildSlots(p: number, total: number): Slot[] {
if (total <= 7) {
return Array.from({ length: total }, (_, i) => i + 1);
}
const out: Slot[] = [1];
if (p <= 4) {
for (let i = 2; i <= 5; i++) out.push(i);
out.push('ellipsis');
out.push(total);
} else if (p >= total - 3) {
out.push('ellipsis');
for (let i = total - 4; i <= total; i++) out.push(i);
} else {
out.push('ellipsis');
out.push(p - 1);
out.push(p);
out.push(p + 1);
out.push('ellipsis');
out.push(total);
}
return out;
}
const slots = $derived(buildSlots(page, totalPages));
</script>
{#if totalPages > 1}
<nav class="pager" aria-label="Pagination" data-testid={testid}>
<button
type="button"
class="step"
disabled={page <= 1}
onclick={() => onChange(page - 1)}
aria-label="Previous page"
>
Prev
</button>
{#each slots as slot, i (i)}
{#if slot === 'ellipsis'}
<span class="ellipsis" aria-hidden="true"></span>
{:else}
<button
type="button"
class="num"
class:active={slot === page}
aria-current={slot === page ? 'page' : undefined}
aria-label={`Go to page ${slot}`}
onclick={() => onChange(slot)}
>
{slot}
</button>
{/if}
{/each}
<button
type="button"
class="step"
disabled={page >= totalPages}
onclick={() => onChange(page + 1)}
aria-label="Next page"
>
Next
</button>
</nav>
{/if}
<style>
.pager {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-1);
margin: var(--space-4) 0;
justify-content: center;
}
.step,
.num {
min-width: 36px;
height: 36px;
padding: 0 var(--space-2);
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-md);
color: var(--text);
cursor: pointer;
font-size: var(--font-sm);
}
.step:hover:not(:disabled),
.num:hover:not(.active) {
border-color: var(--primary);
}
.step:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.num.active {
background: var(--primary);
color: var(--primary-contrast);
border-color: var(--primary);
cursor: default;
}
.ellipsis {
padding: 0 var(--space-1);
color: var(--text-muted);
user-select: none;
}
</style>