first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
# Pagination
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs --> - pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- pagination.variants — `paginationVariants`, `pagina…
- [pagination.variants](./pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
- [pagination.variants](./pagination.variants.md) — `paginationVariants`, `paginationContentVariants`, `paginationItemVariants`, `paginationLinkVariants`, `paginationPrevN…
## Purpose
`<Pagination>` and its sub-components (`<PaginationContent>`, `<PaginationItem>`, `<PaginationLink>`, `<PaginationPrevious>`, `<PaginationNext>`, `<PaginationEllipsis>`, `<PaginationStatus>`) are the chassis page-navigation primitive. Pure HTML composition — no Radix wrapper — so the markup is deterministic and trivially server-rendered. The wrapper is a semantic `<nav>` whose accessible name flows through the bundled `react-ui.pagination.label` translation key. Inside, an unordered list (`<PaginationContent>`) carries page link rows (`<PaginationLink>`) interleaved with previous / next / ellipsis affordances. The current page sets `isActive`, which adds `aria-current="page"` so screen-readers announce it as the active page.
The "Page X of Y" status caption (`<PaginationStatus>`) renders through a real ICU MessageFormat via `@suaveplan/i18n` `MessageParser` so the locale's plural rule chooses the right form. Welsh covers six plural categories (`zero | one | two | few | many | other`), and the chassis test suite exercises each one against the bundled `cy-GB` template post-wireup.
## Features
- Semantic `<nav>` + `<ul>` + `<li>` markup — pure HTML; SSR-trivial; no JavaScript required for the trail to render.
- Translated `aria-label` on the wrapper (`react-ui.pagination.label`).
- `<PaginationLink isActive>` carries `aria-current="page"` so screen-readers announce the active page.
- `<PaginationPrevious>` / `<PaginationNext>` carry translated `aria-label`s (`react-ui.pagination.previous` / `react-ui.pagination.next`) and RTL-flipped chevron icons.
- `<PaginationEllipsis>` provides a collapsed-page indicator with `aria-hidden="true"` plus a translated `aria-label` for assistive tech.
- `<PaginationStatus>` renders "Page X of Y" via `MessageParser` ICU plural so locale-appropriate plurals fire — Welsh's six-form plural is exercised end-to-end.
- Three size variants on `<PaginationLink>` (`default` / `sm` / `icon`).
- All sub-components forward refs to their underlying HTML elements.
## Usage: Basic Example
```tsx
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@suaveplan/react-ui";
export function PagedList({ page }: { page: number }) {
return (
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious href={`?page=${page - 1}`} />
</PaginationItem>
{[1, 2, 3].map((n) => (
<PaginationItem key={n}>
<PaginationLink
href={`?page=${n}`}
isActive={n === page}
>
{n}
</PaginationLink>
</PaginationItem>
))}
<PaginationItem>
<PaginationEllipsis />
</PaginationItem>
<PaginationItem>
<PaginationNext href={`?page=${page + 1}`} />
</PaginationItem>
</PaginationContent>
</Pagination>
);
}
```
## Usage: Advanced Example
```tsx
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
PaginationStatus,
} from "@suaveplan/react-ui";
interface Props {
readonly current: number;
readonly total: number;
readonly buildHref: (page: number) => string;
}
function paginationWindow(
current: number,
total: number
): ReadonlyArray<number | null> {
if (total <= 7) {
return Array.from({ length: total }, (_, i) => i + 1);
}
if (current <= 3) return [1, 2, 3, 4, null, total - 1, total];
if (current >= total - 2) {
return [1, 2, null, total - 3, total - 2, total - 1, total];
}
return [1, null, current - 1, current, current + 1, null, total];
}
export function FullPagination({ current, total, buildHref }: Props) {
const items = paginationWindow(current, total);
return (
<div className="flex flex-col items-center gap-2">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
href={buildHref(Math.max(1, current - 1))}
/>
</PaginationItem>
{items.map((entry, i) => (
<PaginationItem key={`${entry}-${i}`}>
{entry === null ? (
<PaginationEllipsis />
) : (
<PaginationLink
href={buildHref(entry)}
isActive={entry === current}
>
{entry}
</PaginationLink>
)}
</PaginationItem>
))}
<PaginationItem>
<PaginationNext
href={buildHref(Math.min(total, current + 1))}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<PaginationStatus current={current} total={total} />
</div>
);
}
```
## API Reference
### `<Pagination>`
Renders a `<nav role="navigation">` with a translated `aria-label`.
| Prop | Type | Default | Description |
|---|---|---|---|
| `ariaLabelKey` | `TranslationKey` | `react-ui.pagination.label` | Override the accessible name. |
### `<PaginationContent>`, `<PaginationItem>`
Pure layout — `<ul>` and `<li>` containers.
### `<PaginationLink>`
Renders an `<a>`. Sets `aria-current="page"` when `isActive` is true.
| Prop | Type | Default | Description |
|---|---|---|---|
| `size` | `"default" \| "sm" \| "icon"` | `"icon"` | Visual size. |
| `isActive` | `boolean` | `false` | Marks as the current page. |
### `<PaginationPrevious>`, `<PaginationNext>`
Arrow affordances with translated `aria-label`s and chevron icons.
| Prop | Type | Default | Description |
|---|---|---|---|
| `ariaLabelKey` | `TranslationKey` | `react-ui.pagination.previous` / `.next` | Override the accessible name. |
### `<PaginationEllipsis>`
Collapsed-page indicator. Pure visual filler with `aria-hidden="true"` and a translated `aria-label` (`react-ui.pagination.morePages`).
### `<PaginationStatus>`
The "Page X of Y" caption.
| Prop | Type | Default | Description |
|---|---|---|---|
| `current` | `number` | _required_ | Active page number. |
| `total` | `number` | _required_ | Total page count. |
| `statusKey` | `TranslationKey` | `react-ui.pagination.pageXofY` | Override the ICU template key. |
The template format is `"Page {current} of {total}"` in `en-GB`, with locale-appropriate plural variants in the other bundled locales.
## Implementation Notes
- The chassis NEVER wraps pagination in a Radix primitive — pure HTML keeps SSR trivial and matches shadcn upstream's recommendation.
- `<PaginationPrevious>` / `<PaginationNext>` chevron icons carry `rtl:rotate-180` so the arrows flip in RTL contexts.
- The `<PaginationStatus>` uses `aria-live="polite"` and `aria-atomic="true"` so screen-readers announce updates as the user navigates without interrupting the focus flow.
- `MessageParser` is constructed once per render via `useMemo` so the parser cache is preserved across re-renders.
- Per the Wave 3d reconciliation finding (#4), the `t()` call returns the ICU template, then `messageParser.format(template, values, { locale })` performs interpolation and plural resolution.
- Per the Wave 3d reconciliation finding (#2), the active locale is read via the `(i18n as { readonly locale: string }).locale` cast since `useTranslation()` throws `REACT_I18N.NO_PROVIDER` in dev mode if called outside a provider, making the optional-chain branch unreachable.