Feat: Replace pagination bar with numbered page buttons and chevron controls

Adds « ‹ [1][2][3][4] › » navigation to the library. Page window
slides to keep the current page in view. Prev/next/first/last controls
are always rendered but disabled at their respective bounds. Also wires
up karmaConfig in angular.json so FirefoxHeadless is used for tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 18:11:18 +00:00
parent 40ceecda76
commit 0ad82e60ac
9 changed files with 583 additions and 41 deletions

View File

@@ -90,9 +90,17 @@ const PLACEHOLDER_SVG = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/s
<!-- Pagination controls — only when more than one page -->
<div *ngIf="totalPages > 1 && !showSpinner && !error" class="pagination-bar">
<button *ngIf="currentPage > 1" class="prev-btn" (click)="prevPage()">← Previous</button>
<span class="page-indicator">Page {{ currentPage }} of {{ totalPages }}</span>
<button *ngIf="currentPage < totalPages" class="next-btn" (click)="nextPage()">Next →</button>
<button class="pag-btn first-btn" [disabled]="currentPage === 1" (click)="firstPage()" aria-label="First page">«</button>
<button class="pag-btn prev-btn" [disabled]="currentPage === 1" (click)="prevPage()" aria-label="Previous page"></button>
<button
*ngFor="let p of pageWindow"
class="pag-btn page-btn"
[class.active]="p === currentPage"
(click)="goToPage(p)"
[attr.aria-current]="p === currentPage ? 'page' : null"
>{{ p }}</button>
<button class="pag-btn next-btn" [disabled]="currentPage === totalPages" (click)="nextPage()" aria-label="Next page"></button>
<button class="pag-btn last-btn" [disabled]="currentPage === totalPages" (click)="lastPage()" aria-label="Last page">»</button>
</div>
</div>
`,
@@ -130,10 +138,11 @@ const PLACEHOLDER_SVG = `data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/s
.retry-btn { padding: 8px 24px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; transition: border-color var(--transition); }
.retry-btn:hover { border-color: var(--border-focus); }
.total-count { text-align: center; color: var(--text-muted); font-size: 0.85rem; margin: 16px 0 8px; }
.pagination-bar { display: flex; justify-content: center; align-items: center; gap: 16px; margin: 16px 0 24px; }
.prev-btn, .next-btn { padding: 8px 20px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; transition: border-color var(--transition); }
.prev-btn:hover, .next-btn:hover { border-color: var(--border-focus); }
.page-indicator { color: var(--text-muted); font-size: 0.9rem; }
.pagination-bar { display: flex; justify-content: center; align-items: center; gap: 6px; margin: 16px 0 24px; flex-wrap: wrap; }
.pag-btn { min-width: 36px; height: 36px; padding: 0 10px; background: var(--surface-raised); color: var(--text); border: 1px solid var(--border); border-radius: var(--radius); cursor: pointer; font-size: 0.95rem; transition: border-color var(--transition), background var(--transition); }
.pag-btn:hover:not(:disabled) { border-color: var(--border-focus); }
.pag-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.page-btn.active { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
`],
})
export class LibraryComponent implements OnInit {
@@ -225,6 +234,37 @@ export class LibraryComponent implements OnInit {
});
}
get pageWindow(): number[] {
let start = Math.max(1, this.currentPage - 1);
const end = Math.min(this.totalPages, start + 3);
start = Math.max(1, end - 3);
return Array.from({ length: end - start + 1 }, (_, i) => start + i);
}
goToPage(page: number): void {
if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
this.currentPage = page;
this.router.navigate([], { queryParams: { page: this.currentPage }, queryParamsHandling: 'merge' });
this.load();
}
}
firstPage(): void {
if (this.currentPage !== 1) {
this.currentPage = 1;
this.router.navigate([], { queryParams: { page: 1 }, queryParamsHandling: 'merge' });
this.load();
}
}
lastPage(): void {
if (this.currentPage !== this.totalPages) {
this.currentPage = this.totalPages;
this.router.navigate([], { queryParams: { page: this.totalPages }, queryParamsHandling: 'merge' });
this.load();
}
}
nextPage(): void {
if (this.currentPage < this.totalPages) {
this.currentPage++;