@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpzdsp/next-toolkit",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "A reusable React component library for Next.js applications",
5
5
  "type": "module",
6
6
  "private": false,
@@ -71,8 +71,11 @@
71
71
  "@testing-library/react": "^16.3.0",
72
72
  "@testing-library/user-event": "^14.6.1",
73
73
  "@tpzdsp/eslint-config-dsp": "^1.9.0",
74
+ "@turf/turf": "^7.2.0",
75
+ "@types/geojson": "^7946.0.16",
74
76
  "@types/jsonwebtoken": "^9.0.10",
75
77
  "@types/node": "^24.0.15",
78
+ "@types/proj4": "^2.19.0",
76
79
  "@types/react": "^19.1.8",
77
80
  "@types/react-dom": "^19.1.6",
78
81
  "@typescript-eslint/eslint-plugin": "^8.38.0",
@@ -94,18 +97,25 @@
94
97
  "eslint-plugin-react-refresh": "^0.4.20",
95
98
  "eslint-plugin-sonarjs": "^3.0.4",
96
99
  "eslint-plugin-storybook": "^9.0.18",
100
+ "geojson": "^0.5.0",
97
101
  "globals": "^16.3.0",
98
102
  "husky": "^9.1.7",
99
103
  "jsdom": "^26.1.0",
100
104
  "jsonwebtoken": "^9.0.2",
101
105
  "next": "^15.4.2",
106
+ "ol": "^10.6.1",
107
+ "ol-geocoder": "^4.3.3",
108
+ "ol-mapbox-style": "^13.0.1",
102
109
  "postcss": "^8.5.6",
103
110
  "prettier": "^3.6.2",
104
111
  "prettier-plugin-classnames": "^0.8.1",
105
112
  "prettier-plugin-tailwindcss": "^0.6.14",
113
+ "proj4": "^2.19.10",
106
114
  "react": "^19.1.0",
107
115
  "react-dom": "^19.1.0",
108
116
  "react-icons": "^5.5.0",
117
+ "react-select": "^5.10.2",
118
+ "react-select-event": "^5.5.1",
109
119
  "rollup-plugin-peer-deps-external": "^2.2.4",
110
120
  "semantic-release": "^24.2.7",
111
121
  "storybook": "8.6.14",
@@ -120,12 +130,21 @@
120
130
  "peerDependencies": {
121
131
  "@testing-library/react": "^16.0.0",
122
132
  "@testing-library/user-event": "^14.6.1",
133
+ "@turf/turf": "^7.2.0",
134
+ "@types/geojson": "^7946.0.16",
123
135
  "@types/jsonwebtoken": "^9.0.10",
136
+ "@types/proj4": "^2.19.0",
137
+ "geojson": "^0.5.0",
124
138
  "jsonwebtoken": "^9.0.2",
125
139
  "next": "^15.4.2",
140
+ "ol": "^10.6.1",
141
+ "ol-geocoder": "^4.3.3",
142
+ "ol-mapbox-style": "^13.0.1",
143
+ "proj4": "^2.19.10",
126
144
  "react": "^19.1.0",
127
145
  "react-dom": "^19.1.0",
128
- "react-icons": "^5.5.0"
146
+ "react-icons": "^5.5.0",
147
+ "react-select": "^5.10.2"
129
148
  },
130
149
  "release": {
131
150
  "extends": "./release.config.js"
@@ -1,3 +1,5 @@
1
+ @import './ol.css';
2
+
1
3
  /* Global CSS for the component library */
2
4
  @tailwind base;
3
5
  @tailwind components;
@@ -0,0 +1,122 @@
1
+ @import 'ol/ol.css';
2
+ @import 'ol-geocoder/dist/ol-geocoder.min.css';
3
+
4
+ .ol-zoom {
5
+ left: unset;
6
+ right: 0.5rem;
7
+ }
8
+
9
+ .ol-layer-switcher {
10
+ top: 65px;
11
+ right: 0.5rem;
12
+ }
13
+
14
+ .ol-btn {
15
+ display: flex !important;
16
+ justify-content: center;
17
+ align-items: center;
18
+ }
19
+
20
+ .ol-layer-switcher-panel {
21
+ position: absolute;
22
+ top: 0;
23
+ right: -100%; /* Start off-screen */
24
+ min-width: fit-content;
25
+ max-width: 80vw;
26
+ background-color: #008938;
27
+ display: flex;
28
+ flex-direction: row;
29
+ align-items: center;
30
+ padding: 10px;
31
+ gap: 10px;
32
+ transition: right 0.3s ease-in-out;
33
+ overflow: hidden;
34
+ white-space: nowrap;
35
+ }
36
+
37
+ .ol-layer-switcher-panel.open {
38
+ right: 1.5rem; /* Slide into view */
39
+ }
40
+
41
+ .ol-layer-switcher-panel button {
42
+ display: flex;
43
+ flex-direction: column;
44
+ align-items: center;
45
+ justify-content: center;
46
+ border: 2px solid #ccc;
47
+ background-color: #fff;
48
+ min-width: 135px;
49
+ min-height: 135px;
50
+ cursor: pointer;
51
+ color: #000;
52
+ }
53
+
54
+ .ol-layer-switcher-panel button:hover {
55
+ background-color: #ddd;
56
+ }
57
+
58
+ .ol-layer-switcher-panel button.active {
59
+ background-color: #007bff; /* Bright blue background */
60
+ color: white; /* White text */
61
+ border-color: #0056b3; /* Darker border for contrast */
62
+ box-shadow: 0 4px 10px rgba(0, 123, 255, 0.3); /* Subtle shadow effect */
63
+ transform: scale(1.05); /* Slightly scale the active button */
64
+ transition: all 0.2s ease; /* Smooth transition for all styles */
65
+ }
66
+
67
+ .ol-layer-switcher-panel button.active img {
68
+ filter: brightness(1.2); /* Makes the image brighter */
69
+ }
70
+
71
+ .ol-layer-switcher-panel button:not(.active) img {
72
+ filter: grayscale(1); /* Apply grayscale to inactive images */
73
+ }
74
+
75
+ .ol-layer-switcher-panel button img {
76
+ max-width: 80px;
77
+ height: auto;
78
+ border: 2px solid #000;
79
+ border-radius: 0.3rem;
80
+ }
81
+
82
+ .ol-layer-switcher-panel button div {
83
+ font-size: 14px;
84
+ padding-top: 0.5rem;
85
+ }
86
+
87
+ .ol-geocoder .gcd-txt-control {
88
+ height: unset !important;
89
+ }
90
+
91
+ .ol-geocoder .gcd-txt-glass,
92
+ .ol-geocoder ul.gcd-txt-result {
93
+ top: unset !important;
94
+ }
95
+
96
+ .ol-geocoder ul.gcd-txt-result>li:nth-child(odd) {
97
+ background-color: #008938;
98
+ }
99
+
100
+ .ol-geocoder ul.gcd-txt-result>li:nth-child(even) {
101
+ background-color: #bddabd;
102
+ }
103
+
104
+ .ol-geocoder ul.gcd-txt-result>li>a:hover {
105
+ background-color: #fff;
106
+ }
107
+
108
+ /* Change the text color on hover */
109
+ .ol-geocoder ul.gcd-txt-result>li>a:hover .gcd-address,
110
+ .ol-geocoder ul.gcd-txt-result>li>a:hover .gcd-road,
111
+ .ol-geocoder ul.gcd-txt-result>li>a:hover .gcd-city,
112
+ .ol-geocoder ul.gcd-txt-result>li>a:hover .gcd-country {
113
+ color: #000;
114
+ }
115
+
116
+ /* Ensure the text color when not hovering */
117
+ .ol-geocoder ul.gcd-txt-result .gcd-address,
118
+ .ol-geocoder ul.gcd-txt-result .gcd-road,
119
+ .ol-geocoder ul.gcd-txt-result .gcd-city,
120
+ .ol-geocoder ul.gcd-txt-result .gcd-country {
121
+ color: #fff;
122
+ }
@@ -14,25 +14,25 @@ export const AllButtons: StoryObj<typeof Button> = {
14
14
  render: () => (
15
15
  <div className="flex flex-col gap-4">
16
16
  <div>
17
- <Button type="primary" onClick={action('primary-click')}>
17
+ <Button variant="primary" onClick={action('primary-click')}>
18
18
  Primary
19
19
  </Button>
20
20
  </div>
21
21
 
22
22
  <div>
23
- <Button type="secondary" onClick={action('secondary-click')}>
23
+ <Button variant="secondary" onClick={action('secondary-click')}>
24
24
  Secondary
25
25
  </Button>
26
26
  </div>
27
27
 
28
28
  <div className="p-4 bg-brand">
29
- <Button type="inverse" onClick={action('inverse-click')}>
29
+ <Button variant="inverse" onClick={action('inverse-click')}>
30
30
  Inverse
31
31
  </Button>
32
32
  </div>
33
33
 
34
34
  <div>
35
- <Button type="primary" disabled>
35
+ <Button variant="primary" disabled>
36
36
  Disabled Button
37
37
  </Button>
38
38
  </div>
@@ -1,21 +1,48 @@
1
+ import { twMerge } from 'tailwind-merge';
2
+
1
3
  export type HeadingProps = {
2
4
  type: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
5
+ className?: string;
3
6
  children: React.ReactNode;
4
7
  };
5
8
 
6
- export const Heading = ({ type, children }: HeadingProps) => {
9
+ export const Heading = ({ type, className, children }: HeadingProps) => {
7
10
  switch (type) {
8
11
  case 'h1':
9
- return <h1 className="py-4 text-4xl font-bold text-text-primary">{children}</h1>;
12
+ return (
13
+ <h1 className={twMerge('py-4 text-4xl font-bold text-text-primary', className)}>
14
+ {children}
15
+ </h1>
16
+ );
10
17
  case 'h2':
11
- return <h2 className="py-3 text-3xl font-bold text-text-primary">{children}</h2>;
18
+ return (
19
+ <h2 className={twMerge('py-3 text-3xl font-bold text-text-primary', className)}>
20
+ {children}
21
+ </h2>
22
+ );
12
23
  case 'h3':
13
- return <h3 className="py-3 text-xl font-bold text-text-primary">{children}</h3>;
24
+ return (
25
+ <h3 className={twMerge('py-3 text-xl font-bold text-text-primary', className)}>
26
+ {children}
27
+ </h3>
28
+ );
14
29
  case 'h4':
15
- return <h4 className="py-3 text-lg font-bold text-text-primary">{children}</h4>;
30
+ return (
31
+ <h4 className={twMerge('py-3 text-lg font-bold text-text-primary', className)}>
32
+ {children}
33
+ </h4>
34
+ );
16
35
  case 'h5':
17
- return <h5 className="py-2 text-base font-bold text-text-primary">{children}</h5>;
36
+ return (
37
+ <h5 className={twMerge('py-2 text-base font-bold text-text-primary', className)}>
38
+ {children}
39
+ </h5>
40
+ );
18
41
  case 'h6':
19
- return <h6 className="py-2 text-sm font-bold text-text-primary">{children}</h6>;
42
+ return (
43
+ <h6 className={twMerge('py-2 text-sm font-bold text-text-primary', className)}>
44
+ {children}
45
+ </h6>
46
+ );
20
47
  }
21
48
  };
@@ -0,0 +1,252 @@
1
+ /* eslint-disable storybook/no-renderer-packages */
2
+ import { useEffect, useState } from 'react';
3
+
4
+ import type { Meta, StoryObj } from '@storybook/react';
5
+
6
+ import { Modal } from './Modal';
7
+
8
+ const MODAL_ROOT_ID = 'modal-root';
9
+
10
+ const meta: Meta<typeof Modal> = {
11
+ title: 'Components/Modal',
12
+ component: Modal,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ description: {
17
+ component: 'A modal dialog component that renders content in a portal overlay.',
18
+ },
19
+ },
20
+ },
21
+ tags: ['autodocs'],
22
+ argTypes: {
23
+ isOpen: {
24
+ control: 'boolean',
25
+ description: 'Whether the modal is open',
26
+ },
27
+ onClose: {
28
+ action: 'onClose',
29
+ description: 'Callback function called when the modal should close',
30
+ },
31
+ children: {
32
+ control: false,
33
+ description: 'Content to display inside the modal',
34
+ },
35
+ },
36
+ decorators: [
37
+ // eslint-disable-next-line @typescript-eslint/naming-convention
38
+ (Story) => {
39
+ useEffect(() => {
40
+ // Ensure modal-root exists
41
+ if (!document.getElementById(MODAL_ROOT_ID)) {
42
+ const modalRoot = document.createElement('div');
43
+
44
+ modalRoot.id = MODAL_ROOT_ID;
45
+ document.body.appendChild(modalRoot);
46
+ }
47
+
48
+ return () => {
49
+ // Clean up on unmount
50
+ const modalRoot = document.getElementById(MODAL_ROOT_ID);
51
+
52
+ if (modalRoot) {
53
+ document.body.removeChild(modalRoot);
54
+ }
55
+ };
56
+ }, []);
57
+
58
+ return (
59
+ <div>
60
+ <Story />
61
+ </div>
62
+ );
63
+ },
64
+ ],
65
+ };
66
+
67
+ export default meta;
68
+ type Story = StoryObj<typeof Modal>;
69
+
70
+ export const Default: Story = {
71
+ args: {
72
+ isOpen: true,
73
+ children: (
74
+ <div>
75
+ <h2 className="text-xl font-bold mb-4">Modal Title</h2>
76
+
77
+ <p className="text-gray-600 mb-4">
78
+ This is a basic modal with some content. You can close it by clicking the X button,
79
+ pressing Escape, or clicking outside the modal.
80
+ </p>
81
+
82
+ <div className="flex gap-2">
83
+ <button className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
84
+ Confirm
85
+ </button>
86
+
87
+ <button className="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">
88
+ Cancel
89
+ </button>
90
+ </div>
91
+ </div>
92
+ ),
93
+ },
94
+ };
95
+
96
+ export const Closed: Story = {
97
+ args: {
98
+ isOpen: false,
99
+ children: (
100
+ <div>
101
+ <h2 className="text-xl font-bold mb-4">You won&apos;t see this</h2>
102
+
103
+ <p>This modal is closed, so the content is not visible.</p>
104
+ </div>
105
+ ),
106
+ },
107
+ };
108
+
109
+ export const SimpleMessage: Story = {
110
+ args: {
111
+ isOpen: true,
112
+ children: (
113
+ <div className="text-center">
114
+ <h3 className="text-lg font-semibold mb-2">Success!</h3>
115
+
116
+ <p className="text-gray-600">Your action was completed successfully.</p>
117
+ </div>
118
+ ),
119
+ },
120
+ };
121
+
122
+ type ModalWrapperProps = {
123
+ children: React.ReactNode;
124
+ isOpen: boolean;
125
+ onClose: () => void;
126
+ };
127
+
128
+ const ModalWrapper = ({ children, isOpen, onClose }: ModalWrapperProps) => {
129
+ useEffect(() => {
130
+ if (!document.getElementById(MODAL_ROOT_ID)) {
131
+ const modalRoot = document.createElement('div');
132
+
133
+ modalRoot.id = MODAL_ROOT_ID;
134
+ document.body.appendChild(modalRoot);
135
+ }
136
+ }, []);
137
+
138
+ return (
139
+ <Modal isOpen={isOpen} onClose={onClose}>
140
+ {children}
141
+ </Modal>
142
+ );
143
+ };
144
+
145
+ export const WithWrapper: Story = {
146
+ render: (args) => (
147
+ <ModalWrapper {...args}>
148
+ <div>
149
+ <h2 className="text-xl font-bold mb-4">Modal with Wrapper</h2>
150
+
151
+ <p className="text-gray-600">This modal uses a wrapper to ensure modal-root exists.</p>
152
+ </div>
153
+ </ModalWrapper>
154
+ ),
155
+ args: {
156
+ isOpen: true,
157
+ },
158
+ };
159
+
160
+ export const Interactive: Story = {
161
+ render: () => {
162
+ const [isOpen, setIsOpen] = useState(false);
163
+ const [selectedModal, setSelectedModal] = useState<string | null>(null);
164
+
165
+ useEffect(() => {
166
+ if (!document.getElementById(MODAL_ROOT_ID)) {
167
+ const modalRoot = document.createElement('div');
168
+
169
+ modalRoot.id = MODAL_ROOT_ID;
170
+ document.body.appendChild(modalRoot);
171
+ }
172
+ }, []);
173
+
174
+ const openModal = (type: string) => {
175
+ setSelectedModal(type);
176
+ setIsOpen(true);
177
+ };
178
+
179
+ const closeModal = () => {
180
+ setIsOpen(false);
181
+ setSelectedModal(null);
182
+ };
183
+
184
+ const renderModalContent = () => {
185
+ switch (selectedModal) {
186
+ case 'info':
187
+ return (
188
+ <div>
189
+ <h3 className="text-lg font-semibold mb-2">Information</h3>
190
+
191
+ <p className="text-gray-600">This is an informational modal.</p>
192
+ </div>
193
+ );
194
+ case 'warning':
195
+ return (
196
+ <div>
197
+ <h3 className="text-lg font-semibold mb-2 text-yellow-600">Warning</h3>
198
+
199
+ <p className="text-gray-600">This action requires confirmation.</p>
200
+ </div>
201
+ );
202
+ case 'error':
203
+ return (
204
+ <div>
205
+ <h3 className="text-lg font-semibold mb-2 text-red-600">Error</h3>
206
+
207
+ <p className="text-gray-600">Something went wrong. Please try again.</p>
208
+ </div>
209
+ );
210
+ default:
211
+ return <p>Default modal content</p>;
212
+ }
213
+ };
214
+
215
+ return (
216
+ <div className="p-8">
217
+ <h2 className="text-xl font-bold mb-4">Interactive Modal Demo</h2>
218
+
219
+ <p className="text-gray-600 mb-6">
220
+ Click any button below to open different types of modals.
221
+ </p>
222
+
223
+ <div className="space-x-4">
224
+ <button
225
+ onClick={() => openModal('info')}
226
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"
227
+ >
228
+ Info Modal
229
+ </button>
230
+
231
+ <button
232
+ onClick={() => openModal('warning')}
233
+ className="px-4 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600"
234
+ >
235
+ Warning Modal
236
+ </button>
237
+
238
+ <button
239
+ onClick={() => openModal('error')}
240
+ className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
241
+ >
242
+ Error Modal
243
+ </button>
244
+ </div>
245
+
246
+ <Modal isOpen={isOpen} onClose={closeModal}>
247
+ {renderModalContent()}
248
+ </Modal>
249
+ </div>
250
+ );
251
+ },
252
+ };