@regardio/react 0.4.5 → 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.
Files changed (202) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +5 -5
  3. package/dist/{components/background-slideshow.js → background-slideshow/index.js} +2 -11
  4. package/dist/{components/blurry-gradient.js → blurry-gradient/index.js} +15 -9
  5. package/dist/{components/carousel.d.ts → carousel/index.d.ts} +17 -9
  6. package/dist/{components/carousel.js → carousel/index.js} +34 -30
  7. package/dist/{components/countdown.js → countdown/index.js} +2 -11
  8. package/dist/{components/generic-error.js → generic-error/index.js} +1 -1
  9. package/dist/grid/index.d.ts +1196 -0
  10. package/dist/grid/index.js +239 -0
  11. package/dist/heading/index.d.ts +24 -0
  12. package/dist/{components/heading.js → heading/index.js} +15 -34
  13. package/dist/highlight/index.d.ts +13 -0
  14. package/dist/{components/highlight.js → highlight/index.js} +9 -17
  15. package/dist/hooks/{use-current-route-data.js → use-current-route-data/index.js} +1 -1
  16. package/dist/hooks/{use-focus-search.js → use-focus-search/index.js} +1 -1
  17. package/dist/hooks/{use-matches-data.js → use-matches-data/index.js} +1 -1
  18. package/dist/hooks/{use-media-query.js → use-media-query/index.js} +1 -1
  19. package/dist/hooks/{use-mobile.js → use-mobile/index.js} +1 -1
  20. package/dist/hooks/use-nonce/index.d.ts +6 -0
  21. package/dist/hooks/use-nonce/index.js +8 -0
  22. package/dist/hooks/{use-orientation.d.ts → use-orientation/index.d.ts} +1 -1
  23. package/dist/hooks/{use-orientation.js → use-orientation/index.js} +1 -1
  24. package/dist/hooks/{use-user.js → use-user/index.js} +1 -1
  25. package/dist/{components/icon-button.js → icon-button/index.js} +1 -1
  26. package/dist/{components/if.js → if/index.js} +1 -1
  27. package/dist/{components/iframe.js → iframe/index.js} +2 -11
  28. package/dist/{components/link.d.ts → link/index.d.ts} +19 -13
  29. package/dist/{components/link.js → link/index.js} +31 -36
  30. package/dist/list/index.d.ts +69 -0
  31. package/dist/list/index.js +65 -0
  32. package/dist/{components/markdown-container.js → markdown-container/index.js} +3 -67
  33. package/dist/{components/password-input.js → password-input/index.js} +2 -11
  34. package/dist/{components/picture.js → picture/index.js} +2 -11
  35. package/dist/{components/protected-email.d.ts → protected-email/index.d.ts} +1 -1
  36. package/dist/{components/protected-email.js → protected-email/index.js} +1 -1
  37. package/dist/text/index.d.ts +20 -0
  38. package/dist/text/index.js +38 -0
  39. package/dist/utils/author/index.d.ts +3 -0
  40. package/dist/utils/author/index.js +33 -0
  41. package/dist/utils/text/index.d.ts +15 -0
  42. package/dist/utils/text/index.js +73 -0
  43. package/package.json +124 -26
  44. package/src/{stories/BackgroundSlideshow.stories.tsx → background-slideshow/background-slideshow.stories.tsx} +1 -1
  45. package/src/{components → background-slideshow}/background-slideshow.tsx +3 -1
  46. package/src/background-slideshow/index.ts +2 -0
  47. package/src/{stories/BlurryGradient.stories.tsx → blurry-gradient/blurry-gradient.stories.tsx} +1 -1
  48. package/src/{components → blurry-gradient}/blurry-gradient.tsx +14 -8
  49. package/src/blurry-gradient/index.ts +2 -0
  50. package/src/carousel/carousel-content.tsx +16 -0
  51. package/src/carousel/carousel-item.tsx +23 -0
  52. package/src/carousel/carousel-next.tsx +22 -0
  53. package/src/carousel/carousel-previous.tsx +22 -0
  54. package/src/{components/carousel.tsx → carousel/carousel-root.tsx} +8 -78
  55. package/src/carousel/carousel.stories.tsx +89 -0
  56. package/src/carousel/index.parts.ts +5 -0
  57. package/src/carousel/index.ts +4 -0
  58. package/src/{stories/Countdown.stories.tsx → countdown/countdown.stories.tsx} +1 -1
  59. package/src/{components → countdown}/countdown.tsx +3 -7
  60. package/src/countdown/index.ts +1 -0
  61. package/src/{stories/GenericError.stories.tsx → generic-error/generic-error.stories.tsx} +1 -1
  62. package/src/{components → generic-error}/generic-error.tsx +2 -0
  63. package/src/generic-error/index.ts +2 -0
  64. package/src/grid/grid-item.tsx +188 -0
  65. package/src/grid/grid-root.tsx +72 -0
  66. package/src/grid/grid.stories.tsx +236 -0
  67. package/src/grid/index.parts.ts +2 -0
  68. package/src/grid/index.ts +5 -0
  69. package/src/{stories/Heading.stories.tsx → heading/heading.stories.tsx} +1 -1
  70. package/src/{components → heading}/heading.tsx +17 -25
  71. package/src/heading/index.ts +2 -0
  72. package/src/{stories/Highlight.stories.tsx → highlight/highlight.stories.tsx} +1 -1
  73. package/src/{components → highlight}/highlight.tsx +13 -9
  74. package/src/highlight/index.ts +2 -0
  75. package/src/hooks/use-current-route-data/index.ts +1 -0
  76. package/src/hooks/use-focus-search/index.ts +1 -0
  77. package/src/hooks/use-matches-data/index.ts +1 -0
  78. package/src/hooks/use-media-query/index.ts +1 -0
  79. package/src/hooks/use-mobile/index.ts +1 -0
  80. package/src/hooks/use-nonce/index.ts +1 -0
  81. package/src/hooks/use-orientation/index.ts +1 -0
  82. package/src/hooks/use-user/index.ts +2 -0
  83. package/src/{stories/IconButton.stories.tsx → icon-button/icon-button.stories.tsx} +1 -1
  84. package/src/icon-button/index.ts +2 -0
  85. package/src/{stories/If.stories.tsx → if/if.stories.tsx} +1 -1
  86. package/src/if/index.ts +1 -0
  87. package/src/{stories/Iframe.stories.tsx → iframe/iframe.stories.tsx} +1 -1
  88. package/src/{components → iframe}/iframe.tsx +1 -1
  89. package/src/iframe/index.ts +2 -0
  90. package/src/link/index.ts +2 -0
  91. package/src/{stories/Link.stories.tsx → link/link.stories.tsx} +1 -1
  92. package/src/{components → link}/link.tsx +39 -28
  93. package/src/list/index.parts.ts +2 -0
  94. package/src/list/index.ts +4 -0
  95. package/src/list/list-item.tsx +63 -0
  96. package/src/list/list-root-context.ts +21 -0
  97. package/src/list/list-root.tsx +81 -0
  98. package/src/list/list.css +32 -0
  99. package/src/list/list.stories.tsx +119 -0
  100. package/src/list/list.test.tsx +168 -0
  101. package/src/markdown-container/index.ts +2 -0
  102. package/src/{stories/MarkdownContainer.stories.tsx → markdown-container/markdown-container.stories.tsx} +1 -1
  103. package/src/{components → markdown-container}/markdown-container.tsx +3 -1
  104. package/src/password-input/index.ts +2 -0
  105. package/src/{stories/PasswordInput.stories.tsx → password-input/password-input.stories.tsx} +1 -1
  106. package/src/{components → password-input}/password-input.tsx +4 -4
  107. package/src/picture/index.ts +2 -0
  108. package/src/{stories/Picture.stories.tsx → picture/picture.stories.tsx} +1 -1
  109. package/src/{components → picture}/picture.tsx +2 -4
  110. package/src/protected-email/index.ts +2 -0
  111. package/src/{stories/ProtectedEmail.stories.tsx → protected-email/protected-email.stories.tsx} +1 -1
  112. package/src/{components → protected-email}/protected-email.tsx +3 -1
  113. package/src/tailwind.css +10 -0
  114. package/src/text/index.ts +2 -0
  115. package/src/{stories/Text.stories.tsx → text/text.stories.tsx} +1 -1
  116. package/src/text/text.tsx +46 -0
  117. package/src/utils/author/author.tsx +36 -0
  118. package/src/utils/author/index.ts +1 -0
  119. package/src/utils/text/index.ts +1 -0
  120. package/src/utils/text/text.tsx +103 -0
  121. package/dist/components/box.d.ts +0 -20
  122. package/dist/components/box.js +0 -50
  123. package/dist/components/definition-list.d.ts +0 -43
  124. package/dist/components/definition-list.js +0 -89
  125. package/dist/components/heading.d.ts +0 -27
  126. package/dist/components/highlight.d.ts +0 -19
  127. package/dist/components/item.d.ts +0 -70
  128. package/dist/components/item.js +0 -512
  129. package/dist/components/leaflet-map.d.ts +0 -34
  130. package/dist/components/leaflet-map.js +0 -201
  131. package/dist/components/list-item.d.ts +0 -19
  132. package/dist/components/list-item.js +0 -37
  133. package/dist/components/maptiler-map.d.ts +0 -27
  134. package/dist/components/maptiler-map.js +0 -129
  135. package/dist/components/text.d.ts +0 -20
  136. package/dist/components/text.js +0 -45
  137. package/dist/components/unordered-list.d.ts +0 -19
  138. package/dist/components/unordered-list.js +0 -39
  139. package/dist/hooks/use-nonce.d.ts +0 -12
  140. package/dist/hooks/use-nonce.js +0 -13
  141. package/dist/utils/author.d.ts +0 -9
  142. package/dist/utils/author.js +0 -55
  143. package/dist/utils/cn.d.ts +0 -9
  144. package/dist/utils/cn.js +0 -14
  145. package/dist/utils/is-route-active.d.ts +0 -19
  146. package/dist/utils/is-route-active.js +0 -56
  147. package/dist/utils/text.d.ts +0 -24
  148. package/dist/utils/text.js +0 -127
  149. package/src/components/box.tsx +0 -45
  150. package/src/components/definition-list.tsx +0 -90
  151. package/src/components/item.tsx +0 -340
  152. package/src/components/leaflet-map.tsx +0 -294
  153. package/src/components/link.test.tsx +0 -387
  154. package/src/components/list-item.tsx +0 -30
  155. package/src/components/maptiler-map.tsx +0 -181
  156. package/src/components/text.tsx +0 -38
  157. package/src/components/unordered-list.tsx +0 -32
  158. package/src/hooks/use-nonce.test.ts +0 -35
  159. package/src/stories/Box.stories.tsx +0 -83
  160. package/src/stories/Carousel.stories.tsx +0 -95
  161. package/src/stories/DefinitionList.stories.tsx +0 -51
  162. package/src/stories/Item.stories.tsx +0 -79
  163. package/src/stories/ListItem.stories.tsx +0 -38
  164. package/src/stories/UnorderedList.stories.tsx +0 -73
  165. package/src/styles/tailwind.css +0 -7
  166. package/src/test-setup.ts +0 -1
  167. package/src/utils/author.test.ts +0 -54
  168. package/src/utils/author.tsx +0 -73
  169. package/src/utils/cn.test.ts +0 -48
  170. package/src/utils/cn.ts +0 -14
  171. package/src/utils/is-route-active.test.ts +0 -80
  172. package/src/utils/is-route-active.ts +0 -100
  173. package/src/utils/text.test.ts +0 -152
  174. package/src/utils/text.tsx +0 -209
  175. package/src/vite-env.d.ts +0 -1
  176. /package/dist/{components/background-slideshow.d.ts → background-slideshow/index.d.ts} +0 -0
  177. /package/dist/{components/blurry-gradient.d.ts → blurry-gradient/index.d.ts} +0 -0
  178. /package/dist/{components/countdown.d.ts → countdown/index.d.ts} +0 -0
  179. /package/dist/{components/generic-error.d.ts → generic-error/index.d.ts} +0 -0
  180. /package/dist/hooks/{use-current-route-data.d.ts → use-current-route-data/index.d.ts} +0 -0
  181. /package/dist/hooks/{use-focus-search.d.ts → use-focus-search/index.d.ts} +0 -0
  182. /package/dist/hooks/{use-matches-data.d.ts → use-matches-data/index.d.ts} +0 -0
  183. /package/dist/hooks/{use-media-query.d.ts → use-media-query/index.d.ts} +0 -0
  184. /package/dist/hooks/{use-mobile.d.ts → use-mobile/index.d.ts} +0 -0
  185. /package/dist/hooks/{use-user.d.ts → use-user/index.d.ts} +0 -0
  186. /package/dist/{components/icon-button.d.ts → icon-button/index.d.ts} +0 -0
  187. /package/dist/{components/if.d.ts → if/index.d.ts} +0 -0
  188. /package/dist/{components/iframe.d.ts → iframe/index.d.ts} +0 -0
  189. /package/dist/{components/markdown-container.d.ts → markdown-container/index.d.ts} +0 -0
  190. /package/dist/{components/password-input.d.ts → password-input/index.d.ts} +0 -0
  191. /package/dist/{components/picture.d.ts → picture/index.d.ts} +0 -0
  192. /package/src/hooks/{use-current-route-data.ts → use-current-route-data/use-current-route-data.ts} +0 -0
  193. /package/src/hooks/{use-focus-search.ts → use-focus-search/use-focus-search.ts} +0 -0
  194. /package/src/hooks/{use-matches-data.ts → use-matches-data/use-matches-data.ts} +0 -0
  195. /package/src/hooks/{use-media-query.ts → use-media-query/use-media-query.ts} +0 -0
  196. /package/src/hooks/{use-mobile.ts → use-mobile/use-mobile.ts} +0 -0
  197. /package/src/hooks/{use-nonce.ts → use-nonce/use-nonce.ts} +0 -0
  198. /package/src/hooks/{use-orientation.ts → use-orientation/use-orientation.ts} +0 -0
  199. /package/src/hooks/{use-user.tsx → use-user/use-user.tsx} +0 -0
  200. /package/src/{components → icon-button}/icon-button.tsx +0 -0
  201. /package/src/{components → if}/if.tsx +0 -0
  202. /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
- };