@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/README.md +295 -15
- package/dist/index.cjs +555 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -4
- package/dist/index.d.ts +202 -4
- package/dist/index.mjs +561 -16
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
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(
|
|
48
|
-
return store.get(
|
|
57
|
+
async read(key) {
|
|
58
|
+
return store.get(key) ?? null;
|
|
49
59
|
},
|
|
50
|
-
async write(
|
|
51
|
-
store.set(
|
|
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 = (
|
|
66
|
+
const key = (storageKey) => `${namespace}:${storageKey}`;
|
|
57
67
|
const available = () => typeof window !== "undefined" && !!window.localStorage;
|
|
58
68
|
return {
|
|
59
|
-
async read(
|
|
69
|
+
async read(storageKey) {
|
|
60
70
|
if (!available()) return null;
|
|
61
71
|
try {
|
|
62
|
-
const raw = window.localStorage.getItem(key(
|
|
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(
|
|
78
|
+
async write(storageKey, value) {
|
|
69
79
|
if (!available()) return;
|
|
70
80
|
try {
|
|
71
|
-
window.localStorage.setItem(key(
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
523
|
+
const fallback = lastElementRef.current;
|
|
466
524
|
focusOriginRef.current = null;
|
|
467
|
-
|
|
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
|