@tpzdsp/next-toolkit 1.12.1 → 1.14.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/README.md +4 -4
- package/package.json +13 -6
- package/src/assets/styles/ol.css +147 -176
- package/src/components/ButtonLink/ButtonLink.stories.tsx +72 -0
- package/src/components/ButtonLink/ButtonLink.test.tsx +154 -0
- package/src/components/ButtonLink/ButtonLink.tsx +33 -0
- package/src/components/InfoBox/InfoBox.stories.tsx +460 -0
- package/src/components/InfoBox/InfoBox.test.tsx +330 -0
- package/src/components/InfoBox/InfoBox.tsx +168 -0
- package/src/components/InfoBox/types.ts +6 -0
- package/src/components/LinkButton/LinkButton.stories.tsx +74 -0
- package/src/components/LinkButton/LinkButton.test.tsx +177 -0
- package/src/components/LinkButton/LinkButton.tsx +80 -0
- package/src/components/index.ts +5 -0
- package/src/components/link/ExternalLink.test.tsx +104 -0
- package/src/components/link/ExternalLink.tsx +1 -0
- package/src/map/FullScreenControl.ts +126 -0
- package/src/map/LayerSwitcherControl.ts +87 -181
- package/src/map/LayerSwitcherPanel.tsx +173 -0
- package/src/map/MapComponent.tsx +12 -46
- package/src/map/createControlButton.ts +72 -0
- package/src/map/geocoder/Geocoder.test.tsx +115 -0
- package/src/map/geocoder/Geocoder.tsx +393 -0
- package/src/map/geocoder/groupResults.ts +12 -0
- package/src/map/geocoder/index.ts +4 -0
- package/src/map/geocoder/types.ts +11 -0
- package/src/map/index.ts +4 -1
- package/src/map/osOpenNamesSearch.ts +112 -57
- package/src/test/renderers.tsx +9 -20
- package/src/map/geocoder.ts +0 -61
- package/src/ol-geocoder.d.ts +0 -1
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ interface MyComponentProps extends BaseProps {
|
|
|
139
139
|
|
|
140
140
|
```bash
|
|
141
141
|
# Install OpenLayers dependencies
|
|
142
|
-
npm install ol ol-
|
|
142
|
+
npm install ol ol-mapbox-style proj4
|
|
143
143
|
```
|
|
144
144
|
|
|
145
145
|
```typescript
|
|
@@ -152,14 +152,14 @@ import '@your-org/nextjs-library/styles/ol';
|
|
|
152
152
|
Some components require additional peer dependencies that are marked as optional:
|
|
153
153
|
|
|
154
154
|
- **Select components**: Require `react-select`
|
|
155
|
-
- **Map components**: Require OpenLayers packages (`ol`, `ol-
|
|
155
|
+
- **Map components**: Require OpenLayers packages (`ol`, `ol-mapbox-style`, etc.)
|
|
156
156
|
|
|
157
157
|
```bash
|
|
158
158
|
# For Select components
|
|
159
159
|
npm install react-select
|
|
160
160
|
|
|
161
161
|
# For Map components
|
|
162
|
-
npm install ol ol-
|
|
162
|
+
npm install ol ol-mapbox-style proj4 @turf/turf geojson
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
```typescript
|
|
@@ -451,7 +451,7 @@ npm install react-select
|
|
|
451
451
|
|
|
452
452
|
# Error: Cannot resolve module 'ol/ol.css'
|
|
453
453
|
# Solution: Install OpenLayers dependencies
|
|
454
|
-
npm install ol ol-
|
|
454
|
+
npm install ol ol-mapbox-style proj4
|
|
455
455
|
|
|
456
456
|
# Or import components selectively to avoid these dependencies:
|
|
457
457
|
import { Button, Card } from '@your-org/nextjs-library'; // ✅ No extra deps needed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpzdsp/next-toolkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.0",
|
|
4
4
|
"description": "A reusable React component library for Next.js applications",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">= 24.12.0",
|
|
@@ -57,6 +57,11 @@
|
|
|
57
57
|
"import": "./src/components/select/index.ts",
|
|
58
58
|
"require": "./src/components/select/index.ts"
|
|
59
59
|
},
|
|
60
|
+
"./components/info-box": {
|
|
61
|
+
"types": "./src/components/InfoBox/InfoBox.tsx",
|
|
62
|
+
"import": "./src/components/InfoBox/InfoBox.tsx",
|
|
63
|
+
"require": "./src/components/InfoBox/InfoBox.tsx"
|
|
64
|
+
},
|
|
60
65
|
"./http": {
|
|
61
66
|
"types": "./src/http/index.ts",
|
|
62
67
|
"import": "./src/http/index.ts",
|
|
@@ -171,7 +176,6 @@
|
|
|
171
176
|
"jsonwebtoken": "^9.0.3",
|
|
172
177
|
"next": "^16.1.4",
|
|
173
178
|
"ol": "^10.6.1",
|
|
174
|
-
"ol-geocoder": "^4.3.3",
|
|
175
179
|
"ol-mapbox-style": "^13.0.1",
|
|
176
180
|
"postcss": "^8.5.6",
|
|
177
181
|
"prettier": "^3.8.1",
|
|
@@ -201,6 +205,7 @@
|
|
|
201
205
|
},
|
|
202
206
|
"peerDependencies": {
|
|
203
207
|
"@better-fetch/fetch": "^1.1.21",
|
|
208
|
+
"@floating-ui/react": "^0.27.17",
|
|
204
209
|
"@tanstack/react-query": "^5.90.19",
|
|
205
210
|
"@testing-library/react": "^16.0.0",
|
|
206
211
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -213,7 +218,6 @@
|
|
|
213
218
|
"geojson": "^0.5.0",
|
|
214
219
|
"next": "^16.1.4",
|
|
215
220
|
"ol": "^10.6.1",
|
|
216
|
-
"ol-geocoder": "^4.3.3",
|
|
217
221
|
"ol-mapbox-style": "^13.0.1",
|
|
218
222
|
"proj4": "^2.19.10",
|
|
219
223
|
"react": "^19.2.1",
|
|
@@ -229,6 +233,9 @@
|
|
|
229
233
|
"@better-fetch/fetch": {
|
|
230
234
|
"optional": true
|
|
231
235
|
},
|
|
236
|
+
"@floating-ui/react": {
|
|
237
|
+
"optional": true
|
|
238
|
+
},
|
|
232
239
|
"@turf/turf": {
|
|
233
240
|
"optional": true
|
|
234
241
|
},
|
|
@@ -256,9 +263,6 @@
|
|
|
256
263
|
"ol": {
|
|
257
264
|
"optional": true
|
|
258
265
|
},
|
|
259
|
-
"ol-geocoder": {
|
|
260
|
-
"optional": true
|
|
261
|
-
},
|
|
262
266
|
"ol-mapbox-style": {
|
|
263
267
|
"optional": true
|
|
264
268
|
},
|
|
@@ -277,5 +281,8 @@
|
|
|
277
281
|
},
|
|
278
282
|
"release": {
|
|
279
283
|
"extends": "./release.config.js"
|
|
284
|
+
},
|
|
285
|
+
"dependencies": {
|
|
286
|
+
"@floating-ui/react": "^0.27.17"
|
|
280
287
|
}
|
|
281
288
|
}
|
package/src/assets/styles/ol.css
CHANGED
|
@@ -1,42 +1,93 @@
|
|
|
1
1
|
@import 'ol/ol.css';
|
|
2
|
-
@import 'ol-geocoder/dist/ol-geocoder.min.css';
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* OpenLayers Control Styles
|
|
5
|
+
*
|
|
6
|
+
* This file contains styles for OpenLayers map controls (zoom, layer switcher, fullscreen).
|
|
7
|
+
*
|
|
8
|
+
* Design System:
|
|
9
|
+
* - All control buttons extend the `.ol-btn` base class
|
|
10
|
+
* - Control panels should be implemented as React components with Tailwind CSS
|
|
11
|
+
* - Use CSS custom properties (--focus-color, etc.) for consistent theming
|
|
12
|
+
*
|
|
13
|
+
* To add a new control:
|
|
14
|
+
* 1. Add the `ol-btn` class to your button element
|
|
15
|
+
* 2. Override only the properties you need (e.g., font-size, icon)
|
|
16
|
+
* 3. Position the control container with absolute positioning
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
:root {
|
|
20
|
+
--focus-color: #ffdd00;
|
|
21
|
+
--focus-outline-color: #ffbf47;
|
|
22
|
+
--control-bg: #fff;
|
|
23
|
+
--control-border: #505a5f;
|
|
24
|
+
--control-text: #0b0c0c;
|
|
25
|
+
--control-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Base button class for all OpenLayers controls */
|
|
29
|
+
.ol-btn,
|
|
30
|
+
.ol-btn:hover,
|
|
31
|
+
.ol-btn:active,
|
|
32
|
+
.ol-btn:focus,
|
|
33
|
+
.ol-btn:focus-visible {
|
|
34
|
+
width: 40px !important;
|
|
35
|
+
height: 40px !important;
|
|
36
|
+
border: none !important;
|
|
37
|
+
outline: none !important;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ol-btn {
|
|
41
|
+
background: var(--control-bg);
|
|
42
|
+
color: var(--control-text);
|
|
43
|
+
font-size: 1.5rem;
|
|
44
|
+
font-weight: bold;
|
|
45
|
+
line-height: 1;
|
|
46
|
+
box-shadow: var(--control-shadow);
|
|
47
|
+
cursor: pointer;
|
|
48
|
+
transition:
|
|
49
|
+
box-shadow 0.2s,
|
|
50
|
+
background 0.2s;
|
|
51
|
+
margin: 0;
|
|
52
|
+
padding: 0;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/* Ensure SVG icons in buttons are consistently sized and centered */
|
|
59
|
+
.ol-btn svg {
|
|
60
|
+
width: 20px !important;
|
|
61
|
+
height: 20px !important;
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
display: block;
|
|
64
|
+
margin: 0 auto;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.ol-btn:hover,
|
|
68
|
+
.ol-btn:active {
|
|
69
|
+
background: var(--focus-color) !important;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ol-btn:focus,
|
|
73
|
+
.ol-btn:focus-visible {
|
|
74
|
+
outline: 3px solid var(--focus-outline-color) !important;
|
|
75
|
+
background: var(--focus-color) !important;
|
|
76
|
+
z-index: 2;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Universal OpenLayers control focus style for non-button elements */
|
|
5
80
|
#map:focus,
|
|
6
81
|
.ol-viewport:focus,
|
|
7
82
|
.ol-control:focus,
|
|
8
|
-
.ol-control button:focus,
|
|
9
|
-
.ol-control button:focus-visible,
|
|
10
83
|
.ol-attribution:focus,
|
|
11
|
-
.ol-attribution button:focus,
|
|
12
|
-
.ol-zoom:focus,
|
|
13
|
-
.ol-zoom button:focus,
|
|
14
|
-
.ol-layer-switcher-toggle:focus,
|
|
15
|
-
.ol-layer-switcher-close:focus,
|
|
16
|
-
.ol-layer-switcher-panel button:focus,
|
|
17
84
|
.ol-attribution ul li a:focus,
|
|
18
|
-
.ol-attribution ul li a:focus-visible
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
outline: 3px solid #ffbf47 !important;
|
|
22
|
-
border-color: #ffbf47 !important;
|
|
85
|
+
.ol-attribution ul li a:focus-visible {
|
|
86
|
+
outline: 3px solid var(--focus-outline-color) !important;
|
|
87
|
+
border-color: var(--focus-outline-color) !important;
|
|
23
88
|
z-index: 2;
|
|
24
89
|
}
|
|
25
90
|
|
|
26
|
-
.ol-geocoder #gcd-input-search:focus,
|
|
27
|
-
.ol-geocoder .gcd-txt-search:focus,
|
|
28
|
-
.ol-geocoder .gcd-txt-search:focus-visible {
|
|
29
|
-
background-color: #ffbf47 !important;
|
|
30
|
-
z-index: 2;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
.ol-geocoder .gcd-txt-input:focus,
|
|
34
|
-
.ol-geocoder .gcd-txt-input:focus-visible {
|
|
35
|
-
box-shadow:
|
|
36
|
-
inset 0 0 0 1px #ffbf47,
|
|
37
|
-
inset 0 0 6px #ffbf47;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
91
|
/* Zoom control container */
|
|
41
92
|
.ol-zoom {
|
|
42
93
|
top: 1.5rem;
|
|
@@ -47,61 +98,26 @@
|
|
|
47
98
|
z-index: 10;
|
|
48
99
|
}
|
|
49
100
|
|
|
50
|
-
/*
|
|
101
|
+
/* Apply base button styling to zoom buttons */
|
|
51
102
|
.ol-zoom button,
|
|
52
|
-
.ol-control button {
|
|
53
|
-
width: 40px;
|
|
54
|
-
height: 40px;
|
|
55
|
-
background: #fff;
|
|
56
|
-
color: #0b0c0c;
|
|
57
|
-
font-size: 2rem;
|
|
58
|
-
font-weight: bold;
|
|
59
|
-
line-height: 1;
|
|
60
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
|
61
|
-
cursor: pointer;
|
|
62
|
-
transition:
|
|
63
|
-
border-color 0.2s,
|
|
64
|
-
box-shadow 0.2s,
|
|
65
|
-
background 0.2s;
|
|
66
|
-
margin: 0;
|
|
67
|
-
padding: 0;
|
|
68
|
-
display: flex;
|
|
69
|
-
align-items: center;
|
|
70
|
-
justify-content: center;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
103
|
.ol-zoom button:hover,
|
|
74
|
-
.ol-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.ol-layer-switcher {
|
|
82
|
-
top: 0px;
|
|
83
|
-
right: 1rem;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.ol-layer-switcher-toggle {
|
|
87
|
-
position: absolute;
|
|
88
|
-
top: 110px;
|
|
89
|
-
right: 0px;
|
|
104
|
+
.ol-zoom button:active,
|
|
105
|
+
.ol-zoom button:focus,
|
|
106
|
+
.ol-zoom button:focus-visible {
|
|
107
|
+
width: 40px !important;
|
|
108
|
+
height: 40px !important;
|
|
109
|
+
border: none !important;
|
|
110
|
+
outline: none !important;
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
.ol-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
background: #fff;
|
|
96
|
-
border: 2px solid #505a5f;
|
|
97
|
-
color: #0b0c0c;
|
|
98
|
-
font-size: 1.5rem;
|
|
113
|
+
.ol-zoom button {
|
|
114
|
+
background: var(--control-bg) !important;
|
|
115
|
+
color: var(--control-text) !important;
|
|
99
116
|
font-weight: bold;
|
|
100
117
|
line-height: 1;
|
|
101
|
-
box-shadow:
|
|
118
|
+
box-shadow: var(--control-shadow);
|
|
102
119
|
cursor: pointer;
|
|
103
120
|
transition:
|
|
104
|
-
border-color 0.2s,
|
|
105
121
|
box-shadow 0.2s,
|
|
106
122
|
background 0.2s;
|
|
107
123
|
margin: 0;
|
|
@@ -109,124 +125,79 @@
|
|
|
109
125
|
display: flex;
|
|
110
126
|
align-items: center;
|
|
111
127
|
justify-content: center;
|
|
128
|
+
font-size: 0 !important; /* Hide the default +/- text */
|
|
129
|
+
position: relative;
|
|
112
130
|
}
|
|
113
131
|
|
|
114
|
-
.ol-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
right: -100%;
|
|
118
|
-
display: flex;
|
|
119
|
-
flex-direction: column;
|
|
120
|
-
align-items: stretch;
|
|
121
|
-
padding: 1rem 1rem;
|
|
122
|
-
min-width: 220px;
|
|
123
|
-
background: #fff;
|
|
124
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
125
|
-
gap: 0.5rem;
|
|
126
|
-
transition: right 0.3s ease-in-out;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.ol-layer-switcher-panel.open {
|
|
130
|
-
right: 3rem;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/* Header for the close button, aligns it to the top-right */
|
|
134
|
-
.ol-layer-switcher-header {
|
|
135
|
-
display: flex;
|
|
136
|
-
justify-content: flex-end;
|
|
137
|
-
align-items: center;
|
|
138
|
-
width: 100%;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/* Small, circular close button */
|
|
142
|
-
.ol-layer-switcher-close {
|
|
143
|
-
font-size: 1.25rem;
|
|
144
|
-
width: 2rem;
|
|
145
|
-
height: 2rem;
|
|
146
|
-
padding: 0;
|
|
147
|
-
background: none;
|
|
148
|
-
border: none;
|
|
149
|
-
color: #505a5f;
|
|
150
|
-
cursor: pointer;
|
|
151
|
-
border-radius: 50%;
|
|
152
|
-
transition: background 0.2s;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.ol-layer-switcher-close:hover,
|
|
156
|
-
.ol-layer-switcher-close:focus {
|
|
157
|
-
background: #e1e1e1;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/* Content area for basemap buttons */
|
|
161
|
-
.ol-layer-switcher-content {
|
|
162
|
-
display: flex;
|
|
163
|
-
flex-direction: row;
|
|
164
|
-
gap: 10px;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.ol-layer-switcher-content button {
|
|
168
|
-
display: flex;
|
|
169
|
-
flex-direction: column;
|
|
170
|
-
align-items: center;
|
|
171
|
-
justify-content: center;
|
|
172
|
-
border: 2px solid #ccc;
|
|
173
|
-
background-color: #fff;
|
|
174
|
-
min-width: 135px;
|
|
175
|
-
min-height: 135px;
|
|
176
|
-
cursor: pointer;
|
|
177
|
-
color: #000;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
.ol-layer-switcher-content button:hover {
|
|
181
|
-
background-color: #ddd;
|
|
132
|
+
.ol-zoom button:hover,
|
|
133
|
+
.ol-zoom button:active {
|
|
134
|
+
background: var(--focus-color) !important;
|
|
182
135
|
}
|
|
183
136
|
|
|
184
|
-
.ol-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
transform: scale(1.05);
|
|
190
|
-
transition: all 0.2s ease;
|
|
137
|
+
.ol-zoom button:focus,
|
|
138
|
+
.ol-zoom button:focus-visible {
|
|
139
|
+
outline: 3px solid var(--focus-outline-color) !important;
|
|
140
|
+
background: var(--focus-color) !important;
|
|
141
|
+
z-index: 2;
|
|
191
142
|
}
|
|
192
143
|
|
|
193
|
-
|
|
194
|
-
|
|
144
|
+
/* Override default zoom button text with SVG icons */
|
|
145
|
+
.ol-zoom-in::before,
|
|
146
|
+
.ol-zoom-out::before {
|
|
147
|
+
content: '';
|
|
148
|
+
display: block;
|
|
149
|
+
width: 20px;
|
|
150
|
+
height: 20px;
|
|
151
|
+
background-size: contain;
|
|
152
|
+
background-repeat: no-repeat;
|
|
153
|
+
background-position: center;
|
|
154
|
+
font-size: 0; /* Ensure no text shows */
|
|
195
155
|
}
|
|
196
156
|
|
|
197
|
-
.ol-
|
|
198
|
-
|
|
157
|
+
.ol-zoom-in::before {
|
|
158
|
+
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 5v14m-7-7h14" stroke="%230b0c0c" stroke-width="2" stroke-linecap="round"/></svg>');
|
|
199
159
|
}
|
|
200
160
|
|
|
201
|
-
.ol-
|
|
202
|
-
|
|
203
|
-
height: auto;
|
|
204
|
-
border: 2px solid #000;
|
|
161
|
+
.ol-zoom-out::before {
|
|
162
|
+
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M5 12h14" stroke="%230b0c0c" stroke-width="2" stroke-linecap="round"/></svg>');
|
|
205
163
|
}
|
|
206
164
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
165
|
+
/* Layer switcher control */
|
|
166
|
+
.ol-layer-switcher {
|
|
167
|
+
position: absolute;
|
|
168
|
+
top: 110px;
|
|
169
|
+
right: 1rem;
|
|
170
|
+
z-index: 9999;
|
|
210
171
|
}
|
|
211
172
|
|
|
212
|
-
/*
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
173
|
+
/* Full screen control */
|
|
174
|
+
.ol-fullscreen {
|
|
175
|
+
position: absolute;
|
|
176
|
+
top: 155px;
|
|
177
|
+
right: 1rem;
|
|
217
178
|
}
|
|
218
179
|
|
|
219
|
-
|
|
220
|
-
.
|
|
221
|
-
|
|
180
|
+
/* Full screen mode for map container */
|
|
181
|
+
.map-fullscreen {
|
|
182
|
+
position: fixed !important;
|
|
183
|
+
top: 0 !important;
|
|
184
|
+
left: 0 !important;
|
|
185
|
+
right: 0 !important;
|
|
186
|
+
bottom: 0 !important;
|
|
187
|
+
width: 100vw !important;
|
|
188
|
+
height: 100vh !important;
|
|
189
|
+
z-index: 9999 !important;
|
|
190
|
+
margin: 0 !important;
|
|
191
|
+
padding: 0 !important;
|
|
222
192
|
}
|
|
223
193
|
|
|
224
|
-
|
|
225
|
-
.
|
|
226
|
-
|
|
227
|
-
|
|
194
|
+
/* Ensure map fills container in full screen mode */
|
|
195
|
+
.map-fullscreen .ol-viewport {
|
|
196
|
+
width: 100% !important;
|
|
197
|
+
height: 100% !important;
|
|
228
198
|
}
|
|
229
199
|
|
|
230
|
-
|
|
231
|
-
|
|
200
|
+
/* Hide other UI elements when in full screen (optional) */
|
|
201
|
+
.map-fullscreen ~ * {
|
|
202
|
+
display: none;
|
|
232
203
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
|
|
3
|
+
import { ButtonLink } from './ButtonLink';
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: 'Components/ButtonLink',
|
|
7
|
+
component: ButtonLink,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
argTypes: {
|
|
13
|
+
type: {
|
|
14
|
+
control: 'select',
|
|
15
|
+
options: ['button', 'submit', 'reset'],
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof ButtonLink>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof meta>;
|
|
22
|
+
|
|
23
|
+
export const Default: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
children: 'Click me',
|
|
26
|
+
type: 'button',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const WithClickHandler: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
children: 'Show alert',
|
|
33
|
+
onClick: () => alert('Button clicked!'),
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Submit: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
children: 'Submit form',
|
|
40
|
+
type: 'submit',
|
|
41
|
+
},
|
|
42
|
+
render: (args) => (
|
|
43
|
+
<form
|
|
44
|
+
onSubmit={(e) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
alert('Form submitted!');
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<ButtonLink {...args} />
|
|
50
|
+
</form>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Disabled: Story = {
|
|
55
|
+
args: {
|
|
56
|
+
children: 'Disabled button link',
|
|
57
|
+
disabled: true,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithCustomStyling: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
children: 'Custom styled',
|
|
64
|
+
className: 'font-bold text-xl',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const LongText: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
children: 'This is a longer button link that demonstrates how text wraps',
|
|
71
|
+
},
|
|
72
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { ButtonLink } from './ButtonLink';
|
|
2
|
+
import { render, screen, userEvent } from '../../test/renderers';
|
|
3
|
+
|
|
4
|
+
describe('ButtonLink', () => {
|
|
5
|
+
describe('rendering', () => {
|
|
6
|
+
it('should render with children', () => {
|
|
7
|
+
render(<ButtonLink>Click me</ButtonLink>);
|
|
8
|
+
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should render as a button element', () => {
|
|
13
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
14
|
+
|
|
15
|
+
const button = screen.getByRole('button');
|
|
16
|
+
|
|
17
|
+
expect(button.tagName).toBe('BUTTON');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('should have link styling classes', () => {
|
|
21
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
22
|
+
|
|
23
|
+
const button = screen.getByRole('button');
|
|
24
|
+
|
|
25
|
+
expect(button).toHaveClass('text-link', 'underline', 'cursor-pointer');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should merge custom className', () => {
|
|
29
|
+
render(<ButtonLink className="custom-class">Button</ButtonLink>);
|
|
30
|
+
|
|
31
|
+
const button = screen.getByRole('button');
|
|
32
|
+
|
|
33
|
+
expect(button).toHaveClass('custom-class');
|
|
34
|
+
expect(button).toHaveClass('text-link'); // Still has base classes
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('button types', () => {
|
|
39
|
+
it('should default to type="button"', () => {
|
|
40
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
41
|
+
|
|
42
|
+
const button = screen.getByRole('button');
|
|
43
|
+
|
|
44
|
+
expect(button).toHaveAttribute('type', 'button');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should accept type="submit"', () => {
|
|
48
|
+
render(<ButtonLink type="submit">Submit</ButtonLink>);
|
|
49
|
+
|
|
50
|
+
const button = screen.getByRole('button');
|
|
51
|
+
|
|
52
|
+
expect(button).toHaveAttribute('type', 'submit');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should accept type="reset"', () => {
|
|
56
|
+
render(<ButtonLink type="reset">Reset</ButtonLink>);
|
|
57
|
+
|
|
58
|
+
const button = screen.getByRole('button');
|
|
59
|
+
|
|
60
|
+
expect(button).toHaveAttribute('type', 'reset');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('interactions', () => {
|
|
65
|
+
it('should handle click events', async () => {
|
|
66
|
+
const user = userEvent.setup();
|
|
67
|
+
const onClick = vi.fn();
|
|
68
|
+
|
|
69
|
+
render(<ButtonLink onClick={onClick}>Click me</ButtonLink>);
|
|
70
|
+
|
|
71
|
+
await user.click(screen.getByRole('button'));
|
|
72
|
+
|
|
73
|
+
expect(onClick).toHaveBeenCalledTimes(1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should not trigger onClick when disabled', async () => {
|
|
77
|
+
const user = userEvent.setup();
|
|
78
|
+
const onClick = vi.fn();
|
|
79
|
+
|
|
80
|
+
render(
|
|
81
|
+
<ButtonLink onClick={onClick} disabled>
|
|
82
|
+
Disabled
|
|
83
|
+
</ButtonLink>,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
await user.click(screen.getByRole('button'));
|
|
87
|
+
|
|
88
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should submit form when type="submit"', () => {
|
|
92
|
+
const onSubmit = vi.fn((e) => e.preventDefault());
|
|
93
|
+
|
|
94
|
+
render(
|
|
95
|
+
<form onSubmit={onSubmit}>
|
|
96
|
+
<ButtonLink type="submit">Submit</ButtonLink>
|
|
97
|
+
</form>,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const button = screen.getByRole('button');
|
|
101
|
+
|
|
102
|
+
button.click();
|
|
103
|
+
|
|
104
|
+
expect(onSubmit).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('disabled state', () => {
|
|
109
|
+
it('should have disabled attribute when disabled', () => {
|
|
110
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
111
|
+
|
|
112
|
+
const button = screen.getByRole('button');
|
|
113
|
+
|
|
114
|
+
expect(button).toBeDisabled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should have reduced opacity when disabled', () => {
|
|
118
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
119
|
+
|
|
120
|
+
const button = screen.getByRole('button');
|
|
121
|
+
|
|
122
|
+
expect(button).toHaveClass('disabled:opacity-50');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should have not-allowed cursor when disabled', () => {
|
|
126
|
+
render(<ButtonLink disabled>Disabled</ButtonLink>);
|
|
127
|
+
|
|
128
|
+
const button = screen.getByRole('button');
|
|
129
|
+
|
|
130
|
+
expect(button).toHaveClass('disabled:cursor-not-allowed');
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('accessibility', () => {
|
|
135
|
+
it('should have button role', () => {
|
|
136
|
+
render(<ButtonLink>Button</ButtonLink>);
|
|
137
|
+
|
|
138
|
+
expect(screen.getByRole('button')).toBeInTheDocument();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should accept aria attributes', () => {
|
|
142
|
+
render(
|
|
143
|
+
<ButtonLink aria-label="Custom label" aria-describedby="description">
|
|
144
|
+
Button
|
|
145
|
+
</ButtonLink>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const button = screen.getByRole('button', { name: 'Custom label' });
|
|
149
|
+
|
|
150
|
+
expect(button).toBeInTheDocument();
|
|
151
|
+
expect(button).toHaveAttribute('aria-describedby', 'description');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|