@tpzdsp/next-toolkit 1.14.0 → 1.14.2

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 (27) hide show
  1. package/package.json +3 -3
  2. package/src/components/Button/Button.test.tsx +0 -2
  3. package/src/components/ErrorText/ErrorText.test.tsx +0 -2
  4. package/src/components/Heading/Heading.test.tsx +0 -2
  5. package/src/components/Hint/Hint.test.tsx +0 -2
  6. package/src/components/Modal/Modal.stories.tsx +0 -1
  7. package/src/components/Modal/Modal.test.tsx +1 -3
  8. package/src/components/NotificationBanner/NotificationBanner.test.tsx +0 -2
  9. package/src/components/Paragraph/Paragraph.test.tsx +0 -2
  10. package/src/components/SlidingPanel/SlidingPanel.test.tsx +0 -2
  11. package/src/components/accordion/Accordion.test.tsx +0 -2
  12. package/src/components/backToTop/BackToTop.stories.tsx +0 -2
  13. package/src/components/dropdown/DropdownMenu.test.tsx +0 -2
  14. package/src/components/dropdown/useDropdownMenu.ts +0 -1
  15. package/src/components/form/TextArea.test.tsx +0 -2
  16. package/src/components/images/DefraLogo.tsx +1 -1
  17. package/src/components/layout/header/Header.stories.tsx +48 -0
  18. package/src/components/layout/header/Header.test.tsx +36 -0
  19. package/src/components/layout/header/HeaderAuthClient.test.tsx +45 -0
  20. package/src/components/layout/header/HeaderNavClient.test.tsx +44 -0
  21. package/src/components/layout/header/HeaderNavClient.tsx +6 -3
  22. package/src/components/link/ExternalLink.test.tsx +0 -2
  23. package/src/http/stream.test.ts +98 -0
  24. package/src/map/geocoder/Geocoder.test.tsx +1 -2
  25. package/src/map/geocoder/Geocoder.tsx +10 -1
  26. package/src/map/osOpenNamesSearch.ts +37 -27
  27. package/src/types/navigation.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tpzdsp/next-toolkit",
3
- "version": "1.14.0",
3
+ "version": "1.14.2",
4
4
  "description": "A reusable React component library for Next.js applications",
5
5
  "engines": {
6
6
  "node": ">= 24.12.0",
@@ -138,7 +138,7 @@
138
138
  "@testing-library/jest-dom": "^6.6.4",
139
139
  "@testing-library/react": "^16.3.0",
140
140
  "@testing-library/user-event": "^14.6.1",
141
- "@tpzdsp/eslint-config-dsp": "^1.9.2",
141
+ "@tpzdsp/eslint-config-dsp": "^2.0.0",
142
142
  "@turf/turf": "^7.2.0",
143
143
  "@types/geojson": "^7946.0.16",
144
144
  "@types/jsonwebtoken": "^9.0.10",
@@ -158,7 +158,7 @@
158
158
  "eslint-config-prettier": "^10.1.8",
159
159
  "eslint-import-resolver-alias": "^1.1.2",
160
160
  "eslint-import-resolver-typescript": "^4.4.4",
161
- "eslint-plugin-import": "^2.32.0",
161
+ "eslint-plugin-import-x": "^4.16.1",
162
162
  "eslint-plugin-jest-dom": "^5.5.0",
163
163
  "eslint-plugin-jsx-a11y": "^6.10.2",
164
164
  "eslint-plugin-prettier": "^5.5.5",
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Button } from './Button';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { ErrorText } from './ErrorText';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Heading, type HeadingProps } from './Heading';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Hint } from './Hint';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -34,7 +34,6 @@ const meta: Meta<typeof Modal> = {
34
34
  },
35
35
  },
36
36
  decorators: [
37
- // eslint-disable-next-line @typescript-eslint/naming-convention
38
37
  (Story, context) => {
39
38
  const [{ isOpen }, updateArgs] = useArgs();
40
39
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
1
  import { Modal } from './Modal';
4
2
  import { render, screen, userEvent } from '../../test/renderers';
5
3
 
@@ -25,7 +23,7 @@ describe.todo('Modal', () => {
25
23
  const modalRoot = document.getElementById(MODAL_ROOT_ID);
26
24
 
27
25
  if (modalRoot) {
28
- document.body.removeChild(modalRoot);
26
+ modalRoot.remove();
29
27
  }
30
28
  });
31
29
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { NotificationBanner } from './NotificationBanner';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Paragraph } from './Paragraph';
4
2
  import { render, screen } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, it, expect } from 'vitest';
2
-
3
1
  import { SlidingPanel } from './SlidingPanel';
4
2
  import { act, render, screen, userEvent, waitFor } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { Accordion } from './Accordion';
4
2
  import { render, screen, userEvent } from '../../test/renderers';
5
3
 
@@ -1,5 +1,3 @@
1
- /* eslint-disable @typescript-eslint/naming-convention */
2
-
3
1
  import type { Meta, StoryObj } from '@storybook/react-vite';
4
2
 
5
3
  import { BackToTop } from './BackToTop';
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { DropdownMenu, type DropdownMenuItem, type DrowndownMenuButton } from './DropdownMenu';
4
2
  import { render, screen, userEvent, waitFor } from '../../test/renderers';
5
3
 
@@ -190,7 +190,6 @@ export const useDropdownMenu = (itemCount: number): DropdownMenuHook => {
190
190
  },
191
191
  },
192
192
 
193
- // eslint-disable-next-line @typescript-eslint/naming-convention
194
193
  itemProps: Array.from({ length: itemCount }, (_, itemIndex) => ({
195
194
  key: itemIndex,
196
195
 
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { render, screen } from '@testing-library/react';
4
2
 
5
3
  import { TextArea } from './TextArea';
@@ -4,7 +4,7 @@ export const DefraLogo = ({ className, ...props }: DefraLogoProps) => (
4
4
  <svg
5
5
  viewBox="0 0 125 102"
6
6
  xmlns="http://www.w3.org/2000/svg"
7
- fill="white"
7
+ fill="currentColor"
8
8
  focusable="false"
9
9
  className={className}
10
10
  {...props}
@@ -0,0 +1,48 @@
1
+ import type { Meta, StoryObj } from '@storybook/react-vite';
2
+
3
+ import { Header } from './Header';
4
+ import type { Credentials } from '../../../types/auth';
5
+ import type { NavLink } from '../../../types/navigation';
6
+
7
+ const navLinks: NavLink[] = [
8
+ { label: 'Home', url: '/', isExternal: false },
9
+ { label: 'API', url: '/api-docs', isExternal: false },
10
+ { label: 'Support', url: 'https://example.com/support', isExternal: true },
11
+ ];
12
+
13
+ const authenticatedCredentials: Credentials = {
14
+ token: 'mock-token',
15
+ user: {
16
+ name: 'Jamie Taylor',
17
+ email: 'jamie.taylor@example.com',
18
+ groupInfoIds: ['group-1', 'group-2'],
19
+ },
20
+ };
21
+
22
+ export default {
23
+ title: 'Components/Layout/Header',
24
+ component: Header,
25
+ parameters: {
26
+ layout: 'fullscreen',
27
+ docs: {
28
+ description: {
29
+ component:
30
+ 'Header with DSP branding, navigation, and auth state. Includes responsive nav and client-side auth links.',
31
+ },
32
+ },
33
+ },
34
+ args: {
35
+ dspUrl: 'https://environment.data.gov.uk',
36
+ appName: 'Wetlands Inventory',
37
+ navLinks,
38
+ credentials: null,
39
+ },
40
+ } as Meta<typeof Header>;
41
+
42
+ export const Default: StoryObj<typeof Header> = {};
43
+
44
+ export const Authenticated: StoryObj<typeof Header> = {
45
+ args: {
46
+ credentials: authenticatedCredentials,
47
+ },
48
+ };
@@ -0,0 +1,36 @@
1
+ import { Header } from './Header';
2
+ import { render, screen } from '../../../test/renderers';
3
+ import type { NavLink } from '../../../types/navigation';
4
+
5
+ const NAV_LINKS: NavLink[] = [
6
+ { label: 'Home', url: '/', isExternal: false },
7
+ { label: 'API', url: '/api-docs', isExternal: false },
8
+ { label: 'Support', url: 'https://example.com/support', isExternal: true },
9
+ ];
10
+
11
+ describe('Header', () => {
12
+ it('renders the primary brand links and app name', async () => {
13
+ render(
14
+ <Header
15
+ credentials={null}
16
+ dspUrl="https://example.com"
17
+ appName="Wetlands Inventory"
18
+ navLinks={NAV_LINKS}
19
+ />,
20
+ );
21
+
22
+ expect(
23
+ screen.getByRole('link', {
24
+ name: /department for environment food & rural affairs/i,
25
+ }),
26
+ ).toBeInTheDocument();
27
+
28
+ expect(screen.getByRole('link', { name: /data services platform/i })).toBeInTheDocument();
29
+ expect(screen.getByRole('link', { name: /wetlands inventory/i })).toBeInTheDocument();
30
+
31
+ const loginLink = await screen.findByRole('link', { name: /login/i });
32
+
33
+ expect(loginLink).toBeInTheDocument();
34
+ expect(screen.getByRole('button', { name: /open menu/i })).toBeInTheDocument();
35
+ });
36
+ });
@@ -0,0 +1,45 @@
1
+ import { HeaderAuthClient } from './HeaderAuthClient';
2
+ import { render, screen } from '../../../test/renderers';
3
+ import type { Credentials } from '../../../types/auth';
4
+
5
+ const AUTH_CREDENTIALS: Credentials = {
6
+ token: 'token-123',
7
+ user: {
8
+ name: 'Jamie Taylor',
9
+ email: 'jamie.taylor@example.com',
10
+ groupInfoIds: ['group-1'],
11
+ },
12
+ };
13
+
14
+ describe('HeaderAuthClient', () => {
15
+ beforeEach(() => {
16
+ globalThis.history.pushState({}, '', '/header-test');
17
+ });
18
+
19
+ it('renders guest state with login link', async () => {
20
+ render(<HeaderAuthClient hostname="https://example.com" credentials={null} />);
21
+
22
+ expect(screen.getByText(/welcome,/i)).toBeInTheDocument();
23
+ expect(screen.getByText(/guest/i)).toBeInTheDocument();
24
+
25
+ const loginLink = await screen.findByRole('link', { name: /login/i });
26
+
27
+ expect(loginLink).toHaveAttribute(
28
+ 'href',
29
+ `https://example.com/login?redirect-uri=${encodeURIComponent(globalThis.location.href)}`,
30
+ );
31
+ });
32
+
33
+ it('renders authenticated state with logout link', async () => {
34
+ render(<HeaderAuthClient hostname="https://example.com" credentials={AUTH_CREDENTIALS} />);
35
+
36
+ expect(screen.getByText(/jamie.taylor@example.com/i)).toBeInTheDocument();
37
+
38
+ const logoutLink = await screen.findByRole('link', { name: /logout/i });
39
+
40
+ expect(logoutLink).toHaveAttribute(
41
+ 'href',
42
+ `https://example.com/api/logout?redirect-uri=${encodeURIComponent(globalThis.location.href)}`,
43
+ );
44
+ });
45
+ });
@@ -0,0 +1,44 @@
1
+ import { HeaderNavClient } from './HeaderNavClient';
2
+ import { render, screen, userEvent } from '../../../test/renderers';
3
+ import type { NavLink } from '../../../types/navigation';
4
+
5
+ const NAV_LINKS: NavLink[] = [
6
+ { label: 'Home', url: '/', isExternal: false },
7
+ { label: 'API', url: '/api-docs', isExternal: false },
8
+ { label: 'Support', url: 'https://example.com/support', isExternal: true },
9
+ ];
10
+
11
+ describe('HeaderNavClient', () => {
12
+ it('renders both navigation regions', () => {
13
+ render(<HeaderNavClient navLinks={NAV_LINKS} />);
14
+
15
+ expect(
16
+ screen.getByRole('navigation', { name: /small screen navigation/i }),
17
+ ).toBeInTheDocument();
18
+ expect(
19
+ screen.getByRole('navigation', { name: /large screen navigation/i }),
20
+ ).toBeInTheDocument();
21
+ });
22
+
23
+ it('shows menu items in the small-screen dropdown', async () => {
24
+ render(<HeaderNavClient navLinks={NAV_LINKS} />);
25
+
26
+ const user = userEvent.setup();
27
+
28
+ await user.click(screen.getByRole('button', { name: /open menu/i }));
29
+
30
+ expect(await screen.findByRole('menuitem', { name: /home/i })).toBeInTheDocument();
31
+ expect(await screen.findByRole('menuitem', { name: /api/i })).toBeInTheDocument();
32
+ expect(await screen.findByRole('menuitem', { name: /support/i })).toBeInTheDocument();
33
+ });
34
+
35
+ it('renders large-screen links for navigation', () => {
36
+ render(<HeaderNavClient navLinks={NAV_LINKS} />);
37
+
38
+ const supportLink = screen.getByRole('link', { name: /support/i });
39
+
40
+ expect(supportLink).toHaveAttribute('href', 'https://example.com/support');
41
+ expect(screen.getByRole('link', { name: /home/i })).toBeInTheDocument();
42
+ expect(screen.getByRole('link', { name: /api/i })).toBeInTheDocument();
43
+ });
44
+ });
@@ -23,12 +23,15 @@ const InternalNavItem = ({ label, url, icon, ...props }: DropdownMenuItem<NavLin
23
23
  </Link>
24
24
  );
25
25
 
26
- const NavItem = ({ isExternal, ...props }: DropdownMenuItem<NavLink>) => {
26
+ const NavItem = ({ url, openInNewTab, ...props }: DropdownMenuItem<NavLink>) => {
27
+ const isExternal = /^https?:\/\//.test(url);
28
+ const newTabProps = openInNewTab ? { target: '_blank' as const, rel: 'noopener noreferrer' } : {};
29
+
27
30
  if (isExternal) {
28
- return <ExternalNavItem {...props} />;
31
+ return <ExternalNavItem url={url} {...newTabProps} {...props} />;
29
32
  }
30
33
 
31
- return <InternalNavItem {...props} />;
34
+ return <InternalNavItem url={url} {...newTabProps} {...props} />;
32
35
  };
33
36
 
34
37
  export const HeaderNavClient = ({ navLinks }: HeaderNavClientProps) => {
@@ -1,5 +1,3 @@
1
- import { describe, expect, it } from 'vitest';
2
-
3
1
  import { render, screen } from '@testing-library/react';
4
2
 
5
3
  import { ExternalLink } from './ExternalLink';
@@ -0,0 +1,98 @@
1
+ import { readJsonXLinesStream } from './stream';
2
+
3
+ const createMockReader = (chunks: Uint8Array[]) => {
4
+ let index = 0;
5
+
6
+ return {
7
+ read: vi.fn(async () => {
8
+ if (index < chunks.length) {
9
+ return { value: chunks[index++], done: false };
10
+ }
11
+
12
+ return { value: undefined, done: true };
13
+ }),
14
+ releaseLock: vi.fn(),
15
+ };
16
+ };
17
+
18
+ const createMockResponse = (chunks: string[]): Response => {
19
+ const encoder = new TextEncoder();
20
+ const uintChunks = chunks.map((c) => encoder.encode(c));
21
+
22
+ const mockReader = createMockReader(uintChunks);
23
+
24
+ return {
25
+ status: 200,
26
+ body: {
27
+ getReader: () => mockReader,
28
+ },
29
+ } as unknown as Response;
30
+ };
31
+
32
+ describe('readJsonXLinesStream', () => {
33
+ it('parses valid NDJSON chunks', async () => {
34
+ const response = createMockResponse([
35
+ '{"id":1,"name":"Alice"}\n{"id":2,',
36
+ '"name":"Bob"}\n{"id":3,"name":"Charlie"}\n',
37
+ ]);
38
+
39
+ const result = await readJsonXLinesStream(response);
40
+
41
+ expect(result).toEqual([
42
+ { id: 1, name: 'Alice' },
43
+ { id: 2, name: 'Bob' },
44
+ { id: 3, name: 'Charlie' },
45
+ ]);
46
+ });
47
+
48
+ it('ignores invalid JSON lines', async () => {
49
+ const response = createMockResponse([
50
+ '{"id":1,"name":"Alice"}\nINVALID_JSON\n{"id":2,"name":"Bob"}\n',
51
+ ]);
52
+
53
+ const result = await readJsonXLinesStream(response);
54
+
55
+ expect(result).toEqual([
56
+ { id: 1, name: 'Alice' },
57
+ { id: 2, name: 'Bob' },
58
+ ]);
59
+ });
60
+
61
+ it('handles trailing line without newline', async () => {
62
+ const response = createMockResponse(['{"id":1,"name":"Alice"}\n{"id":2,"name":"Bob"}']);
63
+
64
+ const result = await readJsonXLinesStream(response);
65
+
66
+ expect(result).toEqual([
67
+ { id: 1, name: 'Alice' },
68
+ { id: 2, name: 'Bob' },
69
+ ]);
70
+ });
71
+
72
+ it('returns empty array when no valid data', async () => {
73
+ const response = createMockResponse(['\n\n']);
74
+
75
+ const result = await readJsonXLinesStream(response);
76
+
77
+ expect(result).toEqual([]);
78
+ });
79
+
80
+ it('skips whitespace-only lines', async () => {
81
+ const response = createMockResponse([' \n{"id":1,"name":"Alice"}\n \n']);
82
+
83
+ const result = await readJsonXLinesStream(response);
84
+
85
+ expect(result).toEqual([{ id: 1, name: 'Alice' }]);
86
+ });
87
+
88
+ it('logs a warning for invalid JSON', async () => {
89
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
90
+ const response = createMockResponse(['INVALID\n']);
91
+
92
+ await readJsonXLinesStream(response);
93
+
94
+ expect(warnSpy).toHaveBeenCalledTimes(1);
95
+
96
+ warnSpy.mockRestore();
97
+ });
98
+ });
@@ -1,12 +1,11 @@
1
1
  import Map from 'ol/Map';
2
2
  import View from 'ol/View';
3
- import { describe, expect, it, vi } from 'vitest';
4
3
 
5
4
  import { Geocoder } from './Geocoder';
6
5
  import { render, screen, userEvent } from '../../test/renderers';
7
6
 
8
7
  class MockView extends View {
9
- animate = vi.fn((options, callback) => {
8
+ animate = vi.fn((_, callback) => {
10
9
  // Immediately execute callback if provided (simulating instant animation)
11
10
  if (typeof callback === 'function') {
12
11
  callback(true);
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useId, useRef, useState } from 'react';
3
+ import { useEffect, useId, useRef, useState } from 'react';
4
4
 
5
5
  import type Map from 'ol/Map';
6
6
 
@@ -129,6 +129,15 @@ export const Geocoder = ({
129
129
  const [status, setStatus] = useState('');
130
130
  const [isSearching, setIsSearching] = useState(false);
131
131
 
132
+ // Scroll the active result into view when navigating with arrow keys
133
+ useEffect(() => {
134
+ if (activeIndex >= 0) {
135
+ document
136
+ .getElementById(`${listboxId}-option-${activeIndex}`)
137
+ ?.scrollIntoView({ block: 'nearest' });
138
+ }
139
+ }, [activeIndex, listboxId]);
140
+
132
141
  const performSearch = async () => {
133
142
  if (query.length < minChars) {
134
143
  setStatus(`Enter at least ${minChars} characters`);
@@ -63,21 +63,6 @@ export const createOsOpenNamesSearch = (options: OsOpenNamesSearchOptions) => {
63
63
 
64
64
  return features
65
65
  .map((feature, index): GeocoderResult | null => {
66
- if (feature.geometry.type !== 'Point') {
67
- console.error('Geometry type is not Point');
68
-
69
- return null;
70
- }
71
-
72
- const [lon, lat] = feature.geometry.coordinates;
73
-
74
- // Type guard: ensure coordinates are valid numbers
75
- if (typeof lon !== 'number' || typeof lat !== 'number') {
76
- console.error('Invalid coordinates: lon or lat is not a number');
77
-
78
- return null;
79
- }
80
-
81
66
  const properties = feature.properties ?? {};
82
67
 
83
68
  // Handle address which can be an object or string
@@ -88,35 +73,60 @@ export const createOsOpenNamesSearch = (options: OsOpenNamesSearchOptions) => {
88
73
 
89
74
  label = [addr.name, addr.town, addr.country].filter(Boolean).join(', ');
90
75
  } else {
91
- label = properties.address ?? properties.name ?? `${lat}, ${lon}`;
92
- }
93
-
94
- const [x, y] = transform([lon, lat], EPSG_4326, EPSG_3857);
95
-
96
- // Type guard: ensure transformed coordinates are valid numbers
97
- if (typeof x !== 'number' || typeof y !== 'number') {
98
- console.error('Transform failed: x or y is not a number');
99
-
100
- return null;
76
+ label = properties.address ?? properties.name ?? `result-${index}`;
101
77
  }
102
78
 
103
79
  const result: GeocoderResult = {
104
80
  id: properties.id ?? `result-${index}`,
105
81
  label,
106
82
  group: properties.type ?? properties.localType,
107
- center: [x, y],
108
83
  // Assign zoom level based on type, fallback to 14
109
84
  zoom: ZOOM_BY_TYPE[properties.type] ?? ZOOM_BY_TYPE[properties.localType] ?? 14,
110
85
  };
111
86
 
112
- // Transform bbox to map projection if available
87
+ // Prefer bbox works for all geometry types (Point, Polygon, MultiPolygon)
113
88
  if (feature.bbox && Array.isArray(feature.bbox) && feature.bbox.length === 4) {
89
+ const [minLon, minLat, maxLon, maxLat] = feature.bbox;
90
+
114
91
  result.extent = transformExtent(feature.bbox, EPSG_4326, EPSG_3857) as [
115
92
  number,
116
93
  number,
117
94
  number,
118
95
  number,
119
96
  ];
97
+
98
+ const [x, y] = transform(
99
+ [(minLon + maxLon) / 2, (minLat + maxLat) / 2],
100
+ EPSG_4326,
101
+ EPSG_3857,
102
+ );
103
+
104
+ if (typeof x === 'number' && typeof y === 'number') {
105
+ result.center = [x, y];
106
+ }
107
+ } else if (feature.geometry.type === 'Point') {
108
+ // Fallback for Point features without a bbox
109
+ const [lon, lat] = feature.geometry.coordinates;
110
+
111
+ if (typeof lon !== 'number' || typeof lat !== 'number') {
112
+ console.error('Invalid coordinates: lon or lat is not a number');
113
+
114
+ return null;
115
+ }
116
+
117
+ const [x, y] = transform([lon, lat], EPSG_4326, EPSG_3857);
118
+
119
+ if (typeof x !== 'number' || typeof y !== 'number') {
120
+ console.error('Transform failed: x or y is not a number');
121
+
122
+ return null;
123
+ }
124
+
125
+ result.center = [x, y];
126
+ } else {
127
+ console.error('Feature has no bbox and geometry is not a Point');
128
+
129
+ return null;
120
130
  }
121
131
 
122
132
  return result;
@@ -3,6 +3,6 @@ import type { ReactNode } from 'react';
3
3
  export type NavLink = {
4
4
  label: string;
5
5
  url: string;
6
- isExternal?: boolean;
6
+ openInNewTab?: boolean;
7
7
  icon?: ReactNode;
8
8
  };