@apollovisionlabs/guide-core 0.1.1 → 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.cjs CHANGED
@@ -21,19 +21,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/index.ts
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
+ ChecklistContext: () => ChecklistContext,
25
+ ChecklistProvider: () => ChecklistProvider,
24
26
  GuideContext: () => GuideContext,
25
27
  GuideProvider: () => GuideProvider,
28
+ HotspotContext: () => HotspotContext,
29
+ HotspotProvider: () => HotspotProvider,
26
30
  createBrowserStorage: () => createBrowserStorage,
27
31
  createMemoryStorage: () => createMemoryStorage,
28
32
  findMissingTargets: () => findMissingTargets,
29
33
  initialTourState: () => initialTourState,
34
+ isChecklistProgress: () => isChecklistProgress,
35
+ isHotspotsProgress: () => isHotspotsProgress,
30
36
  isLiteralRoute: () => isLiteralRoute,
37
+ isTourProgress: () => isTourProgress,
31
38
  matchRoute: () => matchRoute,
39
+ resolveText: () => resolveText,
32
40
  tourReducer: () => tourReducer,
33
41
  useAnnouncer: () => useAnnouncer,
42
+ useChecklist: () => useChecklist,
34
43
  useElementRect: () => useElementRect,
35
44
  useFocusTrap: () => useFocusTrap,
36
45
  useGuideStep: () => useGuideStep,
46
+ useHotspots: () => useHotspots,
37
47
  usePrefersReducedMotion: () => usePrefersReducedMotion,
38
48
  useTargetElement: () => useTargetElement,
39
49
  useTour: () => useTour
@@ -44,36 +54,51 @@ module.exports = __toCommonJS(index_exports);
44
54
  function createMemoryStorage(initial = {}) {
45
55
  const store = new Map(Object.entries(initial));
46
56
  return {
47
- async read(tourId) {
48
- return store.get(tourId) ?? null;
57
+ async read(key) {
58
+ return store.get(key) ?? null;
49
59
  },
50
- async write(tourId, progress) {
51
- store.set(tourId, progress);
60
+ async write(key, value) {
61
+ store.set(key, value);
52
62
  }
53
63
  };
54
64
  }
55
65
  function createBrowserStorage(namespace = "guide") {
56
- const key = (tourId) => `${namespace}:${tourId}`;
66
+ const key = (storageKey) => `${namespace}:${storageKey}`;
57
67
  const available = () => typeof window !== "undefined" && !!window.localStorage;
58
68
  return {
59
- async read(tourId) {
69
+ async read(storageKey) {
60
70
  if (!available()) return null;
61
71
  try {
62
- const raw = window.localStorage.getItem(key(tourId));
72
+ const raw = window.localStorage.getItem(key(storageKey));
63
73
  return raw ? JSON.parse(raw) : null;
64
74
  } catch {
65
75
  return null;
66
76
  }
67
77
  },
68
- async write(tourId, progress) {
78
+ async write(storageKey, value) {
69
79
  if (!available()) return;
70
80
  try {
71
- window.localStorage.setItem(key(tourId), JSON.stringify(progress));
81
+ window.localStorage.setItem(key(storageKey), JSON.stringify(value));
72
82
  } catch {
73
83
  }
74
84
  }
75
85
  };
76
86
  }
87
+ function isTourProgress(value) {
88
+ if (typeof value !== "object" || value === null) return false;
89
+ const candidate = value;
90
+ return typeof candidate.stepIndex === "number" && Number.isInteger(candidate.stepIndex) && candidate.stepIndex >= 0 && (candidate.status === "in-progress" || candidate.status === "completed");
91
+ }
92
+ function isChecklistProgress(value) {
93
+ if (typeof value !== "object" || value === null) return false;
94
+ const candidate = value;
95
+ return Array.isArray(candidate.completed) && candidate.completed.every((entry) => typeof entry === "string") && typeof candidate.dismissed === "boolean";
96
+ }
97
+ function isHotspotsProgress(value) {
98
+ if (typeof value !== "object" || value === null) return false;
99
+ const candidate = value;
100
+ return Array.isArray(candidate.seen) && candidate.seen.every((entry) => typeof entry === "string");
101
+ }
77
102
 
78
103
  // src/matchRoute.ts
79
104
  function segments(value) {
@@ -295,14 +320,34 @@ function findMissingTargets(tour, location, attribute = "data-guide") {
295
320
  return tour.steps.filter((step) => !step.route || location === void 0 || matchRoute(step.route, location)).map((step) => step.target).filter((target) => !document.querySelector(targetSelector(target, attribute)));
296
321
  }
297
322
 
298
- // src/GuideProvider.tsx
299
- var import_jsx_runtime = require("react/jsx-runtime");
300
- var GuideContext = (0, import_react4.createContext)(null);
323
+ // src/resolveText.ts
301
324
  function resolveText(value, key, translate) {
302
325
  if (value !== void 0) return value;
303
326
  if (key === void 0) return "";
304
327
  return translate ? translate(key) : key;
305
328
  }
329
+
330
+ // src/GuideProvider.tsx
331
+ var import_jsx_runtime = require("react/jsx-runtime");
332
+ var GuideContext = (0, import_react4.createContext)(null);
333
+ function focusFallback(element) {
334
+ const needsTabIndex = !element.hasAttribute("tabindex") && element.tabIndex < 0;
335
+ if (!needsTabIndex) {
336
+ element.focus();
337
+ return;
338
+ }
339
+ element.setAttribute("tabindex", "-1");
340
+ element.focus();
341
+ if (document.activeElement !== element) {
342
+ element.removeAttribute("tabindex");
343
+ return;
344
+ }
345
+ const onBlur = () => {
346
+ element.removeAttribute("tabindex");
347
+ element.removeEventListener("blur", onBlur);
348
+ };
349
+ element.addEventListener("blur", onBlur);
350
+ }
306
351
  function GuideProvider({
307
352
  tours,
308
353
  children,
@@ -327,6 +372,7 @@ function GuideProvider({
327
372
  const [state, dispatch] = (0, import_react4.useReducer)(tourReducer, initialTourState);
328
373
  const announce = useAnnouncer();
329
374
  const focusOriginRef = (0, import_react4.useRef)(null);
375
+ const lastElementRef = (0, import_react4.useRef)(null);
330
376
  const storageWarnedRef = (0, import_react4.useRef)(false);
331
377
  const warnStorageFailure = (0, import_react4.useCallback)((error) => {
332
378
  if (storageWarnedRef.current) return;
@@ -362,6 +408,14 @@ function GuideProvider({
362
408
  dispatch({ type: "NEXT", stepCount: tour.steps.length });
363
409
  if (isLast) emit({ type: "tour:complete", tourId: tour.id });
364
410
  }, [tour, state.stepIndex, emit]);
411
+ const nextRef = (0, import_react4.useRef)(next);
412
+ nextRef.current = next;
413
+ (0, import_react4.useEffect)(() => {
414
+ if (state.status !== "running" || !element || step?.advanceOn !== "click") return;
415
+ const onClick = () => nextRef.current();
416
+ element.addEventListener("click", onClick);
417
+ return () => element.removeEventListener("click", onClick);
418
+ }, [state.status, element, step?.advanceOn]);
365
419
  const previous = (0, import_react4.useCallback)(() => dispatch({ type: "PREVIOUS" }), []);
366
420
  const stop = (0, import_react4.useCallback)(() => {
367
421
  if (tour) emit({ type: "tour:stop", tourId: tour.id, stepIndex: state.stepIndex });
@@ -386,7 +440,8 @@ function GuideProvider({
386
440
  if (options?.from === void 0 && options?.resume !== false && storage) {
387
441
  let progress = null;
388
442
  try {
389
- progress = await storage.read(tourId);
443
+ const stored = await storage.read(`tour:${tourId}`);
444
+ progress = isTourProgress(stored) ? stored : null;
390
445
  } catch (error) {
391
446
  warnStorageFailure(error);
392
447
  }
@@ -453,18 +508,29 @@ function GuideProvider({
453
508
  if (!status) return;
454
509
  try {
455
510
  void Promise.resolve(
456
- storage.write(state.tourId, { status, stepIndex: state.stepIndex })
511
+ storage.write(`tour:${state.tourId}`, { status, stepIndex: state.stepIndex })
457
512
  ).catch(warnStorageFailure);
458
513
  } catch (error) {
459
514
  warnStorageFailure(error);
460
515
  }
461
516
  }, [storage, state.tourId, state.status, state.stepIndex, warnStorageFailure]);
517
+ (0, import_react4.useEffect)(() => {
518
+ if (element) lastElementRef.current = element;
519
+ }, [element]);
462
520
  (0, import_react4.useEffect)(() => {
463
521
  if (state.status !== "idle" && state.status !== "completed") return;
464
522
  const origin = focusOriginRef.current;
465
- if (!origin) return;
523
+ const fallback = lastElementRef.current;
466
524
  focusOriginRef.current = null;
467
- if (typeof document !== "undefined" && document.contains(origin)) origin.focus();
525
+ lastElementRef.current = null;
526
+ if (typeof document === "undefined") return;
527
+ if (origin && document.contains(origin)) {
528
+ origin.focus();
529
+ return;
530
+ }
531
+ if (document.activeElement !== document.body) return;
532
+ if (!fallback || !document.contains(fallback)) return;
533
+ focusFallback(fallback);
468
534
  }, [state.status]);
469
535
  const activeStep = (0, import_react4.useMemo)(() => {
470
536
  if (!tour || !step || !isActive) return null;
@@ -475,6 +541,8 @@ function GuideProvider({
475
541
  stepCount: tour.steps.length,
476
542
  element,
477
543
  rect,
544
+ interactive: step.interactive === true || step.advanceOn !== void 0,
545
+ awaitsAction: step.advanceOn !== void 0,
478
546
  title: resolveText(step.title, step.titleKey, translate),
479
547
  body: resolveText(step.body, step.bodyKey, translate),
480
548
  isFirst: state.stepIndex === 0,
@@ -518,21 +586,492 @@ function useGuideStep() {
518
586
  if (!context) throw new Error("[guide] useGuideStep must be used inside a GuideProvider");
519
587
  return context.activeStep;
520
588
  }
589
+
590
+ // src/ChecklistProvider.tsx
591
+ var import_react7 = require("react");
592
+ var import_jsx_runtime2 = require("react/jsx-runtime");
593
+ var ChecklistContext = (0, import_react7.createContext)(null);
594
+ var emptyProgress = { completed: [], dismissed: false };
595
+ function ChecklistProvider({
596
+ checklists,
597
+ children,
598
+ storage,
599
+ translate,
600
+ navigate,
601
+ onEvent
602
+ }) {
603
+ const checklistsById = (0, import_react7.useMemo)(() => {
604
+ const map = /* @__PURE__ */ new Map();
605
+ for (const candidate of checklists) map.set(candidate.id, candidate);
606
+ return map;
607
+ }, [checklists]);
608
+ const [progress, setProgress] = (0, import_react7.useState)(() => {
609
+ const initial = {};
610
+ for (const candidate of checklists) initial[candidate.id] = emptyProgress;
611
+ return initial;
612
+ });
613
+ const progressRef = (0, import_react7.useRef)(progress);
614
+ const [restoredById, setRestoredById] = (0, import_react7.useState)(() => {
615
+ const initial = {};
616
+ for (const candidate of checklists) initial[candidate.id] = !storage;
617
+ return initial;
618
+ });
619
+ const guide = (0, import_react7.useContext)(GuideContext);
620
+ const storageWarnedRef = (0, import_react7.useRef)(false);
621
+ const warnStorageFailure = (0, import_react7.useCallback)((error) => {
622
+ if (storageWarnedRef.current) return;
623
+ storageWarnedRef.current = true;
624
+ console.warn("[guide] storage failed; checklist progress will not be persisted", error);
625
+ }, []);
626
+ const noGuideWarnedRef = (0, import_react7.useRef)(false);
627
+ const warnNoGuide = (0, import_react7.useCallback)(() => {
628
+ if (noGuideWarnedRef.current) return;
629
+ noGuideWarnedRef.current = true;
630
+ console.warn("[guide] a checklist item needs a GuideProvider to launch a tour");
631
+ }, []);
632
+ const tourStartFailedWarnedRef = (0, import_react7.useRef)(false);
633
+ const warnTourStartFailure = (0, import_react7.useCallback)((error) => {
634
+ if (tourStartFailedWarnedRef.current) return;
635
+ tourStartFailedWarnedRef.current = true;
636
+ console.warn("[guide] starting a tour for a checklist item failed", error);
637
+ }, []);
638
+ const noNavigateWarnedRef = (0, import_react7.useRef)(false);
639
+ const warnNoNavigate = (0, import_react7.useCallback)(() => {
640
+ if (noNavigateWarnedRef.current) return;
641
+ noNavigateWarnedRef.current = true;
642
+ console.warn("[guide] a checklist item declares an href but no navigate function was provided");
643
+ }, []);
644
+ const onEventRef = (0, import_react7.useRef)(onEvent);
645
+ onEventRef.current = onEvent;
646
+ const emit = (0, import_react7.useCallback)((event) => onEventRef.current?.(event), []);
647
+ (0, import_react7.useEffect)(() => {
648
+ if (!storage) return;
649
+ let cancelled = false;
650
+ for (const candidate of checklists) {
651
+ void (async () => {
652
+ try {
653
+ const stored = await storage.read(`checklist:${candidate.id}`);
654
+ if (!cancelled && isChecklistProgress(stored)) {
655
+ const live = progressRef.current[candidate.id] ?? emptyProgress;
656
+ const merged = {
657
+ ...progressRef.current,
658
+ [candidate.id]: {
659
+ completed: live.completed.concat(
660
+ stored.completed.filter((id) => !live.completed.includes(id))
661
+ ),
662
+ dismissed: live.dismissed || stored.dismissed
663
+ }
664
+ };
665
+ progressRef.current = merged;
666
+ setProgress(merged);
667
+ }
668
+ } catch (error) {
669
+ warnStorageFailure(error);
670
+ } finally {
671
+ if (!cancelled) {
672
+ setRestoredById((current) => ({ ...current, [candidate.id]: true }));
673
+ }
674
+ }
675
+ })();
676
+ }
677
+ return () => {
678
+ cancelled = true;
679
+ };
680
+ }, [storage]);
681
+ const applyProgress = (0, import_react7.useCallback)(
682
+ (checklistId, next) => {
683
+ const merged = { ...progressRef.current, [checklistId]: next };
684
+ progressRef.current = merged;
685
+ setProgress(merged);
686
+ if (!storage) return;
687
+ try {
688
+ void Promise.resolve(storage.write(`checklist:${checklistId}`, next)).catch(
689
+ warnStorageFailure
690
+ );
691
+ } catch (error) {
692
+ warnStorageFailure(error);
693
+ }
694
+ },
695
+ [storage, warnStorageFailure]
696
+ );
697
+ const resolveChecklist = (0, import_react7.useCallback)(
698
+ (checklistId) => {
699
+ const checklist = checklistsById.get(checklistId);
700
+ if (!checklist) {
701
+ console.warn(`[guide] unknown checklist "${checklistId}"`);
702
+ return null;
703
+ }
704
+ return checklist;
705
+ },
706
+ [checklistsById]
707
+ );
708
+ const resolveItem = (0, import_react7.useCallback)(
709
+ (checklistId, itemId) => {
710
+ const checklist = resolveChecklist(checklistId);
711
+ if (!checklist) return null;
712
+ const item = checklist.items.find((candidate) => candidate.id === itemId);
713
+ if (!item) {
714
+ console.warn(`[guide] unknown checklist item "${itemId}"`);
715
+ return null;
716
+ }
717
+ return { checklist, item };
718
+ },
719
+ [resolveChecklist]
720
+ );
721
+ const complete = (0, import_react7.useCallback)(
722
+ (checklistId, itemId) => {
723
+ const resolved = resolveItem(checklistId, itemId);
724
+ if (!resolved) return;
725
+ const { checklist } = resolved;
726
+ const current = progressRef.current[checklistId] ?? emptyProgress;
727
+ if (current.completed.includes(itemId)) return;
728
+ const wasComplete = checklist.items.length > 0 && checklist.items.every((candidate) => current.completed.includes(candidate.id));
729
+ const nextCompleted = [...current.completed, itemId];
730
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
731
+ emit({ type: "checklist:item-complete", checklistId, itemId });
732
+ const isNowComplete = checklist.items.length > 0 && checklist.items.every((candidate) => nextCompleted.includes(candidate.id));
733
+ if (isNowComplete && !wasComplete) emit({ type: "checklist:complete", checklistId });
734
+ },
735
+ [resolveItem, applyProgress, emit]
736
+ );
737
+ const completeItemsForTour = (0, import_react7.useCallback)(
738
+ (tourId) => {
739
+ for (const candidate of checklists) {
740
+ for (const item of candidate.items) {
741
+ if (item.tourId === tourId) complete(candidate.id, item.id);
742
+ }
743
+ }
744
+ },
745
+ [checklists, complete]
746
+ );
747
+ const handledCompletionRef = (0, import_react7.useRef)(null);
748
+ (0, import_react7.useEffect)(() => {
749
+ const state = guide?.state;
750
+ if (!state || state.status !== "completed" || !state.tourId) {
751
+ handledCompletionRef.current = null;
752
+ return;
753
+ }
754
+ if (handledCompletionRef.current === state.tourId) return;
755
+ handledCompletionRef.current = state.tourId;
756
+ completeItemsForTour(state.tourId);
757
+ }, [guide?.state, completeItemsForTour]);
758
+ const toggle = (0, import_react7.useCallback)(
759
+ (checklistId, itemId) => {
760
+ const resolved = resolveItem(checklistId, itemId);
761
+ if (!resolved) return;
762
+ const current = progressRef.current[checklistId] ?? emptyProgress;
763
+ if (current.completed.includes(itemId)) {
764
+ const nextCompleted = current.completed.filter((id) => id !== itemId);
765
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
766
+ return;
767
+ }
768
+ complete(checklistId, itemId);
769
+ },
770
+ [resolveItem, applyProgress, complete]
771
+ );
772
+ const dismiss = (0, import_react7.useCallback)(
773
+ (checklistId) => {
774
+ if (!resolveChecklist(checklistId)) return;
775
+ const current = progressRef.current[checklistId] ?? emptyProgress;
776
+ applyProgress(checklistId, { ...current, dismissed: true });
777
+ emit({ type: "checklist:dismiss", checklistId });
778
+ },
779
+ [resolveChecklist, applyProgress, emit]
780
+ );
781
+ const reset = (0, import_react7.useCallback)(
782
+ (checklistId) => {
783
+ if (!resolveChecklist(checklistId)) return;
784
+ applyProgress(checklistId, { completed: [], dismissed: false });
785
+ },
786
+ [resolveChecklist, applyProgress]
787
+ );
788
+ const activate = (0, import_react7.useCallback)(
789
+ (checklistId, itemId) => {
790
+ const resolved = resolveItem(checklistId, itemId);
791
+ if (!resolved) return;
792
+ const { item } = resolved;
793
+ if (item.tourId) {
794
+ if (!guide) {
795
+ warnNoGuide();
796
+ return;
797
+ }
798
+ void guide.start(item.tourId).catch(warnTourStartFailure);
799
+ return;
800
+ }
801
+ if (item.href) {
802
+ if (!navigate) {
803
+ warnNoNavigate();
804
+ return;
805
+ }
806
+ navigate(item.href);
807
+ return;
808
+ }
809
+ toggle(checklistId, itemId);
810
+ },
811
+ [resolveItem, guide, navigate, toggle, warnNoGuide, warnNoNavigate, warnTourStartFailure]
812
+ );
813
+ const value = (0, import_react7.useMemo)(
814
+ () => ({
815
+ checklists,
816
+ progress,
817
+ translate,
818
+ restored: restoredById,
819
+ activate,
820
+ toggle,
821
+ complete,
822
+ dismiss,
823
+ reset
824
+ }),
825
+ [checklists, progress, translate, restoredById, activate, toggle, complete, dismiss, reset]
826
+ );
827
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ChecklistContext.Provider, { value, children });
828
+ }
829
+
830
+ // src/useChecklist.ts
831
+ var import_react8 = require("react");
832
+ function useChecklist(checklistId) {
833
+ const context = (0, import_react8.useContext)(ChecklistContext);
834
+ if (!context)
835
+ throw new Error("[guide] useChecklist must be used inside a ChecklistProvider");
836
+ const checklist = context.checklists.find((entry) => entry.id === checklistId);
837
+ if (!checklist) throw new Error(`[guide] unknown checklist "${checklistId}"`);
838
+ const progress = context.progress[checklistId];
839
+ const completed = progress?.completed ?? [];
840
+ const dismissed = progress?.dismissed ?? false;
841
+ const translate = context.translate;
842
+ const restored = context.restored[checklistId] ?? true;
843
+ const items = (0, import_react8.useMemo)(
844
+ () => checklist.items.map((item) => ({
845
+ id: item.id,
846
+ title: resolveText(item.title, item.titleKey, translate),
847
+ body: resolveText(item.body, item.bodyKey, translate),
848
+ completed: completed.includes(item.id),
849
+ tourId: item.tourId,
850
+ href: item.href
851
+ })),
852
+ [checklist, completed, translate]
853
+ );
854
+ const total = checklist.items.length;
855
+ const completedCount = items.filter((item) => item.completed).length;
856
+ const isComplete = total > 0 && completedCount === total;
857
+ const { activate, toggle, complete, dismiss, reset } = context;
858
+ return (0, import_react8.useMemo)(
859
+ () => ({
860
+ items,
861
+ completedCount,
862
+ total,
863
+ isComplete,
864
+ dismissed,
865
+ restored,
866
+ activate: (itemId) => activate(checklistId, itemId),
867
+ toggle: (itemId) => toggle(checklistId, itemId),
868
+ complete: (itemId) => complete(checklistId, itemId),
869
+ dismiss: () => dismiss(checklistId),
870
+ reset: () => reset(checklistId)
871
+ }),
872
+ [
873
+ items,
874
+ completedCount,
875
+ total,
876
+ isComplete,
877
+ dismissed,
878
+ restored,
879
+ activate,
880
+ toggle,
881
+ complete,
882
+ dismiss,
883
+ reset,
884
+ checklistId
885
+ ]
886
+ );
887
+ }
888
+
889
+ // src/HotspotProvider.tsx
890
+ var import_react9 = require("react");
891
+ var import_jsx_runtime3 = require("react/jsx-runtime");
892
+ var STORAGE_KEY = "hotspots:seen";
893
+ var HotspotContext = (0, import_react9.createContext)(null);
894
+ function HotspotProvider({
895
+ hotspots,
896
+ children,
897
+ storage,
898
+ translate,
899
+ onEvent
900
+ }) {
901
+ const hotspotsById = (0, import_react9.useMemo)(() => {
902
+ const map = /* @__PURE__ */ new Map();
903
+ for (const candidate of hotspots) {
904
+ if (map.has(candidate.id)) {
905
+ throw new Error(`[guide] duplicate hotspot id: ${candidate.id}`);
906
+ }
907
+ map.set(candidate.id, candidate);
908
+ }
909
+ return map;
910
+ }, [hotspots]);
911
+ const [seen, setSeen] = (0, import_react9.useState)([]);
912
+ const [restored, setRestored] = (0, import_react9.useState)(() => !storage);
913
+ const seenRef = (0, import_react9.useRef)(seen);
914
+ const guide = (0, import_react9.useContext)(GuideContext);
915
+ const storageWarnedRef = (0, import_react9.useRef)(false);
916
+ const warnStorageFailure = (0, import_react9.useCallback)((error) => {
917
+ if (storageWarnedRef.current) return;
918
+ storageWarnedRef.current = true;
919
+ console.warn("[guide] storage failed; hotspot state will not be persisted", error);
920
+ }, []);
921
+ const noGuideWarnedRef = (0, import_react9.useRef)(false);
922
+ const warnNoGuide = (0, import_react9.useCallback)(() => {
923
+ if (noGuideWarnedRef.current) return;
924
+ noGuideWarnedRef.current = true;
925
+ console.warn("[guide] a hotspot needs a GuideProvider to launch a tour");
926
+ }, []);
927
+ const tourStartFailedWarnedRef = (0, import_react9.useRef)(false);
928
+ const warnTourStartFailure = (0, import_react9.useCallback)((error) => {
929
+ if (tourStartFailedWarnedRef.current) return;
930
+ tourStartFailedWarnedRef.current = true;
931
+ console.warn("[guide] starting a tour for a hotspot failed", error);
932
+ }, []);
933
+ const onEventRef = (0, import_react9.useRef)(onEvent);
934
+ onEventRef.current = onEvent;
935
+ const emit = (0, import_react9.useCallback)((event) => onEventRef.current?.(event), []);
936
+ (0, import_react9.useEffect)(() => {
937
+ if (!storage) return;
938
+ let cancelled = false;
939
+ void (async () => {
940
+ let stored = null;
941
+ try {
942
+ stored = await storage.read(STORAGE_KEY);
943
+ } catch (error) {
944
+ warnStorageFailure(error);
945
+ if (!cancelled) setRestored(true);
946
+ return;
947
+ }
948
+ if (cancelled) return;
949
+ if (isHotspotsProgress(stored)) {
950
+ const merged = seenRef.current.concat(
951
+ stored.seen.filter((id) => !seenRef.current.includes(id))
952
+ );
953
+ seenRef.current = merged;
954
+ setSeen(merged);
955
+ }
956
+ setRestored(true);
957
+ })();
958
+ return () => {
959
+ cancelled = true;
960
+ };
961
+ }, [storage, warnStorageFailure]);
962
+ const applySeen = (0, import_react9.useCallback)(
963
+ (next) => {
964
+ seenRef.current = next;
965
+ setSeen(next);
966
+ if (!storage) return;
967
+ try {
968
+ void Promise.resolve(storage.write(STORAGE_KEY, { seen: next })).catch(
969
+ warnStorageFailure
970
+ );
971
+ } catch (error) {
972
+ warnStorageFailure(error);
973
+ }
974
+ },
975
+ [storage, warnStorageFailure]
976
+ );
977
+ const resolve = (0, import_react9.useCallback)(
978
+ (hotspotId) => {
979
+ const hotspot = hotspotsById.get(hotspotId);
980
+ if (!hotspot) {
981
+ console.warn(`[guide] unknown hotspot "${hotspotId}"`);
982
+ return null;
983
+ }
984
+ return hotspot;
985
+ },
986
+ [hotspotsById]
987
+ );
988
+ const open = (0, import_react9.useCallback)(
989
+ (hotspotId) => {
990
+ if (!resolve(hotspotId)) return;
991
+ emit({ type: "hotspot:open", hotspotId });
992
+ if (seenRef.current.includes(hotspotId)) return;
993
+ applySeen([...seenRef.current, hotspotId]);
994
+ },
995
+ [resolve, applySeen, emit]
996
+ );
997
+ const startTour = (0, import_react9.useCallback)(
998
+ (hotspotId) => {
999
+ const hotspot = resolve(hotspotId);
1000
+ if (!hotspot?.tourId) return;
1001
+ if (!guide) {
1002
+ warnNoGuide();
1003
+ return;
1004
+ }
1005
+ void guide.start(hotspot.tourId).catch(warnTourStartFailure);
1006
+ },
1007
+ [resolve, guide, warnNoGuide, warnTourStartFailure]
1008
+ );
1009
+ const reset = (0, import_react9.useCallback)(() => applySeen([]), [applySeen]);
1010
+ const shownRef = (0, import_react9.useRef)(/* @__PURE__ */ new Set());
1011
+ const notifyShown = (0, import_react9.useCallback)(
1012
+ (hotspotId) => {
1013
+ if (shownRef.current.has(hotspotId)) return;
1014
+ shownRef.current.add(hotspotId);
1015
+ emit({ type: "hotspot:show", hotspotId });
1016
+ },
1017
+ [emit]
1018
+ );
1019
+ const value = (0, import_react9.useMemo)(
1020
+ () => ({ hotspots, seen, translate, restored, open, startTour, reset, notifyShown }),
1021
+ [hotspots, seen, translate, restored, open, startTour, reset, notifyShown]
1022
+ );
1023
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(HotspotContext.Provider, { value, children });
1024
+ }
1025
+
1026
+ // src/useHotspots.ts
1027
+ var import_react10 = require("react");
1028
+ function useHotspots() {
1029
+ const context = (0, import_react10.useContext)(HotspotContext);
1030
+ if (!context)
1031
+ throw new Error("[guide] useHotspots must be used inside a HotspotProvider");
1032
+ const { seen, translate, restored, open, startTour, reset, notifyShown } = context;
1033
+ const hotspots = (0, import_react10.useMemo)(
1034
+ () => context.hotspots.map((hotspot) => ({
1035
+ id: hotspot.id,
1036
+ target: hotspot.target,
1037
+ title: resolveText(hotspot.title, hotspot.titleKey, translate),
1038
+ body: resolveText(hotspot.body, hotspot.bodyKey, translate),
1039
+ seen: seen.includes(hotspot.id),
1040
+ tourId: hotspot.tourId,
1041
+ placement: hotspot.placement
1042
+ })),
1043
+ [context.hotspots, seen, translate]
1044
+ );
1045
+ return (0, import_react10.useMemo)(
1046
+ () => ({ hotspots, restored, open, startTour, reset, notifyShown }),
1047
+ [hotspots, restored, open, startTour, reset, notifyShown]
1048
+ );
1049
+ }
521
1050
  // Annotate the CommonJS export names for ESM import in node:
522
1051
  0 && (module.exports = {
1052
+ ChecklistContext,
1053
+ ChecklistProvider,
523
1054
  GuideContext,
524
1055
  GuideProvider,
1056
+ HotspotContext,
1057
+ HotspotProvider,
525
1058
  createBrowserStorage,
526
1059
  createMemoryStorage,
527
1060
  findMissingTargets,
528
1061
  initialTourState,
1062
+ isChecklistProgress,
1063
+ isHotspotsProgress,
529
1064
  isLiteralRoute,
1065
+ isTourProgress,
530
1066
  matchRoute,
1067
+ resolveText,
531
1068
  tourReducer,
532
1069
  useAnnouncer,
1070
+ useChecklist,
533
1071
  useElementRect,
534
1072
  useFocusTrap,
535
1073
  useGuideStep,
1074
+ useHotspots,
536
1075
  usePrefersReducedMotion,
537
1076
  useTargetElement,
538
1077
  useTour