@dxos/react-ui-geo 0.8.3 → 0.8.4-main.1068cf700f

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 (86) hide show
  1. package/data/airports.ts +1 -1
  2. package/data/cities.ts +1 -1
  3. package/data/countries-110m.ts +1 -1
  4. package/data/countries-dots-3.ts +1 -1
  5. package/data/countries-dots-4.ts +1 -1
  6. package/dist/lib/browser/chunk-GMWLKTLN.mjs +9 -0
  7. package/dist/lib/browser/{countries-110m-WI4PCLDF.mjs → countries-110m-ZM3ZIEFS.mjs} +2 -2
  8. package/dist/lib/browser/countries-110m-ZM3ZIEFS.mjs.map +7 -0
  9. package/dist/lib/browser/data.mjs +1 -1
  10. package/dist/lib/browser/index.mjs +404 -458
  11. package/dist/lib/browser/index.mjs.map +4 -4
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/node-esm/{chunk-PIIEDZEU.mjs → chunk-JODBF4CC.mjs} +3 -3
  14. package/dist/lib/node-esm/{countries-110m-DQ4XRC4B.mjs → countries-110m-3SFASWVD.mjs} +2 -2
  15. package/dist/lib/node-esm/countries-110m-3SFASWVD.mjs.map +7 -0
  16. package/dist/lib/node-esm/data.mjs +1 -1
  17. package/dist/lib/node-esm/index.mjs +404 -458
  18. package/dist/lib/node-esm/index.mjs.map +4 -4
  19. package/dist/lib/node-esm/meta.json +1 -1
  20. package/dist/types/src/components/Globe/Globe.d.ts +1 -1
  21. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  22. package/dist/types/src/components/Globe/Globe.stories.d.ts +25 -9
  23. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  24. package/dist/types/src/components/Map/Map.d.ts +28 -18
  25. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  26. package/dist/types/src/components/Map/Map.stories.d.ts +14 -8
  27. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  28. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  29. package/dist/types/src/components/index.d.ts +0 -1
  30. package/dist/types/src/components/index.d.ts.map +1 -1
  31. package/dist/types/src/hooks/context.d.ts +7 -7
  32. package/dist/types/src/hooks/context.d.ts.map +1 -1
  33. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +1 -1
  34. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  35. package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
  36. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  37. package/dist/types/src/hooks/useSpinner.d.ts +1 -1
  38. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useTour.d.ts +4 -3
  40. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  41. package/dist/types/src/index.d.ts +2 -1
  42. package/dist/types/src/index.d.ts.map +1 -1
  43. package/dist/types/src/translations.d.ts +12 -0
  44. package/dist/types/src/translations.d.ts.map +1 -0
  45. package/dist/types/src/types.d.ts +2 -1
  46. package/dist/types/src/types.d.ts.map +1 -1
  47. package/dist/types/src/util/path.d.ts +5 -8
  48. package/dist/types/src/util/path.d.ts.map +1 -1
  49. package/dist/types/src/util/render.d.ts +4 -4
  50. package/dist/types/src/util/render.d.ts.map +1 -1
  51. package/dist/types/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +29 -23
  53. package/src/components/Globe/Globe.stories.tsx +85 -37
  54. package/src/components/Globe/Globe.tsx +80 -63
  55. package/src/components/Map/Map.stories.tsx +25 -14
  56. package/src/components/Map/Map.tsx +183 -96
  57. package/src/components/Toolbar/Controls.tsx +14 -20
  58. package/src/components/index.ts +0 -2
  59. package/src/hooks/context.tsx +22 -16
  60. package/src/hooks/useGlobeZoomHandler.ts +9 -3
  61. package/src/hooks/useMapZoomHandler.ts +1 -1
  62. package/src/hooks/useSpinner.ts +2 -1
  63. package/src/hooks/useTour.ts +10 -8
  64. package/src/index.ts +2 -1
  65. package/src/translations.ts +20 -0
  66. package/src/types.ts +3 -1
  67. package/src/util/inertia.ts +1 -1
  68. package/src/util/path.ts +5 -6
  69. package/src/util/render.ts +5 -3
  70. package/dist/lib/browser/chunk-ENCWOTYX.mjs +0 -9
  71. package/dist/lib/browser/countries-110m-WI4PCLDF.mjs.map +0 -7
  72. package/dist/lib/node/chunk-LAICG6L2.cjs +0 -40
  73. package/dist/lib/node/chunk-LAICG6L2.cjs.map +0 -7
  74. package/dist/lib/node/countries-110m-KQ5WAB2O.cjs +0 -37877
  75. package/dist/lib/node/countries-110m-KQ5WAB2O.cjs.map +0 -7
  76. package/dist/lib/node/data.cjs +0 -28
  77. package/dist/lib/node/data.cjs.map +0 -7
  78. package/dist/lib/node/index.cjs +0 -1187
  79. package/dist/lib/node/index.cjs.map +0 -7
  80. package/dist/lib/node/meta.json +0 -1
  81. package/dist/lib/node-esm/countries-110m-DQ4XRC4B.mjs.map +0 -7
  82. package/dist/types/src/components/types.d.ts +0 -15
  83. package/dist/types/src/components/types.d.ts.map +0 -1
  84. package/src/components/types.ts +0 -19
  85. /package/dist/lib/browser/{chunk-ENCWOTYX.mjs.map → chunk-GMWLKTLN.mjs.map} +0 -0
  86. /package/dist/lib/node-esm/{chunk-PIIEDZEU.mjs.map → chunk-JODBF4CC.mjs.map} +0 -0
@@ -2,45 +2,56 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react';
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import React, { useState } from 'react';
9
7
 
10
- import { withLayout, withTheme } from '@dxos/storybook-utils';
8
+ import { withTheme } from '@dxos/react-ui/testing';
11
9
 
12
- import { Map, type MapController } from './Map';
13
10
  import { useMapZoomHandler } from '../../hooks';
14
- import { type MapMarker } from '../../types';
11
+ import { type GeoMarker } from '../../types';
12
+
13
+ import { Map, type MapController } from './Map';
15
14
 
16
- const DefaultStory = ({ markers = [] }: { markers?: MapMarker[] }) => {
15
+ const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
17
16
  const [controller, setController] = useState<MapController>();
18
17
  const handleZoomAction = useMapZoomHandler(controller);
19
18
 
20
19
  return (
21
- <Map.Root>
22
- <Map.Canvas ref={setController} markers={markers} />
20
+ <Map.Root ref={setController}>
21
+ <Map.Tiles />
22
+ <Map.Markers markers={markers} />
23
23
  <Map.Zoom position='bottomleft' onAction={handleZoomAction} />
24
24
  <Map.Action position='bottomright' />
25
25
  </Map.Root>
26
26
  );
27
27
  };
28
28
 
29
- const meta: Meta<typeof DefaultStory> = {
29
+ const meta = {
30
30
  title: 'ui/react-ui-geo/Map',
31
+ component: Map.Root as any,
31
32
  render: DefaultStory,
32
- decorators: [withTheme, withLayout({ fullscreen: true })],
33
- };
33
+ decorators: [withTheme()],
34
+ parameters: {
35
+ layout: 'fullscreen',
36
+ },
37
+ } satisfies Meta<typeof DefaultStory>;
34
38
 
35
39
  export default meta;
36
40
 
37
- type Story = StoryObj<typeof DefaultStory>;
41
+ type Story = StoryObj<typeof meta>;
38
42
 
39
43
  export const Default: Story = {};
40
44
 
41
45
  export const WithMarkers: Story = {
42
46
  args: {
43
47
  markers: [
48
+ { id: 'los angeles', title: 'Los Angeles', location: { lat: 34.0522, lng: -118.2437 } },
49
+ { id: 'new york', title: 'New York', location: { lat: 40.7128, lng: -74.006 } },
50
+ { id: 'warsaw', title: 'Warsaw', location: { lat: 52.2297, lng: 21.0122 } },
51
+ { id: 'london', title: 'London', location: { lat: 51.5074, lng: -0.1278 } },
52
+ { id: 'toronto', title: 'Toronto', location: { lat: 43.6532, lng: -79.3832 } },
53
+ { id: 'seattle', title: 'Seattle', location: { lat: 47.6062, lng: -122.3321 } },
54
+ { id: 'barcelona', title: 'Barcelona', location: { lat: 41.3851, lng: 2.1734 } },
44
55
  { id: 'tokyo', title: 'Tokyo', location: { lat: 35.6762, lng: 139.6503 } },
45
56
  { id: 'sydney', title: 'Sydney', location: { lat: -33.8688, lng: 151.2093 } },
46
57
  { id: 'auckland', title: 'Auckland', location: { lat: -36.8509, lng: 174.7645 } },
@@ -56,6 +67,6 @@ export const WithMarkers: Story = {
56
67
  { id: 'phnom-penh', title: 'Phnom Penh', location: { lat: 11.5564, lng: 104.9282 } },
57
68
  { id: 'vientiane', title: 'Vientiane', location: { lat: 17.9757, lng: 102.6331 } },
58
69
  { id: 'yangon', title: 'Yangon', location: { lat: 16.8661, lng: 96.1951 } },
59
- ] as MapMarker[],
70
+ ] as GeoMarker[],
60
71
  },
61
72
  };
@@ -2,109 +2,195 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- // eslint-disable-next-line no-restricted-imports
6
5
  import 'leaflet/dist/leaflet.css';
7
6
 
8
- import L, { Control, DomEvent, DomUtil, latLngBounds, type ControlPosition, type LatLngExpression } from 'leaflet';
9
- import React, { forwardRef, useEffect, useImperativeHandle, type PropsWithChildren } from 'react';
7
+ import { createContext } from '@radix-ui/react-context';
8
+ import L, { Control, type ControlPosition, DomEvent, DomUtil, type LatLngLiteral, latLngBounds } from 'leaflet';
9
+ import React, { type PropsWithChildren, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
10
10
  import { createRoot } from 'react-dom/client';
11
- import type { MapContainerProps } from 'react-leaflet';
12
- import { MapContainer, Marker, Popup, TileLayer, useMap } from 'react-leaflet';
13
- import { useResizeDetector } from 'react-resize-detector';
11
+ import { MapContainer, type MapContainerProps, Marker, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet';
14
12
 
15
- import { debounce } from '@dxos/async';
16
- import { ThemeProvider, Tooltip, type ThemedClassName } from '@dxos/react-ui';
17
- import { defaultTx, mx } from '@dxos/react-ui-theme';
13
+ import { ThemeProvider, type ThemedClassName, Tooltip } from '@dxos/react-ui';
14
+ import { defaultTx, mx } from '@dxos/ui-theme';
18
15
 
19
- import { ActionControls, controlPositions, ZoomControls, type ControlProps } from '../Toolbar';
20
- import { type MapCanvasProps } from '../types';
16
+ import { type GeoMarker } from '../../types';
17
+ import { ActionControls, type ControlProps, ZoomControls, controlPositions } from '../Toolbar';
21
18
 
22
19
  // TODO(burdon): Explore plugins: https://www.npmjs.com/search?q=keywords%3Areact-leaflet-v4
23
20
  // TODO(burdon): react-leaflet v5 is not compatible with react 18.
21
+ // TODO(burdon): Guess initial location.
24
22
 
25
23
  const defaults = {
26
- // TODO(burdon): Guess location.
27
- center: { lat: 51, lng: 0 } as L.LatLngExpression,
24
+ center: { lat: 51, lng: 0 } as L.LatLngLiteral,
28
25
  zoom: 4,
29
- };
26
+ } as const;
30
27
 
31
28
  //
32
- // Root
29
+ // Controller
33
30
  //
34
31
 
35
- type MapRootProps = ThemedClassName<MapContainerProps>;
36
-
37
- // https://react-leaflet.js.org/docs/api-map
38
- const MapRoot = ({ classNames, center = defaults.center, zoom = defaults.zoom, ...props }: MapRootProps) => {
39
- return (
40
- <MapContainer
41
- className={mx('relative grid grow bg-baseSurface', classNames)}
42
- attributionControl={false}
43
- // TODO(burdon): Only if attention.
44
- scrollWheelZoom={true}
45
- zoomControl={false}
46
- center={center}
47
- zoom={zoom}
48
- {...props}
49
- />
50
- );
32
+ type MapController = {
33
+ setCenter: (center: LatLngLiteral, zoom?: number) => void;
34
+ setZoom: (cb: (zoom: number) => number) => void;
51
35
  };
52
36
 
53
37
  //
54
- // Control
38
+ // Context
55
39
  //
56
40
 
57
- // TODO(burdon): Normalize with Globe.
58
- type MapController = {
59
- setCenter: (center: LatLngExpression, zoom?: number) => void;
60
- setZoom: (cb: (zoom: number) => number) => void;
41
+ type MapContextValue = {
42
+ attention?: boolean;
43
+ onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
61
44
  };
62
45
 
63
- const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center, zoom, onChange }, forwardedRef) => {
64
- const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
65
- const map = useMap();
46
+ const [MapContextProvier, useMapContext] = createContext<MapContextValue>('Map');
47
+
48
+ //
49
+ // Root
50
+ //
66
51
 
67
- useImperativeHandle(
52
+ type MapRootProps = ThemedClassName<MapContainerProps & Pick<MapContextValue, 'onChange'>>;
53
+
54
+ /**
55
+ * https://react-leaflet.js.org/docs/api-map
56
+ */
57
+ const MapRoot = forwardRef<MapController, MapRootProps>(
58
+ (
59
+ { classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, onChange, ...props },
68
60
  forwardedRef,
69
- () => ({
70
- setCenter: (center: LatLngExpression, zoom?: number) => {
71
- map.setView(center, zoom);
72
- },
73
- setZoom: (cb) => {
74
- map.setZoom(cb(map.getZoom()));
75
- },
76
- }),
77
- [map],
78
- );
61
+ ) => {
62
+ const [attention, setAttention] = useState(false);
63
+ const mapRef = useRef<L.Map>(null);
64
+ const map = mapRef.current;
79
65
 
80
- // Resize.
81
- useEffect(() => {
82
- if (width && height) {
83
- map.invalidateSize();
84
- }
85
- }, [width, height]);
66
+ useImperativeHandle(
67
+ forwardedRef,
68
+ () => ({
69
+ setCenter: (center: LatLngLiteral, zoom?: number) => {
70
+ mapRef.current?.setView(center, zoom);
71
+ },
72
+ setZoom: (cb: (zoom: number) => number) => {
73
+ mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
74
+ },
75
+ }),
76
+ [],
77
+ );
78
+
79
+ // Enable/disable scroll wheel zoom.
80
+ // TODO(burdon): Use attention:
81
+ // const {hasAttention} = useAttention(props.id);
82
+ useEffect(() => {
83
+ if (!map) {
84
+ return;
85
+ }
86
+
87
+ if (attention) {
88
+ map.scrollWheelZoom.enable();
89
+ } else {
90
+ map.scrollWheelZoom.disable();
91
+ }
92
+ }, [map, attention]);
93
+
94
+ return (
95
+ <MapContextProvier attention={attention} onChange={onChange}>
96
+ <MapContainer
97
+ {...props}
98
+ ref={mapRef}
99
+ className={mx('group relative grid bs-full is-full !bg-baseSurface dx-focus-ring-inset', classNames)}
100
+ attributionControl={false}
101
+ zoomControl={false}
102
+ scrollWheelZoom={scrollWheelZoom}
103
+ doubleClickZoom={doubleClickZoom}
104
+ touchZoom={touchZoom}
105
+ center={center ?? defaults.center}
106
+ zoom={zoom ?? defaults.zoom}
107
+ // whenReady={() => {}}
108
+ />
109
+ </MapContextProvier>
110
+ );
111
+ },
112
+ );
113
+
114
+ MapRoot.displayName = 'Map.Root';
115
+
116
+ //
117
+ // Tiles
118
+ // https://react-leaflet.js.org/docs/api-components/#tilelayer
119
+ //
120
+
121
+ const MAP_TILES_NAME = 'Map.Tiles';
122
+
123
+ type MapTilesProps = {};
124
+
125
+ const MapTiles = (_props: MapTilesProps) => {
126
+ const ref = useRef<L.TileLayer>(null);
127
+ const { onChange } = useMapContext(MAP_TILES_NAME);
86
128
 
87
- // Position.
129
+ useMapEvents({
130
+ zoomstart: (ev) => {
131
+ onChange?.({
132
+ center: ev.target.getCenter(),
133
+ zoom: ev.target.getZoom(),
134
+ });
135
+ },
136
+ });
137
+
138
+ // NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
139
+ // Tailwind requires setting the property for static analysis.
140
+ const { attention } = useMapContext(MAP_TILES_NAME);
88
141
  useEffect(() => {
89
- if (center) {
90
- map.setView(center, zoom);
91
- } else if (zoom !== undefined) {
92
- map.setZoom(zoom);
142
+ if (ref.current) {
143
+ ref.current.getContainer().dataset.attention = attention ? '1' : '0';
93
144
  }
94
- }, [center, zoom]);
145
+ }, [attention]);
95
146
 
96
- // Events.
97
- useEffect(() => {
98
- const handler = debounce(() => {
99
- onChange?.({ center: map.getCenter(), zoom: map.getZoom() });
100
- }, 100);
101
- map.on('move', handler);
102
- map.on('zoom', handler);
103
- return () => {
104
- map.off('move', handler);
105
- map.off('zoom', handler);
106
- };
107
- }, [map, onChange]);
147
+ // TODO(burdon): Option to add class 'invert'.
148
+ return (
149
+ <>
150
+ <TileLayer
151
+ ref={ref}
152
+ data-attention={attention}
153
+ detectRetina={true}
154
+ className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
155
+ url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
156
+ keepBuffer={4}
157
+ // opacity={attention ? 1 : 0.7}
158
+ />
159
+
160
+ {/* Temperature map. */}
161
+ {/* <WMSTileLayer
162
+ url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
163
+ layers='MODIS_Terra_Land_Surface_Temp_Day'
164
+ format='image/png'
165
+ transparent={true}
166
+ version='1.3.0'
167
+ attribution='NASA GIBS'
168
+ /> */}
169
+
170
+ {/* US Weather. */}
171
+ {/* <WMSTileLayer
172
+ url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
173
+ layers='nexrad-n0r' // layers='nexrad-n0r'
174
+ format='image/png'
175
+ transparent={true}
176
+ /> */}
177
+ </>
178
+ );
179
+ };
180
+
181
+ MapTiles.displayName = MAP_TILES_NAME;
182
+
183
+ //
184
+ // Markers
185
+ //
186
+
187
+ type MapMarkersProps = {
188
+ markers?: GeoMarker[];
189
+ selected?: string[];
190
+ };
191
+
192
+ const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
193
+ const map = useMap();
108
194
 
109
195
  // Set the viewport around the markers, or show the whole world map if `markers` is empty.
110
196
  useEffect(() => {
@@ -117,14 +203,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
117
203
  }, [markers]);
118
204
 
119
205
  return (
120
- <div ref={ref} className='flex w-full h-full overflow-hidden bg-baseSurface'>
121
- {/* Map tiles. */}
122
- <TileLayer
123
- className='dark:filter dark:grayscale dark:invert'
124
- url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
125
- />
126
-
127
- {/* Markers. */}
206
+ <>
128
207
  {markers?.map(({ id, title, location: { lat, lng } }) => {
129
208
  return (
130
209
  <Marker
@@ -132,6 +211,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
132
211
  position={{ lat, lng }}
133
212
  icon={
134
213
  // TODO(burdon): Create custom icon from bundled assets.
214
+ // TODO(burdon): Selection state.
135
215
  new L.Icon({
136
216
  iconUrl: 'https://dxos.network/marker-icon.png',
137
217
  iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
@@ -147,9 +227,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
147
227
  </Marker>
148
228
  );
149
229
  })}
150
- </div>
230
+ </>
151
231
  );
152
- });
232
+ };
233
+
234
+ MapMarkers.displayName = 'Map.Markers';
153
235
 
154
236
  //
155
237
  // Controls
@@ -192,23 +274,28 @@ const CustomControl = ({
192
274
 
193
275
  type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
194
276
 
277
+ const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
278
+ <CustomControl position={position} {...props}>
279
+ <ZoomControls onAction={onAction} />
280
+ </CustomControl>
281
+ );
282
+
283
+ const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
284
+ <CustomControl position={position} {...props}>
285
+ <ActionControls onAction={onAction} />
286
+ </CustomControl>
287
+ );
288
+
195
289
  //
196
290
  // Map
197
291
  //
198
292
 
199
293
  export const Map = {
200
294
  Root: MapRoot,
201
- Canvas: MapCanvas,
202
- Zoom: ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
203
- <CustomControl position={position} {...props}>
204
- <ZoomControls onAction={onAction} />
205
- </CustomControl>
206
- ),
207
- Action: ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
208
- <CustomControl position={position} {...props}>
209
- <ActionControls onAction={onAction} />
210
- </CustomControl>
211
- ),
295
+ Tiles: MapTiles,
296
+ Markers: MapMarkers,
297
+ Zoom: MapZoom,
298
+ Action: MapAction,
212
299
  };
213
300
 
214
- export { type MapCanvasProps, type MapController };
301
+ export { type MapController, type MapRootProps, type MapTilesProps, type MapMarkersProps, type MapControlProps };
@@ -5,7 +5,9 @@
5
5
  import { type ControlPosition } from 'leaflet';
6
6
  import React from 'react';
7
7
 
8
- import { IconButton, type ThemedClassName, Toolbar } from '@dxos/react-ui';
8
+ import { IconButton, type ThemedClassName, Toolbar, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { translationKey } from '../../translations';
9
11
 
10
12
  export type ControlAction = 'toggle' | 'start' | 'zoom-in' | 'zoom-out';
11
13
 
@@ -21,24 +23,20 @@ export const controlPositions: Record<ControlPosition, string> = {
21
23
  };
22
24
 
23
25
  export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
26
+ const { t } = useTranslation(translationKey);
27
+
24
28
  return (
25
- <Toolbar.Root classNames={['gap-1', classNames]}>
29
+ <Toolbar.Root classNames={['gap-2', classNames]}>
26
30
  <IconButton
27
- //
28
31
  icon='ph--plus--regular'
29
- label='zoom in'
30
32
  iconOnly
31
- size={5}
32
- classNames='px-0 aspect-square'
33
+ label={t('zoom in icon button')}
33
34
  onClick={() => onAction?.('zoom-in')}
34
35
  />
35
36
  <IconButton
36
- //
37
37
  icon='ph--minus--regular'
38
- label='zoom out'
39
38
  iconOnly
40
- size={5}
41
- classNames='px-0 aspect-square'
39
+ label={t('zoom out icon button')}
42
40
  onClick={() => onAction?.('zoom-out')}
43
41
  />
44
42
  </Toolbar.Root>
@@ -46,24 +44,20 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
46
44
  };
47
45
 
48
46
  export const ActionControls = ({ classNames, onAction }: ControlProps) => {
47
+ const { t } = useTranslation(translationKey);
48
+
49
49
  return (
50
- <Toolbar.Root classNames={['gap-1', classNames]}>
50
+ <Toolbar.Root classNames={['gap-2', classNames]}>
51
51
  <IconButton
52
- //
53
- icon='ph--play--regular'
54
- label='start'
52
+ icon='ph--path--regular'
55
53
  iconOnly
56
- size={5}
57
- classNames='px-0 aspect-square'
54
+ label={t('start icon button')}
58
55
  onClick={() => onAction?.('start')}
59
56
  />
60
57
  <IconButton
61
- //
62
58
  icon='ph--globe-hemisphere-west--regular'
63
- label='toggle'
64
59
  iconOnly
65
- size={5}
66
- classNames='px-0 aspect-square'
60
+ label={t('toggle icon button')}
67
61
  onClick={() => onAction?.('toggle')}
68
62
  />
69
63
  </Toolbar.Root>
@@ -2,8 +2,6 @@
2
2
  // Copyright 2019 DXOS.org
3
3
  //
4
4
 
5
- export * from './types';
6
-
7
5
  export * from './Globe';
8
6
  export * from './Map';
9
7
  export * from './Toolbar';
@@ -2,12 +2,12 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { createContext, type Dispatch, type PropsWithChildren, type SetStateAction, useContext } from 'react';
5
+ import React, { type Dispatch, type PropsWithChildren, type SetStateAction, createContext, useContext } from 'react';
6
6
 
7
7
  import { raise } from '@dxos/debug';
8
8
  import { useControlledState } from '@dxos/react-ui';
9
9
 
10
- import { type LatLng } from '../util';
10
+ import { type LatLngLiteral } from '../types';
11
11
 
12
12
  // TODO(burdon): Factor out common geometry types.
13
13
  export type Size = { width: number; height: number };
@@ -16,38 +16,44 @@ export type Vector = [number, number, number];
16
16
 
17
17
  export type GlobeContextType = {
18
18
  size: Size;
19
- center: LatLng;
20
- scale: number;
19
+ center?: LatLngLiteral;
20
+ zoom: number;
21
21
  translation: Point;
22
22
  rotation: Vector;
23
- setCenter: Dispatch<SetStateAction<LatLng>>;
24
- setScale: Dispatch<SetStateAction<number>>;
23
+ setCenter: Dispatch<SetStateAction<LatLngLiteral>>;
24
+ setZoom: Dispatch<SetStateAction<number>>;
25
25
  setTranslation: Dispatch<SetStateAction<Point>>;
26
26
  setRotation: Dispatch<SetStateAction<Vector>>;
27
27
  };
28
28
 
29
+ const defaults = {
30
+ center: { lat: 51, lng: 0 } as LatLngLiteral,
31
+ zoom: 4,
32
+ } as const;
33
+
34
+ // TODO(burdon): Replace with radix.
29
35
  const GlobeContext = createContext<GlobeContextType>(undefined);
30
36
 
31
37
  export type GlobeContextProviderProps = PropsWithChildren<
32
- Partial<Pick<GlobeContextType, 'size' | 'center' | 'scale' | 'translation' | 'rotation'>>
38
+ Partial<Pick<GlobeContextType, 'size' | 'center' | 'zoom' | 'translation' | 'rotation'>>
33
39
  >;
34
40
 
35
41
  export const GlobeContextProvider = ({
36
42
  children,
37
43
  size,
38
- center: _center,
39
- scale: _scale,
40
- translation: _translation,
41
- rotation: _rotation,
44
+ center: centerProp = defaults.center,
45
+ zoom: zoomProp = defaults.zoom,
46
+ translation: translationProp,
47
+ rotation: rotationProp,
42
48
  }: GlobeContextProviderProps) => {
43
- const [center, setCenter] = useControlledState(_center);
44
- const [scale, setScale] = useControlledState(_scale);
45
- const [translation, setTranslation] = useControlledState<Point>(_translation);
46
- const [rotation, setRotation] = useControlledState<Vector>(_rotation);
49
+ const [center, setCenter] = useControlledState(centerProp);
50
+ const [zoom, setZoom] = useControlledState(zoomProp);
51
+ const [translation, setTranslation] = useControlledState<Point>(translationProp);
52
+ const [rotation, setRotation] = useControlledState<Vector>(rotationProp);
47
53
 
48
54
  return (
49
55
  <GlobeContext.Provider
50
- value={{ size, center, scale, translation, rotation, setCenter, setScale, setTranslation, setRotation }}
56
+ value={{ size, center, zoom, translation, rotation, setCenter, setZoom, setTranslation, setRotation }}
51
57
  >
52
58
  {children}
53
59
  </GlobeContext.Provider>
@@ -4,7 +4,9 @@
4
4
 
5
5
  import { useCallback } from 'react';
6
6
 
7
- import { type GlobeController, type ControlProps } from '../components';
7
+ import { type ControlProps, type GlobeController } from '../components';
8
+
9
+ const ZOOM_FACTOR = 0.1;
8
10
 
9
11
  export const useGlobeZoomHandler = (controller: GlobeController | null | undefined): ControlProps['onAction'] => {
10
12
  return useCallback<ControlProps['onAction']>(
@@ -15,11 +17,15 @@ export const useGlobeZoomHandler = (controller: GlobeController | null | undefin
15
17
 
16
18
  switch (event) {
17
19
  case 'zoom-in': {
18
- controller.setScale((scale) => scale * 1.1);
20
+ controller.setZoom((zoom) => {
21
+ return zoom * (1 + ZOOM_FACTOR);
22
+ });
19
23
  break;
20
24
  }
21
25
  case 'zoom-out': {
22
- controller.setScale((scale) => scale * 0.9);
26
+ controller.setZoom((zoom) => {
27
+ return zoom * (1 - ZOOM_FACTOR);
28
+ });
23
29
  break;
24
30
  }
25
31
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { useCallback } from 'react';
6
6
 
7
- import { type MapController, type ControlProps } from '../components';
7
+ import { type ControlProps, type MapController } from '../components';
8
8
 
9
9
  export const useMapZoomHandler = (controller: MapController | null | undefined): ControlProps['onAction'] => {
10
10
  return useCallback<ControlProps['onAction']>(
@@ -6,9 +6,10 @@ import { timer as d3Timer } from 'd3';
6
6
  import { type Timer } from 'd3';
7
7
  import { useEffect, useState } from 'react';
8
8
 
9
- import { type Vector } from './context';
10
9
  import { type GlobeController } from '../components';
11
10
 
11
+ import { type Vector } from './context';
12
+
12
13
  export type SpinnerOptions = {
13
14
  disabled?: boolean;
14
15
  delta?: Vector;