@flowsterix/react 0.1.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.
Files changed (96) hide show
  1. package/dist/adapters/radixDialog.d.ts +18 -0
  2. package/dist/adapters/radixDialog.d.ts.map +1 -0
  3. package/dist/chunk-RCASLQRS.mjs +24 -0
  4. package/dist/chunk-RPA2S5UP.mjs +280 -0
  5. package/dist/chunk-U757YVZP.mjs +50 -0
  6. package/dist/components/OverlayBackdrop.d.ts +34 -0
  7. package/dist/components/OverlayBackdrop.d.ts.map +1 -0
  8. package/dist/components/TourFocusManager.d.ts +11 -0
  9. package/dist/components/TourFocusManager.d.ts.map +1 -0
  10. package/dist/components/TourPopoverPortal.d.ts +57 -0
  11. package/dist/components/TourPopoverPortal.d.ts.map +1 -0
  12. package/dist/components/__tests__/TourFocusManager.test.d.ts +2 -0
  13. package/dist/components/__tests__/TourFocusManager.test.d.ts.map +1 -0
  14. package/dist/context.d.ts +56 -0
  15. package/dist/context.d.ts.map +1 -0
  16. package/dist/hooks/__tests__/scrollMargin.test.d.ts +2 -0
  17. package/dist/hooks/__tests__/scrollMargin.test.d.ts.map +1 -0
  18. package/dist/hooks/__tests__/useBodyScrollLock.test.d.ts +2 -0
  19. package/dist/hooks/__tests__/useBodyScrollLock.test.d.ts.map +1 -0
  20. package/dist/hooks/__tests__/useHiddenTargetFallback.test.d.ts +2 -0
  21. package/dist/hooks/__tests__/useHiddenTargetFallback.test.d.ts.map +1 -0
  22. package/dist/hooks/__tests__/waitForPredicate.test.d.ts +2 -0
  23. package/dist/hooks/__tests__/waitForPredicate.test.d.ts.map +1 -0
  24. package/dist/hooks/scrollMargin.d.ts +15 -0
  25. package/dist/hooks/scrollMargin.d.ts.map +1 -0
  26. package/dist/hooks/useAdvanceRules.d.ts +3 -0
  27. package/dist/hooks/useAdvanceRules.d.ts.map +1 -0
  28. package/dist/hooks/useBodyScrollLock.d.ts +2 -0
  29. package/dist/hooks/useBodyScrollLock.d.ts.map +1 -0
  30. package/dist/hooks/useDelayAdvance.d.ts +14 -0
  31. package/dist/hooks/useDelayAdvance.d.ts.map +1 -0
  32. package/dist/hooks/useHiddenTargetFallback.d.ts +16 -0
  33. package/dist/hooks/useHiddenTargetFallback.d.ts.map +1 -0
  34. package/dist/hooks/useHudDescription.d.ts +13 -0
  35. package/dist/hooks/useHudDescription.d.ts.map +1 -0
  36. package/dist/hooks/useHudShortcuts.d.ts +7 -0
  37. package/dist/hooks/useHudShortcuts.d.ts.map +1 -0
  38. package/dist/hooks/useHudState.d.ts +27 -0
  39. package/dist/hooks/useHudState.d.ts.map +1 -0
  40. package/dist/hooks/useHudTargetIssue.d.ts +21 -0
  41. package/dist/hooks/useHudTargetIssue.d.ts.map +1 -0
  42. package/dist/hooks/useTourControls.d.ts +17 -0
  43. package/dist/hooks/useTourControls.d.ts.map +1 -0
  44. package/dist/hooks/useTourFocusDominance.d.ts +9 -0
  45. package/dist/hooks/useTourFocusDominance.d.ts.map +1 -0
  46. package/dist/hooks/useTourHud.d.ts +61 -0
  47. package/dist/hooks/useTourHud.d.ts.map +1 -0
  48. package/dist/hooks/useTourOverlay.d.ts +70 -0
  49. package/dist/hooks/useTourOverlay.d.ts.map +1 -0
  50. package/dist/hooks/useTourTarget.d.ts +16 -0
  51. package/dist/hooks/useTourTarget.d.ts.map +1 -0
  52. package/dist/hooks/useViewportRect.d.ts +3 -0
  53. package/dist/hooks/useViewportRect.d.ts.map +1 -0
  54. package/dist/hooks/waitForPredicate.d.ts +15 -0
  55. package/dist/hooks/waitForPredicate.d.ts.map +1 -0
  56. package/dist/index.cjs +4207 -0
  57. package/dist/index.d.ts +45 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.mjs +3851 -0
  60. package/dist/labels.d.ts +22 -0
  61. package/dist/labels.d.ts.map +1 -0
  62. package/dist/motion/animationAdapter.d.ts +40 -0
  63. package/dist/motion/animationAdapter.d.ts.map +1 -0
  64. package/dist/motion/useHudMotion.d.ts +14 -0
  65. package/dist/motion/useHudMotion.d.ts.map +1 -0
  66. package/dist/router/index.cjs +275 -0
  67. package/dist/router/index.d.ts +5 -0
  68. package/dist/router/index.d.ts.map +1 -0
  69. package/dist/router/index.mjs +30 -0
  70. package/dist/router/nextAppRouterAdapter.cjs +224 -0
  71. package/dist/router/nextAppRouterAdapter.d.ts +2 -0
  72. package/dist/router/nextAppRouterAdapter.d.ts.map +1 -0
  73. package/dist/router/nextAppRouterAdapter.mjs +35 -0
  74. package/dist/router/nextPagesRouterAdapter.cjs +217 -0
  75. package/dist/router/nextPagesRouterAdapter.d.ts +2 -0
  76. package/dist/router/nextPagesRouterAdapter.d.ts.map +1 -0
  77. package/dist/router/nextPagesRouterAdapter.mjs +28 -0
  78. package/dist/router/reactRouterAdapter.cjs +220 -0
  79. package/dist/router/reactRouterAdapter.d.ts +2 -0
  80. package/dist/router/reactRouterAdapter.d.ts.map +1 -0
  81. package/dist/router/reactRouterAdapter.mjs +31 -0
  82. package/dist/router/routeGating.d.ts +13 -0
  83. package/dist/router/routeGating.d.ts.map +1 -0
  84. package/dist/router/tanstackRouterAdapter.cjs +202 -0
  85. package/dist/router/tanstackRouterAdapter.d.ts +2 -0
  86. package/dist/router/tanstackRouterAdapter.d.ts.map +1 -0
  87. package/dist/router/tanstackRouterAdapter.mjs +7 -0
  88. package/dist/router/tanstackRouterSync.d.ts +20 -0
  89. package/dist/router/tanstackRouterSync.d.ts.map +1 -0
  90. package/dist/router/utils.d.ts +2 -0
  91. package/dist/router/utils.d.ts.map +1 -0
  92. package/dist/utils/dom.d.ts +22 -0
  93. package/dist/utils/dom.d.ts.map +1 -0
  94. package/dist/utils/focus.d.ts +4 -0
  95. package/dist/utils/focus.d.ts.map +1 -0
  96. package/package.json +96 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3851 @@
1
+ import {
2
+ TanStackRouterSync,
3
+ getTanStackRouter,
4
+ getTourRouter,
5
+ setTanStackRouter,
6
+ setTourRouter
7
+ } from "./chunk-U757YVZP.mjs";
8
+ import {
9
+ useTanStackRouterTourAdapter
10
+ } from "./chunk-RCASLQRS.mjs";
11
+ import {
12
+ createPathString,
13
+ expandRect,
14
+ getClientRect,
15
+ getCurrentRoutePath,
16
+ getScrollParents,
17
+ getViewportRect,
18
+ isBrowser,
19
+ notifyRouteChange,
20
+ portalHost,
21
+ subscribeToRouteChanges,
22
+ supportsMasking
23
+ } from "./chunk-RPA2S5UP.mjs";
24
+
25
+ // src/context.tsx
26
+ import {
27
+ createFlowStore,
28
+ createLocalStorageAdapter,
29
+ resolveMaybePromise,
30
+ serializeVersion
31
+ } from "@flowsterix/core";
32
+ import {
33
+ createContext as createContext3,
34
+ useCallback,
35
+ useContext as useContext3,
36
+ useEffect as useEffect2,
37
+ useMemo as useMemo2,
38
+ useRef,
39
+ useState as useState2
40
+ } from "react";
41
+
42
+ // src/labels.ts
43
+ import { createContext, useContext } from "react";
44
+ var defaultLabels = {
45
+ back: "Back",
46
+ next: "Next",
47
+ finish: "Finish",
48
+ skip: "Skip tour",
49
+ holdToConfirm: "Hold to confirm",
50
+ ariaStepProgress: ({ current, total }) => `Step ${current} of ${total}`,
51
+ ariaTimeRemaining: ({ ms }) => `${Math.ceil(ms / 1e3)} seconds remaining`,
52
+ ariaDelayProgress: "Auto-advance progress",
53
+ formatTimeRemaining: ({ ms }) => `${Math.ceil(ms / 1e3)}s remaining`
54
+ };
55
+ var LabelsContext = createContext(defaultLabels);
56
+ var LabelsProvider = LabelsContext.Provider;
57
+ function useTourLabels() {
58
+ return useContext(LabelsContext);
59
+ }
60
+
61
+ // src/motion/animationAdapter.tsx
62
+ import { motion } from "motion/react";
63
+ import { createContext as createContext2, useContext as useContext2, useEffect, useMemo, useState } from "react";
64
+ import { jsx } from "react/jsx-runtime";
65
+ var defaultAdapter = {
66
+ components: {
67
+ MotionDiv: motion.div,
68
+ MotionSection: motion.section,
69
+ MotionSvg: motion.svg,
70
+ MotionDefs: motion.defs,
71
+ MotionMask: motion.mask,
72
+ MotionRect: motion.rect,
73
+ MotionSpan: motion.span,
74
+ MotionButton: motion.button
75
+ },
76
+ transitions: {
77
+ overlayHighlight: {
78
+ duration: 0.35,
79
+ ease: "easeOut",
80
+ type: "spring",
81
+ damping: 25,
82
+ stiffness: 300,
83
+ mass: 0.7
84
+ },
85
+ overlayFade: {
86
+ duration: 0.35,
87
+ ease: "easeOut"
88
+ },
89
+ popoverEntrance: {
90
+ duration: 0.25,
91
+ ease: "easeOut"
92
+ },
93
+ popoverExit: {
94
+ duration: 0.2,
95
+ ease: "easeOut"
96
+ },
97
+ popoverContent: {
98
+ duration: 0.2,
99
+ ease: "easeOut"
100
+ },
101
+ delayIndicator: {
102
+ duration: 0.18,
103
+ ease: "easeOut"
104
+ }
105
+ }
106
+ };
107
+ var AnimationAdapterContext = createContext2(defaultAdapter);
108
+ var AnimationAdapterProvider = ({
109
+ adapter,
110
+ children
111
+ }) => {
112
+ const value = useMemo(() => adapter ?? defaultAdapter, [adapter]);
113
+ return /* @__PURE__ */ jsx(AnimationAdapterContext.Provider, { value, children });
114
+ };
115
+ var useAnimationAdapter = () => {
116
+ return useContext2(AnimationAdapterContext);
117
+ };
118
+ var defaultAnimationAdapter = defaultAdapter;
119
+ var reducedMotionAnimationAdapter = {
120
+ components: defaultAdapter.components,
121
+ transitions: {
122
+ overlayHighlight: {
123
+ duration: 1e-3,
124
+ ease: "linear",
125
+ type: "tween"
126
+ },
127
+ overlayFade: {
128
+ duration: 1e-3,
129
+ ease: "linear"
130
+ },
131
+ popoverEntrance: {
132
+ duration: 1e-3,
133
+ ease: "linear"
134
+ },
135
+ popoverExit: {
136
+ duration: 1e-3,
137
+ ease: "linear"
138
+ },
139
+ popoverContent: {
140
+ duration: 1e-3,
141
+ ease: "linear"
142
+ },
143
+ delayIndicator: {
144
+ duration: 1e-3,
145
+ ease: "linear"
146
+ }
147
+ }
148
+ };
149
+ var REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
150
+ var usePreferredAnimationAdapter = (options) => {
151
+ const {
152
+ defaultAdapter: defaultOption = defaultAnimationAdapter,
153
+ reducedMotionAdapter: reducedOption = reducedMotionAnimationAdapter,
154
+ enabled = true
155
+ } = options ?? {};
156
+ const [prefersReduced, setPrefersReduced] = useState(() => {
157
+ if (!enabled || typeof window === "undefined") return false;
158
+ return window.matchMedia(REDUCED_MOTION_QUERY).matches;
159
+ });
160
+ useEffect(() => {
161
+ if (!enabled || typeof window === "undefined") return;
162
+ const mediaQuery = window.matchMedia(REDUCED_MOTION_QUERY);
163
+ const handleChange = (event) => {
164
+ setPrefersReduced(event.matches);
165
+ };
166
+ setPrefersReduced(mediaQuery.matches);
167
+ mediaQuery.addEventListener("change", handleChange);
168
+ return () => mediaQuery.removeEventListener("change", handleChange);
169
+ }, [enabled]);
170
+ if (!enabled) {
171
+ return defaultOption;
172
+ }
173
+ return prefersReduced ? reducedOption : defaultOption;
174
+ };
175
+
176
+ // src/context.tsx
177
+ import { jsx as jsx2 } from "react/jsx-runtime";
178
+ var TourContext = createContext3(void 0);
179
+ var DEFAULT_STORAGE_PREFIX = "tour";
180
+ var useFlowMap = (flows) => {
181
+ return useMemo2(() => {
182
+ const map = /* @__PURE__ */ new Map();
183
+ for (const flow of flows) {
184
+ map.set(flow.id, flow);
185
+ }
186
+ return map;
187
+ }, [flows]);
188
+ };
189
+ var TourProvider = ({
190
+ flows,
191
+ children,
192
+ storageAdapter,
193
+ storageNamespace,
194
+ persistOnChange = true,
195
+ defaultDebug = false,
196
+ animationAdapter: animationAdapterProp = defaultAnimationAdapter,
197
+ reducedMotionAdapter,
198
+ autoDetectReducedMotion = true,
199
+ analytics,
200
+ backdropInteraction: backdropInteractionProp = "passthrough",
201
+ lockBodyScroll: lockBodyScrollProp = false,
202
+ labels: labelsProp,
203
+ onVersionMismatch
204
+ }) => {
205
+ const mergedLabels = useMemo2(
206
+ () => ({ ...defaultLabels, ...labelsProp }),
207
+ [labelsProp]
208
+ );
209
+ const flowMap = useFlowMap(flows);
210
+ const storeRef = useRef(null);
211
+ const unsubscribeRef = useRef(null);
212
+ const stepHooksUnsubscribeRef = useRef(null);
213
+ const fallbackStorageRef = useRef(void 0);
214
+ const pendingResumeRef = useRef(/* @__PURE__ */ new Set());
215
+ const autoStartRequestedRef = useRef(null);
216
+ const [activeFlowId, setActiveFlowId] = useState2(null);
217
+ const [state, setState] = useState2(null);
218
+ const [events, setEvents] = useState2(
219
+ null
220
+ );
221
+ const [debugEnabled, setDebugEnabled] = useState2(defaultDebug);
222
+ const [delayInfo, setDelayInfo] = useState2(null);
223
+ const autoStartFlow = useMemo2(() => {
224
+ return flows.find((flow) => flow.autoStart);
225
+ }, [flows]);
226
+ const teardownStore = useCallback(() => {
227
+ unsubscribeRef.current?.();
228
+ unsubscribeRef.current = null;
229
+ stepHooksUnsubscribeRef.current?.();
230
+ stepHooksUnsubscribeRef.current = null;
231
+ storeRef.current?.destroy();
232
+ storeRef.current = null;
233
+ setDelayInfo(null);
234
+ pendingResumeRef.current.clear();
235
+ }, []);
236
+ useEffect2(() => {
237
+ return () => {
238
+ teardownStore();
239
+ setState(null);
240
+ setEvents(null);
241
+ setActiveFlowId(null);
242
+ };
243
+ }, [teardownStore]);
244
+ useEffect2(() => {
245
+ if (!activeFlowId) return;
246
+ const definition = flowMap.get(activeFlowId);
247
+ if (!definition) {
248
+ teardownStore();
249
+ setState(null);
250
+ setEvents(null);
251
+ setActiveFlowId(null);
252
+ }
253
+ }, [activeFlowId, flowMap, teardownStore]);
254
+ const invokeStepHookSync = (hook, context, phase) => {
255
+ if (!hook) return;
256
+ try {
257
+ const result = hook(context);
258
+ if (typeof result === "object" && result !== null && typeof result.then === "function") {
259
+ ;
260
+ result.catch((error) => {
261
+ console.warn(`[tour][step] ${phase} hook rejected`, error);
262
+ });
263
+ }
264
+ } catch (error) {
265
+ console.warn(`[tour][step] ${phase} hook failed`, error);
266
+ }
267
+ };
268
+ const ensureStore = useCallback(
269
+ (flowId) => {
270
+ const existing = storeRef.current;
271
+ if (existing && existing.definition.id === flowId) {
272
+ return existing;
273
+ }
274
+ teardownStore();
275
+ const definition = flowMap.get(flowId);
276
+ if (!definition) {
277
+ throw new Error(`Flow with id "${flowId}" is not registered.`);
278
+ }
279
+ if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
280
+ fallbackStorageRef.current = createLocalStorageAdapter();
281
+ }
282
+ const resolvedStorageAdapter = storageAdapter ? storageAdapter : fallbackStorageRef.current;
283
+ const store = createFlowStore(definition, {
284
+ storageAdapter: resolvedStorageAdapter,
285
+ storageKey: storageNamespace ? `${storageNamespace}:${definition.id}` : void 0,
286
+ persistOnChange,
287
+ analytics,
288
+ onVersionMismatch
289
+ });
290
+ const unsubscribeEnter = store.events.on("stepEnter", (payload) => {
291
+ const step = payload.currentStep;
292
+ if (!step.onEnter) return;
293
+ invokeStepHookSync(
294
+ step.onEnter,
295
+ { flow: definition, state: payload.state, step },
296
+ "enter"
297
+ );
298
+ });
299
+ const unsubscribeExit = store.events.on("stepExit", (payload) => {
300
+ const step = payload.previousStep;
301
+ if (!step.onExit) return;
302
+ invokeStepHookSync(
303
+ step.onExit,
304
+ { flow: definition, state: payload.state, step },
305
+ "exit"
306
+ );
307
+ });
308
+ stepHooksUnsubscribeRef.current = () => {
309
+ unsubscribeEnter();
310
+ unsubscribeExit();
311
+ };
312
+ unsubscribeRef.current = store.subscribe(setState);
313
+ setEvents(store.events);
314
+ storeRef.current = store;
315
+ return store;
316
+ },
317
+ [
318
+ analytics,
319
+ flowMap,
320
+ onVersionMismatch,
321
+ persistOnChange,
322
+ storageAdapter,
323
+ storageNamespace,
324
+ teardownStore
325
+ ]
326
+ );
327
+ const getActiveStore = useCallback(() => {
328
+ const store = storeRef.current;
329
+ if (!store) {
330
+ throw new Error(
331
+ "No active flow. Call startFlow before controlling progression."
332
+ );
333
+ }
334
+ return store;
335
+ }, []);
336
+ const isPromiseLike2 = (value) => {
337
+ return typeof value === "object" && value !== null && typeof value.then === "function";
338
+ };
339
+ const invokeStepHook = useCallback(
340
+ async (hook, context, phase) => {
341
+ if (!hook) return;
342
+ try {
343
+ const result = hook(context);
344
+ if (isPromiseLike2(result)) {
345
+ await result;
346
+ }
347
+ } catch (error) {
348
+ console.warn(`[tour][step] ${phase} hook failed`, error);
349
+ }
350
+ },
351
+ []
352
+ );
353
+ const runResumeHooks = useCallback(
354
+ async (definition, flowState, strategy) => {
355
+ if (flowState.status !== "running") return;
356
+ if (strategy === "current") {
357
+ const index = flowState.stepIndex;
358
+ if (index < 0 || index >= definition.steps.length) return;
359
+ const step = definition.steps[index];
360
+ if (!step.onResume) return;
361
+ await invokeStepHook(
362
+ step.onResume,
363
+ {
364
+ flow: definition,
365
+ state: flowState,
366
+ step
367
+ },
368
+ "resume"
369
+ );
370
+ return;
371
+ }
372
+ const maxIndex = Math.min(
373
+ flowState.stepIndex,
374
+ definition.steps.length - 1
375
+ );
376
+ if (maxIndex < 0) return;
377
+ for (let index = 0; index <= maxIndex; index += 1) {
378
+ const step = definition.steps[index];
379
+ if (!step.onResume) continue;
380
+ await invokeStepHook(
381
+ step.onResume,
382
+ {
383
+ flow: definition,
384
+ state: flowState,
385
+ step
386
+ },
387
+ "resume"
388
+ );
389
+ }
390
+ },
391
+ [invokeStepHook]
392
+ );
393
+ const resolveResumeStrategy = useCallback(
394
+ (definition, options) => {
395
+ return options?.resumeStrategy ?? definition.resumeStrategy ?? "chain";
396
+ },
397
+ []
398
+ );
399
+ const startFlow = useCallback(
400
+ (flowId, options) => {
401
+ const store = ensureStore(flowId);
402
+ const previousState = store.getState();
403
+ setActiveFlowId(flowId);
404
+ if (options?.resume) {
405
+ pendingResumeRef.current.add(flowId);
406
+ } else {
407
+ pendingResumeRef.current.delete(flowId);
408
+ }
409
+ const nextState = store.start(options);
410
+ if (!options?.resume) {
411
+ return nextState;
412
+ }
413
+ if (previousState.stepIndex >= 0 && nextState.status === "running") {
414
+ pendingResumeRef.current.delete(flowId);
415
+ const resumeStrategy = resolveResumeStrategy(store.definition, options);
416
+ const shouldRunResumeHooks = resumeStrategy === "current" ? nextState.stepIndex >= 0 : nextState.stepIndex > 0;
417
+ if (shouldRunResumeHooks) {
418
+ void runResumeHooks(store.definition, nextState, resumeStrategy);
419
+ }
420
+ } else if (nextState.status !== "idle" && nextState.stepIndex <= 0) {
421
+ pendingResumeRef.current.delete(flowId);
422
+ }
423
+ return nextState;
424
+ },
425
+ [ensureStore, resolveResumeStrategy, runResumeHooks]
426
+ );
427
+ useEffect2(() => {
428
+ if (!autoStartFlow) {
429
+ autoStartRequestedRef.current = null;
430
+ return;
431
+ }
432
+ if (activeFlowId) return;
433
+ if (autoStartRequestedRef.current === autoStartFlow.id) return;
434
+ autoStartRequestedRef.current = autoStartFlow.id;
435
+ let cancelled = false;
436
+ const maybeAutoStart = async () => {
437
+ if (!storageAdapter && !fallbackStorageRef.current && isBrowser) {
438
+ fallbackStorageRef.current = createLocalStorageAdapter();
439
+ }
440
+ const resolvedStorageAdapter = storageAdapter ? storageAdapter : fallbackStorageRef.current;
441
+ if (!resolvedStorageAdapter) {
442
+ startFlow(autoStartFlow.id, { resume: true });
443
+ return;
444
+ }
445
+ const storageKey = storageNamespace ? `${storageNamespace}:${autoStartFlow.id}` : `${DEFAULT_STORAGE_PREFIX}:${autoStartFlow.id}`;
446
+ const snapshot = await resolveMaybePromise(
447
+ resolvedStorageAdapter.get(storageKey)
448
+ );
449
+ if (cancelled) return;
450
+ const currentVersionStr = serializeVersion(autoStartFlow.version);
451
+ const storedVersionStr = typeof snapshot?.version === "number" ? serializeVersion({ major: snapshot.version, minor: 0 }) : snapshot?.version;
452
+ if (snapshot && storedVersionStr === currentVersionStr) {
453
+ const storedState = snapshot.value;
454
+ const isFinished = storedState.status === "completed";
455
+ const isSkipped = storedState.status === "cancelled" && storedState.cancelReason === "skipped";
456
+ if (isFinished || isSkipped) {
457
+ return;
458
+ }
459
+ }
460
+ startFlow(autoStartFlow.id, { resume: true });
461
+ };
462
+ void maybeAutoStart();
463
+ return () => {
464
+ cancelled = true;
465
+ if (!activeFlowId) {
466
+ autoStartRequestedRef.current = null;
467
+ }
468
+ };
469
+ }, [activeFlowId, autoStartFlow, startFlow, storageAdapter, storageNamespace]);
470
+ const next = useCallback(() => getActiveStore().next(), [getActiveStore]);
471
+ const back = useCallback(() => getActiveStore().back(), [getActiveStore]);
472
+ const goToStep = useCallback(
473
+ (step) => getActiveStore().goToStep(step),
474
+ [getActiveStore]
475
+ );
476
+ const pause = useCallback(() => getActiveStore().pause(), [getActiveStore]);
477
+ const resume = useCallback(() => {
478
+ const store = getActiveStore();
479
+ const previousState = store.getState();
480
+ if (previousState.status === "paused") {
481
+ pendingResumeRef.current.add(store.definition.id);
482
+ }
483
+ const nextState = store.resume();
484
+ if (previousState.status === "paused" && nextState.status === "running" && nextState.stepIndex >= 0) {
485
+ pendingResumeRef.current.delete(store.definition.id);
486
+ const resumeStrategy = resolveResumeStrategy(store.definition);
487
+ const shouldRunResumeHooks = resumeStrategy === "current" ? true : nextState.stepIndex > 0;
488
+ if (shouldRunResumeHooks) {
489
+ void runResumeHooks(store.definition, nextState, resumeStrategy);
490
+ }
491
+ }
492
+ return nextState;
493
+ }, [getActiveStore, resolveResumeStrategy, runResumeHooks]);
494
+ const cancel = useCallback(
495
+ (reason) => getActiveStore().cancel(reason),
496
+ [getActiveStore]
497
+ );
498
+ const complete = useCallback(
499
+ () => getActiveStore().complete(),
500
+ [getActiveStore]
501
+ );
502
+ const toggleDebug = useCallback(() => {
503
+ setDebugEnabled((previous) => !previous);
504
+ }, []);
505
+ const activeStep = useMemo2(() => {
506
+ if (!state || !storeRef.current) return null;
507
+ if (state.stepIndex < 0) return null;
508
+ return storeRef.current.definition.steps[state.stepIndex] ?? null;
509
+ }, [state]);
510
+ useEffect2(() => {
511
+ if (!activeFlowId) return;
512
+ if (!pendingResumeRef.current.has(activeFlowId)) return;
513
+ if (!state || state.status !== "running") return;
514
+ const definition = flowMap.get(activeFlowId);
515
+ if (!definition) return;
516
+ const resumeStrategy = resolveResumeStrategy(definition);
517
+ const shouldRunResumeHooks = resumeStrategy === "current" ? state.stepIndex >= 0 : state.stepIndex > 0;
518
+ if (!shouldRunResumeHooks) {
519
+ pendingResumeRef.current.delete(activeFlowId);
520
+ return;
521
+ }
522
+ pendingResumeRef.current.delete(activeFlowId);
523
+ void runResumeHooks(definition, state, resumeStrategy);
524
+ }, [activeFlowId, flowMap, resolveResumeStrategy, runResumeHooks, state]);
525
+ const contextValue = useMemo2(
526
+ () => ({
527
+ flows: flowMap,
528
+ activeFlowId,
529
+ state,
530
+ activeStep,
531
+ startFlow,
532
+ next,
533
+ back,
534
+ goToStep,
535
+ pause,
536
+ resume,
537
+ cancel,
538
+ complete,
539
+ events,
540
+ debugEnabled,
541
+ setDebugEnabled,
542
+ toggleDebug,
543
+ delayInfo,
544
+ setDelayInfo,
545
+ backdropInteraction: backdropInteractionProp,
546
+ lockBodyScroll: lockBodyScrollProp
547
+ }),
548
+ [
549
+ activeFlowId,
550
+ activeStep,
551
+ back,
552
+ cancel,
553
+ complete,
554
+ debugEnabled,
555
+ events,
556
+ flowMap,
557
+ goToStep,
558
+ next,
559
+ pause,
560
+ resume,
561
+ delayInfo,
562
+ setDelayInfo,
563
+ setDebugEnabled,
564
+ startFlow,
565
+ state,
566
+ toggleDebug,
567
+ backdropInteractionProp,
568
+ lockBodyScrollProp
569
+ ]
570
+ );
571
+ const resolvedAnimationAdapter = usePreferredAnimationAdapter({
572
+ defaultAdapter: animationAdapterProp,
573
+ reducedMotionAdapter,
574
+ enabled: autoDetectReducedMotion
575
+ });
576
+ return /* @__PURE__ */ jsx2(AnimationAdapterProvider, { adapter: resolvedAnimationAdapter, children: /* @__PURE__ */ jsx2(LabelsProvider, { value: mergedLabels, children: /* @__PURE__ */ jsx2(TourContext.Provider, { value: contextValue, children }) }) });
577
+ };
578
+ var useTour = () => {
579
+ const context = useContext3(TourContext);
580
+ if (!context) {
581
+ throw new Error("useTour must be used within a TourProvider");
582
+ }
583
+ return context;
584
+ };
585
+ var useTourEvents = (event, handler) => {
586
+ const { events } = useTour();
587
+ useEffect2(() => {
588
+ if (!events) return;
589
+ return events.on(event, handler);
590
+ }, [event, events, handler]);
591
+ };
592
+
593
+ // src/hooks/useTourTarget.ts
594
+ import { useEffect as useEffect3, useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
595
+
596
+ // src/hooks/scrollMargin.ts
597
+ var DEFAULT_SCROLL_MARGIN = 16;
598
+ var sanitize = (value, fallback) => {
599
+ if (typeof value !== "number" || !Number.isFinite(value)) {
600
+ return fallback;
601
+ }
602
+ return value < 0 ? 0 : value;
603
+ };
604
+ var resolveScrollMargin = (margin, fallback = DEFAULT_SCROLL_MARGIN) => {
605
+ if (typeof margin === "number") {
606
+ const safe = sanitize(margin, fallback);
607
+ return {
608
+ top: safe,
609
+ bottom: safe,
610
+ left: safe,
611
+ right: safe
612
+ };
613
+ }
614
+ return {
615
+ top: sanitize(margin?.top, fallback),
616
+ bottom: sanitize(margin?.bottom, fallback),
617
+ left: sanitize(margin?.left, fallback),
618
+ right: sanitize(margin?.right, fallback)
619
+ };
620
+ };
621
+
622
+ // src/hooks/waitForPredicate.ts
623
+ var defaultWarn = (...args) => {
624
+ if (typeof console !== "undefined" && typeof console.warn === "function") {
625
+ console.warn(...args);
626
+ }
627
+ };
628
+ var isPromiseLike = (value) => {
629
+ return typeof value === "object" && value !== null && typeof value.then === "function";
630
+ };
631
+ var createWaitForPredicateController = ({
632
+ waitFor,
633
+ context,
634
+ onChange,
635
+ warn = defaultWarn
636
+ }) => {
637
+ const hasPredicate = Boolean(waitFor?.predicate && context);
638
+ const hasSubscription = Boolean(waitFor?.subscribe && context);
639
+ let satisfied = !hasPredicate && !hasSubscription;
640
+ let destroyed = false;
641
+ let pollId = null;
642
+ let subscriptionCleanup = null;
643
+ let lastCheckId = 0;
644
+ const update = (nextValue) => {
645
+ const normalized = Boolean(nextValue);
646
+ if (satisfied === normalized) return;
647
+ satisfied = normalized;
648
+ onChange?.(satisfied);
649
+ };
650
+ const evaluate = () => {
651
+ if (!waitFor?.predicate || !context || destroyed) {
652
+ return;
653
+ }
654
+ const checkId = ++lastCheckId;
655
+ let result;
656
+ try {
657
+ result = waitFor.predicate(context);
658
+ } catch (error) {
659
+ warn("[tour][waitFor] predicate threw an error", error);
660
+ update(false);
661
+ return;
662
+ }
663
+ if (isPromiseLike(result)) {
664
+ result.then(
665
+ (value) => {
666
+ if (destroyed || checkId !== lastCheckId) return;
667
+ update(Boolean(value));
668
+ },
669
+ (error) => {
670
+ if (destroyed || checkId !== lastCheckId) return;
671
+ warn("[tour][waitFor] predicate rejected", error);
672
+ update(false);
673
+ }
674
+ );
675
+ return;
676
+ }
677
+ update(Boolean(result));
678
+ };
679
+ const attachSubscription = () => {
680
+ if (!waitFor?.subscribe || !context) return;
681
+ try {
682
+ const cleanup = waitFor.subscribe({
683
+ ...context,
684
+ notify: (nextValue) => {
685
+ if (destroyed) return;
686
+ if (typeof nextValue === "boolean") {
687
+ update(nextValue);
688
+ return;
689
+ }
690
+ evaluate();
691
+ }
692
+ });
693
+ if (typeof cleanup === "function") {
694
+ subscriptionCleanup = cleanup;
695
+ }
696
+ } catch (error) {
697
+ warn("[tour][waitFor] subscribe handler threw an error", error);
698
+ }
699
+ };
700
+ const start = () => {
701
+ destroyed = false;
702
+ satisfied = !hasPredicate && !hasSubscription;
703
+ lastCheckId = 0;
704
+ if (waitFor?.predicate && context) {
705
+ evaluate();
706
+ const pollMs = Math.max(0, waitFor.pollMs ?? 200);
707
+ if (pollMs > 0) {
708
+ pollId = window.setInterval(evaluate, pollMs);
709
+ }
710
+ }
711
+ attachSubscription();
712
+ };
713
+ const stop = () => {
714
+ destroyed = true;
715
+ if (pollId !== null) {
716
+ window.clearInterval(pollId);
717
+ pollId = null;
718
+ }
719
+ if (subscriptionCleanup) {
720
+ try {
721
+ subscriptionCleanup();
722
+ } catch (error) {
723
+ warn("[tour][waitFor] subscribe cleanup threw an error", error);
724
+ }
725
+ subscriptionCleanup = null;
726
+ }
727
+ };
728
+ const isSatisfied = () => {
729
+ if (!waitFor) return true;
730
+ if (!hasPredicate && !hasSubscription) return true;
731
+ return satisfied;
732
+ };
733
+ return {
734
+ start,
735
+ stop,
736
+ evaluate,
737
+ isSatisfied
738
+ };
739
+ };
740
+
741
+ // src/hooks/useTourTarget.ts
742
+ var INITIAL_TARGET_INFO = {
743
+ element: null,
744
+ rect: null,
745
+ lastResolvedRect: null,
746
+ isScreen: false,
747
+ status: "idle",
748
+ stepId: null,
749
+ lastUpdated: 0,
750
+ visibility: "unknown",
751
+ rectSource: "none"
752
+ };
753
+ var DEFAULT_SCROLL_MODE = "center";
754
+ var MAX_AUTO_SCROLL_CHECKS = 10;
755
+ var STALLED_CHECKS_BEFORE_AUTO = 4;
756
+ var RECT_PROGRESS_THRESHOLD = 0.5;
757
+ var lastResolvedRectByStep = /* @__PURE__ */ new Map();
758
+ var rectHasMeaningfulSize = (rect) => !!rect && rect.width > 0 && rect.height > 0 && Number.isFinite(rect.top) && Number.isFinite(rect.left);
759
+ var computeRectSource = (rect, storedRect, isScreen) => {
760
+ if (isScreen) return "viewport";
761
+ if (rectHasMeaningfulSize(rect)) return "live";
762
+ if (storedRect) return "stored";
763
+ return "none";
764
+ };
765
+ var computeVisibilityState = (element, rect, isScreen) => {
766
+ if (!isBrowser) return "unknown";
767
+ if (isScreen) return "visible";
768
+ if (!element) return "missing";
769
+ if (!document.documentElement.contains(element)) return "detached";
770
+ const style = window.getComputedStyle(element);
771
+ const hiddenByStyle = style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse";
772
+ const transparent = Number.parseFloat(style.opacity || "1") === 0;
773
+ if (hiddenByStyle || transparent) {
774
+ return "hidden";
775
+ }
776
+ if (!rectHasMeaningfulSize(rect)) {
777
+ return "hidden";
778
+ }
779
+ return "visible";
780
+ };
781
+ var scrollContainerBy = (container, topDelta, leftDelta, behavior) => {
782
+ if (!isBrowser) return;
783
+ if (Math.abs(topDelta) < 0.5 && Math.abs(leftDelta) < 0.5) {
784
+ return;
785
+ }
786
+ const isRootContainer = container === document.body || container === document.documentElement || container === document.scrollingElement;
787
+ if (isRootContainer) {
788
+ window.scrollBy({
789
+ top: topDelta,
790
+ left: leftDelta,
791
+ behavior: behavior ?? "auto"
792
+ });
793
+ return;
794
+ }
795
+ const elementContainer = container;
796
+ if (typeof elementContainer.scrollBy === "function") {
797
+ elementContainer.scrollBy({
798
+ top: topDelta,
799
+ left: leftDelta,
800
+ behavior: behavior ?? "auto"
801
+ });
802
+ return;
803
+ }
804
+ elementContainer.scrollTop += topDelta;
805
+ elementContainer.scrollLeft += leftDelta;
806
+ };
807
+ var alignWithinViewport = (element, margin, behavior, mode) => {
808
+ if (mode === "preserve") return;
809
+ const viewportRect = getViewportRect();
810
+ const finalRect = getClientRect(element);
811
+ const availableHeight = viewportRect.height - (margin.top + margin.bottom);
812
+ if (availableHeight <= 0) return;
813
+ const desiredTop = mode === "center" ? margin.top + (availableHeight - finalRect.height) / 2 : margin.top;
814
+ const delta = finalRect.top - desiredTop;
815
+ if (Math.abs(delta) < 0.5) return;
816
+ window.scrollBy({
817
+ top: delta,
818
+ behavior: behavior ?? "auto"
819
+ });
820
+ };
821
+ var ensureElementInView = (element, margin, options) => {
822
+ const behavior = options?.behavior ?? "auto";
823
+ const mode = options?.mode ?? "preserve";
824
+ if (!isBrowser) return;
825
+ const scrollParents = getScrollParents(element);
826
+ const rootScroller = document.scrollingElement;
827
+ if (rootScroller && !scrollParents.includes(rootScroller)) {
828
+ scrollParents.push(rootScroller);
829
+ }
830
+ for (const container of scrollParents) {
831
+ const containerRect = container === rootScroller || container === document.body || container === document.documentElement ? getViewportRect() : getClientRect(container);
832
+ const targetRect = getClientRect(element);
833
+ let topDelta = 0;
834
+ if (targetRect.top < containerRect.top + margin.top) {
835
+ topDelta = targetRect.top - (containerRect.top + margin.top);
836
+ } else if (targetRect.bottom > containerRect.bottom - margin.bottom) {
837
+ topDelta = targetRect.bottom - (containerRect.bottom - margin.bottom);
838
+ }
839
+ let leftDelta = 0;
840
+ if (targetRect.left < containerRect.left + margin.left) {
841
+ leftDelta = targetRect.left - (containerRect.left + margin.left);
842
+ } else if (targetRect.right > containerRect.right - margin.right) {
843
+ leftDelta = targetRect.right - (containerRect.right - margin.right);
844
+ }
845
+ if (topDelta !== 0 || leftDelta !== 0) {
846
+ scrollContainerBy(container, topDelta, leftDelta, behavior);
847
+ }
848
+ }
849
+ const viewportRect = getViewportRect();
850
+ const finalRect = getClientRect(element);
851
+ let viewportTopDelta = 0;
852
+ if (finalRect.top < viewportRect.top + margin.top) {
853
+ viewportTopDelta = finalRect.top - (viewportRect.top + margin.top);
854
+ } else if (finalRect.bottom > viewportRect.bottom - margin.bottom) {
855
+ viewportTopDelta = finalRect.bottom - (viewportRect.bottom - margin.bottom);
856
+ }
857
+ let viewportLeftDelta = 0;
858
+ if (finalRect.left < viewportRect.left + margin.left) {
859
+ viewportLeftDelta = finalRect.left - (viewportRect.left + margin.left);
860
+ } else if (finalRect.right > viewportRect.right - margin.right) {
861
+ viewportLeftDelta = finalRect.right - (viewportRect.right - margin.right);
862
+ }
863
+ if (viewportTopDelta !== 0 || viewportLeftDelta !== 0) {
864
+ window.scrollBy({
865
+ top: viewportTopDelta,
866
+ left: viewportLeftDelta,
867
+ behavior
868
+ });
869
+ }
870
+ alignWithinViewport(element, margin, behavior, mode);
871
+ };
872
+ var resolveStepTarget = (target) => {
873
+ if (!isBrowser) return null;
874
+ if (target === "screen") {
875
+ return document.body;
876
+ }
877
+ if (target.getNode) {
878
+ try {
879
+ const node = target.getNode();
880
+ if (node) {
881
+ return node;
882
+ }
883
+ } catch {
884
+ }
885
+ }
886
+ if (target.selector) {
887
+ try {
888
+ return document.querySelector(target.selector);
889
+ } catch {
890
+ return null;
891
+ }
892
+ }
893
+ return null;
894
+ };
895
+ var useTourTarget = () => {
896
+ const { activeStep, state, activeFlowId, flows } = useTour();
897
+ const [targetInfo, setTargetInfo] = useState3(INITIAL_TARGET_INFO);
898
+ const autoScrollStateRef = useRef2({ stepId: null, checks: 0, stalledChecks: 0, done: false, lastRect: null });
899
+ const autoScrollRafRef = useRef2(null);
900
+ const autoScrollTimeoutRef = useRef2(null);
901
+ const lastRectRef = useRef2(null);
902
+ const initialScrollStepRef = useRef2(null);
903
+ const cancelAutoScrollLoop = () => {
904
+ if (!isBrowser) return;
905
+ if (autoScrollTimeoutRef.current !== null) {
906
+ globalThis.clearTimeout(autoScrollTimeoutRef.current);
907
+ autoScrollTimeoutRef.current = null;
908
+ }
909
+ if (autoScrollRafRef.current !== null) {
910
+ window.cancelAnimationFrame(autoScrollRafRef.current);
911
+ autoScrollRafRef.current = null;
912
+ }
913
+ };
914
+ useEffect3(() => {
915
+ if (!activeStep) {
916
+ initialScrollStepRef.current = null;
917
+ }
918
+ return () => {
919
+ initialScrollStepRef.current = null;
920
+ };
921
+ }, [activeStep?.id]);
922
+ useLayoutEffect(() => {
923
+ if (!isBrowser) return;
924
+ if (!activeStep) return;
925
+ if (targetInfo.status !== "ready") return;
926
+ if (targetInfo.isScreen) return;
927
+ if (!targetInfo.element) return;
928
+ if (initialScrollStepRef.current === activeStep.id) {
929
+ return;
930
+ }
931
+ initialScrollStepRef.current = activeStep.id;
932
+ const margin = resolveScrollMargin(
933
+ activeStep.targetBehavior?.scrollMargin,
934
+ DEFAULT_SCROLL_MARGIN
935
+ );
936
+ const scrollMode = activeStep.targetBehavior?.scrollMode ?? DEFAULT_SCROLL_MODE;
937
+ const hasLiveRect = targetInfo.rectSource === "live";
938
+ const scrollBehavior = hasLiveRect ? "smooth" : "auto";
939
+ ensureElementInView(targetInfo.element, margin, {
940
+ behavior: scrollBehavior,
941
+ mode: scrollMode
942
+ });
943
+ }, [
944
+ activeStep?.id,
945
+ activeStep?.targetBehavior?.scrollMargin,
946
+ activeStep?.targetBehavior?.scrollMode,
947
+ targetInfo.rect,
948
+ targetInfo.lastResolvedRect,
949
+ targetInfo.element,
950
+ targetInfo.isScreen,
951
+ targetInfo.status,
952
+ targetInfo.rectSource
953
+ ]);
954
+ useEffect3(() => {
955
+ if (!activeStep || !state || state.status !== "running") {
956
+ setTargetInfo(INITIAL_TARGET_INFO);
957
+ autoScrollStateRef.current = {
958
+ stepId: null,
959
+ checks: 0,
960
+ stalledChecks: 0,
961
+ done: false,
962
+ lastRect: null
963
+ };
964
+ cancelAutoScrollLoop();
965
+ return;
966
+ }
967
+ if (!isBrowser) {
968
+ const storedRect = lastResolvedRectByStep.get(activeStep.id) ?? null;
969
+ setTargetInfo({
970
+ element: null,
971
+ rect: null,
972
+ lastResolvedRect: storedRect ? { ...storedRect } : null,
973
+ isScreen: activeStep.target === "screen",
974
+ status: "resolving",
975
+ stepId: activeStep.id,
976
+ lastUpdated: Date.now(),
977
+ visibility: "unknown",
978
+ rectSource: storedRect ? "stored" : "none"
979
+ });
980
+ return;
981
+ }
982
+ const currentStep = activeStep;
983
+ const activeFlow = activeFlowId ? flows.get(activeFlowId) ?? null : null;
984
+ const isScreen = currentStep.target === "screen";
985
+ const waitForSelectorRaw = currentStep.waitFor?.selector;
986
+ const waitForSelector = typeof waitForSelectorRaw === "string" ? waitForSelectorRaw.trim() : void 0;
987
+ const hasWaitForSelector = Boolean(waitForSelector);
988
+ const waitForTimeout = Math.max(0, currentStep.waitFor?.timeout ?? 8e3);
989
+ const waitContext = activeFlow ? {
990
+ flow: activeFlow,
991
+ state,
992
+ step: currentStep
993
+ } : null;
994
+ let cancelled = false;
995
+ let resolvePollId = null;
996
+ let waitForPollId = null;
997
+ let resizeObserver = null;
998
+ let mutationObserver = null;
999
+ let scrollParents = [];
1000
+ const cleanupFns = [];
1001
+ let element = null;
1002
+ let rafId = null;
1003
+ let lastStatus = "idle";
1004
+ let lastElement = null;
1005
+ let hasEmitted = false;
1006
+ let waitForStartedAt = null;
1007
+ let waitForTimedOut = false;
1008
+ let waitForSelectorWarned = false;
1009
+ let waitForTimeoutWarned = false;
1010
+ let waitForPredicateController = null;
1011
+ lastRectRef.current = null;
1012
+ function clearResolvePolling() {
1013
+ if (resolvePollId !== null) {
1014
+ window.clearInterval(resolvePollId);
1015
+ resolvePollId = null;
1016
+ }
1017
+ }
1018
+ function clearWaitForPoll() {
1019
+ if (waitForPollId !== null) {
1020
+ window.clearInterval(waitForPollId);
1021
+ waitForPollId = null;
1022
+ }
1023
+ }
1024
+ const rectChanged = (nextRect) => {
1025
+ const previous = lastRectRef.current;
1026
+ if (!previous || !nextRect) {
1027
+ return previous !== nextRect;
1028
+ }
1029
+ const threshold = 0.25;
1030
+ return Math.abs(previous.top - nextRect.top) > threshold || Math.abs(previous.left - nextRect.left) > threshold || Math.abs(previous.width - nextRect.width) > threshold || Math.abs(previous.height - nextRect.height) > threshold;
1031
+ };
1032
+ const isWaitForSelectorSatisfied = () => {
1033
+ if (!hasWaitForSelector) return true;
1034
+ try {
1035
+ return document.querySelector(waitForSelector) !== null;
1036
+ } catch (error) {
1037
+ if (!waitForSelectorWarned && typeof console !== "undefined") {
1038
+ console.warn(
1039
+ "[tour][waitFor] selector lookup failed",
1040
+ waitForSelector,
1041
+ error
1042
+ );
1043
+ waitForSelectorWarned = true;
1044
+ }
1045
+ return false;
1046
+ }
1047
+ };
1048
+ const isWaitForSatisfied = () => {
1049
+ const selectorReady = !hasWaitForSelector || isWaitForSelectorSatisfied();
1050
+ const predicateReady = waitForPredicateController?.isSatisfied() ?? true;
1051
+ return selectorReady && predicateReady;
1052
+ };
1053
+ function updateTargetState(status, rectOverride) {
1054
+ if (cancelled) return;
1055
+ const rect = typeof rectOverride !== "undefined" ? rectOverride : isScreen ? getViewportRect() : element ? getClientRect(element) : null;
1056
+ if (status === "ready" && hasWaitForSelector && waitForStartedAt === null) {
1057
+ waitForStartedAt = Date.now();
1058
+ }
1059
+ if (!waitForTimedOut && waitForTimeout > 0 && waitForStartedAt !== null && Date.now() - waitForStartedAt >= waitForTimeout) {
1060
+ waitForTimedOut = true;
1061
+ if (!waitForTimeoutWarned && typeof console !== "undefined") {
1062
+ console.warn(
1063
+ "[tour][waitFor] timeout exceeded for step",
1064
+ currentStep.id,
1065
+ "selector:",
1066
+ waitForSelector
1067
+ );
1068
+ waitForTimeoutWarned = true;
1069
+ }
1070
+ }
1071
+ const hasRect = rectHasMeaningfulSize(rect);
1072
+ const waitConditionMet = waitForTimedOut || isWaitForSatisfied();
1073
+ const nextStatus = status === "ready" && (isScreen || hasRect) && waitConditionMet ? "ready" : "resolving";
1074
+ if (waitConditionMet) {
1075
+ clearWaitForPoll();
1076
+ }
1077
+ const storedRect = lastResolvedRectByStep.get(currentStep.id) ?? null;
1078
+ const shouldUpdate = !hasEmitted || rectChanged(rect) || lastStatus !== nextStatus || element !== lastElement;
1079
+ if (!shouldUpdate) {
1080
+ return;
1081
+ }
1082
+ lastRectRef.current = rect ? { ...rect } : null;
1083
+ lastStatus = nextStatus;
1084
+ hasEmitted = true;
1085
+ lastElement = element ?? null;
1086
+ const shouldPersistRect = nextStatus === "ready" && !isScreen && rectHasMeaningfulSize(rect);
1087
+ if (shouldPersistRect && rect) {
1088
+ lastResolvedRectByStep.set(currentStep.id, { ...rect });
1089
+ }
1090
+ const lastResolvedRect = shouldPersistRect && rect ? { ...rect } : storedRect ? { ...storedRect } : null;
1091
+ const visibility = computeVisibilityState(element ?? null, rect, isScreen);
1092
+ const rectSource = computeRectSource(rect, lastResolvedRect, isScreen);
1093
+ setTargetInfo({
1094
+ element: element ?? null,
1095
+ rect,
1096
+ lastResolvedRect,
1097
+ isScreen,
1098
+ status: nextStatus,
1099
+ stepId: currentStep.id,
1100
+ lastUpdated: Date.now(),
1101
+ visibility,
1102
+ rectSource
1103
+ });
1104
+ }
1105
+ const commitInfo = (status) => {
1106
+ updateTargetState(status);
1107
+ };
1108
+ waitForPredicateController = createWaitForPredicateController({
1109
+ waitFor: currentStep.waitFor,
1110
+ context: waitContext,
1111
+ onChange: () => {
1112
+ updateTargetState(element ? "ready" : "resolving");
1113
+ }
1114
+ });
1115
+ waitForPredicateController.start();
1116
+ function stopRaf() {
1117
+ if (rafId !== null) {
1118
+ window.cancelAnimationFrame(rafId);
1119
+ rafId = null;
1120
+ }
1121
+ }
1122
+ function startRafMonitor() {
1123
+ if (isScreen || !isBrowser) return;
1124
+ stopRaf();
1125
+ const tick = () => {
1126
+ if (cancelled) return;
1127
+ if (!element) {
1128
+ updateTargetState("resolving", null);
1129
+ } else {
1130
+ const rect = getClientRect(element);
1131
+ if (rectChanged(rect)) {
1132
+ updateTargetState("ready", rect);
1133
+ }
1134
+ }
1135
+ rafId = window.requestAnimationFrame(tick);
1136
+ };
1137
+ rafId = window.requestAnimationFrame(tick);
1138
+ cleanupFns.push(stopRaf);
1139
+ }
1140
+ function resetObservers() {
1141
+ cleanupFns.forEach((dispose) => dispose());
1142
+ cleanupFns.length = 0;
1143
+ if (resizeObserver) {
1144
+ resizeObserver.disconnect();
1145
+ resizeObserver = null;
1146
+ }
1147
+ if (mutationObserver) {
1148
+ mutationObserver.disconnect();
1149
+ mutationObserver = null;
1150
+ }
1151
+ scrollParents = [];
1152
+ clearWaitForPoll();
1153
+ stopRaf();
1154
+ }
1155
+ function startObservers() {
1156
+ if (cancelled) return;
1157
+ resetObservers();
1158
+ clearResolvePolling();
1159
+ if (isScreen) {
1160
+ const onResize = () => commitInfo("ready");
1161
+ window.addEventListener("resize", onResize);
1162
+ window.addEventListener("scroll", onResize, true);
1163
+ cleanupFns.push(() => {
1164
+ window.removeEventListener("resize", onResize);
1165
+ window.removeEventListener("scroll", onResize, true);
1166
+ });
1167
+ if (typeof window !== "undefined" && window.visualViewport) {
1168
+ const onViewportChange = () => commitInfo("ready");
1169
+ window.visualViewport.addEventListener("resize", onViewportChange);
1170
+ window.visualViewport.addEventListener("scroll", onViewportChange);
1171
+ cleanupFns.push(() => {
1172
+ window.visualViewport?.removeEventListener(
1173
+ "resize",
1174
+ onViewportChange
1175
+ );
1176
+ window.visualViewport?.removeEventListener(
1177
+ "scroll",
1178
+ onViewportChange
1179
+ );
1180
+ });
1181
+ }
1182
+ } else if (element) {
1183
+ if (typeof ResizeObserver === "function") {
1184
+ resizeObserver = new ResizeObserver(() => updateTargetState("ready"));
1185
+ resizeObserver.observe(element);
1186
+ }
1187
+ const onReposition = () => commitInfo("ready");
1188
+ window.addEventListener("resize", onReposition);
1189
+ window.addEventListener("scroll", onReposition, true);
1190
+ cleanupFns.push(() => {
1191
+ window.removeEventListener("resize", onReposition);
1192
+ window.removeEventListener("scroll", onReposition, true);
1193
+ });
1194
+ const onAncestorScroll = () => commitInfo("ready");
1195
+ scrollParents = getScrollParents(element);
1196
+ if (scrollParents.length > 0) {
1197
+ scrollParents.forEach(
1198
+ (parent) => parent.addEventListener("scroll", onAncestorScroll, {
1199
+ passive: true
1200
+ })
1201
+ );
1202
+ cleanupFns.push(() => {
1203
+ scrollParents.forEach(
1204
+ (parent) => parent.removeEventListener("scroll", onAncestorScroll)
1205
+ );
1206
+ scrollParents = [];
1207
+ });
1208
+ }
1209
+ startRafMonitor();
1210
+ if (typeof MutationObserver === "function") {
1211
+ mutationObserver = new MutationObserver(() => {
1212
+ if (cancelled) return;
1213
+ if (element && document.documentElement.contains(element)) {
1214
+ return;
1215
+ }
1216
+ resetObservers();
1217
+ element = null;
1218
+ commitInfo("resolving");
1219
+ startResolvePolling();
1220
+ });
1221
+ mutationObserver.observe(document.body, {
1222
+ childList: true,
1223
+ subtree: true
1224
+ });
1225
+ cleanupFns.push(() => {
1226
+ mutationObserver?.disconnect();
1227
+ mutationObserver = null;
1228
+ });
1229
+ }
1230
+ }
1231
+ if (hasWaitForSelector) {
1232
+ const pollWaitFor = () => updateTargetState("ready");
1233
+ pollWaitFor();
1234
+ clearWaitForPoll();
1235
+ waitForPollId = window.setInterval(pollWaitFor, 150);
1236
+ cleanupFns.push(clearWaitForPoll);
1237
+ }
1238
+ commitInfo("ready");
1239
+ }
1240
+ function attemptAttach() {
1241
+ const nextElement = resolveStepTarget(currentStep.target);
1242
+ if (isScreen || nextElement) {
1243
+ element = nextElement;
1244
+ startObservers();
1245
+ return true;
1246
+ }
1247
+ commitInfo("resolving");
1248
+ return false;
1249
+ }
1250
+ function startResolvePolling() {
1251
+ clearResolvePolling();
1252
+ const pollInterval = 200;
1253
+ const timeout = waitForTimeout;
1254
+ const startedAt = Date.now();
1255
+ resolvePollId = window.setInterval(() => {
1256
+ if (attemptAttach()) {
1257
+ clearResolvePolling();
1258
+ return;
1259
+ }
1260
+ if (timeout > 0 && Date.now() - startedAt >= timeout) {
1261
+ clearResolvePolling();
1262
+ }
1263
+ }, pollInterval);
1264
+ }
1265
+ if (!attemptAttach()) {
1266
+ startResolvePolling();
1267
+ }
1268
+ return () => {
1269
+ cancelled = true;
1270
+ clearResolvePolling();
1271
+ clearWaitForPoll();
1272
+ resetObservers();
1273
+ waitForPredicateController?.stop();
1274
+ waitForPredicateController = null;
1275
+ };
1276
+ }, [activeStep, activeFlowId, flows, state]);
1277
+ useEffect3(() => {
1278
+ if (!isBrowser) return;
1279
+ if (!activeStep) {
1280
+ cancelAutoScrollLoop();
1281
+ return;
1282
+ }
1283
+ if (targetInfo.status !== "ready") {
1284
+ cancelAutoScrollLoop();
1285
+ return;
1286
+ }
1287
+ if (targetInfo.isScreen) {
1288
+ cancelAutoScrollLoop();
1289
+ return;
1290
+ }
1291
+ if (!targetInfo.element) {
1292
+ cancelAutoScrollLoop();
1293
+ return;
1294
+ }
1295
+ const autoState = autoScrollStateRef.current;
1296
+ if (autoState.stepId !== activeStep.id) {
1297
+ autoState.stepId = activeStep.id;
1298
+ autoState.checks = 0;
1299
+ autoState.stalledChecks = 0;
1300
+ autoState.done = false;
1301
+ autoState.lastRect = null;
1302
+ cancelAutoScrollLoop();
1303
+ } else if (autoState.done) {
1304
+ cancelAutoScrollLoop();
1305
+ return;
1306
+ }
1307
+ const { element } = targetInfo;
1308
+ const scrollMode = activeStep.targetBehavior?.scrollMode ?? "center";
1309
+ const runCheck = () => {
1310
+ autoScrollRafRef.current = null;
1311
+ if (!isBrowser) return;
1312
+ if (autoState.stepId !== activeStep.id) return;
1313
+ if (!element.isConnected) return;
1314
+ const rect = getClientRect(element);
1315
+ const viewport = getViewportRect();
1316
+ const margin = resolveScrollMargin(
1317
+ activeStep.targetBehavior?.scrollMargin,
1318
+ DEFAULT_SCROLL_MARGIN
1319
+ );
1320
+ const fitsHeight = rect.height <= viewport.height - (margin.top + margin.bottom);
1321
+ const fitsWidth = rect.width <= viewport.width - (margin.left + margin.right);
1322
+ const verticalSatisfied = fitsHeight ? rect.top >= margin.top && rect.bottom <= viewport.height - margin.bottom : rect.top <= margin.top && rect.bottom >= viewport.height - margin.bottom;
1323
+ const horizontalSatisfied = fitsWidth ? rect.left >= margin.left && rect.right <= viewport.width - margin.right : rect.left <= margin.left && rect.right >= viewport.width - margin.right;
1324
+ if (verticalSatisfied && horizontalSatisfied) {
1325
+ autoState.done = true;
1326
+ return;
1327
+ }
1328
+ autoState.checks += 1;
1329
+ if (autoState.checks >= MAX_AUTO_SCROLL_CHECKS) {
1330
+ autoState.done = true;
1331
+ return;
1332
+ }
1333
+ const previousRect = autoState.lastRect;
1334
+ const hasProgress = !previousRect || Math.abs(previousRect.top - rect.top) > RECT_PROGRESS_THRESHOLD || Math.abs(previousRect.left - rect.left) > RECT_PROGRESS_THRESHOLD || Math.abs(previousRect.bottom - rect.bottom) > RECT_PROGRESS_THRESHOLD || Math.abs(previousRect.right - rect.right) > RECT_PROGRESS_THRESHOLD;
1335
+ autoState.lastRect = rect;
1336
+ if (hasProgress) {
1337
+ autoState.stalledChecks = 0;
1338
+ } else {
1339
+ autoState.stalledChecks += 1;
1340
+ }
1341
+ const behavior = autoState.stalledChecks >= STALLED_CHECKS_BEFORE_AUTO ? "auto" : "smooth";
1342
+ ensureElementInView(element, margin, {
1343
+ behavior,
1344
+ mode: scrollMode
1345
+ });
1346
+ autoScrollTimeoutRef.current = globalThis.setTimeout(() => {
1347
+ autoScrollRafRef.current = window.requestAnimationFrame(runCheck);
1348
+ }, 120);
1349
+ };
1350
+ cancelAutoScrollLoop();
1351
+ autoScrollRafRef.current = window.requestAnimationFrame(runCheck);
1352
+ return cancelAutoScrollLoop;
1353
+ }, [activeStep, activeStep?.targetBehavior?.scrollMode, targetInfo]);
1354
+ return targetInfo;
1355
+ };
1356
+
1357
+ // src/hooks/useHudState.ts
1358
+ import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useState as useState6 } from "react";
1359
+
1360
+ // src/hooks/useAdvanceRules.ts
1361
+ import { useEffect as useEffect4 } from "react";
1362
+ var DEFAULT_POLL_MS = 250;
1363
+ var isListenerTarget = (value) => {
1364
+ return !!value && typeof value.addEventListener === "function" && typeof value.removeEventListener === "function";
1365
+ };
1366
+ var resolveEventTarget = (rule, target) => {
1367
+ if (!isBrowser) return null;
1368
+ if (!rule.on || rule.on === "target") {
1369
+ return target.element ?? null;
1370
+ }
1371
+ if (rule.on === "window") {
1372
+ return window;
1373
+ }
1374
+ if (rule.on === "document") {
1375
+ return document;
1376
+ }
1377
+ try {
1378
+ return document.querySelector(rule.on);
1379
+ } catch {
1380
+ return null;
1381
+ }
1382
+ };
1383
+ var matchesRouteRule = (rule, path) => {
1384
+ if (!rule.to) return true;
1385
+ if (typeof rule.to === "string") {
1386
+ return path === rule.to;
1387
+ }
1388
+ return rule.to.test(path);
1389
+ };
1390
+ var useAdvanceRules = (target) => {
1391
+ const {
1392
+ activeFlowId,
1393
+ flows,
1394
+ activeStep,
1395
+ state,
1396
+ next,
1397
+ complete,
1398
+ setDelayInfo
1399
+ } = useTour();
1400
+ useEffect4(() => {
1401
+ if (!isBrowser) return;
1402
+ if (!state || state.status !== "running") return;
1403
+ if (!activeStep) return;
1404
+ const definition = activeFlowId ? flows.get(activeFlowId) : void 0;
1405
+ if (!definition) return;
1406
+ const rules = activeStep.advance;
1407
+ if (!rules || rules.length === 0) return;
1408
+ let resolved = false;
1409
+ const hasResolved = () => resolved;
1410
+ const cleanupFns = [];
1411
+ const runCleanup = () => {
1412
+ while (cleanupFns.length > 0) {
1413
+ const dispose = cleanupFns.pop();
1414
+ try {
1415
+ dispose?.();
1416
+ } catch (error) {
1417
+ console.warn("[tour][advance] cleanup failed", error);
1418
+ }
1419
+ }
1420
+ };
1421
+ const addCleanup = (fn) => {
1422
+ if (fn) {
1423
+ cleanupFns.push(fn);
1424
+ }
1425
+ };
1426
+ const clearDelayInfo = () => {
1427
+ setDelayInfo((info) => {
1428
+ if (!info) return null;
1429
+ if (info.flowId !== definition.id) return info;
1430
+ if (info.stepId !== activeStep.id) return info;
1431
+ return null;
1432
+ });
1433
+ };
1434
+ const finish = () => {
1435
+ if (resolved) return;
1436
+ resolved = true;
1437
+ runCleanup();
1438
+ clearDelayInfo();
1439
+ const totalSteps = definition.steps.length;
1440
+ if (totalSteps > 0 && state.stepIndex >= totalSteps - 1) {
1441
+ complete();
1442
+ } else {
1443
+ next();
1444
+ }
1445
+ };
1446
+ const predicateCtx = {
1447
+ flow: definition,
1448
+ state,
1449
+ step: activeStep
1450
+ };
1451
+ for (const rule of rules) {
1452
+ if (hasResolved()) break;
1453
+ switch (rule.type) {
1454
+ case "manual": {
1455
+ break;
1456
+ }
1457
+ case "delay": {
1458
+ const totalMs = Math.max(0, rule.ms);
1459
+ const startedAt = typeof performance !== "undefined" ? performance.now() : Date.now();
1460
+ const endsAt = startedAt + totalMs;
1461
+ setDelayInfo({
1462
+ flowId: definition.id,
1463
+ stepId: activeStep.id,
1464
+ totalMs,
1465
+ startedAt,
1466
+ endsAt
1467
+ });
1468
+ const timer = window.setTimeout(() => {
1469
+ finish();
1470
+ }, totalMs);
1471
+ addCleanup(() => window.clearTimeout(timer));
1472
+ break;
1473
+ }
1474
+ case "event": {
1475
+ const eventTarget = resolveEventTarget(rule, target);
1476
+ if (!isListenerTarget(eventTarget)) {
1477
+ continue;
1478
+ }
1479
+ const handler = () => finish();
1480
+ eventTarget.addEventListener(rule.event, handler);
1481
+ addCleanup(() => {
1482
+ eventTarget.removeEventListener(rule.event, handler);
1483
+ });
1484
+ if (rule.event === "click") {
1485
+ const keyupHandler = (event) => {
1486
+ if (!(event instanceof KeyboardEvent)) return;
1487
+ if (event.repeat) return;
1488
+ const key = event.key;
1489
+ if (key === "Enter" || key === " " || key === "Spacebar") {
1490
+ finish();
1491
+ }
1492
+ };
1493
+ eventTarget.addEventListener("keyup", keyupHandler);
1494
+ addCleanup(() => {
1495
+ eventTarget.removeEventListener("keyup", keyupHandler);
1496
+ });
1497
+ }
1498
+ break;
1499
+ }
1500
+ case "predicate": {
1501
+ const pollMs = Math.max(50, rule.pollMs ?? DEFAULT_POLL_MS);
1502
+ const timeoutMs = rule.timeoutMs;
1503
+ const executeCheck = () => {
1504
+ if (resolved) return;
1505
+ try {
1506
+ if (rule.check(predicateCtx)) {
1507
+ finish();
1508
+ }
1509
+ } catch (error) {
1510
+ console.warn("[tour][advance] predicate check failed", error);
1511
+ }
1512
+ };
1513
+ executeCheck();
1514
+ if (hasResolved()) {
1515
+ break;
1516
+ }
1517
+ const intervalId = window.setInterval(executeCheck, pollMs);
1518
+ addCleanup(() => window.clearInterval(intervalId));
1519
+ if (typeof timeoutMs === "number" && timeoutMs > 0) {
1520
+ const timeoutId = window.setTimeout(() => {
1521
+ window.clearInterval(intervalId);
1522
+ }, timeoutMs);
1523
+ addCleanup(() => window.clearTimeout(timeoutId));
1524
+ }
1525
+ break;
1526
+ }
1527
+ case "route": {
1528
+ const checkRoute = (path) => {
1529
+ if (resolved) return;
1530
+ if (matchesRouteRule(rule, path)) {
1531
+ finish();
1532
+ }
1533
+ };
1534
+ const initialPath = getCurrentRoutePath();
1535
+ checkRoute(initialPath);
1536
+ if (hasResolved()) {
1537
+ break;
1538
+ }
1539
+ const unsubscribe = subscribeToRouteChanges((path) => {
1540
+ checkRoute(path);
1541
+ });
1542
+ addCleanup(unsubscribe);
1543
+ break;
1544
+ }
1545
+ default: {
1546
+ const neverRule = rule;
1547
+ console.warn("[tour][advance] unsupported advance rule", neverRule);
1548
+ }
1549
+ }
1550
+ }
1551
+ return () => {
1552
+ resolved = true;
1553
+ runCleanup();
1554
+ clearDelayInfo();
1555
+ };
1556
+ }, [
1557
+ activeFlowId,
1558
+ activeStep,
1559
+ complete,
1560
+ flows,
1561
+ next,
1562
+ state,
1563
+ target.element,
1564
+ setDelayInfo,
1565
+ target.lastUpdated,
1566
+ target.status
1567
+ ]);
1568
+ };
1569
+
1570
+ // src/hooks/useHiddenTargetFallback.ts
1571
+ import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef3, useState as useState4 } from "react";
1572
+ var DEFAULT_DELAY_MS = 900;
1573
+ var HIDDEN_VISIBILITIES = [
1574
+ "hidden",
1575
+ "detached"
1576
+ ];
1577
+ var useHiddenTargetFallback = ({
1578
+ step,
1579
+ target,
1580
+ viewportRect,
1581
+ onSkip
1582
+ }) => {
1583
+ const [usingScreenFallback, setUsingScreenFallback] = useState4(false);
1584
+ const timeoutRef = useRef3(null);
1585
+ const skipTriggeredRef = useRef3(false);
1586
+ const hiddenMode = step?.targetBehavior?.hidden ?? "screen";
1587
+ const hiddenDelayMs = Math.max(
1588
+ 0,
1589
+ step?.targetBehavior?.hiddenDelayMs ?? DEFAULT_DELAY_MS
1590
+ );
1591
+ const clearPendingTimeout = () => {
1592
+ if (timeoutRef.current !== null) {
1593
+ globalThis.clearTimeout(timeoutRef.current);
1594
+ timeoutRef.current = null;
1595
+ }
1596
+ };
1597
+ useEffect5(() => {
1598
+ skipTriggeredRef.current = false;
1599
+ setUsingScreenFallback(false);
1600
+ clearPendingTimeout();
1601
+ return clearPendingTimeout;
1602
+ }, [step?.id]);
1603
+ useEffect5(() => {
1604
+ if (!isBrowser) return void 0;
1605
+ if (!step) return void 0;
1606
+ clearPendingTimeout();
1607
+ const shouldHandleHiddenTarget = HIDDEN_VISIBILITIES.includes(target.visibility) && !target.isScreen && target.status === "ready";
1608
+ if (!shouldHandleHiddenTarget) {
1609
+ setUsingScreenFallback(false);
1610
+ return void 0;
1611
+ }
1612
+ if (hiddenMode !== "screen") {
1613
+ setUsingScreenFallback(false);
1614
+ }
1615
+ timeoutRef.current = globalThis.setTimeout(() => {
1616
+ if (hiddenMode === "screen") {
1617
+ setUsingScreenFallback(true);
1618
+ return;
1619
+ }
1620
+ if (!skipTriggeredRef.current) {
1621
+ skipTriggeredRef.current = true;
1622
+ onSkip();
1623
+ }
1624
+ }, hiddenDelayMs);
1625
+ return clearPendingTimeout;
1626
+ }, [
1627
+ step,
1628
+ target.visibility,
1629
+ target.isScreen,
1630
+ target.status,
1631
+ hiddenMode,
1632
+ hiddenDelayMs,
1633
+ onSkip
1634
+ ]);
1635
+ const resolvedTarget = useMemo3(() => {
1636
+ if (!usingScreenFallback) {
1637
+ return target;
1638
+ }
1639
+ return {
1640
+ ...target,
1641
+ element: null,
1642
+ rect: viewportRect,
1643
+ lastResolvedRect: viewportRect,
1644
+ isScreen: true,
1645
+ rectSource: "viewport",
1646
+ visibility: "visible"
1647
+ };
1648
+ }, [target, usingScreenFallback, viewportRect]);
1649
+ return {
1650
+ target: resolvedTarget,
1651
+ usingScreenFallback
1652
+ };
1653
+ };
1654
+
1655
+ // src/hooks/useViewportRect.ts
1656
+ import { useEffect as useEffect6, useRef as useRef4, useState as useState5 } from "react";
1657
+ var useViewportRect = () => {
1658
+ const [viewport, setViewport] = useState5(
1659
+ () => getViewportRect()
1660
+ );
1661
+ const rafRef = useRef4(null);
1662
+ useEffect6(() => {
1663
+ if (!isBrowser) return;
1664
+ const updateViewport = () => {
1665
+ rafRef.current = null;
1666
+ setViewport(getViewportRect());
1667
+ };
1668
+ const scheduleUpdate = () => {
1669
+ if (rafRef.current !== null) return;
1670
+ rafRef.current = window.requestAnimationFrame(updateViewport);
1671
+ };
1672
+ const listeners = [];
1673
+ const addListener = (target, type, handler) => {
1674
+ if (!target) return;
1675
+ target.addEventListener(type, handler);
1676
+ listeners.push({ target, type, handler });
1677
+ };
1678
+ addListener(window, "resize", scheduleUpdate);
1679
+ addListener(window, "orientationchange", scheduleUpdate);
1680
+ addListener(window, "scroll", scheduleUpdate);
1681
+ addListener(window.visualViewport ?? null, "resize", scheduleUpdate);
1682
+ addListener(window.visualViewport ?? null, "scroll", scheduleUpdate);
1683
+ return () => {
1684
+ listeners.forEach(
1685
+ ({ target, type, handler }) => target.removeEventListener(type, handler)
1686
+ );
1687
+ if (rafRef.current !== null) {
1688
+ window.cancelAnimationFrame(rafRef.current);
1689
+ rafRef.current = null;
1690
+ }
1691
+ };
1692
+ }, []);
1693
+ return viewport;
1694
+ };
1695
+
1696
+ // src/hooks/useHudState.ts
1697
+ var EXIT_BUFFER_MS = 450;
1698
+ var normalizeFlowFilter = (value) => {
1699
+ if (!value) return null;
1700
+ return Array.isArray(value) ? value : [value];
1701
+ };
1702
+ var useHudState = (options = {}) => {
1703
+ const { flowId } = options;
1704
+ const flowFilter = useMemo4(() => normalizeFlowFilter(flowId), [flowId]);
1705
+ const { state, activeStep, activeFlowId, flows, next, complete } = useTour();
1706
+ const target = useTourTarget();
1707
+ const viewportRect = useViewportRect();
1708
+ useAdvanceRules(target);
1709
+ const matchesFlowFilter = useMemo4(() => {
1710
+ if (!flowFilter || flowFilter.length === 0) return true;
1711
+ if (!activeFlowId) return false;
1712
+ return flowFilter.includes(activeFlowId);
1713
+ }, [activeFlowId, flowFilter]);
1714
+ const isRunning = state?.status === "running";
1715
+ const runningState = isRunning && matchesFlowFilter ? state : null;
1716
+ const runningStep = runningState && activeStep ? activeStep : null;
1717
+ const [shouldRender, setShouldRender] = useState6(
1718
+ Boolean(runningStep)
1719
+ );
1720
+ useEffect7(() => {
1721
+ if (runningStep) {
1722
+ setShouldRender(true);
1723
+ }
1724
+ }, [runningStep?.id]);
1725
+ useEffect7(() => {
1726
+ if (!shouldRender) return;
1727
+ if (runningStep) return;
1728
+ if (target.status !== "idle") return;
1729
+ const timeoutId = window.setTimeout(() => {
1730
+ setShouldRender(false);
1731
+ }, EXIT_BUFFER_MS);
1732
+ return () => {
1733
+ window.clearTimeout(timeoutId);
1734
+ };
1735
+ }, [runningStep, shouldRender, target.status]);
1736
+ const skipHiddenStep = useCallback2(() => {
1737
+ if (!runningState || runningState.status !== "running") return;
1738
+ if (!activeFlowId) return;
1739
+ const flow = flows.get(activeFlowId);
1740
+ if (!flow) return;
1741
+ const isLastStep = runningState.stepIndex >= flow.steps.length - 1 && flow.steps.length > 0;
1742
+ if (isLastStep) {
1743
+ complete();
1744
+ } else {
1745
+ next();
1746
+ }
1747
+ }, [activeFlowId, complete, flows, next, runningState]);
1748
+ const { target: hudTarget } = useHiddenTargetFallback({
1749
+ step: runningStep,
1750
+ target,
1751
+ viewportRect,
1752
+ onSkip: skipHiddenStep
1753
+ });
1754
+ const canRenderStep = Boolean(runningStep && runningState);
1755
+ const focusTrapActive = canRenderStep;
1756
+ const flowHudOptions = matchesFlowFilter && activeFlowId ? flows.get(activeFlowId)?.hud ?? null : null;
1757
+ const hudRenderMode = flowHudOptions?.render ?? "default";
1758
+ return {
1759
+ state,
1760
+ runningState,
1761
+ runningStep,
1762
+ shouldRender,
1763
+ canRenderStep,
1764
+ focusTrapActive,
1765
+ target,
1766
+ hudTarget,
1767
+ flowHudOptions,
1768
+ hudRenderMode,
1769
+ matchesFlowFilter,
1770
+ activeFlowId
1771
+ };
1772
+ };
1773
+
1774
+ // src/hooks/useHudDescription.ts
1775
+ import { useMemo as useMemo5 } from "react";
1776
+ var sanitizeForId = (value) => {
1777
+ const normalized = value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1778
+ return normalized.length > 0 ? normalized : "step";
1779
+ };
1780
+ var useHudDescription = (options) => {
1781
+ const { step, fallbackAriaDescribedBy } = options;
1782
+ const targetDescription = useMemo5(() => {
1783
+ if (!step) return null;
1784
+ if (typeof step.target !== "object") return null;
1785
+ const description = step.target.description;
1786
+ return typeof description === "string" ? description : null;
1787
+ }, [step]);
1788
+ const descriptionId = useMemo5(() => {
1789
+ if (!step || !targetDescription) return void 0;
1790
+ return `tour-step-${sanitizeForId(step.id)}-description`;
1791
+ }, [step, targetDescription]);
1792
+ const combinedAriaDescribedBy = useMemo5(() => {
1793
+ const parts = [fallbackAriaDescribedBy, descriptionId].filter(Boolean);
1794
+ return parts.length > 0 ? parts.join(" ") : void 0;
1795
+ }, [descriptionId, fallbackAriaDescribedBy]);
1796
+ return {
1797
+ targetDescription,
1798
+ descriptionId,
1799
+ combinedAriaDescribedBy
1800
+ };
1801
+ };
1802
+
1803
+ // src/hooks/useHudShortcuts.ts
1804
+ import { useEffect as useEffect8 } from "react";
1805
+
1806
+ // src/hooks/useTourControls.ts
1807
+ import { useCallback as useCallback3, useMemo as useMemo6 } from "react";
1808
+ var hasManualAdvance = (rules) => rules.some((rule) => rule.type === "manual");
1809
+ var didPreviousAdvanceViaRoute = (rules) => rules.some((rule) => rule.type === "route");
1810
+ var didPreviousAdvanceViaTargetEvent = (rules) => rules.some((rule) => rule.type === "event" && rule.on === "target");
1811
+ var useTourControls = () => {
1812
+ const tour = useTour();
1813
+ const {
1814
+ back,
1815
+ next,
1816
+ cancel,
1817
+ complete,
1818
+ state,
1819
+ activeFlowId,
1820
+ flows,
1821
+ activeStep
1822
+ } = tour;
1823
+ const computed = useMemo6(() => {
1824
+ if (!state || state.status !== "running" || !activeStep) {
1825
+ return {
1826
+ isActive: false,
1827
+ isFirst: true,
1828
+ isLast: true,
1829
+ showBackButton: false,
1830
+ backDisabled: true,
1831
+ showNextButton: false,
1832
+ nextDisabled: true
1833
+ };
1834
+ }
1835
+ const definition = activeFlowId ? flows.get(activeFlowId) : null;
1836
+ const totalSteps = definition?.steps.length ?? 0;
1837
+ const stepIndex = state.stepIndex;
1838
+ const isFirst2 = stepIndex <= 0;
1839
+ const isLast2 = totalSteps > 0 && stepIndex >= totalSteps - 1;
1840
+ const previousStep = !definition || stepIndex <= 0 ? null : definition.steps[stepIndex - 1];
1841
+ const advanceRules = activeStep.advance ?? [];
1842
+ const hasAdvanceRules = advanceRules.length > 0;
1843
+ const previousAdvanceRules = previousStep?.advance ?? [];
1844
+ const backControlState = activeStep.controls?.back ?? "auto";
1845
+ const nextControlState = activeStep.controls?.next ?? "auto";
1846
+ const showBackButton2 = backControlState !== "hidden" && !isFirst2 && !didPreviousAdvanceViaRoute(previousAdvanceRules) && !didPreviousAdvanceViaTargetEvent(previousAdvanceRules);
1847
+ const backDisabled2 = backControlState === "disabled";
1848
+ const manualAdvancePresent = hasManualAdvance(advanceRules);
1849
+ const showNextButton2 = nextControlState !== "hidden" && (isLast2 || !hasAdvanceRules || manualAdvancePresent);
1850
+ const nextDisabled2 = nextControlState === "disabled";
1851
+ return {
1852
+ isActive: true,
1853
+ isFirst: isFirst2,
1854
+ isLast: isLast2,
1855
+ showBackButton: showBackButton2,
1856
+ backDisabled: backDisabled2,
1857
+ showNextButton: showNextButton2,
1858
+ nextDisabled: nextDisabled2
1859
+ };
1860
+ }, [activeFlowId, activeStep, flows, state]);
1861
+ const {
1862
+ isActive,
1863
+ isFirst,
1864
+ isLast,
1865
+ showBackButton,
1866
+ backDisabled,
1867
+ showNextButton,
1868
+ nextDisabled
1869
+ } = computed;
1870
+ const canGoBack = showBackButton && !backDisabled;
1871
+ const canGoNext = showNextButton && !nextDisabled;
1872
+ const goBack = useCallback3(() => {
1873
+ if (!canGoBack) return;
1874
+ back();
1875
+ }, [back, canGoBack]);
1876
+ const goNext = useCallback3(() => {
1877
+ if (!canGoNext) return;
1878
+ if (isLast) {
1879
+ complete();
1880
+ } else {
1881
+ next();
1882
+ }
1883
+ }, [canGoNext, complete, isLast, next]);
1884
+ return useMemo6(
1885
+ () => ({
1886
+ showBackButton,
1887
+ backDisabled,
1888
+ canGoBack,
1889
+ showNextButton,
1890
+ nextDisabled,
1891
+ canGoNext,
1892
+ isFirst,
1893
+ isLast,
1894
+ isActive,
1895
+ goBack,
1896
+ goNext,
1897
+ cancel
1898
+ }),
1899
+ [
1900
+ backDisabled,
1901
+ canGoBack,
1902
+ canGoNext,
1903
+ cancel,
1904
+ goBack,
1905
+ goNext,
1906
+ isFirst,
1907
+ isLast,
1908
+ isActive,
1909
+ nextDisabled,
1910
+ showBackButton,
1911
+ showNextButton
1912
+ ]
1913
+ );
1914
+ };
1915
+
1916
+ // src/hooks/useHudShortcuts.ts
1917
+ var isInteractiveElement = (node) => {
1918
+ if (!node) return false;
1919
+ if (node.getAttribute("role") === "button") return true;
1920
+ if (node.hasAttribute("contenteditable")) return true;
1921
+ const interactiveSelector = 'button, a[href], input, textarea, select, summary, [role="button"], [data-tour-prevent-shortcut="true"]';
1922
+ return Boolean(node.closest(interactiveSelector));
1923
+ };
1924
+ var useHudShortcuts = (target, options) => {
1925
+ const enabled = options?.enabled ?? true;
1926
+ const escapeEnabled = options?.escape ?? true;
1927
+ const { state } = useTour();
1928
+ const { cancel, canGoBack, goBack, canGoNext, goNext, isActive } = useTourControls();
1929
+ useEffect8(() => {
1930
+ if (!isBrowser) return void 0;
1931
+ if (!enabled) return void 0;
1932
+ if (!target) return void 0;
1933
+ if (!state || state.status !== "running") return void 0;
1934
+ if (!isActive) return void 0;
1935
+ const handler = (event) => {
1936
+ if (event.defaultPrevented) return;
1937
+ if (event.key === "Escape" && escapeEnabled) {
1938
+ cancel("keyboard");
1939
+ event.preventDefault();
1940
+ return;
1941
+ }
1942
+ if (event.key === "ArrowLeft") {
1943
+ if (canGoBack) {
1944
+ goBack();
1945
+ event.preventDefault();
1946
+ }
1947
+ return;
1948
+ }
1949
+ if (event.key === "ArrowRight") {
1950
+ if (canGoNext) {
1951
+ goNext();
1952
+ event.preventDefault();
1953
+ }
1954
+ return;
1955
+ }
1956
+ if (event.key === "Enter" || event.key === " ") {
1957
+ if (target.status !== "ready") return;
1958
+ if (event.target instanceof Element) {
1959
+ if (target.element && target.element.contains(event.target)) {
1960
+ return;
1961
+ }
1962
+ if (event.target.closest("[data-tour-popover]")) {
1963
+ return;
1964
+ }
1965
+ if (isInteractiveElement(event.target)) {
1966
+ return;
1967
+ }
1968
+ }
1969
+ if (canGoNext) {
1970
+ goNext();
1971
+ event.preventDefault();
1972
+ }
1973
+ }
1974
+ };
1975
+ window.addEventListener("keydown", handler);
1976
+ return () => {
1977
+ window.removeEventListener("keydown", handler);
1978
+ };
1979
+ }, [
1980
+ canGoBack,
1981
+ canGoNext,
1982
+ cancel,
1983
+ enabled,
1984
+ goBack,
1985
+ goNext,
1986
+ isActive,
1987
+ state,
1988
+ target
1989
+ ]);
1990
+ };
1991
+
1992
+ // src/hooks/useTourHud.ts
1993
+ import { useMemo as useMemo8, useState as useState8 } from "react";
1994
+
1995
+ // src/hooks/useBodyScrollLock.ts
1996
+ import { useEffect as useEffect9 } from "react";
1997
+ var lockCount = 0;
1998
+ var previousOverflow = null;
1999
+ var acquireLock = () => {
2000
+ if (!isBrowser) return;
2001
+ if (lockCount === 0) {
2002
+ previousOverflow = document.body.style.overflow;
2003
+ document.body.style.overflow = "hidden";
2004
+ }
2005
+ lockCount += 1;
2006
+ };
2007
+ var releaseLock = () => {
2008
+ if (!isBrowser) return;
2009
+ if (lockCount === 0) return;
2010
+ lockCount -= 1;
2011
+ if (lockCount === 0) {
2012
+ document.body.style.overflow = previousOverflow ?? "";
2013
+ previousOverflow = null;
2014
+ }
2015
+ };
2016
+ var useBodyScrollLock = (enabled) => {
2017
+ useEffect9(() => {
2018
+ if (!enabled) return;
2019
+ acquireLock();
2020
+ return () => {
2021
+ releaseLock();
2022
+ };
2023
+ }, [enabled]);
2024
+ };
2025
+
2026
+ // src/hooks/useHudTargetIssue.ts
2027
+ import { useEffect as useEffect10, useMemo as useMemo7, useState as useState7 } from "react";
2028
+ var deriveTargetIssue = (target) => {
2029
+ if (target.isScreen) return null;
2030
+ if (target.status === "idle") return null;
2031
+ switch (target.visibility) {
2032
+ case "missing":
2033
+ return {
2034
+ type: "missing",
2035
+ title: "Looking for the target",
2036
+ body: "Flowsterix is still trying to find this element. Make sure the UI piece is mounted or adjust the selector.",
2037
+ hint: target.rectSource === "stored" ? "Showing the last known position until the element returns." : void 0
2038
+ };
2039
+ case "hidden":
2040
+ return {
2041
+ type: "hidden",
2042
+ title: "Target is hidden",
2043
+ body: "The element exists but is currently hidden, collapsed, or zero-sized. Expand it so the highlight can lock on."
2044
+ };
2045
+ case "detached":
2046
+ return {
2047
+ type: "detached",
2048
+ title: "Target left the page",
2049
+ body: "Navigate back to the screen that contains this element or reopen it before continuing the tour."
2050
+ };
2051
+ default:
2052
+ return null;
2053
+ }
2054
+ };
2055
+ var useHudTargetIssue = (target, options) => {
2056
+ const delayMs = Math.max(0, options?.delayMs ?? 500);
2057
+ const [armed, setArmed] = useState7(false);
2058
+ const rawIssue = useMemo7(
2059
+ () => deriveTargetIssue(target),
2060
+ [target.isScreen, target.rectSource, target.status, target.visibility]
2061
+ );
2062
+ useEffect10(() => {
2063
+ if (!rawIssue) {
2064
+ setArmed(false);
2065
+ return;
2066
+ }
2067
+ if (!isBrowser) {
2068
+ setArmed(true);
2069
+ return;
2070
+ }
2071
+ const timeoutId = globalThis.setTimeout(() => setArmed(true), delayMs);
2072
+ return () => {
2073
+ setArmed(false);
2074
+ globalThis.clearTimeout(timeoutId);
2075
+ };
2076
+ }, [delayMs, rawIssue?.type]);
2077
+ return {
2078
+ issue: armed ? rawIssue : null,
2079
+ rawIssue
2080
+ };
2081
+ };
2082
+
2083
+ // src/hooks/useTourHud.ts
2084
+ var DEFAULT_SHORTCUTS = true;
2085
+ var DEFAULT_BODY_SCROLL_LOCK = true;
2086
+ var useTourHud = (options = {}) => {
2087
+ const {
2088
+ overlayPadding,
2089
+ overlayRadius,
2090
+ bodyScrollLock = DEFAULT_BODY_SCROLL_LOCK
2091
+ } = options;
2092
+ const shortcuts = options.shortcuts ?? DEFAULT_SHORTCUTS;
2093
+ const { backdropInteraction, lockBodyScroll } = useTour();
2094
+ const hudState = useHudState();
2095
+ const disableDefaultHud = hudState.hudRenderMode === "none";
2096
+ const [popoverNode, setPopoverNode] = useState8(null);
2097
+ const popoverOptions = hudState.flowHudOptions?.popover;
2098
+ const description = useHudDescription({
2099
+ step: hudState.runningStep,
2100
+ fallbackAriaDescribedBy: popoverOptions?.ariaDescribedBy
2101
+ });
2102
+ const targetIssue = useHudTargetIssue(hudState.hudTarget);
2103
+ const shouldLockBodyScroll = Boolean(
2104
+ bodyScrollLock && (hudState.flowHudOptions?.behavior?.lockBodyScroll ?? lockBodyScroll) && hudState.focusTrapActive
2105
+ );
2106
+ useBodyScrollLock(shouldLockBodyScroll);
2107
+ const shortcutOptions = typeof shortcuts === "object" ? shortcuts : {};
2108
+ const shortcutsEnabled = Boolean(
2109
+ (typeof shortcuts === "boolean" ? shortcuts : shortcuts.enabled ?? true) && hudState.shouldRender
2110
+ );
2111
+ useHudShortcuts(shortcutsEnabled ? hudState.hudTarget : null, {
2112
+ ...shortcutOptions,
2113
+ enabled: shortcutsEnabled
2114
+ });
2115
+ const overlay = {
2116
+ padding: overlayPadding,
2117
+ radius: overlayRadius,
2118
+ interactionMode: hudState.flowHudOptions?.backdrop?.interaction ?? backdropInteraction
2119
+ };
2120
+ const popover = useMemo8(() => {
2121
+ return {
2122
+ offset: popoverOptions?.offset ?? 16,
2123
+ role: popoverOptions?.role ?? "dialog",
2124
+ ariaLabel: popoverOptions?.ariaLabel,
2125
+ ariaDescribedBy: popoverOptions?.ariaDescribedBy,
2126
+ ariaModal: popoverOptions?.ariaModal ?? false,
2127
+ width: popoverOptions?.width,
2128
+ maxWidth: popoverOptions?.maxWidth,
2129
+ placement: hudState.runningStep?.placement
2130
+ };
2131
+ }, [hudState.runningStep?.placement, popoverOptions]);
2132
+ const descriptionResult = useMemo8(() => {
2133
+ return {
2134
+ ...description,
2135
+ text: description.targetDescription
2136
+ };
2137
+ }, [description]);
2138
+ const focusManager = useMemo8(
2139
+ () => ({
2140
+ active: hudState.focusTrapActive,
2141
+ target: hudState.hudTarget,
2142
+ popoverNode,
2143
+ setPopoverNode
2144
+ }),
2145
+ [hudState.focusTrapActive, hudState.hudTarget, popoverNode, setPopoverNode]
2146
+ );
2147
+ return {
2148
+ hudState,
2149
+ disableDefaultHud,
2150
+ overlay,
2151
+ popover,
2152
+ description: descriptionResult,
2153
+ focusManager,
2154
+ targetIssue,
2155
+ shouldLockBodyScroll,
2156
+ shortcutsEnabled
2157
+ };
2158
+ };
2159
+
2160
+ // src/hooks/useTourOverlay.ts
2161
+ import { useEffect as useEffect11, useMemo as useMemo9, useRef as useRef5 } from "react";
2162
+ var DEFAULT_PADDING = 12;
2163
+ var DEFAULT_RADIUS = 12;
2164
+ var DEFAULT_EDGE_BUFFER = 0;
2165
+ var useTourOverlay = (options) => {
2166
+ const {
2167
+ target,
2168
+ padding = DEFAULT_PADDING,
2169
+ radius = DEFAULT_RADIUS,
2170
+ edgeBuffer = DEFAULT_EDGE_BUFFER,
2171
+ interactionMode = "passthrough"
2172
+ } = options;
2173
+ const hasShownRef = useRef5(false);
2174
+ const lastReadyTargetRef = useRef5(null);
2175
+ useEffect11(() => {
2176
+ if (!isBrowser) return;
2177
+ if (target.status === "ready") {
2178
+ hasShownRef.current = true;
2179
+ lastReadyTargetRef.current = {
2180
+ ...target,
2181
+ rect: target.rect ? { ...target.rect } : null
2182
+ };
2183
+ return;
2184
+ }
2185
+ if (target.status === "idle") {
2186
+ hasShownRef.current = false;
2187
+ lastReadyTargetRef.current = null;
2188
+ }
2189
+ }, [target]);
2190
+ const viewport = getViewportRect();
2191
+ const cachedTarget = lastReadyTargetRef.current;
2192
+ const highlightTarget = target.status === "ready" ? target : cachedTarget;
2193
+ const resolvedRect = highlightTarget?.rect ?? target.rect;
2194
+ const resolvedIsScreen = highlightTarget?.isScreen ?? target.isScreen;
2195
+ const expandedRect = resolvedIsScreen || !resolvedRect ? viewport : expandRect(resolvedRect, padding);
2196
+ const safeBuffer = Math.max(0, edgeBuffer);
2197
+ const insetTop = expandedRect.top <= 0 ? Math.min(safeBuffer, Math.max(0, expandedRect.height) / 2) : 0;
2198
+ const insetLeft = expandedRect.left <= 0 ? Math.min(safeBuffer, Math.max(0, expandedRect.width) / 2) : 0;
2199
+ const insetBottom = expandedRect.top + expandedRect.height >= viewport.height ? Math.min(safeBuffer, Math.max(0, expandedRect.height) / 2) : 0;
2200
+ const insetRight = expandedRect.left + expandedRect.width >= viewport.width ? Math.min(safeBuffer, Math.max(0, expandedRect.width) / 2) : 0;
2201
+ const highlightTop = expandedRect.top + insetTop;
2202
+ const highlightLeft = expandedRect.left + insetLeft;
2203
+ const highlightWidth = Math.max(
2204
+ 0,
2205
+ expandedRect.width - insetLeft - insetRight
2206
+ );
2207
+ const highlightHeight = Math.max(
2208
+ 0,
2209
+ expandedRect.height - insetTop - insetBottom
2210
+ );
2211
+ const highlightRadius = Math.max(
2212
+ 0,
2213
+ Math.min(radius, highlightWidth / 2, highlightHeight / 2)
2214
+ );
2215
+ const highlightCenterX = highlightLeft + highlightWidth / 2;
2216
+ const highlightCenterY = highlightTop + highlightHeight / 2;
2217
+ const hasHighlightBounds = !!highlightTarget && !resolvedIsScreen && highlightWidth > 0 && highlightHeight > 0;
2218
+ const highlightRect = hasHighlightBounds ? {
2219
+ top: highlightTop,
2220
+ left: highlightLeft,
2221
+ width: highlightWidth,
2222
+ height: highlightHeight,
2223
+ radius: highlightRadius
2224
+ } : null;
2225
+ const maskCapable = useMemo9(() => supportsMasking(), []);
2226
+ const isActive = target.status === "ready" || target.status === "resolving" && cachedTarget !== null;
2227
+ const shouldMask = maskCapable && isActive;
2228
+ const maskId = useMemo9(
2229
+ () => `tour-overlay-mask-${Math.random().toString(36).slice(2, 10)}`,
2230
+ []
2231
+ );
2232
+ const maskUrl = shouldMask ? `url(#${maskId})` : void 0;
2233
+ const fallbackSegments = useMemo9(() => {
2234
+ if (!isActive || shouldMask || !hasHighlightBounds || !highlightRect) {
2235
+ return null;
2236
+ }
2237
+ const topEdge = Math.max(0, Math.min(highlightRect.top, viewport.height));
2238
+ const bottomEdge = Math.max(
2239
+ topEdge,
2240
+ Math.min(highlightRect.top + highlightRect.height, viewport.height)
2241
+ );
2242
+ const leftEdge = Math.max(0, Math.min(highlightRect.left, viewport.width));
2243
+ const rightEdge = Math.max(
2244
+ leftEdge,
2245
+ Math.min(highlightRect.left + highlightRect.width, viewport.width)
2246
+ );
2247
+ const middleHeight = Math.max(0, bottomEdge - topEdge);
2248
+ return [
2249
+ {
2250
+ key: "top",
2251
+ top: 0,
2252
+ left: 0,
2253
+ width: viewport.width,
2254
+ height: topEdge
2255
+ },
2256
+ {
2257
+ key: "bottom",
2258
+ top: bottomEdge,
2259
+ left: 0,
2260
+ width: viewport.width,
2261
+ height: Math.max(0, viewport.height - bottomEdge)
2262
+ },
2263
+ {
2264
+ key: "left",
2265
+ top: topEdge,
2266
+ left: 0,
2267
+ width: leftEdge,
2268
+ height: middleHeight
2269
+ },
2270
+ {
2271
+ key: "right",
2272
+ top: topEdge,
2273
+ left: rightEdge,
2274
+ width: Math.max(0, viewport.width - rightEdge),
2275
+ height: middleHeight
2276
+ }
2277
+ ].filter((segment) => segment.width > 0 && segment.height > 0);
2278
+ }, [
2279
+ hasHighlightBounds,
2280
+ highlightRect,
2281
+ isActive,
2282
+ shouldMask,
2283
+ viewport.height,
2284
+ viewport.width
2285
+ ]);
2286
+ const blockerSegments = useMemo9(() => {
2287
+ if (interactionMode !== "block") {
2288
+ return null;
2289
+ }
2290
+ if (!hasHighlightBounds || !highlightRect) {
2291
+ return [
2292
+ {
2293
+ key: "blocker-full",
2294
+ top: 0,
2295
+ left: 0,
2296
+ width: viewport.width,
2297
+ height: viewport.height
2298
+ }
2299
+ ];
2300
+ }
2301
+ const topEdge = Math.max(0, Math.min(highlightRect.top, viewport.height));
2302
+ const bottomEdge = Math.max(
2303
+ topEdge,
2304
+ Math.min(highlightRect.top + highlightRect.height, viewport.height)
2305
+ );
2306
+ const leftEdge = Math.max(0, Math.min(highlightRect.left, viewport.width));
2307
+ const rightEdge = Math.max(
2308
+ leftEdge,
2309
+ Math.min(highlightRect.left + highlightRect.width, viewport.width)
2310
+ );
2311
+ const middleHeight = Math.max(0, bottomEdge - topEdge);
2312
+ return [
2313
+ {
2314
+ key: "blocker-top",
2315
+ top: 0,
2316
+ left: 0,
2317
+ width: viewport.width,
2318
+ height: topEdge
2319
+ },
2320
+ {
2321
+ key: "blocker-bottom",
2322
+ top: bottomEdge,
2323
+ left: 0,
2324
+ width: viewport.width,
2325
+ height: Math.max(0, viewport.height - bottomEdge)
2326
+ },
2327
+ {
2328
+ key: "blocker-left",
2329
+ top: topEdge,
2330
+ left: 0,
2331
+ width: leftEdge,
2332
+ height: middleHeight
2333
+ },
2334
+ {
2335
+ key: "blocker-right",
2336
+ top: topEdge,
2337
+ left: rightEdge,
2338
+ width: Math.max(0, viewport.width - rightEdge),
2339
+ height: middleHeight
2340
+ }
2341
+ ].filter((segment) => segment.width > 0 && segment.height > 0);
2342
+ }, [
2343
+ hasHighlightBounds,
2344
+ highlightRect,
2345
+ interactionMode,
2346
+ viewport.height,
2347
+ viewport.width
2348
+ ]);
2349
+ const showBaseOverlay = isActive && (shouldMask || !hasHighlightBounds);
2350
+ return {
2351
+ isActive,
2352
+ highlight: {
2353
+ rect: highlightRect,
2354
+ centerX: highlightCenterX,
2355
+ centerY: highlightCenterY,
2356
+ target: highlightTarget ?? null,
2357
+ isScreen: resolvedIsScreen
2358
+ },
2359
+ shouldMask,
2360
+ maskId: shouldMask ? maskId : null,
2361
+ maskUrl,
2362
+ fallbackSegments,
2363
+ blockerSegments,
2364
+ showBaseOverlay,
2365
+ viewport
2366
+ };
2367
+ };
2368
+
2369
+ // src/hooks/useTourFocusDominance.ts
2370
+ var DEFAULT_ENABLED = true;
2371
+ var useTourFocusDominance = (options = {}) => {
2372
+ const { enabled = DEFAULT_ENABLED } = options;
2373
+ const { state } = useTour();
2374
+ const isRunning = state?.status === "running";
2375
+ const active = Boolean(enabled && isRunning);
2376
+ return {
2377
+ active,
2378
+ suspendExternalFocusTrap: active
2379
+ };
2380
+ };
2381
+
2382
+ // src/adapters/radixDialog.ts
2383
+ var useRadixDialogAdapter = (options = {}) => {
2384
+ const { suspendExternalFocusTrap } = useTourFocusDominance(options);
2385
+ const { disableEscapeClose = false } = options;
2386
+ const preventDismiss = (event) => {
2387
+ if (suspendExternalFocusTrap) {
2388
+ event.preventDefault();
2389
+ }
2390
+ };
2391
+ const preventEscape = (event) => {
2392
+ if (suspendExternalFocusTrap && disableEscapeClose) {
2393
+ event.preventDefault();
2394
+ }
2395
+ };
2396
+ return {
2397
+ dialogProps: {
2398
+ modal: !suspendExternalFocusTrap
2399
+ },
2400
+ contentProps: {
2401
+ trapFocus: !suspendExternalFocusTrap,
2402
+ onInteractOutside: preventDismiss,
2403
+ onFocusOutside: preventDismiss,
2404
+ onEscapeKeyDown: preventEscape
2405
+ },
2406
+ suspendExternalFocusTrap
2407
+ };
2408
+ };
2409
+
2410
+ // src/hooks/useDelayAdvance.ts
2411
+ import { useEffect as useEffect12, useMemo as useMemo10, useState as useState9 } from "react";
2412
+ var getTimestamp = () => typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
2413
+ var useDelayAdvance = () => {
2414
+ const { delayInfo, activeStep, state } = useTour();
2415
+ const [now, setNow] = useState9(() => getTimestamp());
2416
+ useEffect12(() => {
2417
+ if (!delayInfo) return;
2418
+ if (!activeStep || activeStep.id !== delayInfo.stepId) return;
2419
+ if (!state || state.status !== "running") return;
2420
+ if (!isBrowser) return;
2421
+ let frameId = null;
2422
+ const tick = () => {
2423
+ setNow(getTimestamp());
2424
+ frameId = window.requestAnimationFrame(tick);
2425
+ };
2426
+ frameId = window.requestAnimationFrame(tick);
2427
+ return () => {
2428
+ if (frameId !== null) {
2429
+ window.cancelAnimationFrame(frameId);
2430
+ }
2431
+ };
2432
+ }, [delayInfo, activeStep, state]);
2433
+ useEffect12(() => {
2434
+ if (!delayInfo) {
2435
+ setNow(getTimestamp());
2436
+ }
2437
+ }, [delayInfo]);
2438
+ return useMemo10(() => {
2439
+ const matchingStep = !!delayInfo && !!activeStep && activeStep.id === delayInfo.stepId;
2440
+ const isRunning = matchingStep && state?.status === "running";
2441
+ if (!delayInfo) {
2442
+ return {
2443
+ isActive: false,
2444
+ flowId: null,
2445
+ stepId: null,
2446
+ totalMs: 0,
2447
+ remainingMs: 0,
2448
+ elapsedMs: 0,
2449
+ fractionElapsed: 0,
2450
+ fractionRemaining: 1,
2451
+ startedAt: null,
2452
+ endsAt: null
2453
+ };
2454
+ }
2455
+ if (!isRunning) {
2456
+ return {
2457
+ isActive: false,
2458
+ flowId: delayInfo.flowId,
2459
+ stepId: delayInfo.stepId,
2460
+ totalMs: delayInfo.totalMs,
2461
+ remainingMs: delayInfo.totalMs,
2462
+ elapsedMs: 0,
2463
+ fractionElapsed: 0,
2464
+ fractionRemaining: 1,
2465
+ startedAt: delayInfo.startedAt,
2466
+ endsAt: delayInfo.endsAt
2467
+ };
2468
+ }
2469
+ const clampedNow = Math.min(now, delayInfo.endsAt);
2470
+ const elapsedMs = Math.max(0, clampedNow - delayInfo.startedAt);
2471
+ const remainingMs = Math.max(0, delayInfo.endsAt - now);
2472
+ const totalMs = delayInfo.totalMs;
2473
+ const fractionElapsed = totalMs > 0 ? Math.min(1, Math.max(0, elapsedMs / totalMs)) : 1;
2474
+ const fractionRemaining = 1 - fractionElapsed;
2475
+ return {
2476
+ isActive: true,
2477
+ flowId: delayInfo.flowId,
2478
+ stepId: delayInfo.stepId,
2479
+ totalMs,
2480
+ remainingMs,
2481
+ elapsedMs,
2482
+ fractionElapsed,
2483
+ fractionRemaining,
2484
+ startedAt: delayInfo.startedAt,
2485
+ endsAt: delayInfo.endsAt
2486
+ };
2487
+ }, [activeStep, delayInfo, now, state]);
2488
+ };
2489
+
2490
+ // src/components/OverlayBackdrop.tsx
2491
+ import { useEffect as useEffect13, useRef as useRef6 } from "react";
2492
+ import { createPortal } from "react-dom";
2493
+ import { AnimatePresence } from "motion/react";
2494
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
2495
+ var styles = {
2496
+ root: {
2497
+ position: "fixed",
2498
+ inset: 0,
2499
+ pointerEvents: "none"
2500
+ },
2501
+ svgMask: {
2502
+ position: "absolute"
2503
+ },
2504
+ overlay: {
2505
+ position: "absolute",
2506
+ transformOrigin: "center",
2507
+ inset: 0,
2508
+ pointerEvents: "none"
2509
+ },
2510
+ segment: {
2511
+ position: "absolute",
2512
+ transformOrigin: "center",
2513
+ pointerEvents: "none"
2514
+ },
2515
+ blockerContainer: {
2516
+ position: "absolute",
2517
+ inset: 0,
2518
+ pointerEvents: "none"
2519
+ },
2520
+ blockerSegment: {
2521
+ position: "absolute",
2522
+ pointerEvents: "auto"
2523
+ },
2524
+ highlightRing: {
2525
+ position: "absolute",
2526
+ transformOrigin: "center",
2527
+ pointerEvents: "none"
2528
+ }
2529
+ };
2530
+ var DEFAULT_HIGHLIGHT_TRANSITION = {
2531
+ duration: 0.35,
2532
+ ease: "easeOut",
2533
+ type: "spring",
2534
+ damping: 25,
2535
+ stiffness: 300,
2536
+ mass: 0.7
2537
+ };
2538
+ var DEFAULT_HIGHLIGHT_COLLAPSE_TRANSITION = {
2539
+ duration: 0.18,
2540
+ ease: "easeOut",
2541
+ type: "tween"
2542
+ };
2543
+ var DEFAULT_OVERLAY_TRANSITION = {
2544
+ duration: 0.35,
2545
+ ease: "easeOut"
2546
+ };
2547
+ var OverlayBackdrop = ({
2548
+ overlay,
2549
+ zIndex = 1e3,
2550
+ color,
2551
+ colorClassName: _colorClassName,
2552
+ opacity = 1,
2553
+ shadow,
2554
+ blurAmount,
2555
+ ariaHidden,
2556
+ rootClassName,
2557
+ overlayClassName,
2558
+ segmentClassName,
2559
+ ringClassName,
2560
+ showHighlightRing = true,
2561
+ showInteractionBlocker = true,
2562
+ transitionsOverride
2563
+ }) => {
2564
+ if (!isBrowser) return null;
2565
+ const host = portalHost();
2566
+ if (!host) return null;
2567
+ const adapter = useAnimationAdapter();
2568
+ const {
2569
+ highlight,
2570
+ shouldMask,
2571
+ maskId,
2572
+ maskUrl,
2573
+ fallbackSegments,
2574
+ blockerSegments,
2575
+ showBaseOverlay,
2576
+ isActive,
2577
+ viewport
2578
+ } = overlay;
2579
+ const hasHighlightBounds = Boolean(highlight.rect);
2580
+ const prevScreenTargetRef = useRef6(null);
2581
+ const shouldSnapHighlight = prevScreenTargetRef.current === true && !highlight.isScreen && hasHighlightBounds;
2582
+ useEffect13(() => {
2583
+ prevScreenTargetRef.current = highlight.isScreen;
2584
+ }, [highlight.isScreen]);
2585
+ const resolvedBlur = typeof blurAmount === "number" ? `${blurAmount}px` : "0px";
2586
+ const defaultInsetShadow = "inset 0 0 0 2px rgba(56,189,248,0.4), inset 0 0 0 8px rgba(15,23,42,0.3)";
2587
+ const highlightAppearance = shadow ? { boxShadow: shadow } : { boxShadow: defaultInsetShadow };
2588
+ const { MotionDiv, MotionSvg, MotionDefs, MotionMask, MotionRect } = adapter.components;
2589
+ const highlightTransition = transitionsOverride?.overlayHighlight ?? adapter.transitions.overlayHighlight ?? DEFAULT_HIGHLIGHT_TRANSITION;
2590
+ const snapTransition = { type: "tween", duration: 0 };
2591
+ const resolvedHighlightTransition = shouldSnapHighlight ? snapTransition : highlightTransition;
2592
+ const overlayTransition = transitionsOverride?.overlayFade ?? adapter.transitions.overlayFade ?? DEFAULT_OVERLAY_TRANSITION;
2593
+ const highlightCollapseTransition = highlight.isScreen ? snapTransition : transitionsOverride?.overlayHighlightCollapse ?? DEFAULT_HIGHLIGHT_COLLAPSE_TRANSITION;
2594
+ const highlightRectTransition = hasHighlightBounds ? resolvedHighlightTransition : highlightCollapseTransition;
2595
+ const highlightRectAnimation = shouldMask ? {
2596
+ x: highlight.rect?.left ?? highlight.centerX,
2597
+ y: highlight.rect?.top ?? highlight.centerY,
2598
+ width: highlight.rect?.width ?? 0,
2599
+ height: highlight.rect?.height ?? 0,
2600
+ rx: highlight.rect?.radius ?? 0,
2601
+ ry: highlight.rect?.radius ?? 0
2602
+ } : {
2603
+ x: highlight.centerX,
2604
+ y: highlight.centerY,
2605
+ width: 0,
2606
+ height: 0,
2607
+ rx: 0,
2608
+ ry: 0
2609
+ };
2610
+ const highlightRingAnimation = hasHighlightBounds ? {
2611
+ top: highlight.centerY,
2612
+ left: highlight.centerX,
2613
+ width: highlight.rect?.width ?? 0,
2614
+ height: highlight.rect?.height ?? 0,
2615
+ borderRadius: highlight.rect?.radius ?? 0,
2616
+ opacity: 1,
2617
+ transform: "translate(-50%, -50%)"
2618
+ } : {
2619
+ top: highlight.centerY,
2620
+ left: highlight.centerX,
2621
+ width: 0,
2622
+ height: 0,
2623
+ borderRadius: 0,
2624
+ opacity: 0,
2625
+ transform: "translate(-50%, -50%)"
2626
+ };
2627
+ const overlayStyle = {};
2628
+ if (shouldMask) {
2629
+ overlayStyle.maskRepeat = "no-repeat";
2630
+ overlayStyle.WebkitMaskRepeat = "no-repeat";
2631
+ overlayStyle.maskSize = "100% 100%";
2632
+ overlayStyle.WebkitMaskSize = "100% 100%";
2633
+ }
2634
+ if (maskUrl) {
2635
+ overlayStyle.mask = maskUrl;
2636
+ overlayStyle.WebkitMask = maskUrl;
2637
+ }
2638
+ if (color) {
2639
+ overlayStyle.backgroundColor = color;
2640
+ }
2641
+ return createPortal(
2642
+ /* @__PURE__ */ jsxs(
2643
+ MotionDiv,
2644
+ {
2645
+ className: rootClassName,
2646
+ style: { ...styles.root, zIndex },
2647
+ "aria-hidden": ariaHidden,
2648
+ "data-tour-overlay": "",
2649
+ children: [
2650
+ /* @__PURE__ */ jsx3(AnimatePresence, { mode: "popLayout", children: shouldMask ? /* @__PURE__ */ jsx3(
2651
+ MotionSvg,
2652
+ {
2653
+ width: "0",
2654
+ height: "0",
2655
+ "aria-hidden": true,
2656
+ focusable: "false",
2657
+ style: styles.svgMask,
2658
+ initial: { opacity: 0 },
2659
+ animate: { opacity: 1 },
2660
+ exit: { opacity: 0 },
2661
+ transition: overlayTransition,
2662
+ children: /* @__PURE__ */ jsx3(MotionDefs, { children: /* @__PURE__ */ jsxs(
2663
+ MotionMask,
2664
+ {
2665
+ id: maskId ?? void 0,
2666
+ initial: false,
2667
+ maskUnits: "userSpaceOnUse",
2668
+ maskContentUnits: "userSpaceOnUse",
2669
+ x: "0",
2670
+ y: "0",
2671
+ animate: { width: viewport.width, height: viewport.height },
2672
+ transition: highlightTransition,
2673
+ children: [
2674
+ /* @__PURE__ */ jsx3(
2675
+ MotionRect,
2676
+ {
2677
+ x: "0",
2678
+ y: "0",
2679
+ initial: false,
2680
+ animate: {
2681
+ width: viewport.width,
2682
+ height: viewport.height,
2683
+ opacity: 1
2684
+ },
2685
+ fill: "white",
2686
+ transition: highlightTransition,
2687
+ exit: { opacity: 0 }
2688
+ }
2689
+ ),
2690
+ /* @__PURE__ */ jsx3(
2691
+ MotionRect,
2692
+ {
2693
+ initial: false,
2694
+ animate: highlightRectAnimation,
2695
+ exit: {
2696
+ x: highlight.centerX,
2697
+ y: highlight.centerY
2698
+ },
2699
+ transition: highlightRectTransition,
2700
+ fill: "black"
2701
+ }
2702
+ )
2703
+ ]
2704
+ }
2705
+ ) })
2706
+ },
2707
+ "tour-mask"
2708
+ ) : null }),
2709
+ /* @__PURE__ */ jsx3(AnimatePresence, { mode: "popLayout", children: showBaseOverlay ? /* @__PURE__ */ jsx3(
2710
+ MotionDiv,
2711
+ {
2712
+ className: overlayClassName,
2713
+ "data-tour-overlay-layer": "backdrop",
2714
+ style: {
2715
+ ...styles.overlay,
2716
+ ...overlayStyle,
2717
+ zIndex,
2718
+ backgroundColor: color ?? void 0
2719
+ },
2720
+ initial: {
2721
+ opacity: 0,
2722
+ transition: overlayTransition,
2723
+ backdropFilter: `blur(0px)`
2724
+ },
2725
+ animate: {
2726
+ opacity,
2727
+ backdropFilter: `blur(${resolvedBlur})`
2728
+ },
2729
+ exit: {
2730
+ opacity: 0,
2731
+ backdropFilter: `blur(${resolvedBlur})`
2732
+ },
2733
+ transition: overlayTransition
2734
+ },
2735
+ "tour-overlay"
2736
+ ) : null }),
2737
+ /* @__PURE__ */ jsx3(AnimatePresence, { mode: "popLayout", children: fallbackSegments ? fallbackSegments.map((segment) => /* @__PURE__ */ jsx3(
2738
+ MotionDiv,
2739
+ {
2740
+ className: segmentClassName,
2741
+ "data-tour-overlay-layer": "segment",
2742
+ style: {
2743
+ ...styles.segment,
2744
+ zIndex,
2745
+ top: segment.top,
2746
+ left: segment.left,
2747
+ width: segment.width,
2748
+ height: segment.height,
2749
+ backgroundColor: color ?? void 0
2750
+ },
2751
+ initial: {
2752
+ opacity: 0,
2753
+ backdropFilter: `blur(0px)`
2754
+ },
2755
+ animate: {
2756
+ opacity,
2757
+ backdropFilter: `blur(${resolvedBlur})`
2758
+ },
2759
+ exit: {
2760
+ opacity: 0,
2761
+ backdropFilter: `blur(0px)`
2762
+ },
2763
+ transition: overlayTransition
2764
+ },
2765
+ `tour-overlay-fallback-${segment.key}`
2766
+ )) : null }),
2767
+ showInteractionBlocker && blockerSegments ? /* @__PURE__ */ jsx3(
2768
+ "div",
2769
+ {
2770
+ style: { ...styles.blockerContainer, zIndex },
2771
+ "data-tour-overlay-layer": "interaction-blocker",
2772
+ "aria-hidden": true,
2773
+ children: blockerSegments.map((segment) => /* @__PURE__ */ jsx3(
2774
+ "div",
2775
+ {
2776
+ style: {
2777
+ ...styles.blockerSegment,
2778
+ top: segment.top,
2779
+ left: segment.left,
2780
+ width: segment.width,
2781
+ height: segment.height
2782
+ }
2783
+ },
2784
+ segment.key
2785
+ ))
2786
+ }
2787
+ ) : null,
2788
+ /* @__PURE__ */ jsx3(AnimatePresence, { mode: "popLayout", children: showHighlightRing && isActive && hasHighlightBounds ? /* @__PURE__ */ jsx3(
2789
+ MotionDiv,
2790
+ {
2791
+ className: ringClassName,
2792
+ style: {
2793
+ ...styles.highlightRing,
2794
+ zIndex: zIndex + 1,
2795
+ ...highlightAppearance
2796
+ },
2797
+ "data-tour-overlay-layer": "highlight-ring",
2798
+ initial: false,
2799
+ animate: highlightRingAnimation,
2800
+ exit: {
2801
+ opacity: 0,
2802
+ transition: {
2803
+ duration: 0.35,
2804
+ ease: "easeOut"
2805
+ }
2806
+ },
2807
+ transition: resolvedHighlightTransition
2808
+ },
2809
+ "tour-ring"
2810
+ ) : null })
2811
+ ]
2812
+ }
2813
+ ),
2814
+ host
2815
+ );
2816
+ };
2817
+
2818
+ // src/components/TourPopoverPortal.tsx
2819
+ import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect2, useMemo as useMemo11, useRef as useRef7, useState as useState10 } from "react";
2820
+ import { createPortal as createPortal2 } from "react-dom";
2821
+ import {
2822
+ autoPlacement,
2823
+ computePosition,
2824
+ flip,
2825
+ offset as floatingOffset,
2826
+ shift
2827
+ } from "@floating-ui/dom";
2828
+ var FLOATING_OFFSET = 8;
2829
+ var DOCKED_MARGIN = 24;
2830
+ var MOBILE_BREAKPOINT = 640;
2831
+ var MOBILE_HEIGHT_BREAKPOINT = 560;
2832
+ var MOBILE_HORIZONTAL_GUTTER = 12;
2833
+ var DEFAULT_POPOVER_ENTRANCE_TRANSITION = {
2834
+ duration: 0.25,
2835
+ ease: "easeOut"
2836
+ };
2837
+ var DEFAULT_POPOVER_EXIT_TRANSITION = {
2838
+ duration: 0.2,
2839
+ ease: "easeOut"
2840
+ };
2841
+ var DEFAULT_POPOVER_CONTENT_TRANSITION = {
2842
+ duration: 0.4,
2843
+ ease: "easeOut"
2844
+ };
2845
+ var floatingPositionCache = /* @__PURE__ */ new Map();
2846
+ var getFloatingCacheKey = (target) => {
2847
+ if (target.stepId) {
2848
+ return `step:${target.stepId}`;
2849
+ }
2850
+ if (target.isScreen) {
2851
+ return "screen";
2852
+ }
2853
+ return null;
2854
+ };
2855
+ var TourPopoverPortal = ({
2856
+ target,
2857
+ children,
2858
+ offset = 16,
2859
+ width,
2860
+ maxWidth,
2861
+ zIndex = 1001,
2862
+ placement,
2863
+ role,
2864
+ ariaLabel,
2865
+ ariaDescribedBy,
2866
+ ariaModal,
2867
+ descriptionId,
2868
+ descriptionText,
2869
+ onContainerChange,
2870
+ layoutId,
2871
+ containerComponent,
2872
+ contentComponent,
2873
+ transitionsOverride
2874
+ }) => {
2875
+ if (!isBrowser) return null;
2876
+ const host = portalHost();
2877
+ if (!host) return null;
2878
+ const adapter = useAnimationAdapter();
2879
+ const Container = containerComponent ?? adapter.components.MotionDiv;
2880
+ const Content = contentComponent ?? adapter.components.MotionDiv;
2881
+ const popoverEntranceTransition = transitionsOverride?.popoverEntrance ?? adapter.transitions.popoverEntrance ?? DEFAULT_POPOVER_ENTRANCE_TRANSITION;
2882
+ const popoverExitTransition = transitionsOverride?.popoverExit ?? adapter.transitions.popoverExit ?? DEFAULT_POPOVER_EXIT_TRANSITION;
2883
+ const popoverContentTransition = transitionsOverride?.popoverContent ?? adapter.transitions.popoverContent ?? DEFAULT_POPOVER_CONTENT_TRANSITION;
2884
+ const viewport = useViewportRect();
2885
+ const prefersMobileLayout = viewport.width <= MOBILE_BREAKPOINT || viewport.height <= MOBILE_HEIGHT_BREAKPOINT;
2886
+ const prefersMobileRef = useRef7(prefersMobileLayout);
2887
+ useEffect14(() => {
2888
+ prefersMobileRef.current = prefersMobileLayout;
2889
+ }, [prefersMobileLayout]);
2890
+ const lastReadyTargetRef = useRef7(null);
2891
+ useEffect14(() => {
2892
+ if (target.status === "ready" && target.rect) {
2893
+ lastReadyTargetRef.current = {
2894
+ rect: { ...target.rect },
2895
+ isScreen: target.isScreen
2896
+ };
2897
+ } else if (target.status === "idle") {
2898
+ lastReadyTargetRef.current = null;
2899
+ }
2900
+ }, [target.isScreen, target.rect, target.status]);
2901
+ const cachedTarget = lastReadyTargetRef.current;
2902
+ const resolvedRect = target.rect ?? target.lastResolvedRect ?? cachedTarget?.rect ?? null;
2903
+ const resolvedIsScreen = target.status === "ready" ? target.isScreen : cachedTarget?.isScreen ?? target.isScreen;
2904
+ const fallbackRect = resolvedRect ?? viewport;
2905
+ const fallbackIsScreen = resolvedIsScreen;
2906
+ const [floatingSize, setFloatingSize] = useState10(null);
2907
+ const clampVertical = (value) => Math.min(viewport.height - 24, Math.max(24, value));
2908
+ const clampHorizontal = (value) => Math.min(viewport.width - 24, Math.max(24, value));
2909
+ const screenCenteredTop = viewport.height / 2 - (floatingSize?.height ?? 0) / 2;
2910
+ const screenCenteredLeft = viewport.width / 2 - (floatingSize?.width ?? 0) / 2;
2911
+ const floatingWidth = floatingSize?.width ?? 0;
2912
+ const baseTop = fallbackIsScreen ? screenCenteredTop : fallbackRect.top + fallbackRect.height + offset;
2913
+ const top = fallbackIsScreen ? clampVertical(screenCenteredTop) : clampVertical(baseTop);
2914
+ const leftBase = fallbackIsScreen ? screenCenteredLeft : fallbackRect.left + fallbackRect.width / 2 - floatingWidth / 2;
2915
+ const left = clampHorizontal(leftBase);
2916
+ const fallbackTransform = "translate3d(0px, 0px, 0px)";
2917
+ const fallbackPosition = useMemo11(
2918
+ () => ({
2919
+ top,
2920
+ left,
2921
+ transform: fallbackTransform
2922
+ }),
2923
+ [fallbackTransform, left, top]
2924
+ );
2925
+ const centerInitialPosition = useMemo11(
2926
+ () => ({
2927
+ top: viewport.height / 2,
2928
+ left: viewport.width / 2,
2929
+ transform: "translate3d(-50%, -50%, 0px)"
2930
+ }),
2931
+ [viewport.height, viewport.width]
2932
+ );
2933
+ const floatingRef = useRef7(null);
2934
+ const cachedFloatingPositionRef = useRef7(null);
2935
+ const appliedFloatingCacheRef = useRef7(null);
2936
+ const deferredScreenSnapRef = useRef7(null);
2937
+ const [layoutMode, setLayoutMode] = useState10(
2938
+ () => prefersMobileLayout ? "mobile" : "floating"
2939
+ );
2940
+ const [floatingPosition, setFloatingPosition] = useState10(fallbackPosition);
2941
+ const [dragPosition, setDragPosition] = useState10(null);
2942
+ const [isDragging, setIsDragging] = useState10(false);
2943
+ const dragStateRef = useRef7(null);
2944
+ const overflowRetryRef = useRef7({
2945
+ stepId: null,
2946
+ attempts: 0
2947
+ });
2948
+ const overflowRetryTimeoutRef = useRef7(null);
2949
+ useLayoutEffect2(() => {
2950
+ if (!isBrowser) return;
2951
+ const node = floatingRef.current;
2952
+ if (!node) return;
2953
+ const updateSize = () => {
2954
+ const rect = node.getBoundingClientRect();
2955
+ setFloatingSize({ width: rect.width, height: rect.height });
2956
+ };
2957
+ updateSize();
2958
+ if (typeof ResizeObserver === "undefined") return;
2959
+ const observer = new ResizeObserver(updateSize);
2960
+ observer.observe(node);
2961
+ return () => observer.disconnect();
2962
+ }, [target.stepId]);
2963
+ const resolvedPlacement = placement ?? "bottom";
2964
+ const isAutoPlacement = resolvedPlacement.startsWith("auto");
2965
+ const autoAlignment = resolvedPlacement.endsWith(
2966
+ "-start"
2967
+ ) ? "start" : resolvedPlacement.endsWith("-end") ? "end" : void 0;
2968
+ useEffect14(() => {
2969
+ setDragPosition(null);
2970
+ setLayoutMode(prefersMobileRef.current ? "mobile" : "floating");
2971
+ cachedFloatingPositionRef.current = null;
2972
+ appliedFloatingCacheRef.current = null;
2973
+ }, [target.stepId]);
2974
+ useEffect14(() => {
2975
+ if (layoutMode !== "manual") {
2976
+ setDragPosition(null);
2977
+ }
2978
+ }, [layoutMode]);
2979
+ useEffect14(() => {
2980
+ cachedFloatingPositionRef.current = floatingPosition;
2981
+ const cacheKey = getFloatingCacheKey(target);
2982
+ if (cacheKey) {
2983
+ floatingPositionCache.set(cacheKey, floatingPosition);
2984
+ }
2985
+ }, [floatingPosition, target.isScreen, target.stepId]);
2986
+ const dockedPosition = useMemo11(
2987
+ () => ({
2988
+ top: viewport.height - DOCKED_MARGIN,
2989
+ left: viewport.width - DOCKED_MARGIN,
2990
+ transform: "translate3d(-100%, -100%, 0px)"
2991
+ }),
2992
+ [viewport.height, viewport.width]
2993
+ );
2994
+ const mobilePosition = useMemo11(
2995
+ () => ({
2996
+ top: viewport.height - MOBILE_HORIZONTAL_GUTTER,
2997
+ left: viewport.width / 2,
2998
+ transform: "translate3d(-50%, -100%, 0px)"
2999
+ }),
3000
+ [viewport.height, viewport.width]
3001
+ );
3002
+ useEffect14(() => {
3003
+ if (layoutMode === "docked") {
3004
+ setFloatingPosition(dockedPosition);
3005
+ }
3006
+ }, [dockedPosition, layoutMode]);
3007
+ useEffect14(() => {
3008
+ if (layoutMode === "mobile") {
3009
+ setFloatingPosition(mobilePosition);
3010
+ }
3011
+ }, [layoutMode, mobilePosition]);
3012
+ useEffect14(() => {
3013
+ if (prefersMobileLayout) {
3014
+ if (layoutMode !== "mobile") {
3015
+ setLayoutMode("mobile");
3016
+ setDragPosition(null);
3017
+ }
3018
+ return;
3019
+ }
3020
+ if (layoutMode === "mobile") {
3021
+ setLayoutMode("floating");
3022
+ setFloatingPosition(fallbackPosition);
3023
+ }
3024
+ }, [fallbackPosition, layoutMode, prefersMobileLayout]);
3025
+ useEffect14(() => {
3026
+ if (layoutMode !== "floating") return;
3027
+ const stepId = target.stepId;
3028
+ if (!stepId) return;
3029
+ if (appliedFloatingCacheRef.current === stepId) return;
3030
+ const cacheKey = getFloatingCacheKey(target);
3031
+ const cached = cacheKey ? floatingPositionCache.get(cacheKey) ?? null : null;
3032
+ if (cached) {
3033
+ appliedFloatingCacheRef.current = stepId;
3034
+ setFloatingPosition(cached);
3035
+ return;
3036
+ }
3037
+ appliedFloatingCacheRef.current = stepId;
3038
+ if (target.status !== "ready" || target.isScreen) {
3039
+ setFloatingPosition(fallbackPosition);
3040
+ }
3041
+ }, [
3042
+ fallbackPosition,
3043
+ layoutMode,
3044
+ target.isScreen,
3045
+ target.status,
3046
+ target.stepId
3047
+ ]);
3048
+ const shouldDeferScreenSnap = layoutMode === "floating" && target.isScreen && Boolean(layoutId);
3049
+ useEffect14(() => {
3050
+ return () => {
3051
+ if (deferredScreenSnapRef.current !== null) {
3052
+ cancelAnimationFrame(deferredScreenSnapRef.current);
3053
+ deferredScreenSnapRef.current = null;
3054
+ }
3055
+ };
3056
+ }, []);
3057
+ useLayoutEffect2(() => {
3058
+ if (layoutMode !== "floating") return;
3059
+ if (target.status === "ready" && !target.isScreen) return;
3060
+ if (shouldDeferScreenSnap) return;
3061
+ setFloatingPosition(fallbackPosition);
3062
+ }, [
3063
+ fallbackPosition,
3064
+ layoutMode,
3065
+ shouldDeferScreenSnap,
3066
+ target.isScreen,
3067
+ target.status
3068
+ ]);
3069
+ useEffect14(() => {
3070
+ if (!shouldDeferScreenSnap) return;
3071
+ if (deferredScreenSnapRef.current !== null) {
3072
+ cancelAnimationFrame(deferredScreenSnapRef.current);
3073
+ deferredScreenSnapRef.current = null;
3074
+ }
3075
+ let nextFrame = null;
3076
+ deferredScreenSnapRef.current = requestAnimationFrame(() => {
3077
+ nextFrame = requestAnimationFrame(() => {
3078
+ setFloatingPosition(fallbackPosition);
3079
+ deferredScreenSnapRef.current = null;
3080
+ if (nextFrame !== null) {
3081
+ cancelAnimationFrame(nextFrame);
3082
+ }
3083
+ });
3084
+ });
3085
+ return () => {
3086
+ if (deferredScreenSnapRef.current !== null) {
3087
+ cancelAnimationFrame(deferredScreenSnapRef.current);
3088
+ deferredScreenSnapRef.current = null;
3089
+ }
3090
+ if (nextFrame !== null) {
3091
+ cancelAnimationFrame(nextFrame);
3092
+ nextFrame = null;
3093
+ }
3094
+ };
3095
+ }, [fallbackPosition, shouldDeferScreenSnap]);
3096
+ useEffect14(() => {
3097
+ return () => {
3098
+ if (overflowRetryTimeoutRef.current !== null) {
3099
+ window.clearTimeout(overflowRetryTimeoutRef.current);
3100
+ }
3101
+ };
3102
+ }, []);
3103
+ useLayoutEffect2(() => {
3104
+ if (!isBrowser) return;
3105
+ const floatingEl = floatingRef.current;
3106
+ const rectInfo = target.rect;
3107
+ if (!floatingEl) return;
3108
+ if (target.status !== "ready") return;
3109
+ if (!rectInfo || target.isScreen) return;
3110
+ if (layoutMode === "mobile" || layoutMode === "manual") return;
3111
+ const cancelState = { cancelled: false };
3112
+ const retryState = overflowRetryRef.current;
3113
+ const currentStepId = target.stepId ?? null;
3114
+ if (retryState.stepId !== currentStepId) {
3115
+ retryState.stepId = currentStepId;
3116
+ retryState.attempts = 0;
3117
+ }
3118
+ const clearRetryTimeout = () => {
3119
+ if (overflowRetryTimeoutRef.current !== null) {
3120
+ window.clearTimeout(overflowRetryTimeoutRef.current);
3121
+ overflowRetryTimeoutRef.current = null;
3122
+ }
3123
+ };
3124
+ const virtualReference = {
3125
+ contextElement: target.element ?? void 0,
3126
+ getBoundingClientRect: () => DOMRectReadOnly.fromRect({
3127
+ width: rectInfo.width,
3128
+ height: rectInfo.height,
3129
+ x: rectInfo.left,
3130
+ y: rectInfo.top
3131
+ })
3132
+ };
3133
+ const computePlacement = isAutoPlacement ? void 0 : resolvedPlacement;
3134
+ const middleware = [
3135
+ floatingOffset(offset),
3136
+ ...isAutoPlacement ? [
3137
+ autoPlacement({
3138
+ padding: FLOATING_OFFSET,
3139
+ alignment: autoAlignment
3140
+ })
3141
+ ] : [flip({ padding: FLOATING_OFFSET })],
3142
+ shift({ padding: FLOATING_OFFSET })
3143
+ ];
3144
+ const updatePosition = async () => {
3145
+ const { x, y } = await computePosition(virtualReference, floatingEl, {
3146
+ placement: computePlacement,
3147
+ strategy: "fixed",
3148
+ middleware
3149
+ });
3150
+ if (cancelState.cancelled) return;
3151
+ const floatingBox = floatingEl.getBoundingClientRect();
3152
+ const viewportRect = getViewportRect();
3153
+ const viewportTop = viewportRect.top;
3154
+ const viewportBottom = viewportRect.top + viewportRect.height;
3155
+ const viewportLeft = viewportRect.left;
3156
+ const viewportRight = viewportRect.left + viewportRect.width;
3157
+ const overflowLeft = Math.max(0, viewportRect.left + FLOATING_OFFSET - x);
3158
+ const overflowRight = Math.max(
3159
+ 0,
3160
+ x + floatingBox.width + FLOATING_OFFSET - (viewportRect.left + viewportRect.width)
3161
+ );
3162
+ const overflowTop = Math.max(0, viewportRect.top + FLOATING_OFFSET - y);
3163
+ const overflowBottom = Math.max(
3164
+ 0,
3165
+ y + floatingBox.height + FLOATING_OFFSET - (viewportRect.top + viewportRect.height)
3166
+ );
3167
+ const maxOverflow = Math.max(
3168
+ overflowTop,
3169
+ overflowRight,
3170
+ overflowBottom,
3171
+ overflowLeft
3172
+ );
3173
+ const viewportHeight = viewportRect.height;
3174
+ const viewportWidth = viewportRect.width;
3175
+ const overflowThreshold = Math.max(
3176
+ FLOATING_OFFSET * 2,
3177
+ viewportHeight * 0.05,
3178
+ viewportWidth * 0.05
3179
+ );
3180
+ const targetRect = rectInfo;
3181
+ const intersectsViewport = targetRect.bottom > viewportTop + FLOATING_OFFSET && targetRect.top < viewportBottom - FLOATING_OFFSET && targetRect.right > viewportLeft + FLOATING_OFFSET && targetRect.left < viewportRight - FLOATING_OFFSET;
3182
+ const spaceAbove = targetRect.top - viewportTop;
3183
+ const spaceBelow = viewportBottom - targetRect.bottom;
3184
+ const spaceLeft = targetRect.left - viewportLeft;
3185
+ const spaceRight = viewportRight - targetRect.right;
3186
+ const minSpaceNeeded = floatingBox.height + FLOATING_OFFSET * 2;
3187
+ const hasVerticalSpace = spaceAbove >= minSpaceNeeded || spaceBelow >= minSpaceNeeded;
3188
+ const hasHorizontalSpace = spaceLeft >= minSpaceNeeded || spaceRight >= minSpaceNeeded;
3189
+ const targetNearlyFillsViewport = !target.isScreen && !hasVerticalSpace && !hasHorizontalSpace;
3190
+ const shouldDock = intersectsViewport && (targetNearlyFillsViewport || maxOverflow > overflowThreshold);
3191
+ if (shouldDock) {
3192
+ if (!targetNearlyFillsViewport && retryState.attempts < 2) {
3193
+ retryState.attempts += 1;
3194
+ clearRetryTimeout();
3195
+ overflowRetryTimeoutRef.current = window.setTimeout(() => {
3196
+ overflowRetryTimeoutRef.current = null;
3197
+ if (cancelState.cancelled) return;
3198
+ void updatePosition();
3199
+ }, 120);
3200
+ return;
3201
+ }
3202
+ retryState.attempts = 0;
3203
+ if (layoutMode !== "docked") {
3204
+ setLayoutMode("docked");
3205
+ setFloatingPosition(dockedPosition);
3206
+ }
3207
+ return;
3208
+ }
3209
+ retryState.attempts = 0;
3210
+ if (layoutMode !== "floating") {
3211
+ setLayoutMode("floating");
3212
+ }
3213
+ setFloatingPosition({
3214
+ top: y,
3215
+ left: x,
3216
+ transform: "translate3d(0px, 0px, 0px)"
3217
+ });
3218
+ };
3219
+ void updatePosition();
3220
+ return () => {
3221
+ cancelState.cancelled = true;
3222
+ clearRetryTimeout();
3223
+ };
3224
+ }, [
3225
+ autoAlignment,
3226
+ dockedPosition,
3227
+ isAutoPlacement,
3228
+ layoutMode,
3229
+ offset,
3230
+ resolvedPlacement,
3231
+ target.element,
3232
+ target.isScreen,
3233
+ target.lastUpdated,
3234
+ target.status,
3235
+ target.stepId
3236
+ ]);
3237
+ useLayoutEffect2(() => {
3238
+ if (layoutMode !== "manual" || !dragPosition) return;
3239
+ setFloatingPosition({
3240
+ top: dragPosition.top,
3241
+ left: dragPosition.left,
3242
+ transform: "translate3d(0px, 0px, 0px)"
3243
+ });
3244
+ }, [dragPosition, layoutMode]);
3245
+ const clampToViewport = (rawLeft, rawTop) => {
3246
+ const rect = getViewportRect();
3247
+ const floatingEl = floatingRef.current;
3248
+ const floatingElWidth = floatingEl?.offsetWidth ?? 0;
3249
+ const floatingElHeight = floatingEl?.offsetHeight ?? 0;
3250
+ const minLeft = rect.left + FLOATING_OFFSET;
3251
+ const maxLeft = rect.left + rect.width - floatingElWidth - FLOATING_OFFSET;
3252
+ const minTop = rect.top + FLOATING_OFFSET;
3253
+ const maxTop = rect.top + rect.height - floatingElHeight - FLOATING_OFFSET;
3254
+ return {
3255
+ left: Math.min(Math.max(rawLeft, minLeft), Math.max(minLeft, maxLeft)),
3256
+ top: Math.min(Math.max(rawTop, minTop), Math.max(minTop, maxTop))
3257
+ };
3258
+ };
3259
+ const onPointerMove = (event) => {
3260
+ const dragState = dragStateRef.current;
3261
+ if (!dragState) return;
3262
+ if (event.pointerId !== dragState.pointerId) return;
3263
+ const rawLeft = event.clientX - dragState.offsetX;
3264
+ const rawTop = event.clientY - dragState.offsetY;
3265
+ const next = clampToViewport(rawLeft, rawTop);
3266
+ setLayoutMode("manual");
3267
+ setDragPosition(next);
3268
+ };
3269
+ const handlePointerEnd = (event) => {
3270
+ const dragState = dragStateRef.current;
3271
+ if (!dragState) return;
3272
+ if (event.pointerId !== dragState.pointerId) return;
3273
+ endDrag();
3274
+ };
3275
+ const endDrag = () => {
3276
+ const dragState = dragStateRef.current;
3277
+ if (!dragState) return;
3278
+ const floatingEl = floatingRef.current;
3279
+ if (floatingEl) {
3280
+ const supportsRelease = typeof floatingEl.releasePointerCapture === "function";
3281
+ const supportsHasCapture = typeof floatingEl.hasPointerCapture === "function";
3282
+ const hasCapture = supportsHasCapture ? floatingEl.hasPointerCapture(dragState.pointerId) : false;
3283
+ if (supportsRelease && hasCapture) {
3284
+ try {
3285
+ floatingEl.releasePointerCapture(dragState.pointerId);
3286
+ } catch {
3287
+ }
3288
+ }
3289
+ }
3290
+ dragStateRef.current = null;
3291
+ setIsDragging(false);
3292
+ window.removeEventListener("pointermove", onPointerMove);
3293
+ window.removeEventListener("pointerup", handlePointerEnd);
3294
+ window.removeEventListener("pointercancel", handlePointerEnd);
3295
+ };
3296
+ const startDrag = (event) => {
3297
+ if (event.button !== 0) return;
3298
+ const floatingEl = floatingRef.current;
3299
+ if (!floatingEl) return;
3300
+ const rect = floatingEl.getBoundingClientRect();
3301
+ dragStateRef.current = {
3302
+ pointerId: event.pointerId,
3303
+ offsetX: event.clientX - rect.left,
3304
+ offsetY: event.clientY - rect.top
3305
+ };
3306
+ const next = clampToViewport(rect.left, rect.top);
3307
+ setLayoutMode("manual");
3308
+ setDragPosition(next);
3309
+ setIsDragging(true);
3310
+ window.addEventListener("pointermove", onPointerMove);
3311
+ window.addEventListener("pointerup", handlePointerEnd);
3312
+ window.addEventListener("pointercancel", handlePointerEnd);
3313
+ const supportsPointerCapture = typeof floatingEl.setPointerCapture === "function";
3314
+ if (supportsPointerCapture) {
3315
+ try {
3316
+ floatingEl.setPointerCapture(event.pointerId);
3317
+ } catch {
3318
+ }
3319
+ }
3320
+ event.preventDefault();
3321
+ };
3322
+ useEffect14(() => endDrag, []);
3323
+ const shouldUseFallbackInitial = layoutMode !== "mobile" && (Boolean(target.lastResolvedRect) || Boolean(cachedTarget));
3324
+ const floatingCacheKey = layoutMode === "mobile" ? null : getFloatingCacheKey(target);
3325
+ const persistedFloatingInitial = floatingCacheKey && floatingPositionCache.has(floatingCacheKey) ? floatingPositionCache.get(floatingCacheKey) ?? null : null;
3326
+ const cachedFloatingInitial = layoutMode === "mobile" ? null : cachedFloatingPositionRef.current ?? persistedFloatingInitial;
3327
+ const hasCachedFloatingInitial = Boolean(cachedFloatingInitial);
3328
+ const resolvedInitialPosition = layoutMode === "mobile" ? mobilePosition : hasCachedFloatingInitial && cachedFloatingInitial ? cachedFloatingInitial : shouldUseFallbackInitial ? fallbackPosition : centerInitialPosition;
3329
+ const initialTop = resolvedInitialPosition.top;
3330
+ const initialLeft = resolvedInitialPosition.left;
3331
+ const initialTransform = resolvedInitialPosition.transform;
3332
+ const containerStyle = {
3333
+ position: "fixed",
3334
+ pointerEvents: "auto",
3335
+ zIndex,
3336
+ maxWidth: layoutMode === "mobile" || typeof maxWidth === "undefined" ? void 0 : maxWidth,
3337
+ width: layoutMode === "mobile" || typeof width === "undefined" ? void 0 : width,
3338
+ cursor: isDragging ? "grabbing" : void 0
3339
+ };
3340
+ const setFloatingNode = (node) => {
3341
+ floatingRef.current = node;
3342
+ onContainerChange?.(node);
3343
+ };
3344
+ const containerProps = {
3345
+ ref: setFloatingNode,
3346
+ role: role ?? "dialog",
3347
+ "aria-modal": ariaModal ?? false,
3348
+ "aria-label": ariaLabel,
3349
+ "aria-describedby": ariaDescribedBy,
3350
+ tabIndex: -1,
3351
+ "aria-live": "polite",
3352
+ "data-tour-popover": "",
3353
+ "data-layout": layoutMode,
3354
+ "data-target-visibility": target.visibility,
3355
+ "data-rect-source": target.rectSource,
3356
+ style: containerStyle,
3357
+ initial: {
3358
+ filter: "blur(4px)",
3359
+ opacity: 0,
3360
+ top: initialTop,
3361
+ left: initialLeft,
3362
+ transform: initialTransform
3363
+ },
3364
+ animate: {
3365
+ filter: "blur(0px)",
3366
+ opacity: 1,
3367
+ top: floatingPosition.top,
3368
+ left: floatingPosition.left,
3369
+ transform: floatingPosition.transform
3370
+ },
3371
+ exit: {
3372
+ filter: "blur(4px)",
3373
+ opacity: 0,
3374
+ transition: popoverExitTransition
3375
+ },
3376
+ transition: popoverEntranceTransition,
3377
+ ...layoutId ? { layoutId } : {}
3378
+ };
3379
+ const contentProps = {
3380
+ key: target.stepId ?? void 0,
3381
+ "data-tour-popover-content": "",
3382
+ initial: { opacity: 0, translateX: 0, filter: "blur(4px)" },
3383
+ animate: { opacity: 1, translateX: 0, filter: "blur(0px)" },
3384
+ exit: { opacity: 0, translateX: 0, filter: "blur(4px)" },
3385
+ transition: popoverContentTransition
3386
+ };
3387
+ const showDragHandle = layoutMode === "docked" || layoutMode === "manual";
3388
+ const dragHandleProps = {
3389
+ type: "button",
3390
+ "aria-label": "Move tour popover",
3391
+ style: { touchAction: "none" },
3392
+ onPointerDown: startDrag
3393
+ };
3394
+ const descriptionProps = {
3395
+ id: descriptionId,
3396
+ text: descriptionText
3397
+ };
3398
+ const context = {
3399
+ Container,
3400
+ Content,
3401
+ containerProps,
3402
+ contentProps,
3403
+ layoutMode,
3404
+ isDragging,
3405
+ showDragHandle,
3406
+ dragHandleProps,
3407
+ descriptionProps
3408
+ };
3409
+ return createPortal2(children(context), host);
3410
+ };
3411
+
3412
+ // src/components/TourFocusManager.tsx
3413
+ import { useEffect as useEffect15, useLayoutEffect as useLayoutEffect3, useRef as useRef8, useState as useState11 } from "react";
3414
+ import { createPortal as createPortal3 } from "react-dom";
3415
+
3416
+ // src/utils/focus.ts
3417
+ var FOCUSABLE_SELECTOR = 'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), summary, [contenteditable="true"], [tabindex]';
3418
+ var isElementVisible = (element) => {
3419
+ if (element.offsetParent !== null) return true;
3420
+ if (element.getClientRects().length > 0) return true;
3421
+ return false;
3422
+ };
3423
+ var isFocusableElement = (element) => {
3424
+ if (!(element instanceof HTMLElement)) return false;
3425
+ if (element.hasAttribute("disabled")) return false;
3426
+ if (element.getAttribute("aria-hidden") === "true") return false;
3427
+ if (element.tabIndex >= 0) return true;
3428
+ const tagName = element.tagName.toLowerCase();
3429
+ if (tagName === "input") {
3430
+ const type = element.getAttribute("type");
3431
+ if (type === "hidden") return false;
3432
+ return !element.hasAttribute("disabled");
3433
+ }
3434
+ if (tagName === "button" || tagName === "select" || tagName === "textarea") {
3435
+ return !element.hasAttribute("disabled");
3436
+ }
3437
+ if (tagName === "a" || tagName === "area") {
3438
+ return element.hasAttribute("href");
3439
+ }
3440
+ if (element.hasAttribute("contenteditable")) {
3441
+ return element.getAttribute("contenteditable") !== "false";
3442
+ }
3443
+ if (!element.hasAttribute("tabindex")) return false;
3444
+ return element.tabIndex >= 0;
3445
+ };
3446
+ var getFocusableIn = (root) => {
3447
+ if (!root) return [];
3448
+ const scope = root instanceof Document ? root.body : root;
3449
+ const matched = Array.from(
3450
+ scope.querySelectorAll(FOCUSABLE_SELECTOR)
3451
+ );
3452
+ const results = [];
3453
+ const seen = /* @__PURE__ */ new Set();
3454
+ for (const element of matched) {
3455
+ if (element.tabIndex < 0 && !element.hasAttribute("tabindex")) continue;
3456
+ if (!isElementVisible(element)) continue;
3457
+ if (!isFocusableElement(element)) continue;
3458
+ if (element.closest('[data-tour-focus-skip="true"]')) continue;
3459
+ if (seen.has(element)) continue;
3460
+ seen.add(element);
3461
+ results.push(element);
3462
+ }
3463
+ return results;
3464
+ };
3465
+ var focusElement = (element, options) => {
3466
+ if (!element) return;
3467
+ try {
3468
+ element.focus(options ?? { preventScroll: true });
3469
+ } catch {
3470
+ }
3471
+ };
3472
+
3473
+ // src/components/TourFocusManager.tsx
3474
+ import { jsx as jsx4 } from "react/jsx-runtime";
3475
+ var runMicrotask = (callback) => {
3476
+ if (typeof queueMicrotask === "function") {
3477
+ queueMicrotask(callback);
3478
+ } else {
3479
+ setTimeout(callback, 0);
3480
+ }
3481
+ };
3482
+ var TourFocusManager = ({
3483
+ active,
3484
+ target,
3485
+ popoverNode,
3486
+ highlightRect,
3487
+ targetRingOffset = -2
3488
+ }) => {
3489
+ const previousFocusRef = useRef8(null);
3490
+ const guardNodesRef = useRef8({
3491
+ "target-start": null,
3492
+ "target-end": null,
3493
+ "popover-start": null,
3494
+ "popover-end": null
3495
+ });
3496
+ const lastTabDirectionRef = useRef8("forward");
3497
+ const suppressGuardHopRef = useRef8(null);
3498
+ const ringStylesRef = useRef8(
3499
+ /* @__PURE__ */ new WeakMap()
3500
+ );
3501
+ const [targetRingActive, setTargetRingActive] = useState11(false);
3502
+ const restoreFocus = () => {
3503
+ const previous = previousFocusRef.current;
3504
+ previousFocusRef.current = null;
3505
+ if (previous && previous.isConnected) {
3506
+ runMicrotask(() => {
3507
+ focusElement(previous, { preventScroll: true });
3508
+ });
3509
+ }
3510
+ };
3511
+ useLayoutEffect3(() => {
3512
+ if (!isBrowser) return;
3513
+ if (!active) {
3514
+ restoreFocus();
3515
+ return;
3516
+ }
3517
+ if (previousFocusRef.current) return;
3518
+ const doc = popoverNode?.ownerDocument ?? target.element?.ownerDocument;
3519
+ const activeEl = (doc ?? document).activeElement;
3520
+ if (activeEl instanceof HTMLElement) {
3521
+ previousFocusRef.current = activeEl;
3522
+ }
3523
+ return () => {
3524
+ restoreFocus();
3525
+ };
3526
+ }, [active, popoverNode, target.element]);
3527
+ useEffect15(() => {
3528
+ if (!isBrowser) return;
3529
+ if (!active) return;
3530
+ const doc = popoverNode?.ownerDocument ?? target.element?.ownerDocument ?? document;
3531
+ const createGuard = (key) => {
3532
+ const node = doc.createElement("div");
3533
+ node.tabIndex = 0;
3534
+ node.setAttribute("data-tour-focus-guard", key);
3535
+ node.setAttribute("data-tour-prevent-shortcut", "true");
3536
+ const label = key.startsWith("target") ? "Tour highlight boundary" : "Tour popover boundary";
3537
+ node.setAttribute("aria-label", label);
3538
+ Object.assign(node.style, {
3539
+ position: "fixed",
3540
+ top: "0",
3541
+ left: "0",
3542
+ width: "1px",
3543
+ height: "1px",
3544
+ opacity: "0",
3545
+ outline: "none",
3546
+ padding: "0",
3547
+ margin: "0",
3548
+ border: "0",
3549
+ pointerEvents: "auto"
3550
+ });
3551
+ return node;
3552
+ };
3553
+ const applyRing = (element, activeRing) => {
3554
+ if (!element) return;
3555
+ const cache = ringStylesRef.current;
3556
+ if (activeRing) {
3557
+ if (!cache.has(element)) {
3558
+ cache.set(element, {
3559
+ outline: element.style.outline,
3560
+ outlineOffset: element.style.outlineOffset
3561
+ });
3562
+ }
3563
+ element.style.outline = "2px solid var(--tour-focus-ring-color, rgba(59, 130, 246, 0.8))";
3564
+ element.style.outlineOffset = "3px";
3565
+ return;
3566
+ }
3567
+ const previous = cache.get(element);
3568
+ if (previous) {
3569
+ element.style.outline = previous.outline;
3570
+ element.style.outlineOffset = previous.outlineOffset;
3571
+ cache.delete(element);
3572
+ } else {
3573
+ element.style.outline = "";
3574
+ element.style.outlineOffset = "";
3575
+ }
3576
+ };
3577
+ const clearRings = () => {
3578
+ setTargetRingActive(false);
3579
+ applyRing(popoverNode, false);
3580
+ };
3581
+ const removeGuards = () => {
3582
+ for (const key of Object.keys(guardNodesRef.current)) {
3583
+ const node = guardNodesRef.current[key];
3584
+ if (node?.parentNode) {
3585
+ node.parentNode.removeChild(node);
3586
+ }
3587
+ guardNodesRef.current[key] = null;
3588
+ }
3589
+ clearRings();
3590
+ };
3591
+ const ensureGuards = () => {
3592
+ const targetElement = !target.isScreen && target.element instanceof HTMLElement ? target.element : null;
3593
+ const popoverElement = popoverNode ?? null;
3594
+ if (targetElement) {
3595
+ if (!guardNodesRef.current["target-start"]) {
3596
+ guardNodesRef.current["target-start"] = createGuard("target-start");
3597
+ }
3598
+ if (!guardNodesRef.current["target-end"]) {
3599
+ guardNodesRef.current["target-end"] = createGuard("target-end");
3600
+ }
3601
+ const startGuard = guardNodesRef.current["target-start"];
3602
+ const endGuard = guardNodesRef.current["target-end"];
3603
+ if (startGuard.parentElement !== targetElement.parentElement) {
3604
+ targetElement.insertAdjacentElement("beforebegin", startGuard);
3605
+ }
3606
+ if (endGuard.parentElement !== targetElement.parentElement) {
3607
+ targetElement.insertAdjacentElement("afterend", endGuard);
3608
+ }
3609
+ } else {
3610
+ for (const key of ["target-start", "target-end"]) {
3611
+ const node = guardNodesRef.current[key];
3612
+ if (node?.parentNode) {
3613
+ node.parentNode.removeChild(node);
3614
+ }
3615
+ guardNodesRef.current[key] = null;
3616
+ }
3617
+ }
3618
+ if (popoverElement) {
3619
+ if (!guardNodesRef.current["popover-start"]) {
3620
+ guardNodesRef.current["popover-start"] = createGuard("popover-start");
3621
+ }
3622
+ if (!guardNodesRef.current["popover-end"]) {
3623
+ guardNodesRef.current["popover-end"] = createGuard("popover-end");
3624
+ }
3625
+ const startGuard = guardNodesRef.current["popover-start"];
3626
+ const endGuard = guardNodesRef.current["popover-end"];
3627
+ if (startGuard.parentElement !== popoverElement) {
3628
+ popoverElement.prepend(startGuard);
3629
+ }
3630
+ if (endGuard.parentElement !== popoverElement) {
3631
+ popoverElement.append(endGuard);
3632
+ }
3633
+ } else {
3634
+ for (const key of ["popover-start", "popover-end"]) {
3635
+ const node = guardNodesRef.current[key];
3636
+ if (node?.parentNode) {
3637
+ node.parentNode.removeChild(node);
3638
+ }
3639
+ guardNodesRef.current[key] = null;
3640
+ }
3641
+ }
3642
+ };
3643
+ const deriveFocusables = () => {
3644
+ const nodes = [];
3645
+ if (popoverNode) {
3646
+ nodes.push(...getFocusableIn(popoverNode));
3647
+ }
3648
+ const targetIsFocusable = !target.isScreen && target.visibility === "visible" && target.element instanceof HTMLElement;
3649
+ if (targetIsFocusable) {
3650
+ const targetElement = target.element;
3651
+ if (!targetElement.closest('[data-tour-focus-skip="true"]') && isFocusableElement(targetElement)) {
3652
+ nodes.push(targetElement);
3653
+ }
3654
+ nodes.push(...getFocusableIn(targetElement));
3655
+ }
3656
+ if (nodes.length === 0 && popoverNode?.hasAttribute("tabindex")) {
3657
+ nodes.push(popoverNode);
3658
+ }
3659
+ const unique = [];
3660
+ const seen = /* @__PURE__ */ new Set();
3661
+ for (const node of nodes) {
3662
+ if (seen.has(node)) continue;
3663
+ seen.add(node);
3664
+ unique.push(node);
3665
+ }
3666
+ return unique;
3667
+ };
3668
+ const isWithinTrap = (element) => {
3669
+ if (!(element instanceof HTMLElement)) return false;
3670
+ if (popoverNode?.contains(element)) return true;
3671
+ if (target.element instanceof HTMLElement && target.element.contains(element)) {
3672
+ return true;
3673
+ }
3674
+ return Object.values(guardNodesRef.current).some(
3675
+ (node) => node === element
3676
+ );
3677
+ };
3678
+ const ensureFocus = () => {
3679
+ const firstGuard = guardNodesRef.current["target-start"] ?? guardNodesRef.current["popover-start"];
3680
+ if (firstGuard) {
3681
+ focusElement(firstGuard);
3682
+ return;
3683
+ }
3684
+ const focusables = deriveFocusables();
3685
+ if (focusables.length === 0) return;
3686
+ focusElement(focusables[0]);
3687
+ };
3688
+ const handleKeyDown = (event) => {
3689
+ if (event.key !== "Tab") return;
3690
+ lastTabDirectionRef.current = event.shiftKey ? "backward" : "forward";
3691
+ };
3692
+ const handleFocusIn = (event) => {
3693
+ const targetNode = event.target;
3694
+ if (!(targetNode instanceof HTMLElement)) return;
3695
+ if (targetNode.hasAttribute("data-tour-focus-guard")) {
3696
+ if (suppressGuardHopRef.current === targetNode) {
3697
+ suppressGuardHopRef.current = null;
3698
+ } else {
3699
+ const direction = lastTabDirectionRef.current;
3700
+ const key2 = targetNode.getAttribute("data-tour-focus-guard");
3701
+ const targetStart = guardNodesRef.current["target-start"];
3702
+ const targetEnd = guardNodesRef.current["target-end"];
3703
+ const popoverStart = guardNodesRef.current["popover-start"];
3704
+ const popoverEnd = guardNodesRef.current["popover-end"];
3705
+ const hasTargetGuards = Boolean(targetStart && targetEnd);
3706
+ const nextGuard = direction === "forward" ? key2 === "target-end" ? popoverStart : key2 === "popover-end" ? hasTargetGuards ? targetStart : popoverStart : null : key2 === "popover-start" ? hasTargetGuards ? targetEnd : popoverEnd : key2 === "target-start" ? popoverEnd : null;
3707
+ if (nextGuard) {
3708
+ suppressGuardHopRef.current = nextGuard;
3709
+ focusElement(nextGuard);
3710
+ return;
3711
+ }
3712
+ }
3713
+ const key = targetNode.getAttribute("data-tour-focus-guard");
3714
+ if (key?.startsWith("target")) {
3715
+ setTargetRingActive(true);
3716
+ applyRing(popoverNode, false);
3717
+ } else if (key?.startsWith("popover")) {
3718
+ setTargetRingActive(false);
3719
+ applyRing(popoverNode, true);
3720
+ }
3721
+ return;
3722
+ }
3723
+ clearRings();
3724
+ if (isWithinTrap(targetNode)) return;
3725
+ ensureFocus();
3726
+ };
3727
+ ensureGuards();
3728
+ doc.addEventListener("keydown", handleKeyDown, true);
3729
+ doc.addEventListener("focusin", handleFocusIn, true);
3730
+ return () => {
3731
+ doc.removeEventListener("keydown", handleKeyDown, true);
3732
+ doc.removeEventListener("focusin", handleFocusIn, true);
3733
+ removeGuards();
3734
+ };
3735
+ }, [
3736
+ active,
3737
+ popoverNode,
3738
+ target.element,
3739
+ target.isScreen,
3740
+ target.lastUpdated,
3741
+ target.status,
3742
+ target.stepId,
3743
+ target.visibility
3744
+ ]);
3745
+ if (!isBrowser) return null;
3746
+ const host = portalHost();
3747
+ if (!host || !highlightRect || !targetRingActive) return null;
3748
+ const offset = Math.max(0, targetRingOffset);
3749
+ const ringStyle = {
3750
+ position: "fixed",
3751
+ top: highlightRect.top - offset,
3752
+ left: highlightRect.left - offset,
3753
+ width: highlightRect.width + offset * 2,
3754
+ height: highlightRect.height + offset * 2,
3755
+ borderRadius: highlightRect.radius + offset,
3756
+ boxShadow: [
3757
+ "0 0 0 2px var(--tour-focus-ring-offset-color, transparent)",
3758
+ "0 0 0 2px var(--tour-focus-ring-color, rgba(59, 130, 246, 0.8))"
3759
+ ].join(", "),
3760
+ pointerEvents: "none",
3761
+ zIndex: 2001
3762
+ };
3763
+ return createPortal3(/* @__PURE__ */ jsx4("div", { style: ringStyle, "aria-hidden": true }), host);
3764
+ };
3765
+
3766
+ // src/motion/useHudMotion.ts
3767
+ import { useMemo as useMemo12 } from "react";
3768
+ var DEFAULT_HIGHLIGHT_TRANSITION2 = {
3769
+ duration: 0.35,
3770
+ ease: "easeOut",
3771
+ type: "spring",
3772
+ damping: 25,
3773
+ stiffness: 300,
3774
+ mass: 0.7
3775
+ };
3776
+ var DEFAULT_OVERLAY_TRANSITION2 = {
3777
+ duration: 0.35,
3778
+ ease: "easeOut"
3779
+ };
3780
+ var DEFAULT_POPOVER_ENTRANCE_TRANSITION2 = {
3781
+ duration: 0.25,
3782
+ ease: "easeOut"
3783
+ };
3784
+ var DEFAULT_POPOVER_EXIT_TRANSITION2 = {
3785
+ duration: 0.2,
3786
+ ease: "easeOut"
3787
+ };
3788
+ var DEFAULT_POPOVER_CONTENT_TRANSITION2 = {
3789
+ duration: 0.6,
3790
+ ease: "easeOut"
3791
+ };
3792
+ var useHudMotion = () => {
3793
+ const adapter = useAnimationAdapter();
3794
+ return useMemo12(() => {
3795
+ const components = {
3796
+ ...adapter.components
3797
+ };
3798
+ return {
3799
+ components,
3800
+ transitions: {
3801
+ highlight: adapter.transitions.overlayHighlight ?? DEFAULT_HIGHLIGHT_TRANSITION2,
3802
+ overlayFade: adapter.transitions.overlayFade ?? DEFAULT_OVERLAY_TRANSITION2,
3803
+ popoverEntrance: adapter.transitions.popoverEntrance ?? DEFAULT_POPOVER_ENTRANCE_TRANSITION2,
3804
+ popoverExit: adapter.transitions.popoverExit ?? DEFAULT_POPOVER_EXIT_TRANSITION2,
3805
+ popoverContent: adapter.transitions.popoverContent ?? DEFAULT_POPOVER_CONTENT_TRANSITION2
3806
+ }
3807
+ };
3808
+ }, [adapter]);
3809
+ };
3810
+ export {
3811
+ AnimationAdapterProvider,
3812
+ OverlayBackdrop,
3813
+ TanStackRouterSync,
3814
+ TourFocusManager,
3815
+ TourPopoverPortal,
3816
+ TourProvider,
3817
+ createPathString,
3818
+ createWaitForPredicateController,
3819
+ defaultAnimationAdapter,
3820
+ defaultLabels,
3821
+ getCurrentRoutePath,
3822
+ getTanStackRouter,
3823
+ getTourRouter,
3824
+ notifyRouteChange,
3825
+ reducedMotionAnimationAdapter,
3826
+ setTanStackRouter,
3827
+ setTourRouter,
3828
+ subscribeToRouteChanges,
3829
+ useAdvanceRules,
3830
+ useAnimationAdapter,
3831
+ useBodyScrollLock,
3832
+ useDelayAdvance,
3833
+ useHiddenTargetFallback,
3834
+ useHudDescription,
3835
+ useHudMotion,
3836
+ useHudShortcuts,
3837
+ useHudState,
3838
+ useHudTargetIssue,
3839
+ usePreferredAnimationAdapter,
3840
+ useRadixDialogAdapter,
3841
+ useTanStackRouterTourAdapter,
3842
+ useTour,
3843
+ useTourControls,
3844
+ useTourEvents,
3845
+ useTourFocusDominance,
3846
+ useTourHud,
3847
+ useTourLabels,
3848
+ useTourOverlay,
3849
+ useTourTarget,
3850
+ useViewportRect
3851
+ };