@dxos/react-ui-geo 0.8.4-main.a4bbb77 → 0.8.4-main.abd8ff62ef
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/dist/lib/browser/{countries-110m-ZM3ZIEFS.mjs → countries-110m-RE5RNRQG.mjs} +1 -1
- package/dist/lib/browser/data.mjs +4 -3
- package/dist/lib/browser/data.mjs.map +4 -4
- package/dist/lib/browser/index.mjs +364 -462
- package/dist/lib/browser/index.mjs.map +3 -3
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/translations.mjs +19 -0
- package/dist/lib/browser/translations.mjs.map +7 -0
- package/dist/lib/node-esm/{countries-110m-3SFASWVD.mjs → countries-110m-4EDBXSFJ.mjs} +1 -1
- package/dist/lib/node-esm/data.mjs +5 -3
- package/dist/lib/node-esm/data.mjs.map +4 -4
- package/dist/lib/node-esm/index.mjs +364 -461
- package/dist/lib/node-esm/index.mjs.map +3 -3
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/translations.mjs +21 -0
- package/dist/lib/node-esm/translations.mjs.map +7 -0
- package/dist/types/data/airports.d.ts +4 -4
- package/dist/types/data/airports.d.ts.map +1 -1
- package/dist/types/data/cities.d.ts.map +1 -1
- package/dist/types/data/countries-110m.d.ts.map +1 -1
- package/dist/types/data/countries-dots-3.d.ts.map +1 -1
- package/dist/types/data/countries-dots-4.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.d.ts +5 -3
- package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
- package/dist/types/src/components/Globe/Globe.stories.d.ts +6 -4
- package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.d.ts +22 -11
- package/dist/types/src/components/Map/Map.d.ts.map +1 -1
- package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
- package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
- package/dist/types/src/hooks/context.d.ts +1 -3
- package/dist/types/src/hooks/context.d.ts.map +1 -1
- package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
- package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
- package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
- package/dist/types/src/hooks/useTour.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +0 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +12 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/util/debug.d.ts.map +1 -1
- package/dist/types/src/util/inertia.d.ts.map +1 -1
- package/dist/types/src/util/path.d.ts.map +1 -1
- package/dist/types/src/util/render.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +41 -35
- package/src/components/Globe/Globe.stories.tsx +7 -8
- package/src/components/Globe/Globe.tsx +55 -28
- package/src/components/Map/Map.stories.tsx +9 -8
- package/src/components/Map/Map.tsx +82 -71
- package/src/components/Toolbar/Controls.tsx +12 -14
- package/src/hooks/context.tsx +6 -29
- package/src/hooks/useGlobeZoomHandler.ts +8 -2
- package/src/hooks/useSpinner.ts +0 -1
- package/src/hooks/useTour.ts +1 -0
- package/src/index.ts +0 -1
- package/src/translations.ts +20 -0
- package/src/util/render.ts +0 -1
- package/dist/lib/browser/chunk-GMWLKTLN.mjs +0 -9
- package/dist/lib/browser/chunk-GMWLKTLN.mjs.map +0 -7
- package/dist/lib/node-esm/chunk-JODBF4CC.mjs +0 -11
- package/dist/lib/node-esm/chunk-JODBF4CC.mjs.map +0 -7
- /package/dist/lib/browser/{countries-110m-ZM3ZIEFS.mjs.map → countries-110m-RE5RNRQG.mjs.map} +0 -0
- /package/dist/lib/node-esm/{countries-110m-3SFASWVD.mjs.map → countries-110m-4EDBXSFJ.mjs.map} +0 -0
package/package.json
CHANGED
|
@@ -1,43 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/react-ui-geo",
|
|
3
|
-
"version": "0.8.4-main.
|
|
3
|
+
"version": "0.8.4-main.abd8ff62ef",
|
|
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,
|
|
10
14
|
"type": "module",
|
|
15
|
+
"imports": {
|
|
16
|
+
"#translations": "./src/translations.ts"
|
|
17
|
+
},
|
|
11
18
|
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/types/src/index.d.ts",
|
|
22
|
+
"browser": "./dist/lib/browser/index.mjs",
|
|
23
|
+
"node": "./dist/lib/node-esm/index.mjs"
|
|
24
|
+
},
|
|
12
25
|
"./data": {
|
|
13
26
|
"source": "./src/data.ts",
|
|
14
27
|
"types": "./dist/types/src/data.d.ts",
|
|
15
28
|
"browser": "./dist/lib/browser/data.mjs",
|
|
16
29
|
"node": "./dist/lib/node-esm/data.mjs"
|
|
17
30
|
},
|
|
18
|
-
"
|
|
19
|
-
"source": "./src/
|
|
20
|
-
"types": "./dist/types/src/
|
|
21
|
-
"browser": "./dist/lib/browser/
|
|
22
|
-
"node": "./dist/lib/node-esm/
|
|
31
|
+
"./translations": {
|
|
32
|
+
"source": "./src/translations.ts",
|
|
33
|
+
"types": "./dist/types/src/translations.d.ts",
|
|
34
|
+
"browser": "./dist/lib/browser/translations.mjs",
|
|
35
|
+
"node": "./dist/lib/node-esm/translations.mjs"
|
|
23
36
|
}
|
|
24
37
|
},
|
|
25
38
|
"types": "dist/types/src/index.d.ts",
|
|
26
|
-
"typesVersions": {
|
|
27
|
-
"*": {
|
|
28
|
-
"data": [
|
|
29
|
-
"dist/types/src/data.d.ts"
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
39
|
"files": [
|
|
34
40
|
"data",
|
|
35
41
|
"dist",
|
|
36
42
|
"src"
|
|
37
43
|
],
|
|
38
44
|
"dependencies": {
|
|
39
|
-
"@
|
|
40
|
-
"@radix-ui/react-context": "
|
|
45
|
+
"@radix-ui/react-compose-refs": "1.1.1",
|
|
46
|
+
"@radix-ui/react-context": "1.1.1",
|
|
41
47
|
"d3": "^7.9.0",
|
|
42
48
|
"d3-geo-projection": "^4.0.0",
|
|
43
49
|
"d3-hexbin": "^0.2.2",
|
|
@@ -49,39 +55,39 @@
|
|
|
49
55
|
"topojson-client": "^3.1.0",
|
|
50
56
|
"topojson-simplify": "^3.0.3",
|
|
51
57
|
"versor": "^0.2.0",
|
|
52
|
-
"@dxos/async": "0.8.4-main.
|
|
53
|
-
"@dxos/
|
|
54
|
-
"@dxos/
|
|
55
|
-
"@dxos/node-std": "0.8.4-main.
|
|
56
|
-
"@dxos/util": "0.8.4-main.
|
|
58
|
+
"@dxos/async": "0.8.4-main.abd8ff62ef",
|
|
59
|
+
"@dxos/debug": "0.8.4-main.abd8ff62ef",
|
|
60
|
+
"@dxos/log": "0.8.4-main.abd8ff62ef",
|
|
61
|
+
"@dxos/node-std": "0.8.4-main.abd8ff62ef",
|
|
62
|
+
"@dxos/util": "0.8.4-main.abd8ff62ef"
|
|
57
63
|
},
|
|
58
64
|
"devDependencies": {
|
|
59
|
-
"@react-three/drei": "^
|
|
60
|
-
"@react-three/fiber": "^9.
|
|
65
|
+
"@react-three/drei": "^10.7.7",
|
|
66
|
+
"@react-three/fiber": "^9.5.0",
|
|
61
67
|
"@types/d3": "^7.4.3",
|
|
62
68
|
"@types/geojson": "^7946.0.14",
|
|
63
69
|
"@types/leaflet": "^1.9.16",
|
|
64
|
-
"@types/react": "~19.2.
|
|
65
|
-
"@types/react-dom": "~19.2.
|
|
70
|
+
"@types/react": "~19.2.7",
|
|
71
|
+
"@types/react-dom": "~19.2.3",
|
|
66
72
|
"@types/three": "0.165.0",
|
|
67
73
|
"@types/topojson-client": "^3.1.4",
|
|
68
74
|
"@types/topojson-simplify": "^3.0.3",
|
|
69
75
|
"@types/topojson-specification": "^1.0.5",
|
|
70
76
|
"JSONStream": "^1.3.5",
|
|
71
77
|
"geojson2h3": "^1.2.0",
|
|
72
|
-
"leva": "^0.
|
|
73
|
-
"react": "~19.2.
|
|
74
|
-
"react-dom": "~19.2.
|
|
75
|
-
"three": "0.
|
|
76
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
77
|
-
"@dxos/
|
|
78
|
-
"@dxos/
|
|
78
|
+
"leva": "^0.10.1",
|
|
79
|
+
"react": "~19.2.3",
|
|
80
|
+
"react-dom": "~19.2.3",
|
|
81
|
+
"three": "^0.178.0",
|
|
82
|
+
"@dxos/react-ui": "0.8.4-main.abd8ff62ef",
|
|
83
|
+
"@dxos/storybook-utils": "0.8.4-main.abd8ff62ef",
|
|
84
|
+
"@dxos/ui-theme": "0.8.4-main.abd8ff62ef"
|
|
79
85
|
},
|
|
80
86
|
"peerDependencies": {
|
|
81
|
-
"react": "
|
|
82
|
-
"react-dom": "
|
|
83
|
-
"@dxos/react-ui": "0.8.4-main.
|
|
84
|
-
"@dxos/
|
|
87
|
+
"react": "~19.2.3",
|
|
88
|
+
"react-dom": "~19.2.3",
|
|
89
|
+
"@dxos/react-ui": "0.8.4-main.abd8ff62ef",
|
|
90
|
+
"@dxos/ui-theme": "0.8.4-main.abd8ff62ef"
|
|
85
91
|
},
|
|
86
92
|
"publishConfig": {
|
|
87
93
|
"access": "public"
|
|
@@ -9,13 +9,12 @@ 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';
|
|
16
16
|
import { type StyleSet, closestPoint } from '../../util';
|
|
17
17
|
import { type ControlProps } from '../Toolbar';
|
|
18
|
-
|
|
19
18
|
import { Globe, type GlobeCanvasProps, type GlobeController, type GlobeRootProps } from './Globe';
|
|
20
19
|
|
|
21
20
|
// TODO(burdon): Load from JSON at runtime?
|
|
@@ -128,7 +127,7 @@ const createTrip = (
|
|
|
128
127
|
);
|
|
129
128
|
};
|
|
130
129
|
|
|
131
|
-
type
|
|
130
|
+
type DefaultStoryProps = Pick<GlobeRootProps, 'zoom' | 'translation' | 'rotation'> &
|
|
132
131
|
Pick<GlobeCanvasProps, 'projection' | 'styles'> & {
|
|
133
132
|
drag?: boolean;
|
|
134
133
|
spin?: boolean;
|
|
@@ -137,7 +136,7 @@ type StoryProps = Pick<GlobeRootProps, 'zoom' | 'translation' | 'rotation'> &
|
|
|
137
136
|
};
|
|
138
137
|
|
|
139
138
|
const DefaultStory = ({
|
|
140
|
-
zoom:
|
|
139
|
+
zoom: zoomProp = 1,
|
|
141
140
|
translation,
|
|
142
141
|
rotation = [0, 0, 0],
|
|
143
142
|
projection,
|
|
@@ -146,7 +145,7 @@ const DefaultStory = ({
|
|
|
146
145
|
spin = false,
|
|
147
146
|
tour = false,
|
|
148
147
|
xAxis = false,
|
|
149
|
-
}:
|
|
148
|
+
}: DefaultStoryProps) => {
|
|
150
149
|
const controller = useRef<GlobeController>(null);
|
|
151
150
|
const [dots] = useAsyncState(async () => {
|
|
152
151
|
const points = (await import('../../../data/countries-dots-3.ts')).default;
|
|
@@ -203,13 +202,13 @@ const DefaultStory = ({
|
|
|
203
202
|
};
|
|
204
203
|
|
|
205
204
|
return (
|
|
206
|
-
<Globe.Root
|
|
205
|
+
<Globe.Root zoom={zoomProp} translation={translation} rotation={rotation}>
|
|
207
206
|
<Globe.Canvas
|
|
208
|
-
ref={controller}
|
|
209
207
|
topology={styles?.dots ? dots : topology}
|
|
210
208
|
projection={projection}
|
|
211
209
|
styles={styles}
|
|
212
210
|
features={tour ? { points: features?.points ?? [] } : features}
|
|
211
|
+
ref={controller}
|
|
213
212
|
/>
|
|
214
213
|
<Globe.Zoom onAction={handleAction} />
|
|
215
214
|
<Globe.Action onAction={handleAction} />
|
|
@@ -227,7 +226,7 @@ const meta = {
|
|
|
227
226
|
title: 'ui/react-ui-geo/Globe',
|
|
228
227
|
component: Globe.Root,
|
|
229
228
|
render: DefaultStory,
|
|
230
|
-
decorators: [withTheme],
|
|
229
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
231
230
|
parameters: {
|
|
232
231
|
layout: 'fullscreen',
|
|
233
232
|
},
|
|
@@ -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 ThemeMode, type ThemedClassName, useDynamicRef, useThemeContext } from '@dxos/react-ui';
|
|
30
|
-
import { mx } from '@dxos/react-ui-theme';
|
|
31
|
-
|
|
32
29
|
import {
|
|
33
|
-
|
|
34
|
-
type
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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,
|
|
@@ -126,18 +128,43 @@ const getProjection = (type: GlobeCanvasProps['projection'] = 'orthographic'): G
|
|
|
126
128
|
// Root
|
|
127
129
|
//
|
|
128
130
|
|
|
129
|
-
type GlobeRootProps =
|
|
130
|
-
|
|
131
|
-
const GlobeRoot =
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
131
|
+
type GlobeRootProps = Partial<Pick<GlobeContextType, 'center' | 'zoom' | 'translation' | 'rotation'>>;
|
|
132
|
+
|
|
133
|
+
const GlobeRoot = composable<HTMLDivElement, GlobeRootProps>(
|
|
134
|
+
(
|
|
135
|
+
{ children, center: centerProp, zoom: zoomProp, translation: translationProp, rotation: rotationProp, ...props },
|
|
136
|
+
forwardedRef,
|
|
137
|
+
) => {
|
|
138
|
+
const localRef = useRef<HTMLDivElement>(null);
|
|
139
|
+
const composedRef = useComposedRefs<HTMLDivElement>(localRef, forwardedRef);
|
|
140
|
+
const { width, height } = useResizeDetector<HTMLDivElement>({ targetRef: localRef });
|
|
141
|
+
|
|
142
|
+
const [center, setCenter] = useControlledState(centerProp);
|
|
143
|
+
const [zoom, setZoom] = useControlledState(zoomProp ?? 4);
|
|
144
|
+
const [translation, setTranslation] = useControlledState<Point>(translationProp);
|
|
145
|
+
const [rotation, setRotation] = useControlledState<Vector>(rotationProp);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<GlobeContext.Provider
|
|
149
|
+
value={{
|
|
150
|
+
size: { width, height },
|
|
151
|
+
center,
|
|
152
|
+
zoom,
|
|
153
|
+
translation,
|
|
154
|
+
rotation,
|
|
155
|
+
setCenter,
|
|
156
|
+
setZoom,
|
|
157
|
+
setTranslation,
|
|
158
|
+
setRotation,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
<div {...composableProps(props, { classNames: 'relative dx-container' })} ref={composedRef}>
|
|
162
|
+
{children}
|
|
163
|
+
</div>
|
|
164
|
+
</GlobeContext.Provider>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
141
168
|
|
|
142
169
|
//
|
|
143
170
|
// Canvas
|
|
@@ -156,16 +183,16 @@ type GlobeCanvasProps = {
|
|
|
156
183
|
*/
|
|
157
184
|
// TODO(burdon): Move controller to root.
|
|
158
185
|
const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
|
|
159
|
-
({ projection:
|
|
186
|
+
({ projection: projectionProp, topology, features, styles: stylesProp }, forwardRef) => {
|
|
160
187
|
const { themeMode } = useThemeContext();
|
|
161
|
-
const styles = useMemo(() =>
|
|
188
|
+
const styles = useMemo(() => stylesProp ?? defaultStyles[themeMode], [stylesProp, themeMode]);
|
|
162
189
|
|
|
163
190
|
// Canvas.
|
|
164
191
|
const [canvas, setCanvas] = useState<HTMLCanvasElement>(null);
|
|
165
192
|
const canvasRef = (canvas: HTMLCanvasElement) => setCanvas(canvas);
|
|
166
193
|
|
|
167
194
|
// Projection.
|
|
168
|
-
const projection = useMemo(() => getProjection(
|
|
195
|
+
const projection = useMemo(() => getProjection(projectionProp), [projectionProp]);
|
|
169
196
|
|
|
170
197
|
// Layers.
|
|
171
198
|
// TODO(burdon): Generate on the fly based on what is visible.
|
|
@@ -199,9 +226,9 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
|
|
|
199
226
|
translation,
|
|
200
227
|
rotation,
|
|
201
228
|
setCenter,
|
|
202
|
-
setZoom: (
|
|
203
|
-
if (typeof
|
|
204
|
-
const is = interpolateNumber(zoomRef.current,
|
|
229
|
+
setZoom: (state) => {
|
|
230
|
+
if (typeof state === 'function') {
|
|
231
|
+
const is = interpolateNumber(zoomRef.current, state(zoomRef.current));
|
|
205
232
|
// Stop easing if already zooming.
|
|
206
233
|
transition()
|
|
207
234
|
.ease(zooming.current ? easeLinear : easeSinOut)
|
|
@@ -211,7 +238,7 @@ const GlobeCanvas = forwardRef<GlobeController, GlobeCanvasProps>(
|
|
|
211
238
|
zooming.current = false;
|
|
212
239
|
});
|
|
213
240
|
} else {
|
|
214
|
-
setZoom(
|
|
241
|
+
setZoom(state);
|
|
215
242
|
}
|
|
216
243
|
},
|
|
217
244
|
setTranslation,
|
|
@@ -258,7 +285,7 @@ const GlobeDebug = ({ position = 'topleft' }: { position?: ControlPosition }) =>
|
|
|
258
285
|
return (
|
|
259
286
|
<div
|
|
260
287
|
className={mx(
|
|
261
|
-
'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded',
|
|
288
|
+
'z-10 absolute w-96 p-2 overflow-hidden border border-green-700 rounded-sm',
|
|
262
289
|
controlPositions[position],
|
|
263
290
|
)}
|
|
264
291
|
>
|
|
@@ -5,11 +5,10 @@
|
|
|
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';
|
|
12
|
-
|
|
13
12
|
import { Map, type MapController } from './Map';
|
|
14
13
|
|
|
15
14
|
const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
|
|
@@ -17,11 +16,13 @@ const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
|
|
|
17
16
|
const handleZoomAction = useMapZoomHandler(controller);
|
|
18
17
|
|
|
19
18
|
return (
|
|
20
|
-
<Map.Root
|
|
21
|
-
<Map.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
<Map.Root>
|
|
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
|
};
|
|
@@ -30,7 +31,7 @@ const meta = {
|
|
|
30
31
|
title: 'ui/react-ui-geo/Map',
|
|
31
32
|
component: Map.Root as any,
|
|
32
33
|
render: DefaultStory,
|
|
33
|
-
decorators: [withTheme],
|
|
34
|
+
decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
|
|
34
35
|
parameters: {
|
|
35
36
|
layout: 'fullscreen',
|
|
36
37
|
},
|
|
@@ -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
|
|
9
|
+
import React, { type PropsWithChildren, forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
|
|
10
10
|
import { createRoot } from 'react-dom/client';
|
|
11
|
-
import type
|
|
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 {
|
|
15
|
-
import {
|
|
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 [
|
|
46
|
+
const [MapContextProvider, useMapContext] = createContext<MapContextValue>('Map');
|
|
48
47
|
|
|
49
48
|
//
|
|
50
49
|
// Root
|
|
51
50
|
//
|
|
52
51
|
|
|
53
|
-
type MapRootProps =
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
|
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
|
-
<
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
</
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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('
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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--
|
|
51
|
-
label='start'
|
|
52
|
+
icon='ph--path--regular'
|
|
52
53
|
iconOnly
|
|
53
|
-
|
|
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
|
-
|
|
62
|
-
classNames='px-0 aspect-square'
|
|
60
|
+
label={t('toggle-icon.button')}
|
|
63
61
|
onClick={() => onAction?.('toggle')}
|
|
64
62
|
/>
|
|
65
63
|
</Toolbar.Root>
|