|
1 | 1 | <div class="flex flex-col min-h-screen w-full font-display min-h-[calc(100vh-64px)]"> |
2 | 2 |
|
3 | 3 | <main class="flex-1 max-w-5xl w-full mx-auto px-4 md:px-6 py-6 md:py-10 pb-20"> |
| 4 | + <header class="mb-6"> |
| 5 | + <h1 class="text-2xl md:text-3xl font-bold tracking-tight text-gray-900 dark:text-white mb-1">{{ heading() }}</h1> |
| 6 | + <p class="text-sm text-gray-500 dark:text-gray-400">{{ intro() }}</p> |
| 7 | + </header> |
| 8 | + |
4 | 9 | <div |
5 | 10 | class="sticky top-0 z-40 bg-white/80 dark:bg-black/0 backdrop-blur-md pb-4 pt-2 -mt-2 mb-6 transition-colors duration-300"> |
6 | 11 | <div class="flex items-center gap-4"> |
|
23 | 28 | class="flex flex-wrap items-center gap-3 mb-6 pb-5 border-b border-gray-100 dark:border-gray-800/60"> |
24 | 29 | <div class="flex flex-wrap items-center gap-2"> |
25 | 30 | @for (reg of registries(); track reg.value) { |
26 | | - <button type="button" (click)="!reg.disabled && toggleRegistry(reg.value)" [class.opacity-40]="reg.disabled" |
27 | | - [attr.aria-pressed]="isRegistrySelected(reg.value)" [attr.aria-disabled]="reg.disabled ? 'true' : null" |
28 | | - [class.cursor-not-allowed]="reg.disabled" |
| 31 | + @if (reg.disabled) { |
| 32 | + <span aria-disabled="true" |
| 33 | + class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold border border-gray-200 dark:border-gray-700/60 opacity-40 cursor-not-allowed"> |
| 34 | + <img [src]="reg.icon" alt="" width="14" height="14" loading="lazy" class="w-3.5 h-3.5 object-contain" |
| 35 | + (error)="hideBrokenIcon($event)" /> |
| 36 | + <span>{{ reg.label }}</span> |
| 37 | + </span> |
| 38 | + } @else { |
| 39 | + <a routerLink="/packages" [queryParams]="registryParams(reg.value)" |
| 40 | + [attr.aria-current]="isRegistrySelected(reg.value) ? 'true' : null" |
29 | 41 | class="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-semibold border transition-all duration-200" |
30 | 42 | [ngClass]="{ |
31 | 43 | 'bg-primary/10 text-primary border-primary/30 shadow-sm shadow-primary/5': isRegistrySelected(reg.value), |
32 | | - 'bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300': !isRegistrySelected(reg.value) && !reg.disabled |
| 44 | + 'bg-gray-50 dark:bg-gray-900 text-gray-500 dark:text-gray-400 border-gray-200 dark:border-gray-700/60 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300': !isRegistrySelected(reg.value) |
33 | 45 | }"> |
34 | 46 | <img [src]="reg.icon" alt="" width="14" height="14" loading="lazy" class="w-3.5 h-3.5 object-contain" |
35 | 47 | (error)="hideBrokenIcon($event)" /> |
36 | 48 | <span>{{ reg.label }}</span> |
37 | | - </button> |
| 49 | + </a> |
| 50 | + } |
38 | 51 | } |
39 | 52 | </div> |
40 | 53 |
|
@@ -116,13 +129,35 @@ <h3 class="text-lg font-bold text-gray-900 dark:text-white">The package list cou |
116 | 129 | } |
117 | 130 | </div> |
118 | 131 |
|
119 | | - @if (hasMore()) { |
120 | | - <div class="mt-8 flex justify-center"> |
121 | | - <button type="button" (click)="loadMore()" |
122 | | - class="w-full md:w-auto px-8 py-2.5 border border-gray-200 dark:border-gray-700 rounded-lg text-sm font-medium text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"> |
123 | | - Load more packages |
124 | | - </button> |
125 | | - </div> |
| 132 | + @if (totalPages() > 1) { |
| 133 | + <nav class="mt-8 flex flex-wrap justify-center gap-2" aria-label="Package list pages"> |
| 134 | + @if (currentPage() > 1) { |
| 135 | + <a routerLink="/packages" [queryParams]="pageParams(currentPage() - 1)" rel="prev" aria-label="Previous page" |
| 136 | + class="flex items-center px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"> |
| 137 | + <lucide-icon [img]="ChevronLeftIcon" [size]="16"></lucide-icon> |
| 138 | + </a> |
| 139 | + } |
| 140 | + @for (link of pages(); track link.kind === 'page' ? link.page : link.key) { |
| 141 | + @if (link.kind === 'page') { |
| 142 | + <a routerLink="/packages" [queryParams]="pageParams(link.page)" |
| 143 | + [attr.aria-current]="currentPage() === link.page ? 'page' : null" |
| 144 | + class="px-3 md:px-4 py-2 border rounded-lg text-sm font-medium transition-colors" |
| 145 | + [ngClass]="currentPage() === link.page |
| 146 | + ? 'bg-primary/10 text-primary border-primary/30' |
| 147 | + : 'border-gray-200 dark:border-gray-700 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900'"> |
| 148 | + {{ link.page }} |
| 149 | + </a> |
| 150 | + } @else { |
| 151 | + <span class="px-1 py-2 text-sm text-gray-400 dark:text-gray-600" aria-hidden="true">…</span> |
| 152 | + } |
| 153 | + } |
| 154 | + @if (currentPage() < totalPages()) { |
| 155 | + <a routerLink="/packages" [queryParams]="pageParams(currentPage() + 1)" rel="next" aria-label="Next page" |
| 156 | + class="flex items-center px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-lg text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors"> |
| 157 | + <lucide-icon [img]="ChevronRightIcon" [size]="16"></lucide-icon> |
| 158 | + </a> |
| 159 | + } |
| 160 | + </nav> |
126 | 161 | } |
127 | 162 | } @else { |
128 | 163 | <div |
|
0 commit comments