shadimage

Pagination

A pagination component with page navigation controls

registry/preskok/ui/preskok-ui/pagination.tsx

"use client"

import { twMerge } from "cn"

import {
  buttonStyles,
  type ButtonProps,
} from "@/components/ui/preskok-ui/button"
import { Link, type LinkProps } from "@/components/ui/preskok-ui/link"

const Pagination = ({
  className,
  ref,
  ...props
}: React.ComponentProps<"nav">) => (
  <nav
    data-slot="pagination"
    aria-label="pagination"
    className={twMerge(
      "mx-auto flex w-full items-center justify-center gap-(--pagination-gap) [--pagination-gap:--spacing(2)] [--section-radius:calc(var(--radius-lg)-1px)] **:data-[slot=control]:w-auto",
      "**:data-[slot=pagination-item]:cursor-default",
      className
    )}
    ref={ref}
    {...props}
  />
)

const PaginationSection = ({
  className,
  ref,
  ...props
}: React.ComponentProps<"ul">) => (
  <li data-slot="pagination-section" className="list-none">
    <ul
      ref={ref}
      className={twMerge(
        "flex h-full list-none gap-1.5 p-0 text-sm/6",
        className
      )}
      {...props}
    />
  </li>
)

const PaginationList = ({
  className,
  ref,
  ...props
}: React.ComponentProps<"ul">) => {
  return (
    <ul
      ref={ref}
      data-slot="pagination-list"
      aria-label={props["aria-label"] || "Pagination"}
      className={twMerge("flex list-none gap-[5px] p-0", className)}
      {...props}
    />
  )
}

interface PaginationItemProps
  extends
    Omit<LinkProps, "children">,
    Pick<ButtonProps, "isCircle" | "size" | "intent"> {
  className?: string
  isCurrent?: boolean
  children?: string | number
}

const PaginationItem = ({
  className,
  size = "sm",
  isCircle,
  isCurrent,
  ...props
}: PaginationItemProps) => {
  return (
    <li className="list-none">
      <Link
        data-slot="pagination-item"
        href={isCurrent ? undefined : props.href}
        aria-current={isCurrent ? "page" : undefined}
        className={buttonStyles({
          size: size,
          isCircle: isCircle,
          intent: isCurrent ? "outline" : "plain",
          className: twMerge("touch-target min-w-9 shrink-0", className),
        })}
        {...props}
      />
    </li>
  )
}

interface PaginationAttributesProps
  extends
    Omit<LinkProps, "className">,
    Pick<ButtonProps, "size" | "isCircle" | "intent"> {
  className?: string
}

const PaginationFirst = ({
  className,
  children,
  size = "sq-sm",
  intent = "outline",
  isCircle,
  ...props
}: PaginationAttributesProps) => {
  return (
    <li className="list-none">
      <Link
        data-slot="pagination-item"
        aria-label="First page"
        className={buttonStyles({
          size: children ? "sm" : size,
          isCircle,
          intent,
          className: twMerge("shrink-0", className),
        })}
        {...props}
      >
        <>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width={16}
            height={16}
            fill="none"
            viewBox="0 0 25 24"
            data-slot="icon"
            aria-hidden="true"
          >
            <path
              stroke="currentColor"
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="1.5"
              d="m17.5 18-6-6 6-6m-10 0v12"
            />
          </svg>
          {children}
        </>
      </Link>
    </li>
  )
}
const PaginationPrevious = ({
  className,
  children,
  size = "sq-sm",
  intent = "outline",
  isCircle = false,
  ...props
}: PaginationAttributesProps) => {
  return (
    <li className="list-none">
      <Link
        data-slot="pagination-item"
        aria-label="Previous page"
        className={buttonStyles({
          size: children ? "sm" : size,
          isCircle,
          intent,
          className: twMerge("shrink-0", className),
        })}
        {...props}
      >
        <>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20"
            fill="currentColor"
            aria-hidden="true"
            data-slot="icon"
          >
            <path
              fillRule="evenodd"
              d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z"
              clipRule="evenodd"
            />
          </svg>
          {children}
        </>
      </Link>
    </li>
  )
}
const PaginationNext = ({
  className,
  children,
  size = "sq-sm",
  intent = "outline",
  isCircle = false,
  ...props
}: PaginationAttributesProps) => {
  return (
    <li className="list-none">
      <Link
        data-slot="pagination-item"
        aria-label="Next page"
        className={buttonStyles({
          size: children ? "sm" : size,
          isCircle,
          intent,
          className: twMerge("shrink-0", className),
        })}
        {...props}
      >
        <>
          {children}
          <svg
            xmlns="http://www.w3.org/2000/svg"
            viewBox="0 0 20 20"
            fill="currentColor"
            aria-hidden="true"
            data-slot="icon"
          >
            <path
              fillRule="evenodd"
              d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 0 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z"
              clipRule="evenodd"
            />
          </svg>
        </>
      </Link>
    </li>
  )
}
const PaginationLast = ({
  className,
  children,
  size = "sq-sm",
  intent = "outline",
  isCircle = false,
  ...props
}: PaginationAttributesProps) => {
  return (
    <li className="list-none">
      <Link
        data-slot="pagination-item"
        aria-label="Last page"
        className={buttonStyles({
          size: children ? "sm" : size,
          isCircle,
          intent,
          className: twMerge("shrink-0", className),
        })}
        {...props}
      >
        <>
          {children}
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width={16}
            height={16}
            fill="none"
            viewBox="0 0 25 24"
            className="size-4"
            data-slot="icon"
            aria-hidden="true"
          >
            <path
              stroke="currentColor"
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="1.5"
              d="m7.5 18 6-6-6-6m10 0v12"
            />
          </svg>
        </>
      </Link>
    </li>
  )
}

const PaginationSpacer = ({
  className,
  ref,
  ...props
}: React.ComponentProps<"div">) => {
  return (
    <div
      aria-hidden
      ref={ref}
      className={twMerge("flex-1", className)}
      {...props}
    />
  )
}

const PaginationGap = ({
  className,
  children = <>&hellip;</>,
  ...props
}: React.ComponentProps<"li">) => {
  return (
    <li
      data-slot="pagination-gap"
      className={twMerge(
        "text-foreground w-9 list-none text-center text-sm/6 font-semibold outline-hidden select-none",
        className
      )}
      {...props}
      aria-hidden
    >
      {children}
    </li>
  )
}

const PaginationLabel = ({
  className,
  ref,
  ...props
}: React.ComponentProps<"li">) => {
  return (
    <li
      ref={ref}
      data-slot="pagination-label"
      className={twMerge(
        "text-foreground *:[strong]:text-foreground min-w-4 list-none self-center *:[strong]:font-medium",
        className
      )}
      {...props}
    />
  )
}

const PaginationInfo = ({ className, ...props }: React.ComponentProps<"p">) => {
  return (
    <p
      className={twMerge(
        "text-muted-foreground *:[strong]:text-foreground text-sm/6 *:[strong]:font-medium",
        className
      )}
      {...props}
    />
  )
}

export {
  Pagination,
  PaginationFirst,
  PaginationGap,
  PaginationInfo,
  PaginationItem,
  PaginationLabel,
  PaginationLast,
  PaginationList,
  PaginationNext,
  PaginationPrevious,
  PaginationSection,
  PaginationSpacer,
}

No selection tags yet — empty means no evidence, not lack of support.

DependenciesBundle weight · light

cn@^0.2.4