@regardio/react 0.4.7 → 0.5.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.
- package/LICENSE +1 -1
- package/README.md +5 -5
- package/dist/{components/background-slideshow.js → background-slideshow/index.js} +2 -11
- package/dist/{components/blurry-gradient.js → blurry-gradient/index.js} +15 -9
- package/dist/{components/carousel.d.ts → carousel/index.d.ts} +17 -9
- package/dist/{components/carousel.js → carousel/index.js} +34 -30
- package/dist/{components/countdown.js → countdown/index.js} +2 -11
- package/dist/{components/generic-error.js → generic-error/index.js} +1 -1
- package/dist/grid/index.d.ts +1196 -0
- package/dist/grid/index.js +239 -0
- package/dist/heading/index.d.ts +24 -0
- package/dist/{components/heading.js → heading/index.js} +15 -34
- package/dist/highlight/index.d.ts +13 -0
- package/dist/{components/highlight.js → highlight/index.js} +9 -17
- package/dist/hooks/{use-current-route-data.js → use-current-route-data/index.js} +1 -1
- package/dist/hooks/{use-focus-search.js → use-focus-search/index.js} +1 -1
- package/dist/hooks/{use-matches-data.js → use-matches-data/index.js} +1 -1
- package/dist/hooks/{use-media-query.js → use-media-query/index.js} +1 -1
- package/dist/hooks/{use-mobile.js → use-mobile/index.js} +1 -1
- package/dist/hooks/use-nonce/index.d.ts +6 -0
- package/dist/hooks/use-nonce/index.js +8 -0
- package/dist/hooks/{use-orientation.d.ts → use-orientation/index.d.ts} +1 -1
- package/dist/hooks/{use-orientation.js → use-orientation/index.js} +1 -1
- package/dist/hooks/{use-user.js → use-user/index.js} +1 -1
- package/dist/{components/icon-button.js → icon-button/index.js} +1 -1
- package/dist/{components/if.js → if/index.js} +1 -1
- package/dist/{components/iframe.js → iframe/index.js} +2 -11
- package/dist/{components/link.d.ts → link/index.d.ts} +19 -13
- package/dist/{components/link.js → link/index.js} +31 -36
- package/dist/list/index.d.ts +69 -0
- package/dist/list/index.js +65 -0
- package/dist/{components/markdown-container.js → markdown-container/index.js} +3 -67
- package/dist/{components/password-input.js → password-input/index.js} +2 -11
- package/dist/{components/picture.js → picture/index.js} +2 -11
- package/dist/{components/protected-email.d.ts → protected-email/index.d.ts} +1 -1
- package/dist/{components/protected-email.js → protected-email/index.js} +1 -1
- package/dist/text/index.d.ts +20 -0
- package/dist/text/index.js +38 -0
- package/dist/utils/author/index.d.ts +3 -0
- package/dist/utils/author/index.js +33 -0
- package/dist/utils/text/index.d.ts +15 -0
- package/dist/utils/text/index.js +73 -0
- package/package.json +92 -121
- package/src/{stories/BackgroundSlideshow.stories.tsx → background-slideshow/background-slideshow.stories.tsx} +1 -1
- package/src/{components → background-slideshow}/background-slideshow.tsx +3 -1
- package/src/background-slideshow/index.ts +2 -0
- package/src/{stories/BlurryGradient.stories.tsx → blurry-gradient/blurry-gradient.stories.tsx} +1 -1
- package/src/{components → blurry-gradient}/blurry-gradient.tsx +14 -8
- package/src/blurry-gradient/index.ts +2 -0
- package/src/carousel/carousel-content.tsx +16 -0
- package/src/carousel/carousel-item.tsx +23 -0
- package/src/carousel/carousel-next.tsx +22 -0
- package/src/carousel/carousel-previous.tsx +22 -0
- package/src/{components/carousel.tsx → carousel/carousel-root.tsx} +8 -78
- package/src/carousel/carousel.stories.tsx +89 -0
- package/src/carousel/index.parts.ts +5 -0
- package/src/carousel/index.ts +4 -0
- package/src/{stories/Countdown.stories.tsx → countdown/countdown.stories.tsx} +1 -1
- package/src/{components → countdown}/countdown.tsx +3 -7
- package/src/countdown/index.ts +1 -0
- package/src/{stories/GenericError.stories.tsx → generic-error/generic-error.stories.tsx} +1 -1
- package/src/{components → generic-error}/generic-error.tsx +2 -0
- package/src/generic-error/index.ts +2 -0
- package/src/grid/grid-item.tsx +188 -0
- package/src/grid/grid-root.tsx +72 -0
- package/src/grid/grid.stories.tsx +236 -0
- package/src/grid/index.parts.ts +2 -0
- package/src/grid/index.ts +5 -0
- package/src/{stories/Heading.stories.tsx → heading/heading.stories.tsx} +1 -1
- package/src/{components → heading}/heading.tsx +17 -25
- package/src/heading/index.ts +2 -0
- package/src/{stories/Highlight.stories.tsx → highlight/highlight.stories.tsx} +1 -1
- package/src/{components → highlight}/highlight.tsx +13 -9
- package/src/highlight/index.ts +2 -0
- package/src/hooks/use-current-route-data/index.ts +1 -0
- package/src/hooks/use-focus-search/index.ts +1 -0
- package/src/hooks/use-matches-data/index.ts +1 -0
- package/src/hooks/use-media-query/index.ts +1 -0
- package/src/hooks/use-mobile/index.ts +1 -0
- package/src/hooks/use-nonce/index.ts +1 -0
- package/src/hooks/use-orientation/index.ts +1 -0
- package/src/hooks/use-user/index.ts +2 -0
- package/src/{stories/IconButton.stories.tsx → icon-button/icon-button.stories.tsx} +1 -1
- package/src/icon-button/index.ts +2 -0
- package/src/{stories/If.stories.tsx → if/if.stories.tsx} +1 -1
- package/src/if/index.ts +1 -0
- package/src/{stories/Iframe.stories.tsx → iframe/iframe.stories.tsx} +1 -1
- package/src/{components → iframe}/iframe.tsx +1 -1
- package/src/iframe/index.ts +2 -0
- package/src/link/index.ts +2 -0
- package/src/{stories/Link.stories.tsx → link/link.stories.tsx} +1 -1
- package/src/{components → link}/link.tsx +39 -28
- package/src/list/index.parts.ts +2 -0
- package/src/list/index.ts +4 -0
- package/src/list/list-item.tsx +63 -0
- package/src/list/list-root-context.ts +21 -0
- package/src/list/list-root.tsx +81 -0
- package/src/list/list.css +32 -0
- package/src/list/list.stories.tsx +119 -0
- package/src/list/list.test.tsx +168 -0
- package/src/markdown-container/index.ts +2 -0
- package/src/{stories/MarkdownContainer.stories.tsx → markdown-container/markdown-container.stories.tsx} +1 -1
- package/src/{components → markdown-container}/markdown-container.tsx +3 -1
- package/src/password-input/index.ts +2 -0
- package/src/{stories/PasswordInput.stories.tsx → password-input/password-input.stories.tsx} +1 -1
- package/src/{components → password-input}/password-input.tsx +4 -4
- package/src/picture/index.ts +2 -0
- package/src/{stories/Picture.stories.tsx → picture/picture.stories.tsx} +1 -1
- package/src/{components → picture}/picture.tsx +2 -4
- package/src/protected-email/index.ts +2 -0
- package/src/{stories/ProtectedEmail.stories.tsx → protected-email/protected-email.stories.tsx} +1 -1
- package/src/{components → protected-email}/protected-email.tsx +3 -1
- package/src/tailwind.css +10 -0
- package/src/text/index.ts +2 -0
- package/src/{stories/Text.stories.tsx → text/text.stories.tsx} +1 -1
- package/src/text/text.tsx +46 -0
- package/src/utils/author/author.tsx +36 -0
- package/src/utils/author/index.ts +1 -0
- package/src/utils/text/index.ts +1 -0
- package/src/utils/text/text.tsx +103 -0
- package/dist/components/box.d.ts +0 -20
- package/dist/components/box.js +0 -50
- package/dist/components/definition-list.d.ts +0 -43
- package/dist/components/definition-list.js +0 -89
- package/dist/components/heading.d.ts +0 -27
- package/dist/components/highlight.d.ts +0 -19
- package/dist/components/item.d.ts +0 -70
- package/dist/components/item.js +0 -512
- package/dist/components/leaflet-map.d.ts +0 -34
- package/dist/components/leaflet-map.js +0 -201
- package/dist/components/list-item.d.ts +0 -19
- package/dist/components/list-item.js +0 -37
- package/dist/components/maptiler-map.d.ts +0 -27
- package/dist/components/maptiler-map.js +0 -129
- package/dist/components/text.d.ts +0 -20
- package/dist/components/text.js +0 -45
- package/dist/components/unordered-list.d.ts +0 -19
- package/dist/components/unordered-list.js +0 -39
- package/dist/hooks/use-nonce.d.ts +0 -12
- package/dist/hooks/use-nonce.js +0 -13
- package/dist/utils/author.d.ts +0 -9
- package/dist/utils/author.js +0 -55
- package/dist/utils/cn.d.ts +0 -9
- package/dist/utils/cn.js +0 -14
- package/dist/utils/is-route-active.d.ts +0 -19
- package/dist/utils/is-route-active.js +0 -56
- package/dist/utils/text.d.ts +0 -24
- package/dist/utils/text.js +0 -127
- package/src/components/box.tsx +0 -45
- package/src/components/definition-list.tsx +0 -90
- package/src/components/item.tsx +0 -340
- package/src/components/leaflet-map.tsx +0 -294
- package/src/components/link.test.tsx +0 -387
- package/src/components/list-item.tsx +0 -30
- package/src/components/maptiler-map.tsx +0 -181
- package/src/components/text.tsx +0 -38
- package/src/components/unordered-list.tsx +0 -32
- package/src/hooks/use-nonce.test.ts +0 -35
- package/src/stories/Box.stories.tsx +0 -83
- package/src/stories/Carousel.stories.tsx +0 -95
- package/src/stories/DefinitionList.stories.tsx +0 -51
- package/src/stories/Item.stories.tsx +0 -79
- package/src/stories/ListItem.stories.tsx +0 -38
- package/src/stories/UnorderedList.stories.tsx +0 -73
- package/src/styles/tailwind.css +0 -7
- package/src/test-setup.ts +0 -1
- package/src/utils/author.test.ts +0 -54
- package/src/utils/author.tsx +0 -73
- package/src/utils/cn.test.ts +0 -48
- package/src/utils/cn.ts +0 -14
- package/src/utils/is-route-active.test.ts +0 -80
- package/src/utils/is-route-active.ts +0 -100
- package/src/utils/text.test.ts +0 -152
- package/src/utils/text.tsx +0 -209
- package/src/vite-env.d.ts +0 -1
- /package/dist/{components/background-slideshow.d.ts → background-slideshow/index.d.ts} +0 -0
- /package/dist/{components/blurry-gradient.d.ts → blurry-gradient/index.d.ts} +0 -0
- /package/dist/{components/countdown.d.ts → countdown/index.d.ts} +0 -0
- /package/dist/{components/generic-error.d.ts → generic-error/index.d.ts} +0 -0
- /package/dist/hooks/{use-current-route-data.d.ts → use-current-route-data/index.d.ts} +0 -0
- /package/dist/hooks/{use-focus-search.d.ts → use-focus-search/index.d.ts} +0 -0
- /package/dist/hooks/{use-matches-data.d.ts → use-matches-data/index.d.ts} +0 -0
- /package/dist/hooks/{use-media-query.d.ts → use-media-query/index.d.ts} +0 -0
- /package/dist/hooks/{use-mobile.d.ts → use-mobile/index.d.ts} +0 -0
- /package/dist/hooks/{use-user.d.ts → use-user/index.d.ts} +0 -0
- /package/dist/{components/icon-button.d.ts → icon-button/index.d.ts} +0 -0
- /package/dist/{components/if.d.ts → if/index.d.ts} +0 -0
- /package/dist/{components/iframe.d.ts → iframe/index.d.ts} +0 -0
- /package/dist/{components/markdown-container.d.ts → markdown-container/index.d.ts} +0 -0
- /package/dist/{components/password-input.d.ts → password-input/index.d.ts} +0 -0
- /package/dist/{components/picture.d.ts → picture/index.d.ts} +0 -0
- /package/src/hooks/{use-current-route-data.ts → use-current-route-data/use-current-route-data.ts} +0 -0
- /package/src/hooks/{use-focus-search.ts → use-focus-search/use-focus-search.ts} +0 -0
- /package/src/hooks/{use-matches-data.ts → use-matches-data/use-matches-data.ts} +0 -0
- /package/src/hooks/{use-media-query.ts → use-media-query/use-media-query.ts} +0 -0
- /package/src/hooks/{use-mobile.ts → use-mobile/use-mobile.ts} +0 -0
- /package/src/hooks/{use-nonce.ts → use-nonce/use-nonce.ts} +0 -0
- /package/src/hooks/{use-orientation.ts → use-orientation/use-orientation.ts} +0 -0
- /package/src/hooks/{use-user.tsx → use-user/use-user.tsx} +0 -0
- /package/src/{components → icon-button}/icon-button.tsx +0 -0
- /package/src/{components → if}/if.tsx +0 -0
- /package/src/{styles/storybook.css → storybook.css} +0 -0
|
@@ -1,294 +0,0 @@
|
|
|
1
|
-
import 'leaflet/dist/leaflet.css';
|
|
2
|
-
import L from 'leaflet';
|
|
3
|
-
import { useEffect, useRef } from 'react';
|
|
4
|
-
|
|
5
|
-
interface MapMarker {
|
|
6
|
-
lat: number;
|
|
7
|
-
lng: number;
|
|
8
|
-
id: string;
|
|
9
|
-
content?: string;
|
|
10
|
-
htmlContent?: string;
|
|
11
|
-
imageUrl?: string;
|
|
12
|
-
imageAlt?: string;
|
|
13
|
-
offset?: { x: number; y: number };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface LeafletMapProps {
|
|
17
|
-
markers: MapMarker[];
|
|
18
|
-
mapUrl: string;
|
|
19
|
-
center?: { lat: number; lng: number };
|
|
20
|
-
zoom?: number;
|
|
21
|
-
icon?: {
|
|
22
|
-
iconUrl: string;
|
|
23
|
-
iconSize: [number, number];
|
|
24
|
-
iconAnchor: [number, number];
|
|
25
|
-
};
|
|
26
|
-
attribution?: string;
|
|
27
|
-
showPopupsOnHover?: boolean;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export const LeafletMap = ({
|
|
31
|
-
markers,
|
|
32
|
-
mapUrl,
|
|
33
|
-
center,
|
|
34
|
-
zoom = 12,
|
|
35
|
-
icon = {
|
|
36
|
-
iconAnchor: [12, 41] as [number, number],
|
|
37
|
-
iconSize: [25, 41] as [number, number],
|
|
38
|
-
iconUrl: '/marker-icon-2x.png',
|
|
39
|
-
},
|
|
40
|
-
attribution = '',
|
|
41
|
-
showPopupsOnHover = false,
|
|
42
|
-
}: LeafletMapProps) => {
|
|
43
|
-
const mapContainerRef = useRef<HTMLDivElement>(null);
|
|
44
|
-
const mapRef = useRef<L.Map | null>(null);
|
|
45
|
-
const markersRef = useRef<L.Marker[]>([]);
|
|
46
|
-
|
|
47
|
-
useEffect(() => {
|
|
48
|
-
if (!mapContainerRef.current || typeof window === 'undefined') return;
|
|
49
|
-
|
|
50
|
-
// Calculate center
|
|
51
|
-
let calculatedCenter: [number, number];
|
|
52
|
-
if (center) {
|
|
53
|
-
calculatedCenter = [center.lat, center.lng];
|
|
54
|
-
} else if (markers.length > 0) {
|
|
55
|
-
const firstMarker = markers[0];
|
|
56
|
-
if (firstMarker) {
|
|
57
|
-
calculatedCenter = [firstMarker.lat, firstMarker.lng];
|
|
58
|
-
} else {
|
|
59
|
-
calculatedCenter = [52.520008, 13.404954];
|
|
60
|
-
}
|
|
61
|
-
} else {
|
|
62
|
-
calculatedCenter = [52.520008, 13.404954];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Initialize map - check if already initialized
|
|
66
|
-
if (mapRef.current) {
|
|
67
|
-
try {
|
|
68
|
-
// Clean up existing markers
|
|
69
|
-
markersRef.current.forEach((marker) => {
|
|
70
|
-
try {
|
|
71
|
-
mapRef.current?.removeLayer(marker);
|
|
72
|
-
} catch (_error) {
|
|
73
|
-
// Marker might already be removed
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
mapRef.current.remove();
|
|
77
|
-
} catch (_error) {
|
|
78
|
-
// Map might already be removed
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Ensure container is clean and prevent duplicate initialization
|
|
83
|
-
const container = mapContainerRef.current;
|
|
84
|
-
if (container) {
|
|
85
|
-
// Clear any existing content and remove any existing map
|
|
86
|
-
container.innerHTML = '';
|
|
87
|
-
|
|
88
|
-
// Check if container already has a map
|
|
89
|
-
const leafletContainer = container as HTMLDivElement & {
|
|
90
|
-
_leaflet_id?: number;
|
|
91
|
-
};
|
|
92
|
-
if (leafletContainer._leaflet_id) {
|
|
93
|
-
delete leafletContainer._leaflet_id;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Ensure container has unique ID or remove existing map
|
|
97
|
-
container.setAttribute('data-map-initialized', 'false');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const map = new L.Map(mapContainerRef.current, {
|
|
101
|
-
center: calculatedCenter,
|
|
102
|
-
scrollWheelZoom: false,
|
|
103
|
-
tapTolerance: 100,
|
|
104
|
-
zoom: zoom,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Add tile layer
|
|
108
|
-
new L.TileLayer(mapUrl, {
|
|
109
|
-
attribution,
|
|
110
|
-
}).addTo(map);
|
|
111
|
-
|
|
112
|
-
// Create custom icon
|
|
113
|
-
const customIcon = new L.Icon({
|
|
114
|
-
iconAnchor: icon.iconAnchor,
|
|
115
|
-
iconSize: icon.iconSize,
|
|
116
|
-
iconUrl: icon.iconUrl,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Add markers with popups
|
|
120
|
-
const newMarkers: L.Marker[] = [];
|
|
121
|
-
|
|
122
|
-
markers.forEach((marker) => {
|
|
123
|
-
const markerLatLng: [number, number] = [marker.lat, marker.lng];
|
|
124
|
-
const leafletMarker = new L.Marker(markerLatLng, {
|
|
125
|
-
icon: customIcon,
|
|
126
|
-
}).addTo(map);
|
|
127
|
-
|
|
128
|
-
// Apply offset if provided
|
|
129
|
-
const offset = marker.offset || { x: 0, y: 0 };
|
|
130
|
-
|
|
131
|
-
// Create popup content
|
|
132
|
-
let popupContent = '';
|
|
133
|
-
if (marker.htmlContent) {
|
|
134
|
-
popupContent = marker.htmlContent;
|
|
135
|
-
} else if (marker.content) {
|
|
136
|
-
popupContent = marker.content;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Add image if provided
|
|
140
|
-
if (marker.imageUrl) {
|
|
141
|
-
const imageHtml = `<img src="${marker.imageUrl}" alt="${marker.imageAlt || ''}" style="max-width: 100%; height: auto; display: block; margin-bottom: 8px;" />`;
|
|
142
|
-
popupContent = imageHtml + popupContent;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (popupContent) {
|
|
146
|
-
const popup = new L.Popup({
|
|
147
|
-
className: 'custom-map-popup',
|
|
148
|
-
closeButton: false,
|
|
149
|
-
offset: [offset.x, offset.y],
|
|
150
|
-
}).setContent(popupContent);
|
|
151
|
-
|
|
152
|
-
leafletMarker.bindPopup(popup);
|
|
153
|
-
|
|
154
|
-
// Show popup on hover if enabled
|
|
155
|
-
if (showPopupsOnHover) {
|
|
156
|
-
leafletMarker.on('mouseover', () => {
|
|
157
|
-
leafletMarker.openPopup();
|
|
158
|
-
});
|
|
159
|
-
leafletMarker.on('mouseout', () => {
|
|
160
|
-
leafletMarker.closePopup();
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
newMarkers.push(leafletMarker);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
markersRef.current = newMarkers;
|
|
169
|
-
|
|
170
|
-
// Fit bounds if markers exist
|
|
171
|
-
if (markers.length > 0) {
|
|
172
|
-
const bounds = new L.LatLngBounds(markers.map((m) => new L.LatLng(m.lat, m.lng)));
|
|
173
|
-
map.fitBounds(bounds, { padding: [20, 20] });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
mapRef.current = map;
|
|
177
|
-
|
|
178
|
-
// Cleanup function
|
|
179
|
-
return () => {
|
|
180
|
-
if (mapRef.current) {
|
|
181
|
-
try {
|
|
182
|
-
// Remove all markers
|
|
183
|
-
markersRef.current.forEach((marker) => {
|
|
184
|
-
try {
|
|
185
|
-
mapRef.current?.removeLayer(marker);
|
|
186
|
-
} catch (_error) {
|
|
187
|
-
// Marker might already be removed
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
mapRef.current.remove();
|
|
191
|
-
mapRef.current = null;
|
|
192
|
-
|
|
193
|
-
// Clean up container
|
|
194
|
-
if (mapContainerRef.current) {
|
|
195
|
-
mapContainerRef.current.innerHTML = '';
|
|
196
|
-
mapContainerRef.current.removeAttribute('data-map-initialized');
|
|
197
|
-
}
|
|
198
|
-
} catch (_error) {
|
|
199
|
-
// Map might already be removed
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
markersRef.current = [];
|
|
203
|
-
};
|
|
204
|
-
}, [markers, mapUrl, zoom, icon, attribution, center, showPopupsOnHover]);
|
|
205
|
-
|
|
206
|
-
// Update markers when they change
|
|
207
|
-
useEffect(() => {
|
|
208
|
-
const map = mapRef.current;
|
|
209
|
-
if (!map || typeof window === 'undefined') return;
|
|
210
|
-
|
|
211
|
-
// Remove existing markers
|
|
212
|
-
markersRef.current.forEach((marker) => {
|
|
213
|
-
try {
|
|
214
|
-
map.removeLayer(marker);
|
|
215
|
-
} catch (_error) {
|
|
216
|
-
// Marker might already be removed
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
markersRef.current = [];
|
|
220
|
-
|
|
221
|
-
// Create new icon
|
|
222
|
-
const customIcon = new L.Icon({
|
|
223
|
-
iconAnchor: icon.iconAnchor,
|
|
224
|
-
iconSize: icon.iconSize,
|
|
225
|
-
iconUrl: icon.iconUrl,
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Add new markers with popups
|
|
229
|
-
const newMarkers: L.Marker[] = [];
|
|
230
|
-
|
|
231
|
-
markers.forEach((marker) => {
|
|
232
|
-
const markerLatLng: [number, number] = [marker.lat, marker.lng];
|
|
233
|
-
const leafletMarker = new L.Marker(markerLatLng, {
|
|
234
|
-
icon: customIcon,
|
|
235
|
-
}).addTo(map);
|
|
236
|
-
|
|
237
|
-
// Apply offset if provided
|
|
238
|
-
const offset = marker.offset || { x: 0, y: 0 };
|
|
239
|
-
|
|
240
|
-
// Create popup content
|
|
241
|
-
let popupContent = '';
|
|
242
|
-
if (marker.htmlContent) {
|
|
243
|
-
popupContent = marker.htmlContent;
|
|
244
|
-
} else if (marker.content) {
|
|
245
|
-
popupContent = marker.content;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Add image if provided
|
|
249
|
-
if (marker.imageUrl) {
|
|
250
|
-
const imageHtml = `<img src="${marker.imageUrl}" alt="${marker.imageAlt || ''}" style="max-width: 100%; height: auto; display: block; margin-bottom: 8px;" />`;
|
|
251
|
-
popupContent = imageHtml + popupContent;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (popupContent) {
|
|
255
|
-
const popup = new L.Popup({
|
|
256
|
-
className: 'custom-map-popup',
|
|
257
|
-
closeButton: false,
|
|
258
|
-
offset: [offset.x, offset.y],
|
|
259
|
-
}).setContent(popupContent);
|
|
260
|
-
|
|
261
|
-
leafletMarker.bindPopup(popup);
|
|
262
|
-
|
|
263
|
-
// Show popup on hover if enabled
|
|
264
|
-
if (showPopupsOnHover) {
|
|
265
|
-
leafletMarker.on('mouseover', () => {
|
|
266
|
-
leafletMarker.openPopup();
|
|
267
|
-
});
|
|
268
|
-
leafletMarker.on('mouseout', () => {
|
|
269
|
-
leafletMarker.closePopup();
|
|
270
|
-
});
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
newMarkers.push(leafletMarker);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
markersRef.current = newMarkers;
|
|
278
|
-
|
|
279
|
-
// Update bounds if markers exist
|
|
280
|
-
if (markers.length > 0) {
|
|
281
|
-
const bounds = new L.LatLngBounds(markers.map((m) => new L.LatLng(m.lat, m.lng)));
|
|
282
|
-
map.fitBounds(bounds, { padding: [20, 20] });
|
|
283
|
-
}
|
|
284
|
-
}, [markers, icon, showPopupsOnHover]);
|
|
285
|
-
|
|
286
|
-
return (
|
|
287
|
-
<div
|
|
288
|
-
className="h-full w-full"
|
|
289
|
-
ref={mapContainerRef}
|
|
290
|
-
/>
|
|
291
|
-
);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
export default LeafletMap;
|
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { fireEvent, render, screen } from '@testing-library/react';
|
|
2
|
-
import { MemoryRouter } from 'react-router';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
4
|
-
import { Link, LinkBase, MarkdownLink, PathResolverProvider, usePathResolver } from './link';
|
|
5
|
-
|
|
6
|
-
// Helper to render with router context
|
|
7
|
-
const renderWithRouter = (ui: React.ReactNode, { route = '/' } = {}) => {
|
|
8
|
-
return render(<MemoryRouter initialEntries={[route]}>{ui}</MemoryRouter>);
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
describe('LinkBase', () => {
|
|
12
|
-
describe('path resolution', () => {
|
|
13
|
-
test('renders with string "to" prop', () => {
|
|
14
|
-
renderWithRouter(<LinkBase to="/about">About</LinkBase>);
|
|
15
|
-
|
|
16
|
-
const link = screen.getByRole('link', { name: 'About' });
|
|
17
|
-
expect(link).toHaveAttribute('href', '/about');
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('renders with object "to" prop containing pathname', () => {
|
|
21
|
-
renderWithRouter(<LinkBase to={{ pathname: '/contact' }}>Contact</LinkBase>);
|
|
22
|
-
|
|
23
|
-
const link = screen.getByRole('link', { name: 'Contact' });
|
|
24
|
-
expect(link).toHaveAttribute('href', '/contact');
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test('renders with object "to" prop containing pathname, search, and hash', () => {
|
|
28
|
-
renderWithRouter(
|
|
29
|
-
<LinkBase to={{ hash: '#section', pathname: '/page', search: '?foo=bar' }}>
|
|
30
|
-
Full Path
|
|
31
|
-
</LinkBase>,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const link = screen.getByRole('link', { name: 'Full Path' });
|
|
35
|
-
expect(link).toHaveAttribute('href', '/page?foo=bar#section');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test('uses pathResolver when routeKey is provided', () => {
|
|
39
|
-
const mockResolver = vi.fn().mockReturnValue('/resolved-path');
|
|
40
|
-
|
|
41
|
-
renderWithRouter(
|
|
42
|
-
<PathResolverProvider value={mockResolver}>
|
|
43
|
-
<LinkBase routeKey="home">Home</LinkBase>
|
|
44
|
-
</PathResolverProvider>,
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
expect(mockResolver).toHaveBeenCalledWith('home');
|
|
48
|
-
const link = screen.getByRole('link', { name: 'Home' });
|
|
49
|
-
expect(link).toHaveAttribute('href', '/resolved-path');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test('falls back to "to" prop when no pathResolver is available', () => {
|
|
53
|
-
renderWithRouter(
|
|
54
|
-
<LinkBase
|
|
55
|
-
routeKey="home"
|
|
56
|
-
to="/fallback"
|
|
57
|
-
>
|
|
58
|
-
Home
|
|
59
|
-
</LinkBase>,
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
const link = screen.getByRole('link', { name: 'Home' });
|
|
63
|
-
expect(link).toHaveAttribute('href', '/fallback');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('renders children only when path is empty', () => {
|
|
67
|
-
renderWithRouter(<LinkBase>No Link</LinkBase>);
|
|
68
|
-
|
|
69
|
-
expect(screen.queryByRole('link')).toBeNull();
|
|
70
|
-
expect(screen.getByText('No Link')).toBeInTheDocument();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('renders nothing for function children when path is empty', () => {
|
|
74
|
-
const { container } = renderWithRouter(
|
|
75
|
-
<LinkBase>{() => <span>Function Child</span>}</LinkBase>,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
expect(screen.queryByRole('link')).toBeNull();
|
|
79
|
-
expect(container.textContent).toBe('');
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('external links', () => {
|
|
84
|
-
let windowOpenSpy: ReturnType<typeof vi.spyOn>;
|
|
85
|
-
|
|
86
|
-
beforeEach(() => {
|
|
87
|
-
windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
afterEach(() => {
|
|
91
|
-
windowOpenSpy.mockRestore();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('renders tel: links as anchor elements', () => {
|
|
95
|
-
renderWithRouter(<LinkBase to="tel:+1234567890">Call Us</LinkBase>);
|
|
96
|
-
|
|
97
|
-
const link = screen.getByRole('link', { name: 'Call Us' });
|
|
98
|
-
expect(link.tagName).toBe('A');
|
|
99
|
-
expect(link).toHaveAttribute('href', 'tel:+1234567890');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('renders mailto: links as anchor elements', () => {
|
|
103
|
-
renderWithRouter(<LinkBase to="mailto:test@example.com">Email Us</LinkBase>);
|
|
104
|
-
|
|
105
|
-
const link = screen.getByRole('link', { name: 'Email Us' });
|
|
106
|
-
expect(link.tagName).toBe('A');
|
|
107
|
-
expect(link).toHaveAttribute('href', 'mailto:test@example.com');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('opens http links in new tab on click', () => {
|
|
111
|
-
renderWithRouter(<LinkBase to="https://example.com">External</LinkBase>);
|
|
112
|
-
|
|
113
|
-
const link = screen.getByRole('link', { name: 'External' });
|
|
114
|
-
fireEvent.click(link);
|
|
115
|
-
|
|
116
|
-
expect(windowOpenSpy).toHaveBeenCalledWith(
|
|
117
|
-
'https://example.com',
|
|
118
|
-
'_blank',
|
|
119
|
-
'noopener,noreferrer',
|
|
120
|
-
);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
test('handles hash links with smooth scroll', () => {
|
|
124
|
-
const mockElement = document.createElement('div');
|
|
125
|
-
mockElement.id = 'section';
|
|
126
|
-
mockElement.scrollIntoView = vi.fn();
|
|
127
|
-
document.body.appendChild(mockElement);
|
|
128
|
-
|
|
129
|
-
renderWithRouter(<LinkBase to="#section">Go to Section</LinkBase>);
|
|
130
|
-
|
|
131
|
-
const link = screen.getByRole('link', { name: 'Go to Section' });
|
|
132
|
-
fireEvent.click(link);
|
|
133
|
-
|
|
134
|
-
expect(mockElement.scrollIntoView).toHaveBeenCalledWith({ behavior: 'smooth' });
|
|
135
|
-
|
|
136
|
-
document.body.removeChild(mockElement);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('does not scroll if hash element does not exist', () => {
|
|
140
|
-
renderWithRouter(<LinkBase to="#nonexistent">Missing Section</LinkBase>);
|
|
141
|
-
|
|
142
|
-
const link = screen.getByRole('link', { name: 'Missing Section' });
|
|
143
|
-
// Should not throw
|
|
144
|
-
expect(() => fireEvent.click(link)).not.toThrow();
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('onClick handling', () => {
|
|
149
|
-
test('calls custom onClick handler', () => {
|
|
150
|
-
const handleClick = vi.fn();
|
|
151
|
-
|
|
152
|
-
renderWithRouter(
|
|
153
|
-
<LinkBase
|
|
154
|
-
onClick={handleClick}
|
|
155
|
-
to="/page"
|
|
156
|
-
>
|
|
157
|
-
Click Me
|
|
158
|
-
</LinkBase>,
|
|
159
|
-
);
|
|
160
|
-
|
|
161
|
-
const link = screen.getByRole('link', { name: 'Click Me' });
|
|
162
|
-
fireEvent.click(link);
|
|
163
|
-
|
|
164
|
-
expect(handleClick).toHaveBeenCalled();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test('respects preventDefault from custom onClick', () => {
|
|
168
|
-
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
|
169
|
-
const handleClick = vi.fn((e: React.MouseEvent) => e.preventDefault());
|
|
170
|
-
|
|
171
|
-
renderWithRouter(
|
|
172
|
-
<LinkBase
|
|
173
|
-
onClick={handleClick}
|
|
174
|
-
to="https://example.com"
|
|
175
|
-
>
|
|
176
|
-
External
|
|
177
|
-
</LinkBase>,
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
const link = screen.getByRole('link', { name: 'External' });
|
|
181
|
-
fireEvent.click(link);
|
|
182
|
-
|
|
183
|
-
expect(handleClick).toHaveBeenCalled();
|
|
184
|
-
expect(windowOpenSpy).not.toHaveBeenCalled();
|
|
185
|
-
|
|
186
|
-
windowOpenSpy.mockRestore();
|
|
187
|
-
});
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
describe('className and style handling', () => {
|
|
191
|
-
test('applies static className to external links', () => {
|
|
192
|
-
renderWithRouter(
|
|
193
|
-
<LinkBase
|
|
194
|
-
className="custom-class"
|
|
195
|
-
to="https://example.com"
|
|
196
|
-
>
|
|
197
|
-
External
|
|
198
|
-
</LinkBase>,
|
|
199
|
-
);
|
|
200
|
-
|
|
201
|
-
const link = screen.getByRole('link', { name: 'External' });
|
|
202
|
-
expect(link).toHaveClass('custom-class');
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test('resolves function className for external links', () => {
|
|
206
|
-
renderWithRouter(
|
|
207
|
-
<LinkBase
|
|
208
|
-
className={({ isActive }) => (isActive ? 'active' : 'inactive')}
|
|
209
|
-
to="https://example.com"
|
|
210
|
-
>
|
|
211
|
-
External
|
|
212
|
-
</LinkBase>,
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
const link = screen.getByRole('link', { name: 'External' });
|
|
216
|
-
expect(link).toHaveClass('inactive');
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('resolves function style for external links', () => {
|
|
220
|
-
renderWithRouter(
|
|
221
|
-
<LinkBase
|
|
222
|
-
style={({ isActive }) => ({ color: isActive ? 'red' : 'blue' })}
|
|
223
|
-
to="https://example.com"
|
|
224
|
-
>
|
|
225
|
-
External
|
|
226
|
-
</LinkBase>,
|
|
227
|
-
);
|
|
228
|
-
|
|
229
|
-
const link = screen.getByRole('link', { name: 'External' });
|
|
230
|
-
expect(link).toHaveStyle({ color: 'rgb(0, 0, 255)' });
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
test('resolves function children for external links', () => {
|
|
234
|
-
renderWithRouter(
|
|
235
|
-
<LinkBase to="https://example.com">
|
|
236
|
-
{({ isActive }) => (isActive ? 'Active' : 'Inactive')}
|
|
237
|
-
</LinkBase>,
|
|
238
|
-
);
|
|
239
|
-
|
|
240
|
-
expect(screen.getByText('Inactive')).toBeInTheDocument();
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
describe('viewTransition prop', () => {
|
|
245
|
-
test('defaults to true for internal links', () => {
|
|
246
|
-
renderWithRouter(<LinkBase to="/page">Page</LinkBase>);
|
|
247
|
-
|
|
248
|
-
// NavLink should receive viewTransition=true by default
|
|
249
|
-
const link = screen.getByRole('link', { name: 'Page' });
|
|
250
|
-
expect(link).toBeInTheDocument();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
test('can be disabled', () => {
|
|
254
|
-
renderWithRouter(
|
|
255
|
-
<LinkBase
|
|
256
|
-
to="/page"
|
|
257
|
-
viewTransition={false}
|
|
258
|
-
>
|
|
259
|
-
Page
|
|
260
|
-
</LinkBase>,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const link = screen.getByRole('link', { name: 'Page' });
|
|
264
|
-
expect(link).toBeInTheDocument();
|
|
265
|
-
});
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
describe('Link', () => {
|
|
270
|
-
test('applies variant classes', () => {
|
|
271
|
-
renderWithRouter(
|
|
272
|
-
<Link
|
|
273
|
-
to="/page"
|
|
274
|
-
variant="button"
|
|
275
|
-
>
|
|
276
|
-
Button Link
|
|
277
|
-
</Link>,
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
const link = screen.getByRole('link', { name: 'Button Link' });
|
|
281
|
-
expect(link).toHaveClass('button');
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
test('applies arrow classes', () => {
|
|
285
|
-
renderWithRouter(
|
|
286
|
-
<Link
|
|
287
|
-
arrow="rarr"
|
|
288
|
-
to="/page"
|
|
289
|
-
>
|
|
290
|
-
Arrow Link
|
|
291
|
-
</Link>,
|
|
292
|
-
);
|
|
293
|
-
|
|
294
|
-
const link = screen.getByRole('link', { name: 'Arrow Link' });
|
|
295
|
-
expect(link).toHaveClass('rarr');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
test('merges custom className with variant classes', () => {
|
|
299
|
-
renderWithRouter(
|
|
300
|
-
<Link
|
|
301
|
-
className="custom"
|
|
302
|
-
to="/page"
|
|
303
|
-
variant="code"
|
|
304
|
-
>
|
|
305
|
-
Code Link
|
|
306
|
-
</Link>,
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
const link = screen.getByRole('link', { name: 'Code Link' });
|
|
310
|
-
expect(link).toHaveClass('font-monospace');
|
|
311
|
-
expect(link).toHaveClass('custom');
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
test('passes through routeKey to LinkBase', () => {
|
|
315
|
-
const mockResolver = vi.fn().mockReturnValue('/resolved');
|
|
316
|
-
|
|
317
|
-
renderWithRouter(
|
|
318
|
-
<PathResolverProvider value={mockResolver}>
|
|
319
|
-
<Link routeKey="test">Test</Link>
|
|
320
|
-
</PathResolverProvider>,
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
expect(mockResolver).toHaveBeenCalledWith('test');
|
|
324
|
-
});
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
describe('MarkdownLink', () => {
|
|
328
|
-
test('renders Link when href is provided', () => {
|
|
329
|
-
renderWithRouter(<MarkdownLink href="/page">Markdown Link</MarkdownLink>);
|
|
330
|
-
|
|
331
|
-
const link = screen.getByRole('link', { name: 'Markdown Link' });
|
|
332
|
-
expect(link).toHaveAttribute('href', '/page');
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
test('renders null when href is not provided', () => {
|
|
336
|
-
const { container } = renderWithRouter(<MarkdownLink>No Href</MarkdownLink>);
|
|
337
|
-
|
|
338
|
-
expect(container.textContent).toBe('');
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
test('passes through Link props', () => {
|
|
342
|
-
renderWithRouter(
|
|
343
|
-
<MarkdownLink
|
|
344
|
-
href="/page"
|
|
345
|
-
variant="subtitle"
|
|
346
|
-
>
|
|
347
|
-
Styled Link
|
|
348
|
-
</MarkdownLink>,
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
const link = screen.getByRole('link', { name: 'Styled Link' });
|
|
352
|
-
expect(link).toHaveClass('text-lg');
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
describe('usePathResolver', () => {
|
|
357
|
-
test('returns null when no provider is present', () => {
|
|
358
|
-
let resolverValue: ReturnType<typeof usePathResolver> = vi.fn();
|
|
359
|
-
|
|
360
|
-
const TestComponent = () => {
|
|
361
|
-
resolverValue = usePathResolver();
|
|
362
|
-
return null;
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
render(<TestComponent />);
|
|
366
|
-
|
|
367
|
-
expect(resolverValue).toBeNull();
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test('returns resolver function when provider is present', () => {
|
|
371
|
-
const mockResolver = vi.fn().mockReturnValue('/path');
|
|
372
|
-
let resolverValue: ReturnType<typeof usePathResolver> = null;
|
|
373
|
-
|
|
374
|
-
const TestComponent = () => {
|
|
375
|
-
resolverValue = usePathResolver();
|
|
376
|
-
return null;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
render(
|
|
380
|
-
<PathResolverProvider value={mockResolver}>
|
|
381
|
-
<TestComponent />
|
|
382
|
-
</PathResolverProvider>,
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
expect(resolverValue).toBe(mockResolver);
|
|
386
|
-
});
|
|
387
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { ComponentProps } from 'react';
|
|
2
|
-
import { cva, type VariantProps } from '../utils/cn';
|
|
3
|
-
|
|
4
|
-
const li = cva({
|
|
5
|
-
defaultVariants: {
|
|
6
|
-
variant: 'primary',
|
|
7
|
-
},
|
|
8
|
-
variants: {
|
|
9
|
-
variant: {
|
|
10
|
-
primary: [],
|
|
11
|
-
},
|
|
12
|
-
},
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
export interface ListItemProps extends ComponentProps<'li'>, VariantProps<typeof li> {}
|
|
16
|
-
|
|
17
|
-
export const ListItem = (props: ListItemProps) => {
|
|
18
|
-
const { children, className, variant } = props;
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
<li
|
|
22
|
-
className={li({
|
|
23
|
-
className,
|
|
24
|
-
variant,
|
|
25
|
-
})}
|
|
26
|
-
>
|
|
27
|
-
{children}
|
|
28
|
-
</li>
|
|
29
|
-
);
|
|
30
|
-
};
|