feat: pagination initial structure added, tailwind variants upgraded

This commit is contained in:
Junior Garcia 2023-03-11 11:13:44 -03:00
parent d12ee9e768
commit 99dffc3c9d
17 changed files with 862 additions and 422 deletions

View File

@ -1,15 +1,20 @@
import Pagination from "./pagination";
import PaginationItem from "./pagination-item";
import PaginationCursor from "./pagination-cursor";
// export types
export type {PaginationProps} from "./pagination";
export type {PaginationItemRenderProps} from "./use-pagination";
export type {PaginationItemProps} from "./pagination-item";
export type {DOTS as PAGINATION_DOTS} from "@nextui-org/use-pagination";
export type {PaginationCursorProps} from "./pagination-cursor";
// misc
export type {PaginationItemValue} from "@nextui-org/use-pagination";
export {PaginationItemType} from "@nextui-org/use-pagination";
// export hooks
export {usePagination} from "./use-pagination";
export {usePaginationItem} from "./use-pagination-item";
// export component
export {Pagination, PaginationItem};
export {Pagination, PaginationItem, PaginationCursor};

View File

@ -0,0 +1,26 @@
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
export interface PaginationCursorProps extends HTMLNextUIProps<"span"> {
/**
* The current active page.
*/
activePage?: number;
}
const PaginationCursor = forwardRef<PaginationCursorProps, "span">((props, ref) => {
const {as, activePage, ...otherProps} = props;
const Component = as || "span";
const domRef = useDOMRef(ref);
return (
<Component ref={domRef} aria-hidden={true} {...otherProps}>
{activePage}
</Component>
);
});
PaginationCursor.displayName = "NextUI.PaginationCursor";
export default PaginationCursor;

View File

@ -1,63 +0,0 @@
import {useState, useEffect, useMemo} from "react";
import {mergeProps} from "@react-aria/utils";
import {forwardRef, HTMLNextUIProps} from "@nextui-org/system";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx} from "@nextui-org/shared-utils";
import {StyledPaginationHighlight} from "./pagination.styles";
export interface PaginationHighlightProps extends HTMLNextUIProps<"div"> {
active: number;
rounded?: boolean;
animated?: boolean;
noMargin?: boolean;
shadow?: boolean;
}
const PaginationHighlight = forwardRef<PaginationHighlightProps, "div">((props, ref) => {
const {className, css, active, shadow, noMargin, rounded, ...otherProps} = props;
const [selfActive, setSelfActive] = useState(active);
const [moveClassName, setMoveClassName] = useState("");
const domRef = useDOMRef(ref);
useEffect(() => {
if (active !== selfActive) {
setSelfActive(active);
setMoveClassName(`nextui-pagination-highlight--moving`);
const timer = setTimeout(() => {
setMoveClassName("");
clearTimeout(timer);
}, 350);
}
}, [active]);
const leftValue = useMemo(
() =>
noMargin
? `var(--nextui--paginationSize) * ${selfActive}`
: `var(--nextui--paginationSize) * ${selfActive} + ${selfActive * 4 + 2}px`,
[selfActive, noMargin],
);
return (
<StyledPaginationHighlight
ref={domRef}
aria-hidden={true}
className={clsx("nextui-pagination-highlight", moveClassName, className)}
css={{
left: "var(--nextui--paginationLeft)",
...css,
}}
noMargin={noMargin}
rounded={rounded}
shadow={shadow}
style={mergeProps({"--nextui--paginationLeft": `calc(${leftValue})`}, props?.style || {})}
{...otherProps}
/>
);
});
PaginationHighlight.displayName = "NextUI.PaginationHighlight";
export default PaginationHighlight;

View File

@ -1,53 +1,61 @@
import {forwardRef} from "@nextui-org/system";
import {PaginationItemParam} from "@nextui-org/use-pagination";
import {PaginationItemValue} from "@nextui-org/use-pagination";
import {useCallback} from "react";
import {DOTS} from "@nextui-org/use-pagination";
import {PaginationItemType} from "@nextui-org/use-pagination";
import {UsePaginationProps, usePagination} from "./use-pagination";
import PaginationItem from "./pagination-item";
import PaginationCursor from "./pagination-cursor";
export interface PaginationProps extends UsePaginationProps {}
export interface PaginationProps extends Omit<UsePaginationProps, "ref"> {}
const Pagination = forwardRef<PaginationProps, "ul">((props, ref) => {
const {
Component,
// showControls,
dotsJump,
loop,
// loop,
slots,
styles,
total,
range,
active,
setPage,
activePage,
// setPage,
// onPrevious,
// onNext,
disableCursor,
disableAnimation,
renderItem: renderItemProp,
getBaseProps,
getItemProps,
getCursorProps,
} = usePagination({ref, ...props});
const renderItem = useCallback(
(value: PaginationItemParam, index: number) => {
const isBefore = index < range.indexOf(active);
(value: PaginationItemValue, index: number) => {
// const isBefore = index < range.indexOf(activePage);
if (renderItemProp && typeof renderItemProp === "function") {
return renderItemProp({
value,
index,
dotsJump,
isDots: value === DOTS,
isBefore,
isActive: value === active,
isPrevious: value === active - 1,
isNext: value === active + 1,
isActive: value === activePage,
isPrevious: value === activePage - 1,
isNext: value === activePage + 1,
isFirst: value === 1,
isLast: value === total,
className: slots.item({class: styles?.item}),
});
}
if (value === PaginationItemType.PREV) {
return <PaginationItem className={slots.prev({class: styles?.prev})}>{"<"}</PaginationItem>;
}
if (value === PaginationItemType.NEXT) {
return <PaginationItem className={slots.next({class: styles?.next})}>{">"}</PaginationItem>;
}
if (value === DOTS) {
return <li>...</li>;
if (value === PaginationItemType.DOTS) {
return <PaginationItem className={slots.item({class: styles?.item})}>...</PaginationItem>;
// return (
// <PaginationEllipsis
// key={`nextui-pagination-item-${value}-${index}`}
@ -66,22 +74,17 @@ const Pagination = forwardRef<PaginationProps, "ul">((props, ref) => {
}
return (
<PaginationItem
key={`${value}-${index}`}
className={slots.item({class: styles?.item})}
isActive={value === active}
value={value}
onPress={() => value !== active && setPage(value)}
>
<PaginationItem key={`${value}-${index}`} {...getItemProps({value})}>
{value}
</PaginationItem>
);
},
[active, dotsJump, loop, range, renderItemProp, setPage, slots.item, styles?.item, total],
[activePage, dotsJump, getItemProps, range, renderItemProp, slots, styles, total],
);
return (
<Component {...getBaseProps()}>
{!disableCursor && !disableAnimation && <PaginationCursor {...getCursorProps()} />}
{range.map(renderItem)}
{/* {controls && (

View File

@ -3,8 +3,9 @@ import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import type {PressEvent} from "@react-types/shared";
import {useMemo} from "react";
import {DOTS} from "@nextui-org/use-pagination";
import {dataAttr} from "@nextui-org/shared-utils";
import {PaginationItemType} from "@nextui-org/use-pagination";
import {ringClasses} from "@nextui-org/theme";
import {clsx, dataAttr, warn} from "@nextui-org/shared-utils";
import {mergeProps} from "@react-aria/utils";
import {useDOMRef} from "@nextui-org/dom-utils";
import {usePress} from "@react-aria/interactions";
@ -34,7 +35,7 @@ export interface UsePaginationItemProps extends Omit<HTMLNextUIProps<"li">, "onC
* @param e MouseEvent
* @deprecated Use `onPress` instead.
*/
onClick?: () => void;
onClick?: HTMLNextUIProps<"li">["onClick"];
/**
* Callback fired when the item is clicked.
* @param e PressEvent
@ -52,18 +53,18 @@ export interface UsePaginationItemProps extends Omit<HTMLNextUIProps<"li">, "onC
const getItemAriaLabel = (page?: string | number) => {
if (!page) return;
switch (page) {
case DOTS:
case PaginationItemType.DOTS:
return "dots element";
case "<":
case PaginationItemType.PREV:
return "previous page button";
case ">":
case PaginationItemType.NEXT:
return "next page button";
case "first":
return "first page button";
case "last":
return "last page button";
default:
return `${page} item`;
return `pagination item ${page}`;
}
};
@ -78,6 +79,7 @@ export function usePaginationItem(props: UsePaginationItemProps) {
onPress,
onClick,
getAriaLabel = getItemAriaLabel,
className,
...otherProps
} = props;
@ -90,7 +92,9 @@ export function usePaginationItem(props: UsePaginationItemProps) {
);
const handlePress = (e: PressEvent) => {
onClick?.();
if (onClick) {
warn("onClick is deprecated, use onPress instead.", "PaginationItem");
}
onPress?.(e);
};
@ -112,6 +116,7 @@ export function usePaginationItem(props: UsePaginationItemProps) {
"data-active": dataAttr(isActive),
"data-focus-visible": dataAttr(isFocusVisible),
"data-focused": dataAttr(isFocused),
className: clsx(isFocusVisible && [...ringClasses], className),
...mergeProps(props, pressProps, focusProps, otherProps),
};
};

View File

@ -1,32 +1,31 @@
import type {ReactNode, Ref} from "react";
import type {PaginationSlots, PaginationVariantProps, SlotsToClasses} from "@nextui-org/theme";
import {HTMLNextUIProps, mapPropsVariants, PropGetter} from "@nextui-org/system";
import {
PaginationItemParam,
usePagination as useBasePagination,
import type {Timer} from "@nextui-org/shared-utils";
import type {ReactNode, Ref} from "react";
import type {HTMLNextUIProps, PropGetter} from "@nextui-org/system";
import type {
UsePaginationProps as UseBasePaginationProps,
PaginationItemValue,
} from "@nextui-org/use-pagination";
import {useMemo} from "react";
import {useEffect, useRef, useMemo} from "react";
import {mapPropsVariants} from "@nextui-org/system";
import {usePagination as useBasePagination} from "@nextui-org/use-pagination";
import {pagination} from "@nextui-org/theme";
import {useDOMRef} from "@nextui-org/dom-utils";
import {clsx, dataAttr} from "@nextui-org/shared-utils";
export type PaginationItemRenderProps = {
value: PaginationItemParam;
value: PaginationItemValue;
index: number;
dotsJump: number;
isActive: boolean;
isNext: boolean;
isPrevious: boolean;
isFirst: boolean;
isLast: boolean;
isDots: boolean;
isBefore: boolean;
isNext: boolean;
isPrevious: boolean;
className: string;
};
interface Props extends HTMLNextUIProps<"ul"> {
interface Props extends Omit<HTMLNextUIProps<"ul">, "onChange"> {
/**
* Ref to the DOM node.
*/
@ -61,7 +60,9 @@ interface Props extends HTMLNextUIProps<"ul"> {
* <Pagination styles={{
* base:"base-classes",
* wrapper: "wrapper-classes",
* prev: "prev-classes", // prev button classes
* item: "item-classes",
* next: "next-classes", // next button classes
* cursor: "cursor-classes", // this is the one that moves when an item is selected
* }} />
* ```
@ -71,6 +72,8 @@ interface Props extends HTMLNextUIProps<"ul"> {
export type UsePaginationProps = Props & UseBasePaginationProps & PaginationVariantProps;
export const CURSOR_TRANSITION_TIMEOUT = 300; // in ms
export function usePagination(originalProps: UsePaginationProps) {
const [props, variantProps] = mapPropsVariants(originalProps, pagination.variantKeys);
@ -78,11 +81,11 @@ export function usePagination(originalProps: UsePaginationProps) {
as,
ref,
styles,
showControls = true,
dotsJump = 5,
loop = false,
showControls = false,
total = 1,
initialPage,
initialPage = 1,
page,
siblings,
boundaries,
@ -95,20 +98,85 @@ export function usePagination(originalProps: UsePaginationProps) {
const Component = as || "ul";
const domRef = useDOMRef(ref);
const cursorRef = useRef<HTMLElement>(null);
const itemsRef = useRef<Map<number, HTMLElement>>();
const {range, active, setPage, previous, next, first, last} = useBasePagination({
let cursorTimer: Timer;
function getItemsRefMap() {
if (!itemsRef.current) {
// Initialize the Map on first usage.
itemsRef.current = new Map();
}
return itemsRef.current;
}
function getItemRef(node: HTMLElement, value: number) {
const map = getItemsRefMap();
if (node) {
map.set(value, node);
} else {
map.delete(value);
}
}
function scrollTo(value: number) {
const map = getItemsRefMap();
const node = map.get(value);
// clean up the cursor timer
clearTimeout(cursorTimer);
if (node) {
// get position of the item
const {offsetLeft} = node;
// move the cursor to the item
if (cursorRef.current) {
cursorRef.current.setAttribute("data-moving", "true");
cursorRef.current.style.transform = `translateX(${offsetLeft}px) scale(1.1)`;
}
cursorTimer = setTimeout(() => {
// reset the scale of the cursor
if (cursorRef.current) {
cursorRef.current.setAttribute("data-moving", "false");
cursorRef.current.style.transform = `translateX(${offsetLeft}px) scale(1)`;
}
clearTimeout(cursorTimer);
}, CURSOR_TRANSITION_TIMEOUT);
}
}
const {range, activePage, setPage, previous, next, first, last} = useBasePagination({
page,
total,
initialPage,
siblings,
boundaries,
total,
showControls,
onChange,
});
useEffect(() => {
if (activePage && !originalProps.disableAnimation) {
scrollTo(activePage);
}
}, [
activePage,
originalProps.disableAnimation,
originalProps.isEven,
originalProps.disableCursor,
]);
const slots = useMemo(
() =>
pagination({
...variantProps,
disableCursor: originalProps.disableCursor || originalProps.disableAnimation,
}),
[...Object.values(variantProps)],
);
@ -116,7 +184,7 @@ export function usePagination(originalProps: UsePaginationProps) {
const baseStyles = clsx(styles?.base, className);
const onNext = () => {
if (loop && active === total) {
if (loop && activePage === total) {
return first();
}
@ -124,7 +192,7 @@ export function usePagination(originalProps: UsePaginationProps) {
};
const onPrevious = () => {
if (loop && active === 1) {
if (loop && activePage === 1) {
return last();
}
@ -135,16 +203,40 @@ export function usePagination(originalProps: UsePaginationProps) {
return {
...props,
ref: domRef,
role: "navigation",
"data-controls": dataAttr(showControls),
"data-loop": dataAttr(loop),
"data-dots-jump": dotsJump,
"data-total": total,
"data-active": active,
"data-active-page": activePage,
className: slots.base({class: baseStyles}),
...otherProps,
};
};
const getItemProps: PropGetter = (props = {}) => {
return {
...props,
ref: (node) => getItemRef(node, props.value),
isActive: props.value === activePage,
className: slots.item({class: styles?.item}),
onPress: () => {
if (props.value !== activePage) {
setPage(props.value);
}
},
};
};
const getCursorProps: PropGetter = (props = {}) => {
return {
...props,
ref: cursorRef,
activePage,
className: slots.cursor({class: styles?.cursor}),
};
};
return {
Component,
showControls,
@ -154,12 +246,16 @@ export function usePagination(originalProps: UsePaginationProps) {
loop,
total,
range,
active,
activePage,
disableCursor: originalProps.disableCursor,
disableAnimation: originalProps.disableAnimation,
setPage,
onPrevious,
onNext,
renderItem,
getBaseProps,
getItemProps,
getCursorProps,
};
}

View File

@ -1,6 +1,6 @@
import React from "react";
import {ComponentStory, ComponentMeta} from "@storybook/react";
import {pagination} from "@nextui-org/theme";
import {button, pagination} from "@nextui-org/theme";
import {Pagination, PaginationProps} from "../src";
@ -8,10 +8,25 @@ export default {
title: "Components/Pagination",
component: Pagination,
argTypes: {
page: {
control: {
type: "number",
},
},
siblings: {
control: {
type: "number",
},
},
boundaries: {
control: {
type: "number",
},
},
variant: {
control: {
type: "select",
options: ["solid", "bordered", "light", "flat", "faded", "shadow", "dot"],
options: ["flat", "bordered", "light", "faded"],
},
},
color: {
@ -32,6 +47,11 @@ export default {
options: ["xs", "sm", "md", "lg", "xl"],
},
},
showShadow: {
control: {
type: "boolean",
},
},
isDisabled: {
control: {
type: "boolean",
@ -43,6 +63,8 @@ export default {
const defaultProps = {
...pagination.defaultVariants,
total: 10,
siblings: 1,
boundaries: 1,
initialPage: 1,
};
@ -55,6 +77,55 @@ Default.args = {
...defaultProps,
};
export const WithControls = Template.bind({});
WithControls.args = {
...defaultProps,
showControls: true,
};
export const InitialPage = Template.bind({});
InitialPage.args = {
...defaultProps,
initialPage: 3,
};
export const IsEven = Template.bind({});
IsEven.args = {
...defaultProps,
isEven: true,
};
export const Controlled = () => {
const [currentPage, setCurrentPage] = React.useState(1);
return (
<div className="flex flex-col gap-5">
<p>Page: {currentPage}</p>
<Pagination
{...defaultProps}
showShadow
color="secondary"
page={currentPage}
onChange={setCurrentPage}
/>
<div className="flex gap-2">
<button
className={button({color: "secondary", size: "sm", variant: "flat"})}
onClick={() => setCurrentPage((prev) => (prev > 1 ? prev - 1 : prev))}
>
Previous
</button>
<button
className={button({color: "secondary", size: "sm", variant: "flat"})}
onClick={() => setCurrentPage((prev) => (prev < defaultProps.total ? prev + 1 : prev))}
>
Next
</button>
</div>
</div>
);
};
// import {Grid} from "@nextui-org/grid";
// import {Pagination} from "../src";

View File

@ -53,7 +53,7 @@
"lodash.foreach": "^4.5.0",
"lodash.get": "^4.4.2",
"lodash.isempty": "^4.4.0",
"tailwind-variants": "^0.0.31",
"tailwind-variants": "^0.1.0",
"tailwindcss": "^3.2.7"
},
"peerDependencies": {

View File

@ -1,106 +1,95 @@
import {tv, type VariantProps} from "tailwind-variants";
import {ringClasses} from "../utils";
import {colorVariants, ringClasses} from "../utils";
/**
* Pagination wrapper **Tailwind Variants** component
*
* const {base, item, cursor} = pagination({...})
* const {base,cursor, prev, next, item } = pagination({...})
*
* @example
* <ul className={base()} aria-label="pagination navigation">
* <li className={cursor()} aria-hidden="true"/> // this marks the active page
* <li role="button" className={item()} aria-label="Go to previous page" data-disabled="true">Prev</li>
* <li className={cursor()} aria-hidden="true">{active page}</li> // this marks the active page
* <li role="button" className={prev()} aria-label="Go to previous page" data-disabled="true">Prev</li>
* <li role="button" className={item()} aria-label="page 1" data-active="true">1</li>
* <li role="button" className={item()} aria-label="page 2">2</li>
* <li role="button" className={item()} aria-hidden="true">...</li>
* <li role="button" className={item()} aria-label="page 10">10</li>
* <li role="button" className={item()} aria-label="Go to next page">Next</li>
* <li role="button" className={next()} aria-label="Go to next page">Next</li>
* </ul>
*/
const pagination = tv({
slots: {
base: "flex gap-1",
item: [
base: "flex flex-wrap relative gap-1 max-w-fit",
item: "",
prev: "",
next: "",
cursor: [
"absolute",
"flex",
"overflow-visible",
"items-center",
"justify-center",
"bg-neutral-100",
"hover:bg-neutral-200",
"text-neutral-contrastText",
"origin-center",
"left-0",
],
cursor: "",
},
variants: {
variant: {
solid: {},
bordered: {
item: "border-1.5 !bg-transparent",
item: ["border-1.5", "border-neutral", "bg-transparent", "hover:bg-neutral-100"],
},
light: {
item: "!bg-transparent",
item: "bg-transparent",
},
flat: {},
faded: {
item: "border-1.5",
},
shadow: {},
},
color: {
neutral: {},
primary: {},
secondary: {},
success: {},
warning: {},
danger: {},
neutral: {
cursor: colorVariants.solid.neutral,
},
primary: {
cursor: colorVariants.solid.primary,
},
secondary: {
cursor: colorVariants.solid.secondary,
},
success: {
cursor: colorVariants.solid.success,
},
warning: {
cursor: colorVariants.solid.warning,
},
danger: {
cursor: colorVariants.solid.danger,
},
},
size: {
xs: {
item: "w-7 h-7 text-xs",
},
sm: {
item: "w-8 h-8 text-sm",
},
md: {
item: "w-9 h-9 text-sm",
},
lg: {
item: "w-10 h-10 text-base",
},
xl: {
item: "w-11 h-11 text-lg",
},
xs: {},
sm: {},
md: {},
lg: {},
xl: {},
},
radius: {
none: {
item: "rounded-none",
},
base: {
item: "rounded",
},
sm: {
item: "rounded-sm",
},
md: {
item: "rounded-md",
},
lg: {
item: "rounded-lg",
},
xl: {
item: "rounded-xl",
},
full: {
item: "rounded-full",
},
none: {},
base: {},
sm: {},
md: {},
lg: {},
xl: {},
full: {},
},
isEven: {
true: {
base: "gap-0",
item: [
"first:rounded-r-none",
"last:rounded-l-none",
"[&:not(:first-child):not(:last-child)]:rounded-none",
"first-of-type:rounded-r-none",
"last-of-type:rounded-l-none",
"[&:not(:first-of-type):not(:last-of-type)]:rounded-none",
],
},
},
@ -114,22 +103,289 @@ const pagination = tv({
wrapper: [...ringClasses],
},
},
showShadow: {
true: {},
},
disableAnimation: {
true: {},
false: {
item: "transition-background",
cursor: ["transition-transform", "!duration-300"],
},
},
disableCursor: {
true: {
cursor: "hidden",
},
},
},
defaultVariants: {
variant: "solid",
variant: "flat",
color: "primary",
size: "md",
radius: "xl",
isEven: false,
isDisabled: false,
showShadow: false,
disableAnimation: false,
disableCursor: false,
},
compoundVariants: [
// showShadow / color
{
showShadow: true,
color: "neutral",
class: {
cursor: colorVariants.shadow.neutral,
},
},
{
showShadow: true,
color: "primary",
class: {
cursor: colorVariants.shadow.primary,
},
},
{
showShadow: true,
color: "secondary",
class: {
cursor: colorVariants.shadow.secondary,
},
},
{
showShadow: true,
color: "success",
class: {
cursor: colorVariants.shadow.success,
},
},
{
showShadow: true,
color: "warning",
class: {
cursor: colorVariants.shadow.warning,
},
},
{
showShadow: true,
color: "danger",
class: {
cursor: colorVariants.shadow.danger,
},
},
// isEven / bordered
{
isEven: true,
variant: "bordered",
class: {
item: "[&:not(:first-of-type)]:border-l-0",
},
},
/**
* --------------------------------------------------------
* disableCursor
* the styles will be applied to the active item
* --------------------------------------------------------
*/
// disableCursor / color
{
disableCursor: true,
color: "neutral",
class: {
item: [
"data-[active=true]:bg-neutral-400",
"data-[active=true]:border-neutral-400",
"data-[active=true]:text-neutral-contrastText",
],
},
},
{
disableCursor: true,
color: "primary",
class: {
item: [
"data-[active=true]:bg-primary",
"data-[active=true]:border-primary",
"data-[active=true]:text-primary-contrastText",
],
},
},
{
disableCursor: true,
color: "secondary",
class: {
item: [
"data-[active=true]:bg-secondary",
"data-[active=true]:border-secondary",
"data-[active=true]:text-secondary-contrastText",
],
},
},
{
disableCursor: true,
color: "success",
class: {
item: [
"data-[active=true]:bg-success",
"data-[active=true]:border-success",
"data-[active=true]:text-success-contrastText",
],
},
},
{
disableCursor: true,
color: "warning",
class: {
item: [
"data-[active=true]:bg-warning",
"data-[active=true]:border-warning",
"data-[active=true]:text-warning-contrastText",
],
},
},
{
disableCursor: true,
color: "danger",
class: {
item: [
"data-[active=true]:bg-danger",
"data-[active=true]:border-danger",
"data-[active=true]:text-danger-contrastText",
],
},
},
// shadow / color
{
disableCursor: true,
showShadow: true,
color: "neutral",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-neutral/50"],
},
},
{
disableCursor: true,
showShadow: true,
color: "primary",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-primary/40"],
},
},
{
disableCursor: true,
showShadow: true,
color: "secondary",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-secondary/40"],
},
},
{
disableCursor: true,
showShadow: true,
color: "success",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-success/40"],
},
},
{
disableCursor: true,
showShadow: true,
color: "warning",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-warning/40"],
},
},
{
disableCursor: true,
showShadow: true,
color: "danger",
class: {
item: ["data-[active=true]:shadow-lg", "data-[active=true]:shadow-danger/40"],
},
},
],
compoundSlots: [
// without variant
{
slots: ["item", "prev", "next"],
class: [
"flex",
"flex-wrap",
"truncate",
"box-border",
"outline-none",
"items-center",
"justify-center",
"bg-neutral-100",
"hover:bg-neutral-200",
"active:bg-neutral-300",
"text-neutral-contrastText",
],
},
// size
{
slots: ["item", "cursor", "prev", "next"],
size: "xs",
class: "w-7 h-7 text-xs",
},
{
slots: ["item", "cursor", "prev", "next"],
size: "sm",
class: "w-8 h-8 text-sm",
},
{
slots: ["item", "cursor", "prev", "next"],
size: "md",
class: "w-9 h-9 text-sm",
},
{
slots: ["item", "cursor", "prev", "next"],
size: "lg",
class: "w-10 h-10 text-base",
},
{
slots: ["item", "cursor", "prev", "next"],
size: "xl",
class: "w-11 h-11 text-base",
},
// radius
{
slots: ["item", "cursor", "prev", "next"],
radius: "none",
class: "rounded-none",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "base",
class: "rounded-base",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "sm",
class: "rounded-sm",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "md",
class: "rounded",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "lg",
class: "rounded-lg",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "xl",
class: "rounded-xl",
},
{
slots: ["item", "cursor", "prev", "next"],
radius: "full",
class: "rounded-full",
},
],
});
export type PaginationVariantProps = VariantProps<typeof pagination>;

View File

@ -14,6 +14,7 @@ import {semanticColors, commonColors} from "./colors";
import {animations} from "./animations";
import {utilities} from "./utilities";
import {removeDefaultKeys} from "./utils/object";
import {baseStyles} from "./utils/styles";
interface MaybeNested<K extends keyof any = string, V = string> {
[key: string]: V | MaybeNested<K, V>;
@ -141,7 +142,13 @@ const corePlugin = (config: ConfigObject | ConfigFunction = {}) => {
const resolved = resolveConfig(config);
return plugin(
({addUtilities, addVariant}) => {
({addBase, addUtilities, addVariant}) => {
// add base styles
addBase({
[":root, [data-theme]"]: {
...baseStyles,
},
});
// add the css variables to "@layer utilities"
addUtilities({...resolved.utilities, ...utilities});
// add the theme as variant e.g. "theme-[name]:text-2xl"

View File

@ -1,3 +1,12 @@
/**
* This is the base styles for all elements.
* Is meant to be used with the `addBase` method from tailwindcss.
*/
export const baseStyles = {
color: "hsl(var(--nextui-foreground))",
backgroundColor: "hsl(var(--nextui-background))",
};
/**
* focus styles when the element is focused by keyboard.
*/

View File

@ -1,6 +1,12 @@
import {useMemo, useCallback, useState, useEffect} from "react";
import {range} from "@nextui-org/shared-utils";
export enum PaginationItemType {
DOTS = "dots",
PREV = "prev",
NEXT = "next",
}
export interface UsePaginationProps {
/**
* The total number of pages.
@ -25,17 +31,29 @@ export interface UsePaginationProps {
* @default 1
*/
boundaries?: number;
/**
* If `true`, the range will include "prev" and "next" buttons.
* @default false
*/
showControls?: boolean;
/**
* Callback fired when the page changes.
*/
onChange?: (page: number) => void;
}
export const DOTS = "dots";
export type PaginationItemParam = number | typeof DOTS;
export type PaginationItemValue = number | PaginationItemType;
export function usePagination(props: UsePaginationProps) {
const {page, total, siblings = 1, boundaries = 1, initialPage = 1, onChange} = props;
const {
page,
total,
siblings = 1,
boundaries = 1,
initialPage = 1,
showControls = false,
onChange,
} = props;
const [activePage, setActivePage] = useState(page || initialPage);
const onChangeActivePage = (newPage: number) => {
@ -67,11 +85,22 @@ export function usePagination(props: UsePaginationProps) {
const first = () => setPage(1);
const last = () => setPage(total);
const paginationRange = useMemo((): PaginationItemParam[] => {
const formatRange = useCallback(
(range: PaginationItemValue[]) => {
if (showControls) {
return [PaginationItemType.PREV, ...range, PaginationItemType.NEXT];
}
return range;
},
[showControls],
);
const paginationRange = useMemo((): PaginationItemValue[] => {
const totalPageNumbers = siblings * 2 + 3 + boundaries * 2;
if (totalPageNumbers >= total) {
return range(1, total);
return formatRange(range(1, total));
}
const leftSiblingIndex = Math.max(activePage - siblings, boundaries);
const rightSiblingIndex = Math.min(activePage + siblings, total - boundaries);
@ -87,27 +116,35 @@ export function usePagination(props: UsePaginationProps) {
if (!shouldShowLeftDots && shouldShowRightDots) {
const leftItemCount = siblings * 2 + boundaries + 2;
return [...range(1, leftItemCount), DOTS, ...range(total - (boundaries - 1), total)];
return formatRange([
...range(1, leftItemCount),
PaginationItemType.DOTS,
...range(total - (boundaries - 1), total),
]);
}
if (shouldShowLeftDots && !shouldShowRightDots) {
const rightItemCount = boundaries + 1 + 2 * siblings;
return [...range(1, boundaries), DOTS, ...range(total - rightItemCount, total)];
return formatRange([
...range(1, boundaries),
PaginationItemType.DOTS,
...range(total - rightItemCount, total),
]);
}
return [
return formatRange([
...range(1, boundaries),
DOTS,
PaginationItemType.DOTS,
...range(leftSiblingIndex, rightSiblingIndex),
DOTS,
PaginationItemType.DOTS,
...range(total - boundaries + 1, total),
];
}, [total, siblings, activePage]);
]);
}, [total, activePage, siblings, boundaries, formatRange]);
return {
range: paginationRange,
active: activePage,
activePage,
setPage,
next,
previous,

View File

@ -13,7 +13,6 @@ export const decorators = [
];
export const parameters = {
viewMode: 'docs',
actions: {argTypesRegex: "^on[A-Z].*"},
controls: {
matchers: {

View File

@ -33,9 +33,6 @@
"prepack": "clean-package",
"postpack": "clean-package restore"
},
"dependencies": {
"deepmerge": "4.3.0"
},
"peerDependencies": {
"react": ">=18"
},

View File

@ -1,5 +1,3 @@
export {default as deepMerge} from "deepmerge";
export * from "./assertion";
export * from "./refs";
export * from "./clsx";
@ -13,3 +11,4 @@ export * from "./context";
export * from "./numbers";
export * from "./console";
export * from "./react";
export * from "./types";

View File

@ -0,0 +1 @@
export type Timer = ReturnType<typeof setTimeout>;

430
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff