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]>
136 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|