@chronogrove/ui 0.79.0 → 0.81.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.
- package/README.md +40 -19
- package/package.json +73 -6
- package/src/__snapshots__/header.spec.js.snap +8 -8
- package/src/__snapshots__/theme.spec.js.snap +39 -20
- package/src/action-button.js +6 -6
- package/src/action-button.spec.js +14 -2
- package/src/action-card-layout.js +13 -0
- package/src/action-card-layout.spec.js +13 -0
- package/src/animated-page-background/ChronogroveAnimatedPageBackground.js +153 -0
- package/src/animated-page-background/ChronogroveAnimatedPageBackground.spec.js +189 -0
- package/src/animated-page-background/ColorBends.js +309 -0
- package/src/animated-page-background/color-bends.css +13 -0
- package/src/animated-page-background/index.js +2 -0
- package/src/animated-page-background/index.spec.js +18 -0
- package/src/button.js +4 -3
- package/src/category-label.js +23 -0
- package/src/category-label.spec.js +24 -0
- package/src/chevron-icons.js +37 -0
- package/src/chronogrove-theme-surface-colors.js +22 -0
- package/src/color-mode/browser-sync.js +7 -0
- package/src/color-mode/browser-sync.spec.js +7 -0
- package/src/color-mode/chronogrove-head-theme.js +22 -0
- package/src/color-mode/head-inline.js +40 -5
- package/src/color-mode/head-inline.spec.js +29 -0
- package/src/color-mode/index.js +3 -0
- package/src/color-mode/resolve-theme-colors.js +18 -6
- package/src/color-mode/resolve-theme-colors.spec.js +13 -3
- package/src/color-mode/spa-navigation.js +14 -0
- package/src/color-mode/spa-navigation.spec.js +25 -0
- package/src/color-mode/use-document-color-mode-surface.js +52 -0
- package/src/color-mode/use-document-color-mode-surface.node.spec.js +12 -0
- package/src/color-mode/use-document-color-mode-surface.spec.js +154 -0
- package/src/color-toggle-styles.css +10 -0
- package/src/color-toggle.js +11 -2
- package/src/emotion-cache.node.spec.js +13 -0
- package/src/emotion-cache.spec.js +12 -0
- package/src/external-link-icon.js +30 -0
- package/src/external-link-icon.spec.js +16 -0
- package/src/gatsby/build-theme-ui-color-mode-head-components.js +7 -1
- package/src/gatsby/index.spec.js +42 -0
- package/src/gatsby/on-route-update-color-mode.js +1 -14
- package/src/header.js +4 -16
- package/src/lazy-load.js +30 -11
- package/src/lazy-load.spec.js +9 -5
- package/src/metric-badge.js +10 -0
- package/src/metric-badge.spec.js +15 -0
- package/src/metric-card.js +95 -0
- package/src/metric-card.spec.js +60 -0
- package/src/muted-card-footer.js +22 -0
- package/src/muted-card-footer.spec.js +25 -0
- package/src/next/app-shell.js +34 -0
- package/src/next/emotion-registry.js +68 -0
- package/src/next/emotion-registry.spec.js +99 -0
- package/src/next/index.js +4 -0
- package/src/next/root-layout-head.js +42 -0
- package/src/next/root-layout-head.spec.js +17 -0
- package/src/next/theme-ui-color-mode-route-sync.js +32 -0
- package/src/page-backdrop.js +42 -0
- package/src/page-backdrop.spec.js +41 -0
- package/src/pagination-button.js +4 -4
- package/src/pagination-button.spec.js +26 -2
- package/src/pagination.js +198 -0
- package/src/pagination.spec.js +281 -0
- package/src/skip-nav/SkipNavLink.js +6 -5
- package/src/skip-nav/SkipNavLink.spec.js +11 -0
- package/src/status-card.js +18 -0
- package/src/status-card.spec.js +38 -0
- package/src/theme.js +27 -20
- package/src/widget-call-to-action.js +106 -0
- package/src/widget-call-to-action.spec.js +115 -0
- package/src/widget-section.js +83 -0
- package/src/widget-section.spec.js +59 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chronogroveHeadTheme,
|
|
3
|
+
resolveChronogroveSurfaceColors,
|
|
4
|
+
buildThemeUiNoFlashInlineScript,
|
|
5
|
+
buildHtmlBackgroundInlineScript,
|
|
6
|
+
buildThemeUiColorModeFallbackCss
|
|
7
|
+
} from '../color-mode/index.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Server Component fragment for the root `<head>`: Emotion insertion point, Theme UI no-flash
|
|
11
|
+
* script, HTML background script, and fallback CSS (same composition as
|
|
12
|
+
* `buildThemeUiColorModeHeadComponents` for Gatsby).
|
|
13
|
+
*/
|
|
14
|
+
export function ChronogroveNextRootLayoutHead() {
|
|
15
|
+
const surface = resolveChronogroveSurfaceColors(chronogroveHeadTheme)
|
|
16
|
+
const colorModeScript = buildThemeUiNoFlashInlineScript()
|
|
17
|
+
const htmlBackgroundScript = buildHtmlBackgroundInlineScript({
|
|
18
|
+
defaultBackgroundHex: surface.defaultBackgroundHex,
|
|
19
|
+
darkBackgroundHex: surface.darkBackgroundHex
|
|
20
|
+
})
|
|
21
|
+
const colorModeFallbackCSS = buildThemeUiColorModeFallbackCss({
|
|
22
|
+
defaultBackgroundHex: surface.defaultBackgroundHex,
|
|
23
|
+
darkBackgroundHex: surface.darkBackgroundHex,
|
|
24
|
+
defaultTextHex: surface.defaultTextHex,
|
|
25
|
+
defaultTextMutedHex: surface.defaultTextMutedHex,
|
|
26
|
+
darkTextHex: surface.darkTextHex,
|
|
27
|
+
darkTextMutedHex: surface.darkTextMutedHex,
|
|
28
|
+
defaultPanelBackground: surface.defaultPanelBackground,
|
|
29
|
+
darkPanelBackground: surface.darkPanelBackground,
|
|
30
|
+
defaultPanelText: surface.defaultPanelText,
|
|
31
|
+
darkPanelText: surface.darkPanelText
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<meta name='emotion-insertion-point' content='' />
|
|
37
|
+
<script dangerouslySetInnerHTML={{ __html: colorModeScript }} />
|
|
38
|
+
<script dangerouslySetInnerHTML={{ __html: htmlBackgroundScript }} />
|
|
39
|
+
<style dangerouslySetInnerHTML={{ __html: colorModeFallbackCSS }} />
|
|
40
|
+
</>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
|
|
7
|
+
import { ChronogroveNextRootLayoutHead } from './root-layout-head.js'
|
|
8
|
+
|
|
9
|
+
describe('ChronogroveNextRootLayoutHead', () => {
|
|
10
|
+
it('emits scripts and fallback CSS (meta belongs in document <head> in real App Router layouts)', () => {
|
|
11
|
+
const { container } = render(<ChronogroveNextRootLayoutHead />)
|
|
12
|
+
// RTL renders into a div; React omits invalid <meta> there — in Next, `<head><ChronogroveNextRootLayoutHead /></head>` keeps the meta.
|
|
13
|
+
expect(container.querySelectorAll('script')).toHaveLength(2)
|
|
14
|
+
expect(container.querySelector('style')).toBeTruthy()
|
|
15
|
+
expect(container.textContent).toMatch(/theme-ui-color-mode/)
|
|
16
|
+
})
|
|
17
|
+
})
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
5
|
+
|
|
6
|
+
import { reconcileThemeUiColorModeOnNavigation } from '../color-mode/index.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Keeps Theme UI color mode aligned after Next.js **client-side navigations** (same role as Gatsby
|
|
10
|
+
* `onRouteUpdate`). Do **not** run on initial mount: `reconcileThemeUiColorModeOnNavigation` syncs the
|
|
11
|
+
* DOM from `localStorage`, while Theme UI applies a toggle by updating React state first and writing
|
|
12
|
+
* `localStorage` in a `useEffect` — an eager reconcile can re-read stale `localStorage` and force
|
|
13
|
+
* the page back to the previous mode (e.g. stuck in dark).
|
|
14
|
+
*/
|
|
15
|
+
export function ChronogroveNextThemeUiColorModeRouteSync() {
|
|
16
|
+
const pathname = usePathname()
|
|
17
|
+
const previousPathname = useRef(null)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (previousPathname.current === null) {
|
|
21
|
+
previousPathname.current = pathname
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
if (previousPathname.current === pathname) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
previousPathname.current = pathname
|
|
28
|
+
reconcileThemeUiColorModeOnNavigation()
|
|
29
|
+
}, [pathname])
|
|
30
|
+
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Box } from '@theme-ui/components'
|
|
4
|
+
import { useColorMode } from 'theme-ui'
|
|
5
|
+
|
|
6
|
+
import isDarkMode from './helpers/isDarkMode.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fixed viewport layer behind page content (`z-index: 0`), matching the role of
|
|
10
|
+
* `AnimatedPageBackground` on the Gatsby home: a real surface under `z-index: 1` UI so
|
|
11
|
+
* frosted panels (`backdrop-filter` + `panel-background`) have something to blur and tint
|
|
12
|
+
* against. The full site uses WebGL Color Bends there; this package ships a **lightweight**
|
|
13
|
+
* CSS gradient treatment only (no three.js).
|
|
14
|
+
*/
|
|
15
|
+
export function ChronogrovePageBackdrop() {
|
|
16
|
+
const [colorMode] = useColorMode()
|
|
17
|
+
const dark = isDarkMode(colorMode)
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<Box
|
|
21
|
+
aria-hidden
|
|
22
|
+
sx={{
|
|
23
|
+
position: 'fixed',
|
|
24
|
+
inset: 0,
|
|
25
|
+
width: '100vw',
|
|
26
|
+
minHeight: '100vh',
|
|
27
|
+
zIndex: 0,
|
|
28
|
+
pointerEvents: 'none',
|
|
29
|
+
bg: 'background',
|
|
30
|
+
// Dark: subtle primary/secondary glows — same intent as Color Bends, fraction of the cost.
|
|
31
|
+
backgroundImage: dark
|
|
32
|
+
? `
|
|
33
|
+
radial-gradient(ellipse 120% 85% at 50% -15%, rgba(74, 158, 255, 0.16) 0%, transparent 52%),
|
|
34
|
+
radial-gradient(ellipse 90% 70% at 95% 85%, rgba(113, 30, 155, 0.12) 0%, transparent 48%),
|
|
35
|
+
radial-gradient(ellipse 70% 50% at 10% 60%, rgba(128, 0, 128, 0.08) 0%, transparent 45%)
|
|
36
|
+
`
|
|
37
|
+
: 'none',
|
|
38
|
+
transition: 'background-image 0.3s ease'
|
|
39
|
+
}}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { render } from '@testing-library/react'
|
|
6
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
7
|
+
|
|
8
|
+
import chronogroveTheme from './theme.js'
|
|
9
|
+
import { ChronogrovePageBackdrop } from './page-backdrop.js'
|
|
10
|
+
|
|
11
|
+
describe('ChronogrovePageBackdrop', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
window.localStorage.removeItem('theme-ui-color-mode')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders a fixed backdrop layer', () => {
|
|
17
|
+
const { container } = render(
|
|
18
|
+
<ThemeUIProvider theme={chronogroveTheme}>
|
|
19
|
+
<ChronogrovePageBackdrop />
|
|
20
|
+
</ThemeUIProvider>
|
|
21
|
+
)
|
|
22
|
+
const layer = container.querySelector('[aria-hidden="true"]')
|
|
23
|
+
expect(layer).toBeTruthy()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('adds gradient overlays in dark mode', () => {
|
|
27
|
+
window.localStorage.setItem('theme-ui-color-mode', 'dark')
|
|
28
|
+
const { container } = render(
|
|
29
|
+
<ThemeUIProvider
|
|
30
|
+
theme={{
|
|
31
|
+
...chronogroveTheme,
|
|
32
|
+
config: { useLocalStorage: true, useColorSchemeMediaQuery: false }
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
<ChronogrovePageBackdrop />
|
|
36
|
+
</ThemeUIProvider>
|
|
37
|
+
)
|
|
38
|
+
const layer = container.querySelector('[aria-hidden="true"]')
|
|
39
|
+
expect(window.getComputedStyle(layer).backgroundImage).toMatch(/radial-gradient/)
|
|
40
|
+
})
|
|
41
|
+
})
|
package/src/pagination-button.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** @jsx jsx */
|
|
2
1
|
import React from 'react'
|
|
3
|
-
import {
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
import { useThemeUI } from 'theme-ui'
|
|
4
4
|
import isDarkMode from './helpers/isDarkMode.js'
|
|
5
5
|
import { hexToRgb } from './color-utils.js'
|
|
6
6
|
|
|
@@ -97,9 +97,9 @@ const PaginationButton = ({
|
|
|
97
97
|
)
|
|
98
98
|
|
|
99
99
|
return (
|
|
100
|
-
<button type='button' onClick={onClick} disabled={disabled} sx={baseStyles} {...props}>
|
|
100
|
+
<Box as='button' type='button' onClick={onClick} disabled={disabled} sx={baseStyles} {...props}>
|
|
101
101
|
{content}
|
|
102
|
-
</
|
|
102
|
+
</Box>
|
|
103
103
|
)
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
/** @jsx jsx */
|
|
2
|
-
import { jsx } from 'theme-ui'
|
|
3
1
|
import { render, screen, fireEvent } from '@testing-library/react'
|
|
4
2
|
import { ThemeUIProvider } from 'theme-ui'
|
|
5
3
|
|
|
@@ -96,6 +94,20 @@ describe('PaginationButton', () => {
|
|
|
96
94
|
})
|
|
97
95
|
})
|
|
98
96
|
|
|
97
|
+
it('uses dark secondary palette when color mode is dark', () => {
|
|
98
|
+
mockUseThemeUI.mockReturnValueOnce({
|
|
99
|
+
colorMode: 'dark',
|
|
100
|
+
theme: {
|
|
101
|
+
colors: {
|
|
102
|
+
primary: BUTTON_PRIMARY_COLORS.light,
|
|
103
|
+
primaryRgb: '66, 46, 163'
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
renderWithProviders(<PaginationButton variant='secondary'>2</PaginationButton>)
|
|
108
|
+
expect(screen.getByRole('button', { name: /2/i })).toHaveStyle({ color: '#888' })
|
|
109
|
+
})
|
|
110
|
+
|
|
99
111
|
it('applies small size styles', () => {
|
|
100
112
|
renderWithProviders(<PaginationButton size='small'>1</PaginationButton>)
|
|
101
113
|
|
|
@@ -118,6 +130,18 @@ describe('PaginationButton', () => {
|
|
|
118
130
|
})
|
|
119
131
|
})
|
|
120
132
|
|
|
133
|
+
it('falls back to medium size when size is invalid', () => {
|
|
134
|
+
renderWithProviders(<PaginationButton size='invalid'>1</PaginationButton>)
|
|
135
|
+
|
|
136
|
+
const button = screen.getByRole('button', { name: /1/i })
|
|
137
|
+
expect(button).toHaveStyle({
|
|
138
|
+
fontSize: '11px',
|
|
139
|
+
minWidth: '28px',
|
|
140
|
+
height: '28px',
|
|
141
|
+
padding: '6px 10px'
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
121
145
|
it('renders with icon', () => {
|
|
122
146
|
const TestIcon = () => <span data-testid='test-icon'>←</span>
|
|
123
147
|
renderWithProviders(<PaginationButton icon={<TestIcon />}>Prev</PaginationButton>)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
|
|
4
|
+
import PaginationButton from './pagination-button.js'
|
|
5
|
+
import { ChevronLeftIcon, ChevronRightIcon } from './chevron-icons.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Full pagination control (prev/next + page numbers, or simple mode).
|
|
9
|
+
* Default prev/next icons are inline SVGs; pass `prevIcon` / `nextIcon` to override.
|
|
10
|
+
*/
|
|
11
|
+
const Pagination = ({
|
|
12
|
+
currentPage,
|
|
13
|
+
totalPages,
|
|
14
|
+
onPageChange,
|
|
15
|
+
variant = 'primary',
|
|
16
|
+
size = 'medium',
|
|
17
|
+
showPageInfo = true,
|
|
18
|
+
maxVisiblePages = 5,
|
|
19
|
+
simple = false,
|
|
20
|
+
prevIcon = <ChevronLeftIcon />,
|
|
21
|
+
nextIcon = <ChevronRightIcon />
|
|
22
|
+
}) => {
|
|
23
|
+
const goToPage = page => {
|
|
24
|
+
if (page >= 1 && page <= totalPages && page !== currentPage) {
|
|
25
|
+
onPageChange(page)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const goToPrevious = () => goToPage(currentPage - 1)
|
|
30
|
+
const goToNext = () => goToPage(currentPage + 1)
|
|
31
|
+
|
|
32
|
+
const getVisiblePages = () => {
|
|
33
|
+
const pages = []
|
|
34
|
+
const halfVisible = Math.floor(maxVisiblePages / 2)
|
|
35
|
+
|
|
36
|
+
let startPage = Math.max(1, currentPage - halfVisible)
|
|
37
|
+
let endPage = Math.min(totalPages, currentPage + halfVisible)
|
|
38
|
+
|
|
39
|
+
if (endPage - startPage + 1 < maxVisiblePages) {
|
|
40
|
+
if (startPage === 1) {
|
|
41
|
+
endPage = Math.min(totalPages, startPage + maxVisiblePages - 1)
|
|
42
|
+
} else {
|
|
43
|
+
startPage = Math.max(1, endPage - maxVisiblePages + 1)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
48
|
+
pages.push(i)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return pages
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (totalPages <= 1) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const visiblePages = getVisiblePages()
|
|
59
|
+
|
|
60
|
+
if (simple) {
|
|
61
|
+
return (
|
|
62
|
+
<Box sx={{ width: '100%' }}>
|
|
63
|
+
<Box
|
|
64
|
+
sx={{
|
|
65
|
+
display: 'flex',
|
|
66
|
+
justifyContent: 'center',
|
|
67
|
+
alignItems: 'center',
|
|
68
|
+
gap: 1.5
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<PaginationButton
|
|
72
|
+
onClick={goToPrevious}
|
|
73
|
+
disabled={currentPage === 1}
|
|
74
|
+
variant={variant}
|
|
75
|
+
size={size}
|
|
76
|
+
icon={prevIcon}
|
|
77
|
+
aria-label='Previous page'
|
|
78
|
+
/>
|
|
79
|
+
{showPageInfo && (
|
|
80
|
+
<Box as='span' sx={{ fontSize: 0, color: 'textMuted' }}>
|
|
81
|
+
{currentPage}/{totalPages}
|
|
82
|
+
</Box>
|
|
83
|
+
)}
|
|
84
|
+
<PaginationButton
|
|
85
|
+
onClick={goToNext}
|
|
86
|
+
disabled={currentPage === totalPages}
|
|
87
|
+
variant={variant}
|
|
88
|
+
size={size}
|
|
89
|
+
icon={nextIcon}
|
|
90
|
+
aria-label='Next page'
|
|
91
|
+
/>
|
|
92
|
+
</Box>
|
|
93
|
+
</Box>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Box sx={{ width: '100%' }}>
|
|
99
|
+
<Box
|
|
100
|
+
sx={{
|
|
101
|
+
display: 'flex',
|
|
102
|
+
justifyContent: 'center',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
mt: 4,
|
|
105
|
+
gap: [1, 2]
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<PaginationButton
|
|
109
|
+
onClick={goToPrevious}
|
|
110
|
+
disabled={currentPage === 1}
|
|
111
|
+
variant={variant}
|
|
112
|
+
size={size}
|
|
113
|
+
icon={prevIcon}
|
|
114
|
+
aria-label='Previous page'
|
|
115
|
+
/>
|
|
116
|
+
|
|
117
|
+
<Box
|
|
118
|
+
sx={{
|
|
119
|
+
display: 'flex',
|
|
120
|
+
alignItems: 'center',
|
|
121
|
+
gap: 1,
|
|
122
|
+
mx: 2
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{visiblePages[0] > 1 && (
|
|
126
|
+
<>
|
|
127
|
+
<PaginationButton onClick={() => goToPage(1)} variant={variant} size={size} aria-label='Go to page 1'>
|
|
128
|
+
1
|
|
129
|
+
</PaginationButton>
|
|
130
|
+
{visiblePages[0] > 2 && (
|
|
131
|
+
<Box as='span' sx={{ color: 'textMuted', fontSize: '12px', px: 1 }}>
|
|
132
|
+
...
|
|
133
|
+
</Box>
|
|
134
|
+
)}
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{visiblePages.map(page => (
|
|
139
|
+
<PaginationButton
|
|
140
|
+
key={page}
|
|
141
|
+
onClick={() => goToPage(page)}
|
|
142
|
+
active={page === currentPage}
|
|
143
|
+
variant={variant}
|
|
144
|
+
size={size}
|
|
145
|
+
aria-label={`Go to page ${page}`}
|
|
146
|
+
aria-current={page === currentPage ? 'page' : undefined}
|
|
147
|
+
>
|
|
148
|
+
{page}
|
|
149
|
+
</PaginationButton>
|
|
150
|
+
))}
|
|
151
|
+
|
|
152
|
+
{visiblePages[visiblePages.length - 1] < totalPages && (
|
|
153
|
+
<>
|
|
154
|
+
{visiblePages[visiblePages.length - 1] < totalPages - 1 && (
|
|
155
|
+
<Box as='span' sx={{ color: 'textMuted', fontSize: '12px', px: 1 }}>
|
|
156
|
+
...
|
|
157
|
+
</Box>
|
|
158
|
+
)}
|
|
159
|
+
<PaginationButton
|
|
160
|
+
onClick={() => goToPage(totalPages)}
|
|
161
|
+
variant={variant}
|
|
162
|
+
size={size}
|
|
163
|
+
aria-label={`Go to page ${totalPages}`}
|
|
164
|
+
>
|
|
165
|
+
{totalPages}
|
|
166
|
+
</PaginationButton>
|
|
167
|
+
</>
|
|
168
|
+
)}
|
|
169
|
+
</Box>
|
|
170
|
+
|
|
171
|
+
<PaginationButton
|
|
172
|
+
onClick={goToNext}
|
|
173
|
+
disabled={currentPage === totalPages}
|
|
174
|
+
variant={variant}
|
|
175
|
+
size={size}
|
|
176
|
+
icon={nextIcon}
|
|
177
|
+
aria-label='Next page'
|
|
178
|
+
/>
|
|
179
|
+
</Box>
|
|
180
|
+
|
|
181
|
+
{showPageInfo && (
|
|
182
|
+
<Box
|
|
183
|
+
sx={{
|
|
184
|
+
textAlign: 'center',
|
|
185
|
+
mt: 2,
|
|
186
|
+
fontSize: 0,
|
|
187
|
+
color: 'textMuted',
|
|
188
|
+
display: 'block'
|
|
189
|
+
}}
|
|
190
|
+
>
|
|
191
|
+
Page {currentPage} of {totalPages}
|
|
192
|
+
</Box>
|
|
193
|
+
)}
|
|
194
|
+
</Box>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export default Pagination
|