@dxos/react-ui-geo 0.8.4-main.c4373fc → 0.8.4-main.c85a9c8dae

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.
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-geo",
3
- "version": "0.8.4-main.c4373fc",
3
+ "version": "0.8.4-main.c85a9c8dae",
4
4
  "description": "Geo components.",
5
5
  "homepage": "https://github.com/dxos",
6
6
  "bugs": "https://github.com/dxos/issues",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
7
11
  "license": "MIT",
8
12
  "author": "DXOS.org",
9
13
  "sideEffects": true,
@@ -36,8 +40,7 @@
36
40
  "src"
37
41
  ],
38
42
  "dependencies": {
39
- "@preact-signals/safe-react": "^0.9.0",
40
- "@radix-ui/react-context": "^1.0.5",
43
+ "@radix-ui/react-context": "1.1.1",
41
44
  "d3": "^7.9.0",
42
45
  "d3-geo-projection": "^4.0.0",
43
46
  "d3-hexbin": "^0.2.2",
@@ -49,39 +52,39 @@
49
52
  "topojson-client": "^3.1.0",
50
53
  "topojson-simplify": "^3.0.3",
51
54
  "versor": "^0.2.0",
52
- "@dxos/async": "0.8.4-main.c4373fc",
53
- "@dxos/debug": "0.8.4-main.c4373fc",
54
- "@dxos/log": "0.8.4-main.c4373fc",
55
- "@dxos/node-std": "0.8.4-main.c4373fc",
56
- "@dxos/util": "0.8.4-main.c4373fc"
55
+ "@dxos/debug": "0.8.4-main.c85a9c8dae",
56
+ "@dxos/log": "0.8.4-main.c85a9c8dae",
57
+ "@dxos/async": "0.8.4-main.c85a9c8dae",
58
+ "@dxos/node-std": "0.8.4-main.c85a9c8dae",
59
+ "@dxos/util": "0.8.4-main.c85a9c8dae"
57
60
  },
58
61
  "devDependencies": {
59
- "@react-three/drei": "^9.99.0",
60
- "@react-three/fiber": "^9.3.0",
62
+ "@react-three/drei": "^10.7.7",
63
+ "@react-three/fiber": "^9.5.0",
61
64
  "@types/d3": "^7.4.3",
62
65
  "@types/geojson": "^7946.0.14",
63
66
  "@types/leaflet": "^1.9.16",
64
- "@types/react": "~19.2.2",
65
- "@types/react-dom": "~19.2.1",
67
+ "@types/react": "~19.2.7",
68
+ "@types/react-dom": "~19.2.3",
66
69
  "@types/three": "0.165.0",
67
70
  "@types/topojson-client": "^3.1.4",
68
71
  "@types/topojson-simplify": "^3.0.3",
69
72
  "@types/topojson-specification": "^1.0.5",
70
73
  "JSONStream": "^1.3.5",
71
74
  "geojson2h3": "^1.2.0",
72
- "leva": "^0.9.35",
73
- "react": "~19.2.0",
74
- "react-dom": "~19.2.0",
75
- "three": "0.165.0",
76
- "@dxos/react-ui": "0.8.4-main.c4373fc",
77
- "@dxos/react-ui-theme": "0.8.4-main.c4373fc",
78
- "@dxos/storybook-utils": "0.8.4-main.c4373fc"
75
+ "leva": "^0.10.1",
76
+ "react": "~19.2.3",
77
+ "react-dom": "~19.2.3",
78
+ "three": "^0.178.0",
79
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
80
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae",
81
+ "@dxos/storybook-utils": "0.8.4-main.c85a9c8dae"
79
82
  },
80
83
  "peerDependencies": {
81
- "react": "^19.0.0",
82
- "react-dom": "^19.0.0",
83
- "@dxos/react-ui": "0.8.4-main.c4373fc",
84
- "@dxos/react-ui-theme": "0.8.4-main.c4373fc"
84
+ "react": "~19.2.3",
85
+ "react-dom": "~19.2.3",
86
+ "@dxos/react-ui": "0.8.4-main.c85a9c8dae",
87
+ "@dxos/ui-theme": "0.8.4-main.c85a9c8dae"
85
88
  },
86
89
  "publishConfig": {
87
90
  "access": "public"
@@ -9,7 +9,7 @@ import React, { useMemo, useRef, useState } from 'react';
9
9
  import { type Topology } from 'topojson-specification';
10
10
 
11
11
  import { useAsyncState } from '@dxos/react-ui';
12
- import { withTheme } from '@dxos/react-ui/testing';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
13
13
 
14
14
  import { type Vector, useDrag, useGlobeZoomHandler, useSpinner, useTour } from '../../hooks';
15
15
  import { type LatLngLiteral } from '../../types';
@@ -227,7 +227,7 @@ const meta = {
227
227
  title: 'ui/react-ui-geo/Globe',
228
228
  component: Globe.Root,
229
229
  render: DefaultStory,
230
- decorators: [withTheme],
230
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
231
231
  parameters: {
232
232
  layout: 'fullscreen',
233
233
  },
@@ -27,7 +27,7 @@ import { useResizeDetector } from 'react-resize-detector';
27
27
  import { type Topology } from 'topojson-specification';
28
28
 
29
29
  import { type ThemeMode, type ThemedClassName, useDynamicRef, useThemeContext } from '@dxos/react-ui';
30
- import { mx } from '@dxos/react-ui-theme';
30
+ import { mx } from '@dxos/ui-theme';
31
31
 
32
32
  import {
33
33
  GlobeContextProvider,
@@ -130,6 +130,7 @@ type GlobeRootProps = PropsWithChildren<ThemedClassName<GlobeContextProviderProp
130
130
 
131
131
  const GlobeRoot = ({ classNames, children, ...props }: GlobeRootProps) => {
132
132
  const { ref, width, height } = useResizeDetector<HTMLDivElement>();
133
+
133
134
  return (
134
135
  <div ref={ref} className={mx('relative flex grow overflow-hidden', classNames)}>
135
136
  <GlobeContextProvider size={{ width, height }} {...props}>
@@ -156,16 +157,16 @@ type GlobeCanvasProps = {
156
157
  */
157
158
  // TODO(burdon): Move controller to root.
158
159
  const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
159
- ({ projection: _projection, topology, features, styles: _styles }, forwardRef) => {
160
+ ({ projection: projectionProp, topology, features, styles: stylesProp }, forwardRef) => {
160
161
  const { themeMode } = useThemeContext();
161
- const styles = useMemo(() => _styles ?? defaultStyles[themeMode], [_styles, themeMode]);
162
+ const styles = useMemo(() => stylesProp ?? defaultStyles[themeMode], [stylesProp, themeMode]);
162
163
 
163
164
  // Canvas.
164
165
  const [canvas, setCanvas] = useState<HTMLCanvasElement>(null);
165
166
  const canvasRef = (canvas: HTMLCanvasElement) => setCanvas(canvas);
166
167
 
167
168
  // Projection.
168
- const projection = useMemo(() => getProjection(_projection), [_projection]);
169
+ const projection = useMemo(() => getProjection(projectionProp), [projectionProp]);
169
170
 
170
171
  // Layers.
171
172
  // TODO(burdon): Generate on the fly based on what is visible.
@@ -199,9 +200,9 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
199
200
  translation,
200
201
  rotation,
201
202
  setCenter,
202
- setZoom: (s) => {
203
- if (typeof s === 'function') {
204
- const is = interpolateNumber(zoomRef.current, s(zoomRef.current));
203
+ setZoom: (state) => {
204
+ if (typeof state === 'function') {
205
+ const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
205
206
  // Stop easing if already zooming.
206
207
  transition()
207
208
  .ease(zooming.current ? easeLinear : easeSinOut)
@@ -211,7 +212,7 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
211
212
  zooming.current = false;
212
213
  });
213
214
  } else {
214
- setZoom(s);
215
+ setZoom(state);
215
216
  }
216
217
  },
217
218
  setTranslation,
@@ -258,7 +259,7 @@ const GlobeDebug = ({ position = 'topleft' }: { position?: ControlPosition }) =>
258
259
  return (
259
260
  <div
260
261
  className={mx(
261
- 'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded',
262
+ 'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm',
262
263
  controlPositions[position],
263
264
  )}
264
265
  >
@@ -5,7 +5,7 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React, { useState } from 'react';
7
7
 
8
- import { withTheme } from '@dxos/react-ui/testing';
8
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
9
9
 
10
10
  import { useMapZoomHandler } from '../../hooks';
11
11
  import { type GeoMarker } from '../../types';
@@ -30,7 +30,7 @@ const meta = {
30
30
  title: 'ui/react-ui-geo/Map',
31
31
  component: Map.Root as any,
32
32
  render: DefaultStory,
33
- decorators: [withTheme],
33
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
34
34
  parameters: {
35
35
  layout: 'fullscreen',
36
36
  },
@@ -8,12 +8,10 @@ import { createContext } from '@radix-ui/react-context';
8
8
  import L, { Control, type ControlPosition, DomEvent, DomUtil, type LatLngLiteral, latLngBounds } from 'leaflet';
9
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';
11
+ import { MapContainer, type MapContainerProps, Marker, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet';
13
12
 
14
- import { debounce } from '@dxos/async';
15
13
  import { ThemeProvider, type ThemedClassName, Tooltip } from '@dxos/react-ui';
16
- import { defaultTx, mx } from '@dxos/react-ui-theme';
14
+ import { defaultTx, mx } from '@dxos/ui-theme';
17
15
 
18
16
  import { type GeoMarker } from '../../types';
19
17
  import { ActionControls, type ControlProps, ZoomControls, controlPositions } from '../Toolbar';
@@ -25,7 +23,7 @@ import { ActionControls, type ControlProps, ZoomControls, controlPositions } fro
25
23
  const defaults = {
26
24
  center: { lat: 51, lng: 0 } as L.LatLngLiteral,
27
25
  zoom: 4,
28
- };
26
+ } as const;
29
27
 
30
28
  //
31
29
  // Controller
@@ -42,6 +40,7 @@ type MapController = {
42
40
 
43
41
  type MapContextValue = {
44
42
  attention?: boolean;
43
+ onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
45
44
  };
46
45
 
47
46
  const [MapContextProvier, useMapContext] = createContext<MapContextValue>('Map');
@@ -50,27 +49,14 @@ const [MapContextProvier, useMapContext] = createContext<MapContextValue>('Map')
50
49
  // Root
51
50
  //
52
51
 
53
- type MapRootProps = ThemedClassName<
54
- MapContainerProps & {
55
- onChange?: (ev: { center: LatLngLiteral; zoom: number }) => void;
56
- }
57
- >;
52
+ type MapRootProps = ThemedClassName<MapContainerProps & Pick<MapContextValue, 'onChange'>>;
58
53
 
59
54
  /**
60
55
  * https://react-leaflet.js.org/docs/api-map
61
56
  */
62
57
  const MapRoot = forwardRef<MapController, MapRootProps>(
63
58
  (
64
- {
65
- classNames,
66
- scrollWheelZoom = true,
67
- doubleClickZoom = true,
68
- touchZoom = true,
69
- center = defaults.center,
70
- zoom = defaults.zoom,
71
- onChange,
72
- ...props
73
- },
59
+ { classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, onChange, ...props },
74
60
  forwardedRef,
75
61
  ) => {
76
62
  const [attention, setAttention] = useState(false);
@@ -90,32 +76,6 @@ const MapRoot = forwardRef<MapController, MapRootProps>(
90
76
  [],
91
77
  );
92
78
 
93
- // Events.
94
- useEffect(() => {
95
- if (!map) {
96
- return;
97
- }
98
-
99
- const handler = debounce(() => {
100
- setAttention(true);
101
- onChange?.({
102
- center: map.getCenter(),
103
- zoom: map.getZoom(),
104
- });
105
- }, 100);
106
-
107
- map.on('move', handler);
108
- map.on('zoom', handler);
109
- map.on('focus', () => setAttention(true));
110
- map.on('blur', () => setAttention(false));
111
- return () => {
112
- map.off('move');
113
- map.off('zoom');
114
- map.off('focus');
115
- map.off('blur');
116
- };
117
- }, [map, onChange]);
118
-
119
79
  // Enable/disable scroll wheel zoom.
120
80
  // TODO(burdon): Use attention:
121
81
  // const {hasAttention} = useAttention(props.id);
@@ -132,18 +92,18 @@ const MapRoot = forwardRef<MapController, MapRootProps>(
132
92
  }, [map, attention]);
133
93
 
134
94
  return (
135
- <MapContextProvier attention={attention}>
95
+ <MapContextProvier attention={attention} onChange={onChange}>
136
96
  <MapContainer
137
97
  {...props}
138
98
  ref={mapRef}
139
- className={mx('group relative grid bs-full is-full !bg-baseSurface dx-focus-ring-inset', classNames)}
99
+ className={mx('group relative grid h-full w-full !bg-base-surface dx-focus-ring-inset', classNames)}
140
100
  attributionControl={false}
141
101
  zoomControl={false}
142
102
  scrollWheelZoom={scrollWheelZoom}
143
103
  doubleClickZoom={doubleClickZoom}
144
104
  touchZoom={touchZoom}
145
- center={center}
146
- zoom={zoom}
105
+ center={center ?? defaults.center}
106
+ zoom={zoom ?? defaults.zoom}
147
107
  // whenReady={() => {}}
148
108
  />
149
109
  </MapContextProvier>
@@ -158,14 +118,26 @@ MapRoot.displayName = 'Map.Root';
158
118
  // https://react-leaflet.js.org/docs/api-components/#tilelayer
159
119
  //
160
120
 
121
+ const MAP_TILES_NAME = 'Map.Tiles';
122
+
161
123
  type MapTilesProps = {};
162
124
 
163
125
  const MapTiles = (_props: MapTilesProps) => {
164
126
  const ref = useRef<L.TileLayer>(null);
127
+ const { onChange } = useMapContext(MAP_TILES_NAME);
128
+
129
+ useMapEvents({
130
+ zoomstart: (ev) => {
131
+ onChange?.({
132
+ center: ev.target.getCenter(),
133
+ zoom: ev.target.getZoom(),
134
+ });
135
+ },
136
+ });
165
137
 
166
138
  // NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
167
139
  // Tailwind requires setting the property for static analysis.
168
- const { attention } = useMapContext(MapTiles.displayName);
140
+ const { attention } = useMapContext(MAP_TILES_NAME);
169
141
  useEffect(() => {
170
142
  if (ref.current) {
171
143
  ref.current.getContainer().dataset.attention = attention ? '1' : '0';
@@ -206,7 +178,7 @@ const MapTiles = (_props: MapTilesProps) => {
206
178
  );
207
179
  };
208
180
 
209
- MapTiles.displayName = 'Map.Tiles';
181
+ MapTiles.displayName = MAP_TILES_NAME;
210
182
 
211
183
  //
212
184
  // Markers
@@ -277,7 +249,7 @@ const CustomControl = ({
277
249
  useEffect(() => {
278
250
  const control = new Control({ position });
279
251
  control.onAdd = () => {
280
- const container = DomUtil.create('div', mx('!m-0', controlPositions[position]));
252
+ const container = DomUtil.create('div', mx('m-0!', controlPositions[position]));
281
253
  DomEvent.disableClickPropagation(container);
282
254
  DomEvent.disableScrollPropagation(container);
283
255
 
@@ -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,22 +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
29
  <Toolbar.Root classNames={['gap-2', classNames]}>
26
30
  <IconButton
27
31
  icon='ph--plus--regular'
28
- label='zoom in'
29
32
  iconOnly
30
- size={5}
31
- classNames='px-0 aspect-square'
33
+ label={t('zoom in icon button')}
32
34
  onClick={() => onAction?.('zoom-in')}
33
35
  />
34
36
  <IconButton
35
37
  icon='ph--minus--regular'
36
- label='zoom out'
37
38
  iconOnly
38
- size={5}
39
- classNames='px-0 aspect-square'
39
+ label={t('zoom out icon button')}
40
40
  onClick={() => onAction?.('zoom-out')}
41
41
  />
42
42
  </Toolbar.Root>
@@ -44,22 +44,20 @@ export const ZoomControls = ({ classNames, onAction }: ControlProps) => {
44
44
  };
45
45
 
46
46
  export const ActionControls = ({ classNames, onAction }: ControlProps) => {
47
+ const { t } = useTranslation(translationKey);
48
+
47
49
  return (
48
50
  <Toolbar.Root classNames={['gap-2', classNames]}>
49
51
  <IconButton
50
- icon='ph--play--regular'
51
- label='start'
52
+ icon='ph--path--regular'
52
53
  iconOnly
53
- size={5}
54
- classNames='px-0 aspect-square'
54
+ label={t('start icon button')}
55
55
  onClick={() => onAction?.('start')}
56
56
  />
57
57
  <IconButton
58
58
  icon='ph--globe-hemisphere-west--regular'
59
- label='toggle'
60
59
  iconOnly
61
- size={5}
62
- classNames='px-0 aspect-square'
60
+ label={t('toggle icon button')}
63
61
  onClick={() => onAction?.('toggle')}
64
62
  />
65
63
  </Toolbar.Root>
@@ -26,6 +26,12 @@ export type GlobeContextType = {
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<
@@ -35,15 +41,15 @@ export type GlobeContextProviderProps = PropsWithChildren<
35
41
  export const GlobeContextProvider = ({
36
42
  children,
37
43
  size,
38
- center: _center,
39
- zoom: _zoom,
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 [zoom, setZoom] = useControlledState(_zoom);
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
@@ -6,6 +6,8 @@ import { useCallback } from 'react';
6
6
 
7
7
  import { type ControlProps, type GlobeController } from '../components';
8
8
 
9
+ const ZOOM_FACTOR = 0.1;
10
+
9
11
  export const useGlobeZoomHandler = (controller: GlobeController | null | undefined): ControlProps['onAction'] => {
10
12
  return useCallback<ControlProps['onAction']>(
11
13
  (event) => {
@@ -15,11 +17,15 @@ export const useGlobeZoomHandler = (controller: GlobeController | null | undefin
15
17
 
16
18
  switch (event) {
17
19
  case 'zoom-in': {
18
- controller.setZoom((zoom) => zoom * 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.setZoom((zoom) => zoom * 0.9);
26
+ controller.setZoom((zoom) => {
27
+ return zoom * (1 - ZOOM_FACTOR);
28
+ });
23
29
  break;
24
30
  }
25
31
  }
@@ -34,6 +34,7 @@ export const useTour = (
34
34
  options: TourOptions = {},
35
35
  ): [boolean, Dispatch<SetStateAction<boolean>>] => {
36
36
  const selection = useMemo(() => d3Selection(), []);
37
+ // TODO(burdon): Redo controlled state.
37
38
  const [running, setRunning] = useState(options.running ?? false);
38
39
  useEffect(() => {
39
40
  if (!running) {
package/src/index.ts CHANGED
@@ -5,5 +5,6 @@
5
5
  export * from './components';
6
6
  export * from './data';
7
7
  export * from './hooks';
8
+ export * from './translations';
8
9
  export type * from './types';
9
10
  export * from './util';
@@ -0,0 +1,20 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { type Resource } from '@dxos/react-ui';
6
+
7
+ export const translationKey = '@dxos/react-ui-geo';
8
+
9
+ export const translations = [
10
+ {
11
+ 'en-US': {
12
+ [translationKey]: {
13
+ 'zoom in icon button': 'Zoom in',
14
+ 'zoom out icon button': 'Zoom out',
15
+ 'start icon button': 'Start',
16
+ 'toggle icon button': 'Toggle',
17
+ },
18
+ },
19
+ },
20
+ ] as const satisfies Resource[];