diff --git a/src/app/shared/ui/pagination/pagination.component.html b/src/app/shared/ui/pagination/pagination.component.html new file mode 100644 index 0000000..af7d941 --- /dev/null +++ b/src/app/shared/ui/pagination/pagination.component.html @@ -0,0 +1,37 @@ + diff --git a/src/app/shared/ui/pagination/pagination.component.scss b/src/app/shared/ui/pagination/pagination.component.scss new file mode 100644 index 0000000..c4b99b3 --- /dev/null +++ b/src/app/shared/ui/pagination/pagination.component.scss @@ -0,0 +1,48 @@ +.app-pagination { + display: flex; + align-items: center; + gap: var(--space-xs, 0.25rem); +} + +.app-pagination__page { + min-width: 32px; + height: 32px; + padding: 0 var(--space-xs, 0.25rem); + border: 1px solid transparent; + border-radius: var(--radius-md, 6px); + background: transparent; + color: var(--text-primary, #1f322d); + font-size: 0.8125rem; + cursor: pointer; + transition: background-color var(--transition-fast, 120ms ease), + border-color var(--transition-fast, 120ms ease); + + &:hover { + background: var(--bg-secondary, #f4f4f5); + } + + &:focus-visible { + outline: 2px solid var(--primary-color, #2f6e5d); + outline-offset: 2px; + } +} + +.app-pagination__page--active { + background: var(--primary-color, #2f6e5d); + color: var(--bg-primary, #fff); + + &:hover { + background: var(--primary-color, #2f6e5d); + } +} + +.app-pagination__ellipsis { + padding: 0 var(--space-xs, 0.25rem); + color: var(--text-secondary, #6b7280); +} + +@media (prefers-reduced-motion: reduce) { + .app-pagination__page { + transition: none; + } +} diff --git a/src/app/shared/ui/pagination/pagination.component.ts b/src/app/shared/ui/pagination/pagination.component.ts new file mode 100644 index 0000000..97b9485 --- /dev/null +++ b/src/app/shared/ui/pagination/pagination.component.ts @@ -0,0 +1,58 @@ +import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; +import { ButtonComponent } from '../button/button.component'; + +const SIBLING_COUNT = 1; +const ELLIPSIS = '…' as const; + +@Component({ + selector: 'app-pagination', + standalone: true, + imports: [ButtonComponent], + templateUrl: './pagination.component.html', + styleUrl: './pagination.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class PaginationComponent { + readonly currentPage = input.required(); + readonly totalPages = input.required(); + readonly previousLabel = input('Previous'); + readonly nextLabel = input('Next'); + + readonly pageChange = output(); + + protected readonly pages = computed<(number | typeof ELLIPSIS)[]>(() => { + const total = this.totalPages(); + const current = this.currentPage(); + const result: (number | typeof ELLIPSIS)[] = []; + + const rangeStart = Math.max(2, current - SIBLING_COUNT); + const rangeEnd = Math.min(total - 1, current + SIBLING_COUNT); + + result.push(1); + if (rangeStart > 2) { + result.push(ELLIPSIS); + } + for (let page = rangeStart; page <= rangeEnd; page++) { + result.push(page); + } + if (rangeEnd < total - 1) { + result.push(ELLIPSIS); + } + if (total > 1) { + result.push(total); + } + + return result; + }); + + protected goTo(page: number): void { + if (page === this.currentPage() || page < 1 || page > this.totalPages()) { + return; + } + this.pageChange.emit(page); + } + + protected isEllipsis(value: number | typeof ELLIPSIS): value is typeof ELLIPSIS { + return value === ELLIPSIS; + } +}