@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.mjs CHANGED
@@ -4,36 +4,51 @@
4
4
  function createMemoryStorage(initial = {}) {
5
5
  const store = new Map(Object.entries(initial));
6
6
  return {
7
- async read(tourId) {
8
- return store.get(tourId) ?? null;
7
+ async read(key) {
8
+ return store.get(key) ?? null;
9
9
  },
10
- async write(tourId, progress) {
11
- store.set(tourId, progress);
10
+ async write(key, value) {
11
+ store.set(key, value);
12
12
  }
13
13
  };
14
14
  }
15
15
  function createBrowserStorage(namespace = "guide") {
16
- const key = (tourId) => `${namespace}:${tourId}`;
16
+ const key = (storageKey) => `${namespace}:${storageKey}`;
17
17
  const available = () => typeof window !== "undefined" && !!window.localStorage;
18
18
  return {
19
- async read(tourId) {
19
+ async read(storageKey) {
20
20
  if (!available()) return null;
21
21
  try {
22
- const raw = window.localStorage.getItem(key(tourId));
22
+ const raw = window.localStorage.getItem(key(storageKey));
23
23
  return raw ? JSON.parse(raw) : null;
24
24
  } catch {
25
25
  return null;
26
26
  }
27
27
  },
28
- async write(tourId, progress) {
28
+ async write(storageKey, value) {
29
29
  if (!available()) return;
30
30
  try {
31
- window.localStorage.setItem(key(tourId), JSON.stringify(progress));
31
+ window.localStorage.setItem(key(storageKey), JSON.stringify(value));
32
32
  } catch {
33
33
  }
34
34
  }
35
35
  };
36
36
  }
37
+ function isTourProgress(value) {
38
+ if (typeof value !== "object" || value === null) return false;
39
+ const candidate = value;
40
+ return typeof candidate.stepIndex === "number" && Number.isInteger(candidate.stepIndex) && candidate.stepIndex >= 0 && (candidate.status === "in-progress" || candidate.status === "completed");
41
+ }
42
+ function isChecklistProgress(value) {
43
+ if (typeof value !== "object" || value === null) return false;
44
+ const candidate = value;
45
+ return Array.isArray(candidate.completed) && candidate.completed.every((entry) => typeof entry === "string") && typeof candidate.dismissed === "boolean";
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
+ }
37
52
 
38
53
  // src/matchRoute.ts
39
54
  function segments(value) {
@@ -263,14 +278,34 @@ function findMissingTargets(tour, location, attribute = "data-guide") {
263
278
  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)));
264
279
  }
265
280
 
266
- // src/GuideProvider.tsx
267
- import { jsx } from "react/jsx-runtime";
268
- var GuideContext = createContext(null);
281
+ // src/resolveText.ts
269
282
  function resolveText(value, key, translate) {
270
283
  if (value !== void 0) return value;
271
284
  if (key === void 0) return "";
272
285
  return translate ? translate(key) : key;
273
286
  }
287
+
288
+ // src/GuideProvider.tsx
289
+ import { jsx } from "react/jsx-runtime";
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
+ }
274
309
  function GuideProvider({
275
310
  tours,
276
311
  children,
@@ -295,6 +330,7 @@ function GuideProvider({
295
330
  const [state, dispatch] = useReducer(tourReducer, initialTourState);
296
331
  const announce = useAnnouncer();
297
332
  const focusOriginRef = useRef(null);
333
+ const lastElementRef = useRef(null);
298
334
  const storageWarnedRef = useRef(false);
299
335
  const warnStorageFailure = useCallback2((error) => {
300
336
  if (storageWarnedRef.current) return;
@@ -330,6 +366,14 @@ function GuideProvider({
330
366
  dispatch({ type: "NEXT", stepCount: tour.steps.length });
331
367
  if (isLast) emit({ type: "tour:complete", tourId: tour.id });
332
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]);
333
377
  const previous = useCallback2(() => dispatch({ type: "PREVIOUS" }), []);
334
378
  const stop = useCallback2(() => {
335
379
  if (tour) emit({ type: "tour:stop", tourId: tour.id, stepIndex: state.stepIndex });
@@ -354,7 +398,8 @@ function GuideProvider({
354
398
  if (options?.from === void 0 && options?.resume !== false && storage) {
355
399
  let progress = null;
356
400
  try {
357
- progress = await storage.read(tourId);
401
+ const stored = await storage.read(`tour:${tourId}`);
402
+ progress = isTourProgress(stored) ? stored : null;
358
403
  } catch (error) {
359
404
  warnStorageFailure(error);
360
405
  }
@@ -421,18 +466,29 @@ function GuideProvider({
421
466
  if (!status) return;
422
467
  try {
423
468
  void Promise.resolve(
424
- storage.write(state.tourId, { status, stepIndex: state.stepIndex })
469
+ storage.write(`tour:${state.tourId}`, { status, stepIndex: state.stepIndex })
425
470
  ).catch(warnStorageFailure);
426
471
  } catch (error) {
427
472
  warnStorageFailure(error);
428
473
  }
429
474
  }, [storage, state.tourId, state.status, state.stepIndex, warnStorageFailure]);
475
+ useEffect4(() => {
476
+ if (element) lastElementRef.current = element;
477
+ }, [element]);
430
478
  useEffect4(() => {
431
479
  if (state.status !== "idle" && state.status !== "completed") return;
432
480
  const origin = focusOriginRef.current;
433
- if (!origin) return;
481
+ const fallback = lastElementRef.current;
434
482
  focusOriginRef.current = null;
435
- 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);
436
492
  }, [state.status]);
437
493
  const activeStep = useMemo(() => {
438
494
  if (!tour || !step || !isActive) return null;
@@ -443,6 +499,8 @@ function GuideProvider({
443
499
  stepCount: tour.steps.length,
444
500
  element,
445
501
  rect,
502
+ interactive: step.interactive === true || step.advanceOn !== void 0,
503
+ awaitsAction: step.advanceOn !== void 0,
446
504
  title: resolveText(step.title, step.titleKey, translate),
447
505
  body: resolveText(step.body, step.bodyKey, translate),
448
506
  isFirst: state.stepIndex === 0,
@@ -486,20 +544,507 @@ function useGuideStep() {
486
544
  if (!context) throw new Error("[guide] useGuideStep must be used inside a GuideProvider");
487
545
  return context.activeStep;
488
546
  }
547
+
548
+ // src/ChecklistProvider.tsx
549
+ import {
550
+ createContext as createContext2,
551
+ useCallback as useCallback3,
552
+ useContext as useContext3,
553
+ useEffect as useEffect5,
554
+ useMemo as useMemo3,
555
+ useRef as useRef2,
556
+ useState as useState5
557
+ } from "react";
558
+ import { jsx as jsx2 } from "react/jsx-runtime";
559
+ var ChecklistContext = createContext2(null);
560
+ var emptyProgress = { completed: [], dismissed: false };
561
+ function ChecklistProvider({
562
+ checklists,
563
+ children,
564
+ storage,
565
+ translate,
566
+ navigate,
567
+ onEvent
568
+ }) {
569
+ const checklistsById = useMemo3(() => {
570
+ const map = /* @__PURE__ */ new Map();
571
+ for (const candidate of checklists) map.set(candidate.id, candidate);
572
+ return map;
573
+ }, [checklists]);
574
+ const [progress, setProgress] = useState5(() => {
575
+ const initial = {};
576
+ for (const candidate of checklists) initial[candidate.id] = emptyProgress;
577
+ return initial;
578
+ });
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
+ });
585
+ const guide = useContext3(GuideContext);
586
+ const storageWarnedRef = useRef2(false);
587
+ const warnStorageFailure = useCallback3((error) => {
588
+ if (storageWarnedRef.current) return;
589
+ storageWarnedRef.current = true;
590
+ console.warn("[guide] storage failed; checklist progress will not be persisted", error);
591
+ }, []);
592
+ const noGuideWarnedRef = useRef2(false);
593
+ const warnNoGuide = useCallback3(() => {
594
+ if (noGuideWarnedRef.current) return;
595
+ noGuideWarnedRef.current = true;
596
+ console.warn("[guide] a checklist item needs a GuideProvider to launch a tour");
597
+ }, []);
598
+ const tourStartFailedWarnedRef = useRef2(false);
599
+ const warnTourStartFailure = useCallback3((error) => {
600
+ if (tourStartFailedWarnedRef.current) return;
601
+ tourStartFailedWarnedRef.current = true;
602
+ console.warn("[guide] starting a tour for a checklist item failed", error);
603
+ }, []);
604
+ const noNavigateWarnedRef = useRef2(false);
605
+ const warnNoNavigate = useCallback3(() => {
606
+ if (noNavigateWarnedRef.current) return;
607
+ noNavigateWarnedRef.current = true;
608
+ console.warn("[guide] a checklist item declares an href but no navigate function was provided");
609
+ }, []);
610
+ const onEventRef = useRef2(onEvent);
611
+ onEventRef.current = onEvent;
612
+ const emit = useCallback3((event) => onEventRef.current?.(event), []);
613
+ useEffect5(() => {
614
+ if (!storage) return;
615
+ let cancelled = false;
616
+ for (const candidate of checklists) {
617
+ void (async () => {
618
+ try {
619
+ const stored = await storage.read(`checklist:${candidate.id}`);
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
+ }
634
+ } catch (error) {
635
+ warnStorageFailure(error);
636
+ } finally {
637
+ if (!cancelled) {
638
+ setRestoredById((current) => ({ ...current, [candidate.id]: true }));
639
+ }
640
+ }
641
+ })();
642
+ }
643
+ return () => {
644
+ cancelled = true;
645
+ };
646
+ }, [storage]);
647
+ const applyProgress = useCallback3(
648
+ (checklistId, next) => {
649
+ const merged = { ...progressRef.current, [checklistId]: next };
650
+ progressRef.current = merged;
651
+ setProgress(merged);
652
+ if (!storage) return;
653
+ try {
654
+ void Promise.resolve(storage.write(`checklist:${checklistId}`, next)).catch(
655
+ warnStorageFailure
656
+ );
657
+ } catch (error) {
658
+ warnStorageFailure(error);
659
+ }
660
+ },
661
+ [storage, warnStorageFailure]
662
+ );
663
+ const resolveChecklist = useCallback3(
664
+ (checklistId) => {
665
+ const checklist = checklistsById.get(checklistId);
666
+ if (!checklist) {
667
+ console.warn(`[guide] unknown checklist "${checklistId}"`);
668
+ return null;
669
+ }
670
+ return checklist;
671
+ },
672
+ [checklistsById]
673
+ );
674
+ const resolveItem = useCallback3(
675
+ (checklistId, itemId) => {
676
+ const checklist = resolveChecklist(checklistId);
677
+ if (!checklist) return null;
678
+ const item = checklist.items.find((candidate) => candidate.id === itemId);
679
+ if (!item) {
680
+ console.warn(`[guide] unknown checklist item "${itemId}"`);
681
+ return null;
682
+ }
683
+ return { checklist, item };
684
+ },
685
+ [resolveChecklist]
686
+ );
687
+ const complete = useCallback3(
688
+ (checklistId, itemId) => {
689
+ const resolved = resolveItem(checklistId, itemId);
690
+ if (!resolved) return;
691
+ const { checklist } = resolved;
692
+ const current = progressRef.current[checklistId] ?? emptyProgress;
693
+ if (current.completed.includes(itemId)) return;
694
+ const wasComplete = checklist.items.length > 0 && checklist.items.every((candidate) => current.completed.includes(candidate.id));
695
+ const nextCompleted = [...current.completed, itemId];
696
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
697
+ emit({ type: "checklist:item-complete", checklistId, itemId });
698
+ const isNowComplete = checklist.items.length > 0 && checklist.items.every((candidate) => nextCompleted.includes(candidate.id));
699
+ if (isNowComplete && !wasComplete) emit({ type: "checklist:complete", checklistId });
700
+ },
701
+ [resolveItem, applyProgress, emit]
702
+ );
703
+ const completeItemsForTour = useCallback3(
704
+ (tourId) => {
705
+ for (const candidate of checklists) {
706
+ for (const item of candidate.items) {
707
+ if (item.tourId === tourId) complete(candidate.id, item.id);
708
+ }
709
+ }
710
+ },
711
+ [checklists, complete]
712
+ );
713
+ const handledCompletionRef = useRef2(null);
714
+ useEffect5(() => {
715
+ const state = guide?.state;
716
+ if (!state || state.status !== "completed" || !state.tourId) {
717
+ handledCompletionRef.current = null;
718
+ return;
719
+ }
720
+ if (handledCompletionRef.current === state.tourId) return;
721
+ handledCompletionRef.current = state.tourId;
722
+ completeItemsForTour(state.tourId);
723
+ }, [guide?.state, completeItemsForTour]);
724
+ const toggle = useCallback3(
725
+ (checklistId, itemId) => {
726
+ const resolved = resolveItem(checklistId, itemId);
727
+ if (!resolved) return;
728
+ const current = progressRef.current[checklistId] ?? emptyProgress;
729
+ if (current.completed.includes(itemId)) {
730
+ const nextCompleted = current.completed.filter((id) => id !== itemId);
731
+ applyProgress(checklistId, { ...current, completed: nextCompleted });
732
+ return;
733
+ }
734
+ complete(checklistId, itemId);
735
+ },
736
+ [resolveItem, applyProgress, complete]
737
+ );
738
+ const dismiss = useCallback3(
739
+ (checklistId) => {
740
+ if (!resolveChecklist(checklistId)) return;
741
+ const current = progressRef.current[checklistId] ?? emptyProgress;
742
+ applyProgress(checklistId, { ...current, dismissed: true });
743
+ emit({ type: "checklist:dismiss", checklistId });
744
+ },
745
+ [resolveChecklist, applyProgress, emit]
746
+ );
747
+ const reset = useCallback3(
748
+ (checklistId) => {
749
+ if (!resolveChecklist(checklistId)) return;
750
+ applyProgress(checklistId, { completed: [], dismissed: false });
751
+ },
752
+ [resolveChecklist, applyProgress]
753
+ );
754
+ const activate = useCallback3(
755
+ (checklistId, itemId) => {
756
+ const resolved = resolveItem(checklistId, itemId);
757
+ if (!resolved) return;
758
+ const { item } = resolved;
759
+ if (item.tourId) {
760
+ if (!guide) {
761
+ warnNoGuide();
762
+ return;
763
+ }
764
+ void guide.start(item.tourId).catch(warnTourStartFailure);
765
+ return;
766
+ }
767
+ if (item.href) {
768
+ if (!navigate) {
769
+ warnNoNavigate();
770
+ return;
771
+ }
772
+ navigate(item.href);
773
+ return;
774
+ }
775
+ toggle(checklistId, itemId);
776
+ },
777
+ [resolveItem, guide, navigate, toggle, warnNoGuide, warnNoNavigate, warnTourStartFailure]
778
+ );
779
+ const value = useMemo3(
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]
792
+ );
793
+ return /* @__PURE__ */ jsx2(ChecklistContext.Provider, { value, children });
794
+ }
795
+
796
+ // src/useChecklist.ts
797
+ import { useContext as useContext4, useMemo as useMemo4 } from "react";
798
+ function useChecklist(checklistId) {
799
+ const context = useContext4(ChecklistContext);
800
+ if (!context)
801
+ throw new Error("[guide] useChecklist must be used inside a ChecklistProvider");
802
+ const checklist = context.checklists.find((entry) => entry.id === checklistId);
803
+ if (!checklist) throw new Error(`[guide] unknown checklist "${checklistId}"`);
804
+ const progress = context.progress[checklistId];
805
+ const completed = progress?.completed ?? [];
806
+ const dismissed = progress?.dismissed ?? false;
807
+ const translate = context.translate;
808
+ const restored = context.restored[checklistId] ?? true;
809
+ const items = useMemo4(
810
+ () => checklist.items.map((item) => ({
811
+ id: item.id,
812
+ title: resolveText(item.title, item.titleKey, translate),
813
+ body: resolveText(item.body, item.bodyKey, translate),
814
+ completed: completed.includes(item.id),
815
+ tourId: item.tourId,
816
+ href: item.href
817
+ })),
818
+ [checklist, completed, translate]
819
+ );
820
+ const total = checklist.items.length;
821
+ const completedCount = items.filter((item) => item.completed).length;
822
+ const isComplete = total > 0 && completedCount === total;
823
+ const { activate, toggle, complete, dismiss, reset } = context;
824
+ return useMemo4(
825
+ () => ({
826
+ items,
827
+ completedCount,
828
+ total,
829
+ isComplete,
830
+ dismissed,
831
+ restored,
832
+ activate: (itemId) => activate(checklistId, itemId),
833
+ toggle: (itemId) => toggle(checklistId, itemId),
834
+ complete: (itemId) => complete(checklistId, itemId),
835
+ dismiss: () => dismiss(checklistId),
836
+ reset: () => reset(checklistId)
837
+ }),
838
+ [
839
+ items,
840
+ completedCount,
841
+ total,
842
+ isComplete,
843
+ dismissed,
844
+ restored,
845
+ activate,
846
+ toggle,
847
+ complete,
848
+ dismiss,
849
+ reset,
850
+ checklistId
851
+ ]
852
+ );
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
+ }
489
1024
  export {
1025
+ ChecklistContext,
1026
+ ChecklistProvider,
490
1027
  GuideContext,
491
1028
  GuideProvider,
1029
+ HotspotContext,
1030
+ HotspotProvider,
492
1031
  createBrowserStorage,
493
1032
  createMemoryStorage,
494
1033
  findMissingTargets,
495
1034
  initialTourState,
1035
+ isChecklistProgress,
1036
+ isHotspotsProgress,
496
1037
  isLiteralRoute,
1038
+ isTourProgress,
497
1039
  matchRoute,
1040
+ resolveText,
498
1041
  tourReducer,
499
1042
  useAnnouncer,
1043
+ useChecklist,
500
1044
  useElementRect,
501
1045
  useFocusTrap,
502
1046
  useGuideStep,
1047
+ useHotspots,
503
1048
  usePrefersReducedMotion,
504
1049
  useTargetElement,
505
1050
  useTour