react-native-maplibre-lite 0.2.0 → 0.2.1

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 (78) hide show
  1. package/README.md +92 -6
  2. package/lib/module/components/MapView.js +116 -14
  3. package/lib/module/components/MapView.js.map +1 -1
  4. package/lib/module/components/NavigatorHud.js +152 -0
  5. package/lib/module/components/NavigatorHud.js.map +1 -0
  6. package/lib/module/components/NavigatorRecenterButton.js +48 -0
  7. package/lib/module/components/NavigatorRecenterButton.js.map +1 -0
  8. package/lib/module/components/NavigatorVoiceControl.js +173 -0
  9. package/lib/module/components/NavigatorVoiceControl.js.map +1 -0
  10. package/lib/module/components/navigatorChromeTheme.js +98 -0
  11. package/lib/module/components/navigatorChromeTheme.js.map +1 -0
  12. package/lib/module/components/navigatorManeuverIcon.js +210 -0
  13. package/lib/module/components/navigatorManeuverIcon.js.map +1 -0
  14. package/lib/module/components/navigatorVoiceCatalog.js +225 -0
  15. package/lib/module/components/navigatorVoiceCatalog.js.map +1 -0
  16. package/lib/module/components/navigatorVoiceKeys.js +14 -0
  17. package/lib/module/components/navigatorVoiceKeys.js.map +1 -0
  18. package/lib/module/components/navigatorVoicePlayer.js +100 -0
  19. package/lib/module/components/navigatorVoicePlayer.js.map +1 -0
  20. package/lib/module/components/navigatorVoiceStrings.js +31 -0
  21. package/lib/module/components/navigatorVoiceStrings.js.map +1 -0
  22. package/lib/module/components/types.js +22 -0
  23. package/lib/module/components/types.js.map +1 -1
  24. package/lib/module/components/useNavigatorVoice.js +78 -0
  25. package/lib/module/components/useNavigatorVoice.js.map +1 -0
  26. package/lib/module/components/utils.js +26 -0
  27. package/lib/module/components/utils.js.map +1 -1
  28. package/lib/module/components/webMapBuild.js +1 -1
  29. package/lib/module/components/webMapBuild.js.map +1 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/typescript/src/components/MapView.d.ts +14 -1
  32. package/lib/typescript/src/components/MapView.d.ts.map +1 -1
  33. package/lib/typescript/src/components/NavigatorHud.d.ts +13 -0
  34. package/lib/typescript/src/components/NavigatorHud.d.ts.map +1 -0
  35. package/lib/typescript/src/components/NavigatorRecenterButton.d.ts +11 -0
  36. package/lib/typescript/src/components/NavigatorRecenterButton.d.ts.map +1 -0
  37. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts +20 -0
  38. package/lib/typescript/src/components/NavigatorVoiceControl.d.ts.map +1 -0
  39. package/lib/typescript/src/components/navigatorChromeTheme.d.ts +19 -0
  40. package/lib/typescript/src/components/navigatorChromeTheme.d.ts.map +1 -0
  41. package/lib/typescript/src/components/navigatorManeuverIcon.d.ts +20 -0
  42. package/lib/typescript/src/components/navigatorManeuverIcon.d.ts.map +1 -0
  43. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts +50 -0
  44. package/lib/typescript/src/components/navigatorVoiceCatalog.d.ts.map +1 -0
  45. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts +10 -0
  46. package/lib/typescript/src/components/navigatorVoiceKeys.d.ts.map +1 -0
  47. package/lib/typescript/src/components/navigatorVoicePlayer.d.ts +15 -0
  48. package/lib/typescript/src/components/navigatorVoicePlayer.d.ts.map +1 -0
  49. package/lib/typescript/src/components/navigatorVoiceStrings.d.ts +19 -0
  50. package/lib/typescript/src/components/navigatorVoiceStrings.d.ts.map +1 -0
  51. package/lib/typescript/src/components/types.d.ts +83 -17
  52. package/lib/typescript/src/components/types.d.ts.map +1 -1
  53. package/lib/typescript/src/components/useNavigatorVoice.d.ts +20 -0
  54. package/lib/typescript/src/components/useNavigatorVoice.d.ts.map +1 -0
  55. package/lib/typescript/src/components/utils.d.ts +9 -0
  56. package/lib/typescript/src/components/utils.d.ts.map +1 -1
  57. package/lib/typescript/src/components/webMapBuild.d.ts +1 -1
  58. package/lib/typescript/src/components/webMapBuild.d.ts.map +1 -1
  59. package/lib/typescript/src/index.d.ts +1 -1
  60. package/lib/typescript/src/index.d.ts.map +1 -1
  61. package/package.json +16 -7
  62. package/resources/README.md +62 -0
  63. package/resources/map.html +797 -0
  64. package/src/components/MapView.tsx +154 -8
  65. package/src/components/NavigatorHud.tsx +166 -0
  66. package/src/components/NavigatorRecenterButton.tsx +45 -0
  67. package/src/components/NavigatorVoiceControl.tsx +198 -0
  68. package/src/components/navigatorChromeTheme.ts +118 -0
  69. package/src/components/navigatorManeuverIcon.tsx +177 -0
  70. package/src/components/navigatorVoiceCatalog.ts +275 -0
  71. package/src/components/navigatorVoiceKeys.ts +132 -0
  72. package/src/components/navigatorVoicePlayer.tsx +126 -0
  73. package/src/components/navigatorVoiceStrings.ts +42 -0
  74. package/src/components/types.ts +87 -18
  75. package/src/components/useNavigatorVoice.ts +96 -0
  76. package/src/components/utils.ts +28 -0
  77. package/src/components/webMapBuild.ts +1 -1
  78. package/src/index.tsx +8 -0
@@ -4,6 +4,7 @@ import {
4
4
  useContext,
5
5
  useEffect,
6
6
  useImperativeHandle,
7
+ useMemo,
7
8
  useRef,
8
9
  useState,
9
10
  } from 'react';
@@ -21,25 +22,40 @@ import KeepAwake from '@sayem314/react-native-keep-awake';
21
22
 
22
23
  import MapPlaceholder from './MapPlaceholder';
23
24
  import MapSelectPoint, { type MapSelectPointType } from './MapSelectPoint';
25
+ import { NavigatorHud } from './NavigatorHud';
26
+ import { NavigatorRecenterButton } from './NavigatorRecenterButton';
27
+ import { NavigatorVoiceControl } from './NavigatorVoiceControl';
28
+ import { resolveNavigatorChromeTheme } from './navigatorChromeTheme';
29
+ import { keysToClipUrls, volumeLevelToGain } from './navigatorVoiceCatalog';
30
+ import { NavigatorVoicePlayer, type NavigatorVoicePlayerRef } from './navigatorVoicePlayer';
31
+ import { navigatorUiStrings } from './navigatorVoiceStrings';
32
+ import { useNavigatorVoice } from './useNavigatorVoice';
24
33
  import {
25
34
  type EventParams,
26
35
  type MapLiteInitParams,
36
+ type MapLiteMapErrorEventParams,
37
+ type MapLiteMarkerParams,
38
+ type MapLitePolygonParams,
39
+ type MapLitePolylineParams,
40
+ type MapLiteRemoveOverlayParams,
27
41
  type MapLiteSetNavigatorPositionParams,
28
42
  type MapLiteUpdateParams,
29
43
  type MapLiteWebError,
30
44
  type MarkerProps,
31
45
  type NativeToWebCommand,
32
46
  type NavigatorChromeParams,
47
+ type NavigatorHudState,
33
48
  type NavigatorInstructionParams,
34
49
  type NavigatorLang,
35
50
  type NavigatorProfile,
36
51
  type NavigatorPositionSetParams,
37
52
  type NavigatorRouteSetParams,
53
+ type NavigatorVoicePlayParams,
38
54
  type PolygonProps,
39
55
  type PolylineProps,
40
56
  type WebToNativeMessage,
41
57
  } from './types';
42
- import { loadResources } from './utils';
58
+ import { loadResources, getNativeMapHtmlUri } from './utils';
43
59
  import { MAP_HTML } from './webMapBuild';
44
60
 
45
61
 
@@ -69,6 +85,8 @@ interface MapViewProps {
69
85
  onZoomStart?: (eventParams: EventParams) => void;
70
86
  onZoomEnd?: (eventParams: EventParams) => void;
71
87
  onIdle?: (eventParams: EventParams) => void;
88
+ /** MapLibre `error` из WebView (`event: 'error'`). */
89
+ onMapError?: (params: MapLiteMapErrorEventParams) => void;
72
90
  showSelectPoint?: boolean;
73
91
  selectPointColor?: string;
74
92
  selectPointBackgroundColor?: string;
@@ -98,12 +116,23 @@ interface MapViewProps {
98
116
  * Sent once in WebView `init` as `navigatorChrome`.
99
117
  */
100
118
  navigatorChrome?: NavigatorChromeParams;
119
+ /**
120
+ * URL каталога голосов навигатора (`.../voices/data.json`). Каталог,
121
+ * манифест, выбор голоса/громкости и воспроизведение — на нативной
122
+ * стороне. Без него озвучка и FAB выбора голоса отключены.
123
+ */
124
+ navigatorVoiceUrl?: string;
101
125
  onNavigatorRouteSet?: (params: NavigatorRouteSetParams) => void;
102
126
  onNavigatorInstruction?: (params: NavigatorInstructionParams) => void;
103
127
  onNavigatorPositionSet?: (params: NavigatorPositionSetParams) => void;
104
128
  /** Ошибки команд WebView (`type: 'error'` из карты). */
105
129
  onMapLiteError?: (err: MapLiteWebError) => void;
106
130
 
131
+ /**
132
+ * Загружать WebView из `map.html` в нативных ресурсах приложения вместо inline `MAP_HTML`.
133
+ * Размещение файла — см. `resources/README.md`.
134
+ */
135
+ useNativeMapHtml?: boolean;
107
136
 
108
137
  developerLocalhostBundleUrl?: string;
109
138
  }
@@ -165,6 +194,36 @@ const getBoundsFromCoords = (
165
194
  ];
166
195
  };
167
196
 
197
+ const toWebMarkerParams = (props: MarkerProps): MapLiteMarkerParams => ({
198
+ uniqueId: props.uniqueId,
199
+ latitude: props.latitude,
200
+ longitude: props.longitude,
201
+ color: props.color,
202
+ iconUrl: props.iconUrl,
203
+ iconWidth: props.iconWidth,
204
+ iconHeight: props.iconHeight,
205
+ html: props.html,
206
+ });
207
+
208
+ const toWebPolylineParams = (props: PolylineProps): MapLitePolylineParams => ({
209
+ uniqueId: props.uniqueId,
210
+ coordinates: props.coordinates,
211
+ color: props.color,
212
+ width: props.width,
213
+ });
214
+
215
+ const toWebPolygonParams = (props: PolygonProps): MapLitePolygonParams => ({
216
+ uniqueId: props.uniqueId,
217
+ coordinates: props.coordinates,
218
+ fillColor: props.fillColor,
219
+ fillOpacity: props.fillOpacity,
220
+ strokeColor: props.strokeColor,
221
+ strokeOpacity: props.strokeOpacity,
222
+ strokeWidth: props.strokeWidth,
223
+ });
224
+
225
+ const toWebRemoveParams = (uniqueId: string): MapLiteRemoveOverlayParams => ({ uniqueId });
226
+
168
227
  /** Минимальный интервал между отправками позиции в WebView (мост RN↔JS). */
169
228
  const NAVIGATOR_GPS_FORWARD_MIN_MS = 500;
170
229
 
@@ -177,11 +236,51 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
177
236
  initedRef.current = inited;
178
237
  }, [inited]);
179
238
 
239
+ /** View-model HUD маршрута, приходит из WebView (`navigatorHud`). */
240
+ const [hudState, setHudState] = useState<NavigatorHudState | null>(null);
241
+ const voicePlayerRef = useRef<NavigatorVoicePlayerRef | null>(null);
242
+
243
+ /** Озвучка ограничена русским набором фраз (как в веб-навигаторе). */
244
+ const voiceSupported = props.navigator === true && props.navigatorLang !== 'en';
245
+ const voice = useNavigatorVoice(voiceSupported ? props.navigatorVoiceUrl : undefined);
246
+
247
+ const chromeTheme = useMemo(
248
+ () => resolveNavigatorChromeTheme(props.navigatorChrome),
249
+ [props.navigatorChrome]
250
+ );
251
+ const navigatorStrings = useMemo(
252
+ () => navigatorUiStrings(props.navigatorLang),
253
+ [props.navigatorLang]
254
+ );
255
+
256
+ useEffect(() => {
257
+ voicePlayerRef.current?.setVolume(volumeLevelToGain(voice.volumeLevel));
258
+ }, [voice.volumeLevel]);
259
+
260
+ const handleNavigatorVoice = (params: NavigatorVoicePlayParams) => {
261
+ const player = voicePlayerRef.current;
262
+ if (!player) return;
263
+ if (params.action === 'stop') {
264
+ player.stop();
265
+ return;
266
+ }
267
+ if (params.action === 'prefetch') return;
268
+ if (!voice.voiceEnabled || !voice.manifest || !voice.clipBaseUrl) return;
269
+ const urls = keysToClipUrls(voice.manifest, voice.clipBaseUrl, params.keys);
270
+ if (urls.length > 0) player.playUrls(urls);
271
+ };
272
+
180
273
  const coordsInMapRef = useRef<Record<string, [number, number][]>>({});
181
274
  const markersClickHandlers = useRef<Record<string, () => void>>({});
182
275
  const mapSelectPointRef = useRef<MapSelectPointType | null>(null);
183
276
  const performanceMode = props.performanceMode ?? (Platform.OS === 'android' ? 'balanced' : 'quality');
184
277
 
278
+ const webViewSource = props.developerLocalhostBundleUrl
279
+ ? { uri: props.developerLocalhostBundleUrl }
280
+ : props.useNativeMapHtml
281
+ ? { uri: getNativeMapHtmlUri() }
282
+ : { html: MAP_HTML };
283
+
185
284
  const fallbackPixelRatio = performanceMode === 'performance'
186
285
  ? 0.85
187
286
  : (performanceMode === 'balanced' && Platform.OS === 'android' ? 1 : undefined);
@@ -247,14 +346,14 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
247
346
 
248
347
  updateMarkerClickHandler(propsMarker);
249
348
 
250
- sendToWebView({ function: 'addMarker', params: propsMarker });
349
+ sendToWebView({ function: 'addMarker', params: toWebMarkerParams(propsMarker) });
251
350
  scheduleAutoFitBounds();
252
351
  };
253
352
 
254
353
  const removeMarker = (propsMarker: MarkerProps) => {
255
354
  delete coordsInMapRef.current[propsMarker.uniqueId];
256
355
  delete markersClickHandlers.current[propsMarker.uniqueId];
257
- sendToWebView({ function: 'removeMarker', params: propsMarker });
356
+ sendToWebView({ function: 'removeMarker', params: toWebRemoveParams(propsMarker.uniqueId) });
258
357
  scheduleAutoFitBounds();
259
358
  };
260
359
 
@@ -262,13 +361,13 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
262
361
  if (propsPolyline.coordinates && !propsPolyline.ignoreFitBounds) {
263
362
  coordsInMapRef.current[propsPolyline.uniqueId] = propsPolyline.coordinates;
264
363
  }
265
- sendToWebView({ function: 'addPolyline', params: propsPolyline });
364
+ sendToWebView({ function: 'addPolyline', params: toWebPolylineParams(propsPolyline) });
266
365
  scheduleAutoFitBounds();
267
366
  };
268
367
 
269
368
  const removePolyline = (propsPolyline: PolylineProps) => {
270
369
  delete coordsInMapRef.current[propsPolyline.uniqueId];
271
- sendToWebView({ function: 'removePolyline', params: propsPolyline });
370
+ sendToWebView({ function: 'removePolyline', params: toWebRemoveParams(propsPolyline.uniqueId) });
272
371
  scheduleAutoFitBounds();
273
372
  };
274
373
 
@@ -276,13 +375,13 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
276
375
  if (propsPolygon.coordinates && !propsPolygon.ignoreFitBounds) {
277
376
  coordsInMapRef.current[propsPolygon.uniqueId] = propsPolygon.coordinates;
278
377
  }
279
- sendToWebView({ function: 'addPolygon', params: propsPolygon });
378
+ sendToWebView({ function: 'addPolygon', params: toWebPolygonParams(propsPolygon) });
280
379
  scheduleAutoFitBounds();
281
380
  };
282
381
 
283
382
  const removePolygon = (propsPolygon: PolygonProps) => {
284
383
  delete coordsInMapRef.current[propsPolygon.uniqueId];
285
- sendToWebView({ function: 'removePolygon', params: propsPolygon });
384
+ sendToWebView({ function: 'removePolygon', params: toWebRemoveParams(propsPolygon.uniqueId) });
286
385
  scheduleAutoFitBounds();
287
386
  };
288
387
 
@@ -334,6 +433,10 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
334
433
  sendToWebView({ function: 'pickNavigatorPosition', params: {} });
335
434
  };
336
435
 
436
+ const recenterNavigatorCamera = () => {
437
+ sendToWebView({ function: 'recenterNavigatorCamera', params: {} });
438
+ };
439
+
337
440
  useImperativeHandle(ref, () => ({
338
441
  fitBounds,
339
442
  flyTo,
@@ -407,6 +510,9 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
407
510
  case 'idle':
408
511
  props.onIdle?.(msg.params as EventParams);
409
512
  break;
513
+ case 'error':
514
+ props.onMapError?.(msg.params as MapLiteMapErrorEventParams);
515
+ break;
410
516
  case 'navigatorRouteSet':
411
517
  props.onNavigatorRouteSet?.(msg.params as NavigatorRouteSetParams);
412
518
  break;
@@ -416,6 +522,12 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
416
522
  case 'navigatorPositionSet':
417
523
  props.onNavigatorPositionSet?.(msg.params as NavigatorPositionSetParams);
418
524
  break;
525
+ case 'navigatorHud':
526
+ setHudState(msg.params as NavigatorHudState);
527
+ break;
528
+ case 'navigatorVoice':
529
+ handleNavigatorVoice(msg.params as NavigatorVoicePlayParams);
530
+ break;
419
531
  }
420
532
  return;
421
533
  }
@@ -523,7 +635,7 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
523
635
  ref={webViewRef}
524
636
  style={{ flex: 1, backgroundColor: 'transparent' }}
525
637
  originWhitelist={['*']}
526
- source={!!props.developerLocalhostBundleUrl ? { uri: props.developerLocalhostBundleUrl } : { html: MAP_HTML }}
638
+ source={webViewSource}
527
639
  onMessage={event => {
528
640
  onReceiveMessageFromWebView(event.nativeEvent.data);
529
641
  }}
@@ -554,6 +666,40 @@ export const MapView = forwardRef<MapViewRef, MapViewProps>((props, ref) => {
554
666
  />
555
667
  </View>
556
668
  )}
669
+
670
+ {inited && props.navigator && (
671
+ <>
672
+ <NavigatorHud
673
+ state={hudState}
674
+ theme={chromeTheme}
675
+ pickHint={navigatorStrings.pickHint}
676
+ />
677
+ <NavigatorRecenterButton
678
+ theme={chromeTheme}
679
+ accessibilityLabel={navigatorStrings.recenterAria}
680
+ onPress={recenterNavigatorCamera}
681
+ />
682
+ {voiceSupported && (
683
+ <NavigatorVoiceControl
684
+ theme={chromeTheme}
685
+ strings={navigatorStrings}
686
+ catalog={voice.catalog}
687
+ selectedDir={voice.selectedDir}
688
+ voiceEnabled={voice.voiceEnabled}
689
+ volumeLevel={voice.volumeLevel}
690
+ onSelectVoice={voice.selectVoice}
691
+ onSelectVolume={voice.selectVolume}
692
+ onDisable={voice.disableVoice}
693
+ />
694
+ )}
695
+ {voiceSupported && (
696
+ <NavigatorVoicePlayer
697
+ ref={voicePlayerRef}
698
+ initialVolume={volumeLevelToGain(voice.volumeLevel)}
699
+ />
700
+ )}
701
+ </>
702
+ )}
557
703
  </MapViewContext.Provider>
558
704
  {!inited && (
559
705
  <MapPlaceholder theme={props.placeholderTheme ?? 'light'} />
@@ -0,0 +1,166 @@
1
+ import { Text, View } from 'react-native';
2
+
3
+ import { NavigatorManeuverIcon } from './navigatorManeuverIcon';
4
+ import type { NavigatorChromeTheme } from './navigatorChromeTheme';
5
+ import type { NavigatorHudState } from './types';
6
+
7
+ /**
8
+ * Нативная панель навигатора (HUD): порт DOM-плашки из веб-навигатора.
9
+ * Получает уже отформатированную view-model (`navigatorHud`) и тему из
10
+ * `navigatorChrome`; вся локализация/форматирование остаются в вебе.
11
+ */
12
+ export function NavigatorHud({
13
+ state,
14
+ theme,
15
+ pickHint,
16
+ }: {
17
+ state: NavigatorHudState | null;
18
+ theme: NavigatorChromeTheme;
19
+ pickHint: string;
20
+ }) {
21
+ if (!state || !state.visible) return null;
22
+
23
+ if (state.pick) {
24
+ return (
25
+ <View
26
+ pointerEvents="none"
27
+ style={{
28
+ position: 'absolute',
29
+ top: 12,
30
+ left: 12,
31
+ right: 12,
32
+ padding: 14,
33
+ borderRadius: 14,
34
+ backgroundColor: 'rgba(120, 53, 15, 0.92)',
35
+ borderWidth: 2,
36
+ borderColor: '#facc15',
37
+ }}
38
+ >
39
+ <Text
40
+ style={{
41
+ textAlign: 'center',
42
+ fontSize: 14,
43
+ fontWeight: '600',
44
+ color: '#fef3c7',
45
+ }}
46
+ >
47
+ {pickHint}
48
+ </Text>
49
+ </View>
50
+ );
51
+ }
52
+
53
+ const speedLimit = state.speedLimitKmh;
54
+ const showSpeed = typeof speedLimit === 'number' && Number.isFinite(speedLimit) && speedLimit > 0;
55
+
56
+ return (
57
+ <View
58
+ pointerEvents="none"
59
+ style={{
60
+ position: 'absolute',
61
+ top: 12,
62
+ left: 12,
63
+ right: 12,
64
+ flexDirection: 'row',
65
+ alignItems: 'center',
66
+ gap: 12,
67
+ padding: 14,
68
+ borderRadius: 14,
69
+ backgroundColor: theme.background,
70
+ shadowColor: '#0f172a',
71
+ shadowOpacity: 0.32,
72
+ shadowRadius: 24,
73
+ shadowOffset: { width: 0, height: 10 },
74
+ elevation: 6,
75
+ }}
76
+ >
77
+ <View
78
+ style={{
79
+ width: 44,
80
+ height: 44,
81
+ borderRadius: 12,
82
+ backgroundColor: theme.iconBackground,
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ }}
86
+ >
87
+ {typeof state.sign === 'number' ? (
88
+ <NavigatorManeuverIcon sign={state.sign} color={theme.iconForeground} />
89
+ ) : null}
90
+ </View>
91
+
92
+ <View style={{ flex: 1, minWidth: 0, gap: 2 }}>
93
+ {state.distanceLabel ? (
94
+ <Text
95
+ numberOfLines={1}
96
+ style={{
97
+ fontSize: 18,
98
+ fontWeight: '700',
99
+ lineHeight: 20,
100
+ color: theme.foreground,
101
+ }}
102
+ >
103
+ {state.distanceLabel}
104
+ </Text>
105
+ ) : null}
106
+
107
+ {state.maneuverText ? (
108
+ <Text
109
+ numberOfLines={1}
110
+ style={{ fontSize: 14, fontWeight: '500', color: theme.subtle }}
111
+ >
112
+ {state.maneuverText}
113
+ </Text>
114
+ ) : null}
115
+
116
+ {state.streetName ? (
117
+ <Text numberOfLines={1} style={{ fontSize: 12, color: theme.muted }}>
118
+ {state.streetName}
119
+ </Text>
120
+ ) : null}
121
+
122
+ {state.summaryText ? (
123
+ <Text
124
+ style={{
125
+ marginTop: 4,
126
+ paddingTop: 6,
127
+ borderTopWidth: 1,
128
+ borderTopColor: theme.divider,
129
+ fontSize: 12,
130
+ fontWeight: '600',
131
+ color: theme.summary,
132
+ lineHeight: 16,
133
+ }}
134
+ >
135
+ {state.summaryText}
136
+ </Text>
137
+ ) : null}
138
+
139
+ {state.extrasText ? (
140
+ <Text style={{ marginTop: 2, fontSize: 11, fontWeight: '500', color: theme.muted }}>
141
+ {state.extrasText}
142
+ </Text>
143
+ ) : null}
144
+ </View>
145
+
146
+ {showSpeed ? (
147
+ <View
148
+ style={{
149
+ width: 52,
150
+ height: 52,
151
+ borderRadius: 26,
152
+ borderWidth: 3,
153
+ borderColor: '#dc2626',
154
+ backgroundColor: '#ffffff',
155
+ alignItems: 'center',
156
+ justifyContent: 'center',
157
+ }}
158
+ >
159
+ <Text style={{ fontSize: 14, fontWeight: '800', color: '#0f172a' }}>
160
+ {Math.round(speedLimit as number)}
161
+ </Text>
162
+ </View>
163
+ ) : null}
164
+ </View>
165
+ );
166
+ }
@@ -0,0 +1,45 @@
1
+ import { Pressable } from 'react-native';
2
+
3
+ import { NavigatorRecenterIcon } from './navigatorManeuverIcon';
4
+ import type { NavigatorChromeTheme } from './navigatorChromeTheme';
5
+
6
+ /**
7
+ * FAB «вернуть камеру на положение». При нажатии шлёт команду
8
+ * `recenterNavigatorCamera` в WebView — навигатор делает recenter.
9
+ */
10
+ export function NavigatorRecenterButton({
11
+ theme,
12
+ accessibilityLabel,
13
+ onPress,
14
+ }: {
15
+ theme: NavigatorChromeTheme;
16
+ accessibilityLabel: string;
17
+ onPress: () => void;
18
+ }) {
19
+ return (
20
+ <Pressable
21
+ accessibilityRole="button"
22
+ accessibilityLabel={accessibilityLabel}
23
+ onPress={onPress}
24
+ style={({ pressed }) => ({
25
+ position: 'absolute',
26
+ right: 16,
27
+ bottom: 16,
28
+ width: 56,
29
+ height: 56,
30
+ borderRadius: 28,
31
+ alignItems: 'center',
32
+ justifyContent: 'center',
33
+ backgroundColor: theme.background,
34
+ shadowColor: '#0f172a',
35
+ shadowOpacity: 0.32,
36
+ shadowRadius: 24,
37
+ shadowOffset: { width: 0, height: 10 },
38
+ elevation: 6,
39
+ transform: [{ scale: pressed ? 0.96 : 1 }],
40
+ })}
41
+ >
42
+ <NavigatorRecenterIcon color={theme.iconForeground} />
43
+ </Pressable>
44
+ );
45
+ }