@dxos/react-ui-geo 0.8.4-main.84f28bd → 0.8.4-main.8baae0fced

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 (92) hide show
  1. package/LICENSE +102 -5
  2. package/data/airports.ts +1 -1
  3. package/data/cities.ts +1 -1
  4. package/data/countries-110m.ts +1 -1
  5. package/data/countries-dots-3.ts +1 -1
  6. package/data/countries-dots-4.ts +1 -1
  7. package/dist/lib/browser/{countries-110m-37VAAFCK.mjs → countries-110m-RE5RNRQG.mjs} +1 -1
  8. package/dist/lib/browser/countries-110m-RE5RNRQG.mjs.map +7 -0
  9. package/dist/lib/browser/data.mjs +4 -3
  10. package/dist/lib/browser/data.mjs.map +4 -4
  11. package/dist/lib/browser/index.mjs +388 -451
  12. package/dist/lib/browser/index.mjs.map +3 -3
  13. package/dist/lib/browser/meta.json +1 -1
  14. package/dist/lib/browser/translations.mjs +19 -0
  15. package/dist/lib/browser/translations.mjs.map +7 -0
  16. package/dist/lib/node-esm/{countries-110m-36TTKK5B.mjs → countries-110m-4EDBXSFJ.mjs} +1 -1
  17. package/dist/lib/node-esm/countries-110m-4EDBXSFJ.mjs.map +7 -0
  18. package/dist/lib/node-esm/data.mjs +5 -3
  19. package/dist/lib/node-esm/data.mjs.map +4 -4
  20. package/dist/lib/node-esm/index.mjs +388 -450
  21. package/dist/lib/node-esm/index.mjs.map +3 -3
  22. package/dist/lib/node-esm/meta.json +1 -1
  23. package/dist/lib/node-esm/translations.mjs +21 -0
  24. package/dist/lib/node-esm/translations.mjs.map +7 -0
  25. package/dist/types/data/airports.d.ts +4 -4
  26. package/dist/types/data/airports.d.ts.map +1 -1
  27. package/dist/types/data/cities.d.ts.map +1 -1
  28. package/dist/types/data/countries-110m.d.ts.map +1 -1
  29. package/dist/types/data/countries-dots-3.d.ts.map +1 -1
  30. package/dist/types/data/countries-dots-4.d.ts.map +1 -1
  31. package/dist/types/src/components/Globe/Globe.d.ts +6 -4
  32. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  33. package/dist/types/src/components/Globe/Globe.stories.d.ts +27 -9
  34. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  35. package/dist/types/src/components/Map/Map.d.ts +44 -18
  36. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  37. package/dist/types/src/components/Map/Map.stories.d.ts +14 -8
  38. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  39. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  40. package/dist/types/src/components/index.d.ts +0 -1
  41. package/dist/types/src/components/index.d.ts.map +1 -1
  42. package/dist/types/src/hooks/context.d.ts +6 -8
  43. package/dist/types/src/hooks/context.d.ts.map +1 -1
  44. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  45. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +2 -2
  46. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  47. package/dist/types/src/hooks/useMapZoomHandler.d.ts +2 -2
  48. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  49. package/dist/types/src/hooks/useSpinner.d.ts +1 -1
  50. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  51. package/dist/types/src/hooks/useTour.d.ts +4 -3
  52. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  53. package/dist/types/src/index.d.ts +1 -2
  54. package/dist/types/src/index.d.ts.map +1 -1
  55. package/dist/types/src/translations.d.ts +12 -0
  56. package/dist/types/src/translations.d.ts.map +1 -0
  57. package/dist/types/src/types.d.ts +2 -1
  58. package/dist/types/src/types.d.ts.map +1 -1
  59. package/dist/types/src/util/debug.d.ts.map +1 -1
  60. package/dist/types/src/util/inertia.d.ts.map +1 -1
  61. package/dist/types/src/util/path.d.ts +5 -8
  62. package/dist/types/src/util/path.d.ts.map +1 -1
  63. package/dist/types/src/util/render.d.ts +4 -4
  64. package/dist/types/src/util/render.d.ts.map +1 -1
  65. package/dist/types/tsconfig.tsbuildinfo +1 -1
  66. package/package.json +44 -35
  67. package/src/components/Globe/Globe.stories.tsx +82 -35
  68. package/src/components/Globe/Globe.tsx +133 -81
  69. package/src/components/Map/Map.stories.tsx +27 -15
  70. package/src/components/Map/Map.tsx +231 -99
  71. package/src/components/Toolbar/Controls.tsx +14 -20
  72. package/src/components/index.ts +0 -2
  73. package/src/hooks/context.tsx +11 -34
  74. package/src/hooks/useGlobeZoomHandler.ts +9 -3
  75. package/src/hooks/useMapZoomHandler.ts +1 -1
  76. package/src/hooks/useSpinner.ts +1 -1
  77. package/src/hooks/useTour.ts +10 -8
  78. package/src/index.ts +1 -2
  79. package/src/translations.ts +20 -0
  80. package/src/types.ts +3 -1
  81. package/src/util/inertia.ts +1 -1
  82. package/src/util/path.ts +5 -6
  83. package/src/util/render.ts +4 -3
  84. package/dist/lib/browser/chunk-CYCBMCOP.mjs +0 -9
  85. package/dist/lib/browser/chunk-CYCBMCOP.mjs.map +0 -7
  86. package/dist/lib/browser/countries-110m-37VAAFCK.mjs.map +0 -7
  87. package/dist/lib/node-esm/chunk-OPJPAAEK.mjs +0 -11
  88. package/dist/lib/node-esm/chunk-OPJPAAEK.mjs.map +0 -7
  89. package/dist/lib/node-esm/countries-110m-36TTKK5B.mjs.map +0 -7
  90. package/dist/types/src/components/types.d.ts +0 -15
  91. package/dist/types/src/components/types.d.ts.map +0 -1
  92. package/src/components/types.ts +0 -19
@@ -4,14 +4,14 @@
4
4
 
5
5
  import {
6
6
  type GeoProjection,
7
+ easeLinear,
8
+ easeSinOut,
7
9
  geoMercator,
8
10
  geoOrthographic,
9
11
  geoPath,
10
12
  geoTransverseMercator,
11
13
  interpolateNumber,
12
14
  transition,
13
- easeLinear,
14
- easeSinOut,
15
15
  } from 'd3';
16
16
  import { type ControlPosition } from 'leaflet';
17
17
  import React, {
@@ -26,15 +26,17 @@ import React, {
26
26
  import { useResizeDetector } from 'react-resize-detector';
27
27
  import { type Topology } from 'topojson-specification';
28
28
 
29
- import { type ThemedClassName, type ThemeMode, useDynamicRef, useThemeContext } from '@dxos/react-ui';
30
- import { mx } from '@dxos/react-ui-theme';
31
-
32
29
  import {
33
- GlobeContextProvider,
34
- type GlobeContextProviderProps,
35
- type GlobeContextType,
36
- useGlobeContext,
37
- } from '../../hooks';
30
+ type ThemeMode,
31
+ type ThemedClassName,
32
+ useComposedRefs,
33
+ useControlledState,
34
+ useDynamicRef,
35
+ useThemeContext,
36
+ } from '@dxos/react-ui';
37
+ import { composable, composableProps, mx } from '@dxos/ui-theme';
38
+
39
+ import { GlobeContext, type GlobeContextType, type Point, type Vector, useGlobeContext } from '../../hooks';
38
40
  import {
39
41
  type Features,
40
42
  type StyleSet,
@@ -44,7 +46,7 @@ import {
44
46
  renderLayers,
45
47
  timer,
46
48
  } from '../../util';
47
- import { ZoomControls, ActionControls, type ControlProps, controlPositions } from '../Toolbar';
49
+ import { ActionControls, type ControlProps, ZoomControls, controlPositions } from '../Toolbar';
48
50
 
49
51
  /**
50
52
  * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute
@@ -103,7 +105,7 @@ const defaultStyles: Record<ThemeMode, StyleSet> = {
103
105
  export type GlobeController = {
104
106
  canvas: HTMLCanvasElement;
105
107
  projection: GeoProjection;
106
- } & Pick<GlobeContextType, 'scale' | 'translation' | 'rotation' | 'setScale' | 'setTranslation' | 'setRotation'>;
108
+ } & Pick<GlobeContextType, 'zoom' | 'translation' | 'rotation' | 'setZoom' | 'setTranslation' | 'setRotation'>;
107
109
 
108
110
  export type ProjectionType = 'orthographic' | 'mercator' | 'transverse-mercator';
109
111
 
@@ -126,18 +128,52 @@ const getProjection = (type: GlobeCanvasProps['projection'] = 'orthographic'): G
126
128
  // Root
127
129
  //
128
130
 
129
- type GlobeRootProps = PropsWithChildren<ThemedClassName<GlobeContextProviderProps>>;
131
+ const DEFAULT_ZOOM = 1.5;
130
132
 
131
- const GlobeRoot = ({ classNames, children, ...props }: GlobeRootProps) => {
132
- const { ref, width, height } = useResizeDetector<HTMLDivElement>();
133
- return (
134
- <div ref={ref} className={mx('relative flex grow overflow-hidden', classNames)}>
135
- <GlobeContextProvider size={{ width, height }} {...props}>
136
- {children}
137
- </GlobeContextProvider>
138
- </div>
139
- );
140
- };
133
+ type GlobeRootProps = Partial<Pick<GlobeContextType, 'center' | 'zoom' | 'translation' | 'rotation'>>;
134
+
135
+ const GlobeRoot = composable<HTMLDivElement, GlobeRootProps>(
136
+ (
137
+ {
138
+ children,
139
+ center: centerProp,
140
+ zoom: zoomProp = DEFAULT_ZOOM,
141
+ translation: translationProp,
142
+ rotation: rotationProp,
143
+ ...props
144
+ },
145
+ forwardedRef,
146
+ ) => {
147
+ const localRef = useRef<HTMLDivElement>(null);
148
+ const composedRef = useComposedRefs<HTMLDivElement>(localRef, forwardedRef);
149
+ const { width, height } = useResizeDetector<HTMLDivElement>({ targetRef: localRef });
150
+
151
+ const [center, setCenter] = useControlledState(centerProp);
152
+ const [zoom, setZoom] = useControlledState(zoomProp);
153
+ const [translation, setTranslation] = useControlledState<Point>(translationProp);
154
+ const [rotation, setRotation] = useControlledState<Vector>(rotationProp);
155
+
156
+ return (
157
+ <GlobeContext.Provider
158
+ value={{
159
+ size: { width, height },
160
+ center,
161
+ zoom,
162
+ translation,
163
+ rotation,
164
+ setCenter,
165
+ setZoom,
166
+ setTranslation,
167
+ setRotation,
168
+ }}
169
+ >
170
+ <div {...composableProps(props, { classNames: 'relative dx-container' })} ref={composedRef}>
171
+ {children}
172
+ </div>
173
+ </GlobeContext.Provider>
174
+ );
175
+ },
176
+ );
141
177
 
142
178
  //
143
179
  // Canvas
@@ -154,17 +190,18 @@ type GlobeCanvasProps = {
154
190
  * Basic globe renderer.
155
191
  * https://github.com/topojson/world-atlas
156
192
  */
193
+ // TODO(burdon): Move controller to root.
157
194
  const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
158
- ({ projection: _projection, topology, features, styles: _styles }, forwardRef) => {
195
+ ({ projection: projectionProp, topology, features, styles: stylesProp }, forwardRef) => {
159
196
  const { themeMode } = useThemeContext();
160
- const styles = useMemo(() => _styles ?? defaultStyles[themeMode], [_styles, themeMode]);
197
+ const styles = useMemo(() => stylesProp ?? defaultStyles[themeMode], [stylesProp, themeMode]);
161
198
 
162
199
  // Canvas.
163
200
  const [canvas, setCanvas] = useState<HTMLCanvasElement>(null);
164
201
  const canvasRef = (canvas: HTMLCanvasElement) => setCanvas(canvas);
165
202
 
166
203
  // Projection.
167
- const projection = useMemo(() => getProjection(_projection), [_projection]);
204
+ const projection = useMemo(() => getProjection(projectionProp), [projectionProp]);
168
205
 
169
206
  // Layers.
170
207
  // TODO(burdon): Generate on the fly based on what is visible.
@@ -173,55 +210,50 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
173
210
  }, [topology, features, styles]);
174
211
 
175
212
  // State.
176
- const { size, center, scale, translation, rotation, setCenter, setScale, setTranslation, setRotation } =
213
+ const { size, center, zoom, translation, rotation, setCenter, setZoom, setTranslation, setRotation } =
177
214
  useGlobeContext();
215
+ const zoomRef = useDynamicRef(zoom);
178
216
 
179
- const scaleRef = useDynamicRef(scale);
180
-
181
- // Update rotation.
217
+ // Update rotation when the center changes. Preserve current zoom — callers can set zoom
218
+ // independently via the `zoom` prop or `setZoom` on the controller.
182
219
  useEffect(() => {
183
220
  if (center) {
184
- setScale(1);
185
221
  setRotation(positionToRotation(geoToPosition(center)));
186
222
  }
187
223
  }, [center]);
188
224
 
189
225
  // External controller.
190
226
  const zooming = useRef(false);
191
- useImperativeHandle<GlobeController, GlobeController>(
192
- forwardRef,
193
- () => {
194
- return {
195
- canvas,
196
- projection,
197
- center,
198
- get scale() {
199
- return scaleRef.current;
200
- },
201
- translation,
202
- rotation,
203
- setCenter,
204
- setScale: (s) => {
205
- if (typeof s === 'function') {
206
- const is = interpolateNumber(scaleRef.current, s(scaleRef.current));
207
- // Stop easing if already zooming.
208
- transition()
209
- .ease(zooming.current ? easeLinear : easeSinOut)
210
- .duration(200)
211
- .tween('scale', () => (t) => setScale(is(t)))
212
- .on('end', () => {
213
- zooming.current = false;
214
- });
215
- } else {
216
- setScale(s);
217
- }
218
- },
219
- setTranslation,
220
- setRotation,
221
- };
222
- },
223
- [canvas],
224
- );
227
+ useImperativeHandle<GlobeController, GlobeController>(forwardRef, () => {
228
+ return {
229
+ canvas,
230
+ projection,
231
+ center,
232
+ get zoom() {
233
+ return zoomRef.current;
234
+ },
235
+ translation,
236
+ rotation,
237
+ setCenter,
238
+ setZoom: (state) => {
239
+ if (typeof state === 'function') {
240
+ const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
241
+ // Stop easing if already zooming.
242
+ transition()
243
+ .ease(zooming.current ? easeLinear : easeSinOut)
244
+ .duration(200)
245
+ .tween('scale', () => (t) => setZoom(is(t)))
246
+ .on('end', () => {
247
+ zooming.current = false;
248
+ });
249
+ } else {
250
+ setZoom(state);
251
+ }
252
+ },
253
+ setTranslation,
254
+ setRotation,
255
+ };
256
+ }, [canvas]);
225
257
 
226
258
  // https://d3js.org/d3-geo/path#geoPath
227
259
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext
@@ -236,14 +268,14 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
236
268
  timer(() => {
237
269
  // https://d3js.org/d3-geo/projection
238
270
  projection
239
- .scale((Math.min(size.width, size.height) / 2) * scale)
271
+ .scale((Math.min(size.width, size.height) / 2) * zoom)
240
272
  .translate([size.width / 2 + (translation?.x ?? 0), size.height / 2 + (translation?.y ?? 0)])
241
273
  .rotate(rotation ?? [0, 0, 0]);
242
274
 
243
- renderLayers(generator, layers, scale, styles);
275
+ renderLayers(generator, layers, zoom, styles);
244
276
  });
245
277
  }
246
- }, [generator, size, scale, translation, rotation, layers]);
278
+ }, [generator, size, zoom, translation, rotation, layers]);
247
279
 
248
280
  if (!size.width || !size.height) {
249
281
  return null;
@@ -253,22 +285,30 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
253
285
  },
254
286
  );
255
287
 
288
+ //
289
+ // Debug
290
+ //
291
+
256
292
  const GlobeDebug = ({ position = 'topleft' }: { position?: ControlPosition }) => {
257
- const { size, scale, translation, rotation } = useGlobeContext();
293
+ const { size, zoom, translation, rotation } = useGlobeContext();
258
294
  return (
259
295
  <div
260
296
  className={mx(
261
- 'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded',
297
+ 'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm',
262
298
  controlPositions[position],
263
299
  )}
264
300
  >
265
301
  <pre className='font-mono text-xs text-green-700'>
266
- {JSON.stringify({ size, scale, translation, rotation }, null, 2)}
302
+ {JSON.stringify({ size, zoom, translation, rotation }, null, 2)}
267
303
  </pre>
268
304
  </div>
269
305
  );
270
306
  };
271
307
 
308
+ //
309
+ // Panel
310
+ //
311
+
272
312
  const GlobePanel = ({
273
313
  position,
274
314
  classNames,
@@ -277,25 +317,37 @@ const GlobePanel = ({
277
317
  return <div className={mx('z-10 absolute overflow-hidden', controlPositions[position], classNames)}>{children}</div>;
278
318
  };
279
319
 
320
+ //
321
+ // Controls
322
+ //
323
+
280
324
  const CustomControl = ({ position, children }: PropsWithChildren<{ position: ControlPosition }>) => {
281
325
  return <div className={mx('z-10 absolute overflow-hidden', controlPositions[position])}>{children}</div>;
282
326
  };
283
327
 
284
328
  type GlobeControlProps = { position?: ControlPosition } & Pick<ControlProps, 'onAction'>;
285
329
 
330
+ const GlobeZoom = ({ onAction, position = 'bottomleft', ...props }: GlobeControlProps) => (
331
+ <CustomControl position={position} {...props}>
332
+ <ZoomControls onAction={onAction} />
333
+ </CustomControl>
334
+ );
335
+
336
+ const GlobeAction = ({ onAction, position = 'bottomright', ...props }: GlobeControlProps) => (
337
+ <CustomControl position={position} {...props}>
338
+ <ActionControls onAction={onAction} />
339
+ </CustomControl>
340
+ );
341
+
342
+ //
343
+ // Globe
344
+ //
345
+
286
346
  export const Globe = {
287
347
  Root: GlobeRoot,
288
348
  Canvas: GlobeCanvas,
289
- Zoom: ({ onAction, position = 'bottomleft', ...props }: GlobeControlProps) => (
290
- <CustomControl position={position} {...props}>
291
- <ZoomControls onAction={onAction} />
292
- </CustomControl>
293
- ),
294
- Action: ({ onAction, position = 'bottomright', ...props }: GlobeControlProps) => (
295
- <CustomControl position={position} {...props}>
296
- <ActionControls onAction={onAction} />
297
- </CustomControl>
298
- ),
349
+ Zoom: GlobeZoom,
350
+ Action: GlobeAction,
299
351
  Debug: GlobeDebug,
300
352
  Panel: GlobePanel,
301
353
  };
@@ -2,45 +2,57 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
- import { type StoryObj, type Meta } from '@storybook/react-vite';
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 { withLayout, 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
+ import { Map, type MapController } from './Map';
15
13
 
16
- const DefaultStory = ({ markers = [] }: { markers?: MapMarker[] }) => {
14
+ const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
17
15
  const [controller, setController] = useState<MapController>();
18
16
  const handleZoomAction = useMapZoomHandler(controller);
19
17
 
20
18
  return (
21
19
  <Map.Root>
22
- <Map.Canvas ref={setController} markers={markers} />
23
- <Map.Zoom position='bottomleft' onAction={handleZoomAction} />
24
- <Map.Action position='bottomright' />
20
+ <Map.Content ref={setController}>
21
+ <Map.Tiles />
22
+ <Map.Markers markers={markers} />
23
+ <Map.Zoom position='bottomleft' onAction={handleZoomAction} />
24
+ <Map.Action position='bottomright' />
25
+ </Map.Content>
25
26
  </Map.Root>
26
27
  );
27
28
  };
28
29
 
29
- const meta: Meta<typeof DefaultStory> = {
30
+ const meta = {
30
31
  title: 'ui/react-ui-geo/Map',
32
+ component: Map.Root as any,
31
33
  render: DefaultStory,
32
- decorators: [withTheme, withLayout({ fullscreen: true })],
33
- };
34
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
35
+ parameters: {
36
+ layout: 'fullscreen',
37
+ },
38
+ } satisfies Meta<typeof DefaultStory>;
34
39
 
35
40
  export default meta;
36
41
 
37
- type Story = StoryObj<typeof DefaultStory>;
42
+ type Story = StoryObj<typeof meta>;
38
43
 
39
44
  export const Default: Story = {};
40
45
 
41
46
  export const WithMarkers: Story = {
42
47
  args: {
43
48
  markers: [
49
+ { id: 'los angeles', title: 'Los Angeles', location: { lat: 34.0522, lng: -118.2437 } },
50
+ { id: 'new york', title: 'New York', location: { lat: 40.7128, lng: -74.006 } },
51
+ { id: 'warsaw', title: 'Warsaw', location: { lat: 52.2297, lng: 21.0122 } },
52
+ { id: 'london', title: 'London', location: { lat: 51.5074, lng: -0.1278 } },
53
+ { id: 'toronto', title: 'Toronto', location: { lat: 43.6532, lng: -79.3832 } },
54
+ { id: 'seattle', title: 'Seattle', location: { lat: 47.6062, lng: -122.3321 } },
55
+ { id: 'barcelona', title: 'Barcelona', location: { lat: 41.3851, lng: 2.1734 } },
44
56
  { id: 'tokyo', title: 'Tokyo', location: { lat: 35.6762, lng: 139.6503 } },
45
57
  { id: 'sydney', title: 'Sydney', location: { lat: -33.8688, lng: 151.2093 } },
46
58
  { id: 'auckland', title: 'Auckland', location: { lat: -36.8509, lng: 174.7645 } },
@@ -56,6 +68,6 @@ export const WithMarkers: Story = {
56
68
  { id: 'phnom-penh', title: 'Phnom Penh', location: { lat: 11.5564, lng: 104.9282 } },
57
69
  { id: 'vientiane', title: 'Vientiane', location: { lat: 17.9757, lng: 102.6331 } },
58
70
  { id: 'yangon', title: 'Yangon', location: { lat: 16.8661, lng: 96.1951 } },
59
- ] as MapMarker[],
71
+ ] as GeoMarker[],
60
72
  },
61
73
  };