@col/pagination
Versie: 1.0.0 Toegevoegd: 2026-08-23
Herkomst
Geëxtraheerd uit growerportal2/src/components/ui/pagination.tsx, dat daar op
één dag ontstond uit vijf handgeschreven kopieën van dezelfde
Previous/Next-balk (admin/imports/batch-records-dialog.tsx,
admin/imports/data-sync-tab.tsx, admin/imports/salessheet-tab.tsx,
features/fust/components/fust-audit-log.tsx,
features/fust/components/fust-email-log.tsx).
De aanleiding was concreet: de importmonitor toonde 2.743 orderregels over 55
bladzijden, en er was geen andere manier om bij pagina 40 te komen dan
negenendertig keer Next. Drie van de vijf kopieën gebruikten tekstknoppen, twee
pijl-iconen; verder waren ze identiek. KCB heeft dezelfde balk nog in eigen
beheer, in components/settings/email-ingestion-log.tsx en de twee
zendingenschermen — ook daar zonder kiezer.
Waarvoor
De navigatie onder een gepagineerde tabel: een pagina terug, een pagina vooruit, en een kiezer die rechtstreeks naar een willekeurig blad springt. Pak het zodra een lijst meer dan een handvol pagina's kan krijgen — het verschil met kale Previous/Next-knoppen wordt pas voelbaar voorbij een stuk of tien.
Gebruik het niet voor oneindig scrollen, voor een "meer laden"-knop, of voor paginering die de URL moet bijwerken zonder dat de aanroeper dat zelf regelt. Het component is controlled en weet niets van routing.
Het toont ook niet hoeveel rijen er in totaal zijn. Die tekst
("Toont 1-50 van 2.743") staat in de bronapp links naast de balk en verschilt
per scherm — zet hem zelf ernaast in een flex items-center justify-between.
Wat je app moet leveren
| Prop | Type | Betekenis |
|---|---|---|
page |
number |
Huidige pagina, 1-based. Verplicht. Een waarde buiten 1…totalPages wordt geklemd, zodat een filterwijziging die het aantal pagina's verkleint geen kapotte balk oplevert voordat de aanroeper zijn state bijwerkt. |
totalPages |
number |
Aantal pagina's. Verplicht. Bij <= 1 rendert het component null. |
onPageChange |
(page: number) => void |
Aangeroepen met het nieuwe paginanummer. Verplicht. Het component houdt zelf niets bij. |
disabled |
boolean |
Zet alle drie de besturingselementen uit, bedoeld voor de duur van een fetch. |
className |
string |
Extra klassen op de wrapper. |
labels |
zie hieronder | Alle tekst die een gebruiker ziet of hoort. |
interface PaginationLabels {
previous?: string // default 'Previous page'
next?: string // default 'Next page'
picker?: string // default 'Go to page'
page?: (page: number, totalPages: number) => string // default '40 / 55'
pageItem?: (page: number, totalPages: number) => string // default '40'
}
previous, next en picker zijn aria-labels: ze zijn de enige naam die de
knoppen hebben, want er staat alleen een icoon in. Geef ze mee zodra je app
vertaalt.
Typisch gebruik:
const [page, setPage] = useState(1)
const { data, loading } = useFetch(`/api/lots?page=${page}`)
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Toont {start}-{end} van {formatNumber(total)}
</p>
<Pagination
page={page}
totalPages={data?.totalPages ?? 1}
onPageChange={setPage}
disabled={loading}
labels={{ page: (current, total) => `Pagina ${current} van ${total}` }}
/>
</div>
Zet de paginastate op één plek en reset hem naar 1 als een filter verandert — anders vraagt het scherm pagina 40 van een lijst die er nog maar 3 heeft.
Bestanden
| Bestand | Landt in | Soort |
|---|---|---|
pagination.tsx |
components/ |
beheerd |
Upstream shadcn-afhankelijkheden: button, dropdown-menu. Geen cssVars.
Let op: shadcn heeft zelf óók een pagination-primitive. Die staat bewust
niet in registryDependencies, en dit bestand landt in components/, niet
in components/ui/ — zo overschrijft dit item die primitive niet en andersom.
Gebruikt door
| App | Sinds versie | Opmerkingen |
|---|---|---|
growerportal2 |
1.0.0 | Vijf schermen: drie importtabbladen (admin/imports/*) en de twee fust-logs. De eigen kopie in src/components/ui/pagination.tsx is verwijderd. Labels komen uit src/components/pagination-labels.ts — één gedeeld object dat page naar Page 40 of 55 schrijft, want dat portaal is Engelstalig. Let op: npx shadcn add overschreef daar src/components/ui/button.tsx met een nieuwere upstream base-nova-versie (o.a. zónder 'use client'); teruggedraaid met git checkout — README-regel 6, en het is geen theorie. |
Bewuste keuzes
- Een dropdown-menu, geen
Select. De bron ingrowerportal2gebruikt@/components/ui/select, en dat kan hier niet: de consumers zijn verdeeld over Radix en Base UI, en die twee renderen de gekozen waarde in de trigger verschillend (Base UI toont de rauwevaluetenzij je een functie als children meegeeft, Radix toont het label van het item). Datzelfde bestandselect.tsxbestaat bovendien nog niet in deze repo, en het alsregistryDependenciesopnemen zou de Base UI-versie ingrowerportal2stilzwijgend door een Radix-versie vervangen — regel 6 in de README.dropdown-menustaat al in twee andere items en werkt in beide werelden. - De trigger is geen
<Button asChild>.buttonVariantsrechtstreeks opDropdownMenuTrigger, zoals@col/language-selector— AGENTS.md regel 5. nullbij één pagina, in het component zelf. Alle vijf de bronkopieën stonden achter een{totalPages > 1 && …}in de aanroeper. Dat is vijf keer dezelfde regel die vergeten kan worden; hier hoort hij één keer.- Elke pagina staat in de lijst, geen ellipsis. Een
1 … 39 40 41 … 55-balk is de gangbare oplossing, maar lost het probleem juist niet op: het gat is precies waar je heen wilt. Bij een paar honderd pagina's blijft een scrollende lijst werkbaar; zie Bekende beperkingen. - Het standaardlabel is
40 / 55, nietPage 40 of 55. Een registry-item hoort geen taal op te dringen. Wie woorden wil, geeftlabels.pagemee. - Getest tegen Radix, in gebruik op Base UI. De tests in deze repo draaien
op de Radix-primitives die hier in
components/ui/staan; de eerste consumer (growerportal2) draait volledig op Base UI. Dat is precies waarom de trigger geenasChildgebruikt en de kiezer geenSelectis — maar het betekent ook dat een Base UI-eigenaardigheid indropdown-menuhier niet door een test wordt gevangen. tabular-numsop de trigger en de regels. Zonder dat springt de breedte van de trigger bij elke paginawissel, omdat cijfers in de UI-fonts niet even breed zijn.
Bekende beperkingen
- De lijst rendert één regel per pagina. Bij duizenden pagina's (een tabel van honderdduizenden rijen op 50 per blad) wordt dat een trage dropdown. Is dat jouw geval, dan is een invoerveld met een paginanummer het betere besturingselement — dit item biedt dat niet.
- Geen typeahead over meerdere cijfers: het menu springt op de eerste toets naar de eerste regel die daarmee begint, dus "4" landt op pagina 4, niet op 40.
- Geen eerste/laatste-knoppen. Naar het laatste blad ga je via de kiezer.
- Geen ondersteuning voor een aangepaste iconenset;
ChevronLeft,ChevronDownenChevronRightuitlucide-reactliggen vast.