@chronogrove/ui 0.80.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.
@@ -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
+ })
@@ -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
@@ -94,11 +94,9 @@ export const metricCard = {
94
94
  backgroundColor: 'panel-background',
95
95
  boxShadow: 'none',
96
96
  color: 'text',
97
- span: {
98
- fontFamily: 'heading',
99
- fontWeight: 'bold',
100
- padding: 2
101
- }
97
+ /** Stat tiles read clearly on animated / busy page backgrounds */
98
+ border: '1px solid',
99
+ borderColor: 'panel-divider'
102
100
  }
103
101
 
104
102
  export const PostCard = {
@@ -256,6 +254,12 @@ export default merge(tailwind, {
256
254
  ...metricCard
257
255
  },
258
256
 
257
+ metricCardDark: {
258
+ ...card,
259
+ ...metricCard,
260
+ backgroundColor: '#1e2530'
261
+ },
262
+
259
263
  /* The following styles represent specific card components, indicated in PascalCase. */
260
264
 
261
265
  UserProfile: {
@@ -563,6 +567,12 @@ export default merge(tailwind, {
563
567
  mt: 2
564
568
  },
565
569
 
570
+ mutedCardFooter: {
571
+ display: 'flex',
572
+ justifyContent: 'space-between',
573
+ mt: 2
574
+ },
575
+
566
576
  PageFooter: {
567
577
  zIndex: '10',
568
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
@@ -0,0 +1,115 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import { ThemeUIProvider } from 'theme-ui'
3
+
4
+ import WidgetCallToAction, { WidgetCtaLoadingIndicator } from './widget-call-to-action.js'
5
+
6
+ const LinkMock = ({ to, href, children, ...rest }) => (
7
+ <a href={href ?? to} data-testid='router-link' {...rest}>
8
+ {children}
9
+ </a>
10
+ )
11
+
12
+ const theme = { links: { widgetCta: { color: 'text' } } }
13
+
14
+ const wrap = ui => render(<ThemeUIProvider theme={theme}>{ui}</ThemeUIProvider>)
15
+
16
+ describe('WidgetCtaLoadingIndicator', () => {
17
+ it('renders svg', () => {
18
+ const { container } = wrap(<WidgetCtaLoadingIndicator />)
19
+ expect(container.querySelector('svg')).toBeInTheDocument()
20
+ })
21
+ })
22
+
23
+ describe('WidgetCallToAction', () => {
24
+ it('renders external link with url', () => {
25
+ wrap(
26
+ <WidgetCallToAction title='t' url='https://example.com'>
27
+ Go
28
+ </WidgetCallToAction>
29
+ )
30
+ const a = screen.getByRole('link')
31
+ expect(a).toHaveAttribute('href', 'https://example.com')
32
+ })
33
+
34
+ it('prefers href over url', () => {
35
+ wrap(
36
+ <WidgetCallToAction href='https://a.test' url='https://b.test'>
37
+ Go
38
+ </WidgetCallToAction>
39
+ )
40
+ expect(screen.getByRole('link')).toHaveAttribute('href', 'https://a.test')
41
+ })
42
+
43
+ it('uses linkComponent with to (Gatsby-style)', () => {
44
+ wrap(
45
+ <WidgetCallToAction linkComponent={LinkMock} to='/blog'>
46
+ Posts
47
+ </WidgetCallToAction>
48
+ )
49
+ expect(screen.getByTestId('router-link')).toHaveAttribute('href', '/blog')
50
+ })
51
+
52
+ it('uses linkComponent with href (Next.js-style)', () => {
53
+ wrap(
54
+ <WidgetCallToAction linkComponent={LinkMock} href='/dashboard'>
55
+ Dash
56
+ </WidgetCallToAction>
57
+ )
58
+ expect(screen.getByTestId('router-link')).toHaveAttribute('href', '/dashboard')
59
+ })
60
+
61
+ it('uses linkComponent with to when href is empty', () => {
62
+ wrap(
63
+ <WidgetCallToAction linkComponent={LinkMock} href='' to='/blog'>
64
+ Posts
65
+ </WidgetCallToAction>
66
+ )
67
+ expect(screen.getByTestId('router-link')).toHaveAttribute('href', '/blog')
68
+ })
69
+
70
+ it('ignores linkComponent for external url when no href or to', () => {
71
+ wrap(
72
+ <WidgetCallToAction linkComponent={LinkMock} url='https://example.com/out'>
73
+ Out
74
+ </WidgetCallToAction>
75
+ )
76
+ expect(screen.queryByTestId('router-link')).not.toBeInTheDocument()
77
+ expect(screen.getByRole('link')).toHaveAttribute('href', 'https://example.com/out')
78
+ })
79
+
80
+ it('uses plain anchor when to without linkComponent', () => {
81
+ wrap(
82
+ <WidgetCallToAction to='/path' linkComponent={undefined}>
83
+ P
84
+ </WidgetCallToAction>
85
+ )
86
+ expect(screen.getByRole('link')).toHaveAttribute('href', '/path')
87
+ })
88
+
89
+ it('shows loading indicator', () => {
90
+ const { container } = wrap(
91
+ <WidgetCallToAction isLoading title='t'>
92
+ X
93
+ </WidgetCallToAction>
94
+ )
95
+ expect(container.querySelector('svg')).toBeInTheDocument()
96
+ })
97
+
98
+ it('uses custom loadingSlot', () => {
99
+ wrap(
100
+ <WidgetCallToAction isLoading loadingSlot={<span data-testid='ld'>wait</span>}>
101
+ X
102
+ </WidgetCallToAction>
103
+ )
104
+ expect(screen.getByTestId('ld')).toBeInTheDocument()
105
+ })
106
+
107
+ it('merges sx', () => {
108
+ wrap(
109
+ <WidgetCallToAction href='https://x.test' sx={{ m: 2 }}>
110
+ L
111
+ </WidgetCallToAction>
112
+ )
113
+ expect(screen.getByRole('link')).toBeInTheDocument()
114
+ })
115
+ })
@@ -0,0 +1,83 @@
1
+ import React from 'react'
2
+ import { Box } from '@theme-ui/components'
3
+ import { useThemeUI } from 'theme-ui'
4
+
5
+ import isDarkMode from './helpers/isDarkMode.js'
6
+
7
+ const sectionSx = {
8
+ mb: 4,
9
+ pt: [0, 3, 4],
10
+ pb: [0, 3, 4]
11
+ }
12
+
13
+ /**
14
+ * Wraps a dashboard widget: vertical spacing and optional fatal-error overlay.
15
+ */
16
+ const WidgetSection = ({ children, hasFatalError, id, styleOverrides = {}, ...props }) => {
17
+ const { colorMode } = useThemeUI()
18
+ const darkMode = isDarkMode(colorMode)
19
+
20
+ return (
21
+ <Box
22
+ as='section'
23
+ sx={{
24
+ ...sectionSx,
25
+ ...styleOverrides,
26
+ ...(hasFatalError
27
+ ? {
28
+ position: 'relative'
29
+ }
30
+ : {})
31
+ }}
32
+ {...(id ? { id } : {})}
33
+ {...props}
34
+ >
35
+ {hasFatalError && (
36
+ <Box
37
+ sx={{
38
+ alignItems: 'center',
39
+ bottom: 0,
40
+ display: 'flex',
41
+ justifyContent: 'center',
42
+ left: 0,
43
+ position: 'absolute',
44
+ right: 0,
45
+ top: 0
46
+ }}
47
+ >
48
+ <Box
49
+ sx={{
50
+ background: darkMode ? '#252e3c' : 'white',
51
+ borderLeft: '2px solid red',
52
+ borderRight: '2px solid red',
53
+ borderRadius: '2px',
54
+ boxShadow: 'xl',
55
+ py: 3,
56
+ px: 4,
57
+ zIndex: 480
58
+ }}
59
+ >
60
+ <h4>Something went wrong</h4>
61
+ <p>Failed to load this widget.</p>
62
+ </Box>
63
+ <Box
64
+ sx={{
65
+ top: 0,
66
+ right: 0,
67
+ bottom: 0,
68
+ left: 0,
69
+ background: darkMode
70
+ ? 'radial-gradient(rgba(14.5,18,23.5,0.4) 20%, transparent 50%);'
71
+ : 'radial-gradient(rgba(255, 255, 255, 0.4) 20%, transparent 50%)',
72
+ position: 'absolute',
73
+ zIndex: 470
74
+ }}
75
+ />
76
+ </Box>
77
+ )}
78
+ {children}
79
+ </Box>
80
+ )
81
+ }
82
+
83
+ export default WidgetSection