@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.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 = probe(() => {
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 (posthogId) identities.posthog = posthogId;
35
- return identities;
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
- if (!storageKey) {
109
- setCompletedTourKeys(/* @__PURE__ */ new Set());
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) writeCompletedTours(storageKey, next);
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
- }, [navigateToStep, startTour]);
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
- function applyProgress(progress, setSteps, setTours) {
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 readCompletedTours(storageKey) {
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 writeCompletedTours(storageKey, tourKeys) {
832
+ function writeStoredKeys(storageKey, keys) {
592
833
  if (typeof window === "undefined") return;
593
834
  try {
594
- window.localStorage.setItem(storageKey, JSON.stringify([...tourKeys].sort()));
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 || step.route) return "Open";
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
- export { Sunboard, SunboardChecklist, SunboardHotspots, SunboardProvider, SunboardRuntimeApiError, SunboardRuntimeClient, SunboardSurvey, SunboardTour, useChecklist, useSunboard, useSurvey };
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