wovvmap-webview-bridge 1.0.29 → 1.1.0

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 (74) hide show
  1. package/README.md +1644 -226
  2. package/dist/handlers/WebBridgeHandlers.d.ts +3 -15
  3. package/dist/handlers/WebBridgeHandlers.d.ts.map +1 -1
  4. package/dist/handlers/WebBridgeHandlers.js +94 -53
  5. package/dist/index.d.ts +19 -3
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +19 -3
  8. package/dist/types/types.d.ts +278 -316
  9. package/dist/types/types.d.ts.map +1 -1
  10. package/dist/web.d.ts +19 -3
  11. package/dist/web.d.ts.map +1 -1
  12. package/dist/web.js +19 -3
  13. package/dist/webviewBridge/BridgeService.d.ts +34 -17
  14. package/dist/webviewBridge/BridgeService.d.ts.map +1 -1
  15. package/dist/webviewBridge/BridgeService.js +227 -51
  16. package/dist/webviewBridge/WebViewBridgeRef.d.ts +3 -3
  17. package/dist/webviewBridge/WebViewBridgeRef.d.ts.map +1 -1
  18. package/dist/webviewBridge/WebViewBridgeRef.js +1 -1
  19. package/dist/webviewBridge/WebViewScreen.d.ts.map +1 -1
  20. package/dist/webviewBridge/services/DirectionBridgeService.d.ts +32 -0
  21. package/dist/webviewBridge/services/DirectionBridgeService.d.ts.map +1 -0
  22. package/dist/webviewBridge/services/DirectionBridgeService.js +72 -0
  23. package/dist/webviewBridge/services/FloorBridgeService.d.ts +4 -0
  24. package/dist/webviewBridge/services/FloorBridgeService.d.ts.map +1 -0
  25. package/dist/webviewBridge/services/FloorBridgeService.js +11 -0
  26. package/dist/webviewBridge/services/SelectionBridgeService.d.ts +10 -0
  27. package/dist/webviewBridge/services/SelectionBridgeService.d.ts.map +1 -0
  28. package/dist/webviewBridge/services/SelectionBridgeService.js +43 -0
  29. package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts +7 -0
  30. package/dist/webviewBridge/services/StepNavigationBridgeService.d.ts.map +1 -0
  31. package/dist/webviewBridge/services/StepNavigationBridgeService.js +15 -0
  32. package/dist/webviewBridge/services/ViewerBridgeService.d.ts +8 -0
  33. package/dist/webviewBridge/services/ViewerBridgeService.d.ts.map +1 -0
  34. package/dist/webviewBridge/services/ViewerBridgeService.js +20 -0
  35. package/dist/webviewBridge/services/ZoomBridgeService.d.ts +5 -0
  36. package/dist/webviewBridge/services/ZoomBridgeService.d.ts.map +1 -0
  37. package/dist/webviewBridge/services/ZoomBridgeService.js +9 -0
  38. package/dist/webviewBridge/store/useAmenityStore.d.ts +20 -0
  39. package/dist/webviewBridge/store/useAmenityStore.d.ts.map +1 -0
  40. package/dist/webviewBridge/store/useAmenityStore.js +6 -0
  41. package/dist/webviewBridge/store/useBridgeStorage.d.ts +14 -0
  42. package/dist/webviewBridge/store/useBridgeStorage.d.ts.map +1 -0
  43. package/dist/webviewBridge/store/useBridgeStorage.js +13 -0
  44. package/dist/webviewBridge/store/useCategoryStore.d.ts +20 -0
  45. package/dist/webviewBridge/store/useCategoryStore.d.ts.map +1 -0
  46. package/dist/webviewBridge/store/useCategoryStore.js +6 -0
  47. package/dist/webviewBridge/store/useDirectionStore.d.ts +55 -0
  48. package/dist/webviewBridge/store/useDirectionStore.d.ts.map +1 -0
  49. package/dist/webviewBridge/store/useDirectionStore.js +35 -0
  50. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts +14 -0
  51. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.d.ts.map +1 -0
  52. package/dist/webviewBridge/store/useDirectionsRouteRequestStore.js +25 -0
  53. package/dist/webviewBridge/store/useFloorStore.d.ts +18 -0
  54. package/dist/webviewBridge/store/useFloorStore.d.ts.map +1 -0
  55. package/dist/webviewBridge/store/useFloorStore.js +8 -0
  56. package/dist/webviewBridge/store/useNavigationStore.d.ts +23 -0
  57. package/dist/webviewBridge/store/useNavigationStore.d.ts.map +1 -0
  58. package/dist/webviewBridge/store/useNavigationStore.js +18 -0
  59. package/dist/webviewBridge/store/useNodePointStore.d.ts +42 -0
  60. package/dist/webviewBridge/store/useNodePointStore.d.ts.map +1 -0
  61. package/dist/webviewBridge/store/useNodePointStore.js +19 -0
  62. package/dist/webviewBridge/store/useSelectionStore.d.ts +35 -0
  63. package/dist/webviewBridge/store/useSelectionStore.d.ts.map +1 -0
  64. package/dist/webviewBridge/store/useSelectionStore.js +32 -0
  65. package/dist/webviewBridge/store/useSubCategoryStore.d.ts +20 -0
  66. package/dist/webviewBridge/store/useSubCategoryStore.d.ts.map +1 -0
  67. package/dist/webviewBridge/store/useSubCategoryStore.js +6 -0
  68. package/dist/webviewBridge/store/useViewerStore.d.ts +37 -0
  69. package/dist/webviewBridge/store/useViewerStore.d.ts.map +1 -0
  70. package/dist/webviewBridge/store/useViewerStore.js +41 -0
  71. package/dist/webviewBridge/useBridgeStorage.d.ts +45 -0
  72. package/dist/webviewBridge/useBridgeStorage.d.ts.map +1 -0
  73. package/dist/webviewBridge/useBridgeStorage.js +43 -0
  74. package/package.json +1 -1
package/README.md CHANGED
@@ -1,289 +1,1707 @@
1
1
  # wovvmap-webview-bridge
2
2
 
3
- A typed bridge between React Native and a WebView for Wovvmap maps. It provides:
4
- - WebViewScreen wrapper for React Native WebView
5
- - BridgeService helpers to send events to the WebView
6
- - Zustand store (useBridgeStorage) that keeps incoming state in sync
7
- - Strongly typed message contracts
3
+ `wovvmap-webview-bridge` is the host-side SDK for embedding a WovvMap viewer inside a web iframe or a React Native WebView.
4
+
5
+ It gives the host app:
6
+
7
+ - iframe/WebView components
8
+ - typed bridge command services
9
+ - synced Zustand stores for viewer data
10
+ - event handlers for viewer interactions
11
+ - TypeScript types for categories, stores, directions, floors, navigation, and viewer configuration
12
+
13
+ The package is designed so a host developer does not need to create bridge command services manually. UI code should call the exported `*BridgeService` classes and read state from the exported stores.
14
+
15
+ ## Entry Points
16
+
17
+ Use the web entry point in React web apps:
18
+
19
+ ```ts
20
+ import {
21
+ WebIframeScreen,
22
+ DirectionBridgeService,
23
+ useCategoryStore,
24
+ } from "wovvmap-webview-bridge/web";
25
+ ```
26
+
27
+ Use the root entry point in React Native apps:
28
+
29
+ ```ts
30
+ import {
31
+ WebViewScreen,
32
+ DirectionBridgeService,
33
+ useCategoryStore,
34
+ } from "wovvmap-webview-bridge";
35
+ ```
8
36
 
9
37
  ## Installation
10
38
 
39
+ For web iframe hosts:
40
+
11
41
  ```bash
12
- npm install react-native-webview zustand
13
- # or
14
- yarn add react-native-webview zustand
42
+ npm install wovvmap-webview-bridge zustand
15
43
  ```
16
44
 
17
- ## Quick start
45
+ For React Native hosts:
46
+
47
+ ```bash
48
+ npm install wovvmap-webview-bridge zustand react-native-webview
49
+ ```
18
50
 
19
- ### Web (React TS) usage
51
+ ## Basic Web Iframe Setup
20
52
 
21
- Use the web entrypoint so you don't need `react-native` or `react-native-webview` in your web app.
53
+ The host app owns the map id. The package does not store `mapId` because each host decides which map to open.
54
+ The embedded viewer URL must use `template=embedded`. If the host app has its own URL state, keep the host template separate, for example `/websdk?template=kiosk`, and pass `template=embedded` only to the iframe/WebView URL.
22
55
 
23
56
  ```tsx
24
- import React from "react";
25
57
  import { WebIframeScreen } from "wovvmap-webview-bridge/web";
26
58
 
27
- export default function App() {
59
+ export function MapHost({ mapId }: { mapId: string | null }) {
60
+ if (!mapId) {
61
+ return null;
62
+ }
63
+
28
64
  return (
29
65
  <WebIframeScreen
30
- url="https://your-map-app-url.com"
31
- origin="https://your-map-app-url.com"
32
- onload={() => console.log("iframe loaded")}
33
- style={{ width: "100%", height: "100vh" }}
66
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
67
+ origin="https://viewer.example.com"
68
+ onload={() => console.log("Viewer iframe loaded")}
69
+ style={{ width: "100%", height: "100%", border: "none" }}
34
70
  />
35
71
  );
36
72
  }
37
73
  ```
38
74
 
39
- You can also attach to your own iframe ref:
75
+ For local development this can look like:
76
+
77
+ ```tsx
78
+ <WebIframeScreen
79
+ url={`http://localhost:5174/viewer/${mapId}?v=v2&template=embedded`}
80
+ origin="http://localhost:5174"
81
+ style={{ width: "100%", height: "100%", border: "none" }}
82
+ />
83
+ ```
84
+
85
+ ## Recommended Ready Gate
86
+
87
+ For kiosk demos and production embeds, keep the host chrome hidden until the embedded viewer has sent enough state to render the real mall UI. This avoids showing placeholder titles such as "Web SDK Demo" before the mall name arrives.
40
88
 
41
89
  ```tsx
42
- import React, { useEffect, useRef } from "react";
43
90
  import {
44
- attachIframeBridge,
45
- registerBridgeHandler,
46
- sendStartPointToBridge,
91
+ WebIframeScreen,
92
+ useBridgeStorage,
93
+ useFloorStore,
94
+ useNodePointStore,
95
+ useViewerStore,
47
96
  } from "wovvmap-webview-bridge/web";
48
97
 
49
- export default function App() {
98
+ export function KioskHost({ mapId }: { mapId: string }) {
99
+ const mallName = useViewerStore((s) => s.mallName);
100
+ const isMapParsed = useViewerStore((s) => s.isMapParsed);
101
+ const isLoading = useViewerStore((s) => s.isLoading);
102
+ const isBridgeLoaded = useBridgeStorage((s) => s.isBridgeLoaded);
103
+ const isMapLoaded = useBridgeStorage((s) => s.isMapLoaded);
104
+ const floorCount = useFloorStore((s) => s.floors.length);
105
+ const nodeCount = useNodePointStore((s) => s.searchableNodePointMap.length);
106
+
107
+ const isReady =
108
+ Boolean(mallName) &&
109
+ isBridgeLoaded &&
110
+ isMapLoaded &&
111
+ isMapParsed &&
112
+ !isLoading &&
113
+ floorCount > 0 &&
114
+ nodeCount > 0;
115
+
116
+ return (
117
+ <div className="host-shell">
118
+ {isReady && <Header title={`Welcome to ${mallName}`} />}
119
+
120
+ <WebIframeScreen
121
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
122
+ origin="https://viewer.example.com"
123
+ />
124
+
125
+ {isReady && <Footer />}
126
+ {!isReady && <LoadingOverlay />}
127
+ </div>
128
+ );
129
+ }
130
+ ```
131
+
132
+ ## Demo Recording Mode
133
+
134
+ The sample web host can expose a demo URL such as:
135
+
136
+ ```txt
137
+ http://localhost:5173/websdk?demo=1&resetDefault=1
138
+ ```
139
+
140
+ Recommended demo parameters:
141
+
142
+ - `demo=1`: shows labeled walkthrough cards and runs a guided feature sequence.
143
+ - `resetDefault=1`: clears the saved default location so the default-location selection flow can be demonstrated.
144
+ - `mapId=<id>`: optionally chooses the mall map to open.
145
+ - `template=kiosk`: host template for the SDK wrapper.
146
+
147
+ The embedded viewer URL generated by the host must still use `template=embedded`; this parameter belongs only inside the iframe/WebView URL.
148
+
149
+ Suggested video sequence:
150
+
151
+ 1. Web SDK channel URL and embedded viewer load.
152
+ 2. Mall-name header after real map data is ready.
153
+ 3. Default location selection.
154
+ 4. Search store results.
155
+ 5. Store detail card.
156
+ 6. Category and subcategory browsing.
157
+ 7. Amenities and offers.
158
+ 8. Origin/destination directions.
159
+ 9. `pathVisible=true` active route.
160
+ 10. Step-by-step guidance.
161
+ 11. Reverse directions / swap endpoints.
162
+ 12. Floor navigation.
163
+ 13. Zoom, 2D/3D, street/top view, and text controls.
164
+
165
+ ## Basic React Native Setup
166
+
167
+ ```tsx
168
+ import { WebViewScreen } from "wovvmap-webview-bridge";
169
+
170
+ export function MapWebView({ mapId }: { mapId: string }) {
171
+ return (
172
+ <WebViewScreen
173
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
174
+ onload={() => console.log("Viewer loaded")}
175
+ />
176
+ );
177
+ }
178
+ ```
179
+
180
+ ## Optional Manual Iframe Setup
181
+
182
+ Use `attachIframeBridge` when you already render your own iframe.
183
+
184
+ ```tsx
185
+ import { useEffect, useRef } from "react";
186
+ import { attachIframeBridge } from "wovvmap-webview-bridge/web";
187
+
188
+ export function CustomIframe({ url }: { url: string }) {
50
189
  const iframeRef = useRef<HTMLIFrameElement>(null);
51
190
 
52
191
  useEffect(() => {
53
192
  if (!iframeRef.current) return;
54
193
 
55
- const cleanup = attachIframeBridge({
194
+ return attachIframeBridge({
56
195
  iframe: iframeRef.current,
57
- origin: "https://your-map-app-url.com",
58
- onLoad: () => sendStartPointToBridge("A1"),
196
+ origin: "https://viewer.example.com",
197
+ onLoad: () => console.log("loaded"),
59
198
  });
199
+ }, []);
60
200
 
61
- registerBridgeHandler("isSceneClick", (payload) => {
62
- console.log("Scene clicked", payload);
63
- });
201
+ return <iframe ref={iframeRef} src={url} title="Wovv Map" />;
202
+ }
203
+ ```
64
204
 
65
- return cleanup;
66
- }, []);
205
+ ## Recommended Import Surface
206
+
207
+ Most apps should import only from `"wovvmap-webview-bridge/web"` or `"wovvmap-webview-bridge"`.
208
+
209
+ ```ts
210
+ import {
211
+ WebIframeScreen,
212
+ DirectionBridgeService,
213
+ SelectionBridgeService,
214
+ FloorBridgeService,
215
+ StepNavigationBridgeService,
216
+ ZoomBridgeService,
217
+ ViewerBridgeService,
218
+ useBridgeStorage,
219
+ useViewerStore,
220
+ useCategoryStore,
221
+ useSubCategoryStore,
222
+ useAmenityStore,
223
+ useNodePointStore,
224
+ useSelectionStore,
225
+ useDirectionStore,
226
+ useFloorStore,
227
+ useNavigationStore,
228
+ useDirectionsRouteRequestStore,
229
+ registerBridgeHandler,
230
+ } from "wovvmap-webview-bridge/web";
231
+ ```
232
+
233
+ ## Exported Components
234
+
235
+ ### `WebIframeScreen`
236
+
237
+ Web-only component for embedding the WovvMap viewer in an iframe.
238
+
239
+ Props:
240
+
241
+ ```ts
242
+ type WebIframeScreenProps = {
243
+ url: string;
244
+ origin?: string;
245
+ onload?: () => void;
246
+ title?: string;
247
+ style?: React.CSSProperties;
248
+ };
249
+ ```
250
+
251
+ Use this in React web apps.
252
+
253
+ ### `WebViewScreen`
254
+
255
+ React Native component for embedding the WovvMap viewer in `react-native-webview`.
256
+
257
+ Props:
258
+
259
+ ```ts
260
+ type WebViewScreenProps = {
261
+ url: string;
262
+ onload?: () => void;
263
+ };
264
+ ```
265
+
266
+ Use this in React Native apps.
267
+
268
+ ### `attachIframeBridge`
269
+
270
+ Manual bridge attachment for custom iframe implementations.
271
+
272
+ ```ts
273
+ attachIframeBridge({
274
+ iframe,
275
+ origin,
276
+ onLoad,
277
+ });
278
+ ```
279
+
280
+ ## Command Services
281
+
282
+ Use these services from host UI. They update host-side package stores when needed and send the correct bridge event to the embedded viewer.
283
+
284
+ ### `DirectionBridgeService`
285
+
286
+ Controls origin, destination, route generation, path clearing, and origin/destination swap.
287
+
288
+ Methods:
289
+
290
+ ```ts
291
+ DirectionBridgeService.configure(config);
292
+ DirectionBridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
293
+ DirectionBridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
294
+ DirectionBridgeService.clearDestination(clearQuery);
295
+ DirectionBridgeService.clearOrigin();
296
+ DirectionBridgeService.swapOriginDestination();
297
+ DirectionBridgeService.generateDirectionsRoute(callbacks);
298
+ DirectionBridgeService.clearActivePath();
299
+ ```
300
+
301
+ Parameters:
302
+
303
+ - `nodeId`: a node key string. Multiple store locations can be joined with `"<->"`, for example `"2105,1601,3<->3166,1570,3"`.
304
+ - `cameraAnimate`: when `true`, viewer camera animates to the point.
305
+ - `clearSelection`: when `true`, viewer clears active category/subcategory/amenity selection.
306
+ - `clearQuery`: when `true`, your host input adapter clears the visible input text.
307
+
308
+ Basic destination example:
309
+
310
+ ```ts
311
+ DirectionBridgeService.setDestination("2105,1601,3", true, false);
312
+ ```
313
+
314
+ Basic origin example:
315
+
316
+ ```ts
317
+ DirectionBridgeService.setOrigin("4360,2330,5", true, true);
318
+ ```
319
+
320
+ Clear destination:
321
+
322
+ ```ts
323
+ DirectionBridgeService.clearDestination(true);
324
+ ```
325
+
326
+ Generate route:
327
+
328
+ ```ts
329
+ DirectionBridgeService.generateDirectionsRoute({
330
+ onSuccess: (result) => {
331
+ console.log("Route ready", result.navigationResult);
332
+ },
333
+ onError: (result) => {
334
+ console.log("Route failed", result.error);
335
+ },
336
+ });
337
+ ```
338
+
339
+ Swap from/to:
340
+
341
+ ```ts
342
+ DirectionBridgeService.swapOriginDestination();
343
+ ```
344
+
345
+ Clear drawn path:
346
+
347
+ ```ts
348
+ DirectionBridgeService.clearActivePath();
349
+ ```
350
+
351
+ #### Direction Input Adapter
352
+
353
+ The package does not own your input UI state. If your app has local "From" and "To" input text, connect it once with `configure`.
354
+
355
+ ```ts
356
+ import {
357
+ DirectionBridgeService,
358
+ SelectionBridgeService,
359
+ useNodePointStore,
360
+ } from "wovvmap-webview-bridge/web";
361
+ import { useDirectionInputStore } from "./store/useDirectionInputStore";
362
+
363
+ DirectionBridgeService.configure({
364
+ directionInputAdapter: {
365
+ setDestinationState: (query, selectedId) => {
366
+ useDirectionInputStore.getState().setDestinationState(query, selectedId);
367
+ },
368
+ setOriginState: (query, selectedId) => {
369
+ useDirectionInputStore.getState().setOriginState(query, selectedId);
370
+ },
371
+ clearDestinationState: (clearQuery) => {
372
+ useDirectionInputStore.getState().clearDestinationState(clearQuery);
373
+ },
374
+ clearOriginState: () => {
375
+ useDirectionInputStore.getState().clearOriginState();
376
+ },
377
+ setActiveDirectionField: (field) => {
378
+ useDirectionInputStore.getState().setActiveDirectionField(field);
379
+ },
380
+ getOriginQuery: () => useDirectionInputStore.getState().originQuery,
381
+ getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
382
+ },
383
+ resolveLocationName: (nodeId) => {
384
+ const nodeKey = nodeId.split("<->")[0];
385
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
386
+ return node?.assets?.locationName?.text || nodeKey;
387
+ },
388
+ onClearSelection: () => {
389
+ SelectionBridgeService.clearSelection();
390
+ },
391
+ });
392
+ ```
393
+
394
+ Why this exists:
395
+
396
+ - different companies can build different input UIs
397
+ - the SDK still updates local input text when `setOrigin`, `setDestination`, or `swapOriginDestination` is called
398
+ - SDK code does not import app-specific stores
399
+
400
+ If you do not configure an adapter, direction commands still work. Only local input text will not auto-update.
401
+
402
+ ### `SelectionBridgeService`
403
+
404
+ Controls category, subcategory, amenity, offers, and clear selection.
405
+
406
+ Methods:
407
+
408
+ ```ts
409
+ SelectionBridgeService.selectCategory(category);
410
+ SelectionBridgeService.selectSubCategory(subCategory);
411
+ SelectionBridgeService.selectAmenity(amenity);
412
+ SelectionBridgeService.toggleOffers();
413
+ SelectionBridgeService.clearSelection();
414
+ SelectionBridgeService.syncHighlightsWithState();
415
+ ```
416
+
417
+ Category example:
418
+
419
+ ```tsx
420
+ import { SelectionBridgeService, useCategoryStore } from "wovvmap-webview-bridge/web";
421
+
422
+ function CategoryButton({ code }: { code: string }) {
423
+ const category = useCategoryStore((s) => s.categories[code]);
424
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
425
+
426
+ if (!category) return null;
427
+
428
+ const isActive =
429
+ activeSelection.type === "Category" &&
430
+ activeSelection.id === category.code.toString();
67
431
 
68
432
  return (
69
- <iframe
70
- ref={iframeRef}
71
- src="https://your-map-app-url.com"
72
- style={{ width: "100%", height: "100%", border: 0 }}
73
- title="Wovv Map"
74
- />
433
+ <button
434
+ className={isActive ? "active" : ""}
435
+ onClick={() => SelectionBridgeService.selectCategory(category)}
436
+ >
437
+ {category.name}
438
+ </button>
75
439
  );
76
440
  }
77
441
  ```
78
442
 
79
- ### 1) Render the WebView
443
+ Subcategory example:
80
444
 
81
445
  ```tsx
82
- import React from "react";
83
- import { WebViewScreen } from "wovvmap-webview-bridge";
446
+ SelectionBridgeService.selectSubCategory(subCategory);
447
+ ```
448
+
449
+ Amenity example:
450
+
451
+ ```tsx
452
+ SelectionBridgeService.selectAmenity(amenity);
453
+ ```
454
+
455
+ Amenity selection also sets the destination with the amenity node keys:
84
456
 
85
- export default function App() {
86
- return <WebViewScreen url="https://your-map-app-url.com" />;
457
+ ```ts
458
+ DirectionBridgeService.setDestination(amenity.nodePointsKey.join("<->"), true, false);
459
+ ```
460
+
461
+ Offers button example:
462
+
463
+ ```tsx
464
+ import { SelectionBridgeService, useNodePointStore, useSelectionStore } from "wovvmap-webview-bridge/web";
465
+
466
+ export function OffersButton() {
467
+ const offersNodeMap = useNodePointStore((s) => s.offersNodeMap);
468
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
469
+ const hasOffers = (offersNodeMap.all_offers?.nodePointsKey?.length ?? 0) > 0;
470
+
471
+ if (!hasOffers) return null;
472
+
473
+ return (
474
+ <button
475
+ className={isOfferSelected ? "active" : ""}
476
+ onClick={() => SelectionBridgeService.toggleOffers()}
477
+ >
478
+ Offers
479
+ </button>
480
+ );
87
481
  }
88
482
  ```
89
483
 
90
- ### 2) Send events to the WebView (RN -> Web)
484
+ Important selection states:
485
+
486
+ - Category active: `useSelectionStore((s) => s.activeSelection.type === "Category")`
487
+ - Selected category id: `useSelectionStore((s) => s.activeSelection.id)`
488
+ - Active category object: `useSelectionStore((s) => s.activeCategory)`
489
+ - Subcategory active: `activeSelection.type === "SubCategory"`
490
+ - Amenity active: `activeSelection.type === "Amenity"`
491
+ - Offers active: `useSelectionStore((s) => s.isOfferSelected)`
492
+
493
+ ### `FloorBridgeService`
494
+
495
+ Controls active floor.
496
+
497
+ ```ts
498
+ FloorBridgeService.changeFloor(floorIndex, animateCamera);
499
+ ```
500
+
501
+ Floor selector example:
91
502
 
92
503
  ```tsx
93
- import {
94
- sendStartPointToBridge,
95
- sendEndPointToBridge,
96
- sendActiveFloorToBridge,
97
- sendPathNextBtnClick,
98
- sendPathPreBtnClick,
99
- sendPathFinishBtnClick,
100
- sendSelectCategory,
101
- sendZoomIn,
102
- sendZoomOut,
103
- sendClearStartAndEndPoint,
104
- sendPathFilter,
105
- sendGetDirectionToBridge,
106
- sendNavigateToBridge,
107
- sendMapThemeToBridge,
108
- sendEditableViewToBridge,
109
- sendSensorDataToBridge,
110
- } from "wovvmap-webview-bridge";
504
+ import { FloorBridgeService, useFloorStore } from "wovvmap-webview-bridge/web";
505
+
506
+ export function FloorSelector() {
507
+ const floors = useFloorStore((s) => s.floors);
508
+ const activeFloor = useFloorStore((s) => s.activeFloor);
111
509
 
112
- sendStartPointToBridge("A1");
113
- sendEndPointToBridge("B5");
114
- sendActiveFloorToBridge(2);
115
- sendPathNextBtnClick();
116
- sendPathPreBtnClick();
117
- sendPathFinishBtnClick();
118
- sendSelectCategory(["Food", "Fashion"]);
119
- sendZoomIn();
120
- sendZoomOut();
121
- sendClearStartAndEndPoint();
122
- sendPathFilter();
123
- sendGetDirectionToBridge();
124
- sendNavigateToBridge("map-id-123");
125
- sendMapThemeToBridge({ "theme-path-color": "#00AAFF" });
126
- sendEditableViewToBridge(mapDataExportObj);
127
- sendSensorDataToBridge({ timestamp: Date.now(), accelerometerSignal: {x: 0, y: 0, z: 0} });
510
+ return (
511
+ <select
512
+ value={activeFloor ?? ""}
513
+ onChange={(event) => FloorBridgeService.changeFloor(Number(event.target.value), true)}
514
+ >
515
+ {floors.map((floor) => (
516
+ <option key={floor.index ?? floor.FloorNumber} value={floor.index}>
517
+ {floor.FloorName}
518
+ </option>
519
+ ))}
520
+ </select>
521
+ );
522
+ }
128
523
  ```
129
524
 
130
- ### 3) Handle events from the WebView (Web -> RN)
525
+ ### `StepNavigationBridgeService`
526
+
527
+ Controls current step in step-by-step navigation.
528
+
529
+ ```ts
530
+ StepNavigationBridgeService.goToStep(index);
531
+ StepNavigationBridgeService.nextStep();
532
+ StepNavigationBridgeService.prevStep();
533
+ StepNavigationBridgeService.finishStep();
534
+ ```
131
535
 
132
- You can register handlers for click events. All other events are stored in the Zustand store.
536
+ Step list example:
133
537
 
134
538
  ```tsx
135
- import { registerBridgeHandler } from "wovvmap-webview-bridge";
539
+ import { StepNavigationBridgeService, useNavigationStore } from "wovvmap-webview-bridge/web";
136
540
 
137
- registerBridgeHandler("isSceneClick", (payload) => {
138
- console.log("Scene clicked", payload);
139
- });
541
+ export function StepList() {
542
+ const result = useNavigationStore((s) => s.navigationResult);
543
+ const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
140
544
 
141
- registerBridgeHandler("isShapeClick", (payload) => {
142
- console.log("Shape clicked", payload.value);
143
- });
545
+ if (!result) return null;
546
+
547
+ return (
548
+ <div>
549
+ {result.steps.map((step, index) => (
550
+ <button
551
+ key={step.id}
552
+ className={currentStepIndex === index ? "active" : ""}
553
+ onClick={() => StepNavigationBridgeService.goToStep(index)}
554
+ >
555
+ {step.instruction}
556
+ </button>
557
+ ))}
558
+ </div>
559
+ );
560
+ }
144
561
  ```
145
562
 
146
- ### 4) Read synced state from the store
147
-
148
- ```tsx
149
- import { useBridgeStorage } from "wovvmap-webview-bridge";
150
-
151
- const state = useBridgeStorage((s) => ({
152
- isBridgeLoaded: s.isBridgeLoaded,
153
- isMapLoaded: s.isMapLoaded,
154
- searchablePoints: s.searchablePoints,
155
- amenities: s.amenities,
156
- allOffers: s.allOffers,
157
- activeFloor: s.activeFloor,
158
- stepByStepList: s.stepByStepList,
159
- pathSummary: s.pathSummary,
160
- categories: s.categories,
161
- subCategories: s.subCategories,
162
- }));
163
- ```
164
-
165
- Store fields:
166
- - isBridgeLoaded
167
- - isMapLoaded
168
- - searchablePoints
169
- - amenities
170
- - allOffers
171
- - activeFloor
172
- - elevator, escalator
173
- - floorImages
174
- - stepByStepList
175
- - pathSummary
176
- - nextPreState
177
- - pointsByKey
178
- - categories
179
- - subCategories
180
- - cameraControllerState
181
-
182
- ## Bridge message contracts
183
-
184
- ### IncomingMessage (Web -> RN)
185
- Keys and payloads:
186
- - pong: boolean
187
- - isConnection: boolean
188
- - mapLoaded: boolean
189
- - _searchablePoints: NodePoint[]
190
- - _allAmenities: AmenityWithNodePoint[]
191
- - _allOffers: string[]
192
- - _activeFloor: number
193
- - FloorImg: FloorImage[]
194
- - categories: Record<ExternalId, Category>
195
- - subCategories: Record<ExternalId, SubCategory>
196
- - isSceneClick: no value
197
- - isShapeClick: NodePoint
198
- - stepByStepList: StepByStepResult
199
- - pathNextPreState: NavState
200
- - cameraControllerState: { cameraPosition: CameraPosition; controlsPosition: ControlsPosition }
201
-
202
- ### OutgoingMessage (RN -> Web)
203
- Keys and payloads:
204
- - applyCSS: { selector: string; style: Partial<CSSStyleDeclaration> }
205
- - ping: boolean
206
- - setEndPoint: string
207
- - setStartPoint: string
208
- - setActiveFloor: number
209
- - pathNextBtnClick: no value
210
- - pathPreBtnClick: no value
211
- - pathFinishBtnClick: no value
212
- - setSelectCategory: string | string[] | null
213
- - zoomIn: no value
214
- - zoomOut: no value
215
- - clearStartAndEndPoint: no value
216
- - setPathFilter: filterPath
217
- - getDirection: no value
218
- - setMapTheme: Theme | null
219
- - navigateTo: string
220
- - editableView: MapApiResponse
221
- - pathHighlightByStepIndex: number
222
- - sensorData: SensorDataPayload
223
-
224
- ## Exported API
563
+ Next/previous example:
225
564
 
226
- ### Components
227
- - WebViewScreen
228
- - WebIframeScreen (web only)
229
-
230
- ### Bridge helpers
231
- - sendStartPointToBridge
232
- - sendEndPointToBridge
233
- - sendActiveFloorToBridge
234
- - sendPathNextBtnClick
235
- - sendPathPreBtnClick
236
- - sendPathFinishBtnClick
237
- - sendSelectCategory
238
- - sendZoomIn
239
- - sendZoomOut
240
- - sendClearStartAndEndPoint
241
- - sendGetDirectionToBridge
242
- - sendPathFilter
243
- - sendNavigateToBridge
244
- - sendMapThemeToBridge
245
- - sendEditableViewToBridge
246
- - sendSensorDataToBridge
565
+ ```tsx
566
+ <button onClick={() => StepNavigationBridgeService.prevStep()}>Previous</button>
567
+ <button onClick={() => StepNavigationBridgeService.nextStep()}>Next</button>
568
+ <button onClick={() => StepNavigationBridgeService.finishStep()}>Finish</button>
569
+ ```
247
570
 
248
- ### Handlers
249
- - registerBridgeHandler
571
+ ### `ZoomBridgeService`
250
572
 
251
- ### Store
252
- - useBridgeStorage (Zustand)
573
+ Controls viewer zoom.
253
574
 
254
- ### Types
255
- - IncomingMessage, OutgoingMessage
256
- - NodePoint
257
- - AmenityWithNodePoint
258
- - Category, SubCategory, ExternalId
259
- - Environment, DayHours, WeeklyHours
260
- - NodePointOffer, BrandInfo
261
- - MapExportContext, GeometryFloor, GeometryNodePoint, GeometryLayer
262
- - NodeMetadata, AssetPayloads
263
- - FloorImage
264
- - StepInstruction, PathSummary, StepByStepResult
265
- - NavState
266
- - filterPath
267
- - Theme
268
- - CameraPosition, ControlsPosition
269
- - MapApiResponse, MapDataExport
270
-
271
- ## File structure
272
-
273
- ```
274
- src/
275
- index.ts
276
- webviewBridge/
277
- WebViewScreen.tsx
278
- BridgeService.ts
279
- BridgeStorage.ts
280
- WebViewBridgeRef.ts
281
- handlers/
282
- WebBridgeHandlers.ts
283
- types/
284
- types.ts
285
- ```
286
-
287
- ## License
288
-
289
- MIT
575
+ ```ts
576
+ ZoomBridgeService.zoomIn();
577
+ ZoomBridgeService.zoomOut();
578
+ ```
579
+
580
+ Example:
581
+
582
+ ```tsx
583
+ <button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
584
+ <button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
585
+ ```
586
+
587
+ ### `ViewerBridgeService`
588
+
589
+ Controls viewer mode, camera view, and text rendering.
590
+
591
+ ```ts
592
+ ViewerBridgeService.setViewMode("2D", true);
593
+ ViewerBridgeService.setViewMode("3D", true);
594
+ ViewerBridgeService.setCameraView("street");
595
+ ViewerBridgeService.setCameraView("sky");
596
+ ViewerBridgeService.setTextType("2D");
597
+ ViewerBridgeService.setTextType("3D");
598
+ ViewerBridgeService.setShow2DIcon(true);
599
+ ViewerBridgeService.setShow2DIcon(false);
600
+ ```
601
+
602
+ Toolbar example:
603
+
604
+ ```tsx
605
+ import { ViewerBridgeService, useViewerStore } from "wovvmap-webview-bridge/web";
606
+
607
+ export function ViewerToolbar() {
608
+ const viewMode = useViewerStore((s) => s.viewMode);
609
+ const cameraView = useViewerStore((s) => s.cameraView);
610
+ const textType = useViewerStore((s) => s.textType);
611
+ const show2DIcon = useViewerStore((s) => s.show2DIcon);
612
+
613
+ return (
614
+ <div>
615
+ <button
616
+ className={viewMode === "2D" ? "active" : ""}
617
+ onClick={() => ViewerBridgeService.setViewMode("2D", true)}
618
+ >
619
+ 2D View
620
+ </button>
621
+ <button
622
+ className={viewMode === "3D" ? "active" : ""}
623
+ onClick={() => ViewerBridgeService.setViewMode("3D", true)}
624
+ >
625
+ 3D View
626
+ </button>
627
+ <button
628
+ className={cameraView === "street" ? "active" : ""}
629
+ onClick={() => ViewerBridgeService.setCameraView("street")}
630
+ >
631
+ Street
632
+ </button>
633
+ <button
634
+ className={cameraView === "sky" ? "active" : ""}
635
+ onClick={() => ViewerBridgeService.setCameraView("sky")}
636
+ >
637
+ Sky
638
+ </button>
639
+ <button
640
+ className={textType === "2D" ? "active" : ""}
641
+ onClick={() => ViewerBridgeService.setTextType("2D")}
642
+ >
643
+ 2D Text
644
+ </button>
645
+ <button
646
+ className={textType === "3D" ? "active" : ""}
647
+ onClick={() => ViewerBridgeService.setTextType("3D")}
648
+ >
649
+ 3D Text
650
+ </button>
651
+ <button
652
+ className={show2DIcon ? "active" : ""}
653
+ onClick={() => ViewerBridgeService.setShow2DIcon(!show2DIcon)}
654
+ >
655
+ 2D Icon
656
+ </button>
657
+ </div>
658
+ );
659
+ }
660
+ ```
661
+
662
+ ### `BridgeService`
663
+
664
+ Low-level event sender. Use command services above for normal UI code.
665
+
666
+ Available low-level methods:
667
+
668
+ ```ts
669
+ BridgeService.setViewerConfig(config);
670
+ BridgeService.setDestination(nodeId, cameraAnimate, clearSelection);
671
+ BridgeService.clearDestination();
672
+ BridgeService.setOrigin(nodeId, cameraAnimate, clearSelection);
673
+ BridgeService.clearOrigin();
674
+ BridgeService.swapOriginDestination();
675
+ BridgeService.setActiveSelection(type, code);
676
+ BridgeService.clearActiveSelection();
677
+ BridgeService.setOfferSelected(isSelected);
678
+ BridgeService.generateDirectionsRoute(callbacks);
679
+ BridgeService.clearActivePath();
680
+ BridgeService.changeFloor(floorIndex, animateCamera);
681
+ BridgeService.goToStep(index);
682
+ BridgeService.nextStep();
683
+ BridgeService.prevStep();
684
+ BridgeService.finishStep();
685
+ BridgeService.zoomIn();
686
+ BridgeService.zoomOut();
687
+ BridgeService.setViewMode(viewMode, animate);
688
+ BridgeService.setCameraView(cameraView);
689
+ BridgeService.setTextType(textType);
690
+ BridgeService.setShow2DIcon(show2DIcon);
691
+ ```
692
+
693
+ ## Synced Stores
694
+
695
+ All stores are Zustand stores. Use them directly in React components.
696
+
697
+ ### `useBridgeStorage`
698
+
699
+ Bridge and tenant state.
700
+
701
+ Fields:
702
+
703
+ - `isMapLoaded`: viewer sent map loaded state
704
+ - `isBridgeLoaded`: bridge connection event received
705
+ - `imageBaseUrl`: base URL for category, subcategory, amenity, and store images
706
+
707
+ Example:
708
+
709
+ ```tsx
710
+ const isReady = useBridgeStorage((s) => s.isBridgeLoaded && s.isMapLoaded);
711
+ const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
712
+ ```
713
+
714
+ ### `useViewerStore`
715
+
716
+ Viewer configuration and loading state.
717
+
718
+ Fields:
719
+
720
+ - `mallName`: mall name from the embedded viewer, or `null` before map data is ready
721
+ - `apiVersion`: viewer API version, for example `"v2"`
722
+ - `viewMode`: `"2D"` or `"3D"`
723
+ - `cameraView`: `"street"` or `"sky"`
724
+ - `textType`: `"2D"` or `"3D"`
725
+ - `show2DIcon`: boolean
726
+ - `isMapParsed`: true after viewer parsed map data
727
+ - `isLoading`: viewer loading flag
728
+
729
+ Example:
730
+
731
+ ```tsx
732
+ const mallName = useViewerStore((s) => s.mallName);
733
+ const viewMode = useViewerStore((s) => s.viewMode);
734
+ const cameraView = useViewerStore((s) => s.cameraView);
735
+ ```
736
+
737
+ ### `useCategoryStore`
738
+
739
+ Category catalog.
740
+
741
+ Fields:
742
+
743
+ - `categories`: `Record<string, Category>`
744
+
745
+ Example:
746
+
747
+ ```tsx
748
+ const categories = useCategoryStore((s) => Object.values(s.categories));
749
+ ```
750
+
751
+ Category active state:
752
+
753
+ ```tsx
754
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
755
+ const isActiveCategory =
756
+ activeSelection.type === "Category" &&
757
+ activeSelection.id === category.code.toString();
758
+ ```
759
+
760
+ ### `useSubCategoryStore`
761
+
762
+ Subcategory catalog.
763
+
764
+ Fields:
765
+
766
+ - `subCategories`: `Record<string, SubCategory>`
767
+
768
+ Example:
769
+
770
+ ```tsx
771
+ const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
772
+ ```
773
+
774
+ Subcategory active state:
775
+
776
+ ```tsx
777
+ const isActiveSubCategory =
778
+ activeSelection.type === "SubCategory" &&
779
+ activeSelection.id === subCategory.code.toString();
780
+ ```
781
+
782
+ ### `useAmenityStore`
783
+
784
+ Amenity catalog.
785
+
786
+ Fields:
787
+
788
+ - `amenities`: `Record<string, Amenity>`
789
+
790
+ Example:
791
+
792
+ ```tsx
793
+ const amenities = useAmenityStore((s) => Object.values(s.amenities));
794
+ ```
795
+
796
+ Amenity active state:
797
+
798
+ ```tsx
799
+ const isActiveAmenity =
800
+ activeSelection.type === "Amenity" &&
801
+ activeSelection.id === amenity.id.toString();
802
+ ```
803
+
804
+ ### `useNodePointStore`
805
+
806
+ Searchable stores/locations and node maps.
807
+
808
+ Fields:
809
+
810
+ - `searchableNodePointMap`: `NodePoint[]`
811
+ - `locationGroupedNodeMap`: `Record<string, string[]>`
812
+ - `keyNodeMap`: `Record<string, NodePoint>`
813
+ - `offersNodeMap`: `OffersNodeMap`
814
+
815
+ Actions/helpers:
816
+
817
+ - `getNodePointByKey(key)`
818
+ - `clearSearchableNodePoints()`
819
+
820
+ Example: search source data:
821
+
822
+ ```tsx
823
+ const nodePoints = useNodePointStore((s) => s.searchableNodePointMap);
824
+ ```
825
+
826
+ Example: get store name:
827
+
828
+ ```ts
829
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
830
+ const name = node?.assets?.locationName?.text || node?.key || "";
831
+ ```
832
+
833
+ Example: group multi-location store:
834
+
835
+ ```ts
836
+ const groupKeys = useNodePointStore.getState().locationGroupedNodeMap["Aldo"] || [];
837
+ DirectionBridgeService.setDestination(groupKeys.join("<->"), true, false);
838
+ ```
839
+
840
+ Example: offers data:
841
+
842
+ ```tsx
843
+ const allOffers = useNodePointStore((s) => s.offersNodeMap.all_offers);
844
+ const hasOffers = (allOffers?.nodePointsKey?.length ?? 0) > 0;
845
+ ```
846
+
847
+ ### `useSelectionStore`
848
+
849
+ Active category/subcategory/amenity and offers state.
850
+
851
+ Fields:
852
+
853
+ - `activeSelection`: `{ type: "Category" | "SubCategory" | "Amenity" | null; id: string | null }`
854
+ - `activeCategory`: `Category | null`
855
+ - `isOfferSelected`: boolean
856
+
857
+ Examples:
858
+
859
+ ```tsx
860
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
861
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
862
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
863
+ ```
864
+
865
+ Offer button active:
866
+
867
+ ```tsx
868
+ const isActive = useSelectionStore((s) => s.isOfferSelected);
869
+ ```
870
+
871
+ Category tab active:
872
+
873
+ ```tsx
874
+ const isActive =
875
+ activeSelection.type === "Category" &&
876
+ activeSelection.id === category.code.toString();
877
+ ```
878
+
879
+ Subcategory tab active:
880
+
881
+ ```tsx
882
+ const isActive =
883
+ activeSelection.type === "SubCategory" &&
884
+ activeSelection.id === subCategory.code.toString();
885
+ ```
886
+
887
+ Amenity active:
888
+
889
+ ```tsx
890
+ const isActive =
891
+ activeSelection.type === "Amenity" &&
892
+ activeSelection.id === amenity.id.toString();
893
+ ```
894
+
895
+ ### `useDirectionStore`
896
+
897
+ Origin/destination and pathfinding state mirrored from the viewer.
898
+
899
+ Fields:
900
+
901
+ - `selectedOriginPointId`: selected start node id or joined node ids
902
+ - `selectedDestinationPointId`: selected end node id or joined node ids
903
+ - `pathfindingError`: route error string or `null`
904
+ - `routingPreferences`: `{ elevator?: boolean; escalator?: boolean; stair?: boolean }`
905
+
906
+ Examples:
907
+
908
+ ```tsx
909
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
910
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
911
+ const pathfindingError = useDirectionStore((s) => s.pathfindingError);
912
+ const routingPreferences = useDirectionStore((s) => s.routingPreferences);
913
+ ```
914
+
915
+ Show route error:
916
+
917
+ ```tsx
918
+ {pathfindingError && <div>No Routes Available: {pathfindingError}</div>}
919
+ ```
920
+
921
+ Enable get directions button:
922
+
923
+ ```tsx
924
+ const canGenerateRoute = Boolean(origin && destination);
925
+ ```
926
+
927
+ Update routing preference from host:
928
+
929
+ ```tsx
930
+ useDirectionStore.getState().setRoutingPreferences({
931
+ elevator: true,
932
+ escalator: false,
933
+ });
934
+ ```
935
+
936
+ ### `useDirectionsRouteRequestStore`
937
+
938
+ Request state for `DirectionBridgeService.generateDirectionsRoute`.
939
+
940
+ Fields:
941
+
942
+ - `status`: `"idle" | "loading" | "success" | "error"`
943
+ - `result`: `DirectionsRouteResult | null`
944
+ - `error`: string or `null`
945
+ - `isLoading`: boolean
946
+
947
+ Example:
948
+
949
+ ```tsx
950
+ const routeRequest = useDirectionsRouteRequestStore();
951
+
952
+ if (routeRequest.isLoading) {
953
+ return <div>Generating route...</div>;
954
+ }
955
+
956
+ if (routeRequest.status === "error") {
957
+ return <div>{routeRequest.error}</div>;
958
+ }
959
+ ```
960
+
961
+ Navigate to step-by-step only when route succeeds:
962
+
963
+ ```tsx
964
+ DirectionBridgeService.generateDirectionsRoute({
965
+ onSuccess: () => {
966
+ navigateToStepByStep();
967
+ },
968
+ onError: (result) => {
969
+ console.log(result.error);
970
+ },
971
+ });
972
+ ```
973
+
974
+ ### `useFloorStore`
975
+
976
+ Floor list and current floor.
977
+
978
+ Fields:
979
+
980
+ - `floors`: `FloorImage[]`
981
+ - `activeFloor`: number or `null`
982
+
983
+ Example:
984
+
985
+ ```tsx
986
+ const floors = useFloorStore((s) => s.floors);
987
+ const activeFloor = useFloorStore((s) => s.activeFloor);
988
+ ```
989
+
990
+ ### `useNavigationStore`
991
+
992
+ Step-by-step navigation state.
993
+
994
+ Fields:
995
+
996
+ - `activePath`: `NodePoint[] | null`
997
+ - `navigationResult`: `NavigationResult | null`
998
+ - `simulationMode`: `"none" | "full" | "current"`
999
+ - `currentStepIndex`: number
1000
+
1001
+ Example:
1002
+
1003
+ ```tsx
1004
+ const navigationResult = useNavigationStore((s) => s.navigationResult);
1005
+ const currentStepIndex = useNavigationStore((s) => s.currentStepIndex);
1006
+ ```
1007
+
1008
+ Step data:
1009
+
1010
+ ```tsx
1011
+ navigationResult?.steps.map((step) => (
1012
+ <div key={step.id}>
1013
+ {step.instruction} - {step.distanceText}
1014
+ </div>
1015
+ ));
1016
+ ```
1017
+
1018
+ ## Viewer Click Events
1019
+
1020
+ Use `registerBridgeHandler` for viewer interaction events that should trigger host UI behavior.
1021
+
1022
+ Currently supported handler:
1023
+
1024
+ - `isShapeClick`: fires when a map shape/store is clicked in the viewer. Value is the clicked node id.
1025
+
1026
+ Example:
1027
+
1028
+ ```tsx
1029
+ import { registerBridgeHandler } from "wovvmap-webview-bridge/web";
1030
+
1031
+ useEffect(() => {
1032
+ registerBridgeHandler("isShapeClick", (message) => {
1033
+ console.log("Clicked node id", message.value);
1034
+ openStoreDetail(message.value);
1035
+ });
1036
+ }, []);
1037
+ ```
1038
+
1039
+ ## Full Kiosk Host Example
1040
+
1041
+ This example shows the main pieces together.
1042
+
1043
+ ```tsx
1044
+ import { useEffect, useRef } from "react";
1045
+ import {
1046
+ WebIframeScreen,
1047
+ DirectionBridgeService,
1048
+ SelectionBridgeService,
1049
+ ViewerBridgeService,
1050
+ ZoomBridgeService,
1051
+ useCategoryStore,
1052
+ useDirectionStore,
1053
+ useNodePointStore,
1054
+ registerBridgeHandler,
1055
+ } from "wovvmap-webview-bridge/web";
1056
+ import { useDirectionInputStore } from "./useDirectionInputStore";
1057
+
1058
+ export function KioskMap({ mapId }: { mapId: string | null }) {
1059
+ const categories = useCategoryStore((s) => Object.values(s.categories));
1060
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
1061
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
1062
+ const configuredRef = useRef(false);
1063
+
1064
+ useEffect(() => {
1065
+ if (configuredRef.current) return;
1066
+ configuredRef.current = true;
1067
+
1068
+ DirectionBridgeService.configure({
1069
+ directionInputAdapter: {
1070
+ setDestinationState: (query, selectedId) =>
1071
+ useDirectionInputStore.getState().setDestinationState(query, selectedId),
1072
+ setOriginState: (query, selectedId) =>
1073
+ useDirectionInputStore.getState().setOriginState(query, selectedId),
1074
+ clearDestinationState: (clearQuery) =>
1075
+ useDirectionInputStore.getState().clearDestinationState(clearQuery),
1076
+ clearOriginState: () =>
1077
+ useDirectionInputStore.getState().clearOriginState(),
1078
+ setActiveDirectionField: (field) =>
1079
+ useDirectionInputStore.getState().setActiveDirectionField(field),
1080
+ getOriginQuery: () => useDirectionInputStore.getState().originQuery,
1081
+ getDestinationQuery: () => useDirectionInputStore.getState().destinationQuery,
1082
+ },
1083
+ resolveLocationName: (nodeId) => {
1084
+ const nodeKey = nodeId.split("<->")[0];
1085
+ const node = useNodePointStore.getState().getNodePointByKey(nodeKey);
1086
+ return node?.assets?.locationName?.text || nodeKey;
1087
+ },
1088
+ onClearSelection: () => SelectionBridgeService.clearSelection(),
1089
+ });
1090
+ }, []);
1091
+
1092
+ useEffect(() => {
1093
+ registerBridgeHandler("isShapeClick", (message) => {
1094
+ DirectionBridgeService.setDestination(message.value, true, false);
1095
+ });
1096
+ }, []);
1097
+
1098
+ if (!mapId) return null;
1099
+
1100
+ return (
1101
+ <div>
1102
+ <WebIframeScreen
1103
+ url={`https://viewer.example.com/viewer/${mapId}?v=v2&template=embedded`}
1104
+ origin="https://viewer.example.com"
1105
+ />
1106
+
1107
+ <div>
1108
+ {categories.map((category) => (
1109
+ <button
1110
+ key={category.code}
1111
+ onClick={() => SelectionBridgeService.selectCategory(category)}
1112
+ >
1113
+ {category.name}
1114
+ </button>
1115
+ ))}
1116
+ </div>
1117
+
1118
+ <button onClick={() => ViewerBridgeService.setViewMode("2D", true)}>
1119
+ 2D View
1120
+ </button>
1121
+ <button onClick={() => ViewerBridgeService.setViewMode("3D", true)}>
1122
+ 3D View
1123
+ </button>
1124
+ <button onClick={() => ZoomBridgeService.zoomOut()}>Zoom Out</button>
1125
+ <button onClick={() => ZoomBridgeService.zoomIn()}>Zoom In</button>
1126
+
1127
+ <button
1128
+ disabled={!origin || !destination}
1129
+ onClick={() =>
1130
+ DirectionBridgeService.generateDirectionsRoute({
1131
+ onSuccess: () => console.log("route ready"),
1132
+ })
1133
+ }
1134
+ >
1135
+ Get Directions
1136
+ </button>
1137
+ </div>
1138
+ );
1139
+ }
1140
+ ```
1141
+
1142
+ ## Data Flow
1143
+
1144
+ 1. Host renders `WebIframeScreen` or `WebViewScreen`.
1145
+ 2. Viewer initializes and sends grouped state events.
1146
+ 3. Package handlers apply those events into Zustand stores.
1147
+ 4. Host UI reads stores and renders categories, amenities, floors, offers, directions, and navigation.
1148
+ 5. Host UI calls `*BridgeService` methods.
1149
+ 6. Package sends typed bridge event to viewer.
1150
+ 7. Viewer updates its internal state/scene and sends fresh grouped state back.
1151
+
1152
+ ## Incoming Viewer Events
1153
+
1154
+ These are sent by the viewer to the host package and applied into stores automatically.
1155
+
1156
+ | Key | Store updated | Value |
1157
+ | --- | --- | --- |
1158
+ | `bridgeState` | `useBridgeStorage.isMapLoaded` | `{ isMapLoaded: boolean }` |
1159
+ | `isConnection` | `useBridgeStorage.isBridgeLoaded` | `boolean` |
1160
+ | `tenantState` | `useBridgeStorage.imageBaseUrl` | `{ imageBaseUrl }` |
1161
+ | `viewerState` | `useViewerStore` | mallName, API version, view mode, camera/text settings, parsed/loading state |
1162
+ | `catalogState` | `useCategoryStore`, `useSubCategoryStore`, `useAmenityStore` | categories, subCategories, amenities |
1163
+ | `floorState` | `useFloorStore` | activeFloor, floors |
1164
+ | `nodePointState` | `useNodePointStore` | searchableNodePointMap, locationGroupedNodeMap, keyNodeMap, offersNodeMap |
1165
+ | `selectionState` | `useSelectionStore` | activeSelection, activeCategory, isOfferSelected |
1166
+ | `directionState` | `useDirectionStore` | selectedOriginPointId, selectedDestinationPointId, pathfindingError, routingPreferences |
1167
+ | `navigationState` | `useNavigationStore` | activePath, navigationResult, simulationMode, currentStepIndex |
1168
+ | `directionsRouteResult` | `useDirectionsRouteRequestStore` | status, navigationResult, error |
1169
+ | `isShapeClick` | event handler | clicked node id |
1170
+
1171
+ ## Outgoing Host Commands
1172
+
1173
+ These are sent by command services to the viewer.
1174
+
1175
+ | Service | Message key |
1176
+ | --- | --- |
1177
+ | `DirectionBridgeService.setDestination` | `setDestination` |
1178
+ | `DirectionBridgeService.clearDestination` | `clearDestination` |
1179
+ | `DirectionBridgeService.setOrigin` | `setOrigin` |
1180
+ | `DirectionBridgeService.clearOrigin` | `clearOrigin` |
1181
+ | `DirectionBridgeService.swapOriginDestination` | `swapOriginDestination` |
1182
+ | `DirectionBridgeService.generateDirectionsRoute` | `generateDirectionsRoute` |
1183
+ | `DirectionBridgeService.clearActivePath` | `clearActivePath` |
1184
+ | `SelectionBridgeService.selectCategory` | `setActiveSelection` |
1185
+ | `SelectionBridgeService.selectSubCategory` | `setActiveSelection` |
1186
+ | `SelectionBridgeService.selectAmenity` | `setActiveSelection` plus destination |
1187
+ | `SelectionBridgeService.toggleOffers` | `setOfferSelected` |
1188
+ | `SelectionBridgeService.clearSelection` | `clearActiveSelection` and `setOfferSelected(false)` |
1189
+ | `FloorBridgeService.changeFloor` | `changeFloor` |
1190
+ | `StepNavigationBridgeService.goToStep` | `goToStep` |
1191
+ | `StepNavigationBridgeService.nextStep` | `nextStep` |
1192
+ | `StepNavigationBridgeService.prevStep` | `prevStep` |
1193
+ | `StepNavigationBridgeService.finishStep` | `finishStep` |
1194
+ | `ZoomBridgeService.zoomIn` | `zoomIn` |
1195
+ | `ZoomBridgeService.zoomOut` | `zoomOut` |
1196
+ | `ViewerBridgeService.setViewMode` | `setViewMode` |
1197
+ | `ViewerBridgeService.setCameraView` | `setCameraView` |
1198
+ | `ViewerBridgeService.setTextType` | `setTextType` |
1199
+ | `ViewerBridgeService.setShow2DIcon` | `setShow2DIcon` |
1200
+
1201
+ ## Type Exports
1202
+
1203
+ The package exports these core types:
1204
+
1205
+ ```ts
1206
+ type IncomingMessage;
1207
+ type OutgoingMessage;
1208
+ type NodePoint;
1209
+ type FloorImage;
1210
+ type Amenity;
1211
+ type Category;
1212
+ type SubCategory;
1213
+ type ViewerBridgeState;
1214
+ type ViewMode;
1215
+ type CameraView;
1216
+ type TextType;
1217
+ type NavigationBridgeState;
1218
+ type DirectionsRouteResult;
1219
+ type NavigationResult;
1220
+ type NavigationStep;
1221
+ type NavigationSummary;
1222
+ type SimulationMode;
1223
+ type DirectionBridgeServiceConfig;
1224
+ type DirectionInputAdapter;
1225
+ type ViewerConfig;
1226
+ ```
1227
+
1228
+ ## Important Type Shapes
1229
+
1230
+ ### `Category`
1231
+
1232
+ ```ts
1233
+ type Category = {
1234
+ code: string | number;
1235
+ name: string;
1236
+ image?: string;
1237
+ subCategoryCodes: string[] | number[];
1238
+ nodePointsKey?: string[];
1239
+ nodePoints?: NodePoint[];
1240
+ };
1241
+ ```
1242
+
1243
+ ### `SubCategory`
1244
+
1245
+ ```ts
1246
+ type SubCategory = {
1247
+ code: string | number;
1248
+ name: string;
1249
+ image?: string;
1250
+ description?: string;
1251
+ categoryCode: string | number;
1252
+ nodePointsKey?: string[];
1253
+ nodePoints?: NodePoint[];
1254
+ };
1255
+ ```
1256
+
1257
+ ### `Amenity`
1258
+
1259
+ ```ts
1260
+ type Amenity = {
1261
+ id: string | number;
1262
+ status: number;
1263
+ fieldCode: string | number;
1264
+ name: string;
1265
+ image: string;
1266
+ wayfinding_id?: string | number | null;
1267
+ nodePointsKey?: string[];
1268
+ nodePoints?: NodePoint[];
1269
+ };
1270
+ ```
1271
+
1272
+ ### `NodePoint`
1273
+
1274
+ Important fields:
1275
+
1276
+ ```ts
1277
+ type NodePoint = {
1278
+ key: string;
1279
+ floorIndex: number;
1280
+ floorName: string;
1281
+ assets: {
1282
+ locationName: { text: string } | null;
1283
+ pointImages: unknown | null;
1284
+ logo: string | null;
1285
+ brandLogo: string | null;
1286
+ brandImages: string[];
1287
+ };
1288
+ categoryCode?: string | number;
1289
+ subCategoryCode?: string | number;
1290
+ offers: NodePointOffer[];
1291
+ up: boolean;
1292
+ down: boolean;
1293
+ };
1294
+ ```
1295
+
1296
+ ### `DirectionsRouteResult`
1297
+
1298
+ ```ts
1299
+ type DirectionsRouteResult = {
1300
+ status: "success" | "error";
1301
+ navigationResult: NavigationResult | null;
1302
+ error: string | null;
1303
+ };
1304
+ ```
1305
+
1306
+ ### `NavigationStep`
1307
+
1308
+ ```ts
1309
+ type NavigationStep = {
1310
+ id: string;
1311
+ instruction: string;
1312
+ action:
1313
+ | "straight"
1314
+ | "turn_left"
1315
+ | "turn_right"
1316
+ | "take_elevator"
1317
+ | "take_escalator"
1318
+ | "take_elevator_up"
1319
+ | "take_elevator_down"
1320
+ | "take_escalator_up"
1321
+ | "take_escalator_down"
1322
+ | "exit_elevator"
1323
+ | "exit_escalator"
1324
+ | "start"
1325
+ | "arrive";
1326
+ distanceText: string;
1327
+ timeText: string;
1328
+ floorIndex: number;
1329
+ };
1330
+ ```
1331
+
1332
+ ## Common UI Recipes
1333
+
1334
+ ### Category Footer
1335
+
1336
+ ```tsx
1337
+ const categories = useCategoryStore((s) => Object.values(s.categories));
1338
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1339
+
1340
+ return categories.map((category) => {
1341
+ const active =
1342
+ activeSelection.type === "Category" &&
1343
+ activeSelection.id === category.code.toString();
1344
+
1345
+ return (
1346
+ <button
1347
+ key={category.code}
1348
+ className={active ? "active" : ""}
1349
+ onClick={() => SelectionBridgeService.selectCategory(category)}
1350
+ >
1351
+ {category.name}
1352
+ </button>
1353
+ );
1354
+ });
1355
+ ```
1356
+
1357
+ ### Subcategory Footer
1358
+
1359
+ ```tsx
1360
+ const subCategories = useSubCategoryStore((s) => Object.values(s.subCategories));
1361
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
1362
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1363
+
1364
+ const visibleSubCategories = subCategories.filter((subCategory) =>
1365
+ activeCategory?.subCategoryCodes.includes(subCategory.code)
1366
+ );
1367
+
1368
+ return visibleSubCategories.map((subCategory) => {
1369
+ const active =
1370
+ activeSelection.type === "SubCategory" &&
1371
+ activeSelection.id === subCategory.code.toString();
1372
+
1373
+ return (
1374
+ <button
1375
+ key={subCategory.code}
1376
+ className={active ? "active" : ""}
1377
+ onClick={() => SelectionBridgeService.selectSubCategory(subCategory)}
1378
+ >
1379
+ {subCategory.name}
1380
+ </button>
1381
+ );
1382
+ });
1383
+ ```
1384
+
1385
+ ### Amenity List
1386
+
1387
+ ```tsx
1388
+ const amenities = useAmenityStore((s) => Object.values(s.amenities));
1389
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1390
+
1391
+ return amenities.map((amenity) => {
1392
+ const active =
1393
+ activeSelection.type === "Amenity" &&
1394
+ activeSelection.id === amenity.id.toString();
1395
+
1396
+ return (
1397
+ <button
1398
+ key={amenity.id}
1399
+ className={active ? "active" : ""}
1400
+ onClick={() => SelectionBridgeService.selectAmenity(amenity)}
1401
+ >
1402
+ {amenity.name}
1403
+ </button>
1404
+ );
1405
+ });
1406
+ ```
1407
+
1408
+ ### Store Search Selection
1409
+
1410
+ ```tsx
1411
+ function selectStore(nodeKey: string, field: "from" | "to") {
1412
+ const group = useNodePointStore.getState().locationGroupedNodeMap[nodeKey] || [nodeKey];
1413
+ const joinedNodeIds = group.join("<->");
1414
+
1415
+ if (field === "from") {
1416
+ DirectionBridgeService.setOrigin(joinedNodeIds, true, false);
1417
+ } else {
1418
+ DirectionBridgeService.setDestination(joinedNodeIds, true, false);
1419
+ }
1420
+ }
1421
+ ```
1422
+
1423
+ ### Default Location
1424
+
1425
+ Default location should stay in the host app because only the host knows the map id and user preference.
1426
+
1427
+ ```tsx
1428
+ const defaultLocationKey = getDefaultLocationForMap(mapId);
1429
+
1430
+ if (defaultLocationKey) {
1431
+ DirectionBridgeService.setOrigin(defaultLocationKey, false, false);
1432
+ }
1433
+ ```
1434
+
1435
+ To show default location badge:
1436
+
1437
+ ```tsx
1438
+ const isDefaultLocation = selectedOriginPointId === defaultLocationKey;
1439
+ ```
1440
+
1441
+ ### Get Directions Button
1442
+
1443
+ ```tsx
1444
+ const origin = useDirectionStore((s) => s.selectedOriginPointId);
1445
+ const destination = useDirectionStore((s) => s.selectedDestinationPointId);
1446
+ const isLoading = useDirectionsRouteRequestStore((s) => s.isLoading);
1447
+
1448
+ return (
1449
+ <button
1450
+ disabled={!origin || !destination || isLoading}
1451
+ onClick={() =>
1452
+ DirectionBridgeService.generateDirectionsRoute({
1453
+ onSuccess: () => navigateToStepByStep(),
1454
+ })
1455
+ }
1456
+ >
1457
+ {isLoading ? "Loading..." : "Get Directions"}
1458
+ </button>
1459
+ );
1460
+ ```
1461
+
1462
+ ### Pathfinding Error
1463
+
1464
+ ```tsx
1465
+ const pathfindingError = useDirectionStore((s) => s.pathfindingError);
1466
+
1467
+ return pathfindingError ? (
1468
+ <div>
1469
+ <strong>No Routes Available</strong>
1470
+ <span>{pathfindingError}</span>
1471
+ </div>
1472
+ ) : null;
1473
+ ```
1474
+
1475
+ ### Step-by-Step Panel
1476
+
1477
+ ```tsx
1478
+ const navigationResult = useNavigationStore((s) => s.navigationResult);
1479
+
1480
+ if (!navigationResult) return null;
1481
+
1482
+ return (
1483
+ <div>
1484
+ <h3>Total Distance {navigationResult.summary.totalDistance}</h3>
1485
+ <p>Arrive in {navigationResult.summary.totalApproxTime}</p>
1486
+ {navigationResult.steps.map((step, index) => (
1487
+ <button key={step.id} onClick={() => StepNavigationBridgeService.goToStep(index)}>
1488
+ {step.instruction}
1489
+ </button>
1490
+ ))}
1491
+ </div>
1492
+ );
1493
+ ```
1494
+
1495
+ ## Image URLs
1496
+
1497
+ Image base URL comes from tenant state:
1498
+
1499
+ ```tsx
1500
+ const imageBaseUrl = useBridgeStorage((s) => s.imageBaseUrl);
1501
+ ```
1502
+
1503
+ Category image:
1504
+
1505
+ ```tsx
1506
+ const src = category.image && imageBaseUrl ? `${imageBaseUrl}${category.image}` : "";
1507
+ ```
1508
+
1509
+ Amenity image:
1510
+
1511
+ ```tsx
1512
+ const src = amenity.image && imageBaseUrl ? `${imageBaseUrl}${amenity.image}` : "";
1513
+ ```
1514
+
1515
+ ## What Should Stay In Host App
1516
+
1517
+ Keep these in the host app:
1518
+
1519
+ - `mapId`
1520
+ - default location per map id
1521
+ - local direction input query store if your UI has text inputs
1522
+ - host-specific flow history
1523
+ - custom UI layout
1524
+ - business-specific filtering that is not part of bridge protocol
1525
+
1526
+ Use the package for:
1527
+
1528
+ - iframe/WebView bridge setup
1529
+ - reading viewer-synced stores
1530
+ - sending viewer commands
1531
+ - route result lifecycle
1532
+ - selection/offers/floor/zoom/viewer controls
1533
+
1534
+ ## Troubleshooting
1535
+
1536
+ ### Stores are empty after refresh
1537
+
1538
+ Check:
1539
+
1540
+ - iframe/WebView URL is correct
1541
+ - viewer is using embedded template and initializes bridge after map parse
1542
+ - `useBridgeStorage((s) => s.isBridgeLoaded)` becomes `true`
1543
+ - `useBridgeStorage((s) => s.isMapLoaded)` becomes `true`
1544
+ - host is not rendering UI before `mapId` exists
1545
+
1546
+ ### Category or amenity images do not show
1547
+
1548
+ Use `imageBaseUrl` from `useBridgeStorage`.
1549
+
1550
+ Correct:
1551
+
1552
+ ```tsx
1553
+ const src = imageBaseUrl && item.image ? `${imageBaseUrl}${item.image}` : "";
1554
+ ```
1555
+
1556
+ Wrong:
1557
+
1558
+ ```tsx
1559
+ const src = imageBaseUrl || "" + item.image;
1560
+ ```
1561
+
1562
+ The wrong version returns only `imageBaseUrl` when it exists.
1563
+
1564
+ ### Direction input text is not updating
1565
+
1566
+ Call `DirectionBridgeService.configure` and provide `directionInputAdapter`.
1567
+
1568
+ Without adapter:
1569
+
1570
+ - bridge command still works
1571
+ - viewer origin/destination still changes
1572
+ - local input text does not change automatically
1573
+
1574
+ ### Route success needs UI navigation
1575
+
1576
+ Use `generateDirectionsRoute` callbacks or `useDirectionsRouteRequestStore`.
1577
+
1578
+ ```ts
1579
+ DirectionBridgeService.generateDirectionsRoute({
1580
+ onSuccess: () => navigateToStepByStep(),
1581
+ onError: (result) => showError(result.error),
1582
+ });
1583
+ ```
1584
+
1585
+ ### Offers button active state
1586
+
1587
+ Use:
1588
+
1589
+ ```tsx
1590
+ const isOfferSelected = useSelectionStore((s) => s.isOfferSelected);
1591
+ ```
1592
+
1593
+ Do not infer offer active state from category/subcategory.
1594
+
1595
+ ### Category active state
1596
+
1597
+ Use:
1598
+
1599
+ ```tsx
1600
+ const activeSelection = useSelectionStore((s) => s.activeSelection);
1601
+ const isActive =
1602
+ activeSelection.type === "Category" &&
1603
+ activeSelection.id === category.code.toString();
1604
+ ```
1605
+
1606
+ ### Subcategory unselect should keep category visible
1607
+
1608
+ Keep `activeCategory` from `useSelectionStore`.
1609
+
1610
+ ```tsx
1611
+ const activeCategory = useSelectionStore((s) => s.activeCategory);
1612
+ ```
1613
+
1614
+ Use `activeCategory` to render subcategory list, not only `activeSelection`.
1615
+
1616
+ ### Text/view controls click but do not change
1617
+
1618
+ Make sure your control overlay is above iframe/canvas layers.
1619
+
1620
+ ```tsx
1621
+ <div className="absolute z-[1000] pointer-events-auto">
1622
+ ...
1623
+ </div>
1624
+ ```
1625
+
1626
+ ### Do not duplicate bridge services in host app
1627
+
1628
+ Do not recreate old local services like:
1629
+
1630
+ - `TemplateDirectionService`
1631
+ - `TemplateSelectionService`
1632
+ - `TemplateFloorService`
1633
+ - `TemplateStepNavigationService`
1634
+ - `TemplateZoomService`
1635
+ - `TemplateViewerService`
1636
+
1637
+ Use package exports instead:
1638
+
1639
+ ```ts
1640
+ import {
1641
+ DirectionBridgeService,
1642
+ SelectionBridgeService,
1643
+ FloorBridgeService,
1644
+ StepNavigationBridgeService,
1645
+ ZoomBridgeService,
1646
+ ViewerBridgeService,
1647
+ } from "wovvmap-webview-bridge/web";
1648
+ ```
1649
+
1650
+ ## Export Checklist
1651
+
1652
+ ### Components
1653
+
1654
+ - `WebIframeScreen`
1655
+ - `WebViewScreen`
1656
+ - `attachIframeBridge`
1657
+
1658
+ ### Services
1659
+
1660
+ - `BridgeService`
1661
+ - `DirectionBridgeService`
1662
+ - `SelectionBridgeService`
1663
+ - `FloorBridgeService`
1664
+ - `StepNavigationBridgeService`
1665
+ - `ZoomBridgeService`
1666
+ - `ViewerBridgeService`
1667
+
1668
+ ### Stores
1669
+
1670
+ - `useBridgeStorage`
1671
+ - `useViewerStore`
1672
+ - `useCategoryStore`
1673
+ - `useSubCategoryStore`
1674
+ - `useAmenityStore`
1675
+ - `useNodePointStore`
1676
+ - `useSelectionStore`
1677
+ - `useDirectionStore`
1678
+ - `useFloorStore`
1679
+ - `useNavigationStore`
1680
+ - `useDirectionsRouteRequestStore`
1681
+
1682
+ ### Handlers
1683
+
1684
+ - `registerBridgeHandler`
1685
+
1686
+ ### Types
1687
+
1688
+ - `IncomingMessage`
1689
+ - `OutgoingMessage`
1690
+ - `NodePoint`
1691
+ - `FloorImage`
1692
+ - `Amenity`
1693
+ - `Category`
1694
+ - `SubCategory`
1695
+ - `ViewerBridgeState`
1696
+ - `ViewMode`
1697
+ - `CameraView`
1698
+ - `TextType`
1699
+ - `NavigationBridgeState`
1700
+ - `DirectionsRouteResult`
1701
+ - `NavigationResult`
1702
+ - `NavigationStep`
1703
+ - `NavigationSummary`
1704
+ - `SimulationMode`
1705
+ - `DirectionBridgeServiceConfig`
1706
+ - `DirectionInputAdapter`
1707
+ - `ViewerConfig`