@dxos/react-ui-geo 0.8.4-main.fd6878d → 0.8.4-staging.ac66bdf99f

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 (39) hide show
  1. package/dist/lib/browser/index.mjs +380 -455
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node-esm/index.mjs +380 -455
  5. package/dist/lib/node-esm/index.mjs.map +4 -4
  6. package/dist/lib/node-esm/meta.json +1 -1
  7. package/dist/types/src/components/Globe/Globe.d.ts +5 -3
  8. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  9. package/dist/types/src/components/Globe/Globe.stories.d.ts +27 -9
  10. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  11. package/dist/types/src/components/Map/Map.d.ts +14 -11
  12. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  13. package/dist/types/src/components/Map/Map.stories.d.ts +13 -7
  14. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  15. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  16. package/dist/types/src/hooks/context.d.ts +1 -3
  17. package/dist/types/src/hooks/context.d.ts.map +1 -1
  18. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  19. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  20. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  21. package/dist/types/src/index.d.ts +1 -0
  22. package/dist/types/src/index.d.ts.map +1 -1
  23. package/dist/types/src/translations.d.ts +12 -0
  24. package/dist/types/src/translations.d.ts.map +1 -0
  25. package/dist/types/src/util/render.d.ts.map +1 -1
  26. package/dist/types/tsconfig.tsbuildinfo +1 -1
  27. package/package.json +28 -24
  28. package/src/components/Globe/Globe.stories.tsx +68 -26
  29. package/src/components/Globe/Globe.tsx +55 -28
  30. package/src/components/Map/Map.stories.tsx +16 -13
  31. package/src/components/Map/Map.tsx +82 -71
  32. package/src/components/Toolbar/Controls.tsx +12 -14
  33. package/src/hooks/context.tsx +6 -29
  34. package/src/hooks/useGlobeZoomHandler.ts +8 -2
  35. package/src/hooks/useSpinner.ts +0 -1
  36. package/src/hooks/useTour.ts +1 -0
  37. package/src/index.ts +1 -0
  38. package/src/translations.ts +20 -0
  39. package/src/util/render.ts +0 -1
@@ -6,14 +6,12 @@ import 'leaflet/dist/leaflet.css';
6
6
 
7
7
  import { createContext } from '@radix-ui/react-context';
8
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';
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';
11
+ import { MapContainer, type MapContainerProps, Marker, Popup, TileLayer, useMap, useMapEvents } from 'react-leaflet';
13
12
 
14
- import { debounce } from '@dxos/async';
15
- import { ThemeProvider, type ThemedClassName, Tooltip } from '@dxos/react-ui';
16
- 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';
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,38 +40,57 @@ 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
- const [MapContextProvier, useMapContext] = createContext<MapContextValue>('Map');
46
+ const [MapContextProvider, useMapContext] = createContext<MapContextValue>('Map');
48
47
 
49
48
  //
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 = Pick<MapContextValue, 'onChange'>;
53
+
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;
60
+ return (
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>
72
+ );
73
+ });
74
+
75
+ MapRoot.displayName = 'Map.Root';
76
+
77
+ //
78
+ // Content
79
+ //
80
+
81
+ type MapContentProps = ThemedClassName<Omit<MapContainerProps, 'children'> & PropsWithChildren>;
58
82
 
59
83
  /**
60
84
  * https://react-leaflet.js.org/docs/api-map
61
85
  */
62
- const MapRoot = forwardRef<MapController, MapRootProps>(
86
+ const MAP_CONTENT_NAME = 'Map.Content';
87
+
88
+ const MapContent = forwardRef<MapController, MapContentProps>(
63
89
  (
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
- },
90
+ { classNames, scrollWheelZoom = true, doubleClickZoom = true, touchZoom = true, center, zoom, children, ...props },
74
91
  forwardedRef,
75
92
  ) => {
76
- const [attention, setAttention] = useState(false);
93
+ const { attention } = useMapContext(MAP_CONTENT_NAME);
77
94
  const mapRef = useRef<L.Map>(null);
78
95
  const map = mapRef.current;
79
96
 
@@ -90,32 +107,6 @@ const MapRoot = forwardRef<MapController, MapRootProps>(
90
107
  [],
91
108
  );
92
109
 
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
110
  // Enable/disable scroll wheel zoom.
120
111
  // TODO(burdon): Use attention:
121
112
  // const {hasAttention} = useAttention(props.id);
@@ -132,40 +123,52 @@ const MapRoot = forwardRef<MapController, MapRootProps>(
132
123
  }, [map, attention]);
133
124
 
134
125
  return (
135
- <MapContextProvier attention={attention}>
136
- <MapContainer
137
- {...props}
138
- ref={mapRef}
139
- className={mx('group relative grid bs-full is-full !bg-baseSurface dx-focus-ring-inset', classNames)}
140
- attributionControl={false}
141
- zoomControl={false}
142
- scrollWheelZoom={scrollWheelZoom}
143
- doubleClickZoom={doubleClickZoom}
144
- touchZoom={touchZoom}
145
- center={center}
146
- zoom={zoom}
147
- // whenReady={() => {}}
148
- />
149
- </MapContextProvier>
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>
150
141
  );
151
142
  },
152
143
  );
153
144
 
154
- MapRoot.displayName = 'Map.Root';
145
+ MapContent.displayName = 'Map.Content';
155
146
 
156
147
  //
157
148
  // Tiles
158
149
  // https://react-leaflet.js.org/docs/api-components/#tilelayer
159
150
  //
160
151
 
152
+ const MAP_TILES_NAME = 'Map.Tiles';
153
+
161
154
  type MapTilesProps = {};
162
155
 
163
156
  const MapTiles = (_props: MapTilesProps) => {
164
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
+ });
165
168
 
166
169
  // NOTE: Need to dynamically update data attribute since TileLayer doesn't update, but
167
170
  // Tailwind requires setting the property for static analysis.
168
- const { attention } = useMapContext(MapTiles.displayName);
171
+ const { attention } = useMapContext(MAP_TILES_NAME);
169
172
  useEffect(() => {
170
173
  if (ref.current) {
171
174
  ref.current.getContainer().dataset.attention = attention ? '1' : '0';
@@ -206,7 +209,7 @@ const MapTiles = (_props: MapTilesProps) => {
206
209
  );
207
210
  };
208
211
 
209
- MapTiles.displayName = 'Map.Tiles';
212
+ MapTiles.displayName = MAP_TILES_NAME;
210
213
 
211
214
  //
212
215
  // Markers
@@ -277,7 +280,7 @@ const CustomControl = ({
277
280
  useEffect(() => {
278
281
  const control = new Control({ position });
279
282
  control.onAdd = () => {
280
- const container = DomUtil.create('div', mx('!m-0', controlPositions[position]));
283
+ const container = DomUtil.create('div', mx('m-0!', controlPositions[position]));
281
284
  DomEvent.disableClickPropagation(container);
282
285
  DomEvent.disableScrollPropagation(container);
283
286
 
@@ -320,10 +323,18 @@ const MapAction = ({ onAction, position = 'bottomright', ...props }: MapControlP
320
323
 
321
324
  export const Map = {
322
325
  Root: MapRoot,
326
+ Content: MapContent,
323
327
  Tiles: MapTiles,
324
328
  Markers: MapMarkers,
325
329
  Zoom: MapZoom,
326
330
  Action: MapAction,
327
331
  };
328
332
 
329
- export { type MapController, type MapRootProps, type MapTilesProps, type MapMarkersProps, type MapControlProps };
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,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>
@@ -2,16 +2,17 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import React, { type Dispatch, type PropsWithChildren, type SetStateAction, createContext, 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
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 = {
@@ -26,33 +27,9 @@ export type GlobeContextType = {
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' | 'zoom' | 'translation' | 'rotation'>>
33
- >;
34
-
35
- export const GlobeContextProvider = ({
36
- children,
37
- size,
38
- center: _center,
39
- zoom: _zoom,
40
- translation: _translation,
41
- rotation: _rotation,
42
- }: 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);
47
-
48
- return (
49
- <GlobeContext.Provider
50
- value={{ size, center, zoom, translation, rotation, setCenter, setZoom, 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'));
@@ -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
  }
@@ -7,7 +7,6 @@ import { type Timer } from 'd3';
7
7
  import { useEffect, useState } from 'react';
8
8
 
9
9
  import { type GlobeController } from '../components';
10
-
11
10
  import { type Vector } from './context';
12
11
 
13
12
  export type SpinnerOptions = {
@@ -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[];
@@ -7,7 +7,6 @@ import { feature, mesh } from 'topojson-client';
7
7
  import { type Topology } from 'topojson-specification';
8
8
 
9
9
  import { type LatLngLiteral } from '../types';
10
-
11
10
  import { geoLine, geoPoint } from './path';
12
11
 
13
12
  export type Styles = Record<string, any>;