@tpzdsp/next-toolkit 1.0.1 → 1.2.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.
Files changed (54) hide show
  1. package/package.json +21 -2
  2. package/src/assets/styles/globals.css +2 -0
  3. package/src/assets/styles/ol.css +122 -0
  4. package/src/components/Button/Button.stories.tsx +4 -4
  5. package/src/components/Heading/Heading.tsx +34 -7
  6. package/src/components/Modal/Modal.stories.tsx +252 -0
  7. package/src/components/Modal/Modal.test.tsx +248 -0
  8. package/src/components/Modal/Modal.tsx +61 -0
  9. package/src/components/SlidingPanel/SlidingPanel.stories.tsx +31 -0
  10. package/src/components/SlidingPanel/SlidingPanel.test.tsx +86 -0
  11. package/src/components/SlidingPanel/SlidingPanel.tsx +133 -0
  12. package/src/components/accordion/Accordion.stories.tsx +235 -0
  13. package/src/components/accordion/Accordion.test.tsx +199 -0
  14. package/src/components/accordion/Accordion.tsx +47 -0
  15. package/src/components/divider/RuleDivider.stories.tsx +255 -0
  16. package/src/components/divider/RuleDivider.test.tsx +164 -0
  17. package/src/components/divider/RuleDivider.tsx +18 -0
  18. package/src/components/index.ts +11 -2
  19. package/src/components/layout/header/HeaderAuthClient.tsx +16 -8
  20. package/src/components/layout/header/HeaderNavClient.tsx +2 -2
  21. package/src/components/map/LayerSwitcherControl.ts +147 -0
  22. package/src/components/map/Map.tsx +230 -0
  23. package/src/components/map/MapContext.tsx +211 -0
  24. package/src/components/map/Popup.tsx +74 -0
  25. package/src/components/map/basemaps.ts +79 -0
  26. package/src/components/map/geocoder.ts +61 -0
  27. package/src/components/map/geometries.ts +60 -0
  28. package/src/components/map/images/basemaps/OS.png +0 -0
  29. package/src/components/map/images/basemaps/dark.png +0 -0
  30. package/src/components/map/images/basemaps/sat-map-tiler.png +0 -0
  31. package/src/components/map/images/basemaps/satellite-map-tiler.png +0 -0
  32. package/src/components/map/images/basemaps/satellite.png +0 -0
  33. package/src/components/map/images/basemaps/streets.png +0 -0
  34. package/src/components/map/images/openlayers-logo.png +0 -0
  35. package/src/components/map/index.ts +10 -0
  36. package/src/components/map/map.ts +40 -0
  37. package/src/components/map/osOpenNamesSearch.ts +54 -0
  38. package/src/components/map/projections.ts +14 -0
  39. package/src/components/select/Select.stories.tsx +336 -0
  40. package/src/components/select/Select.test.tsx +473 -0
  41. package/src/components/select/Select.tsx +132 -0
  42. package/src/components/select/SelectSkeleton.stories.tsx +195 -0
  43. package/src/components/select/SelectSkeleton.test.tsx +105 -0
  44. package/src/components/select/SelectSkeleton.tsx +16 -0
  45. package/src/components/select/common.ts +4 -0
  46. package/src/contexts/index.ts +0 -5
  47. package/src/hooks/index.ts +1 -0
  48. package/src/hooks/useClickOutside.test.ts +290 -0
  49. package/src/hooks/useClickOutside.ts +26 -0
  50. package/src/types.ts +51 -1
  51. package/src/utils/http.ts +143 -0
  52. package/src/utils/index.ts +1 -0
  53. package/src/components/link/NextLinkWrapper.tsx +0 -66
  54. package/src/contexts/ThemeContext.tsx +0 -72
@@ -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&apos;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 &quot;Forgot Password&quot;</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&apos;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
+ };
@@ -0,0 +1,199 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { Accordion } from './Accordion';
4
+ import { render, screen, userEvent } from '../../utils/renderers';
5
+
6
+ const TEST_CONTENT = 'Test content';
7
+ const ARIA_EXPANDED = 'aria-expanded';
8
+ const ARIA_HIDDEN = 'aria-hidden';
9
+
10
+ describe('Accordion', () => {
11
+ it('should render with title and content', () => {
12
+ render(
13
+ <Accordion title="Test Section">
14
+ <div>{TEST_CONTENT}</div>
15
+ </Accordion>,
16
+ );
17
+
18
+ expect(screen.getByText('Test Section')).toBeInTheDocument();
19
+ expect(screen.getByText(TEST_CONTENT)).toBeInTheDocument();
20
+
21
+ expect(screen.getByText(TEST_CONTENT).closest('section')).toHaveClass('hidden');
22
+ });
23
+
24
+ it('should render with content closed by default', () => {
25
+ render(
26
+ <Accordion title="Test Section">
27
+ <div>{TEST_CONTENT}</div>
28
+ </Accordion>,
29
+ );
30
+
31
+ expect(screen.getByRole('button', { name: /test section/i })).toHaveAttribute(
32
+ ARIA_EXPANDED,
33
+ 'false',
34
+ );
35
+
36
+ const section = screen.getByText(TEST_CONTENT).closest('section');
37
+
38
+ expect(section).toHaveAttribute(ARIA_HIDDEN, 'true');
39
+ expect(section).toHaveClass('hidden');
40
+ });
41
+
42
+ it('should open and close content when clicked', async () => {
43
+ const user = userEvent.setup();
44
+
45
+ render(
46
+ <Accordion title="Test Section">
47
+ <div>Test content</div>
48
+ </Accordion>,
49
+ );
50
+
51
+ const button = screen.getByRole('button', { name: /test section/i });
52
+ const section = screen.getByText(TEST_CONTENT).closest('section');
53
+
54
+ // Initially closed
55
+ expect(button).toHaveAttribute(ARIA_EXPANDED, 'false');
56
+ expect(section).toHaveAttribute(ARIA_HIDDEN, 'true');
57
+ expect(section).toHaveClass('hidden');
58
+
59
+ // Click to open
60
+ await user.click(button);
61
+ expect(button).toHaveAttribute(ARIA_EXPANDED, 'true');
62
+ expect(section).toHaveAttribute(ARIA_HIDDEN, 'false');
63
+ expect(section).toHaveClass('block');
64
+
65
+ // Click again to close
66
+ await user.click(button);
67
+ expect(button).toHaveAttribute(ARIA_EXPANDED, 'false');
68
+ expect(section).toHaveAttribute(ARIA_HIDDEN, 'true');
69
+ expect(section).toHaveClass('hidden');
70
+ });
71
+
72
+ it('should render with content open when defaultOpen is true', () => {
73
+ render(
74
+ <Accordion title="Test Section" defaultOpen>
75
+ <div>Test content</div>
76
+ </Accordion>,
77
+ );
78
+
79
+ const button = screen.getByRole('button', { name: /test section/i });
80
+
81
+ expect(button).toHaveAttribute(ARIA_EXPANDED, 'true');
82
+ expect(screen.getByText(TEST_CONTENT)).toBeInTheDocument();
83
+ });
84
+
85
+ it('should have proper accessibility attributes', () => {
86
+ render(
87
+ <Accordion title="Test Section">
88
+ <div>Test content</div>
89
+ </Accordion>,
90
+ );
91
+
92
+ const button = screen.getByRole('button', { name: /test section/i });
93
+
94
+ expect(button).toHaveAttribute(ARIA_EXPANDED, 'false');
95
+ expect(button).toHaveAttribute('aria-controls');
96
+ expect(button).toHaveAttribute('id');
97
+
98
+ const content = screen.getByRole('region', { hidden: true });
99
+
100
+ expect(content).toHaveAttribute('aria-labelledby', button.id);
101
+ expect(content).toHaveAttribute(ARIA_HIDDEN, 'true');
102
+ });
103
+
104
+ it('should update aria-hidden when opened', async () => {
105
+ const user = userEvent.setup();
106
+
107
+ render(
108
+ <Accordion title="Test Section">
109
+ <div>Test content</div>
110
+ </Accordion>,
111
+ );
112
+
113
+ const button = screen.getByRole('button', { name: /test section/i });
114
+ const content = screen.getByRole('region', { hidden: true });
115
+
116
+ expect(content).toHaveAttribute(ARIA_HIDDEN, 'true');
117
+
118
+ await user.click(button);
119
+
120
+ expect(content).toHaveAttribute(ARIA_HIDDEN, 'false');
121
+ });
122
+
123
+ it('should render complex content correctly', () => {
124
+ render(
125
+ <Accordion title="Complex Section" defaultOpen>
126
+ <div>
127
+ <p>Paragraph content</p>
128
+
129
+ <button>Nested button</button>
130
+
131
+ <ul>
132
+ <li>List item 1</li>
133
+
134
+ <li>List item 2</li>
135
+ </ul>
136
+ </div>
137
+ </Accordion>,
138
+ );
139
+
140
+ expect(screen.getByText('Paragraph content')).toBeInTheDocument();
141
+ expect(screen.getByRole('button', { name: 'Nested button' })).toBeInTheDocument();
142
+ expect(screen.getByText('List item 1')).toBeInTheDocument();
143
+ expect(screen.getByText('List item 2')).toBeInTheDocument();
144
+ });
145
+
146
+ it('should rotate chevron icon when opened', async () => {
147
+ const user = userEvent.setup();
148
+
149
+ render(
150
+ <Accordion title="Test Section">
151
+ <div>Test content</div>
152
+ </Accordion>,
153
+ );
154
+
155
+ const button = screen.getByRole('button', { name: /test section/i });
156
+ const chevron = button.querySelector('.rotate-180');
157
+
158
+ // Should not have rotate-180 class when closed
159
+ expect(chevron).toBeNull();
160
+
161
+ await user.click(button);
162
+
163
+ // Should have rotate-180 class when opened
164
+ const rotatedChevron = button.querySelector('.rotate-180');
165
+
166
+ expect(rotatedChevron).toBeInTheDocument();
167
+ });
168
+
169
+ it('should have correct styling classes', () => {
170
+ render(
171
+ <Accordion title="Test Section">
172
+ <div>Test content</div>
173
+ </Accordion>,
174
+ );
175
+
176
+ const container = screen.getByRole('button').closest('div');
177
+
178
+ expect(container).toHaveClass(
179
+ 'flex',
180
+ 'flex-col',
181
+ 'gap-2',
182
+ 'border-l-2',
183
+ 'border-neutral-100',
184
+ 'rounded-md',
185
+ );
186
+
187
+ const button = screen.getByRole('button');
188
+
189
+ expect(button).toHaveClass(
190
+ 'flex',
191
+ 'justify-between',
192
+ 'items-center',
193
+ 'px-2',
194
+ 'py-1',
195
+ 'bg-neutral-100',
196
+ 'rounded-md',
197
+ );
198
+ });
199
+ });
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import { type ReactNode, useId, useState } from 'react';
4
+
5
+ import { LuChevronDown } from 'react-icons/lu';
6
+ import { twMerge } from 'tailwind-merge';
7
+
8
+ export type AccordionProps = {
9
+ title: string;
10
+ children: ReactNode;
11
+ defaultOpen?: boolean;
12
+ };
13
+
14
+ export const Accordion = ({ title, children, defaultOpen = false }: AccordionProps) => {
15
+ const contentId = useId();
16
+ const buttonId = useId();
17
+
18
+ const [isOpen, setIsOpen] = useState(defaultOpen);
19
+
20
+ return (
21
+ <div className="flex flex-col gap-2 border-l-2 border-neutral-100 rounded-md">
22
+ <button
23
+ aria-expanded={isOpen}
24
+ aria-controls={contentId}
25
+ className="flex justify-between items-center px-2 py-1 bg-neutral-100 rounded-md"
26
+ id={buttonId}
27
+ onClick={() => setIsOpen(!isOpen)}
28
+ type="button"
29
+ >
30
+ <span>{title}</span>
31
+
32
+ <span aria-hidden="true">
33
+ <LuChevronDown className={twMerge('w-4 h-4', isOpen ? 'rotate-180' : '')} />
34
+ </span>
35
+ </button>
36
+
37
+ <section
38
+ id={contentId}
39
+ aria-labelledby={buttonId}
40
+ aria-hidden={!isOpen}
41
+ className={twMerge('px-2 pb-1', isOpen ? 'block' : 'hidden')}
42
+ >
43
+ {children}
44
+ </section>
45
+ </div>
46
+ );
47
+ };