From d3c2f380bde978d48ab92b041e50e1e0620620ff Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 11 Sep 2026 15:50:46 -0400 Subject: [PATCH 01/16] feat(ui): add Mosaic Pagination component --- .changeset/mosaic-pagination.md | 2 + .../swingset/src/components/DocsViewer.tsx | 1 + packages/swingset/src/lib/registry.ts | 20 ++ packages/swingset/src/stories/pagination.mdx | 74 ++++++ .../src/stories/pagination.stories.tsx | 120 +++++++++ .../src/mosaic/components/pagination/index.ts | 2 + .../components/pagination/page-items.ts | 33 +++ .../pagination/pagination.styles.ts | 41 +++ .../components/pagination/pagination.test.tsx | 249 ++++++++++++++++++ .../components/pagination/pagination.tsx | 213 +++++++++++++++ packages/ui/src/mosaic/icons/registry.tsx | 28 ++ packages/ui/src/mosaic/styles/index.ts | 2 + 12 files changed, 785 insertions(+) create mode 100644 .changeset/mosaic-pagination.md create mode 100644 packages/swingset/src/stories/pagination.mdx create mode 100644 packages/swingset/src/stories/pagination.stories.tsx create mode 100644 packages/ui/src/mosaic/components/pagination/index.ts create mode 100644 packages/ui/src/mosaic/components/pagination/page-items.ts create mode 100644 packages/ui/src/mosaic/components/pagination/pagination.styles.ts create mode 100644 packages/ui/src/mosaic/components/pagination/pagination.test.tsx create mode 100644 packages/ui/src/mosaic/components/pagination/pagination.tsx diff --git a/.changeset/mosaic-pagination.md b/.changeset/mosaic-pagination.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-pagination.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/swingset/src/components/DocsViewer.tsx b/packages/swingset/src/components/DocsViewer.tsx index 784e648e294..fe2694d8be8 100644 --- a/packages/swingset/src/components/DocsViewer.tsx +++ b/packages/swingset/src/components/DocsViewer.tsx @@ -49,6 +49,7 @@ const docModules: Record> = { combobox: dynamic(() => import('../stories/combobox.mdx')), input: dynamic(() => import('../stories/input.mdx')), 'input-group': dynamic(() => import('../stories/input-group.mdx')), + pagination: dynamic(() => import('../stories/pagination.mdx')), 'phone-input': dynamic(() => import('../stories/phone-input.mdx')), item: dynamic(() => import('../stories/item.mdx')), dialog: dynamic(() => import('../stories/dialog.component.mdx')), diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index ebe2f7b9fab..cc2fe2d30a6 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -105,6 +105,15 @@ import { Success as OtpComponentSuccess, } from '../stories/otp.component.stories'; import { meta as otpMeta } from '../stories/otp.stories'; +import { + Disabled as PaginationDisabled, + FirstLast as PaginationFirstLast, + meta as paginationMeta, + Primary as PaginationPrimary, + Siblings as PaginationSiblings, + SinglePage as PaginationSinglePage, + Sizes as PaginationSizes, +} from '../stories/pagination.stories'; import { Default as PhoneInputDefault, Disabled as PhoneInputDisabled, @@ -328,6 +337,16 @@ const inputGroupModule: StoryModule = { Invalid: InputGroupInvalid, }; +const paginationModule: StoryModule = { + meta: paginationMeta, + Primary: PaginationPrimary, + Sizes: PaginationSizes, + FirstLast: PaginationFirstLast, + Siblings: PaginationSiblings, + SinglePage: PaginationSinglePage, + Disabled: PaginationDisabled, +}; + const phoneInputModule: StoryModule = { meta: phoneInputMeta, Default: PhoneInputDefault, @@ -597,6 +616,7 @@ export const registry: StoryModule[] = [ iconFrameModule, menuComponentModule, otpComponentModule, + paginationModule, popoverComponentModule, profileComponentModule, sectionModule, diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx new file mode 100644 index 00000000000..59f36d8b19f --- /dev/null +++ b/packages/swingset/src/stories/pagination.mdx @@ -0,0 +1,74 @@ +import * as PaginationStories from './pagination.stories'; + +# Pagination + +Page navigation for a paged list. Renders a labelled `nav` with previous and next controls, the page numbers around the current one, and a results-per-page control. Pages are 1-based and the component is controlled: `onChange` receives the page the user asked for. The results-per-page control is a placeholder trigger until the Mosaic `Select` lands. + +## Playground + + + +## Props + + void', description: 'Called with the page the user navigated to.' }, + { name: 'step', type: 'number', default: '1', description: 'How many pages the previous and next controls move.' }, + { name: 'siblingCount', type: 'number', default: '1', description: 'Pages shown on each side of the current one.' }, + { name: 'label', type: 'string', default: "'Pagination'", description: 'Accessible name of the `nav`.' }, + ]} +/> + +## Usage + + + +--- + +## Examples + +### Sizes + + + +### First and last + + + +### Sibling count + + + +### Single page + + + +### Disabled + + diff --git a/packages/swingset/src/stories/pagination.stories.tsx b/packages/swingset/src/stories/pagination.stories.tsx new file mode 100644 index 00000000000..0d2a5fa2802 --- /dev/null +++ b/packages/swingset/src/stories/pagination.stories.tsx @@ -0,0 +1,120 @@ +import type { PaginationProps } from '@clerk/ui/mosaic/components/pagination'; +import { Pagination } from '@clerk/ui/mosaic/components/pagination'; +import { useState } from 'react'; + +import type { StoryMeta } from '@/lib/types'; + +// Exposes this file's own source (via the `?raw` webpack rule) so each `` example +// renders a code footer with its function's source. See `StoryModule.__source`. +export { default as __source } from './pagination.stories?raw'; + +// StyleX has no runtime recipe to derive knobs from, so the variant surface is described +// here to drive the playground + prop table. Keys mirror `PaginationProps`. +export const meta: StoryMeta = { + group: 'Components', + status: 'wip', + substatus: 'needs design', + title: 'Pagination', + source: 'packages/ui/src/mosaic/components/pagination/pagination.tsx', + styles: { + _variants: { + size: { sm: {}, md: {}, lg: {} }, + hasFirstLast: { true: {}, false: {} }, + disabled: { true: {}, false: {} }, + }, + _defaultVariants: { + size: 'md', + hasFirstLast: false, + disabled: false, + }, + }, +}; + +// Story functions accept Record (knob values) and cast to PaginationProps. +// The cast is unavoidable: knobs are dynamically typed; Pagination has a strict prop interface. +function knobsAsProps(props: Record) { + return props as unknown as Partial; +} + +export function Primary(props: Record) { + const [page, setPage] = useState(1); + return ( + + ); +} + +export function Sizes(props: Record) { + const [page, setPage] = useState(3); + return ( +
+ {(['sm', 'md', 'lg'] as const).map(size => ( + + ))} +
+ ); +} + +export function FirstLast(props: Record) { + const [page, setPage] = useState(1); + return ( + + ); +} + +export function Siblings(props: Record) { + const [page, setPage] = useState(10); + return ( + + ); +} + +export function SinglePage(props: Record) { + return ( + + ); +} + +export function Disabled(props: Record) { + return ( + + ); +} diff --git a/packages/ui/src/mosaic/components/pagination/index.ts b/packages/ui/src/mosaic/components/pagination/index.ts new file mode 100644 index 00000000000..afa801cd83d --- /dev/null +++ b/packages/ui/src/mosaic/components/pagination/index.ts @@ -0,0 +1,2 @@ +export { Pagination } from './pagination'; +export type { PaginationProps } from './pagination'; diff --git a/packages/ui/src/mosaic/components/pagination/page-items.ts b/packages/ui/src/mosaic/components/pagination/page-items.ts new file mode 100644 index 00000000000..b1d94647ea7 --- /dev/null +++ b/packages/ui/src/mosaic/components/pagination/page-items.ts @@ -0,0 +1,33 @@ +export type PageItem = number | 'start-ellipsis' | 'end-ellipsis'; + +function range(start: number, end: number): number[] { + const pages: number[] = []; + for (let page = start; page <= end; page++) { + pages.push(page); + } + return pages; +} + +export function getPageItems(page: number, pageCount: number, siblingCount: number): PageItem[] { + if (pageCount <= 1) { + return [1]; + } + + const windowStart = Math.max(Math.min(page - siblingCount, pageCount - 2 * siblingCount - 2), 3); + const windowEnd = Math.min(Math.max(page + siblingCount, 2 * siblingCount + 3), pageCount - 2); + + const items: PageItem[] = [1]; + if (windowStart > 3) { + items.push('start-ellipsis'); + } else if (pageCount > 3) { + items.push(2); + } + items.push(...range(windowStart, windowEnd)); + if (windowEnd < pageCount - 2) { + items.push('end-ellipsis'); + } else if (pageCount > 2) { + items.push(pageCount - 1); + } + items.push(pageCount); + return items; +} diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts new file mode 100644 index 00000000000..36b1cf1459c --- /dev/null +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -0,0 +1,41 @@ +import * as stylex from '@stylexjs/stylex'; + +import { colorVars, space } from '../../tokens.stylex'; + +export const styles = stylex.create({ + root: { + gap: space['4'], + alignItems: 'center', + display: 'flex', + flexWrap: 'wrap', + justifyContent: 'space-between', + }, + controls: { + gap: space['1'], + alignItems: 'center', + display: 'flex', + }, + page: { + color: { + default: colorVars['--cl-color-neutral-faded'], + ':where([aria-current="page"])': colorVars['--cl-color-neutral-foreground'], + }, + }, + ellipsis: { + alignItems: 'center', + color: colorVars['--cl-color-neutral-faded'], + display: 'inline-flex', + justifyContent: 'center', + }, + pageSize: { + gap: space['2'], + alignItems: 'center', + display: 'flex', + }, +}); + +export const ellipsisSizes = stylex.create({ + sm: { height: space['6'], width: space['6'] }, + md: { height: space['7'], width: space['7'] }, + lg: { height: space['8'], width: space['8'] }, +}); diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx new file mode 100644 index 00000000000..4c07fe6e200 --- /dev/null +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -0,0 +1,249 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { describe, expect, it, vi } from 'vitest'; + +import { getPageItems } from './page-items'; +import { Pagination } from './pagination'; + +function controls() { + const element = document.querySelector('.cl-pagination-controls'); + if (!(element instanceof HTMLElement)) { + throw new Error('Pagination controls did not render'); + } + return within(element); +} + +describe('getPageItems', () => { + it('shows every page when they all fit', () => { + expect(getPageItems(1, 2, 1)).toEqual([1, 2]); + expect(getPageItems(2, 3, 1)).toEqual([1, 2, 3]); + expect(getPageItems(1, 5, 1)).toEqual([1, 2, 3, 4, 5]); + expect(getPageItems(1, 7, 1)).toEqual([1, 2, 3, 4, 5, 6, 7]); + }); + + it('keeps the sequence the same length as the page moves', () => { + expect(getPageItems(1, 10, 1)).toEqual([1, 2, 3, 4, 5, 'end-ellipsis', 10]); + expect(getPageItems(10, 10, 1)).toEqual([1, 'start-ellipsis', 6, 7, 8, 9, 10]); + }); + + it('keeps the current page, its siblings, and both ends', () => { + expect(getPageItems(5, 10, 1)).toEqual([1, 'start-ellipsis', 4, 5, 6, 'end-ellipsis', 10]); + expect(getPageItems(5, 10, 2)).toEqual([1, 2, 3, 4, 5, 6, 7, 'end-ellipsis', 10]); + }); + + it('shows the page instead of an ellipsis that would hide only one', () => { + expect(getPageItems(4, 10, 1)).toEqual([1, 2, 3, 4, 5, 'end-ellipsis', 10]); + expect(getPageItems(7, 10, 1)).toEqual([1, 'start-ellipsis', 6, 7, 8, 9, 10]); + }); + + it('renders a single page', () => { + expect(getPageItems(1, 1, 1)).toEqual([1]); + }); +}); + +describe('Mosaic Pagination', () => { + it('renders a labelled nav with the styling contract', () => { + render( + , + ); + const nav = screen.getByRole('navigation', { name: 'Pagination' }); + expect(nav).toHaveClass('cl-pagination'); + expect(nav).toHaveAttribute('data-size', 'md'); + }); + + it('uses the label prop as the accessible name', () => { + render( + , + ); + expect(screen.getByRole('navigation', { name: 'Invoices' })).toBeInTheDocument(); + }); + + it('derives the page count from totalItems and pageSize', () => { + render( + , + ); + const pages = controls() + .getAllByRole('button') + .map(button => button.textContent) + .filter(Boolean); + expect(pages).toEqual(['1', '2', '3', '4', '5', '10']); + }); + + it('marks the current page', () => { + render( + , + ); + expect(screen.getByRole('button', { name: '3' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByRole('button', { name: '2' })).not.toHaveAttribute('aria-current'); + }); + + it('calls onChange with the clicked page', async () => { + const onChange = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: '2' })); + expect(onChange).toHaveBeenCalledWith(2); + }); + + it('moves by step from the previous and next buttons', async () => { + const onChange = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'Previous page' })); + expect(onChange).toHaveBeenLastCalledWith(3); + await userEvent.click(screen.getByRole('button', { name: 'Next page' })); + expect(onChange).toHaveBeenLastCalledWith(7); + }); + + it('clamps a step that would overshoot the ends', async () => { + const onChange = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'Next page' })); + expect(onChange).toHaveBeenLastCalledWith(10); + }); + + it('disables previous on the first page and next on the last', () => { + const { rerender } = render( + , + ); + expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Next page' })).toBeEnabled(); + + rerender( + , + ); + expect(screen.getByRole('button', { name: 'Previous page' })).toBeEnabled(); + expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); + }); + + it('renders first and last buttons only when asked', async () => { + const onChange = vi.fn(); + const { rerender } = render( + , + ); + expect(screen.queryByRole('button', { name: 'First page' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Last page' })).not.toBeInTheDocument(); + + rerender( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'First page' })); + expect(onChange).toHaveBeenLastCalledWith(1); + await userEvent.click(screen.getByRole('button', { name: 'Last page' })); + expect(onChange).toHaveBeenLastCalledWith(10); + }); + + it('disables every control when disabled', () => { + render( + , + ); + expect(screen.getByRole('navigation')).toHaveAttribute('data-disabled', ''); + for (const button of screen.getAllByRole('button')) { + expect(button).toBeDisabled(); + } + }); + + it('shows the page size in the results-per-page control', () => { + render( + , + ); + expect(screen.getByText('Results per page')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '25' })).toBeInTheDocument(); + }); + + it('renders one page when there are no items', () => { + render( + , + ); + expect(screen.getByRole('button', { name: '1' })).toHaveAttribute('aria-current', 'page'); + expect(screen.getByRole('button', { name: 'Previous page' })).toBeDisabled(); + expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled(); + }); + + it('lets the consumer className and style win', () => { + render( + , + ); + const nav = screen.getByRole('navigation'); + expect(nav).toHaveClass('cl-pagination', 'my-pagination'); + expect(nav).toHaveStyle({ marginTop: '8px' }); + }); +}); diff --git a/packages/ui/src/mosaic/components/pagination/pagination.tsx b/packages/ui/src/mosaic/components/pagination/pagination.tsx new file mode 100644 index 00000000000..db69aadaf8d --- /dev/null +++ b/packages/ui/src/mosaic/components/pagination/pagination.tsx @@ -0,0 +1,213 @@ +import * as stylex from '@stylexjs/stylex'; +import React from 'react'; + +import type { MosaicElementProps } from '../../props'; +import { mergeStyleProps, themeProps } from '../../props'; +import { reset } from '../../utils/reset.styles'; +import { Button } from '../button'; +import { ButtonContext } from '../button/button.context'; +import { Icon } from '../icon'; +import { Text } from '../text'; +import { getPageItems } from './page-items'; +import { ellipsisSizes, styles } from './pagination.styles'; + +export interface PaginationProps extends Omit, 'onChange'> { + page: number; + totalItems: number; + pageSize: number; + onChange?: (page: number) => void; + hasFirstLast?: boolean; + step?: number; + siblingCount?: number; + size?: 'sm' | 'md' | 'lg'; + disabled?: boolean; + label?: string; +} + +const controlSizes = { + sm: { button: 'xs', icon: 'sm', text: 'xs' }, + md: { button: 'sm', icon: 'sm', text: 'sm' }, + lg: { button: 'md', icon: 'md', text: 'sm' }, +} as const; + +/** + * Page navigation for a paged list. Renders a labelled `nav` with previous/next controls, the + * page numbers around the current one, and a results-per-page control. Pages are 1-based; + * `onChange` receives the page the user asked for. + * + * @example + * + * + * @example + * // With jump-to-ends controls and a wider window of pages + * + */ +export const Pagination = React.forwardRef(function MosaicPagination( + { + page, + totalItems, + pageSize, + onChange, + hasFirstLast = false, + step = 1, + siblingCount = 1, + size = 'md', + disabled = false, + label = 'Pagination', + className, + style, + ...rest + }, + ref, +) { + const pageCount = Math.max(1, Math.ceil(totalItems / pageSize)); + const current = Math.min(Math.max(page, 1), pageCount); + const isFirst = current <= 1; + const isLast = current >= pageCount; + const control = controlSizes[size]; + + const pageDefaults = React.useMemo( + () => + ({ + color: 'neutral', + variant: 'ghost', + size: control.button, + disabled, + styles: styles.page, + }) as const, + [control.button, disabled], + ); + + const goTo = (next: number) => onChange?.(Math.min(Math.max(next, 1), pageCount)); + + return ( + + ); +}); diff --git a/packages/ui/src/mosaic/icons/registry.tsx b/packages/ui/src/mosaic/icons/registry.tsx index 1afda204741..7ac38a04b0e 100644 --- a/packages/ui/src/mosaic/icons/registry.tsx +++ b/packages/ui/src/mosaic/icons/registry.tsx @@ -44,6 +44,32 @@ const ChevronLeft = glyph( />, ); +const ChevronDoubleLeft = glyph( + <> + + + , +); + +const ChevronDoubleRight = glyph( + <> + + + , +); + const ChevronDown = glyph( Date: Wed, 16 Sep 2026 10:05:38 -0400 Subject: [PATCH 02/16] fix(ui): sanitize Pagination numeric props and label the page-size control --- .../components/pagination/pagination.test.tsx | 49 ++++++++++++++++++- .../components/pagination/pagination.tsx | 24 ++++++--- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 4c07fe6e200..0380df58542 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -215,8 +215,53 @@ describe('Mosaic Pagination', () => { pageSize={25} />, ); - expect(screen.getByText('Results per page')).toBeInTheDocument(); - expect(screen.getByRole('button', { name: '25' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Results per page 25' })).toBeInTheDocument(); + }); + + it('treats a page size below one as one', async () => { + const onChange = vi.fn(); + render( + , + ); + const pages = controls() + .getAllByRole('button') + .map(button => button.textContent) + .filter(Boolean); + expect(pages).toEqual(['1', '2', '3', '4', '5']); + await userEvent.click(controls().getByRole('button', { name: '5' })); + expect(onChange).toHaveBeenCalledWith(5); + }); + + it('treats a step below one as one', async () => { + const onChange = vi.fn(); + render( + , + ); + await userEvent.click(screen.getByRole('button', { name: 'Next page' })); + expect(onChange).toHaveBeenCalledWith(3); + }); + + it('treats a negative sibling count as zero', () => { + render( + , + ); + expect(controls().getByRole('button', { name: '5' })).toHaveAttribute('aria-current', 'page'); }); it('renders one page when there are no items', () => { diff --git a/packages/ui/src/mosaic/components/pagination/pagination.tsx b/packages/ui/src/mosaic/components/pagination/pagination.tsx index db69aadaf8d..f3cd3438a8e 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.tsx @@ -24,6 +24,10 @@ export interface PaginationProps extends Omit, 'onChan label?: string; } +function atLeast(value: number, min: number): number { + return Number.isFinite(value) && value > min ? Math.floor(value) : min; +} + const controlSizes = { sm: { button: 'xs', icon: 'sm', text: 'xs' }, md: { button: 'sm', icon: 'sm', text: 'sm' }, @@ -60,8 +64,13 @@ export const Pagination = React.forwardRef(functio }, ref, ) { - const pageCount = Math.max(1, Math.ceil(totalItems / pageSize)); - const current = Math.min(Math.max(page, 1), pageCount); + const itemsPerPage = atLeast(pageSize, 1); + const pageStep = atLeast(step, 1); + const siblings = atLeast(siblingCount, 0); + const pageCount = Math.max(1, Math.ceil(atLeast(totalItems, 0) / itemsPerPage)); + const current = Math.min(atLeast(page, 1), pageCount); + const labelId = React.useId(); + const pageSizeId = React.useId(); const isFirst = current <= 1; const isLast = current >= pageCount; const control = controlSizes[size]; @@ -118,7 +127,7 @@ export const Pagination = React.forwardRef(functio shape='square' touchTarget={false} disabled={disabled || isFirst} - onClick={() => goTo(current - step)} + onClick={() => goTo(current - pageStep)} > (functio /> - {getPageItems(current, pageCount, siblingCount).map(item => + {getPageItems(current, pageCount, siblings).map(item => typeof item === 'number' ? ( + + ); From 2d5e8f5029397bfe34a0a5228866d6aa24d1cef0 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 15:07:08 -0400 Subject: [PATCH 06/16] fix(ui): use the restructured foreground tokens in Pagination --- .../src/mosaic/components/pagination/pagination.styles.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index 36b1cf1459c..f48c7547d1e 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -17,13 +17,13 @@ export const styles = stylex.create({ }, page: { color: { - default: colorVars['--cl-color-neutral-faded'], - ':where([aria-current="page"])': colorVars['--cl-color-neutral-foreground'], + default: colorVars['--cl-color-foreground-secondary'], + ':where([aria-current="page"])': colorVars['--cl-color-foreground'], }, }, ellipsis: { alignItems: 'center', - color: colorVars['--cl-color-neutral-faded'], + color: colorVars['--cl-color-foreground-secondary'], display: 'inline-flex', justifyContent: 'center', }, From fe961316216e2b583a523c1782a21b265794d90d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 15:28:56 -0400 Subject: [PATCH 07/16] fix(ui): size the Pagination page size options to match the trigger Drop the size prop and render Pagination at the sm control size only, sizing the Select options to the sm Button metrics so the popup aligns with its trigger. --- packages/swingset/src/lib/registry.ts | 2 - packages/swingset/src/stories/pagination.mdx | 7 --- .../src/stories/pagination.stories.tsx | 21 ------- .../pagination/pagination.styles.ts | 17 +++--- .../components/pagination/pagination.test.tsx | 14 ++++- .../components/pagination/pagination.tsx | 58 ++++++++----------- 6 files changed, 48 insertions(+), 71 deletions(-) diff --git a/packages/swingset/src/lib/registry.ts b/packages/swingset/src/lib/registry.ts index 1f5567d71e4..aaa471a1976 100644 --- a/packages/swingset/src/lib/registry.ts +++ b/packages/swingset/src/lib/registry.ts @@ -117,7 +117,6 @@ import { Primary as PaginationPrimary, Siblings as PaginationSiblings, SinglePage as PaginationSinglePage, - Sizes as PaginationSizes, } from '../stories/pagination.stories'; import { Default as PhoneInputDefault, @@ -386,7 +385,6 @@ const inputGroupModule: StoryModule = { const paginationModule: StoryModule = { meta: paginationMeta, Primary: PaginationPrimary, - Sizes: PaginationSizes, FirstLast: PaginationFirstLast, Siblings: PaginationSiblings, SinglePage: PaginationSinglePage, diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx index 55a7ebf3a5c..a7a91bbae18 100644 --- a/packages/swingset/src/stories/pagination.mdx +++ b/packages/swingset/src/stories/pagination.mdx @@ -40,13 +40,6 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex ## Examples -### Sizes - - - ### First and last ) { ); } -export function Sizes(props: Record) { - const [page, setPage] = useState(3); - return ( -
- {(['sm', 'md', 'lg'] as const).map(size => ( - - ))} -
- ); -} - export function FirstLast(props: Record) { const [page, setPage] = useState(1); return ( diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index f48c7547d1e..4b83afdf108 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, space } from '../../tokens.stylex'; +import { colorVars, space, typeScaleVars } from '../../tokens.stylex'; export const styles = stylex.create({ root: { @@ -26,16 +26,19 @@ export const styles = stylex.create({ color: colorVars['--cl-color-foreground-secondary'], display: 'inline-flex', justifyContent: 'center', + height: space['7'], + width: space['7'], }, pageSize: { gap: space['2'], alignItems: 'center', display: 'flex', }, -}); - -export const ellipsisSizes = stylex.create({ - sm: { height: space['6'], width: space['6'] }, - md: { height: space['7'], width: space['7'] }, - lg: { height: space['8'], width: space['8'] }, + pageSizeOption: { + paddingBlock: 0, + paddingInline: space['2.5'], + fontSize: typeScaleVars['--cl-text-xs-size'], + lineHeight: typeScaleVars['--cl-text-xs-leading'], + minHeight: space['7'], + }, }); diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 0114f17e039..80e71875d08 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -6,6 +6,7 @@ import { describe, expect, it, vi } from 'vitest'; import { getPageItems } from './page-items'; import { Pagination } from './pagination'; +import { styles } from './pagination.styles'; const atoms = stylex.create({ spaced: { marginTop: '8px' }, @@ -58,7 +59,6 @@ describe('Mosaic Pagination', () => { ); const nav = screen.getByRole('navigation', { name: 'Pagination' }); expect(nav).toHaveClass('cl-pagination'); - expect(nav).toHaveAttribute('data-size', 'md'); }); it('uses the label prop as the accessible name', () => { @@ -239,6 +239,18 @@ describe('Mosaic Pagination', () => { expect(onPageSizeChange).toHaveBeenCalledWith(50); }); + it('sizes the page size options to match the trigger', async () => { + render( + , + ); + await userEvent.click(screen.getByRole('combobox')); + expect(screen.getByRole('option', { name: '10' })).toHaveClass(stylex.props(styles.pageSizeOption).className ?? ''); + }); + it('offers the given page sizes plus the current one', async () => { render( , 'onChange'> { page: number; @@ -22,7 +22,6 @@ export interface PaginationProps extends Omit, 'onChan hasFirstLast?: boolean; step?: number; siblingCount?: number; - size?: 'sm' | 'md' | 'lg'; disabled?: boolean; label?: string; } @@ -33,12 +32,6 @@ function atLeast(value: number, min: number): number { const defaultPageSizeOptions = [10, 25, 50, 100]; -const controlSizes = { - sm: { button: 'xs', icon: 'sm', text: 'xs' }, - md: { button: 'sm', icon: 'sm', text: 'sm' }, - lg: { button: 'md', icon: 'md', text: 'sm' }, -} as const; - /** * Page navigation for a paged list. Renders a labelled `nav` with previous/next controls, the * page numbers around the current one, and a results-per-page control. Pages are 1-based; @@ -62,7 +55,6 @@ export const Pagination = React.forwardRef(functio hasFirstLast = false, step = 1, siblingCount = 1, - size = 'md', disabled = false, label = 'Pagination', xstyle, @@ -85,18 +77,17 @@ export const Pagination = React.forwardRef(functio }, [pageSizeOptions, itemsPerPage]); const isFirst = current <= 1; const isLast = current >= pageCount; - const control = controlSizes[size]; const pageDefaults = React.useMemo( () => ({ color: 'neutral', variant: 'ghost', - size: control.button, + size: 'sm', disabled, styles: styles.page, }) as const, - [control.button, disabled], + [disabled], ); const goTo = (next: number) => onChange?.(Math.min(Math.max(next, 1), pageCount)); @@ -105,11 +96,7 @@ export const Pagination = React.forwardRef(functio From db99f7d43b464a6d5fd72c8184f49036fb4cbb16 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 15:32:08 -0400 Subject: [PATCH 08/16] fix(ui): inset the Pagination page size options to the sm trigger text --- .../ui/src/mosaic/components/pagination/pagination.styles.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index 4b83afdf108..df9eadaa32f 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -36,7 +36,7 @@ export const styles = stylex.create({ }, pageSizeOption: { paddingBlock: 0, - paddingInline: space['2.5'], + paddingInline: space['2'], fontSize: typeScaleVars['--cl-text-xs-size'], lineHeight: typeScaleVars['--cl-text-xs-leading'], minHeight: space['7'], From 868ce38f366bb9d0393014ea3c9c681ccca6d59d Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 15:35:19 -0400 Subject: [PATCH 09/16] refactor(ui): give the Pagination page size label its own slot --- .../mosaic/components/pagination/pagination.styles.ts | 3 +++ .../mosaic/components/pagination/pagination.test.tsx | 11 +++++++++++ .../src/mosaic/components/pagination/pagination.tsx | 10 ++++------ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index df9eadaa32f..33d7db60ee3 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -29,6 +29,9 @@ export const styles = stylex.create({ height: space['7'], width: space['7'], }, + label: { + color: colorVars['--cl-color-foreground-secondary'], + }, pageSize: { gap: space['2'], alignItems: 'center', diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 80e71875d08..08ed18896fb 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -239,6 +239,17 @@ describe('Mosaic Pagination', () => { expect(onPageSizeChange).toHaveBeenCalledWith(50); }); + it('renders the page size label in its own slot', () => { + render( + , + ); + expect(screen.getByText('Results per page')).toHaveClass('cl-pagination-label'); + }); + it('sizes the page size options to match the trigger', async () => { render( (functio )}
- } - size='sm' - color='neutral' + {...mergeStyleProps(themeProps('pagination-label'), stylex.props(reset.base, sizes.sm, styles.label))} > Results per page - + Date: Wed, 16 Sep 2026 15:40:57 -0400 Subject: [PATCH 10/16] feat(ui): take the Pagination page size label as a prop --- packages/swingset/src/stories/pagination.mdx | 6 ++++++ .../components/pagination/pagination.test.tsx | 13 +++++++++++++ .../src/mosaic/components/pagination/pagination.tsx | 4 +++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx index a7a91bbae18..7609f2b462b 100644 --- a/packages/swingset/src/stories/pagination.mdx +++ b/packages/swingset/src/stories/pagination.mdx @@ -25,6 +25,12 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex { name: 'step', type: 'number', default: '1', description: 'How many pages the previous and next controls move.' }, { name: 'siblingCount', type: 'number', default: '1', description: 'Pages shown on each side of the current one.' }, { name: 'label', type: 'string', default: "'Pagination'", description: 'Accessible name of the `nav`.' }, + { + name: 'pageSizeLabel', + type: 'string', + default: "'Results per page'", + description: 'Text of the label beside the page size select.', + }, ]} /> diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 08ed18896fb..43bfdb5ce1e 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -239,6 +239,19 @@ describe('Mosaic Pagination', () => { expect(onPageSizeChange).toHaveBeenCalledWith(50); }); + it('uses the pageSizeLabel prop as the page size label', () => { + render( + , + ); + expect(screen.getByText('Rows per page')).toHaveClass('cl-pagination-label'); + expect(screen.getByRole('combobox', { name: 'Rows per page 10' })).toBeInTheDocument(); + }); + it('renders the page size label in its own slot', () => { render( , 'onChan siblingCount?: number; disabled?: boolean; label?: string; + pageSizeLabel?: string; } function atLeast(value: number, min: number): number { @@ -57,6 +58,7 @@ export const Pagination = React.forwardRef(functio siblingCount = 1, disabled = false, label = 'Pagination', + pageSizeLabel = 'Results per page', xstyle, ...rest }, @@ -194,7 +196,7 @@ export const Pagination = React.forwardRef(functio id={labelId} {...mergeStyleProps(themeProps('pagination-label'), stylex.props(reset.base, sizes.sm, styles.label))} > - Results per page + {pageSizeLabel} Date: Wed, 16 Sep 2026 16:17:20 -0400 Subject: [PATCH 11/16] refactor(ui): render the Pagination page size label with Text --- .../mosaic/components/pagination/pagination.styles.ts | 3 --- .../mosaic/components/pagination/pagination.test.tsx | 9 ++++++--- .../ui/src/mosaic/components/pagination/pagination.tsx | 10 ++++++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index 33d7db60ee3..df9eadaa32f 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -29,9 +29,6 @@ export const styles = stylex.create({ height: space['7'], width: space['7'], }, - label: { - color: colorVars['--cl-color-foreground-secondary'], - }, pageSize: { gap: space['2'], alignItems: 'center', diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 43bfdb5ce1e..c6e5d77f976 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -248,11 +248,11 @@ describe('Mosaic Pagination', () => { pageSizeLabel='Rows per page' />, ); - expect(screen.getByText('Rows per page')).toHaveClass('cl-pagination-label'); + expect(screen.getByText('Rows per page')).toHaveClass('cl-text'); expect(screen.getByRole('combobox', { name: 'Rows per page 10' })).toBeInTheDocument(); }); - it('renders the page size label in its own slot', () => { + it('renders the page size label as secondary sm text', () => { render( { pageSize={10} />, ); - expect(screen.getByText('Results per page')).toHaveClass('cl-pagination-label'); + const label = screen.getByText('Results per page'); + expect(label).toHaveClass('cl-text'); + expect(label).toHaveAttribute('data-size', 'sm'); + expect(label).toHaveAttribute('data-color', 'foreground-secondary'); }); it('sizes the page size options to match the trigger', async () => { diff --git a/packages/ui/src/mosaic/components/pagination/pagination.tsx b/packages/ui/src/mosaic/components/pagination/pagination.tsx index 8ddc4e88270..870e94750cc 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.tsx @@ -4,11 +4,11 @@ import React from 'react'; import type { MosaicElementProps } from '../../props'; import { mergeStyleProps, themeProps } from '../../props'; import { reset } from '../../utils/reset.styles'; -import { sizes } from '../../utils/typography.styles'; import { Button } from '../button'; import { ButtonContext } from '../button/button.context'; import { Icon } from '../icon'; import { Select } from '../select'; +import { Text } from '../text'; import { getPageItems } from './page-items'; import { styles } from './pagination.styles'; @@ -192,12 +192,14 @@ export const Pagination = React.forwardRef(functio )}
- } + size='sm' + color='foreground-secondary' > {pageSizeLabel} - + Date: Wed, 16 Sep 2026 16:18:45 -0400 Subject: [PATCH 12/16] fix(ui): keep the pagination-label slot class on the Text label --- .../ui/src/mosaic/components/pagination/pagination.test.tsx | 2 +- packages/ui/src/mosaic/components/pagination/pagination.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index c6e5d77f976..c2d84a3c8c5 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -261,7 +261,7 @@ describe('Mosaic Pagination', () => { />, ); const label = screen.getByText('Results per page'); - expect(label).toHaveClass('cl-text'); + expect(label).toHaveClass('cl-text', 'cl-pagination-label'); expect(label).toHaveAttribute('data-size', 'sm'); expect(label).toHaveAttribute('data-color', 'foreground-secondary'); }); diff --git a/packages/ui/src/mosaic/components/pagination/pagination.tsx b/packages/ui/src/mosaic/components/pagination/pagination.tsx index 870e94750cc..af8dbc5288e 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.tsx @@ -197,6 +197,7 @@ export const Pagination = React.forwardRef(functio render={} size='sm' color='foreground-secondary' + {...themeProps('pagination-label')} > {pageSizeLabel} From bdb06d48f7be4a2cabac68599ea9fdcc2bca3fb8 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Wed, 16 Sep 2026 17:05:14 -0400 Subject: [PATCH 13/16] fix(ui): drop invalid and duplicate Pagination page size options --- .../components/pagination/pagination.test.tsx | 13 +++++++++++++ .../src/mosaic/components/pagination/pagination.tsx | 9 ++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index c2d84a3c8c5..69ff0b5dee0 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -291,6 +291,19 @@ describe('Mosaic Pagination', () => { expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['10', '15', '20']); }); + it('drops invalid and duplicate page size options', async () => { + render( + , + ); + await userEvent.click(screen.getByRole('combobox')); + expect(screen.getAllByRole('option').map(option => option.textContent)).toEqual(['10', '12', '25']); + }); + it('treats a page size below one as one', async () => { const onChange = vi.fn(); render( diff --git a/packages/ui/src/mosaic/components/pagination/pagination.tsx b/packages/ui/src/mosaic/components/pagination/pagination.tsx index af8dbc5288e..ba43198522a 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.tsx @@ -71,11 +71,10 @@ export const Pagination = React.forwardRef(functio const current = Math.min(atLeast(page, 1), pageCount); const labelId = React.useId(); const pageSizeItems = React.useMemo(() => { - const sizes = pageSizeOptions.includes(itemsPerPage) ? pageSizeOptions : [...pageSizeOptions, itemsPerPage]; - return sizes - .slice() - .sort((a, b) => a - b) - .map(size => ({ value: String(size), label: String(size) })); + const sizes = new Set( + [...pageSizeOptions, itemsPerPage].filter(size => Number.isFinite(size) && size >= 1).map(Math.floor), + ); + return [...sizes].sort((a, b) => a - b).map(size => ({ value: String(size), label: String(size) })); }, [pageSizeOptions, itemsPerPage]); const isFirst = current <= 1; const isLast = current >= pageCount; From 9ad65e7181a746643b268a743a35746babc45dd7 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 17 Sep 2026 18:43:57 -0400 Subject: [PATCH 14/16] feat(ui): localize Pagination control labels and fill the gaps between controls --- packages/swingset/src/stories/pagination.mdx | 13 +++++-- .../pagination/pagination.styles.ts | 9 +++++ .../components/pagination/pagination.test.tsx | 34 +++++++++++++++++++ .../components/pagination/pagination.tsx | 21 +++++++++--- 4 files changed, 71 insertions(+), 6 deletions(-) diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx index 7609f2b462b..6514f9d74dd 100644 --- a/packages/swingset/src/stories/pagination.mdx +++ b/packages/swingset/src/stories/pagination.mdx @@ -4,9 +4,9 @@ import * as PaginationStories from './pagination.stories'; Page navigation for a paged list. Renders a labelled `nav` with previous and next controls, the page numbers around the current one, and a results-per-page control. Pages are 1-based and the component is controlled: `onChange` receives the page the user asked for, and `onPageSizeChange` receives the size chosen from the results-per-page `Select`. -## Playground +## Example - @@ -31,6 +31,15 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex default: "'Results per page'", description: 'Text of the label beside the page size select.', }, + { name: 'firstPageLabel', type: 'string', default: "'First page'", description: 'Accessible name of the first page control.' }, + { + name: 'previousPageLabel', + type: 'string', + default: "'Previous page'", + description: 'Accessible name of the previous page control.', + }, + { name: 'nextPageLabel', type: 'string', default: "'Next page'", description: 'Accessible name of the next page control.' }, + { name: 'lastPageLabel', type: 'string', default: "'Last page'", description: 'Accessible name of the last page control.' }, ]} /> diff --git a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts index df9eadaa32f..19e7e7bf259 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.styles.ts +++ b/packages/ui/src/mosaic/components/pagination/pagination.styles.ts @@ -15,6 +15,15 @@ export const styles = stylex.create({ alignItems: 'center', display: 'flex', }, + hitTarget: { + position: 'relative', + '::after': { + insetBlock: 0, + insetInline: `calc(${space['1']} / -2)`, + content: '""', + position: 'absolute', + }, + }, page: { color: { default: colorVars['--cl-color-foreground-secondary'], diff --git a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx index 69ff0b5dee0..b1be9b3c664 100644 --- a/packages/ui/src/mosaic/components/pagination/pagination.test.tsx +++ b/packages/ui/src/mosaic/components/pagination/pagination.test.tsx @@ -252,6 +252,40 @@ describe('Mosaic Pagination', () => { expect(screen.getByRole('combobox', { name: 'Rows per page 10' })).toBeInTheDocument(); }); + it('uses the control label props as the control names', () => { + render( + , + ); + expect(screen.getByRole('button', { name: 'Start' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Forward' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'End' })).toBeInTheDocument(); + }); + + it('extends every control hit target across the gap', () => { + render( + , + ); + const hitTarget = stylex.props(styles.hitTarget).className ?? ''; + for (const name of ['First page', 'Previous page', '2', 'Next page', 'Last page']) { + expect(screen.getByRole('button', { name })).toHaveClass(hitTarget); + } + }); + it('renders the page size label as secondary sm text', () => { render( , 'onChan disabled?: boolean; label?: string; pageSizeLabel?: string; + firstPageLabel?: string; + previousPageLabel?: string; + nextPageLabel?: string; + lastPageLabel?: string; } function atLeast(value: number, min: number): number { @@ -59,6 +63,10 @@ export const Pagination = React.forwardRef(functio disabled = false, label = 'Pagination', pageSizeLabel = 'Results per page', + firstPageLabel = 'First page', + previousPageLabel = 'Previous page', + nextPageLabel = 'Next page', + lastPageLabel = 'Last page', xstyle, ...rest }, @@ -102,13 +110,14 @@ export const Pagination = React.forwardRef(functio
{hasFirstLast && ( )} {hasFirstLast && ( @@ -139,6 +141,7 @@ export const Pagination = React.forwardRef(functio > @@ -181,6 +184,7 @@ export const Pagination = React.forwardRef(functio > @@ -198,6 +202,7 @@ export const Pagination = React.forwardRef(functio > diff --git a/packages/swingset/src/stories/pagination.mdx b/packages/swingset/src/stories/pagination.mdx index 6514f9d74dd..3edae7ddb0f 100644 --- a/packages/swingset/src/stories/pagination.mdx +++ b/packages/swingset/src/stories/pagination.mdx @@ -47,7 +47,7 @@ Page navigation for a paged list. Renders a labelled `nav` with previous and nex diff --git a/packages/swingset/src/stories/pagination.stories.tsx b/packages/swingset/src/stories/pagination.stories.tsx index 200faa236bc..b8a2755d379 100644 --- a/packages/swingset/src/stories/pagination.stories.tsx +++ b/packages/swingset/src/stories/pagination.stories.tsx @@ -1,5 +1,5 @@ -import type { PaginationProps } from '@clerk/ui/mosaic/components/pagination'; -import { Pagination } from '@clerk/ui/mosaic/components/pagination'; +import type { PaginationProps } from '@clerk/mosaic/components/pagination'; +import { Pagination } from '@clerk/mosaic/components/pagination'; import { useState } from 'react'; import type { StoryMeta } from '@/lib/types'; @@ -15,7 +15,7 @@ export const meta: StoryMeta = { status: 'wip', substatus: 'needs design', title: 'Pagination', - source: 'packages/ui/src/mosaic/components/pagination/pagination.tsx', + source: 'packages/mosaic/src/components/pagination/pagination.tsx', styles: { _variants: { hasFirstLast: { true: {}, false: {} },