@dxos/react-ui-geo 0.8.4-main.dedc0f3 → 0.8.4-main.dfabb4ec29

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 (68) hide show
  1. package/dist/lib/browser/{countries-110m-ZM3ZIEFS.mjs → countries-110m-RE5RNRQG.mjs} +1 -1
  2. package/dist/lib/browser/data.mjs +4 -3
  3. package/dist/lib/browser/data.mjs.map +4 -4
  4. package/dist/lib/browser/index.mjs +364 -462
  5. package/dist/lib/browser/index.mjs.map +3 -3
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/translations.mjs +19 -0
  8. package/dist/lib/browser/translations.mjs.map +7 -0
  9. package/dist/lib/node-esm/{countries-110m-3SFASWVD.mjs → countries-110m-4EDBXSFJ.mjs} +1 -1
  10. package/dist/lib/node-esm/data.mjs +5 -3
  11. package/dist/lib/node-esm/data.mjs.map +4 -4
  12. package/dist/lib/node-esm/index.mjs +364 -461
  13. package/dist/lib/node-esm/index.mjs.map +3 -3
  14. package/dist/lib/node-esm/meta.json +1 -1
  15. package/dist/lib/node-esm/translations.mjs +21 -0
  16. package/dist/lib/node-esm/translations.mjs.map +7 -0
  17. package/dist/types/data/airports.d.ts +4 -4
  18. package/dist/types/data/airports.d.ts.map +1 -1
  19. package/dist/types/data/cities.d.ts.map +1 -1
  20. package/dist/types/data/countries-110m.d.ts.map +1 -1
  21. package/dist/types/data/countries-dots-3.d.ts.map +1 -1
  22. package/dist/types/data/countries-dots-4.d.ts.map +1 -1
  23. package/dist/types/src/components/Globe/Globe.d.ts +5 -3
  24. package/dist/types/src/components/Globe/Globe.d.ts.map +1 -1
  25. package/dist/types/src/components/Globe/Globe.stories.d.ts +9 -5
  26. package/dist/types/src/components/Globe/Globe.stories.d.ts.map +1 -1
  27. package/dist/types/src/components/Map/Map.d.ts +22 -11
  28. package/dist/types/src/components/Map/Map.d.ts.map +1 -1
  29. package/dist/types/src/components/Map/Map.stories.d.ts +3 -1
  30. package/dist/types/src/components/Map/Map.stories.d.ts.map +1 -1
  31. package/dist/types/src/components/Toolbar/Controls.d.ts.map +1 -1
  32. package/dist/types/src/hooks/context.d.ts +1 -3
  33. package/dist/types/src/hooks/context.d.ts.map +1 -1
  34. package/dist/types/src/hooks/useDrag.d.ts.map +1 -1
  35. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts +1 -1
  36. package/dist/types/src/hooks/useGlobeZoomHandler.d.ts.map +1 -1
  37. package/dist/types/src/hooks/useMapZoomHandler.d.ts +1 -1
  38. package/dist/types/src/hooks/useMapZoomHandler.d.ts.map +1 -1
  39. package/dist/types/src/hooks/useSpinner.d.ts.map +1 -1
  40. package/dist/types/src/hooks/useTour.d.ts.map +1 -1
  41. package/dist/types/src/index.d.ts +0 -1
  42. package/dist/types/src/index.d.ts.map +1 -1
  43. package/dist/types/src/translations.d.ts +12 -0
  44. package/dist/types/src/translations.d.ts.map +1 -0
  45. package/dist/types/src/util/debug.d.ts.map +1 -1
  46. package/dist/types/src/util/inertia.d.ts.map +1 -1
  47. package/dist/types/src/util/path.d.ts.map +1 -1
  48. package/dist/types/src/util/render.d.ts.map +1 -1
  49. package/dist/types/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +42 -36
  51. package/src/components/Globe/Globe.stories.tsx +10 -10
  52. package/src/components/Globe/Globe.tsx +55 -28
  53. package/src/components/Map/Map.stories.tsx +12 -10
  54. package/src/components/Map/Map.tsx +82 -71
  55. package/src/components/Toolbar/Controls.tsx +12 -14
  56. package/src/hooks/context.tsx +6 -29
  57. package/src/hooks/useGlobeZoomHandler.ts +8 -2
  58. package/src/hooks/useSpinner.ts +0 -1
  59. package/src/hooks/useTour.ts +1 -0
  60. package/src/index.ts +0 -1
  61. package/src/translations.ts +20 -0
  62. package/src/util/render.ts +0 -1
  63. package/dist/lib/browser/chunk-GMWLKTLN.mjs +0 -9
  64. package/dist/lib/browser/chunk-GMWLKTLN.mjs.map +0 -7
  65. package/dist/lib/node-esm/chunk-JODBF4CC.mjs +0 -11
  66. package/dist/lib/node-esm/chunk-JODBF4CC.mjs.map +0 -7
  67. /package/dist/lib/browser/{countries-110m-ZM3ZIEFS.mjs.map → countries-110m-RE5RNRQG.mjs.map} +0 -0
  68. /package/dist/lib/node-esm/{countries-110m-3SFASWVD.mjs.map → countries-110m-4EDBXSFJ.mjs.map} +0 -0
package/package.json CHANGED
@@ -1,87 +1,93 @@
1
1
  {
2
2
  "name": "@dxos/react-ui-geo",
3
- "version": "0.8.4-main.dedc0f3",
3
+ "version": "0.8.4-main.dfabb4ec29",
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/index.ts",
20
- "types": "./dist/types/src/index.d.ts",
21
- "browser": "./dist/lib/browser/index.mjs",
22
- "node": "./dist/lib/node-esm/index.mjs"
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
- "@preact-signals/safe-react": "^0.9.0",
40
- "@radix-ui/react-context": "^1.0.5",
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",
44
50
  "geojson": "^0.5.0",
45
51
  "leaflet": "^1.9.4",
46
52
  "lodash.defaultsdeep": "^4.6.1",
47
- "react-leaflet": "^4.2.1",
53
+ "react-leaflet": "^5.0.0",
48
54
  "react-resize-detector": "^11.0.1",
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.dedc0f3",
53
- "@dxos/node-std": "0.8.4-main.dedc0f3",
54
- "@dxos/util": "0.8.4-main.dedc0f3",
55
- "@dxos/log": "0.8.4-main.dedc0f3",
56
- "@dxos/debug": "0.8.4-main.dedc0f3"
58
+ "@dxos/async": "0.8.4-main.dfabb4ec29",
59
+ "@dxos/log": "0.8.4-main.dfabb4ec29",
60
+ "@dxos/node-std": "0.8.4-main.dfabb4ec29",
61
+ "@dxos/debug": "0.8.4-main.dfabb4ec29",
62
+ "@dxos/util": "0.8.4-main.dfabb4ec29"
57
63
  },
58
64
  "devDependencies": {
59
- "@react-three/drei": "^9.99.0",
60
- "@react-three/fiber": "^8.15.0",
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": "~18.2.0",
65
- "@types/react-dom": "~18.2.0",
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.9.35",
73
- "react": "~18.2.0",
74
- "react-dom": "~18.2.0",
75
- "three": "0.165.0",
76
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
77
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3",
78
- "@dxos/storybook-utils": "0.8.4-main.dedc0f3"
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.dfabb4ec29",
83
+ "@dxos/ui-theme": "0.8.4-main.dfabb4ec29",
84
+ "@dxos/storybook-utils": "0.8.4-main.dfabb4ec29"
79
85
  },
80
86
  "peerDependencies": {
81
- "react": "~18.2.0",
82
- "react-dom": "~18.2.0",
83
- "@dxos/react-ui": "0.8.4-main.dedc0f3",
84
- "@dxos/react-ui-theme": "0.8.4-main.dedc0f3"
87
+ "react": "~19.2.3",
88
+ "react-dom": "~19.2.3",
89
+ "@dxos/react-ui": "0.8.4-main.dfabb4ec29",
90
+ "@dxos/ui-theme": "0.8.4-main.dfabb4ec29"
85
91
  },
86
92
  "publishConfig": {
87
93
  "access": "public"
@@ -2,8 +2,6 @@
2
2
  // Copyright 2018 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
8
6
  import { type FeatureCollection, type Geometry, type Position } from 'geojson';
9
7
  import { Leva } from 'leva';
@@ -11,13 +9,12 @@ import React, { useMemo, useRef, useState } from 'react';
11
9
  import { type Topology } from 'topojson-specification';
12
10
 
13
11
  import { useAsyncState } from '@dxos/react-ui';
14
- import { withLayout, withTheme } from '@dxos/storybook-utils';
12
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
15
13
 
16
14
  import { type Vector, useDrag, useGlobeZoomHandler, useSpinner, useTour } from '../../hooks';
17
15
  import { type LatLngLiteral } from '../../types';
18
16
  import { type StyleSet, closestPoint } from '../../util';
19
17
  import { type ControlProps } from '../Toolbar';
20
-
21
18
  import { Globe, type GlobeCanvasProps, type GlobeController, type GlobeRootProps } from './Globe';
22
19
 
23
20
  // TODO(burdon): Load from JSON at runtime?
@@ -130,7 +127,7 @@ const createTrip = (
130
127
  );
131
128
  };
132
129
 
133
- type StoryProps = Pick<GlobeRootProps, 'zoom' | 'translation' | 'rotation'> &
130
+ type DefaultStoryProps = Pick<GlobeRootProps, 'zoom' | 'translation' | 'rotation'> &
134
131
  Pick<GlobeCanvasProps, 'projection' | 'styles'> & {
135
132
  drag?: boolean;
136
133
  spin?: boolean;
@@ -139,7 +136,7 @@ type StoryProps = Pick<GlobeRootProps, 'zoom' | 'translation' | 'rotation'> &
139
136
  };
140
137
 
141
138
  const DefaultStory = ({
142
- zoom: _zoom = 1,
139
+ zoom: zoomProp = 1,
143
140
  translation,
144
141
  rotation = [0, 0, 0],
145
142
  projection,
@@ -148,7 +145,7 @@ const DefaultStory = ({
148
145
  spin = false,
149
146
  tour = false,
150
147
  xAxis = false,
151
- }: StoryProps) => {
148
+ }: DefaultStoryProps) => {
152
149
  const controller = useRef<GlobeController>(null);
153
150
  const [dots] = useAsyncState(async () => {
154
151
  const points = (await import('../../../data/countries-dots-3.ts')).default;
@@ -205,13 +202,13 @@ const DefaultStory = ({
205
202
  };
206
203
 
207
204
  return (
208
- <Globe.Root classNames='absolute inset-0' zoom={_zoom} translation={translation} rotation={rotation}>
205
+ <Globe.Root zoom={zoomProp} translation={translation} rotation={rotation}>
209
206
  <Globe.Canvas
210
- ref={controller}
211
207
  topology={styles?.dots ? dots : topology}
212
208
  projection={projection}
213
209
  styles={styles}
214
210
  features={tour ? { points: features?.points ?? [] } : features}
211
+ ref={controller}
215
212
  />
216
213
  <Globe.Zoom onAction={handleAction} />
217
214
  <Globe.Action onAction={handleAction} />
@@ -229,7 +226,10 @@ const meta = {
229
226
  title: 'ui/react-ui-geo/Globe',
230
227
  component: Globe.Root,
231
228
  render: DefaultStory,
232
- decorators: [withTheme, withLayout({ fullscreen: true, classNames: 'bg-[#000]' })],
229
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
230
+ parameters: {
231
+ layout: 'fullscreen',
232
+ },
233
233
  } satisfies Meta;
234
234
 
235
235
  export default meta;
@@ -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
- 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,
@@ -126,18 +128,43 @@ const getProjection = (type: GlobeCanvasProps['projection'] = 'orthographic'): G
126
128
  // Root
127
129
  //
128
130
 
129
- type GlobeRootProps = PropsWithChildren<ThemedClassName<GlobeContextProviderProps>>;
130
-
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
- };
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: _projection, topology, features, styles: _styles }, forwardRef) => {
186
+ ({ projection: projectionProp, topology, features, styles: stylesProp }, forwardRef) => {
160
187
  const { themeMode } = useThemeContext();
161
- const styles = useMemo(() => _styles ?? defaultStyles[themeMode], [_styles, themeMode]);
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(_projection), [_projection]);
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: (s) => {
203
- if (typeof s === 'function') {
204
- const is = interpolateNumber(zoomRef.current, s(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(s);
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
  >
@@ -2,16 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import '@dxos-theme';
6
-
7
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
10
  import { useMapZoomHandler } from '../../hooks';
13
11
  import { type GeoMarker } from '../../types';
14
-
15
12
  import { Map, type MapController } from './Map';
16
13
 
17
14
  const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
@@ -19,11 +16,13 @@ const DefaultStory = ({ markers = [] }: { markers?: GeoMarker[] }) => {
19
16
  const handleZoomAction = useMapZoomHandler(controller);
20
17
 
21
18
  return (
22
- <Map.Root ref={setController}>
23
- <Map.Tiles />
24
- <Map.Markers markers={markers} />
25
- <Map.Zoom position='bottomleft' onAction={handleZoomAction} />
26
- <Map.Action position='bottomright' />
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>
27
26
  </Map.Root>
28
27
  );
29
28
  };
@@ -32,7 +31,10 @@ const meta = {
32
31
  title: 'ui/react-ui-geo/Map',
33
32
  component: Map.Root as any,
34
33
  render: DefaultStory,
35
- decorators: [withTheme, withLayout({ fullscreen: true })],
34
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen' })],
35
+ parameters: {
36
+ layout: 'fullscreen',
37
+ },
36
38
  } satisfies Meta<typeof DefaultStory>;
37
39
 
38
40
  export default meta;
@@ -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
+ };