@dxos/react-ui-geo 0.8.3 → 0.8.4-main.1c7ec43d41

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 (100) 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/{countries-110m-WI4PCLDF.mjs → countries-110m-RE5RNRQG.mjs} +2 -2
  7. package/dist/lib/browser/countries-110m-RE5RNRQG.mjs.map +7 -0
  8. package/dist/lib/browser/data.mjs +4 -3
  9. package/dist/lib/browser/data.mjs.map +4 -4
  10. package/dist/lib/browser/index.mjs +396 -466
  11. package/dist/lib/browser/index.mjs.map +3 -3
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/browser/translations.mjs +19 -0
  14. package/dist/lib/browser/translations.mjs.map +7 -0
  15. package/dist/lib/node-esm/{countries-110m-DQ4XRC4B.mjs → countries-110m-4EDBXSFJ.mjs} +2 -2
  16. package/dist/lib/node-esm/countries-110m-4EDBXSFJ.mjs.map +7 -0
  17. package/dist/lib/node-esm/data.mjs +5 -3
  18. package/dist/lib/node-esm/data.mjs.map +4 -4
  19. package/dist/lib/node-esm/index.mjs +396 -465
  20. package/dist/lib/node-esm/index.mjs.map +3 -3
  21. package/dist/lib/node-esm/meta.json +1 -1
  22. package/dist/lib/node-esm/translations.mjs +21 -0
  23. package/dist/lib/node-esm/translations.mjs.map +7 -0
  24. package/dist/types/data/airports.d.ts +4 -4
  25. package/dist/types/data/airports.d.ts.map +1 -1
  26. package/dist/types/data/cities.d.ts.map +1 -1
  27. package/dist/types/data/countries-110m.d.ts.map +1 -1
  28. package/dist/types/data/countries-dots-3.d.ts.map +1 -1
  29. package/dist/types/data/countries-dots-4.d.ts.map +1 -1
  30. package/dist/types/src/components/Globe/Globe.d.ts +6 -4
  31. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  32. package/dist/types/src/components/Globe/Globe.stories.d.ts +27 -9
  33. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  34. package/dist/types/src/components/Map/Map.d.ts +42 -18
  35. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  36. package/dist/types/src/components/Map/Map.stories.d.ts +14 -8
  37. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  38. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  39. package/dist/types/src/components/index.d.ts +0 -1
  40. package/dist/types/src/components/index.d.ts.map +1 -1
  41. package/dist/types/src/hooks/context.d.ts +6 -8
  42. package/dist/types/src/hooks/context.d.ts.map +1 -1
  43. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +2 -2
  45. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  46. package/dist/types/src/hooks/useMapZoomHandler.d.ts +2 -2
  47. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  48. package/dist/types/src/hooks/useSpinner.d.ts +1 -1
  49. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  50. package/dist/types/src/hooks/useTour.d.ts +4 -3
  51. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  52. package/dist/types/src/index.d.ts +1 -2
  53. package/dist/types/src/index.d.ts.map +1 -1
  54. package/dist/types/src/translations.d.ts +12 -0
  55. package/dist/types/src/translations.d.ts.map +1 -0
  56. package/dist/types/src/types.d.ts +2 -1
  57. package/dist/types/src/types.d.ts.map +1 -1
  58. package/dist/types/src/util/debug.d.ts.map +1 -1
  59. package/dist/types/src/util/inertia.d.ts.map +1 -1
  60. package/dist/types/src/util/path.d.ts +5 -8
  61. package/dist/types/src/util/path.d.ts.map +1 -1
  62. package/dist/types/src/util/render.d.ts +4 -4
  63. package/dist/types/src/util/render.d.ts.map +1 -1
  64. package/dist/types/tsconfig.tsbuildinfo +1 -1
  65. package/package.json +43 -34
  66. package/src/components/Globe/Globe.stories.tsx +85 -38
  67. package/src/components/Globe/Globe.tsx +124 -81
  68. package/src/components/Map/Map.stories.tsx +27 -15
  69. package/src/components/Map/Map.tsx +220 -94
  70. package/src/components/Toolbar/Controls.tsx +14 -20
  71. package/src/components/index.ts +0 -2
  72. package/src/hooks/context.tsx +11 -34
  73. package/src/hooks/useGlobeZoomHandler.ts +9 -3
  74. package/src/hooks/useMapZoomHandler.ts +1 -1
  75. package/src/hooks/useSpinner.ts +1 -1
  76. package/src/hooks/useTour.ts +10 -8
  77. package/src/index.ts +1 -2
  78. package/src/translations.ts +20 -0
  79. package/src/types.ts +3 -1
  80. package/src/util/inertia.ts +1 -1
  81. package/src/util/path.ts +5 -6
  82. package/src/util/render.ts +4 -3
  83. package/dist/lib/browser/chunk-ENCWOTYX.mjs +0 -9
  84. package/dist/lib/browser/chunk-ENCWOTYX.mjs.map +0 -7
  85. package/dist/lib/browser/countries-110m-WI4PCLDF.mjs.map +0 -7
  86. package/dist/lib/node/chunk-LAICG6L2.cjs +0 -40
  87. package/dist/lib/node/chunk-LAICG6L2.cjs.map +0 -7
  88. package/dist/lib/node/countries-110m-KQ5WAB2O.cjs +0 -37877
  89. package/dist/lib/node/countries-110m-KQ5WAB2O.cjs.map +0 -7
  90. package/dist/lib/node/data.cjs +0 -28
  91. package/dist/lib/node/data.cjs.map +0 -7
  92. package/dist/lib/node/index.cjs +0 -1187
  93. package/dist/lib/node/index.cjs.map +0 -7
  94. package/dist/lib/node/meta.json +0 -1
  95. package/dist/lib/node-esm/chunk-PIIEDZEU.mjs +0 -11
  96. package/dist/lib/node-esm/chunk-PIIEDZEU.mjs.map +0 -7
  97. package/dist/lib/node-esm/countries-110m-DQ4XRC4B.mjs.map +0 -7
  98. package/dist/types/src/components/types.d.ts +0 -15
  99. package/dist/types/src/components/types.d.ts.map +0 -1
  100. package/src/components/types.ts +0 -19
@@ -2,109 +2,226 @@
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 } 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 { type ThemedClassName, ThemeProvider, Tooltip } from '@dxos/react-ui';
14
+ import { composable, composableProps, 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,
26
+ } as const;
27
+
28
+ //
29
+ // Controller
30
+ //
31
+
32
+ type MapController = {
33
+ setCenter: (center: LatLngLiteral, zoom?: number) => void;
34
+ setZoom: (cb: (zoom: number) => number) => void;
35
+ };
36
+
37
+ //
38
+ // Context
39
+ //
40
+
41
+ type MapContextValue = {
42
+ attention?: boolean;
43
+ onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
29
44
  };
30
45
 
46
+ const [MapContextProvider, useMapContext] = createContext<MapContextValue>('Map');
47
+
31
48
  //
32
49
  // Root
33
50
  //
34
51
 
35
- type MapRootProps = ThemedClassName<MapContainerProps>;
52
+ type MapRootProps = Pick<MapContextValue, 'onChange'>;
36
53
 
37
- // https://react-leaflet.js.org/docs/api-map
38
- const MapRoot = ({ classNames, center = defaults.center, zoom = defaults.zoom, ...props }: MapRootProps) => {
54
+ /**
55
+ * Context provider for the map. Must wrap Map.Content.
56
+ */
57
+ const MapRoot = composable<HTMLDivElement, MapRootProps>(({ children, onChange, ...props }, forwardedRef) => {
58
+ // TODO(burdon): Use attention: const [attention, setAttention] = useState(false);
59
+ const attention = false;
39
60
  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
- />
61
+ <MapContextProvider attention={attention} onChange={onChange}>
62
+ <div
63
+ {...composableProps(props, {
64
+ role: 'none',
65
+ classNames: 'dx-container grid dx-focus-ring-inset',
66
+ })}
67
+ ref={forwardedRef}
68
+ >
69
+ {children}
70
+ </div>
71
+ </MapContextProvider>
50
72
  );
51
- };
73
+ });
74
+
75
+ MapRoot.displayName = 'Map.Root';
52
76
 
53
77
  //
54
- // Control
78
+ // Content
55
79
  //
56
80
 
57
- // TODO(burdon): Normalize with Globe.
58
- type MapController = {
59
- setCenter: (center: LatLngExpression, zoom?: number) => void;
60
- setZoom: (cb: (zoom: number) => number) => void;
61
- };
81
+ type MapContentProps = ThemedClassName<Omit<MapContainerProps, 'children'> & PropsWithChildren>;
62
82
 
63
- const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center, zoom, onChange }, forwardedRef) => {
64
- const { ref, width, height } = useResizeDetector({ refreshRate: 200 });
65
- const map = useMap();
83
+ /**
84
+ * https://react-leaflet.js.org/docs/api-map
85
+ */
86
+ const MAP_CONTENT_NAME = 'Map.Content';
66
87
 
67
- useImperativeHandle(
88
+ const MapContent = forwardRef<MapController, MapContentProps>(
89
+ (
90
+ { classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props },
68
91
  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
- );
92
+ ) => {
93
+ const { attention } = useMapContext(MAP_CONTENT_NAME);
94
+ const mapRef = useRef<L.Map>(null);
95
+ const map = mapRef.current;
79
96
 
80
- // Resize.
81
- useEffect(() => {
82
- if (width && height) {
83
- map.invalidateSize();
84
- }
85
- }, [width, height]);
97
+ useImperativeHandle(
98
+ forwardedRef,
99
+ () => ({
100
+ setCenter: (center: LatLngLiteral, zoom?: number) => {
101
+ mapRef.current?.setView(center, zoom);
102
+ },
103
+ setZoom: (cb: (zoom: number) => number) => {
104
+ mapRef.current?.setZoom(cb(mapRef.current?.getZoom() ?? 0));
105
+ },
106
+ }),
107
+ [],
108
+ );
109
+
110
+ // Enable/disable scroll wheel zoom.
111
+ // TODO(burdon): Use attention:
112
+ // const {hasAttention} = useAttention(props.id);
113
+ useEffect(() => {
114
+ if (!map) {
115
+ return;
116
+ }
117
+
118
+ if (attention) {
119
+ map.scrollWheelZoom.enable();
120
+ } else {
121
+ map.scrollWheelZoom.disable();
122
+ }
123
+ }, [map, attention]);
124
+
125
+ return (
126
+ <MapContainer
127
+ {...props}
128
+ className={mx('group relative grid bg-base-surface!', classNames)}
129
+ attributionControl={false}
130
+ zoomControl={false}
131
+ scrollWheelZoom={scrollWheelZoom}
132
+ doubleClickZoom={doubleClickZoom}
133
+ touchZoom={touchZoom}
134
+ center={center ?? defaults.center}
135
+ zoom={zoom ?? defaults.zoom}
136
+ whenReady={() => {}}
137
+ ref={mapRef}
138
+ >
139
+ {children}
140
+ </MapContainer>
141
+ );
142
+ },
143
+ );
144
+
145
+ MapContent.displayName = 'Map.Content';
146
+
147
+ //
148
+ // Tiles
149
+ // https://react-leaflet.js.org/docs/api-components/#tilelayer
150
+ //
151
+
152
+ const MAP_TILES_NAME = 'Map.Tiles';
153
+
154
+ type MapTilesProps = {};
86
155
 
87
- // Position.
156
+ const MapTiles = (_props: MapTilesProps) => {
157
+ const ref = useRef<L.TileLayer>(null);
158
+ const { onChange } = useMapContext(MAP_TILES_NAME);
159
+
160
+ useMapEvents({
161
+ zoomstart: (ev) => {
162
+ onChange?.({
163
+ center: ev.target.getCenter(),
164
+ zoom: ev.target.getZoom(),
165
+ });
166
+ },
167
+ });
168
+
169
+ // NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
170
+ // Tailwind requires setting the property for static analysis.
171
+ const { attention } = useMapContext(MAP_TILES_NAME);
88
172
  useEffect(() => {
89
- if (center) {
90
- map.setView(center, zoom);
91
- } else if (zoom !== undefined) {
92
- map.setZoom(zoom);
173
+ if (ref.current) {
174
+ ref.current.getContainer().dataset.attention = attention ? '1' : '0';
93
175
  }
94
- }, [center, zoom]);
176
+ }, [attention]);
95
177
 
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]);
178
+ // TODO(burdon): Option to add class 'invert'.
179
+ return (
180
+ <>
181
+ <TileLayer
182
+ ref={ref}
183
+ data-attention={attention}
184
+ detectRetina={true}
185
+ className='dark:grayscale dark:invert data-[attention="0"]:!opacity-80'
186
+ url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
187
+ keepBuffer={4}
188
+ // opacity={attention ? 1 : 0.7}
189
+ />
190
+
191
+ {/* Temperature map. */}
192
+ {/* <WMSTileLayer
193
+ url='https://gibs.earthdata.nasa.gov/wms/epsg4326/best/wms.cgi'
194
+ layers='MODIS_Terra_Land_Surface_Temp_Day'
195
+ format='image/png'
196
+ transparent={true}
197
+ version='1.3.0'
198
+ attribution='NASA GIBS'
199
+ /> */}
200
+
201
+ {/* US Weather. */}
202
+ {/* <WMSTileLayer
203
+ url='https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r.cgi'
204
+ layers='nexrad-n0r' // layers='nexrad-n0r'
205
+ format='image/png'
206
+ transparent={true}
207
+ /> */}
208
+ </>
209
+ );
210
+ };
211
+
212
+ MapTiles.displayName = MAP_TILES_NAME;
213
+
214
+ //
215
+ // Markers
216
+ //
217
+
218
+ type MapMarkersProps = {
219
+ markers?: GeoMarker[];
220
+ selected?: string[];
221
+ };
222
+
223
+ const MapMarkers = ({ selected, markers }: MapMarkersProps) => {
224
+ const map = useMap();
108
225
 
109
226
  // Set the viewport around the markers, or show the whole world map if `markers` is empty.
110
227
  useEffect(() => {
@@ -117,14 +234,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
117
234
  }, [markers]);
118
235
 
119
236
  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. */}
237
+ <>
128
238
  {markers?.map(({ id, title, location: { lat, lng } }) => {
129
239
  return (
130
240
  <Marker
@@ -132,6 +242,7 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
132
242
  position={{ lat, lng }}
133
243
  icon={
134
244
  // TODO(burdon): Create custom icon from bundled assets.
245
+ // TODO(burdon): Selection state.
135
246
  new L.Icon({
136
247
  iconUrl: 'https://dxos.network/marker-icon.png',
137
248
  iconRetinaUrl: 'https://dxos.network/marker-icon-2x.png',
@@ -147,9 +258,11 @@ const MapCanvas = forwardRef<MapController, MapCanvasProps>(({ markers, center,
147
258
  </Marker>
148
259
  );
149
260
  })}
150
- </div>
261
+ </>
151
262
  );
152
- });
263
+ };
264
+
265
+ MapMarkers.displayName = 'Map.Markers';
153
266
 
154
267
  //
155
268
  // Controls
@@ -167,7 +280,7 @@ const CustomControl = ({
167
280
  useEffect(() => {
168
281
  const control = new Control({ position });
169
282
  control.onAdd = () => {
170
- const container = DomUtil.create('div', mx('!m-0', controlPositions[position]));
283
+ const container = DomUtil.create('div', mx('m-0!', controlPositions[position]));
171
284
  DomEvent.disableClickPropagation(container);
172
285
  DomEvent.disableScrollPropagation(container);
173
286
 
@@ -192,23 +305,36 @@ const CustomControl = ({
192
305
 
193
306
  type MapControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
194
307
 
308
+ const MapZoom = ({ onAction, position = 'bottomleft', ...props }: MapControlProps) => (
309
+ <CustomControl position={position} {...props}>
310
+ <ZoomControls onAction={onAction} />
311
+ </CustomControl>
312
+ );
313
+
314
+ const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlProps) => (
315
+ <CustomControl position={position} {...props}>
316
+ <ActionControls onAction={onAction} />
317
+ </CustomControl>
318
+ );
319
+
195
320
  //
196
321
  // Map
197
322
  //
198
323
 
199
324
  export const Map = {
200
325
  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
- ),
326
+ Content: MapContent,
327
+ Tiles: MapTiles,
328
+ Markers: MapMarkers,
329
+ Zoom: MapZoom,
330
+ Action: MapAction,
212
331
  };
213
332
 
214
- export { type MapCanvasProps, type MapController };
333
+ export {
334
+ type MapController,
335
+ type MapRootProps,
336
+ type MapContentProps,
337
+ type MapTilesProps,
338
+ type MapMarkersProps,
339
+ type MapControlProps,
340
+ };
@@ -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,57 +2,34 @@
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 { type Dispatch, type SetStateAction, createContext, useContext } from 'react';
6
6
 
7
7
  import { raise } from '@dxos/debug';
8
- import { useControlledState } from '@dxos/react-ui';
9
8
 
10
- import { type LatLng } from '../util';
9
+ import { type LatLngLiteral } from '../types';
11
10
 
12
11
  // TODO(burdon): Factor out common geometry types.
13
12
  export type Size = { width: number; height: number };
13
+
14
14
  export type Point = { x: number; y: number };
15
+
15
16
  export type Vector = [number, number, number];
16
17
 
17
18
  export type GlobeContextType = {
18
19
  size: Size;
19
- center: LatLng;
20
- scale: number;
20
+ center?: LatLngLiteral;
21
+ zoom: number;
21
22
  translation: Point;
22
23
  rotation: Vector;
23
- setCenter: Dispatch<SetStateAction<LatLng>>;
24
- setScale: Dispatch<SetStateAction<number>>;
24
+ setCenter: Dispatch<SetStateAction<LatLngLiteral>>;
25
+ setZoom: Dispatch<SetStateAction<number>>;
25
26
  setTranslation: Dispatch<SetStateAction<Point>>;
26
27
  setRotation: Dispatch<SetStateAction<Vector>>;
27
28
  };
28
29
 
29
- const GlobeContext = createContext<GlobeContextType>(undefined);
30
-
31
- export type GlobeContextProviderProps = PropsWithChildren<
32
- Partial<Pick<GlobeContextType, 'size' | 'center' | 'scale' | 'translation' | 'rotation'>>
33
- >;
34
-
35
- export const GlobeContextProvider = ({
36
- children,
37
- size,
38
- center: _center,
39
- scale: _scale,
40
- translation: _translation,
41
- rotation: _rotation,
42
- }: 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);
47
-
48
- return (
49
- <GlobeContext.Provider
50
- value={{ size, center, scale, translation, rotation, setCenter, setScale, setTranslation, setRotation }}
51
- >
52
- {children}
53
- </GlobeContext.Provider>
54
- );
55
- };
30
+ /** @internal */
31
+ // TODO(burdon): Replace with radix.
32
+ export const GlobeContext = createContext<GlobeContextType>(undefined);
56
33
 
57
34
  export const useGlobeContext = () => {
58
35
  return useContext(GlobeContext) ?? raise(new Error('Missing GlobeContext'));
@@ -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,8 +6,8 @@ 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';
10
+ import { type Vector } from './context';
11
11
 
12
12
  export type SpinnerOptions = {
13
13
  disabled?: boolean;
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { geoPath, geoInterpolate, geoDistance, selection as d3Selection } from 'd3';
6
- import { type SetStateAction, type Dispatch, useEffect, useState, useMemo } from 'react';
5
+ import { selection as d3Selection, geoDistance, geoInterpolate, geoPath } from 'd3';
6
+ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
7
7
  import versor from 'versor';
8
8
 
9
9
  import type { GlobeController } from '../components';
10
- import { geoToPosition, type LatLng, positionToRotation, type StyleSet } from '../util';
10
+ import { type LatLngLiteral } from '../types';
11
+ import { type StyleSet, geoToPosition, positionToRotation } from '../util';
11
12
 
12
13
  const TRANSITION_NAME = 'globe-tour';
13
14
 
@@ -29,10 +30,11 @@ export type TourOptions = {
29
30
  */
30
31
  export const useTour = (
31
32
  controller?: GlobeController | null,
32
- points?: LatLng[],
33
+ points?: LatLngLiteral[],
33
34
  options: TourOptions = {},
34
35
  ): [boolean, Dispatch<SetStateAction<boolean>>] => {
35
36
  const selection = useMemo(() => d3Selection(), []);
37
+ // TODO(burdon): Redo controlled state.
36
38
  const [running, setRunning] = useState(options.running ?? false);
37
39
  useEffect(() => {
38
40
  if (!running) {
@@ -48,7 +50,7 @@ export const useTour = (
48
50
  const path = geoPath(projection, context).pointRadius(2);
49
51
 
50
52
  const tilt = options.tilt ?? 0;
51
- let last: LatLng;
53
+ let last: LatLngLiteral;
52
54
  try {
53
55
  const p = [...points];
54
56
  if (options.loop) {
@@ -82,14 +84,14 @@ export const useTour = (
82
84
  {
83
85
  context.beginPath();
84
86
  context.strokeStyle = options?.styles?.arc?.strokeStyle ?? 'yellow';
85
- context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.scale ?? 1);
87
+ context.lineWidth = (options?.styles?.arc?.lineWidth ?? 1.5) * (controller?.zoom ?? 1);
86
88
  context.setLineDash(options?.styles?.arc?.lineDash ?? []);
87
89
  path({ type: 'LineString', coordinates: [ip(t1), ip(t2)] });
88
90
  context.stroke();
89
91
 
90
92
  context.beginPath();
91
93
  context.fillStyle = options?.styles?.cursor?.fillStyle ?? 'orange';
92
- path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.scale ?? 1));
94
+ path.pointRadius((options?.styles?.cursor?.pointRadius ?? 2) * (controller?.zoom ?? 1));
93
95
  path({ type: 'Point', coordinates: ip(t2) });
94
96
  context.fill();
95
97
  }
@@ -104,7 +106,7 @@ export const useTour = (
104
106
  await transition.end();
105
107
  last = next;
106
108
  }
107
- } catch (err) {
109
+ } catch {
108
110
  // Ignore.
109
111
  } finally {
110
112
  setRunning(false);
package/src/index.ts CHANGED
@@ -3,7 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './components';
6
- export * from './data';
7
6
  export * from './hooks';
8
- export * from './types';
7
+ export type * from './types';
9
8
  export * from './util';