@tpzdsp/next-toolkit 1.1.0 → 1.2.1
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 +70 -8
- package/src/assets/styles/globals.css +2 -0
- package/src/assets/styles/ol.css +122 -0
- package/src/components/Button/Button.test.tsx +1 -1
- package/src/components/Button/Button.tsx +1 -1
- package/src/components/Card/Card.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.test.tsx +1 -1
- package/src/components/ErrorText/ErrorText.tsx +1 -1
- package/src/components/Heading/Heading.test.tsx +1 -1
- package/src/components/Hint/Hint.test.tsx +1 -1
- package/src/components/Hint/Hint.tsx +1 -1
- package/src/components/Modal/Modal.stories.tsx +252 -0
- package/src/components/Modal/Modal.test.tsx +248 -0
- package/src/components/Modal/Modal.tsx +61 -0
- package/src/components/Paragraph/Paragraph.test.tsx +1 -1
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +1 -2
- package/src/components/accordion/Accordion.stories.tsx +235 -0
- package/src/components/accordion/Accordion.test.tsx +199 -0
- package/src/components/accordion/Accordion.tsx +47 -0
- package/src/components/divider/RuleDivider.stories.tsx +255 -0
- package/src/components/divider/RuleDivider.test.tsx +164 -0
- package/src/components/divider/RuleDivider.tsx +18 -0
- package/src/components/dropdown/DropdownMenu.test.tsx +1 -1
- package/src/components/dropdown/useDropdownMenu.ts +1 -1
- package/src/components/index.ts +6 -2
- package/src/components/layout/header/Header.tsx +2 -1
- package/src/components/layout/header/HeaderAuthClient.tsx +17 -9
- package/src/components/layout/header/HeaderNavClient.tsx +3 -3
- package/src/components/link/ExternalLink.tsx +1 -1
- package/src/components/link/Link.tsx +1 -1
- package/src/components/select/Select.stories.tsx +336 -0
- package/src/components/select/Select.test.tsx +473 -0
- package/src/components/select/Select.tsx +132 -0
- package/src/components/select/SelectSkeleton.stories.tsx +195 -0
- package/src/components/select/SelectSkeleton.test.tsx +105 -0
- package/src/components/select/SelectSkeleton.tsx +16 -0
- package/src/components/select/common.ts +4 -0
- package/src/contexts/index.ts +0 -5
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useClickOutside.test.ts +290 -0
- package/src/hooks/useClickOutside.ts +26 -0
- package/src/index.ts +3 -0
- package/src/map/LayerSwitcherControl.ts +147 -0
- package/src/map/Map.tsx +230 -0
- package/src/map/MapContext.tsx +211 -0
- package/src/map/Popup.tsx +74 -0
- package/src/map/basemaps.ts +79 -0
- package/src/map/geocoder.ts +61 -0
- package/src/map/geometries.ts +60 -0
- package/src/map/images/basemaps/OS.png +0 -0
- package/src/map/images/basemaps/dark.png +0 -0
- package/src/map/images/basemaps/sat-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite-map-tiler.png +0 -0
- package/src/map/images/basemaps/satellite.png +0 -0
- package/src/map/images/basemaps/streets.png +0 -0
- package/src/map/images/openlayers-logo.png +0 -0
- package/src/map/index.ts +10 -0
- package/src/map/map.ts +40 -0
- package/src/map/osOpenNamesSearch.ts +54 -0
- package/src/map/projections.ts +14 -0
- package/src/ol-geocoder.d.ts +1 -0
- package/src/test/index.ts +1 -0
- package/src/types/api.ts +52 -0
- package/src/types/auth.ts +13 -0
- package/src/types/index.ts +6 -0
- package/src/types/map.ts +26 -0
- package/src/types/navigation.ts +8 -0
- package/src/types/utils.ts +13 -0
- package/src/utils/auth.ts +1 -1
- package/src/utils/http.ts +143 -0
- package/src/utils/index.ts +1 -1
- package/src/utils/utils.ts +1 -1
- package/src/components/link/NextLinkWrapper.tsx +0 -66
- package/src/contexts/ThemeContext.tsx +0 -72
- package/src/types.ts +0 -99
- /package/src/{utils → test}/renderers.tsx +0 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { Modal } from './Modal';
|
|
4
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
5
|
+
|
|
6
|
+
const MODAL_CONTENT = 'Modal content';
|
|
7
|
+
const MODAL_ROOT_ID = 'modal-root';
|
|
8
|
+
const BACKDROP_SELECTOR = '[aria-modal="true"]';
|
|
9
|
+
|
|
10
|
+
// Mock the useClickOutside hook
|
|
11
|
+
vi.mock('../../hooks/useClickOutside', () => ({
|
|
12
|
+
useClickOutside: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
describe('Modal', () => {
|
|
16
|
+
// Setup modal root element for each test
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
const modalRoot = document.createElement('div');
|
|
19
|
+
|
|
20
|
+
modalRoot.id = MODAL_ROOT_ID;
|
|
21
|
+
document.body.appendChild(modalRoot);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
const modalRoot = document.getElementById(MODAL_ROOT_ID);
|
|
26
|
+
|
|
27
|
+
if (modalRoot) {
|
|
28
|
+
document.body.removeChild(modalRoot);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should render modal when isOpen is true', () => {
|
|
33
|
+
render(
|
|
34
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
35
|
+
<div>{MODAL_CONTENT}</div>
|
|
36
|
+
</Modal>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(screen.getByText(MODAL_CONTENT)).toBeInTheDocument();
|
|
40
|
+
|
|
41
|
+
// Check for the backdrop with aria-modal attribute instead of role
|
|
42
|
+
const backdrop = screen.getByText(MODAL_CONTENT).closest(BACKDROP_SELECTOR);
|
|
43
|
+
|
|
44
|
+
expect(backdrop).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should not render modal when isOpen is false', () => {
|
|
48
|
+
render(
|
|
49
|
+
<Modal isOpen={false} onClose={vi.fn()}>
|
|
50
|
+
<div>{MODAL_CONTENT}</div>
|
|
51
|
+
</Modal>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(screen.queryByText(MODAL_CONTENT)).not.toBeInTheDocument();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not render modal when modal root is missing', () => {
|
|
58
|
+
// Remove modal root
|
|
59
|
+
const modalRoot = document.getElementById(MODAL_ROOT_ID);
|
|
60
|
+
|
|
61
|
+
if (modalRoot) {
|
|
62
|
+
document.body.removeChild(modalRoot);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
render(
|
|
66
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
67
|
+
<div>{MODAL_CONTENT}</div>
|
|
68
|
+
</Modal>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(screen.queryByText(MODAL_CONTENT)).not.toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should have proper accessibility attributes', () => {
|
|
75
|
+
render(
|
|
76
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
77
|
+
<div>{MODAL_CONTENT}</div>
|
|
78
|
+
</Modal>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const backdrop = screen.getByText(MODAL_CONTENT).closest(BACKDROP_SELECTOR);
|
|
82
|
+
|
|
83
|
+
expect(backdrop).toHaveAttribute('aria-modal', 'true');
|
|
84
|
+
|
|
85
|
+
const closeButton = screen.getByRole('button', { name: /close modal/i });
|
|
86
|
+
|
|
87
|
+
expect(closeButton).toHaveAttribute('aria-label', 'Close modal');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should call onClose when close button is clicked', async () => {
|
|
91
|
+
const user = userEvent.setup();
|
|
92
|
+
const onCloseMock = vi.fn();
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<Modal isOpen={true} onClose={onCloseMock}>
|
|
96
|
+
<div>{MODAL_CONTENT}</div>
|
|
97
|
+
</Modal>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const closeButton = screen.getByRole('button', { name: /close modal/i });
|
|
101
|
+
|
|
102
|
+
await user.click(closeButton);
|
|
103
|
+
|
|
104
|
+
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should call onClose when Escape key is pressed', async () => {
|
|
108
|
+
const user = userEvent.setup();
|
|
109
|
+
const onCloseMock = vi.fn();
|
|
110
|
+
|
|
111
|
+
render(
|
|
112
|
+
<Modal isOpen={true} onClose={onCloseMock}>
|
|
113
|
+
<div>{MODAL_CONTENT}</div>
|
|
114
|
+
</Modal>,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await user.keyboard('{Escape}');
|
|
118
|
+
|
|
119
|
+
expect(onCloseMock).toHaveBeenCalledTimes(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should not call onClose when other keys are pressed', async () => {
|
|
123
|
+
const user = userEvent.setup();
|
|
124
|
+
const onCloseMock = vi.fn();
|
|
125
|
+
|
|
126
|
+
render(
|
|
127
|
+
<Modal isOpen={true} onClose={onCloseMock}>
|
|
128
|
+
<div>{MODAL_CONTENT}</div>
|
|
129
|
+
</Modal>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
await user.keyboard('{Enter}');
|
|
133
|
+
await user.keyboard('{Space}');
|
|
134
|
+
await user.keyboard('a');
|
|
135
|
+
|
|
136
|
+
expect(onCloseMock).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should render complex content correctly', () => {
|
|
140
|
+
render(
|
|
141
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
142
|
+
<div>
|
|
143
|
+
<h2>Modal Title</h2>
|
|
144
|
+
|
|
145
|
+
<p>Modal description</p>
|
|
146
|
+
|
|
147
|
+
<button>Action Button</button>
|
|
148
|
+
|
|
149
|
+
<ul>
|
|
150
|
+
<li>Item 1</li>
|
|
151
|
+
|
|
152
|
+
<li>Item 2</li>
|
|
153
|
+
</ul>
|
|
154
|
+
</div>
|
|
155
|
+
</Modal>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
expect(screen.getByText('Modal Title')).toBeInTheDocument();
|
|
159
|
+
expect(screen.getByText('Modal description')).toBeInTheDocument();
|
|
160
|
+
expect(screen.getByRole('button', { name: 'Action Button' })).toBeInTheDocument();
|
|
161
|
+
expect(screen.getByText('Item 1')).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByText('Item 2')).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should have correct styling classes', () => {
|
|
166
|
+
render(
|
|
167
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
168
|
+
<div>{MODAL_CONTENT}</div>
|
|
169
|
+
</Modal>,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const backdrop = screen.getByText(MODAL_CONTENT).closest(BACKDROP_SELECTOR);
|
|
173
|
+
|
|
174
|
+
expect(backdrop).toHaveClass(
|
|
175
|
+
'fixed',
|
|
176
|
+
'inset-0',
|
|
177
|
+
'z-50',
|
|
178
|
+
'flex',
|
|
179
|
+
'items-center',
|
|
180
|
+
'justify-center',
|
|
181
|
+
'bg-black',
|
|
182
|
+
'bg-opacity-50',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const modalContent = backdrop?.firstChild as HTMLElement;
|
|
186
|
+
|
|
187
|
+
expect(modalContent).toHaveClass(
|
|
188
|
+
'bg-white',
|
|
189
|
+
'rounded-lg',
|
|
190
|
+
'p-6',
|
|
191
|
+
'relative',
|
|
192
|
+
'max-w-md',
|
|
193
|
+
'w-full',
|
|
194
|
+
'shadow-lg',
|
|
195
|
+
'z-10',
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should render close icon correctly', () => {
|
|
200
|
+
render(
|
|
201
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
202
|
+
<div>{MODAL_CONTENT}</div>
|
|
203
|
+
</Modal>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const closeButton = screen.getByRole('button', { name: /close modal/i });
|
|
207
|
+
const closeIcon = closeButton.querySelector('svg');
|
|
208
|
+
|
|
209
|
+
expect(closeIcon).toBeInTheDocument();
|
|
210
|
+
expect(closeButton).toHaveClass(
|
|
211
|
+
'text-static-xl',
|
|
212
|
+
'absolute',
|
|
213
|
+
'top-2',
|
|
214
|
+
'right-2',
|
|
215
|
+
'text-gray-600',
|
|
216
|
+
'hover:text-black',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should clean up event listeners when unmounted', () => {
|
|
221
|
+
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
|
|
222
|
+
const onCloseMock = vi.fn();
|
|
223
|
+
|
|
224
|
+
const { unmount } = render(
|
|
225
|
+
<Modal isOpen={true} onClose={onCloseMock}>
|
|
226
|
+
<div>{MODAL_CONTENT}</div>
|
|
227
|
+
</Modal>,
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
unmount();
|
|
231
|
+
|
|
232
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
|
|
233
|
+
|
|
234
|
+
removeEventListenerSpy.mockRestore();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should have modal content with correct tabIndex', () => {
|
|
238
|
+
render(
|
|
239
|
+
<Modal isOpen={true} onClose={vi.fn()}>
|
|
240
|
+
<div>{MODAL_CONTENT}</div>
|
|
241
|
+
</Modal>,
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const modalContent = screen.getByText(MODAL_CONTENT).closest('div[tabIndex="-1"]');
|
|
245
|
+
|
|
246
|
+
expect(modalContent).toHaveAttribute('tabIndex', '-1');
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { IoMdCloseCircle } from 'react-icons/io';
|
|
7
|
+
|
|
8
|
+
import { useClickOutside } from '../../hooks/useClickOutside';
|
|
9
|
+
|
|
10
|
+
type ModalProps = {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
onClose: () => void;
|
|
13
|
+
children: React.ReactNode;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const Modal = ({ isOpen, onClose, children }: ModalProps) => {
|
|
17
|
+
const modalRoot = document.getElementById('modal-root');
|
|
18
|
+
|
|
19
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
20
|
+
|
|
21
|
+
useClickOutside(modalRef, onClose);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const handleEscape = (event: KeyboardEvent) => {
|
|
25
|
+
if (event.key === 'Escape') {
|
|
26
|
+
onClose();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
document.addEventListener('keydown', handleEscape);
|
|
31
|
+
|
|
32
|
+
return () => document.removeEventListener('keydown', handleEscape);
|
|
33
|
+
}, [onClose]);
|
|
34
|
+
|
|
35
|
+
if (!modalRoot || !isOpen) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return createPortal(
|
|
40
|
+
<div
|
|
41
|
+
aria-modal="true"
|
|
42
|
+
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
|
43
|
+
>
|
|
44
|
+
<div
|
|
45
|
+
ref={modalRef}
|
|
46
|
+
className="bg-white rounded-lg p-6 relative max-w-md w-full shadow-lg z-10"
|
|
47
|
+
tabIndex={-1}
|
|
48
|
+
>
|
|
49
|
+
<button
|
|
50
|
+
onClick={onClose}
|
|
51
|
+
className="text-static-xl absolute top-2 right-2 text-gray-600 hover:text-black"
|
|
52
|
+
aria-label="Close modal"
|
|
53
|
+
>
|
|
54
|
+
<IoMdCloseCircle size={20} />
|
|
55
|
+
</button>
|
|
56
|
+
{children}
|
|
57
|
+
</div>
|
|
58
|
+
</div>,
|
|
59
|
+
modalRoot,
|
|
60
|
+
);
|
|
61
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
|
|
3
3
|
import { Paragraph } from './Paragraph';
|
|
4
|
-
import { render, screen } from '../../
|
|
4
|
+
import { render, screen } from '../../test/renderers';
|
|
5
5
|
|
|
6
6
|
describe('Paragraph', () => {
|
|
7
7
|
it('renders with text', () => {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
|
|
3
|
-
import { act, render, screen, userEvent, waitFor } from '@tpzdsp/next-toolkit';
|
|
4
|
-
|
|
5
3
|
import { SlidingPanel } from './SlidingPanel';
|
|
4
|
+
import { act, render, screen, userEvent, waitFor } from '../../test/renderers';
|
|
6
5
|
|
|
7
6
|
const positions = ['center-left', 'center-right', 'center-top', 'center-bottom'] as const;
|
|
8
7
|
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/* eslint-disable storybook/no-renderer-packages */
|
|
2
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
|
+
|
|
4
|
+
import { Accordion } from './Accordion';
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Accordion> = {
|
|
7
|
+
title: 'Components/Accordion',
|
|
8
|
+
component: Accordion,
|
|
9
|
+
parameters: {
|
|
10
|
+
layout: 'padded',
|
|
11
|
+
},
|
|
12
|
+
tags: ['autodocs'],
|
|
13
|
+
argTypes: {
|
|
14
|
+
title: {
|
|
15
|
+
control: 'text',
|
|
16
|
+
description: 'The title displayed in the accordion header',
|
|
17
|
+
},
|
|
18
|
+
defaultOpen: {
|
|
19
|
+
control: 'boolean',
|
|
20
|
+
description: 'Whether the accordion should be open by default',
|
|
21
|
+
},
|
|
22
|
+
children: {
|
|
23
|
+
control: false,
|
|
24
|
+
description: 'The content to display when the accordion is expanded',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
type Story = StoryObj<typeof Accordion>;
|
|
31
|
+
|
|
32
|
+
export const Default: Story = {
|
|
33
|
+
args: {
|
|
34
|
+
title: 'Getting Started',
|
|
35
|
+
children: (
|
|
36
|
+
<div className="space-y-2">
|
|
37
|
+
<p>Welcome to our platform! Here's how to get started:</p>
|
|
38
|
+
|
|
39
|
+
<ul className="list-disc list-inside space-y-1">
|
|
40
|
+
<li>Create your account</li>
|
|
41
|
+
|
|
42
|
+
<li>Verify your email</li>
|
|
43
|
+
|
|
44
|
+
<li>Complete your profile</li>
|
|
45
|
+
</ul>
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const DefaultOpen: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
title: 'Features Overview',
|
|
54
|
+
defaultOpen: true,
|
|
55
|
+
children: (
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<p>Our platform offers the following features:</p>
|
|
58
|
+
|
|
59
|
+
<ul className="list-disc list-inside space-y-1">
|
|
60
|
+
<li>Real-time collaboration</li>
|
|
61
|
+
|
|
62
|
+
<li>Advanced analytics</li>
|
|
63
|
+
|
|
64
|
+
<li>Customizable dashboards</li>
|
|
65
|
+
|
|
66
|
+
<li>API integrations</li>
|
|
67
|
+
</ul>
|
|
68
|
+
</div>
|
|
69
|
+
),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const SimpleContent: Story = {
|
|
74
|
+
args: {
|
|
75
|
+
title: 'What is React?',
|
|
76
|
+
children: <p>React is a JavaScript library for building user interfaces.</p>,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const LongContent: Story = {
|
|
81
|
+
args: {
|
|
82
|
+
title: 'Terms and Conditions',
|
|
83
|
+
children: (
|
|
84
|
+
<div className="space-y-4 max-h-64 overflow-y-auto">
|
|
85
|
+
<p>
|
|
86
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt
|
|
87
|
+
ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation
|
|
88
|
+
ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<p>
|
|
92
|
+
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat
|
|
93
|
+
nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia
|
|
94
|
+
deserunt mollit anim id est laborum.
|
|
95
|
+
</p>
|
|
96
|
+
|
|
97
|
+
<p>
|
|
98
|
+
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque
|
|
99
|
+
laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
|
|
100
|
+
architecto beatae vitae dicta sunt explicabo.
|
|
101
|
+
</p>
|
|
102
|
+
</div>
|
|
103
|
+
),
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const ComplexContent: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
title: 'Pricing Plans',
|
|
110
|
+
children: (
|
|
111
|
+
<div className="space-y-4">
|
|
112
|
+
<p>Choose the plan that works best for you:</p>
|
|
113
|
+
|
|
114
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
115
|
+
<div className="border rounded p-4">
|
|
116
|
+
<h4 className="font-semibold">Basic</h4>
|
|
117
|
+
|
|
118
|
+
<p className="text-gray-600">$9/month</p>
|
|
119
|
+
|
|
120
|
+
<button className="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-sm">
|
|
121
|
+
Choose Plan
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div className="border rounded p-4">
|
|
126
|
+
<h4 className="font-semibold">Pro</h4>
|
|
127
|
+
|
|
128
|
+
<p className="text-gray-600">$29/month</p>
|
|
129
|
+
|
|
130
|
+
<button className="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-sm">
|
|
131
|
+
Choose Plan
|
|
132
|
+
</button>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="border rounded p-4">
|
|
136
|
+
<h4 className="font-semibold">Enterprise</h4>
|
|
137
|
+
|
|
138
|
+
<p className="text-gray-600">Contact us</p>
|
|
139
|
+
|
|
140
|
+
<button className="mt-2 px-3 py-1 bg-blue-500 text-white rounded text-sm">
|
|
141
|
+
Contact Sales
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
),
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const FAQ: Story = {
|
|
151
|
+
render: () => (
|
|
152
|
+
<div className="space-y-4 max-w-2xl">
|
|
153
|
+
<h2 className="text-xl font-bold mb-4">Frequently Asked Questions</h2>
|
|
154
|
+
|
|
155
|
+
<Accordion title="How do I reset my password?">
|
|
156
|
+
<div className="space-y-2">
|
|
157
|
+
<p>To reset your password:</p>
|
|
158
|
+
|
|
159
|
+
<ol className="list-decimal list-inside space-y-1">
|
|
160
|
+
<li>Go to the login page</li>
|
|
161
|
+
|
|
162
|
+
<li>Click "Forgot Password"</li>
|
|
163
|
+
|
|
164
|
+
<li>Enter your email address</li>
|
|
165
|
+
|
|
166
|
+
<li>Check your email for reset instructions</li>
|
|
167
|
+
</ol>
|
|
168
|
+
</div>
|
|
169
|
+
</Accordion>
|
|
170
|
+
|
|
171
|
+
<Accordion title="Can I cancel my subscription anytime?">
|
|
172
|
+
<p>
|
|
173
|
+
Yes, you can cancel your subscription at any time. Your access will continue until the end
|
|
174
|
+
of your current billing period.
|
|
175
|
+
</p>
|
|
176
|
+
</Accordion>
|
|
177
|
+
|
|
178
|
+
<Accordion title="Do you offer refunds?">
|
|
179
|
+
<div className="space-y-2">
|
|
180
|
+
<p>We offer refunds under the following conditions:</p>
|
|
181
|
+
|
|
182
|
+
<ul className="list-disc list-inside space-y-1">
|
|
183
|
+
<li>Within 30 days of purchase</li>
|
|
184
|
+
|
|
185
|
+
<li>Service was not used extensively</li>
|
|
186
|
+
|
|
187
|
+
<li>Technical issues that couldn't be resolved</li>
|
|
188
|
+
</ul>
|
|
189
|
+
|
|
190
|
+
<p className="mt-2">Contact support for refund requests.</p>
|
|
191
|
+
</div>
|
|
192
|
+
</Accordion>
|
|
193
|
+
|
|
194
|
+
<Accordion title="Is my data secure?" defaultOpen>
|
|
195
|
+
<div className="space-y-2">
|
|
196
|
+
<p>Yes, we take data security seriously:</p>
|
|
197
|
+
|
|
198
|
+
<ul className="list-disc list-inside space-y-1">
|
|
199
|
+
<li>All data is encrypted in transit and at rest</li>
|
|
200
|
+
|
|
201
|
+
<li>We use industry-standard security practices</li>
|
|
202
|
+
|
|
203
|
+
<li>Regular security audits and penetration testing</li>
|
|
204
|
+
|
|
205
|
+
<li>GDPR and CCPA compliant</li>
|
|
206
|
+
</ul>
|
|
207
|
+
</div>
|
|
208
|
+
</Accordion>
|
|
209
|
+
</div>
|
|
210
|
+
),
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const Interactive: Story = {
|
|
214
|
+
render: () => (
|
|
215
|
+
<div className="space-y-6 max-w-lg">
|
|
216
|
+
<div>
|
|
217
|
+
<h3 className="text-lg font-semibold mb-4">Multiple Accordions</h3>
|
|
218
|
+
|
|
219
|
+
<div className="space-y-4">
|
|
220
|
+
<Accordion title="Section 1">
|
|
221
|
+
<p>Content for the first section.</p>
|
|
222
|
+
</Accordion>
|
|
223
|
+
|
|
224
|
+
<Accordion title="Section 2" defaultOpen>
|
|
225
|
+
<p>Content for the second section (open by default).</p>
|
|
226
|
+
</Accordion>
|
|
227
|
+
|
|
228
|
+
<Accordion title="Section 3">
|
|
229
|
+
<p>Content for the third section.</p>
|
|
230
|
+
</Accordion>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
),
|
|
235
|
+
};
|