@automattic/vip-design-system 2.18.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/.storybook/preview-head.html +1 -0
  2. package/build/system/Badge/Badge.js +2 -1
  3. package/build/system/DescriptionList/DescriptionList.js +0 -1
  4. package/build/system/Form/Label.d.ts +1 -3
  5. package/build/system/Form/Label.js +1 -3
  6. package/build/system/Form/RadioBoxGroup.jsx +12 -1
  7. package/build/system/Form/RadioBoxGroup.stories.jsx +6 -1
  8. package/build/system/Heading/Heading.js +2 -3
  9. package/build/system/Heading/Heading.stories.js +15 -2
  10. package/build/system/Nav/styles/variants/menu.js +1 -2
  11. package/build/system/Notice/Notice.js +1 -1
  12. package/build/system/Pagination/Pagination.d.ts +10 -20
  13. package/build/system/Pagination/Pagination.js +50 -68
  14. package/build/system/Pagination/Pagination.stories.js +13 -11
  15. package/build/system/Pagination/Pagination.test.js +4 -4
  16. package/build/system/Pagination/PaginationLayout.d.ts +27 -0
  17. package/build/system/Pagination/PaginationLayout.js +63 -0
  18. package/build/system/Pagination/SimplePagination.d.ts +26 -0
  19. package/build/system/Pagination/SimplePagination.js +76 -0
  20. package/build/system/Pagination/SimplePagination.stories.d.ts +13 -0
  21. package/build/system/Pagination/SimplePagination.stories.js +130 -0
  22. package/build/system/Pagination/SimplePagination.test.d.ts +2 -0
  23. package/build/system/Pagination/SimplePagination.test.js +171 -0
  24. package/build/system/Pagination/index.d.ts +3 -1
  25. package/build/system/Pagination/index.js +2 -1
  26. package/build/system/Pagination/styles.js +1 -4
  27. package/build/system/Table/TableCell.js +1 -1
  28. package/build/system/Text/Text.js +0 -1
  29. package/build/system/Text/Text.stories.js +16 -13
  30. package/build/system/Toolbar/Logo.js +22 -6
  31. package/build/system/Wizard/Wizard.stories.js +11 -11
  32. package/build/system/Wizard/WizardStep.js +0 -2
  33. package/build/system/index.d.ts +2 -2
  34. package/build/system/index.js +2 -2
  35. package/build/system/theme/generated/valet-theme-dark.json +224 -227
  36. package/build/system/theme/generated/valet-theme-light.json +224 -227
  37. package/build/system/theme/getPropValue.js +3 -7
  38. package/build/system/theme/index.d.ts +20 -12
  39. package/build/system/theme/index.js +27 -20
  40. package/docs/SETUP.md +1 -1
  41. package/package.json +1 -1
  42. package/src/system/Badge/Badge.tsx +2 -1
  43. package/src/system/DescriptionList/DescriptionList.tsx +0 -1
  44. package/src/system/Form/Label.tsx +1 -3
  45. package/src/system/Form/RadioBoxGroup.jsx +12 -1
  46. package/src/system/Form/RadioBoxGroup.stories.jsx +6 -1
  47. package/src/system/Heading/Heading.stories.tsx +10 -1
  48. package/src/system/Heading/Heading.tsx +1 -2
  49. package/src/system/Nav/styles/variants/menu.ts +1 -2
  50. package/src/system/Notice/Notice.tsx +1 -1
  51. package/src/system/Pagination/Pagination.stories.tsx +13 -10
  52. package/src/system/Pagination/Pagination.test.tsx +4 -6
  53. package/src/system/Pagination/Pagination.tsx +36 -71
  54. package/src/system/Pagination/PaginationLayout.tsx +93 -0
  55. package/src/system/Pagination/SimplePagination.stories.tsx +127 -0
  56. package/src/system/Pagination/SimplePagination.test.tsx +120 -0
  57. package/src/system/Pagination/SimplePagination.tsx +97 -0
  58. package/src/system/Pagination/index.ts +3 -1
  59. package/src/system/Pagination/styles.ts +1 -4
  60. package/src/system/Table/TableCell.tsx +1 -1
  61. package/src/system/Text/Text.stories.tsx +7 -4
  62. package/src/system/Text/Text.tsx +0 -1
  63. package/src/system/Toolbar/Logo.tsx +19 -2
  64. package/src/system/Wizard/Wizard.stories.tsx +11 -11
  65. package/src/system/Wizard/WizardStep.tsx +0 -2
  66. package/src/system/index.ts +2 -1
  67. package/src/system/theme/generated/valet-theme-dark.json +224 -227
  68. package/src/system/theme/generated/valet-theme-light.json +224 -227
  69. package/src/system/theme/getPropValue.ts +1 -8
  70. package/src/system/theme/index.ts +33 -18
  71. package/tokens/valet-core/valet-core.json +39 -9
  72. package/tokens/valet-core/wpvip-product-core.json +88 -125
@@ -16,6 +16,7 @@ var _ThemeBuilder = ThemeBuilder(Valet),
16
16
  ValetTheme = _ThemeBuilder.ValetTheme,
17
17
  getHeadingStyles = _ThemeBuilder.getHeadingStyles;
18
18
  var light = ColorBuilder(ValetTheme);
19
+ var supportLabelDefaultTypography = getPropValue('support', 'label-default');
19
20
 
20
21
  // Dark
21
22
  var _ThemeBuilder2 = ThemeBuilder(ValetDark),
@@ -29,12 +30,6 @@ var outline = {
29
30
  outlineWidth: '1px',
30
31
  boxShadow: "0 0 0 1px " + getPropValue('focus', 'inset') + ", 0 0 0 3px " + getPropValue('focus')
31
32
  };
32
- var fonts = {
33
- body: '-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"',
34
- heading: 'inherit',
35
- monospace: '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace',
36
- serif: 'recoletaregular, Georgia, serif'
37
- };
38
33
  var getComponentColors = function getComponentColors(theme, gColor, gVariants) {
39
34
  return {
40
35
  // Valet Theme Colors
@@ -175,13 +170,15 @@ var getComponentColors = function getComponentColors(theme, gColor, gVariants) {
175
170
  export default {
176
171
  outline: outline,
177
172
  space: getVariants('space'),
178
- fonts: fonts,
173
+ fonts: {
174
+ monospace: getPropValue('fontFamily', 'monospace'),
175
+ "default": getPropValue('fontFamily', 'default')
176
+ },
179
177
  fontSizes: getVariants('fontSize.static'),
180
178
  breakpoints: generateBreakpoints(getVariants('breakpoint')),
181
179
  fontWeights: {
182
- body: getPropValue('fontWeight', 'body'),
183
- heading: getPropValue('fontWeight', 'heading'),
184
180
  regular: getPropValue('fontWeight', 'regular'),
181
+ semibold: getPropValue('fontWeight', 'semibold'),
185
182
  bold: getPropValue('fontWeight', 'bold'),
186
183
  medium: getPropValue('fontWeight', 'medium'),
187
184
  light: getPropValue('fontWeight', 'light')
@@ -276,9 +273,13 @@ export default {
276
273
  }
277
274
  }
278
275
  },
276
+ forms: {
277
+ label: _extends({}, supportLabelDefaultTypography)
278
+ },
279
279
  buttons: {
280
- primary: {
281
- fontFamily: 'body',
280
+ primary: _extends({}, supportLabelDefaultTypography, {
281
+ // Button label weight: theme `medium` (500).
282
+ fontWeight: 'medium',
282
283
  color: 'button.primary.label.default',
283
284
  bg: 'button.primary.background.default',
284
285
  border: '1px solid transparent',
@@ -287,7 +288,6 @@ export default {
287
288
  minHeight: '38px',
288
289
  display: 'inline-flex',
289
290
  cursor: 'pointer',
290
- fontWeight: 'medium',
291
291
  boxShadow: 'none',
292
292
  borderRadius: 1,
293
293
  '&:hover': {
@@ -326,7 +326,7 @@ export default {
326
326
  backgroundColor: 'button.secondary.background.disabled',
327
327
  color: 'button.secondary.label.default'
328
328
  }
329
- },
329
+ }),
330
330
  secondary: {
331
331
  variant: 'buttons.primary',
332
332
  color: 'button.secondary.label.default',
@@ -490,7 +490,14 @@ export default {
490
490
  variant: 'buttons.tertiary'
491
491
  }
492
492
  },
493
- text: getHeadingStyles(),
493
+ text: _extends({}, getHeadingStyles(), getVariants('body'), {
494
+ 'support-helper-text': getPropValue('support', 'helper-text'),
495
+ 'support-label-xs': getPropValue('support', 'label-xs'),
496
+ 'support-label-small': getPropValue('support', 'label-small'),
497
+ 'support-label-default': getPropValue('support', 'label-default'),
498
+ 'support-label-default-quiet': getPropValue('support', 'label-default-quiet'),
499
+ 'support-label-default-loud': getPropValue('support', 'label-default-loud')
500
+ }),
494
501
  dialog: {
495
502
  modal: {
496
503
  position: 'fixed',
@@ -569,10 +576,10 @@ export default {
569
576
  }
570
577
  },
571
578
  styles: {
572
- root: _extends({
573
- fontFamily: 'body',
574
- lineHeight: 'body',
575
- fontWeight: 'body',
579
+ root: {
580
+ fontFamily: getPropValue('body', 'default').fontFamily,
581
+ lineHeight: getPropValue('body', 'default').lineHeight,
582
+ fontWeight: getPropValue('body', 'default').fontWeight,
576
583
  color: 'text',
577
584
  backgroundColor: 'backgrounds.primary',
578
585
  webkitFontSmoothing: 'antialiased',
@@ -589,11 +596,11 @@ export default {
589
596
  display: 'block'
590
597
  },
591
598
  pre: {
592
- fontFamily: 'body'
599
+ fontFamily: 'default'
593
600
  },
594
601
  p: {
595
602
  color: 'text'
596
603
  }
597
- }, getHeadingStyles())
604
+ }
598
605
  }
599
606
  };
package/docs/SETUP.md CHANGED
@@ -41,7 +41,7 @@ Run:
41
41
  npm run dev
42
42
  ```
43
43
 
44
- You can then visit [http://localhost:60006/](http://localhost:60006/) to view.
44
+ You can then visit [http://localhost:6006/](http://localhost:6006/) to view.
45
45
 
46
46
  ## Updating dependencies
47
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/vip-design-system",
3
- "version": "2.18.0",
3
+ "version": "2.19.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/Automattic/vip-design-system"
@@ -27,6 +27,7 @@ export const Badge = forwardRef< HTMLDivElement, BadgeProps >(
27
27
  as="span"
28
28
  sx={ {
29
29
  fontSize: 0,
30
+ letterSpacing: '0.01em',
30
31
  padding: 0, // do we need padding declared twice here?
31
32
  bg: `tag.${ variant }.background`,
32
33
  color: `tag.${ variant }.text`,
@@ -35,7 +36,7 @@ export const Badge = forwardRef< HTMLDivElement, BadgeProps >(
35
36
  px: 2,
36
37
  display: 'inline-block',
37
38
  borderRadius: 1,
38
- fontWeight: 'heading',
39
+ fontWeight: 'medium',
39
40
  a: {
40
41
  color: `tag.${ variant }.text`,
41
42
  '&:hover, &:focus, &:active': {
@@ -42,7 +42,6 @@ const TableComponent = ( { list, className, sx, title }: DescriptionListProps )
42
42
  minWidth: 'auto',
43
43
  '> tbody > tr': {
44
44
  '> td, > th': {
45
- fontWeight: 'heading',
46
45
  border: 'none',
47
46
  pl: 0,
48
47
  '&:first-of-type': { pl: 0 },
@@ -13,9 +13,7 @@ import { RequiredLabel } from './RequiredLabel';
13
13
 
14
14
  export const baseLabelColor = 'input.label.default';
15
15
  export const baseLabelStyle = {
16
- fontWeight: 'heading',
17
- fontSize: 2,
18
- lineHeight: 1.5,
16
+ variant: 'forms.label',
19
17
  color: baseLabelColor,
20
18
  };
21
19
 
@@ -72,7 +72,14 @@ const RadioOption = ( {
72
72
  { ...restOption }
73
73
  />
74
74
  <div
75
- sx={ { mb: 0, color: 'input.radio-box.label.primary.default', p: 3, pr: 0, flex: 'auto' } }
75
+ sx={ {
76
+ mb: 0,
77
+ color: 'input.radio-box.label.primary.default',
78
+ fontWeight: 'semibold',
79
+ p: 3,
80
+ pr: 0,
81
+ flex: 'auto',
82
+ } }
76
83
  >
77
84
  <label htmlFor={ forLabel } { ...labelProps }>
78
85
  { label }
@@ -82,7 +89,11 @@ const RadioOption = ( {
82
89
  sx={ {
83
90
  color: 'input.radio-box.label.secondary.default',
84
91
  mb: 0,
92
+ mt: 1,
85
93
  fontSize: 1,
94
+ fontWeight: 'regular',
95
+ letterSpacing: '0.01em',
96
+ lineHeight: '140%',
86
97
  display: 'block',
87
98
  } }
88
99
  id={ describedById }
@@ -60,7 +60,12 @@ export const Primary = {
60
60
  args: {
61
61
  defaultValue: 'one',
62
62
  options: [
63
- { label: 'One', value: 'one', description: 'This is a description' },
63
+ {
64
+ label: 'One',
65
+ value: 'one',
66
+ description:
67
+ 'This is a longer description that allows us to see the text wrap and determine if the line height is correct',
68
+ },
64
69
  { label: 'Two', value: 'two', description: 'This is a description' },
65
70
  { label: 'Three', value: 'three', description: 'This is a description' },
66
71
  ],
@@ -27,7 +27,16 @@ export const Default: Story = {
27
27
  <Heading variant="h3">Heading Three</Heading>
28
28
  <Heading variant="h4">Heading Four</Heading>
29
29
  <Heading variant="h5">Heading Five</Heading>
30
- <Heading variant="h6">Heading Six</Heading>
30
+
31
+ <Heading variant="h3" as="h1">
32
+ Heading One with Heading Three Styles
33
+ </Heading>
34
+ <Heading as="p" sx={ { variant: 'text.caps' } }>
35
+ Paragraph with Caps Styles
36
+ </Heading>
37
+ <Heading as="h3" sx={ { variant: 'text.caps' } }>
38
+ Heading Three with Caps Styles
39
+ </Heading>
31
40
  </Box>
32
41
  ),
33
42
  };
@@ -20,10 +20,9 @@ export const Heading = forwardRef< HTMLHeadingElement, HeadingProps >(
20
20
  ( { variant = 'h3', sx, className, ...rest }: HeadingProps, ref: Ref< HTMLHeadingElement > ) => (
21
21
  <ThemeHeading
22
22
  as={ variant }
23
+ variant={ variant }
23
24
  sx={ {
24
25
  color: 'heading',
25
- // pass variant prop to sx
26
- variant: `text.${ variant.toString() }`,
27
26
  ...sx,
28
27
  } }
29
28
  className={ classNames( 'vip-heading-component', className ) }
@@ -51,8 +51,7 @@ export const menuItemLinkStyles: MixedStyleProp = {
51
51
  borderRadius: 1,
52
52
  color: 'text',
53
53
  display: 'inline-flex',
54
- fontSize: '0.875rem',
55
- fontWeight: 'body',
54
+ variant: 'text.default',
56
55
  height: 38,
57
56
  mx: 0,
58
57
  mb: 0,
@@ -240,7 +240,7 @@ export const Notice = React.forwardRef< HTMLDivElement, NoticeProps >(
240
240
  as={ headingVariant }
241
241
  sx={ {
242
242
  color: textColor,
243
- mb: 0,
243
+ mb: 1,
244
244
  fontSize: 2,
245
245
  fontWeight: 'bold',
246
246
  } }
@@ -21,10 +21,12 @@ const meta: Meta< typeof Pagination > = {
21
21
  component: `
22
22
  A Pagination component for navigating paged data.
23
23
 
24
- ## Variants
24
+ ## Modes
25
25
 
26
- - **full** (default): Shows individual page number buttons with ellipsis for large page counts.
27
- - **compact**: Shows a dropdown page selector instead of individual page numbers.
26
+ - **Default**: Shows individual page number buttons with ellipsis for large page counts.
27
+ - **Compact** (\`compact\` prop): Shows a dropdown page selector instead of individual page numbers.
28
+
29
+ For cursor-based pagination with only prev/next arrows, see \`SimplePagination\`.
28
30
 
29
31
  ## Component Properties
30
32
  `,
@@ -42,12 +44,13 @@ const PaginationWithState = ( {
42
44
  totalItems = 200,
43
45
  initialItemsPerPage = 20,
44
46
  displayItemsPerPageSelector = false,
47
+ compact = false,
45
48
  ...props
46
49
  }: {
47
50
  initialPage?: number;
48
51
  totalItems?: number;
49
52
  initialItemsPerPage?: number;
50
- variant?: 'full' | 'compact';
53
+ compact?: boolean;
51
54
  pageSizeOptions?: number[];
52
55
  displayItemsPerPageSelector?: boolean;
53
56
  } ) => {
@@ -62,6 +65,7 @@ const PaginationWithState = ( {
62
65
  itemsPerPage={ itemsPerPage }
63
66
  onPageChange={ setCurrentPage }
64
67
  displayItemsPerPageSelector={ displayItemsPerPageSelector }
68
+ compact={ compact }
65
69
  onItemsPerPageChange={ size => {
66
70
  setItemsPerPage( size );
67
71
  setCurrentPage( 1 );
@@ -84,12 +88,12 @@ const OpenEndedPaginationWithState = ( {
84
88
  initialPage = 1,
85
89
  initialItemsPerPage = 20,
86
90
  hasNextPage,
87
- ...props
91
+ compact = false,
88
92
  }: {
89
93
  initialPage?: number;
90
94
  initialItemsPerPage?: number;
91
95
  hasNextPage?: boolean;
92
- variant?: 'full' | 'compact';
96
+ compact?: boolean;
93
97
  } ) => {
94
98
  const [ currentPage, setCurrentPage ] = useState( initialPage );
95
99
  const [ itemsPerPage, setItemsPerPage ] = useState( initialItemsPerPage );
@@ -104,7 +108,7 @@ const OpenEndedPaginationWithState = ( {
104
108
  setCurrentPage( 1 );
105
109
  } }
106
110
  hasNextPage={ hasNextPage }
107
- { ...props }
111
+ compact={ compact }
108
112
  >
109
113
  <Flex sx={ { justifyContent: 'center', alignItems: 'center', verticalAlign: 'middle' } }>
110
114
  <Badge variant="gold" sx={ { mr: 2 } }>
@@ -121,7 +125,6 @@ export const Primary: Story = {
121
125
  currentPage: 1,
122
126
  totalItems: 200,
123
127
  itemsPerPage: 20,
124
- variant: 'full',
125
128
  displayItemsPerPageSelector: false,
126
129
  },
127
130
  };
@@ -131,7 +134,7 @@ export const Default: Story = {
131
134
  };
132
135
 
133
136
  export const Compact: Story = {
134
- render: () => <PaginationWithState variant="compact" />,
137
+ render: () => <PaginationWithState compact />,
135
138
  };
136
139
 
137
140
  export const FewPages: Story = {
@@ -212,7 +215,7 @@ export const OpenEnded: Story = {
212
215
  };
213
216
 
214
217
  export const OpenEndedCompact: Story = {
215
- render: () => <OpenEndedPaginationWithState variant="compact" />,
218
+ render: () => <OpenEndedPaginationWithState compact />,
216
219
  };
217
220
 
218
221
  export const OpenEndedLastPage: Story = {
@@ -109,7 +109,7 @@ describe( '<Pagination />', () => {
109
109
  } );
110
110
 
111
111
  it( 'renders compact variant with Page text', () => {
112
- render( <Pagination { ...defaultProps } variant="compact" currentPage={ 3 } /> );
112
+ render( <Pagination { ...defaultProps } compact currentPage={ 3 } /> );
113
113
 
114
114
  expect( screen.getByText( 'Page' ) ).toBeInTheDocument();
115
115
  expect( screen.getByText( 'of 10' ) ).toBeInTheDocument();
@@ -136,9 +136,7 @@ describe( '<Pagination />', () => {
136
136
  } );
137
137
 
138
138
  it( 'has no accessibility violations (compact variant)', async () => {
139
- const { container } = render(
140
- <Pagination { ...defaultProps } variant="compact" currentPage={ 5 } />
141
- );
139
+ const { container } = render( <Pagination { ...defaultProps } compact currentPage={ 5 } /> );
142
140
 
143
141
  expect( await axe( container ) ).toHaveNoViolations();
144
142
  } );
@@ -304,7 +302,7 @@ describe( '<Pagination /> open-ended mode', () => {
304
302
  } );
305
303
 
306
304
  it( 'renders compact variant with "Page" but without "of Y"', () => {
307
- render( <Pagination { ...openEndedProps } variant="compact" /> );
305
+ render( <Pagination { ...openEndedProps } compact /> );
308
306
 
309
307
  expect( screen.getByText( 'Page' ) ).toBeInTheDocument();
310
308
  expect( screen.queryByText( /of \d+/ ) ).not.toBeInTheDocument();
@@ -317,7 +315,7 @@ describe( '<Pagination /> open-ended mode', () => {
317
315
  } );
318
316
 
319
317
  it( 'has no accessibility violations (open-ended compact)', async () => {
320
- const { container } = render( <Pagination { ...openEndedProps } variant="compact" /> );
318
+ const { container } = render( <Pagination { ...openEndedProps } compact /> );
321
319
 
322
320
  expect( await axe( container ) ).toHaveNoViolations();
323
321
  } );
@@ -1,13 +1,12 @@
1
1
  /** @jsxImportSource theme-ui */
2
2
 
3
- import classNames from 'classnames';
4
3
  import { forwardRef } from 'react';
5
4
  import { BiDotsHorizontalRounded } from 'react-icons/bi';
6
5
  import { MdChevronLeft, MdChevronRight } from 'react-icons/md';
7
- import { Flex, ThemeUIStyleObject } from 'theme-ui';
6
+ import { Flex } from 'theme-ui';
8
7
 
8
+ import { PaginationLayout, PaginationLayoutProps } from './PaginationLayout';
9
9
  import {
10
- containerStyles,
11
10
  navigationStyles,
12
11
  pageButtonStyles,
13
12
  activePageButtonStyles,
@@ -19,13 +18,7 @@ import { Button } from '../Button';
19
18
  import { Select } from '../NewForm';
20
19
  import { Text } from '../Text';
21
20
 
22
- export type PaginationVariant = 'full' | 'compact';
23
-
24
- export interface PaginationProps {
25
- /** Whether to show the items-per-page dropdown selector.
26
- * @default false
27
- */
28
- displayItemsPerPageSelector?: boolean;
21
+ export interface PaginationProps extends PaginationLayoutProps {
29
22
  /** The currently active page number (1-based). */
30
23
  currentPage: number;
31
24
  /** Total number of items across all pages. Used to compute totalPages if not provided. */
@@ -42,20 +35,15 @@ export interface PaginationProps {
42
35
  hasNextPage?: boolean;
43
36
  /** The maximum page number that can be reached. Used for open-ended pagination without totalPages. */
44
37
  maxReachablePage?: number;
45
- /** The display variant: 'full' shows page number buttons, 'compact' shows a page dropdown.
46
- * @default 'full'
38
+ /** When true, shows a compact dropdown page selector instead of individual page buttons.
39
+ * @default false
47
40
  */
48
- variant?: PaginationVariant;
49
- /** Available page size options for the items-per-page selector.
50
- * @default [20, 50, 100]
41
+ compact?: boolean;
42
+ /** Display variant. Use 'compact' for dropdown page selector. Equivalent to the `compact` prop.
43
+ * @default 'full'
44
+ * @deprecated Use the `compact` prop instead, or `SimplePagination` for cursor-based navigation.
51
45
  */
52
- pageSizeOptions?: number[];
53
- /** Additional CSS class name for the pagination container. */
54
- className?: string;
55
- /** Theme UI style overrides. */
56
- sx?: ThemeUIStyleObject;
57
- /** Optional content rendered between the items-per-page selector and page navigation. */
58
- children?: React.ReactNode;
46
+ variant?: 'full' | 'compact';
59
47
  }
60
48
 
61
49
  export type PageNumberItem = number | 'ellipsis';
@@ -132,28 +120,6 @@ export function getPageNumbers(
132
120
  ];
133
121
  }
134
122
 
135
- const ItemsPerPageSelect = ( {
136
- itemsPerPage,
137
- pageSizeOptions,
138
- onItemsPerPageChange,
139
- }: {
140
- itemsPerPage: number;
141
- pageSizeOptions: number[];
142
- onItemsPerPageChange: ( size: number ) => void;
143
- } ) => (
144
- <Select
145
- id="items-per-page"
146
- aria-label="Items per page"
147
- separator={ false }
148
- value={ itemsPerPage }
149
- options={ pageSizeOptions.map( size => ( {
150
- value: size,
151
- label: `${ size.toString() } / page`,
152
- } ) ) }
153
- onChange={ option => onItemsPerPageChange( Number( option?.value ) ) }
154
- />
155
- );
156
-
157
123
  const PageNumbers = ( {
158
124
  currentPage,
159
125
  totalPages,
@@ -235,12 +201,11 @@ const CompactPageSelector = ( {
235
201
 
236
202
  /**
237
203
  * A pagination control for navigating through paged content.
238
- * Supports full page-number buttons and compact dropdown modes, with optional items-per-page selection.
204
+ * Shows page-number buttons by default, or a compact dropdown when `compact` is true.
239
205
  */
240
206
  export const Pagination = forwardRef< HTMLElement, PaginationProps >(
241
207
  (
242
208
  {
243
- displayItemsPerPageSelector = false,
244
209
  currentPage,
245
210
  totalItems,
246
211
  totalPages,
@@ -249,8 +214,10 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
249
214
  onItemsPerPageChange,
250
215
  hasNextPage,
251
216
  maxReachablePage,
252
- variant = 'full',
253
- pageSizeOptions = [ 20, 50, 100 ],
217
+ compact = false,
218
+ variant,
219
+ displayItemsPerPageSelector,
220
+ pageSizeOptions,
254
221
  className,
255
222
  sx,
256
223
  children,
@@ -258,47 +225,45 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
258
225
  },
259
226
  ref
260
227
  ) => {
228
+ const isCompact = compact || variant === 'compact';
229
+
261
230
  const resolvedTotalPages =
262
231
  totalPages ??
263
232
  ( totalItems !== undefined ? Math.ceil( totalItems / itemsPerPage ) : undefined );
264
233
 
265
234
  const isFirstPage = currentPage <= 1;
266
- const isLastPage =
267
- resolvedTotalPages !== undefined ? currentPage >= resolvedTotalPages : hasNextPage === false;
235
+ let isLastPage: boolean;
236
+ if ( resolvedTotalPages !== undefined ) {
237
+ isLastPage = currentPage >= resolvedTotalPages;
238
+ } else {
239
+ isLastPage = hasNextPage === false;
240
+ }
268
241
 
269
242
  return (
270
- <nav
243
+ <PaginationLayout
271
244
  ref={ ref }
272
- aria-label="Pagination"
273
- className={ classNames( 'vip-pagination-component', className ) }
274
- sx={ { ...containerStyles, ...sx } }
245
+ displayItemsPerPageSelector={ displayItemsPerPageSelector }
246
+ itemsPerPage={ itemsPerPage }
247
+ pageSizeOptions={ pageSizeOptions }
248
+ onItemsPerPageChange={ onItemsPerPageChange }
249
+ className={ className }
250
+ sx={ sx }
275
251
  { ...rest }
276
252
  >
277
- <Box>
278
- { displayItemsPerPageSelector && (
279
- <ItemsPerPageSelect
280
- itemsPerPage={ itemsPerPage }
281
- pageSizeOptions={ pageSizeOptions }
282
- onItemsPerPageChange={ onItemsPerPageChange }
283
- />
284
- ) }
285
- </Box>
286
253
  <Box sx={ { flex: 1 } }>{ children }</Box>
287
254
  <Flex sx={ navigationStyles }>
288
- { variant === 'full' && (
289
- <PageNumbers
255
+ { isCompact ? (
256
+ <CompactPageSelector
290
257
  currentPage={ currentPage }
291
258
  totalPages={ resolvedTotalPages }
292
- hasNextPage={ hasNextPage }
293
259
  maxReachablePage={ maxReachablePage }
294
260
  onPageChange={ onPageChange }
295
261
  />
296
- ) }
297
-
298
- { variant === 'compact' && (
299
- <CompactPageSelector
262
+ ) : (
263
+ <PageNumbers
300
264
  currentPage={ currentPage }
301
265
  totalPages={ resolvedTotalPages }
266
+ hasNextPage={ hasNextPage }
302
267
  maxReachablePage={ maxReachablePage }
303
268
  onPageChange={ onPageChange }
304
269
  />
@@ -322,7 +287,7 @@ export const Pagination = forwardRef< HTMLElement, PaginationProps >(
322
287
  <MdChevronRight size={ 20 } />
323
288
  </Button>
324
289
  </Flex>
325
- </nav>
290
+ </PaginationLayout>
326
291
  );
327
292
  }
328
293
  );
@@ -0,0 +1,93 @@
1
+ /** @jsxImportSource theme-ui */
2
+
3
+ import classNames from 'classnames';
4
+ import { forwardRef } from 'react';
5
+ import { ThemeUIStyleObject } from 'theme-ui';
6
+
7
+ import { containerStyles } from './styles';
8
+ import { Box } from '../Box';
9
+ import { Select } from '../NewForm';
10
+
11
+ export interface PaginationLayoutProps {
12
+ /** Whether to show the items-per-page dropdown selector.
13
+ * @default false
14
+ */
15
+ displayItemsPerPageSelector?: boolean;
16
+ /** Number of items displayed per page. */
17
+ itemsPerPage?: number;
18
+ /** Available page size options for the items-per-page selector.
19
+ * @default [20, 50, 100]
20
+ */
21
+ pageSizeOptions?: number[];
22
+ /** Callback fired when the user changes the items-per-page value. */
23
+ onItemsPerPageChange?: ( itemsPerPage: number ) => void;
24
+ /** Additional CSS class name for the pagination container. */
25
+ className?: string;
26
+ /** Theme UI style overrides. */
27
+ sx?: ThemeUIStyleObject;
28
+ /** Slot for variant-specific content (page numbers, arrows, etc.). */
29
+ children?: React.ReactNode;
30
+ }
31
+
32
+ const ItemsPerPageSelect = ( {
33
+ itemsPerPage,
34
+ pageSizeOptions,
35
+ onItemsPerPageChange,
36
+ }: {
37
+ itemsPerPage: number;
38
+ pageSizeOptions: number[];
39
+ onItemsPerPageChange: ( size: number ) => void;
40
+ } ) => (
41
+ <Select
42
+ id="items-per-page"
43
+ aria-label="Items per page"
44
+ separator={ false }
45
+ value={ itemsPerPage }
46
+ options={ pageSizeOptions.map( size => ( {
47
+ value: size,
48
+ label: `${ size.toString() } / page`,
49
+ } ) ) }
50
+ onChange={ option => onItemsPerPageChange( Number( option?.value ) ) }
51
+ />
52
+ );
53
+
54
+ /**
55
+ * Shared layout wrapper for pagination components.
56
+ * Renders the nav landmark, optional items-per-page selector, and variant content.
57
+ */
58
+ export const PaginationLayout = forwardRef< HTMLElement, PaginationLayoutProps >(
59
+ (
60
+ {
61
+ displayItemsPerPageSelector = false,
62
+ itemsPerPage,
63
+ pageSizeOptions = [ 20, 50, 100 ],
64
+ onItemsPerPageChange,
65
+ className,
66
+ sx,
67
+ children,
68
+ ...rest
69
+ },
70
+ ref
71
+ ) => (
72
+ <nav
73
+ ref={ ref }
74
+ aria-label="Pagination"
75
+ className={ classNames( 'vip-pagination-component', className ) }
76
+ sx={ { ...containerStyles, ...sx } }
77
+ { ...rest }
78
+ >
79
+ <Box>
80
+ { displayItemsPerPageSelector && itemsPerPage && onItemsPerPageChange && (
81
+ <ItemsPerPageSelect
82
+ itemsPerPage={ itemsPerPage }
83
+ pageSizeOptions={ pageSizeOptions }
84
+ onItemsPerPageChange={ onItemsPerPageChange }
85
+ />
86
+ ) }
87
+ </Box>
88
+ { children }
89
+ </nav>
90
+ )
91
+ );
92
+
93
+ PaginationLayout.displayName = 'PaginationLayout';