@apollovisionlabs/guide-core 0.2.0 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -19,6 +19,11 @@ interface Step {
19
19
  placement?: Placement;
20
20
  /** Lets the user interact with the page during the step. */
21
21
  interactive?: boolean;
22
+ /**
23
+ * Advances the tour when the user clicks the target. Implies `interactive`: a step that
24
+ * waits for a click has to let the click through.
25
+ */
26
+ advanceOn?: 'click';
22
27
  title?: string;
23
28
  titleKey?: string;
24
29
  body?: string;
@@ -62,10 +67,34 @@ interface ResolvedChecklistItem {
62
67
  tourId?: string;
63
68
  href?: string;
64
69
  }
70
+ interface Hotspot {
71
+ id: string;
72
+ /** Logical key carried by the data-guide attribute on the element. */
73
+ target: string;
74
+ title?: string;
75
+ titleKey?: string;
76
+ body?: string;
77
+ bodyKey?: string;
78
+ /** Tour started from the hotspot's bubble. */
79
+ tourId?: string;
80
+ placement?: Placement;
81
+ }
82
+ interface ResolvedHotspot {
83
+ id: string;
84
+ target: string;
85
+ title: string;
86
+ body: string;
87
+ seen: boolean;
88
+ tourId?: string;
89
+ placement?: Placement;
90
+ }
91
+ interface HotspotsProgress {
92
+ seen: string[];
93
+ }
65
94
  interface GuideStorage {
66
95
  /**
67
96
  * Reads a previously written value. The key is namespaced by the caller,
68
- * `tour:<id>` or `checklist:<id>`, so one storage serves both.
97
+ * `tour:<id>`, `checklist:<id>`, or `hotspots:seen`, so one storage serves them all.
69
98
  */
70
99
  read<T>(key: string): Promise<T | null>;
71
100
  write<T>(key: string, value: T): Promise<void>;
@@ -101,6 +130,12 @@ type GuideEvent = {
101
130
  } | {
102
131
  type: 'checklist:dismiss';
103
132
  checklistId: string;
133
+ } | {
134
+ type: 'hotspot:show';
135
+ hotspotId: string;
136
+ } | {
137
+ type: 'hotspot:open';
138
+ hotspotId: string;
104
139
  };
105
140
  type Translate = (key: string) => string;
106
141
 
@@ -116,6 +151,11 @@ declare function isTourProgress(value: unknown): value is TourProgress;
116
151
  * never trusted until its shape is checked.
117
152
  */
118
153
  declare function isChecklistProgress(value: unknown): value is ChecklistProgress;
154
+ /**
155
+ * Same defensive posture as the two guards above: a stored hotspot value is never trusted
156
+ * until its shape is checked.
157
+ */
158
+ declare function isHotspotsProgress(value: unknown): value is HotspotsProgress;
119
159
 
120
160
  declare function isLiteralRoute(pattern: string): boolean;
121
161
  declare function matchRoute(pattern: string, pathname: string): boolean;
@@ -173,6 +213,14 @@ interface ActiveStep {
173
213
  stepCount: number;
174
214
  element: HTMLElement | null;
175
215
  rect: Rect | null;
216
+ /**
217
+ * Whether the page stays reachable during the step. True when the step declares
218
+ * `interactive`, and also when it declares `advanceOn`, whose click has to reach the
219
+ * element. Renderers read this instead of `step.interactive`, so the rule lives here.
220
+ */
221
+ interactive: boolean;
222
+ /** True when the step advances on a user action rather than on a button. */
223
+ awaitsAction: boolean;
176
224
  title: string;
177
225
  body: string;
178
226
  isFirst: boolean;
@@ -229,6 +277,15 @@ interface ChecklistContextValue {
229
277
  checklists: Checklist[];
230
278
  progress: Record<string, ChecklistProgress>;
231
279
  translate?: Translate;
280
+ /**
281
+ * Whether each checklist's initial restore from storage has settled, keyed by checklist id:
282
+ * true immediately for a checklist when no `storage` prop was given (there is nothing to
283
+ * wait for), and true once that checklist's own read has resolved or rejected. Settled
284
+ * independently per checklist, so a slow or hung read for one checklist never holds another
285
+ * checklist's rendering hostage, and a renderer can wait for its own entry without a broken
286
+ * backend hiding it forever.
287
+ */
288
+ restored: Record<string, boolean>;
232
289
  activate: (checklistId: string, itemId: string) => void;
233
290
  toggle: (checklistId: string, itemId: string) => void;
234
291
  complete: (checklistId: string, itemId: string) => void;
@@ -252,6 +309,14 @@ interface UseChecklistResult {
252
309
  total: number;
253
310
  isComplete: boolean;
254
311
  dismissed: boolean;
312
+ /**
313
+ * Whether this checklist's own initial restore from storage has settled. A renderer should
314
+ * wait for this before drawing anything, or a checklist already dismissed or partly
315
+ * completed in storage can flash its empty initial state on screen once before the restore
316
+ * lands. Settled per checklist: a slow or hung read for a different checklist on the same
317
+ * provider never holds this one false.
318
+ */
319
+ restored: boolean;
255
320
  activate: (itemId: string) => void;
256
321
  toggle: (itemId: string) => void;
257
322
  complete: (itemId: string) => void;
@@ -260,4 +325,49 @@ interface UseChecklistResult {
260
325
  }
261
326
  declare function useChecklist(checklistId: string): UseChecklistResult;
262
327
 
263
- export { type ActiveStep, type Checklist, ChecklistContext, type ChecklistContextValue, type ChecklistItem, type ChecklistProgress, ChecklistProvider, type ChecklistProviderProps, GuideContext, type GuideContextValue, type GuideEvent, GuideProvider, type GuideProviderProps, type GuideStorage, type MissingTargetPolicy, type Placement, type Rect, type ResolvedChecklistItem, type Step, type Tour, type TourAction, type TourProgress, type TourState, type TourStatus, type Translate, type UseChecklistResult, type UseFocusTrapOptions, type UseTargetElementOptions, type UseTourResult, createBrowserStorage, createMemoryStorage, findMissingTargets, initialTourState, isChecklistProgress, isLiteralRoute, isTourProgress, matchRoute, resolveText, tourReducer, useAnnouncer, useChecklist, useElementRect, useFocusTrap, useGuideStep, usePrefersReducedMotion, useTargetElement, useTour };
328
+ interface HotspotContextValue {
329
+ hotspots: Hotspot[];
330
+ seen: string[];
331
+ translate?: Translate;
332
+ /**
333
+ * Whether the initial restore from storage has settled: true immediately when no `storage`
334
+ * prop was given (there is nothing to wait for), and true once the read resolves or
335
+ * rejects, so a renderer can wait for it without a broken backend hiding hotspots forever.
336
+ */
337
+ restored: boolean;
338
+ open: (hotspotId: string) => void;
339
+ startTour: (hotspotId: string) => void;
340
+ reset: () => void;
341
+ notifyShown: (hotspotId: string) => void;
342
+ }
343
+ declare const HotspotContext: react.Context<HotspotContextValue | null>;
344
+ interface HotspotProviderProps {
345
+ hotspots: Hotspot[];
346
+ children: ReactNode;
347
+ storage?: GuideStorage;
348
+ translate?: Translate;
349
+ onEvent?: (event: GuideEvent) => void;
350
+ }
351
+ declare function HotspotProvider({ hotspots, children, storage, translate, onEvent, }: HotspotProviderProps): react.JSX.Element;
352
+
353
+ interface UseHotspotsResult {
354
+ /**
355
+ * Every hotspot, each carrying its own `seen`, rather than the unseen ones alone. A
356
+ * renderer has to keep a marker mounted while its own bubble closes, so it needs the seen
357
+ * one too; filtering is one line at the call site.
358
+ */
359
+ hotspots: ResolvedHotspot[];
360
+ /**
361
+ * Whether the initial restore from storage has settled. A renderer should wait for this
362
+ * before drawing any marker, or a hotspot already seen in storage can flash on screen once
363
+ * before the restore lands.
364
+ */
365
+ restored: boolean;
366
+ open: (hotspotId: string) => void;
367
+ startTour: (hotspotId: string) => void;
368
+ reset: () => void;
369
+ notifyShown: (hotspotId: string) => void;
370
+ }
371
+ declare function useHotspots(): UseHotspotsResult;
372
+
373
+ export { type ActiveStep, type Checklist, ChecklistContext, type ChecklistContextValue, type ChecklistItem, type ChecklistProgress, ChecklistProvider, type ChecklistProviderProps, GuideContext, type GuideContextValue, type GuideEvent, GuideProvider, type GuideProviderProps, type GuideStorage, type Hotspot, HotspotContext, type HotspotContextValue, HotspotProvider, type HotspotProviderProps, type HotspotsProgress, type MissingTargetPolicy, type Placement, type Rect, type ResolvedChecklistItem, type ResolvedHotspot, type Step, type Tour, type TourAction, type TourProgress, type TourState, type TourStatus, type Translate, type UseChecklistResult, type UseFocusTrapOptions, type UseHotspotsResult, type UseTargetElementOptions, type UseTourResult, createBrowserStorage, createMemoryStorage, findMissingTargets, initialTourState, isChecklistProgress, isHotspotsProgress, isLiteralRoute, isTourProgress, matchRoute, resolveText, tourReducer, useAnnouncer, useChecklist, useElementRect, useFocusTrap, useGuideStep, useHotspots, usePrefersReducedMotion, useTargetElement, useTour };
package/dist/index.mjs CHANGED
@@ -44,6 +44,11 @@ function isChecklistProgress(value) {
44
44
  const candidate = value;
45
45
  return Array.isArray(candidate.completed) && candidate.completed.every((entry) => typeof entry === "string") && typeof candidate.dismissed === "boolean";
46
46
  }
47
+ function isHotspotsProgress(value) {
48
+ if (typeof value !== "object" || value === null) return false;
49
+ const candidate = value;
50
+ return Array.isArray(candidate.seen) && candidate.seen.every((entry) => typeof entry === "string");
51
+ }
47
52
 
48
53
  // src/matchRoute.ts
49
54
  function segments(value) {
@@ -283,6 +288,24 @@ function resolveText(value, key, translate) {
283
288
  // src/GuideProvider.tsx
284
289
  import { jsx } from "react/jsx-runtime";
285
290
  var GuideContext = createContext(null);
291
+ function focusFallback(element) {
292
+ const needsTabIndex = !element.hasAttribute("tabindex") && element.tabIndex < 0;
293
+ if (!needsTabIndex) {
294
+ element.focus();
295
+ return;
296
+ }
297
+ element.setAttribute("tabindex", "-1");
298
+ element.focus();
299
+ if (document.activeElement !== element) {
300
+ element.removeAttribute("tabindex");
301
+ return;
302
+ }
303
+ const onBlur = () => {
304
+ element.removeAttribute("tabindex");
305
+ element.removeEventListener("blur", onBlur);
306
+ };
307
+ element.addEventListener("blur", onBlur);
308
+ }
286
309
  function GuideProvider({
287
310
  tours,
288
311
  children,
@@ -307,6 +330,7 @@ function GuideProvider({
307
330
  const [state, dispatch] = useReducer(tourReducer, initialTourState);
308
331
  const announce = useAnnouncer();
309
332
  const focusOriginRef = useRef(null);
333
+ const lastElementRef = useRef(null);
310
334
  const storageWarnedRef = useRef(false);
311
335
  const warnStorageFailure = useCallback2((error) => {
312
336
  if (storageWarnedRef.current) return;
@@ -342,6 +366,14 @@ function GuideProvider({
342
366
  dispatch({ type: "NEXT", stepCount: tour.steps.length });
343
367
  if (isLast) emit({ type: "tour:complete", tourId: tour.id });
344
368
  }, [tour, state.stepIndex, emit]);
369
+ const nextRef = useRef(next);
370
+ nextRef.current = next;
371
+ useEffect4(() => {
372
+ if (state.status !== "running" || !element || step?.advanceOn !== "click") return;
373
+ const onClick = () => nextRef.current();
374
+ element.addEventListener("click", onClick);
375
+ return () => element.removeEventListener("click", onClick);
376
+ }, [state.status, element, step?.advanceOn]);
345
377
  const previous = useCallback2(() => dispatch({ type: "PREVIOUS" }), []);
346
378
  const stop = useCallback2(() => {
347
379
  if (tour) emit({ type: "tour:stop", tourId: tour.id, stepIndex: state.stepIndex });
@@ -440,12 +472,23 @@ function GuideProvider({
440
472
  warnStorageFailure(error);
441
473
  }
442
474
  }, [storage, state.tourId, state.status, state.stepIndex, warnStorageFailure]);
475
+ useEffect4(() => {
476
+ if (element) lastElementRef.current = element;
477
+ }, [element]);
443
478
  useEffect4(() => {
444
479
  if (state.status !== "idle" && state.status !== "completed") return;
445
480
  const origin = focusOriginRef.current;
446
- if (!origin) return;
481
+ const fallback = lastElementRef.current;
447
482
  focusOriginRef.current = null;
448
- if (typeof document !== "undefined" && document.contains(origin)) origin.focus();
483
+ lastElementRef.current = null;
484
+ if (typeof document === "undefined") return;
485
+ if (origin && document.contains(origin)) {
486
+ origin.focus();
487
+ return;
488
+ }
489
+ if (document.activeElement !== document.body) return;
490
+ if (!fallback || !document.contains(fallback)) return;
491
+ focusFallback(fallback);
449
492
  }, [state.status]);
450
493
  const activeStep = useMemo(() => {
451
494
  if (!tour || !step || !isActive) return null;
@@ -456,6 +499,8 @@ function GuideProvider({
456
499
  stepCount: tour.steps.length,
457
500
  element,
458
501
  rect,
502
+ interactive: step.interactive === true || step.advanceOn !== void 0,
503
+ awaitsAction: step.advanceOn !== void 0,
459
504
  title: resolveText(step.title, step.titleKey, translate),
460
505
  body: resolveText(step.body, step.bodyKey, translate),
461
506
  isFirst: state.stepIndex === 0,
@@ -532,6 +577,11 @@ function ChecklistProvider({
532
577
  return initial;
533
578
  });
534
579
  const progressRef = useRef2(progress);
580
+ const [restoredById, setRestoredById] = useState5(() => {
581
+ const initial = {};
582
+ for (const candidate of checklists) initial[candidate.id] = !storage;
583
+ return initial;
584
+ });
535
585
  const guide = useContext3(GuideContext);
536
586
  const storageWarnedRef = useRef2(false);
537
587
  const warnStorageFailure = useCallback3((error) => {
@@ -563,31 +613,33 @@ function ChecklistProvider({
563
613
  useEffect5(() => {
564
614
  if (!storage) return;
565
615
  let cancelled = false;
566
- void (async () => {
567
- const restored = {};
568
- for (const candidate of checklists) {
616
+ for (const candidate of checklists) {
617
+ void (async () => {
569
618
  try {
570
619
  const stored = await storage.read(`checklist:${candidate.id}`);
571
- if (isChecklistProgress(stored)) restored[candidate.id] = stored;
620
+ if (!cancelled && isChecklistProgress(stored)) {
621
+ const live = progressRef.current[candidate.id] ?? emptyProgress;
622
+ const merged = {
623
+ ...progressRef.current,
624
+ [candidate.id]: {
625
+ completed: live.completed.concat(
626
+ stored.completed.filter((id) => !live.completed.includes(id))
627
+ ),
628
+ dismissed: live.dismissed || stored.dismissed
629
+ }
630
+ };
631
+ progressRef.current = merged;
632
+ setProgress(merged);
633
+ }
572
634
  } catch (error) {
573
635
  warnStorageFailure(error);
636
+ } finally {
637
+ if (!cancelled) {
638
+ setRestoredById((current) => ({ ...current, [candidate.id]: true }));
639
+ }
574
640
  }
575
- }
576
- if (!cancelled && Object.keys(restored).length > 0) {
577
- const merged = { ...progressRef.current };
578
- for (const [checklistId, stored] of Object.entries(restored)) {
579
- const live = merged[checklistId] ?? emptyProgress;
580
- merged[checklistId] = {
581
- completed: live.completed.concat(
582
- stored.completed.filter((id) => !live.completed.includes(id))
583
- ),
584
- dismissed: live.dismissed || stored.dismissed
585
- };
586
- }
587
- progressRef.current = merged;
588
- setProgress(merged);
589
- }
590
- })();
641
+ })();
642
+ }
591
643
  return () => {
592
644
  cancelled = true;
593
645
  };
@@ -725,8 +777,18 @@ function ChecklistProvider({
725
777
  [resolveItem, guide, navigate, toggle, warnNoGuide, warnNoNavigate, warnTourStartFailure]
726
778
  );
727
779
  const value = useMemo3(
728
- () => ({ checklists, progress, translate, activate, toggle, complete, dismiss, reset }),
729
- [checklists, progress, translate, activate, toggle, complete, dismiss, reset]
780
+ () => ({
781
+ checklists,
782
+ progress,
783
+ translate,
784
+ restored: restoredById,
785
+ activate,
786
+ toggle,
787
+ complete,
788
+ dismiss,
789
+ reset
790
+ }),
791
+ [checklists, progress, translate, restoredById, activate, toggle, complete, dismiss, reset]
730
792
  );
731
793
  return /* @__PURE__ */ jsx2(ChecklistContext.Provider, { value, children });
732
794
  }
@@ -743,6 +805,7 @@ function useChecklist(checklistId) {
743
805
  const completed = progress?.completed ?? [];
744
806
  const dismissed = progress?.dismissed ?? false;
745
807
  const translate = context.translate;
808
+ const restored = context.restored[checklistId] ?? true;
746
809
  const items = useMemo4(
747
810
  () => checklist.items.map((item) => ({
748
811
  id: item.id,
@@ -765,6 +828,7 @@ function useChecklist(checklistId) {
765
828
  total,
766
829
  isComplete,
767
830
  dismissed,
831
+ restored,
768
832
  activate: (itemId) => activate(checklistId, itemId),
769
833
  toggle: (itemId) => toggle(checklistId, itemId),
770
834
  complete: (itemId) => complete(checklistId, itemId),
@@ -777,6 +841,7 @@ function useChecklist(checklistId) {
777
841
  total,
778
842
  isComplete,
779
843
  dismissed,
844
+ restored,
780
845
  activate,
781
846
  toggle,
782
847
  complete,
@@ -786,16 +851,189 @@ function useChecklist(checklistId) {
786
851
  ]
787
852
  );
788
853
  }
854
+
855
+ // src/HotspotProvider.tsx
856
+ import {
857
+ createContext as createContext3,
858
+ useCallback as useCallback4,
859
+ useContext as useContext5,
860
+ useEffect as useEffect6,
861
+ useMemo as useMemo5,
862
+ useRef as useRef3,
863
+ useState as useState6
864
+ } from "react";
865
+ import { jsx as jsx3 } from "react/jsx-runtime";
866
+ var STORAGE_KEY = "hotspots:seen";
867
+ var HotspotContext = createContext3(null);
868
+ function HotspotProvider({
869
+ hotspots,
870
+ children,
871
+ storage,
872
+ translate,
873
+ onEvent
874
+ }) {
875
+ const hotspotsById = useMemo5(() => {
876
+ const map = /* @__PURE__ */ new Map();
877
+ for (const candidate of hotspots) {
878
+ if (map.has(candidate.id)) {
879
+ throw new Error(`[guide] duplicate hotspot id: ${candidate.id}`);
880
+ }
881
+ map.set(candidate.id, candidate);
882
+ }
883
+ return map;
884
+ }, [hotspots]);
885
+ const [seen, setSeen] = useState6([]);
886
+ const [restored, setRestored] = useState6(() => !storage);
887
+ const seenRef = useRef3(seen);
888
+ const guide = useContext5(GuideContext);
889
+ const storageWarnedRef = useRef3(false);
890
+ const warnStorageFailure = useCallback4((error) => {
891
+ if (storageWarnedRef.current) return;
892
+ storageWarnedRef.current = true;
893
+ console.warn("[guide] storage failed; hotspot state will not be persisted", error);
894
+ }, []);
895
+ const noGuideWarnedRef = useRef3(false);
896
+ const warnNoGuide = useCallback4(() => {
897
+ if (noGuideWarnedRef.current) return;
898
+ noGuideWarnedRef.current = true;
899
+ console.warn("[guide] a hotspot needs a GuideProvider to launch a tour");
900
+ }, []);
901
+ const tourStartFailedWarnedRef = useRef3(false);
902
+ const warnTourStartFailure = useCallback4((error) => {
903
+ if (tourStartFailedWarnedRef.current) return;
904
+ tourStartFailedWarnedRef.current = true;
905
+ console.warn("[guide] starting a tour for a hotspot failed", error);
906
+ }, []);
907
+ const onEventRef = useRef3(onEvent);
908
+ onEventRef.current = onEvent;
909
+ const emit = useCallback4((event) => onEventRef.current?.(event), []);
910
+ useEffect6(() => {
911
+ if (!storage) return;
912
+ let cancelled = false;
913
+ void (async () => {
914
+ let stored = null;
915
+ try {
916
+ stored = await storage.read(STORAGE_KEY);
917
+ } catch (error) {
918
+ warnStorageFailure(error);
919
+ if (!cancelled) setRestored(true);
920
+ return;
921
+ }
922
+ if (cancelled) return;
923
+ if (isHotspotsProgress(stored)) {
924
+ const merged = seenRef.current.concat(
925
+ stored.seen.filter((id) => !seenRef.current.includes(id))
926
+ );
927
+ seenRef.current = merged;
928
+ setSeen(merged);
929
+ }
930
+ setRestored(true);
931
+ })();
932
+ return () => {
933
+ cancelled = true;
934
+ };
935
+ }, [storage, warnStorageFailure]);
936
+ const applySeen = useCallback4(
937
+ (next) => {
938
+ seenRef.current = next;
939
+ setSeen(next);
940
+ if (!storage) return;
941
+ try {
942
+ void Promise.resolve(storage.write(STORAGE_KEY, { seen: next })).catch(
943
+ warnStorageFailure
944
+ );
945
+ } catch (error) {
946
+ warnStorageFailure(error);
947
+ }
948
+ },
949
+ [storage, warnStorageFailure]
950
+ );
951
+ const resolve = useCallback4(
952
+ (hotspotId) => {
953
+ const hotspot = hotspotsById.get(hotspotId);
954
+ if (!hotspot) {
955
+ console.warn(`[guide] unknown hotspot "${hotspotId}"`);
956
+ return null;
957
+ }
958
+ return hotspot;
959
+ },
960
+ [hotspotsById]
961
+ );
962
+ const open = useCallback4(
963
+ (hotspotId) => {
964
+ if (!resolve(hotspotId)) return;
965
+ emit({ type: "hotspot:open", hotspotId });
966
+ if (seenRef.current.includes(hotspotId)) return;
967
+ applySeen([...seenRef.current, hotspotId]);
968
+ },
969
+ [resolve, applySeen, emit]
970
+ );
971
+ const startTour = useCallback4(
972
+ (hotspotId) => {
973
+ const hotspot = resolve(hotspotId);
974
+ if (!hotspot?.tourId) return;
975
+ if (!guide) {
976
+ warnNoGuide();
977
+ return;
978
+ }
979
+ void guide.start(hotspot.tourId).catch(warnTourStartFailure);
980
+ },
981
+ [resolve, guide, warnNoGuide, warnTourStartFailure]
982
+ );
983
+ const reset = useCallback4(() => applySeen([]), [applySeen]);
984
+ const shownRef = useRef3(/* @__PURE__ */ new Set());
985
+ const notifyShown = useCallback4(
986
+ (hotspotId) => {
987
+ if (shownRef.current.has(hotspotId)) return;
988
+ shownRef.current.add(hotspotId);
989
+ emit({ type: "hotspot:show", hotspotId });
990
+ },
991
+ [emit]
992
+ );
993
+ const value = useMemo5(
994
+ () => ({ hotspots, seen, translate, restored, open, startTour, reset, notifyShown }),
995
+ [hotspots, seen, translate, restored, open, startTour, reset, notifyShown]
996
+ );
997
+ return /* @__PURE__ */ jsx3(HotspotContext.Provider, { value, children });
998
+ }
999
+
1000
+ // src/useHotspots.ts
1001
+ import { useContext as useContext6, useMemo as useMemo6 } from "react";
1002
+ function useHotspots() {
1003
+ const context = useContext6(HotspotContext);
1004
+ if (!context)
1005
+ throw new Error("[guide] useHotspots must be used inside a HotspotProvider");
1006
+ const { seen, translate, restored, open, startTour, reset, notifyShown } = context;
1007
+ const hotspots = useMemo6(
1008
+ () => context.hotspots.map((hotspot) => ({
1009
+ id: hotspot.id,
1010
+ target: hotspot.target,
1011
+ title: resolveText(hotspot.title, hotspot.titleKey, translate),
1012
+ body: resolveText(hotspot.body, hotspot.bodyKey, translate),
1013
+ seen: seen.includes(hotspot.id),
1014
+ tourId: hotspot.tourId,
1015
+ placement: hotspot.placement
1016
+ })),
1017
+ [context.hotspots, seen, translate]
1018
+ );
1019
+ return useMemo6(
1020
+ () => ({ hotspots, restored, open, startTour, reset, notifyShown }),
1021
+ [hotspots, restored, open, startTour, reset, notifyShown]
1022
+ );
1023
+ }
789
1024
  export {
790
1025
  ChecklistContext,
791
1026
  ChecklistProvider,
792
1027
  GuideContext,
793
1028
  GuideProvider,
1029
+ HotspotContext,
1030
+ HotspotProvider,
794
1031
  createBrowserStorage,
795
1032
  createMemoryStorage,
796
1033
  findMissingTargets,
797
1034
  initialTourState,
798
1035
  isChecklistProgress,
1036
+ isHotspotsProgress,
799
1037
  isLiteralRoute,
800
1038
  isTourProgress,
801
1039
  matchRoute,
@@ -806,6 +1044,7 @@ export {
806
1044
  useElementRect,
807
1045
  useFocusTrap,
808
1046
  useGuideStep,
1047
+ useHotspots,
809
1048
  usePrefersReducedMotion,
810
1049
  useTargetElement,
811
1050
  useTour