@sunboard/react 0.4.0 → 0.6.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.mts +95 -13
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +402 -21
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +85 -0
- package/package.json +4 -4
package/dist/index.mjs
CHANGED
|
@@ -27,12 +27,64 @@ function resolveIdentities(prop) {
|
|
|
27
27
|
function detectAnalyticsIdentities() {
|
|
28
28
|
const identities = {};
|
|
29
29
|
if (typeof window === "undefined") return identities;
|
|
30
|
-
const posthogId =
|
|
30
|
+
const posthogId = detectPosthogId();
|
|
31
|
+
if (posthogId) identities.posthog = posthogId;
|
|
32
|
+
return identities;
|
|
33
|
+
}
|
|
34
|
+
function detectPosthogId() {
|
|
35
|
+
const fromInstance = probe(() => {
|
|
31
36
|
const posthog = window.posthog;
|
|
32
37
|
return typeof posthog?.get_distinct_id === "function" ? posthog.get_distinct_id() : void 0;
|
|
33
38
|
});
|
|
34
|
-
if (
|
|
35
|
-
|
|
39
|
+
if (fromInstance) return fromInstance;
|
|
40
|
+
const candidates = [...posthogCandidatesFromLocalStorage(), ...posthogCandidatesFromCookie()];
|
|
41
|
+
let best;
|
|
42
|
+
for (const candidate of candidates) if (!best || candidate.lastActivity > best.lastActivity) best = candidate;
|
|
43
|
+
return best?.distinctId;
|
|
44
|
+
}
|
|
45
|
+
const POSTHOG_PERSISTENCE_KEY = /^ph_.+_posthog$/;
|
|
46
|
+
function posthogCandidatesFromLocalStorage() {
|
|
47
|
+
const found = [];
|
|
48
|
+
try {
|
|
49
|
+
const storage = window.localStorage;
|
|
50
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
51
|
+
const key = storage.key(index);
|
|
52
|
+
if (!key || !POSTHOG_PERSISTENCE_KEY.test(key)) continue;
|
|
53
|
+
const candidate = parsePosthogPersistence(() => storage.getItem(key));
|
|
54
|
+
if (candidate) found.push(candidate);
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
function posthogCandidatesFromCookie() {
|
|
60
|
+
const found = [];
|
|
61
|
+
try {
|
|
62
|
+
for (const entry of document.cookie.split(";")) {
|
|
63
|
+
const separator = entry.indexOf("=");
|
|
64
|
+
if (separator === -1) continue;
|
|
65
|
+
const name = entry.slice(0, separator).trim();
|
|
66
|
+
if (!POSTHOG_PERSISTENCE_KEY.test(name)) continue;
|
|
67
|
+
const candidate = parsePosthogPersistence(() => decodeURIComponent(entry.slice(separator + 1)));
|
|
68
|
+
if (candidate) found.push(candidate);
|
|
69
|
+
}
|
|
70
|
+
} catch {}
|
|
71
|
+
return found;
|
|
72
|
+
}
|
|
73
|
+
/** One persistence entry -> candidate; malformed entries parse to nothing. */
|
|
74
|
+
function parsePosthogPersistence(read) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = read();
|
|
77
|
+
if (!raw) return;
|
|
78
|
+
const parsed = JSON.parse(raw);
|
|
79
|
+
if (typeof parsed.distinct_id !== "string" || parsed.distinct_id.length === 0) return;
|
|
80
|
+
const sesid = parsed.$sesid;
|
|
81
|
+
return {
|
|
82
|
+
distinctId: parsed.distinct_id,
|
|
83
|
+
lastActivity: Array.isArray(sesid) && typeof sesid[0] === "number" ? sesid[0] : 0
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
36
88
|
}
|
|
37
89
|
/** Run a probe, keeping only non-empty string results and swallowing throws. */
|
|
38
90
|
function probe(read) {
|
|
@@ -96,28 +148,32 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
96
148
|
}, [user, ownedAttributes]);
|
|
97
149
|
const [completedStepKeys, setCompletedStepKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
98
150
|
const [completedTourKeys, setCompletedTourKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
151
|
+
const [dismissedTooltipKeys, setDismissedTooltipKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
99
152
|
const [activeTourState, setActiveTourState] = useState(null);
|
|
153
|
+
const [requestedTooltipKey, setRequestedTooltipKey] = useState(null);
|
|
154
|
+
const seenTooltipKeys = useRef(/* @__PURE__ */ new Set());
|
|
100
155
|
const lastNavigatedTourStep = useRef(null);
|
|
101
156
|
const storageKey = !hostedMode && resolvedSpec && user ? completedToursStorageKey(resolvedSpec.key, user.id) : null;
|
|
157
|
+
const tooltipStorageKey = !hostedMode && resolvedSpec && user ? dismissedTooltipsStorageKey(resolvedSpec.key, user.id) : null;
|
|
102
158
|
const experienceKey = activeSurvey?.key ?? activeExperience?.key ?? resolvedSpec?.key ?? null;
|
|
103
159
|
const versionId = activeSurvey?.version.id ?? activeExperience?.version.id ?? null;
|
|
104
160
|
useEffect(() => {
|
|
105
161
|
setCompletedStepKeys(/* @__PURE__ */ new Set());
|
|
106
162
|
setActiveTourState(null);
|
|
163
|
+
setRequestedTooltipKey(null);
|
|
164
|
+
seenTooltipKeys.current = /* @__PURE__ */ new Set();
|
|
107
165
|
lastNavigatedTourStep.current = null;
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
setCompletedTourKeys(readCompletedTours(storageKey));
|
|
166
|
+
setCompletedTourKeys(storageKey ? readStoredKeys(storageKey) : /* @__PURE__ */ new Set());
|
|
167
|
+
setDismissedTooltipKeys(tooltipStorageKey ? readStoredKeys(tooltipStorageKey) : /* @__PURE__ */ new Set());
|
|
113
168
|
}, [
|
|
114
169
|
resolvedSpec?.key,
|
|
115
170
|
storageKey,
|
|
171
|
+
tooltipStorageKey,
|
|
116
172
|
user?.id
|
|
117
173
|
]);
|
|
118
174
|
useEffect(() => {
|
|
119
175
|
if (!hostedMode || !activeExperience) return;
|
|
120
|
-
applyProgress(activeExperience.progress, setCompletedStepKeys, setCompletedTourKeys);
|
|
176
|
+
applyProgress(activeExperience.progress, setCompletedStepKeys, setCompletedTourKeys, setDismissedTooltipKeys);
|
|
121
177
|
}, [activeExperience, hostedMode]);
|
|
122
178
|
const effectiveCompletedStepKeys = useMemo(() => {
|
|
123
179
|
if (!hostedMode || !activeExperience) return completedStepKeys;
|
|
@@ -135,6 +191,14 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
135
191
|
completedTourKeys,
|
|
136
192
|
hostedMode
|
|
137
193
|
]);
|
|
194
|
+
const effectiveDismissedTooltipKeys = useMemo(() => {
|
|
195
|
+
if (!hostedMode || !activeExperience) return dismissedTooltipKeys;
|
|
196
|
+
return mergeKeys(dismissedTooltipKeys, activeExperience.progress.dismissedTooltipKeys);
|
|
197
|
+
}, [
|
|
198
|
+
activeExperience,
|
|
199
|
+
dismissedTooltipKeys,
|
|
200
|
+
hostedMode
|
|
201
|
+
]);
|
|
138
202
|
useEffect(() => {
|
|
139
203
|
if (!resolvedSpec) return;
|
|
140
204
|
const tourCompletedStepKeys = (resolvedSpec.checklist?.steps ?? []).filter((step) => step.completeWhen?.tour && effectiveCompletedTourKeys.has(step.completeWhen.tour)).map((step) => step.key);
|
|
@@ -191,7 +255,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
191
255
|
setCompletedTourKeys((current) => {
|
|
192
256
|
const next = new Set(current);
|
|
193
257
|
next.add(tourKey);
|
|
194
|
-
if (storageKey)
|
|
258
|
+
if (storageKey) writeStoredKeys(storageKey, next);
|
|
195
259
|
return next;
|
|
196
260
|
});
|
|
197
261
|
}, [storageKey]);
|
|
@@ -217,7 +281,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
217
281
|
user: toRuntimeUser(user),
|
|
218
282
|
versionId: versionId ?? void 0
|
|
219
283
|
});
|
|
220
|
-
if (response.progress) applyProgress(response.progress, setCompletedStepKeys, setCompletedTourKeys);
|
|
284
|
+
if (response.progress) applyProgress(response.progress, setCompletedStepKeys, setCompletedTourKeys, setDismissedTooltipKeys);
|
|
221
285
|
} catch {
|
|
222
286
|
networkOk = false;
|
|
223
287
|
}
|
|
@@ -371,7 +435,7 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
371
435
|
tourKey,
|
|
372
436
|
user: toRuntimeUser(user),
|
|
373
437
|
versionId: versionId ?? void 0
|
|
374
|
-
})).progress, setCompletedStepKeys, setCompletedTourKeys);
|
|
438
|
+
})).progress, setCompletedStepKeys, setCompletedTourKeys, setDismissedTooltipKeys);
|
|
375
439
|
} catch {}
|
|
376
440
|
}, [
|
|
377
441
|
client,
|
|
@@ -416,10 +480,106 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
416
480
|
resolvedSpec,
|
|
417
481
|
user
|
|
418
482
|
]);
|
|
483
|
+
const openTooltip = useCallback(async (tooltipKey) => {
|
|
484
|
+
if (resolvedLoading || !resolvedSpec) return {
|
|
485
|
+
ok: false,
|
|
486
|
+
reason: "not-ready"
|
|
487
|
+
};
|
|
488
|
+
if (!user) return {
|
|
489
|
+
ok: false,
|
|
490
|
+
reason: "missing-user"
|
|
491
|
+
};
|
|
492
|
+
if (!resolvedSpec.tooltips[tooltipKey]) return {
|
|
493
|
+
ok: false,
|
|
494
|
+
reason: "missing-tooltip"
|
|
495
|
+
};
|
|
496
|
+
setRequestedTooltipKey(tooltipKey);
|
|
497
|
+
return { ok: true };
|
|
498
|
+
}, [
|
|
499
|
+
resolvedLoading,
|
|
500
|
+
resolvedSpec,
|
|
501
|
+
user
|
|
502
|
+
]);
|
|
503
|
+
const dismissTooltip = useCallback(async (tooltipKey, options) => {
|
|
504
|
+
if (resolvedLoading || !resolvedSpec) return {
|
|
505
|
+
ok: false,
|
|
506
|
+
reason: "not-ready"
|
|
507
|
+
};
|
|
508
|
+
if (!user) return {
|
|
509
|
+
ok: false,
|
|
510
|
+
reason: "missing-user"
|
|
511
|
+
};
|
|
512
|
+
if (!resolvedSpec.tooltips[tooltipKey]) return {
|
|
513
|
+
ok: false,
|
|
514
|
+
reason: "missing-tooltip"
|
|
515
|
+
};
|
|
516
|
+
const via = options?.via ?? "close";
|
|
517
|
+
setRequestedTooltipKey((current) => current === tooltipKey ? null : current);
|
|
518
|
+
setDismissedTooltipKeys((current) => {
|
|
519
|
+
const next = mergeKeys(current, [tooltipKey]);
|
|
520
|
+
if (tooltipStorageKey && next !== current) writeStoredKeys(tooltipStorageKey, next);
|
|
521
|
+
return next;
|
|
522
|
+
});
|
|
523
|
+
if (hostedMode && client && experienceKey) client.dismissTooltip({
|
|
524
|
+
experienceKey,
|
|
525
|
+
origin: currentOrigin,
|
|
526
|
+
route: currentRoute,
|
|
527
|
+
tooltipKey,
|
|
528
|
+
user: toRuntimeUser(user),
|
|
529
|
+
versionId: versionId ?? void 0,
|
|
530
|
+
via
|
|
531
|
+
}).catch(() => {});
|
|
532
|
+
return { ok: true };
|
|
533
|
+
}, [
|
|
534
|
+
client,
|
|
535
|
+
currentOrigin,
|
|
536
|
+
currentRoute,
|
|
537
|
+
experienceKey,
|
|
538
|
+
hostedMode,
|
|
539
|
+
resolvedLoading,
|
|
540
|
+
resolvedSpec,
|
|
541
|
+
tooltipStorageKey,
|
|
542
|
+
user,
|
|
543
|
+
versionId
|
|
544
|
+
]);
|
|
545
|
+
const markTooltipSeen = useCallback((tooltipKey) => {
|
|
546
|
+
if (!resolvedSpec?.tooltips[tooltipKey] || seenTooltipKeys.current.has(tooltipKey)) return;
|
|
547
|
+
seenTooltipKeys.current.add(tooltipKey);
|
|
548
|
+
if (hostedMode && client && experienceKey && user) client.recordTooltipSeen({
|
|
549
|
+
experienceKey,
|
|
550
|
+
origin: currentOrigin,
|
|
551
|
+
route: currentRoute,
|
|
552
|
+
tooltipKey,
|
|
553
|
+
user: toRuntimeUser(user),
|
|
554
|
+
versionId: versionId ?? void 0
|
|
555
|
+
}).catch(() => {});
|
|
556
|
+
}, [
|
|
557
|
+
client,
|
|
558
|
+
currentOrigin,
|
|
559
|
+
currentRoute,
|
|
560
|
+
experienceKey,
|
|
561
|
+
hostedMode,
|
|
562
|
+
resolvedSpec,
|
|
563
|
+
user,
|
|
564
|
+
versionId
|
|
565
|
+
]);
|
|
419
566
|
const startChecklistStep = useCallback(async (step) => {
|
|
420
567
|
if (step.action?.type === "startTour") return startTour(step.action.tour);
|
|
568
|
+
if (step.action?.type === "openTooltip") {
|
|
569
|
+
const tooltipKey = step.action.tooltip;
|
|
570
|
+
const destination = resolvedSpec?.tooltips[tooltipKey]?.route ?? step.route;
|
|
571
|
+
if (destination && !routeMatches(destination, currentRoute)) await navigateWithFallback(destination, navigate);
|
|
572
|
+
return openTooltip(tooltipKey);
|
|
573
|
+
}
|
|
421
574
|
await navigateToStep(step);
|
|
422
|
-
}, [
|
|
575
|
+
}, [
|
|
576
|
+
currentRoute,
|
|
577
|
+
navigate,
|
|
578
|
+
navigateToStep,
|
|
579
|
+
openTooltip,
|
|
580
|
+
resolvedSpec,
|
|
581
|
+
startTour
|
|
582
|
+
]);
|
|
423
583
|
const visibleHotspots = useMemo(() => {
|
|
424
584
|
if (!resolvedSpec || activeTour) return [];
|
|
425
585
|
return Object.entries(resolvedSpec.hotspots).filter(([, hotspot]) => routeMatches(hotspot.route, currentRoute)).filter(([, hotspot]) => hotspot.repeatable || !effectiveCompletedTourKeys.has(hotspot.action.tour)).map(([key, hotspot]) => ({
|
|
@@ -432,6 +592,29 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
432
592
|
currentRoute,
|
|
433
593
|
resolvedSpec
|
|
434
594
|
]);
|
|
595
|
+
const visibleTooltips = useMemo(() => {
|
|
596
|
+
if (!resolvedSpec || activeTour) return [];
|
|
597
|
+
const requestedTooltip = requestedTooltipKey ? resolvedSpec.tooltips[requestedTooltipKey] : void 0;
|
|
598
|
+
if (requestedTooltipKey && requestedTooltip) {
|
|
599
|
+
if (!routeMatches(requestedTooltip.route, currentRoute)) return [];
|
|
600
|
+
return [{
|
|
601
|
+
key: requestedTooltipKey,
|
|
602
|
+
requested: true,
|
|
603
|
+
tooltip: requestedTooltip
|
|
604
|
+
}];
|
|
605
|
+
}
|
|
606
|
+
return Object.entries(resolvedSpec.tooltips).filter(([key, tooltip]) => routeMatches(tooltip.route, currentRoute) && !effectiveDismissedTooltipKeys.has(key)).map(([key, tooltip]) => ({
|
|
607
|
+
key,
|
|
608
|
+
requested: false,
|
|
609
|
+
tooltip
|
|
610
|
+
}));
|
|
611
|
+
}, [
|
|
612
|
+
activeTour,
|
|
613
|
+
currentRoute,
|
|
614
|
+
effectiveDismissedTooltipKeys,
|
|
615
|
+
requestedTooltipKey,
|
|
616
|
+
resolvedSpec
|
|
617
|
+
]);
|
|
435
618
|
const value = useMemo(() => ({
|
|
436
619
|
spec: resolvedSpec,
|
|
437
620
|
user: effectiveUser,
|
|
@@ -449,7 +632,12 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
449
632
|
advanceTour,
|
|
450
633
|
navigateToStep,
|
|
451
634
|
startChecklistStep,
|
|
452
|
-
visibleHotspots
|
|
635
|
+
visibleHotspots,
|
|
636
|
+
dismissedTooltipKeys: effectiveDismissedTooltipKeys,
|
|
637
|
+
visibleTooltips,
|
|
638
|
+
openTooltip,
|
|
639
|
+
dismissTooltip,
|
|
640
|
+
markTooltipSeen
|
|
453
641
|
}), [
|
|
454
642
|
resolvedSpec,
|
|
455
643
|
effectiveUser,
|
|
@@ -467,7 +655,12 @@ function SunboardProvider({ spec: specProp, user, loading: loadingProp, route, n
|
|
|
467
655
|
advanceTour,
|
|
468
656
|
navigateToStep,
|
|
469
657
|
startChecklistStep,
|
|
470
|
-
visibleHotspots
|
|
658
|
+
visibleHotspots,
|
|
659
|
+
effectiveDismissedTooltipKeys,
|
|
660
|
+
visibleTooltips,
|
|
661
|
+
openTooltip,
|
|
662
|
+
dismissTooltip,
|
|
663
|
+
markTooltipSeen
|
|
471
664
|
]);
|
|
472
665
|
return /* @__PURE__ */ jsx(SunboardContext.Provider, {
|
|
473
666
|
value,
|
|
@@ -531,8 +724,16 @@ function useSunboardBootstrap(client, options) {
|
|
|
531
724
|
survey: null
|
|
532
725
|
});
|
|
533
726
|
});
|
|
727
|
+
const cancelLateCapture = scheduleLateIdentityCapture({
|
|
728
|
+
client,
|
|
729
|
+
identitiesRef,
|
|
730
|
+
origin,
|
|
731
|
+
sent: identities,
|
|
732
|
+
user
|
|
733
|
+
});
|
|
534
734
|
return () => {
|
|
535
735
|
controller.abort();
|
|
736
|
+
cancelLateCapture();
|
|
536
737
|
};
|
|
537
738
|
}, [
|
|
538
739
|
client,
|
|
@@ -543,9 +744,46 @@ function useSunboardBootstrap(client, options) {
|
|
|
543
744
|
]);
|
|
544
745
|
return state;
|
|
545
746
|
}
|
|
546
|
-
|
|
747
|
+
/** Late-capture probe cadence: ~10s of 1s ticks covers slow analytics init. */
|
|
748
|
+
const IDENTITY_CAPTURE_INTERVAL_MS = 1e3;
|
|
749
|
+
const IDENTITY_CAPTURE_MAX_ATTEMPTS = 10;
|
|
750
|
+
/**
|
|
751
|
+
* Keep probing for analytics-provider ids the bootstrap missed, and push the
|
|
752
|
+
* first find through `identify`. One send per bootstrap: after a successful
|
|
753
|
+
* capture the poll stops; the next bootstrap (new user/session) starts fresh.
|
|
754
|
+
*/
|
|
755
|
+
function scheduleLateIdentityCapture(options) {
|
|
756
|
+
const { client, identitiesRef, origin, sent, user } = options;
|
|
757
|
+
let attempts = 0;
|
|
758
|
+
const timer = setInterval(() => {
|
|
759
|
+
attempts += 1;
|
|
760
|
+
if (identitiesRef.current === false) {
|
|
761
|
+
clearInterval(timer);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
const detected = resolveIdentities(identitiesRef.current);
|
|
765
|
+
if (!detected || !hasUnsentIdentity(detected, sent)) {
|
|
766
|
+
if (attempts >= IDENTITY_CAPTURE_MAX_ATTEMPTS) clearInterval(timer);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
clearInterval(timer);
|
|
770
|
+
client.identify({
|
|
771
|
+
identities: detected,
|
|
772
|
+
user: toRuntimeUser(user),
|
|
773
|
+
...origin ? { origin } : {}
|
|
774
|
+
}).catch((error) => {
|
|
775
|
+
if (isDevEnvironment()) console.warn("[sunboard] late identity capture failed", error);
|
|
776
|
+
});
|
|
777
|
+
}, IDENTITY_CAPTURE_INTERVAL_MS);
|
|
778
|
+
return () => clearInterval(timer);
|
|
779
|
+
}
|
|
780
|
+
function hasUnsentIdentity(detected, sent) {
|
|
781
|
+
return Object.entries(detected).some(([provider, id]) => sent?.[provider] !== id);
|
|
782
|
+
}
|
|
783
|
+
function applyProgress(progress, setSteps, setTours, setDismissedTooltips) {
|
|
547
784
|
setSteps((current) => mergeKeys(current, progress.completedStepKeys));
|
|
548
785
|
setTours((current) => mergeKeys(current, progress.completedTourKeys));
|
|
786
|
+
setDismissedTooltips?.((current) => mergeKeys(current, progress.dismissedTooltipKeys));
|
|
549
787
|
}
|
|
550
788
|
function mergeKeys(current, incoming) {
|
|
551
789
|
if (incoming.length === 0) return current;
|
|
@@ -569,6 +807,9 @@ function toRuntimeUser(user) {
|
|
|
569
807
|
function completedToursStorageKey(specKey, userId) {
|
|
570
808
|
return `sunboard:${specKey}:${userId}:completed-tours`;
|
|
571
809
|
}
|
|
810
|
+
function dismissedTooltipsStorageKey(specKey, userId) {
|
|
811
|
+
return `sunboard:${specKey}:${userId}:dismissed-tooltips`;
|
|
812
|
+
}
|
|
572
813
|
function getBrowserRoute() {
|
|
573
814
|
if (typeof window === "undefined") return "/";
|
|
574
815
|
return `${window.location.pathname}${window.location.search}`;
|
|
@@ -577,7 +818,7 @@ function getBrowserOrigin() {
|
|
|
577
818
|
if (typeof window === "undefined") return;
|
|
578
819
|
return window.location.origin;
|
|
579
820
|
}
|
|
580
|
-
function
|
|
821
|
+
function readStoredKeys(storageKey) {
|
|
581
822
|
if (typeof window === "undefined") return /* @__PURE__ */ new Set();
|
|
582
823
|
try {
|
|
583
824
|
const rawValue = window.localStorage.getItem(storageKey);
|
|
@@ -588,10 +829,10 @@ function readCompletedTours(storageKey) {
|
|
|
588
829
|
return /* @__PURE__ */ new Set();
|
|
589
830
|
}
|
|
590
831
|
}
|
|
591
|
-
function
|
|
832
|
+
function writeStoredKeys(storageKey, keys) {
|
|
592
833
|
if (typeof window === "undefined") return;
|
|
593
834
|
try {
|
|
594
|
-
window.localStorage.setItem(storageKey, JSON.stringify([...
|
|
835
|
+
window.localStorage.setItem(storageKey, JSON.stringify([...keys].sort()));
|
|
595
836
|
} catch {}
|
|
596
837
|
}
|
|
597
838
|
|
|
@@ -644,7 +885,8 @@ function useSunboard() {
|
|
|
644
885
|
//#region src/hooks/useChecklist.ts
|
|
645
886
|
function actionLabelFor(step) {
|
|
646
887
|
if (step.action?.type === "startTour") return "Start";
|
|
647
|
-
if (step.action
|
|
888
|
+
if (step.action?.type === "openTooltip") return "Show me";
|
|
889
|
+
if (step.route) return "Open";
|
|
648
890
|
}
|
|
649
891
|
function interpolateOptional$1(text, user) {
|
|
650
892
|
if (!text) return void 0;
|
|
@@ -1019,6 +1261,109 @@ function Hotspot({ hotspot, hotspotKey }) {
|
|
|
1019
1261
|
}) });
|
|
1020
1262
|
}
|
|
1021
1263
|
|
|
1264
|
+
//#endregion
|
|
1265
|
+
//#region src/components/SunboardTooltips.tsx
|
|
1266
|
+
/**
|
|
1267
|
+
* Renders at most one tooltip: the first visible candidate whose target
|
|
1268
|
+
* resolves. A candidate whose selector never appears is skipped for the rest
|
|
1269
|
+
* of the session (without recording a dismissal — the user rejected nothing),
|
|
1270
|
+
* so one stale selector can't block the tooltips behind it.
|
|
1271
|
+
*/
|
|
1272
|
+
function SunboardTooltips() {
|
|
1273
|
+
const { visibleTooltips } = useSunboard();
|
|
1274
|
+
const [skippedKeys, setSkippedKeys] = useState(() => /* @__PURE__ */ new Set());
|
|
1275
|
+
const active = visibleTooltips.find(({ key }) => !skippedKeys.has(key)) ?? null;
|
|
1276
|
+
if (!active) return null;
|
|
1277
|
+
return /* @__PURE__ */ jsx(Tooltip, {
|
|
1278
|
+
onTargetMissing: () => setSkippedKeys((current) => new Set(current).add(active.key)),
|
|
1279
|
+
requested: active.requested,
|
|
1280
|
+
tooltip: active.tooltip,
|
|
1281
|
+
tooltipKey: active.key
|
|
1282
|
+
}, active.key);
|
|
1283
|
+
}
|
|
1284
|
+
function Tooltip({ tooltip, tooltipKey, requested, onTargetMissing }) {
|
|
1285
|
+
const { dismissTooltip, markTooltipSeen, startTour } = useSunboard();
|
|
1286
|
+
const arrowRef = useRef(null);
|
|
1287
|
+
const title = useInterpolated(tooltip.title);
|
|
1288
|
+
const body = useInterpolated(tooltip.body);
|
|
1289
|
+
const actionLabel = useInterpolated(tooltip.actionLabel) ?? "Show me how";
|
|
1290
|
+
const target = useTargetElement(tooltip.target, true);
|
|
1291
|
+
const { context, refs, floatingStyles, update } = useFloating({
|
|
1292
|
+
placement: tooltip.placement,
|
|
1293
|
+
middleware: [
|
|
1294
|
+
offset(12),
|
|
1295
|
+
flip(),
|
|
1296
|
+
shift({ padding: 12 }),
|
|
1297
|
+
arrow({ element: arrowRef })
|
|
1298
|
+
],
|
|
1299
|
+
whileElementsMounted: autoUpdate
|
|
1300
|
+
});
|
|
1301
|
+
useEffect(() => {
|
|
1302
|
+
if (!target.timedOut) return;
|
|
1303
|
+
onTargetMissing();
|
|
1304
|
+
}, [target.timedOut]);
|
|
1305
|
+
useEffect(() => {
|
|
1306
|
+
if (!target.element) return;
|
|
1307
|
+
refs.setReference(target.element);
|
|
1308
|
+
update();
|
|
1309
|
+
markTooltipSeen(tooltipKey);
|
|
1310
|
+
}, [
|
|
1311
|
+
markTooltipSeen,
|
|
1312
|
+
refs,
|
|
1313
|
+
target.element,
|
|
1314
|
+
tooltipKey,
|
|
1315
|
+
update
|
|
1316
|
+
]);
|
|
1317
|
+
if (!target.element) return null;
|
|
1318
|
+
return /* @__PURE__ */ jsx(FloatingPortal, { children: /* @__PURE__ */ jsxs("section", {
|
|
1319
|
+
"aria-label": title,
|
|
1320
|
+
className: "sunboard-tooltip",
|
|
1321
|
+
"data-sunboard-tooltip": tooltipKey,
|
|
1322
|
+
"data-requested": requested,
|
|
1323
|
+
ref: refs.setFloating,
|
|
1324
|
+
role: "dialog",
|
|
1325
|
+
style: floatingStyles,
|
|
1326
|
+
children: [
|
|
1327
|
+
/* @__PURE__ */ jsx(FloatingArrow, {
|
|
1328
|
+
className: "sunboard-tooltip-arrow",
|
|
1329
|
+
context,
|
|
1330
|
+
fill: "var(--sunboard-tour-background)",
|
|
1331
|
+
ref: arrowRef,
|
|
1332
|
+
stroke: "var(--sunboard-tour-border)",
|
|
1333
|
+
strokeWidth: 1,
|
|
1334
|
+
tipRadius: 2
|
|
1335
|
+
}),
|
|
1336
|
+
/* @__PURE__ */ jsx("button", {
|
|
1337
|
+
"aria-label": `Dismiss ${title}`,
|
|
1338
|
+
className: "sunboard-tooltip-close",
|
|
1339
|
+
onClick: () => void dismissTooltip(tooltipKey),
|
|
1340
|
+
type: "button",
|
|
1341
|
+
children: "x"
|
|
1342
|
+
}),
|
|
1343
|
+
/* @__PURE__ */ jsx("h2", {
|
|
1344
|
+
className: "sunboard-tooltip-title",
|
|
1345
|
+
children: title
|
|
1346
|
+
}),
|
|
1347
|
+
body ? /* @__PURE__ */ jsx("p", {
|
|
1348
|
+
className: "sunboard-tooltip-body",
|
|
1349
|
+
children: body
|
|
1350
|
+
}) : null,
|
|
1351
|
+
tooltip.action ? /* @__PURE__ */ jsx("div", {
|
|
1352
|
+
className: "sunboard-tooltip-actions",
|
|
1353
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
1354
|
+
className: "sunboard-tooltip-action",
|
|
1355
|
+
onClick: () => {
|
|
1356
|
+
dismissTooltip(tooltipKey, { via: "action" });
|
|
1357
|
+
startTour(tooltip.action.tour);
|
|
1358
|
+
},
|
|
1359
|
+
type: "button",
|
|
1360
|
+
children: actionLabel
|
|
1361
|
+
})
|
|
1362
|
+
}) : null
|
|
1363
|
+
]
|
|
1364
|
+
}) });
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1022
1367
|
//#endregion
|
|
1023
1368
|
//#region src/components/SunboardTour.tsx
|
|
1024
1369
|
function SunboardTour() {
|
|
@@ -1428,9 +1773,45 @@ const Sunboard = Object.assign(SunboardChecklist, {
|
|
|
1428
1773
|
Checklist: SunboardChecklist,
|
|
1429
1774
|
Survey: SunboardSurvey,
|
|
1430
1775
|
Hotspots: SunboardHotspots,
|
|
1776
|
+
Tooltips: SunboardTooltips,
|
|
1431
1777
|
Tour: SunboardTour
|
|
1432
1778
|
});
|
|
1433
1779
|
|
|
1434
1780
|
//#endregion
|
|
1435
|
-
|
|
1781
|
+
//#region src/hooks/useTooltips.ts
|
|
1782
|
+
/**
|
|
1783
|
+
* Headless counterpart of `<SunboardTooltips>`: the same visibility, seen,
|
|
1784
|
+
* dismissal, and engagement semantics with rendering left to the host.
|
|
1785
|
+
*/
|
|
1786
|
+
function useTooltips() {
|
|
1787
|
+
const { user, visibleTooltips, markTooltipSeen, dismissTooltip, startTour } = useSunboard();
|
|
1788
|
+
return {
|
|
1789
|
+
tooltips: useMemo(() => visibleTooltips.map(({ key, tooltip, requested }) => ({
|
|
1790
|
+
key,
|
|
1791
|
+
title: interpolateTokens(tooltip.title, user ?? null),
|
|
1792
|
+
body: tooltip.body ? interpolateTokens(tooltip.body, user ?? null) : void 0,
|
|
1793
|
+
target: tooltip.target,
|
|
1794
|
+
placement: tooltip.placement,
|
|
1795
|
+
hasAction: Boolean(tooltip.action),
|
|
1796
|
+
actionLabel: tooltip.actionLabel ? interpolateTokens(tooltip.actionLabel, user ?? null) : void 0,
|
|
1797
|
+
requested
|
|
1798
|
+
})), [user, visibleTooltips]),
|
|
1799
|
+
markSeen: markTooltipSeen,
|
|
1800
|
+
dismiss: (key) => {
|
|
1801
|
+
dismissTooltip(key);
|
|
1802
|
+
},
|
|
1803
|
+
engage: useCallback((key) => {
|
|
1804
|
+
const entry = visibleTooltips.find((candidate) => candidate.key === key);
|
|
1805
|
+
dismissTooltip(key, { via: "action" });
|
|
1806
|
+
if (entry?.tooltip.action) startTour(entry.tooltip.action.tour);
|
|
1807
|
+
}, [
|
|
1808
|
+
dismissTooltip,
|
|
1809
|
+
startTour,
|
|
1810
|
+
visibleTooltips
|
|
1811
|
+
])
|
|
1812
|
+
};
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
//#endregion
|
|
1816
|
+
export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardSurvey, SunboardTooltips, SunboardTour, useChecklist, useSunboard, useSurvey, useTooltips };
|
|
1436
1817
|
//# sourceMappingURL=index.mjs.map
|