@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,281 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
2
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
3
|
+
|
|
4
|
+
import Pagination from './pagination.js'
|
|
5
|
+
|
|
6
|
+
const mockTheme = {
|
|
7
|
+
colors: {
|
|
8
|
+
primary: '#422EA3',
|
|
9
|
+
primaryRgb: '66, 46, 163',
|
|
10
|
+
modes: {
|
|
11
|
+
dark: {
|
|
12
|
+
text: '#ffffff',
|
|
13
|
+
background: '#000000'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const renderWithProviders = component => {
|
|
20
|
+
return render(<ThemeUIProvider theme={mockTheme}>{component}</ThemeUIProvider>)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('Pagination', () => {
|
|
24
|
+
const defaultProps = {
|
|
25
|
+
currentPage: 1,
|
|
26
|
+
totalPages: 5,
|
|
27
|
+
onPageChange: jest.fn()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
jest.clearAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('renders pagination controls', () => {
|
|
35
|
+
renderWithProviders(<Pagination {...defaultProps} />)
|
|
36
|
+
|
|
37
|
+
expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
|
|
38
|
+
expect(screen.getByLabelText('Next page')).toBeInTheDocument()
|
|
39
|
+
expect(screen.getByText('1')).toBeInTheDocument()
|
|
40
|
+
expect(screen.getByText('2')).toBeInTheDocument()
|
|
41
|
+
expect(screen.getByText('3')).toBeInTheDocument()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('renders page info', () => {
|
|
45
|
+
renderWithProviders(<Pagination {...defaultProps} />)
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText('Page 1 of 5')).toBeInTheDocument()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('hides page info when showPageInfo is false', () => {
|
|
51
|
+
renderWithProviders(<Pagination {...defaultProps} showPageInfo={false} />)
|
|
52
|
+
|
|
53
|
+
expect(screen.queryByText('Page 1 of 5')).not.toBeInTheDocument()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('handles page changes', () => {
|
|
57
|
+
renderWithProviders(<Pagination {...defaultProps} />)
|
|
58
|
+
|
|
59
|
+
const page2Button = screen.getByText('2')
|
|
60
|
+
fireEvent.click(page2Button)
|
|
61
|
+
|
|
62
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('does not call onPageChange when clicking the already active page', () => {
|
|
66
|
+
renderWithProviders(<Pagination {...defaultProps} currentPage={2} />)
|
|
67
|
+
jest.clearAllMocks()
|
|
68
|
+
fireEvent.click(screen.getByText('2'))
|
|
69
|
+
expect(defaultProps.onPageChange).not.toHaveBeenCalled()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('handles previous button click', () => {
|
|
73
|
+
renderWithProviders(<Pagination {...defaultProps} currentPage={2} />)
|
|
74
|
+
|
|
75
|
+
const prevButton = screen.getByLabelText('Previous page')
|
|
76
|
+
fireEvent.click(prevButton)
|
|
77
|
+
|
|
78
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('handles next button click', () => {
|
|
82
|
+
renderWithProviders(<Pagination {...defaultProps} />)
|
|
83
|
+
|
|
84
|
+
const nextButton = screen.getByLabelText('Next page')
|
|
85
|
+
fireEvent.click(nextButton)
|
|
86
|
+
|
|
87
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(2)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('disables previous button on first page', () => {
|
|
91
|
+
renderWithProviders(<Pagination {...defaultProps} currentPage={1} />)
|
|
92
|
+
|
|
93
|
+
const prevButton = screen.getByLabelText('Previous page')
|
|
94
|
+
expect(prevButton).toBeDisabled()
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('disables next button on last page', () => {
|
|
98
|
+
renderWithProviders(<Pagination {...defaultProps} currentPage={5} />)
|
|
99
|
+
|
|
100
|
+
const nextButton = screen.getByLabelText('Next page')
|
|
101
|
+
expect(nextButton).toBeDisabled()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('highlights current page', () => {
|
|
105
|
+
renderWithProviders(<Pagination {...defaultProps} currentPage={3} />)
|
|
106
|
+
|
|
107
|
+
const currentPageButton = screen.getByText('3')
|
|
108
|
+
expect(currentPageButton).toHaveAttribute('aria-current', 'page')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('shows ellipsis for large page ranges', () => {
|
|
112
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={5} maxVisiblePages={3} />)
|
|
113
|
+
|
|
114
|
+
expect(screen.getAllByText('...')).toHaveLength(2)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('shows first and last pages when needed', () => {
|
|
118
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={8} maxVisiblePages={3} />)
|
|
119
|
+
|
|
120
|
+
expect(screen.getByText('1')).toBeInTheDocument()
|
|
121
|
+
expect(screen.getByText('10')).toBeInTheDocument()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('returns null when totalPages is 1', () => {
|
|
125
|
+
const { container } = renderWithProviders(<Pagination {...defaultProps} totalPages={1} />)
|
|
126
|
+
|
|
127
|
+
expect(container.firstChild).toBeNull()
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('returns null when totalPages is 0', () => {
|
|
131
|
+
const { container } = renderWithProviders(<Pagination {...defaultProps} totalPages={0} />)
|
|
132
|
+
|
|
133
|
+
expect(container.firstChild).toBeNull()
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
it('applies custom variant', () => {
|
|
137
|
+
renderWithProviders(<Pagination {...defaultProps} variant='secondary' currentPage={2} />)
|
|
138
|
+
|
|
139
|
+
const pageButton = screen.getByText('1')
|
|
140
|
+
expect(pageButton).toHaveStyle({
|
|
141
|
+
color: 'rgb(102, 102, 102)'
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('applies custom size', () => {
|
|
146
|
+
renderWithProviders(<Pagination {...defaultProps} size='large' />)
|
|
147
|
+
|
|
148
|
+
const pageButton = screen.getByText('1')
|
|
149
|
+
expect(pageButton).toHaveStyle({
|
|
150
|
+
fontSize: '12px',
|
|
151
|
+
minWidth: '32px',
|
|
152
|
+
height: '32px'
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('shows ellipsis when gap is exactly 2 pages from start', () => {
|
|
157
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={4} maxVisiblePages={3} />)
|
|
158
|
+
|
|
159
|
+
const ellipsis = screen.getAllByText('...')
|
|
160
|
+
expect(ellipsis.length).toBeGreaterThan(0)
|
|
161
|
+
expect(screen.getByText('1')).toBeInTheDocument()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('shows ellipsis when gap is exactly 2 pages from end', () => {
|
|
165
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={7} maxVisiblePages={3} />)
|
|
166
|
+
|
|
167
|
+
const ellipsis = screen.getAllByText('...')
|
|
168
|
+
expect(ellipsis.length).toBeGreaterThan(0)
|
|
169
|
+
expect(screen.getByText('10')).toBeInTheDocument()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('handles clicking first page button when first page is not visible', () => {
|
|
173
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={5} maxVisiblePages={3} />)
|
|
174
|
+
|
|
175
|
+
const firstPageButton = screen.getByLabelText('Go to page 1')
|
|
176
|
+
fireEvent.click(firstPageButton)
|
|
177
|
+
|
|
178
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('handles clicking last page button when last page is not visible', () => {
|
|
182
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={5} maxVisiblePages={3} />)
|
|
183
|
+
|
|
184
|
+
const lastPageButton = screen.getByLabelText('Go to page 10')
|
|
185
|
+
fireEvent.click(lastPageButton)
|
|
186
|
+
|
|
187
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(10)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('uses custom prev and next icons when provided', () => {
|
|
191
|
+
renderWithProviders(
|
|
192
|
+
<Pagination
|
|
193
|
+
{...defaultProps}
|
|
194
|
+
prevIcon={<span data-testid='prev-custom'>«</span>}
|
|
195
|
+
nextIcon={<span data-testid='next-custom'>»</span>}
|
|
196
|
+
/>
|
|
197
|
+
)
|
|
198
|
+
expect(screen.getByTestId('prev-custom')).toBeInTheDocument()
|
|
199
|
+
expect(screen.getByTestId('next-custom')).toBeInTheDocument()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('expands page window from the end when the range is narrower than maxVisiblePages', () => {
|
|
203
|
+
renderWithProviders(<Pagination {...defaultProps} totalPages={10} currentPage={10} maxVisiblePages={5} />)
|
|
204
|
+
expect(screen.getByLabelText('Go to page 10')).toBeInTheDocument()
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
describe('Simple mode', () => {
|
|
208
|
+
it('renders only prev/next buttons in simple mode', () => {
|
|
209
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} />)
|
|
210
|
+
|
|
211
|
+
expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
|
|
212
|
+
expect(screen.getByLabelText('Next page')).toBeInTheDocument()
|
|
213
|
+
expect(screen.queryByText('1')).not.toBeInTheDocument()
|
|
214
|
+
expect(screen.queryByText('2')).not.toBeInTheDocument()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('shows page counter in simple mode when showPageInfo is true', () => {
|
|
218
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} showPageInfo={true} />)
|
|
219
|
+
|
|
220
|
+
expect(screen.getByText('1/5')).toBeInTheDocument()
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('hides page counter in simple mode when showPageInfo is false', () => {
|
|
224
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} showPageInfo={false} />)
|
|
225
|
+
|
|
226
|
+
expect(screen.queryByText('1/5')).not.toBeInTheDocument()
|
|
227
|
+
expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
|
|
228
|
+
expect(screen.getByLabelText('Next page')).toBeInTheDocument()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('handles previous button click in simple mode', () => {
|
|
232
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} currentPage={2} />)
|
|
233
|
+
|
|
234
|
+
const prevButton = screen.getByLabelText('Previous page')
|
|
235
|
+
fireEvent.click(prevButton)
|
|
236
|
+
|
|
237
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(1)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it('handles next button click in simple mode', () => {
|
|
241
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} />)
|
|
242
|
+
|
|
243
|
+
const nextButton = screen.getByLabelText('Next page')
|
|
244
|
+
fireEvent.click(nextButton)
|
|
245
|
+
|
|
246
|
+
expect(defaultProps.onPageChange).toHaveBeenCalledWith(2)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('disables previous button on first page in simple mode', () => {
|
|
250
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} currentPage={1} />)
|
|
251
|
+
|
|
252
|
+
const prevButton = screen.getByLabelText('Previous page')
|
|
253
|
+
expect(prevButton).toBeDisabled()
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('disables next button on last page in simple mode', () => {
|
|
257
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} currentPage={5} />)
|
|
258
|
+
|
|
259
|
+
const nextButton = screen.getByLabelText('Next page')
|
|
260
|
+
expect(nextButton).toBeDisabled()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('applies custom variant in simple mode', () => {
|
|
264
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} variant='secondary' />)
|
|
265
|
+
|
|
266
|
+
expect(screen.getByLabelText('Previous page')).toBeInTheDocument()
|
|
267
|
+
expect(screen.getByLabelText('Next page')).toBeInTheDocument()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('applies custom size in simple mode', () => {
|
|
271
|
+
renderWithProviders(<Pagination {...defaultProps} simple={true} size='small' />)
|
|
272
|
+
|
|
273
|
+
const prevButton = screen.getByLabelText('Previous page')
|
|
274
|
+
expect(prevButton).toHaveStyle({
|
|
275
|
+
fontSize: '10px',
|
|
276
|
+
minWidth: '24px',
|
|
277
|
+
height: '24px'
|
|
278
|
+
})
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import React, { forwardRef } from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
import { useThemeUI } from 'theme-ui'
|
|
4
4
|
import isDarkMode from '../helpers/isDarkMode.js'
|
|
5
5
|
|
|
6
6
|
const SkipNavLink = forwardRef(function SkipNavLink(
|
|
@@ -13,7 +13,8 @@ const SkipNavLink = forwardRef(function SkipNavLink(
|
|
|
13
13
|
const primaryRgb = theme?.colors?.primaryRgb ?? (darkModeActive ? '74, 158, 255' : '66, 46, 163')
|
|
14
14
|
|
|
15
15
|
return (
|
|
16
|
-
<
|
|
16
|
+
<Box
|
|
17
|
+
as={Comp}
|
|
17
18
|
{...props}
|
|
18
19
|
ref={forwardedRef}
|
|
19
20
|
href={`#${contentId}`}
|
|
@@ -63,7 +64,7 @@ const SkipNavLink = forwardRef(function SkipNavLink(
|
|
|
63
64
|
}}
|
|
64
65
|
>
|
|
65
66
|
{children}
|
|
66
|
-
</
|
|
67
|
+
</Box>
|
|
67
68
|
)
|
|
68
69
|
})
|
|
69
70
|
|
|
@@ -30,6 +30,17 @@ describe('SkipNavLink', () => {
|
|
|
30
30
|
expect(link).toHaveAttribute('data-skip-nav-link', '')
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
+
it('respects custom contentId for the hash target', () => {
|
|
34
|
+
renderLink(<SkipNavLink contentId='main'>Jump</SkipNavLink>)
|
|
35
|
+
expect(screen.getByRole('link', { name: /jump/i })).toHaveAttribute('href', '#main')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('uses default label and skip target when props are omitted', () => {
|
|
39
|
+
renderLink(<SkipNavLink />)
|
|
40
|
+
const link = screen.getByRole('link', { name: /skip to content/i })
|
|
41
|
+
expect(link).toHaveAttribute('href', '#skip-nav-content')
|
|
42
|
+
})
|
|
43
|
+
|
|
33
44
|
it('uses fallback colors when theme omits primary tokens', () => {
|
|
34
45
|
render(
|
|
35
46
|
<ThemeUIProvider theme={{ colors: {} }}>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Card } from '@theme-ui/components'
|
|
3
|
+
import { useThemeUI } from 'theme-ui'
|
|
4
|
+
|
|
5
|
+
import isDarkMode from './helpers/isDarkMode.js'
|
|
6
|
+
|
|
7
|
+
const StatusCard = ({ message, ...props }) => {
|
|
8
|
+
const { colorMode } = useThemeUI()
|
|
9
|
+
const variant = isDarkMode(colorMode) ? 'metricCardDark' : 'metricCard'
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<Card variant={variant} sx={{ mb: 3 }} {...props}>
|
|
13
|
+
{message}
|
|
14
|
+
</Card>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default StatusCard
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { ThemeUIProvider } from 'theme-ui'
|
|
3
|
+
|
|
4
|
+
import StatusCard from './status-card.js'
|
|
5
|
+
|
|
6
|
+
jest.mock('theme-ui', () => {
|
|
7
|
+
const actual = jest.requireActual('theme-ui')
|
|
8
|
+
return {
|
|
9
|
+
...actual,
|
|
10
|
+
useThemeUI: jest.fn(() => ({ colorMode: 'default' }))
|
|
11
|
+
}
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const { useThemeUI } = require('theme-ui')
|
|
15
|
+
|
|
16
|
+
const theme = {}
|
|
17
|
+
|
|
18
|
+
describe('StatusCard', () => {
|
|
19
|
+
it('renders message in light mode variant', () => {
|
|
20
|
+
useThemeUI.mockReturnValue({ colorMode: 'default' })
|
|
21
|
+
render(
|
|
22
|
+
<ThemeUIProvider theme={theme}>
|
|
23
|
+
<StatusCard message='No data' />
|
|
24
|
+
</ThemeUIProvider>
|
|
25
|
+
)
|
|
26
|
+
expect(screen.getByText('No data')).toBeInTheDocument()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('uses dark card variant when dark', () => {
|
|
30
|
+
useThemeUI.mockReturnValue({ colorMode: 'dark' })
|
|
31
|
+
render(
|
|
32
|
+
<ThemeUIProvider theme={theme}>
|
|
33
|
+
<StatusCard message='Err' />
|
|
34
|
+
</ThemeUIProvider>
|
|
35
|
+
)
|
|
36
|
+
expect(screen.getByText('Err')).toBeInTheDocument()
|
|
37
|
+
})
|
|
38
|
+
})
|
package/src/theme.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { tailwind } from '@theme-ui/presets'
|
|
2
2
|
import { merge } from 'theme-ui'
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
chronogroveThemeSurfaceColorsDark,
|
|
6
|
+
chronogroveThemeSurfaceColorsLight
|
|
7
|
+
} from './chronogrove-theme-surface-colors.js'
|
|
8
|
+
|
|
4
9
|
const fonts = {
|
|
5
10
|
sans: '-apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue, helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif',
|
|
6
11
|
serif:
|
|
@@ -74,8 +79,8 @@ export const backdropBlurLight = {
|
|
|
74
79
|
|
|
75
80
|
export const card = {
|
|
76
81
|
borderRadius: 'card',
|
|
77
|
-
|
|
78
|
-
color: '
|
|
82
|
+
bg: 'panel-background',
|
|
83
|
+
color: 'text',
|
|
79
84
|
boxShadow: 'default',
|
|
80
85
|
flexGrow: 1,
|
|
81
86
|
padding: 3,
|
|
@@ -86,22 +91,18 @@ export const card = {
|
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
export const metricCard = {
|
|
89
|
-
backgroundColor: '
|
|
94
|
+
backgroundColor: 'panel-background',
|
|
90
95
|
boxShadow: 'none',
|
|
91
|
-
color: '
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
padding: 2
|
|
96
|
-
}
|
|
96
|
+
color: 'text',
|
|
97
|
+
/** Stat tiles read clearly on animated / busy page backgrounds */
|
|
98
|
+
border: '1px solid',
|
|
99
|
+
borderColor: 'panel-divider'
|
|
97
100
|
}
|
|
98
101
|
|
|
99
102
|
export const PostCard = {
|
|
100
103
|
...card,
|
|
101
104
|
...floatOnHover,
|
|
102
105
|
...glassmorhismPanel,
|
|
103
|
-
backgroundColor: 'var(--theme-ui-colors-panel-background)',
|
|
104
|
-
color: 'var(--theme-ui-colors-panel-text)',
|
|
105
106
|
display: 'flex',
|
|
106
107
|
height: '100%',
|
|
107
108
|
flexDirection: 'column',
|
|
@@ -253,6 +254,12 @@ export default merge(tailwind, {
|
|
|
253
254
|
...metricCard
|
|
254
255
|
},
|
|
255
256
|
|
|
257
|
+
metricCardDark: {
|
|
258
|
+
...card,
|
|
259
|
+
...metricCard,
|
|
260
|
+
backgroundColor: '#1e2530'
|
|
261
|
+
},
|
|
262
|
+
|
|
256
263
|
/* The following styles represent specific card components, indicated in PascalCase. */
|
|
257
264
|
|
|
258
265
|
UserProfile: {
|
|
@@ -369,20 +376,16 @@ export default merge(tailwind, {
|
|
|
369
376
|
|
|
370
377
|
colors: {
|
|
371
378
|
accent: 'deeppink',
|
|
372
|
-
|
|
373
|
-
'panel-background': 'rgba(255, 255, 255, 0.45)',
|
|
379
|
+
...chronogroveThemeSurfaceColorsLight,
|
|
374
380
|
'panel-divider': () => '1px solid rgba(255, 229, 224, 0.17)',
|
|
375
381
|
'panel-highlight': theme => theme.colors.gray[1],
|
|
376
382
|
modes: {
|
|
377
383
|
dark: {
|
|
378
|
-
|
|
379
|
-
'panel-background': 'rgba(20, 20, 31, 0.45)',
|
|
384
|
+
...chronogroveThemeSurfaceColorsDark,
|
|
380
385
|
'panel-divider': theme => `1px solid ${theme.colors.gray[8]}`,
|
|
381
386
|
'panel-highlight': theme => theme.colors.gray[8],
|
|
382
387
|
primary: '#4a9eff',
|
|
383
388
|
primaryRgb: '74, 158, 255',
|
|
384
|
-
text: '#fff',
|
|
385
|
-
textMuted: '#d8d8d8',
|
|
386
389
|
tableText: '#fff',
|
|
387
390
|
tableBackground: 'rgba(30, 30, 47, 0.45)',
|
|
388
391
|
tableHeaderBackground: 'rgba(30, 37, 48, 0.8)',
|
|
@@ -400,9 +403,7 @@ export default merge(tailwind, {
|
|
|
400
403
|
tableHeaderBackground: '#f4f4f9',
|
|
401
404
|
tableRowBackground: 'transparent',
|
|
402
405
|
tableRowAlternateBackground: '#fafafa',
|
|
403
|
-
tableBorder: 'muted'
|
|
404
|
-
text: '#111',
|
|
405
|
-
textMuted: '#333'
|
|
406
|
+
tableBorder: 'muted'
|
|
406
407
|
},
|
|
407
408
|
|
|
408
409
|
fonts: {
|
|
@@ -566,6 +567,12 @@ export default merge(tailwind, {
|
|
|
566
567
|
mt: 2
|
|
567
568
|
},
|
|
568
569
|
|
|
570
|
+
mutedCardFooter: {
|
|
571
|
+
display: 'flex',
|
|
572
|
+
justifyContent: 'space-between',
|
|
573
|
+
mt: 2
|
|
574
|
+
},
|
|
575
|
+
|
|
569
576
|
PageFooter: {
|
|
570
577
|
zIndex: '10',
|
|
571
578
|
color: 'text',
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box } from '@theme-ui/components'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default loading indicator for widget CTAs (SVG spinner, no extra dependencies).
|
|
6
|
+
*/
|
|
7
|
+
export const WidgetCtaLoadingIndicator = () => (
|
|
8
|
+
<Box
|
|
9
|
+
as='svg'
|
|
10
|
+
role='status'
|
|
11
|
+
aria-label='Loading'
|
|
12
|
+
width='24'
|
|
13
|
+
height='24'
|
|
14
|
+
viewBox='0 0 24 24'
|
|
15
|
+
sx={{
|
|
16
|
+
verticalAlign: 'middle',
|
|
17
|
+
color: 'primary',
|
|
18
|
+
animation: 'spin 0.8s linear infinite',
|
|
19
|
+
'@keyframes spin': { to: { transform: 'rotate(360deg)' } }
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<circle
|
|
23
|
+
cx='12'
|
|
24
|
+
cy='12'
|
|
25
|
+
r='10'
|
|
26
|
+
fill='none'
|
|
27
|
+
stroke='currentColor'
|
|
28
|
+
strokeWidth='3'
|
|
29
|
+
strokeLinecap='round'
|
|
30
|
+
strokeDasharray='31.4 31.4'
|
|
31
|
+
/>
|
|
32
|
+
</Box>
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
const linkSxBase = {
|
|
36
|
+
variant: 'links.widgetCta',
|
|
37
|
+
'.read-more-icon': {
|
|
38
|
+
opacity: 0,
|
|
39
|
+
transition: 'all .3s ease',
|
|
40
|
+
ml: 1
|
|
41
|
+
},
|
|
42
|
+
'&:hover .read-more-icon, &:focus .read-more-icon': {
|
|
43
|
+
opacity: 1
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Text-style CTA used next to widget headlines. Pass `linkComponent` (e.g. Gatsby `Link` with `to`, or Next `Link` with `href`).
|
|
49
|
+
* External links: set `href` or `url` (no `linkComponent`).
|
|
50
|
+
*/
|
|
51
|
+
export const WidgetCallToAction = ({
|
|
52
|
+
children,
|
|
53
|
+
isLoading = false,
|
|
54
|
+
loadingSlot,
|
|
55
|
+
title,
|
|
56
|
+
to,
|
|
57
|
+
url,
|
|
58
|
+
href,
|
|
59
|
+
linkComponent,
|
|
60
|
+
sx: sxProp,
|
|
61
|
+
...rest
|
|
62
|
+
}) => {
|
|
63
|
+
const ctaSx = { ...linkSxBase, ...sxProp }
|
|
64
|
+
|
|
65
|
+
if (isLoading) {
|
|
66
|
+
return loadingSlot ?? <WidgetCtaLoadingIndicator />
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (linkComponent) {
|
|
70
|
+
const L = linkComponent
|
|
71
|
+
// Theme UI `sx` only applies when the outer node is a Theme UI `Box` (or other `sx`-aware
|
|
72
|
+
// primitive). Router links (Next.js `Link`, Gatsby `Link`) do not accept `sx` themselves.
|
|
73
|
+
if (href != null && href !== '') {
|
|
74
|
+
return (
|
|
75
|
+
<Box as={L} href={href} sx={ctaSx} title={title} {...rest}>
|
|
76
|
+
{children}
|
|
77
|
+
</Box>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
if (to) {
|
|
81
|
+
return (
|
|
82
|
+
<Box as={L} to={to} sx={ctaSx} title={title} {...rest}>
|
|
83
|
+
{children}
|
|
84
|
+
</Box>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (to) {
|
|
90
|
+
return (
|
|
91
|
+
<Box as='a' href={to} sx={ctaSx} title={title} {...rest}>
|
|
92
|
+
{children}
|
|
93
|
+
</Box>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const externalHref = href ?? url
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Box as='a' href={externalHref} sx={ctaSx} title={title} {...rest}>
|
|
101
|
+
{children}
|
|
102
|
+
</Box>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export default WidgetCallToAction
|