@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.
Files changed (76) hide show
  1. package/package.json +70 -8
  2. package/src/assets/styles/globals.css +2 -0
  3. package/src/assets/styles/ol.css +122 -0
  4. package/src/components/Button/Button.test.tsx +1 -1
  5. package/src/components/Button/Button.tsx +1 -1
  6. package/src/components/Card/Card.test.tsx +1 -1
  7. package/src/components/ErrorText/ErrorText.test.tsx +1 -1
  8. package/src/components/ErrorText/ErrorText.tsx +1 -1
  9. package/src/components/Heading/Heading.test.tsx +1 -1
  10. package/src/components/Hint/Hint.test.tsx +1 -1
  11. package/src/components/Hint/Hint.tsx +1 -1
  12. package/src/components/Modal/Modal.stories.tsx +252 -0
  13. package/src/components/Modal/Modal.test.tsx +248 -0
  14. package/src/components/Modal/Modal.tsx +61 -0
  15. package/src/components/Paragraph/Paragraph.test.tsx +1 -1
  16. package/src/components/SlidingPanel/SlidingPanel.test.tsx +1 -2
  17. package/src/components/accordion/Accordion.stories.tsx +235 -0
  18. package/src/components/accordion/Accordion.test.tsx +199 -0
  19. package/src/components/accordion/Accordion.tsx +47 -0
  20. package/src/components/divider/RuleDivider.stories.tsx +255 -0
  21. package/src/components/divider/RuleDivider.test.tsx +164 -0
  22. package/src/components/divider/RuleDivider.tsx +18 -0
  23. package/src/components/dropdown/DropdownMenu.test.tsx +1 -1
  24. package/src/components/dropdown/useDropdownMenu.ts +1 -1
  25. package/src/components/index.ts +6 -2
  26. package/src/components/layout/header/Header.tsx +2 -1
  27. package/src/components/layout/header/HeaderAuthClient.tsx +17 -9
  28. package/src/components/layout/header/HeaderNavClient.tsx +3 -3
  29. package/src/components/link/ExternalLink.tsx +1 -1
  30. package/src/components/link/Link.tsx +1 -1
  31. package/src/components/select/Select.stories.tsx +336 -0
  32. package/src/components/select/Select.test.tsx +473 -0
  33. package/src/components/select/Select.tsx +132 -0
  34. package/src/components/select/SelectSkeleton.stories.tsx +195 -0
  35. package/src/components/select/SelectSkeleton.test.tsx +105 -0
  36. package/src/components/select/SelectSkeleton.tsx +16 -0
  37. package/src/components/select/common.ts +4 -0
  38. package/src/contexts/index.ts +0 -5
  39. package/src/hooks/index.ts +1 -0
  40. package/src/hooks/useClickOutside.test.ts +290 -0
  41. package/src/hooks/useClickOutside.ts +26 -0
  42. package/src/index.ts +3 -0
  43. package/src/map/LayerSwitcherControl.ts +147 -0
  44. package/src/map/Map.tsx +230 -0
  45. package/src/map/MapContext.tsx +211 -0
  46. package/src/map/Popup.tsx +74 -0
  47. package/src/map/basemaps.ts +79 -0
  48. package/src/map/geocoder.ts +61 -0
  49. package/src/map/geometries.ts +60 -0
  50. package/src/map/images/basemaps/OS.png +0 -0
  51. package/src/map/images/basemaps/dark.png +0 -0
  52. package/src/map/images/basemaps/sat-map-tiler.png +0 -0
  53. package/src/map/images/basemaps/satellite-map-tiler.png +0 -0
  54. package/src/map/images/basemaps/satellite.png +0 -0
  55. package/src/map/images/basemaps/streets.png +0 -0
  56. package/src/map/images/openlayers-logo.png +0 -0
  57. package/src/map/index.ts +10 -0
  58. package/src/map/map.ts +40 -0
  59. package/src/map/osOpenNamesSearch.ts +54 -0
  60. package/src/map/projections.ts +14 -0
  61. package/src/ol-geocoder.d.ts +1 -0
  62. package/src/test/index.ts +1 -0
  63. package/src/types/api.ts +52 -0
  64. package/src/types/auth.ts +13 -0
  65. package/src/types/index.ts +6 -0
  66. package/src/types/map.ts +26 -0
  67. package/src/types/navigation.ts +8 -0
  68. package/src/types/utils.ts +13 -0
  69. package/src/utils/auth.ts +1 -1
  70. package/src/utils/http.ts +143 -0
  71. package/src/utils/index.ts +1 -1
  72. package/src/utils/utils.ts +1 -1
  73. package/src/components/link/NextLinkWrapper.tsx +0 -66
  74. package/src/contexts/ThemeContext.tsx +0 -72
  75. package/src/types.ts +0 -99
  76. /package/src/{utils → test}/renderers.tsx +0 -0
@@ -11,6 +11,7 @@ export { OglLogo } from './images/OglLogo';
11
11
  export { ExternalLink } from './link/ExternalLink';
12
12
  export { Link } from './link/Link';
13
13
  export { Paragraph } from './Paragraph/Paragraph';
14
+ export { RuleDivider } from './divider/RuleDivider';
14
15
 
15
16
  // Export default component types
16
17
  export type { ButtonProps } from './Button/Button';
@@ -25,13 +26,16 @@ export type { ParagraphProps } from './Paragraph/Paragraph';
25
26
  export type { SlidingPanelProps } from './SlidingPanel/SlidingPanel';
26
27
 
27
28
  // Client components - these require 'use client' directive
28
- export { NextLinkWrapper } from './link/NextLinkWrapper';
29
29
  export { DropdownMenu } from './dropdown/DropdownMenu';
30
30
  export { useDropdownMenu } from './dropdown/useDropdownMenu';
31
31
  export { SlidingPanel } from './SlidingPanel/SlidingPanel';
32
+ export { Accordion } from './accordion/Accordion';
33
+ export { Modal } from './Modal/Modal';
34
+ export { Select } from './select/Select';
35
+ export { SelectSkeleton } from './select/SelectSkeleton';
32
36
 
33
37
  // Export client component types
34
- export type { NextLinkWrapperProps } from './link/NextLinkWrapper';
38
+ export type { AccordionProps } from './accordion/Accordion';
35
39
 
36
40
  // Export layout components
37
41
  export { Header } from './layout/header/Header';
@@ -4,7 +4,8 @@ import { FaHouse } from 'react-icons/fa6';
4
4
 
5
5
  import { HeaderAuthClient } from './HeaderAuthClient';
6
6
  import { HeaderNavClient } from './HeaderNavClient';
7
- import type { Credentials, NavLink } from '../../../types';
7
+ import type { Credentials } from '../../../types/auth';
8
+ import type { NavLink } from '../../../types/navigation';
8
9
  import { DefraLogo } from '../../images/DefraLogo';
9
10
  import { EaLogo } from '../../images/EaLogo';
10
11
  import { ExternalLink } from '../../link/ExternalLink';
@@ -1,6 +1,8 @@
1
1
  'use client';
2
2
 
3
- import type { Credentials } from '../../../types';
3
+ import { useEffect, useState } from 'react';
4
+
5
+ import type { Credentials } from '../../../types/auth';
4
6
  import { Link } from '../../link/Link';
5
7
 
6
8
  type HeaderAuthClientProps = {
@@ -9,7 +11,11 @@ type HeaderAuthClientProps = {
9
11
  };
10
12
 
11
13
  export const HeaderAuthClient = ({ hostname, credentials }: HeaderAuthClientProps) => {
12
- console.log('HeaderAuthClient: ', { hostname, credentials });
14
+ const [isClient, setIsClient] = useState(false);
15
+
16
+ useEffect(() => {
17
+ setIsClient(true);
18
+ }, []);
13
19
 
14
20
  return (
15
21
  <span
@@ -20,13 +26,15 @@ export const HeaderAuthClient = ({ hostname, credentials }: HeaderAuthClientProp
20
26
  <span>Welcome,</span> {credentials ? credentials.user.email : 'Guest'}
21
27
  </span>
22
28
 
23
- <Link
24
- href={`${hostname}/${credentials ? 'api/logout' : 'login'}?redirect-uri=${encodeURIComponent(window?.location?.href)}`}
25
- className="text-white visited:text-white active:text-black hover:text-white"
26
- prefetch={false}
27
- >
28
- <span className="text-base">{credentials ? 'Logout' : 'Login'}</span>
29
- </Link>
29
+ {isClient && (
30
+ <Link
31
+ href={`${hostname}/${credentials ? 'api/logout' : 'login'}?redirect-uri=${encodeURIComponent(window.location.href)}`}
32
+ className="text-white visited:text-white active:text-black hover:text-white"
33
+ prefetch={false}
34
+ >
35
+ <span className="text-base">{credentials ? 'Logout' : 'Login'}</span>
36
+ </Link>
37
+ )}
30
38
  </span>
31
39
  );
32
40
  };
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import type { NavLink } from '../../../types';
3
+ import type { NavLink } from '../../../types/navigation';
4
4
  import { DropdownMenu, type DropdownMenuItem } from '../../dropdown/DropdownMenu';
5
5
  import { ExternalLink } from '../../link/ExternalLink';
6
6
  import { Link } from '../../link/Link';
@@ -23,8 +23,8 @@ const InternalNavItem = ({ label, url, icon, ...props }: DropdownMenuItem<NavLin
23
23
  </Link>
24
24
  );
25
25
 
26
- const NavItem = (props: DropdownMenuItem<NavLink>) => {
27
- if (props.isExternal) {
26
+ const NavItem = ({ isExternal, ...props }: DropdownMenuItem<NavLink>) => {
27
+ if (isExternal) {
28
28
  return <ExternalNavItem {...props} />;
29
29
  }
30
30
 
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
2
2
 
3
3
  import { twMerge } from 'tailwind-merge';
4
4
 
5
- import type { ExtendProps } from '../../types';
5
+ import type { ExtendProps } from '../../types/utils';
6
6
 
7
7
  type Props = {
8
8
  children: ReactNode;
@@ -1,7 +1,7 @@
1
1
  import NextLink from 'next/link';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
 
4
- import type { ExtendProps } from '../../types';
4
+ import type { ExtendProps } from '../../types/utils';
5
5
 
6
6
  type Props = {
7
7
  href: string | object;
@@ -0,0 +1,336 @@
1
+ /* eslint-disable storybook/no-renderer-packages */
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
+ import { fn } from '@storybook/test';
6
+
7
+ import { Select } from './Select';
8
+
9
+ const meta = {
10
+ title: 'Components/Select',
11
+ component: Select,
12
+ parameters: {
13
+ layout: 'centered',
14
+ },
15
+ tags: ['autodocs'],
16
+ args: {
17
+ onChange: fn(),
18
+ },
19
+ } satisfies Meta<typeof Select>;
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ const OPTIONS = [
25
+ { value: 'chocolate', label: 'Chocolate' },
26
+ { value: 'strawberry', label: 'Strawberry' },
27
+ { value: 'vanilla', label: 'Vanilla' },
28
+ { value: 'mint', label: 'Mint' },
29
+ { value: 'cookies', label: 'Cookies & Cream' },
30
+ ];
31
+
32
+ const GROUPED_OPTIONS = [
33
+ {
34
+ label: 'Fruits',
35
+ options: [
36
+ { value: 'apple', label: 'Apple' },
37
+ { value: 'banana', label: 'Banana' },
38
+ { value: 'orange', label: 'Orange' },
39
+ ],
40
+ },
41
+ {
42
+ label: 'Vegetables',
43
+ options: [
44
+ { value: 'carrot', label: 'Carrot' },
45
+ { value: 'broccoli', label: 'Broccoli' },
46
+ { value: 'spinach', label: 'Spinach' },
47
+ ],
48
+ },
49
+ ];
50
+
51
+ const USERS = [
52
+ { value: 'john', label: 'John Doe', email: 'john@example.com' },
53
+ { value: 'jane', label: 'Jane Smith', email: 'jane@example.com' },
54
+ { value: 'bob', label: 'Bob Johnson', email: 'bob@example.com' },
55
+ ];
56
+
57
+ const FLAVOUR_PLACEHOLDER_TEXT = 'Select a flavour...';
58
+ const FLAVOUR_MULTI_PLACEHOLDER_TEXT = 'Select multiple flavours...';
59
+
60
+ export const Default: Story = {
61
+ args: {
62
+ options: OPTIONS,
63
+ placeholder: FLAVOUR_PLACEHOLDER_TEXT,
64
+ },
65
+ };
66
+
67
+ export const WithDefaultValue: Story = {
68
+ args: {
69
+ options: OPTIONS,
70
+ defaultValue: OPTIONS[0],
71
+ placeholder: FLAVOUR_PLACEHOLDER_TEXT,
72
+ },
73
+ };
74
+
75
+ export const Clearable: Story = {
76
+ args: {
77
+ options: OPTIONS,
78
+ defaultValue: OPTIONS[1],
79
+ isClearable: true,
80
+ placeholder: FLAVOUR_PLACEHOLDER_TEXT,
81
+ },
82
+ };
83
+
84
+ export const Searchable: Story = {
85
+ args: {
86
+ options: OPTIONS,
87
+ isSearchable: true,
88
+ placeholder: 'Search and select...',
89
+ },
90
+ };
91
+
92
+ export const MultiSelect: Story = {
93
+ args: {
94
+ options: OPTIONS,
95
+ isMulti: true,
96
+ placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
97
+ },
98
+ };
99
+
100
+ export const MultiSelectWithValues: Story = {
101
+ args: {
102
+ options: OPTIONS,
103
+ isMulti: true,
104
+ defaultValue: [OPTIONS[0], OPTIONS[2]],
105
+ placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
106
+ },
107
+ };
108
+
109
+ export const MultiSelectClearable: Story = {
110
+ args: {
111
+ options: OPTIONS,
112
+ isMulti: true,
113
+ isClearable: true,
114
+ defaultValue: [OPTIONS[0], OPTIONS[1]],
115
+ placeholder: FLAVOUR_MULTI_PLACEHOLDER_TEXT,
116
+ },
117
+ };
118
+
119
+ export const Disabled: Story = {
120
+ args: {
121
+ options: OPTIONS,
122
+ isDisabled: true,
123
+ defaultValue: OPTIONS[0],
124
+ placeholder: 'This select is disabled',
125
+ },
126
+ };
127
+
128
+ export const Loading: Story = {
129
+ args: {
130
+ options: OPTIONS,
131
+ isLoading: true,
132
+ placeholder: 'Loading...',
133
+ },
134
+ };
135
+
136
+ export const GroupedOptions: Story = {
137
+ args: {
138
+ options: GROUPED_OPTIONS,
139
+ placeholder: 'Select a food item...',
140
+ },
141
+ };
142
+
143
+ export const NoOptionsMessage: Story = {
144
+ args: {
145
+ options: [],
146
+ placeholder: 'No options available',
147
+ noOptionsMessage: () => 'No options found',
148
+ },
149
+ };
150
+
151
+ export const CustomFormatOptionLabel: Story = {
152
+ args: {
153
+ options: USERS,
154
+ placeholder: 'Select a user...',
155
+ formatOptionLabel: (data: unknown) => {
156
+ const option = data as { value: string; label: string; email: string };
157
+
158
+ return (
159
+ <div>
160
+ <div className="font-medium">{option.label}</div>
161
+
162
+ <div className="text-sm text-gray-500">{option.email}</div>
163
+ </div>
164
+ );
165
+ },
166
+ },
167
+ };
168
+
169
+ export const CustomClassNames: Story = {
170
+ args: {
171
+ options: OPTIONS,
172
+ placeholder: 'Custom styled select...',
173
+ classNames: {
174
+ control: () => 'border-2 border-purple-500 rounded-lg',
175
+ option: (state) =>
176
+ // eslint-disable-next-line sonarjs/no-nested-conditional
177
+ state.isSelected ? 'bg-purple-500 text-white' : state.isFocused ? 'bg-purple-100' : '',
178
+ menu: () => 'border-purple-500',
179
+ },
180
+ },
181
+ };
182
+
183
+ export const SmallSize: Story = {
184
+ args: {
185
+ options: OPTIONS,
186
+ placeholder: 'Small select...',
187
+ classNames: {
188
+ control: () => 'min-h-8 text-sm',
189
+ option: () => 'px-2 py-1 text-sm',
190
+ },
191
+ },
192
+ };
193
+
194
+ export const LargeSize: Story = {
195
+ args: {
196
+ options: OPTIONS,
197
+ placeholder: 'Large select...',
198
+ classNames: {
199
+ control: () => 'min-h-12 text-lg',
200
+ option: () => 'px-6 py-3 text-lg',
201
+ },
202
+ },
203
+ };
204
+
205
+ export const ControlledComponent: Story = {
206
+ render: () => {
207
+ const [value, setValue] = useState<{ value: string; label: string } | null>(OPTIONS[0]);
208
+
209
+ return (
210
+ <div className="space-y-4">
211
+ <Select
212
+ options={OPTIONS}
213
+ value={value}
214
+ onChange={(newValue) => setValue(newValue)}
215
+ placeholder="Controlled select..."
216
+ />
217
+
218
+ <div className="text-sm text-gray-600">Selected: {value ? value.label : 'None'}</div>
219
+
220
+ <button
221
+ onClick={() => setValue(OPTIONS[2])}
222
+ className="px-3 py-1 bg-blue-500 text-white rounded text-sm"
223
+ >
224
+ Set to Vanilla
225
+ </button>
226
+ </div>
227
+ );
228
+ },
229
+ };
230
+ export const WithLongOptions: Story = {
231
+ args: {
232
+ options: [
233
+ { value: 'short', label: 'Short' },
234
+ {
235
+ value: 'long',
236
+ label: 'This is a very long option that might overflow and needs to be handled properly',
237
+ },
238
+ {
239
+ value: 'medium',
240
+ label: 'Medium length option text',
241
+ },
242
+ ],
243
+ placeholder: 'Select an option...',
244
+ },
245
+ };
246
+
247
+ export const Async: Story = {
248
+ render: () => {
249
+ const [options, setOptions] = useState<{ value: string; label: string }[]>([]);
250
+ const [isLoading, setIsLoading] = useState(false);
251
+
252
+ const loadOptions = () => {
253
+ setIsLoading(true);
254
+ // Simulate API call
255
+ setTimeout(() => {
256
+ setOptions([
257
+ { value: 'async1', label: 'Async Option 1' },
258
+ { value: 'async2', label: 'Async Option 2' },
259
+ { value: 'async3', label: 'Async Option 3' },
260
+ ]);
261
+ setIsLoading(false);
262
+ }, 1000);
263
+ };
264
+
265
+ useEffect(() => {
266
+ loadOptions();
267
+ }, []);
268
+
269
+ return (
270
+ <div className="space-y-4">
271
+ <Select options={options} isLoading={isLoading} placeholder="Loading async options..." />
272
+
273
+ <button onClick={loadOptions} className="px-3 py-1 bg-green-500 text-white rounded text-sm">
274
+ Reload Options
275
+ </button>
276
+ </div>
277
+ );
278
+ },
279
+ };
280
+
281
+ export const FormIntegration: Story = {
282
+ render: () => {
283
+ const [formData, setFormData] = useState<{
284
+ flavour: { value: string; label: string } | null;
285
+ toppings: { value: string; label: string }[];
286
+ }>({
287
+ flavour: null,
288
+ toppings: [],
289
+ });
290
+
291
+ return (
292
+ <form className="space-y-4 p-4 border rounded">
293
+ <div>
294
+ <label htmlFor="flavour" className="block text-sm font-medium mb-1">
295
+ Flavour
296
+ </label>
297
+
298
+ <Select
299
+ id="flavour"
300
+ options={OPTIONS}
301
+ value={formData.flavour}
302
+ onChange={(value) => setFormData({ ...formData, flavour: value })}
303
+ placeholder="Select a flavour..."
304
+ isClearable
305
+ />
306
+ </div>
307
+
308
+ <div>
309
+ <label htmlFor="toppings" className="block text-sm font-medium mb-1">
310
+ Toppings
311
+ </label>
312
+
313
+ <Select
314
+ id="toppings"
315
+ options={[
316
+ { value: 'sprinkles', label: 'Sprinkles' },
317
+ { value: 'nuts', label: 'Nuts' },
318
+ { value: 'chocolate-chips', label: 'Chocolate Chips' },
319
+ { value: 'cherry', label: 'Cherry' },
320
+ ]}
321
+ value={formData.toppings}
322
+ onChange={(value) =>
323
+ setFormData({ ...formData, toppings: Array.isArray(value) ? [...value] : [] })
324
+ }
325
+ placeholder="Select toppings..."
326
+ isMulti
327
+ />
328
+ </div>
329
+
330
+ <div className="text-sm text-gray-600">
331
+ <pre>{JSON.stringify(formData, null, 2)}</pre>
332
+ </div>
333
+ </form>
334
+ );
335
+ },
336
+ };