@tpzdsp/next-toolkit 1.7.0 → 1.9.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/package.json +25 -1
- package/src/components/Card/Card.stories.tsx +68 -41
- package/src/components/ErrorBoundary/ErrorFallback.stories.tsx +9 -7
- package/src/components/ErrorBoundary/ErrorFallback.test.tsx +66 -13
- package/src/components/ErrorBoundary/ErrorFallback.tsx +63 -23
- package/src/components/ErrorText/ErrorText.stories.tsx +43 -24
- package/src/components/ErrorText/ErrorText.tsx +4 -2
- package/src/components/Heading/Heading.tsx +6 -3
- package/src/components/Hint/Hint.stories.tsx +46 -28
- package/src/components/Paragraph/Paragraph.stories.tsx +65 -21
- package/src/components/Paragraph/Paragraph.tsx +3 -1
- package/src/components/SlidingPanel/SlidingPanel.stories.tsx +99 -0
- package/src/components/SlidingPanel/SlidingPanel.tsx +120 -111
- package/src/components/chip/Chip.stories.tsx +48 -26
- package/src/errors/ApiError.ts +8 -2
- package/src/http/constants.ts +2 -0
- package/src/map/LayerSwitcherControl.ts +47 -59
- package/src/map/MapComponent.tsx +1 -1
- package/src/map/basemaps.ts +4 -4
- package/src/utils/date.ts +6 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/utils.ts +12 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"autoprefixer": "^10.4.21",
|
|
151
151
|
"buffer": "^6.0.3",
|
|
152
152
|
"crypto-browserify": "^3.12.1",
|
|
153
|
+
"date-fns": "^4.1.0",
|
|
153
154
|
"eslint": "^9.30.1",
|
|
154
155
|
"eslint-config-prettier": "^10.1.8",
|
|
155
156
|
"eslint-import-resolver-alias": "^1.1.2",
|
|
@@ -163,6 +164,8 @@
|
|
|
163
164
|
"eslint-plugin-react-refresh": "^0.4.20",
|
|
164
165
|
"eslint-plugin-sonarjs": "^3.0.4",
|
|
165
166
|
"eslint-plugin-storybook": "^9.0.18",
|
|
167
|
+
"focus-trap": "^7.6.5",
|
|
168
|
+
"focus-trap-react": "^11.0.4",
|
|
166
169
|
"geojson": "^0.5.0",
|
|
167
170
|
"globals": "^16.3.0",
|
|
168
171
|
"husky": "^9.1.7",
|
|
@@ -206,6 +209,9 @@
|
|
|
206
209
|
"@turf/turf": "^7.2.0",
|
|
207
210
|
"@types/geojson": "^7946.0.16",
|
|
208
211
|
"@types/jsonwebtoken": "^9.0.10",
|
|
212
|
+
"date-fns": "^4.1.0",
|
|
213
|
+
"focus-trap": "^7.6.5",
|
|
214
|
+
"focus-trap-react": "^11.0.4",
|
|
209
215
|
"geojson": "^0.5.0",
|
|
210
216
|
"jsonwebtoken": "^9.0.2",
|
|
211
217
|
"next": "^15.4.2",
|
|
@@ -220,6 +226,12 @@
|
|
|
220
226
|
"zod": "^4.1.8"
|
|
221
227
|
},
|
|
222
228
|
"peerDependenciesMeta": {
|
|
229
|
+
"@tanstack/react-query": {
|
|
230
|
+
"optional": true
|
|
231
|
+
},
|
|
232
|
+
"@better-fetch/fetch": {
|
|
233
|
+
"optional": true
|
|
234
|
+
},
|
|
223
235
|
"@turf/turf": {
|
|
224
236
|
"optional": true
|
|
225
237
|
},
|
|
@@ -229,6 +241,15 @@
|
|
|
229
241
|
"@types/jsonwebtoken": {
|
|
230
242
|
"optional": true
|
|
231
243
|
},
|
|
244
|
+
"date-fns": {
|
|
245
|
+
"optional": true
|
|
246
|
+
},
|
|
247
|
+
"focus-trap": {
|
|
248
|
+
"optional": true
|
|
249
|
+
},
|
|
250
|
+
"focus-trap-react": {
|
|
251
|
+
"optional": true
|
|
252
|
+
},
|
|
232
253
|
"geojson": {
|
|
233
254
|
"optional": true
|
|
234
255
|
},
|
|
@@ -249,6 +270,9 @@
|
|
|
249
270
|
},
|
|
250
271
|
"react-select": {
|
|
251
272
|
"optional": true
|
|
273
|
+
},
|
|
274
|
+
"zod": {
|
|
275
|
+
"optional": true
|
|
252
276
|
}
|
|
253
277
|
},
|
|
254
278
|
"optionalDependencies": {
|
|
@@ -1,56 +1,83 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
1
2
|
import { FaChevronRight } from 'react-icons/fa6';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
import type { Meta, StoryFn } from '@storybook/react';
|
|
4
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
5
5
|
|
|
6
|
-
import { Card
|
|
6
|
+
import { Card } from './Card';
|
|
7
|
+
import { ExternalLink } from '../link/ExternalLink';
|
|
7
8
|
import { Paragraph } from '../Paragraph/Paragraph';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const meta: Meta<typeof Card> = {
|
|
11
|
+
title: 'Components/Card',
|
|
11
12
|
component: Card,
|
|
12
|
-
|
|
13
|
+
parameters: {
|
|
14
|
+
layout: 'padded',
|
|
15
|
+
},
|
|
16
|
+
tags: ['autodocs'],
|
|
17
|
+
argTypes: {
|
|
18
|
+
children: {
|
|
19
|
+
description: 'Content of the card',
|
|
20
|
+
control: false,
|
|
21
|
+
},
|
|
22
|
+
className: {
|
|
23
|
+
description: 'Additional TailwindCSS classes to apply',
|
|
24
|
+
control: 'text',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
args: {
|
|
28
|
+
children: 'Hello, this is some simple text',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
13
33
|
|
|
14
|
-
|
|
34
|
+
type Story = StoryObj<typeof Card>;
|
|
15
35
|
|
|
16
|
-
export const
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
export const Default: Story = {};
|
|
37
|
+
|
|
38
|
+
export const ParagraphOfText: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
children: <Paragraph>Hello, this is some simple text</Paragraph>,
|
|
41
|
+
},
|
|
19
42
|
};
|
|
20
43
|
|
|
21
|
-
export const
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
export const ImageAndText: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
children: (
|
|
47
|
+
<div>
|
|
48
|
+
<img
|
|
49
|
+
src="https://images.unsplash.com/photo-1563991655280-cb95c90ca2fb?w=500&auto=format&fit=crop&q=60&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxzZWFyY2h8M3x8bm8lMjBjb3B5cmlnaHR8ZW58MHx8MHx8fDA%3D"
|
|
50
|
+
alt="Card"
|
|
51
|
+
/>
|
|
52
|
+
|
|
53
|
+
<p>This is text about the image</p>
|
|
54
|
+
</div>
|
|
55
|
+
),
|
|
56
|
+
},
|
|
24
57
|
};
|
|
25
58
|
|
|
26
|
-
export const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
export const ComplexChildren: Story = {
|
|
60
|
+
args: {
|
|
61
|
+
children: (
|
|
62
|
+
<>
|
|
63
|
+
<ExternalLink
|
|
64
|
+
href="https://google.co.uk"
|
|
65
|
+
className="mb-4 flex flex-nowrap items-center gap-2 justify-between font-bold"
|
|
66
|
+
>
|
|
67
|
+
<strong>title</strong>
|
|
68
|
+
|
|
69
|
+
<FaChevronRight className="text-base" />
|
|
70
|
+
</ExternalLink>
|
|
71
|
+
|
|
72
|
+
<Paragraph>Some descriptive text</Paragraph>
|
|
73
|
+
</>
|
|
74
|
+
),
|
|
75
|
+
},
|
|
38
76
|
};
|
|
39
77
|
|
|
40
|
-
export const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
href="https://google.co.uk"
|
|
46
|
-
className="mb-4 flex flex-nowrap items-center gap-2 justify-between font-bold"
|
|
47
|
-
>
|
|
48
|
-
<strong>title</strong>
|
|
49
|
-
|
|
50
|
-
<FaChevronRight className="text-base" />
|
|
51
|
-
</a>
|
|
52
|
-
|
|
53
|
-
<Paragraph>Some descriptive text</Paragraph>
|
|
54
|
-
</>
|
|
55
|
-
),
|
|
78
|
+
export const CustomStyling: Story = {
|
|
79
|
+
args: {
|
|
80
|
+
className: 'bg-blue-100 text-blue-800 p-4 rounded-lg shadow-lg',
|
|
81
|
+
children: 'Custom styled card',
|
|
82
|
+
},
|
|
56
83
|
};
|
|
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react';
|
|
|
3
3
|
|
|
4
4
|
import { ErrorFallback } from './ErrorFallback';
|
|
5
5
|
import { ApiError } from '../../errors/ApiError';
|
|
6
|
-
import { HttpStatus } from '../../http';
|
|
6
|
+
import { HttpStatus } from '../../http/constants';
|
|
7
7
|
|
|
8
8
|
const meta = {
|
|
9
9
|
title: 'Components/ErrorFallback',
|
|
@@ -13,7 +13,7 @@ const meta = {
|
|
|
13
13
|
docs: {
|
|
14
14
|
description: {
|
|
15
15
|
component:
|
|
16
|
-
'
|
|
16
|
+
'Displayed when an error is caught by `ErrorBoundary`. Shows the error message, optional details in an Accordion, and Copy/Retry buttons.',
|
|
17
17
|
},
|
|
18
18
|
},
|
|
19
19
|
},
|
|
@@ -23,7 +23,7 @@ const meta = {
|
|
|
23
23
|
export default meta;
|
|
24
24
|
type Story = StoryObj<typeof meta>;
|
|
25
25
|
|
|
26
|
-
// Shared mock reset
|
|
26
|
+
// Shared mock reset function
|
|
27
27
|
const mockReset = () => alert('Reset error boundary called');
|
|
28
28
|
|
|
29
29
|
export const Default: Story = {
|
|
@@ -34,7 +34,8 @@ export const Default: Story = {
|
|
|
34
34
|
parameters: {
|
|
35
35
|
docs: {
|
|
36
36
|
description: {
|
|
37
|
-
story:
|
|
37
|
+
story:
|
|
38
|
+
'Default rendering with a standard JavaScript `Error`. Shows heading, reason, and Copy/Retry buttons.',
|
|
38
39
|
},
|
|
39
40
|
},
|
|
40
41
|
},
|
|
@@ -42,13 +43,14 @@ export const Default: Story = {
|
|
|
42
43
|
|
|
43
44
|
export const WithApiError: Story = {
|
|
44
45
|
args: {
|
|
45
|
-
error: new ApiError('Request failed', HttpStatus.InternalServerError, '
|
|
46
|
+
error: new ApiError('Request failed', HttpStatus.InternalServerError, 'Some technical details'),
|
|
46
47
|
resetErrorBoundary: mockReset,
|
|
47
48
|
},
|
|
48
49
|
parameters: {
|
|
49
50
|
docs: {
|
|
50
51
|
description: {
|
|
51
|
-
story:
|
|
52
|
+
story:
|
|
53
|
+
'Rendering when the error is an `ApiError`. Shows reason, details in an Accordion, digest, and Copy/Retry buttons.',
|
|
52
54
|
},
|
|
53
55
|
},
|
|
54
56
|
},
|
|
@@ -63,7 +65,7 @@ export const WithUnknownError: Story = {
|
|
|
63
65
|
docs: {
|
|
64
66
|
description: {
|
|
65
67
|
story:
|
|
66
|
-
'If the error is not an `Error` or `ApiError`, a generic "Unknown" message is shown.',
|
|
68
|
+
'If the error is not an `Error` or `ApiError`, a generic "Unknown error" message is shown, with Copy/Retry buttons.',
|
|
67
69
|
},
|
|
68
70
|
},
|
|
69
71
|
},
|
|
@@ -1,54 +1,107 @@
|
|
|
1
1
|
import { ErrorFallback } from './ErrorFallback';
|
|
2
2
|
import { ApiError } from '../../errors';
|
|
3
3
|
import { HttpStatus } from '../../http';
|
|
4
|
-
import { render, screen, userEvent } from '../../test/renderers';
|
|
4
|
+
import { render, screen, userEvent, within } from '../../test/renderers';
|
|
5
5
|
import { identityFn } from '../../utils/utils';
|
|
6
6
|
|
|
7
|
+
const FAKE_UUID = 'fake-uuid';
|
|
8
|
+
|
|
9
|
+
Object.defineProperty(navigator, 'clipboard', {
|
|
10
|
+
value: {
|
|
11
|
+
writeText: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
},
|
|
13
|
+
configurable: true,
|
|
14
|
+
});
|
|
15
|
+
|
|
7
16
|
describe('ErrorFallback', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
(navigator.clipboard.writeText as unknown) = vi.fn().mockResolvedValue(undefined);
|
|
19
|
+
vi.spyOn(crypto, 'randomUUID').mockReturnValue(FAKE_UUID);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
8
26
|
it('renders a generic error message for an unknown error type', () => {
|
|
9
27
|
render(<ErrorFallback error={{} as unknown as Error} resetErrorBoundary={identityFn} />);
|
|
10
28
|
|
|
11
29
|
const alert = screen.getByRole('alert');
|
|
12
30
|
|
|
13
|
-
expect(alert).toHaveTextContent(/
|
|
31
|
+
expect(alert).toHaveTextContent(/something went wrong/i);
|
|
14
32
|
expect(alert).toHaveTextContent(/reason: unknown/i);
|
|
15
33
|
});
|
|
16
34
|
|
|
17
35
|
it('renders message for standard Error instance', () => {
|
|
18
|
-
const error = new Error('
|
|
36
|
+
const error = new Error('custom error message');
|
|
19
37
|
|
|
20
38
|
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
21
39
|
|
|
22
|
-
expect(screen.
|
|
40
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: custom error message/i);
|
|
23
41
|
});
|
|
24
42
|
|
|
25
|
-
it('renders message and
|
|
26
|
-
const apiError = new ApiError('Fake Reason', HttpStatus.InternalServerError);
|
|
43
|
+
it('renders message, details and digest for ApiError instance', async () => {
|
|
44
|
+
const apiError = new ApiError('Fake Reason', HttpStatus.InternalServerError, 'Some details');
|
|
27
45
|
|
|
28
46
|
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
29
47
|
|
|
30
|
-
|
|
48
|
+
// Message shown
|
|
49
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: fake reason/i);
|
|
50
|
+
|
|
51
|
+
// Accordion exists but details hidden until toggled
|
|
52
|
+
const accordionButton = screen.getByRole('button', { name: /show details/i });
|
|
53
|
+
|
|
54
|
+
expect(accordionButton).toBeInTheDocument();
|
|
55
|
+
|
|
56
|
+
// Expand and check details text
|
|
57
|
+
await userEvent.click(accordionButton);
|
|
58
|
+
|
|
59
|
+
const section = screen.getByRole('region', { hidden: false });
|
|
60
|
+
|
|
61
|
+
expect(within(section).getByText(/error id:/i)).toBeInTheDocument();
|
|
62
|
+
expect(within(section).getByText(/some details/i)).toBeInTheDocument();
|
|
31
63
|
});
|
|
32
64
|
|
|
33
|
-
it('renders
|
|
34
|
-
const error = new Error('Has a cause', { cause: [
|
|
65
|
+
it('renders non-string causes', () => {
|
|
66
|
+
const error = new Error('Has a cause', { cause: ['I will display'] });
|
|
35
67
|
|
|
36
68
|
render(<ErrorFallback error={error} resetErrorBoundary={identityFn} />);
|
|
37
69
|
|
|
38
|
-
expect(screen.
|
|
39
|
-
expect(screen.
|
|
70
|
+
expect(screen.getByRole('alert')).toHaveTextContent(/reason: has a cause/i);
|
|
71
|
+
expect(screen.queryByRole('button', { name: /show details/i })).toBeInTheDocument();
|
|
40
72
|
});
|
|
41
73
|
|
|
42
|
-
it('calls resetErrorBoundary when
|
|
74
|
+
it('calls resetErrorBoundary when Retry is clicked', async () => {
|
|
43
75
|
const user = userEvent.setup();
|
|
44
76
|
const resetSpy = vi.fn();
|
|
45
77
|
|
|
46
78
|
render(<ErrorFallback error={new Error('Test error')} resetErrorBoundary={resetSpy} />);
|
|
47
79
|
|
|
48
|
-
const button = screen.getByRole('button', { name: /
|
|
80
|
+
const button = screen.getByRole('button', { name: /retry/i });
|
|
49
81
|
|
|
50
82
|
await user.click(button);
|
|
51
83
|
|
|
52
84
|
expect(resetSpy).toHaveBeenCalledTimes(1);
|
|
53
85
|
});
|
|
86
|
+
|
|
87
|
+
it('copies error info to clipboard when Copy is clicked', async () => {
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
const apiError = new ApiError('Copy Reason', HttpStatus.BadRequest, 'Some details');
|
|
90
|
+
|
|
91
|
+
render(<ErrorFallback error={apiError} resetErrorBoundary={identityFn} />);
|
|
92
|
+
|
|
93
|
+
const copyButton = screen.getByRole('button', { name: /copy/i });
|
|
94
|
+
|
|
95
|
+
await user.click(copyButton);
|
|
96
|
+
|
|
97
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining('App Error: Copy Reason'),
|
|
99
|
+
);
|
|
100
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
101
|
+
expect.stringContaining('Details: Some details'),
|
|
102
|
+
);
|
|
103
|
+
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
|
104
|
+
expect.stringContaining(`Digest: ${FAKE_UUID}`),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
54
107
|
});
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useId } from 'react';
|
|
2
4
|
|
|
3
|
-
import {
|
|
5
|
+
import { type FallbackProps } from 'react-error-boundary';
|
|
4
6
|
|
|
5
7
|
import { ApiError } from '../../errors/ApiError';
|
|
8
|
+
import { Accordion } from '../accordion/Accordion';
|
|
9
|
+
import { Button } from '../Button/Button';
|
|
10
|
+
import { Heading } from '../Heading/Heading';
|
|
11
|
+
import { Paragraph } from '../Paragraph/Paragraph';
|
|
6
12
|
|
|
7
13
|
export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
14
|
+
const id = useId();
|
|
15
|
+
|
|
8
16
|
let message;
|
|
9
17
|
let details = undefined;
|
|
10
18
|
let digest = undefined;
|
|
@@ -12,35 +20,67 @@ export const ErrorFallback = ({ resetErrorBoundary, error }: FallbackProps) => {
|
|
|
12
20
|
if (error instanceof ApiError) {
|
|
13
21
|
message = error.message;
|
|
14
22
|
details = error.details;
|
|
23
|
+
digest = error.digest;
|
|
15
24
|
} else if (error instanceof Error) {
|
|
16
25
|
message = error.message;
|
|
17
26
|
details = error.cause?.toString();
|
|
18
27
|
} else {
|
|
19
|
-
message = 'Unknown';
|
|
28
|
+
message = 'Unknown error';
|
|
20
29
|
}
|
|
21
30
|
|
|
31
|
+
const copyToClipboard = () => {
|
|
32
|
+
navigator.clipboard.writeText(
|
|
33
|
+
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
34
|
+
`App Error: ${message}, ${details ? `Details: ${details}, ` : ''}${digest ? `Digest: ${digest}` : ''}`,
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
22
38
|
return (
|
|
23
|
-
<div
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
<div
|
|
40
|
+
role="alert"
|
|
41
|
+
aria-labelledby={id}
|
|
42
|
+
className="grid gap-2 border-form border-transparent border-l-error max-w-full pl-2"
|
|
43
|
+
style={{ gridTemplateColumns: '1fr auto' }}
|
|
44
|
+
>
|
|
45
|
+
<div className="flex flex-col gap-0.5">
|
|
46
|
+
<Heading id={id} type="h3" className="text-error text-base font-semibold py-0">
|
|
47
|
+
Something went wrong
|
|
48
|
+
</Heading>
|
|
49
|
+
|
|
50
|
+
<Paragraph className="leading-snug pb-0">
|
|
51
|
+
<strong>Reason:</strong> {message}
|
|
52
|
+
</Paragraph>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="flex items-start gap-1">
|
|
56
|
+
<Button onClick={copyToClipboard} variant="secondary" className="shrink-0">
|
|
57
|
+
Copy
|
|
58
|
+
</Button>
|
|
59
|
+
|
|
60
|
+
<Button onClick={resetErrorBoundary} variant="secondary" className="shrink-0">
|
|
61
|
+
Retry
|
|
62
|
+
</Button>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{details && (
|
|
66
|
+
<div className="col-span-2">
|
|
67
|
+
<Accordion title="Show details">
|
|
68
|
+
<div className="flex gap-0.5 flex-col">
|
|
69
|
+
{digest && (
|
|
70
|
+
<Paragraph className="leading-snug pb-0">
|
|
71
|
+
<strong>Error ID:</strong> {digest}
|
|
72
|
+
</Paragraph>
|
|
73
|
+
)}
|
|
74
|
+
<pre
|
|
75
|
+
className="text-base bg-white border border-gray-200 p-2 overflow-y-auto max-h-32
|
|
76
|
+
whitespace-pre-wrap"
|
|
77
|
+
>
|
|
78
|
+
{details}
|
|
79
|
+
</pre>
|
|
80
|
+
</div>
|
|
81
|
+
</Accordion>
|
|
82
|
+
</div>
|
|
40
83
|
)}
|
|
41
|
-
<br />
|
|
42
|
-
Digest: {digest ?? 'No digest provided'}
|
|
43
|
-
<Button onClick={resetErrorBoundary}>Try Again</Button>
|
|
44
84
|
</div>
|
|
45
85
|
);
|
|
46
86
|
};
|
|
@@ -1,34 +1,53 @@
|
|
|
1
1
|
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
-
import type { Meta,
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
|
|
4
|
-
import { ErrorText
|
|
4
|
+
import { ErrorText } from './ErrorText';
|
|
5
|
+
import { ExternalLink } from '../link/ExternalLink';
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
const meta: Meta<typeof ErrorText> = {
|
|
8
|
+
title: 'Components/ErrorText',
|
|
8
9
|
component: ErrorText,
|
|
9
|
-
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: 'padded',
|
|
12
|
+
},
|
|
13
|
+
tags: ['autodocs'],
|
|
14
|
+
argTypes: {
|
|
15
|
+
children: {
|
|
16
|
+
description: 'The content to display for the error',
|
|
17
|
+
control: false,
|
|
18
|
+
},
|
|
19
|
+
className: {
|
|
20
|
+
description: 'Additional TailwindCSS classes to apply',
|
|
21
|
+
control: 'text',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
args: {
|
|
25
|
+
children: 'Error message',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
10
28
|
|
|
11
|
-
|
|
29
|
+
export default meta;
|
|
12
30
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
};
|
|
31
|
+
type Story = StoryObj<typeof ErrorText>;
|
|
32
|
+
|
|
33
|
+
export const Default: Story = {};
|
|
17
34
|
|
|
18
|
-
export const CustomStyling =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
35
|
+
export const CustomStyling: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
className: 'text-3xl',
|
|
38
|
+
children: 'Custom styled error message',
|
|
39
|
+
},
|
|
22
40
|
};
|
|
23
41
|
|
|
24
|
-
export const ComplexChildren =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
export const ComplexChildren: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
children: (
|
|
45
|
+
<div>
|
|
46
|
+
Error message with{' '}
|
|
47
|
+
<ExternalLink className="underline text-link" href="/">
|
|
48
|
+
Link
|
|
49
|
+
</ExternalLink>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
},
|
|
34
53
|
};
|
|
@@ -5,13 +5,15 @@ import type { ExtendProps } from '../../types/utils';
|
|
|
5
5
|
export type ErrorTextProps = ExtendProps<'p'>;
|
|
6
6
|
|
|
7
7
|
export const ErrorText = ({ className, children, ...props }: ErrorTextProps) => {
|
|
8
|
+
const Component = typeof children === 'string' ? 'p' : 'div';
|
|
9
|
+
|
|
8
10
|
return (
|
|
9
|
-
<
|
|
11
|
+
<Component
|
|
10
12
|
role="alert"
|
|
11
13
|
className={twMerge('mb-3 text-base text-error font-bold', className)}
|
|
12
14
|
{...props}
|
|
13
15
|
>
|
|
14
16
|
{children}
|
|
15
|
-
</
|
|
17
|
+
</Component>
|
|
16
18
|
);
|
|
17
19
|
};
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { twMerge } from 'tailwind-merge';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import type { ExtendProps } from '../../types';
|
|
4
|
+
|
|
5
|
+
type Props = {
|
|
4
6
|
type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
5
|
-
className?: string;
|
|
6
|
-
children: React.ReactNode;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
// h1 through h6 have identical props
|
|
10
|
+
export type HeadingProps = ExtendProps<'h1', Props>;
|
|
11
|
+
|
|
9
12
|
export const Heading = ({ type, className, children }: HeadingProps) => {
|
|
10
13
|
switch (type) {
|
|
11
14
|
case 'h1':
|