@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.
- package/package.json +21 -2
- package/src/assets/styles/globals.css +2 -0
- package/src/assets/styles/ol.css +122 -0
- package/src/components/Button/Button.stories.tsx +4 -4
- package/src/components/Heading/Heading.tsx +34 -7
- 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/SlidingPanel/SlidingPanel.stories.tsx +31 -0
- package/src/components/SlidingPanel/SlidingPanel.test.tsx +86 -0
- package/src/components/SlidingPanel/SlidingPanel.tsx +133 -0
- 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/index.ts +11 -2
- package/src/components/layout/header/HeaderAuthClient.tsx +16 -8
- package/src/components/layout/header/HeaderNavClient.tsx +2 -2
- package/src/components/map/LayerSwitcherControl.ts +147 -0
- package/src/components/map/Map.tsx +230 -0
- package/src/components/map/MapContext.tsx +211 -0
- package/src/components/map/Popup.tsx +74 -0
- package/src/components/map/basemaps.ts +79 -0
- package/src/components/map/geocoder.ts +61 -0
- package/src/components/map/geometries.ts +60 -0
- package/src/components/map/images/basemaps/OS.png +0 -0
- package/src/components/map/images/basemaps/dark.png +0 -0
- package/src/components/map/images/basemaps/sat-map-tiler.png +0 -0
- package/src/components/map/images/basemaps/satellite-map-tiler.png +0 -0
- package/src/components/map/images/basemaps/satellite.png +0 -0
- package/src/components/map/images/basemaps/streets.png +0 -0
- package/src/components/map/images/openlayers-logo.png +0 -0
- package/src/components/map/index.ts +10 -0
- package/src/components/map/map.ts +40 -0
- package/src/components/map/osOpenNamesSearch.ts +54 -0
- package/src/components/map/projections.ts +14 -0
- 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/types.ts +51 -1
- package/src/utils/http.ts +143 -0
- package/src/utils/index.ts +1 -0
- package/src/components/link/NextLinkWrapper.tsx +0 -66
- 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
|
|
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"
|
|
@@ -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
|
|
17
|
+
<Button variant="primary" onClick={action('primary-click')}>
|
|
18
18
|
Primary
|
|
19
19
|
</Button>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
22
|
<div>
|
|
23
|
-
<Button
|
|
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
|
|
29
|
+
<Button variant="inverse" onClick={action('inverse-click')}>
|
|
30
30
|
Inverse
|
|
31
31
|
</Button>
|
|
32
32
|
</div>
|
|
33
33
|
|
|
34
34
|
<div>
|
|
35
|
-
<Button
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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'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
|
+
};
|