Files
tailwind-ui-library/src/elements/Pagination.tsx
T
tbenandClaude Opus 5.5 7d7eae5801 Custom Radios indicator with one-line labels; keep Pagination within its parent
Radios: each radio stays on one line, with its label clamped to a single
line (full text on hover) like Checkbox. Pass `wrap` to let labels flow
onto multiple lines. The native radio is replaced by a custom indicator
aligned to the label's first line; the input stays (visually hidden) for
forms and keyboard use, so inputProps.className now lands on a hidden
input - style .twui-radio-indicator instead. Adds twui-radios,
twui-radio, twui-radio-label and twui-radio-indicator classes.

Pagination: flex children could not shrink below their content, so many
pages pushed the component past its parent. Add min-w-0 so the page
buttons scroll inside the parent instead. Adds twui-pagination.

Playground: demos for both, and bump bunext to 1.3.5.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-08 08:52:11 +01:00

136 lines
4.5 KiB
TypeScript

import type { ComponentProps, Dispatch, SetStateAction } from "react";
import _ from "lodash";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { twMerge } from "tailwind-merge";
import Row from "../layout/Row";
import Button from "../layout/Button";
import EmptyContent from "./EmptyContent";
import Span from "../layout/Span";
type Props = ComponentProps<typeof Row> & {
page?: number;
setPage?: Dispatch<SetStateAction<number>>;
count?: number;
limit?: number;
};
/**
* # Pagination Component
* Stays within its parent's width: the page buttons scroll horizontally
* when there are too many to fit.
* @className twui-pagination
*/
export default function Pagination({
count,
page,
setPage,
limit,
...props
}: Props) {
if (!page || !limit)
return <EmptyContent title={`page and limit are both required`} />;
if (!count) return null;
const isLimit = limit * page >= count;
const pages = Math.ceil(count / limit);
const next_button = (
<Button
title="Next Page Button"
onClick={() => {
window.scrollTo({ top: 0, behavior: "smooth" });
setPage?.((prev) => prev + 1);
}}
variant="outlined"
size="small"
className="p-1 shrink-0"
disabled={isLimit}
>
<ChevronRight size={20} />
</Button>
);
return (
<Row
{...props}
className={twMerge(
"w-full min-w-0 max-w-full justify-between md:flex-nowrap twui-pagination",
props.className,
)}
>
{pages > 1 && (
<Button
title="Previous Page Button"
onClick={() => {
window.scrollTo({ top: 0, behavior: "smooth" });
setPage?.((prev) => prev - 1);
}}
variant="outlined"
size="small"
className="p-1 shrink-0"
disabled={page == 1}
>
<ChevronLeft size={20} />
</Button>
)}
{pages > 1 && <div className="flex md:hidden">{next_button}</div>}
<Row
className={twMerge(
"gap-6 w-full min-w-0 flex-1 md:flex-nowrap justify-center",
)}
>
<Span
size="small"
variant="faded"
className="whitespace-nowrap shrink-0"
>
Page {page} / {pages}
</Span>
{pages > 1 && (
<Row
className={twMerge(
"flex-nowrap overflow-x-auto p-1 min-w-0 max-w-[90%]",
)}
>
{Array(pages)
.fill(0)
.map((p, index) => {
const isCurrent = page == index + 1;
return (
<Button
title={`Page ${index + 1}`}
onClick={() => {
window.scrollTo({
top: 0,
behavior: "smooth",
});
setPage?.(index + 1);
}}
variant={
isCurrent ? "normal" : "outlined"
}
size="small"
color={isCurrent ? "primary" : "gray"}
className={twMerge(
"p-1 w-6 h-6 min-w-6",
)}
key={index}
>
{index + 1}
</Button>
);
})}
</Row>
)}
</Row>
{pages > 1 && <div className="hidden md:flex">{next_button}</div>}
</Row>
);
}