diff --git a/packages/propel/src/components/pagination/pagination.stories.tsx b/packages/propel/src/components/pagination/pagination.stories.tsx
index 761bb29c..a844039c 100644
--- a/packages/propel/src/components/pagination/pagination.stories.tsx
+++ b/packages/propel/src/components/pagination/pagination.stories.tsx
@@ -2,11 +2,39 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import * as React from "react";
import { expect, fn, userEvent, waitFor, within } from "storybook/test";
+import {
+ PaginationArrowButton,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationList,
+ PaginationPageButton,
+ PaginationPerPage,
+ PaginationPerPageIndicator,
+ PaginationPerPageLabel,
+ PaginationPerPageTrigger,
+ PaginationRange,
+ PaginationRangeCurrent,
+ PaginationSpinner,
+} from "../../ui/pagination/index";
import { Pagination } from "./index";
const meta = {
title: "Components/Pagination",
component: Pagination,
+ subcomponents: {
+ PaginationList,
+ PaginationItem,
+ PaginationPageButton,
+ PaginationArrowButton,
+ PaginationEllipsis,
+ PaginationSpinner,
+ PaginationPerPage,
+ PaginationPerPageTrigger,
+ PaginationPerPageIndicator,
+ PaginationPerPageLabel,
+ PaginationRange,
+ PaginationRangeCurrent,
+ },
parameters: {
design: {
type: "figma",
diff --git a/packages/propel/src/components/pagination/pagination.tsx b/packages/propel/src/components/pagination/pagination.tsx
index 013845bd..3c03d3f4 100644
--- a/packages/propel/src/components/pagination/pagination.tsx
+++ b/packages/propel/src/components/pagination/pagination.tsx
@@ -1,12 +1,21 @@
-import { ArrowLeft, ArrowRight, ChevronDown, LoaderCircle } from "lucide-react";
+import { ArrowLeft, ArrowRight, ChevronDown, LoaderCircle, MoreHorizontal } from "lucide-react";
import * as React from "react";
import { Menu, MenuTrigger } from "../../ui/menu/index";
import {
+ Pagination as PaginationRoot,
PaginationArrowButton,
PaginationEllipsis,
+ PaginationItem,
+ PaginationList,
PaginationPageButton,
+ PaginationPerPage,
+ PaginationPerPageIndicator,
+ PaginationPerPageLabel,
PaginationPerPageTrigger,
+ PaginationRange,
+ PaginationRangeCurrent,
+ PaginationSpinner,
} from "../../ui/pagination/index";
import { MenuContent, MenuItem } from "../menu/index";
@@ -16,17 +25,8 @@ import { MenuContent, MenuItem } from "../menu/index";
// at render time from `page`/`pageCount` — never a prop. What designers can toggle is
// genuinely additive: an optional per-page selector and an optional range label.
//
-// Tokens (Figma node 4762-503):
-// - page-number button: 24px square, radius/sm (4px), text/13, transparent bg that
-// fills to `layer-transparent-hover` on hover and `layer-transparent-active` when
-// it is the current page; disabled/loading dim to the placeholder/disabled colors.
-// - prev/next: 24px square icon buttons, radius/md (6px), 16px arrows. The arrows are
-// directional, so they mirror in RTL via `rtl:-scale-x-100`.
-// - ellipsis: a non-interactive 24px slot holding a 14px more-horizontal glyph.
-// - per-page selector: a `layer-3` pill, 24px tall, radius/md, "50" + chevron-down,
-// followed by "per page" tertiary text. The pill is the trigger for a propel
-// Menu (single-select) whose menu lists the page-size options; picking one
-// reports it through `pageSize.onValueChange`.
+// This components tier only COMPOSES the `ui/pagination` parts (each a single styled
+// element); all chrome lives in their cva — there is no className/cva/cx here.
// Builds the sequence of visible page tokens. Always shows the first and last page;
// shows up to one neighbour either side of the current page; inserts an ellipsis
@@ -89,7 +89,7 @@ export type PaginationLabels = {
/** Visible text on the per-page selector trigger, given the size. Defaults to `"50"`. */
perPageValue: (pageSize: number) => React.ReactNode;
/** Trailing text after the per-page selector. Defaults to `"per page"`. */
- perPage: React.ReactNode;
+ perPage: string;
};
const DEFAULT_LABELS: PaginationLabels = {
@@ -165,22 +165,25 @@ export function Pagination({
const atEnd = page >= pageCount;
return (
-
+
+
+
);
}
diff --git a/packages/propel/src/ui/pagination/index.tsx b/packages/propel/src/ui/pagination/index.tsx
index 724a07b7..13369b6e 100644
--- a/packages/propel/src/ui/pagination/index.tsx
+++ b/packages/propel/src/ui/pagination/index.tsx
@@ -1,13 +1,40 @@
+export { Pagination, type PaginationProps } from "./pagination";
export { PaginationArrowButton, type PaginationArrowButtonProps } from "./pagination-arrow-button";
-export { PaginationEllipsis } from "./pagination-ellipsis";
+export { PaginationEllipsis, type PaginationEllipsisProps } from "./pagination-ellipsis";
+export { PaginationItem, type PaginationItemProps } from "./pagination-item";
+export { PaginationList, type PaginationListProps } from "./pagination-list";
export { PaginationPageButton, type PaginationPageButtonProps } from "./pagination-page-button";
+export { PaginationPerPage, type PaginationPerPageProps } from "./pagination-per-page";
+export {
+ PaginationPerPageIndicator,
+ type PaginationPerPageIndicatorProps,
+} from "./pagination-per-page-indicator";
+export {
+ PaginationPerPageLabel,
+ type PaginationPerPageLabelProps,
+} from "./pagination-per-page-label";
export {
PaginationPerPageTrigger,
type PaginationPerPageTriggerProps,
} from "./pagination-per-page-trigger";
+export { PaginationRange, type PaginationRangeProps } from "./pagination-range";
+export {
+ PaginationRangeCurrent,
+ type PaginationRangeCurrentProps,
+} from "./pagination-range-current";
+export { PaginationSpinner, type PaginationSpinnerProps } from "./pagination-spinner";
export {
- arrowButtonVariants,
- pageButtonVariants,
- perPageTriggerVariants,
- slotBase,
+ paginationArrowButtonVariants,
+ paginationEllipsisVariants,
+ paginationItemVariants,
+ paginationListVariants,
+ paginationPageButtonVariants,
+ paginationPerPageIndicatorVariants,
+ paginationPerPageLabelVariants,
+ paginationPerPageTriggerVariants,
+ paginationPerPageVariants,
+ paginationRangeCurrentVariants,
+ paginationRangeVariants,
+ paginationSpinnerVariants,
+ paginationVariants,
} from "./variants";
diff --git a/packages/propel/src/ui/pagination/pagination-arrow-button.tsx b/packages/propel/src/ui/pagination/pagination-arrow-button.tsx
index 9f7fbd16..918922ea 100644
--- a/packages/propel/src/ui/pagination/pagination-arrow-button.tsx
+++ b/packages/propel/src/ui/pagination/pagination-arrow-button.tsx
@@ -1,6 +1,6 @@
import type * as React from "react";
-import { arrowButtonVariants } from "./variants";
+import { paginationArrowButtonVariants } from "./variants";
export type PaginationArrowButtonProps = Omit<
React.ComponentProps<"button">,
@@ -8,9 +8,10 @@ export type PaginationArrowButtonProps = Omit<
>;
/**
- * A styled prev/next arrow button. Applies `arrowButtonVariants()`; pass the (directional,
- * RTL-mirrored) arrow icon as `children` and wire `aria-label`/`disabled`/`onClick` through props.
+ * A styled prev/next arrow button. Applies `paginationArrowButtonVariants()`; pass the
+ * (directional, RTL-mirrored) arrow icon as `children` and wire `aria-label`/`disabled`/`onClick`
+ * through props.
*/
export function PaginationArrowButton(props: PaginationArrowButtonProps) {
- return ;
+ return ;
}
diff --git a/packages/propel/src/ui/pagination/pagination-ellipsis.tsx b/packages/propel/src/ui/pagination/pagination-ellipsis.tsx
index 2af318f6..414c31fb 100644
--- a/packages/propel/src/ui/pagination/pagination-ellipsis.tsx
+++ b/packages/propel/src/ui/pagination/pagination-ellipsis.tsx
@@ -1,13 +1,17 @@
-import { cx } from "class-variance-authority";
-import { MoreHorizontal } from "lucide-react";
+import type * as React from "react";
-import { slotBase } from "./variants";
+import { paginationEllipsisVariants } from "./variants";
-/** A non-interactive gap marker between distant page numbers. */
-export function PaginationEllipsis() {
- return (
-
-
-
- );
+export type PaginationEllipsisProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * A non-interactive gap marker between distant page numbers. Renders whatever icon you pass (sized
+ * to the slot's `--node-size`, 14px) — never baking a specific glyph in. Decorative, so
+ * `aria-hidden`.
+ */
+export function PaginationEllipsis(props: PaginationEllipsisProps) {
+ return ;
}
diff --git a/packages/propel/src/ui/pagination/pagination-item.tsx b/packages/propel/src/ui/pagination/pagination-item.tsx
new file mode 100644
index 00000000..3a7345d8
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-item.tsx
@@ -0,0 +1,10 @@
+import type * as React from "react";
+
+import { paginationItemVariants } from "./variants";
+
+export type PaginationItemProps = Omit, "className" | "style">;
+
+/** One slot in the pagination list, holding a page button, an arrow button, or the ellipsis. */
+export function PaginationItem(props: PaginationItemProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-list.tsx b/packages/propel/src/ui/pagination/pagination-list.tsx
new file mode 100644
index 00000000..d23668ba
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-list.tsx
@@ -0,0 +1,13 @@
+import type * as React from "react";
+
+import { paginationListVariants } from "./variants";
+
+export type PaginationListProps = Omit, "className" | "style">;
+
+/**
+ * The ordered list of page controls: the previous button, page numbers and ellipses, and the next
+ * button.
+ */
+export function PaginationList(props: PaginationListProps) {
+ return
;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-page-button.tsx b/packages/propel/src/ui/pagination/pagination-page-button.tsx
index cc5d819e..f9136e4c 100644
--- a/packages/propel/src/ui/pagination/pagination-page-button.tsx
+++ b/packages/propel/src/ui/pagination/pagination-page-button.tsx
@@ -1,6 +1,6 @@
import type * as React from "react";
-import { pageButtonVariants } from "./variants";
+import { paginationPageButtonVariants } from "./variants";
export type PaginationPageButtonProps = Omit<
React.ComponentProps<"button">,
@@ -11,9 +11,9 @@ export type PaginationPageButtonProps = Omit<
};
/**
- * A styled page-number button. Applies `pageButtonVariants({ current })`; pass the page number (or
- * a loading spinner) as `children` and wire `aria-current`/`onClick` through props.
+ * A styled page-number button. Applies `paginationPageButtonVariants({ current })`; pass the page
+ * number (or a loading spinner) as `children` and wire `aria-current`/`onClick` through props.
*/
export function PaginationPageButton({ current, ...props }: PaginationPageButtonProps) {
- return ;
+ return ;
}
diff --git a/packages/propel/src/ui/pagination/pagination-per-page-indicator.tsx b/packages/propel/src/ui/pagination/pagination-per-page-indicator.tsx
new file mode 100644
index 00000000..2765a367
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-per-page-indicator.tsx
@@ -0,0 +1,17 @@
+import type * as React from "react";
+
+import { paginationPerPageIndicatorVariants } from "./variants";
+
+export type PaginationPerPageIndicatorProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * The chevron inside the per-page trigger. Renders whatever icon you pass (sized to `--node-size`,
+ * 14px) and rotates a half-turn while the menu is open. Decorative — the trigger carries the
+ * accessible name and state — so `aria-hidden`.
+ */
+export function PaginationPerPageIndicator(props: PaginationPerPageIndicatorProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-per-page-label.tsx b/packages/propel/src/ui/pagination/pagination-per-page-label.tsx
new file mode 100644
index 00000000..1bb262db
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-per-page-label.tsx
@@ -0,0 +1,16 @@
+import type * as React from "react";
+
+import { paginationPerPageLabelVariants } from "./variants";
+
+export type PaginationPerPageLabelProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * The trailing "per page" text after the selector pill (Figma `50 v per page`). Decorative, so
+ * `aria-hidden`.
+ */
+export function PaginationPerPageLabel(props: PaginationPerPageLabelProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-per-page-trigger.tsx b/packages/propel/src/ui/pagination/pagination-per-page-trigger.tsx
index f313a4b9..0bd536c0 100644
--- a/packages/propel/src/ui/pagination/pagination-per-page-trigger.tsx
+++ b/packages/propel/src/ui/pagination/pagination-per-page-trigger.tsx
@@ -1,7 +1,7 @@
import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
-import { perPageTriggerVariants } from "./variants";
+import { paginationPerPageTriggerVariants } from "./variants";
export type PaginationPerPageTriggerProps = Omit<
useRender.ComponentProps<"button">,
@@ -10,12 +10,13 @@ export type PaginationPerPageTriggerProps = Omit<
/**
* The per-page selector pill, styled as a `layer-3` Menu trigger. Compose a `Menu.Trigger` through
- * the `render` prop; pass the visible size, the `sr-only` label, and the chevron as `children`.
+ * the `render` prop; pass the visible size and the `PaginationPerPageIndicator` chevron as
+ * children.
*/
export function PaginationPerPageTrigger({ render, ...props }: PaginationPerPageTriggerProps) {
const defaultProps: useRender.ElementProps<"button"> = {
...(render == null ? { type: "button" } : null),
- className: perPageTriggerVariants(),
+ className: paginationPerPageTriggerVariants(),
};
return useRender({
diff --git a/packages/propel/src/ui/pagination/pagination-per-page.tsx b/packages/propel/src/ui/pagination/pagination-per-page.tsx
new file mode 100644
index 00000000..793a9400
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-per-page.tsx
@@ -0,0 +1,13 @@
+import type * as React from "react";
+
+import { paginationPerPageVariants } from "./variants";
+
+export type PaginationPerPageProps = Omit, "className" | "style">;
+
+/**
+ * The per-page region: the selector pill (`PaginationPerPageTrigger`) followed by the trailing
+ * `PaginationPerPageLabel`.
+ */
+export function PaginationPerPage(props: PaginationPerPageProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-range-current.tsx b/packages/propel/src/ui/pagination/pagination-range-current.tsx
new file mode 100644
index 00000000..5112d0c9
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-range-current.tsx
@@ -0,0 +1,16 @@
+import type * as React from "react";
+
+import { paginationRangeCurrentVariants } from "./variants";
+
+export type PaginationRangeCurrentProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * The emphasized current-range portion (e.g. `1-50`) inside `PaginationRange`, in the primary text
+ * color.
+ */
+export function PaginationRangeCurrent(props: PaginationRangeCurrentProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-range.tsx b/packages/propel/src/ui/pagination/pagination-range.tsx
new file mode 100644
index 00000000..e87563ee
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-range.tsx
@@ -0,0 +1,13 @@
+import type * as React from "react";
+
+import { paginationRangeVariants } from "./variants";
+
+export type PaginationRangeProps = Omit, "className" | "style">;
+
+/**
+ * The optional range label shown before the controls (Figma `1-50 of 250`): tertiary, nowrap. The
+ * current range inside it is emphasized via `PaginationRangeCurrent`.
+ */
+export function PaginationRange(props: PaginationRangeProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination-spinner.tsx b/packages/propel/src/ui/pagination/pagination-spinner.tsx
new file mode 100644
index 00000000..b9e45d40
--- /dev/null
+++ b/packages/propel/src/ui/pagination/pagination-spinner.tsx
@@ -0,0 +1,17 @@
+import type * as React from "react";
+
+import { paginationSpinnerVariants } from "./variants";
+
+export type PaginationSpinnerProps = Omit<
+ React.ComponentPropsWithoutRef<"span">,
+ "className" | "style"
+>;
+
+/**
+ * The in-flight indicator a page button shows in place of its number while navigating to it. Spins
+ * whatever icon you pass, sized to `--node-size` (14px) and tinted the placeholder color.
+ * Decorative, so `aria-hidden`.
+ */
+export function PaginationSpinner(props: PaginationSpinnerProps) {
+ return ;
+}
diff --git a/packages/propel/src/ui/pagination/pagination.stories.tsx b/packages/propel/src/ui/pagination/pagination.stories.tsx
index fc2a34b6..abdf39ab 100644
--- a/packages/propel/src/ui/pagination/pagination.stories.tsx
+++ b/packages/propel/src/ui/pagination/pagination.stories.tsx
@@ -1,17 +1,31 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { ChevronLeft, ChevronRight } from "lucide-react";
+import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
import { expect } from "storybook/test";
-import { PaginationArrowButton, PaginationEllipsis, PaginationPageButton } from "./index";
+import {
+ Pagination,
+ PaginationArrowButton,
+ PaginationEllipsis,
+ PaginationItem,
+ PaginationList,
+ PaginationPageButton,
+} from "./index";
-// UI-tier story: composes the ATOMIC pagination parts (each renders a single styled control).
+// UI-tier story: composes the ATOMIC pagination parts (each renders a single element).
// The components-tier `Pagination` story owns the truncation logic, the per-page Menu and the
-// range label. Here you lay out the raw slots yourself inside a `