@aladinbs/react-guided-tour 1.0.3 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,20 +2,22 @@
2
2
 
3
3
  A modern, flexible React TypeScript tour guide library with advanced highlighting and interaction capabilities. Built for React 19 with a clean, modular architecture that supports complex user onboarding flows.
4
4
 
5
- ## Features
6
-
7
- - 🎯 **Smart Element Targeting** - Flexible element selection with CSS selectors or direct element references
8
- - 🎨 **Modern UI Design** - Beautiful, customizable popover with smooth animations
9
- - 🔧 **Pluggable Architecture** - Extensible integration system for tabs, wizards, and navigation
10
- - 📱 **Responsive Positioning** - Intelligent popover placement that adapts to viewport constraints
11
- - 🎭 **Theme Support** - Comprehensive theming with light/dark mode compatibility
12
- - 💾 **State Persistence** - localStorage integration to remember tour completion and progress
13
- - **Performance Optimized** - Framework-agnostic core with efficient React integration
14
- - 🎪 **Action System** - Automated interactions (clicks, navigation, tab switching)
15
- - 🔄 **Event System** - Rich event hooks for tour lifecycle management
16
- - 🎯 **TypeScript First** - Full type safety with comprehensive type definitions
17
-
18
- ## 🚀 Quick Start
5
+ ## Features
6
+
7
+ - **Smart Element Targeting** - Flexible element selection with CSS selectors or direct element references
8
+ - **Modern UI Design** - Beautiful, customizable popover with smooth animations
9
+ - **Pluggable Architecture** - Extensible integration system for tabs, wizards, and navigation
10
+ - **Responsive Positioning** - Intelligent popover placement that adapts to viewport constraints
11
+ - **Theme Support** - Comprehensive theming with light/dark mode compatibility
12
+ - **State Persistence** - localStorage integration to remember tour completion and progress
13
+ - **Performance Optimized** - Framework-agnostic core with efficient React integration
14
+ - **Action System** - Automated interactions (clicks, navigation, tab switching)
15
+ - **Interaction Blocking** - Prevent user interactions outside the tour target for focused guidance
16
+ - **Click-to-Advance** - Allow users to click target elements directly to advance tour steps
17
+ - **Event System** - Rich event hooks for tour lifecycle management
18
+ - **TypeScript First** - Full type safety with comprehensive type definitions
19
+
20
+ ## Quick Start
19
21
 
20
22
  ### Installation
21
23
 
@@ -141,6 +143,8 @@ interface TourConfig {
141
143
  onStepChange?: (step: TourStep, index: number) => void;
142
144
  allowKeyboardNavigation?: boolean; // Enable keyboard controls
143
145
  allowClickOutside?: boolean; // Allow clicking outside to close
146
+ blockInteractions?: boolean; // Block interactions outside tour target
147
+ clickToAdvance?: boolean; // Enable click-to-advance globally
144
148
  showProgress?: boolean; // Show step progress indicator
145
149
  storage?: {
146
150
  key?: string; // localStorage key (default: 'tour-{id}')
@@ -168,6 +172,9 @@ interface TourStep {
168
172
  canSkip?: boolean; // Allow skipping this step
169
173
  waitForElement?: boolean; // Wait for target element to appear
170
174
  waitTimeout?: number; // Timeout for element waiting (ms)
175
+ blockInteractions?: boolean; // Override global interaction blocking for this step
176
+ clickToAdvance?: boolean; // Override global click-to-advance for this step
177
+ previousButton?: PreviousButtonConfig; // Configure previous button behavior
171
178
  }
172
179
  ```
173
180
 
@@ -185,6 +192,18 @@ interface TourAction {
185
192
  }
186
193
  ```
187
194
 
195
+ ### PreviousButtonConfig
196
+
197
+ Configure the previous button behavior for individual steps:
198
+
199
+ ```tsx
200
+ interface PreviousButtonConfig {
201
+ show?: boolean; // Show/hide the previous button (default: true)
202
+ label?: string; // Custom button label (default: "Previous")
203
+ handler?: () => Promise<void> | void; // Custom click handler
204
+ }
205
+ ```
206
+
188
207
  ### TourTheme
189
208
 
190
209
  Comprehensive theming options:
@@ -279,6 +298,173 @@ Navigate through multi-step wizards:
279
298
  }
280
299
  ```
281
300
 
301
+ ### Interaction Blocking
302
+
303
+ Control user interactions during the tour to create focused guidance experiences:
304
+
305
+ ```tsx
306
+ const tourConfig: TourConfig = {
307
+ id: 'focused-tour',
308
+ blockInteractions: true, // Block interactions globally
309
+ steps: [
310
+ {
311
+ id: 'step1',
312
+ title: 'Focused Step',
313
+ content: 'Users can only interact with the highlighted element.',
314
+ target: '#important-button',
315
+ blockInteractions: true, // Enforce blocking for this step
316
+ },
317
+ {
318
+ id: 'step2',
319
+ title: 'Free Interaction',
320
+ content: 'Users can click anywhere during this step.',
321
+ target: '#another-element',
322
+ blockInteractions: false, // Override global setting for this step
323
+ },
324
+ ],
325
+ };
326
+ ```
327
+
328
+ **Features:**
329
+ - **Global Control**: Set `blockInteractions: true` in tour config to block interactions for all steps
330
+ - **Per-Step Override**: Use `blockInteractions` in individual steps to override the global setting
331
+ - **Visual Feedback**: Shows a "🔒 Interactions blocked" indicator when active
332
+ - **Smart Targeting**: Only the highlighted element and tour UI remain interactive
333
+ - **Keyboard Blocking**: Prevents keyboard interactions except tour navigation keys
334
+
335
+ **When to use:**
336
+ - Complex interfaces where users might get distracted
337
+ - Critical onboarding flows that must be completed in order
338
+ - Preventing accidental clicks that could break the tour flow
339
+ - Ensuring users focus on specific elements
340
+
341
+ ### Click-to-Advance
342
+
343
+ Enable users to interact directly with tour target elements to advance to the next step:
344
+
345
+ ```tsx
346
+ const tourConfig: TourConfig = {
347
+ id: "interactive-tour",
348
+ clickToAdvance: true, // Enable globally
349
+ steps: [
350
+ {
351
+ id: "button-interaction",
352
+ title: "Try the Button",
353
+ content: "Click the button below to continue the tour!",
354
+ target: "#my-button",
355
+ clickToAdvance: true, // Enable for this step
356
+ },
357
+ {
358
+ id: "form-interaction",
359
+ title: "Fill the Form",
360
+ content: "Complete this form field to proceed.",
361
+ target: "#email-input",
362
+ clickToAdvance: false, // Disable for this step (use Next button)
363
+ },
364
+ ],
365
+ };
366
+ ```
367
+
368
+ **Features:**
369
+ - **Global Control**: Set `clickToAdvance: true` in tour config to enable for all steps
370
+ - **Per-Step Override**: Use `clickToAdvance` in individual steps to override the global setting
371
+ - **Smart UI**: Next button is automatically hidden when click-to-advance is active
372
+ - **Visual Guidance**: Shows "👆 Click the highlighted element to continue" instruction
373
+ - **Event Handling**: Automatically captures clicks on target elements and advances tour
374
+
375
+ **When to use:**
376
+ - Interactive tutorials where users need to practice using actual UI elements
377
+ - Onboarding flows for buttons, forms, and interactive components
378
+ - Training scenarios where clicking the real element is part of the learning
379
+ - Reducing cognitive load by eliminating the need to find the Next button
380
+
381
+ **Best practices:**
382
+ - Use for clickable elements like buttons, links, and form controls
383
+ - Combine with `blockInteractions: true` to ensure users click the right element
384
+ - Provide clear instructions in the step content about what to click
385
+ - Consider accessibility - ensure target elements are keyboard accessible
386
+
387
+ ### Configurable Previous Button
388
+
389
+ Control the previous button's visibility, label, and behavior for each step:
390
+
391
+ ```tsx
392
+ const tourConfig: TourConfig = {
393
+ id: "custom-navigation-tour",
394
+ steps: [
395
+ {
396
+ id: "welcome",
397
+ title: "Welcome",
398
+ content: "Welcome to our tour!",
399
+ placement: "center",
400
+ // No previous button configuration - uses defaults
401
+ },
402
+ {
403
+ id: "first-step",
404
+ title: "Getting Started",
405
+ content: "This is the first step with no previous button.",
406
+ target: "#first-element",
407
+ previousButton: {
408
+ show: false, // Hide previous button completely
409
+ },
410
+ },
411
+ {
412
+ id: "custom-previous",
413
+ title: "Custom Previous",
414
+ content: "This step has a custom previous button.",
415
+ target: "#second-element",
416
+ previousButton: {
417
+ label: "Go Back",
418
+ handler: async () => {
419
+ // Custom logic - could navigate to a different step,
420
+ // show a confirmation dialog, save data, etc.
421
+ const confirmed = confirm("Are you sure you want to go back?");
422
+ if (confirmed) {
423
+ // You can access tour methods here if needed
424
+ console.log("Custom previous action executed");
425
+ }
426
+ },
427
+ },
428
+ },
429
+ {
430
+ id: "normal-step",
431
+ title: "Normal Step",
432
+ content: "This step uses the default previous button behavior.",
433
+ target: "#third-element",
434
+ // previousButton not specified - uses default behavior
435
+ },
436
+ ],
437
+ };
438
+ ```
439
+
440
+ **Configuration Options:**
441
+
442
+ - **`show: boolean`** - Control visibility of the previous button
443
+ - `true` (default): Show the previous button
444
+ - `false`: Hide the previous button completely
445
+
446
+ - **`label: string`** - Customize the button text
447
+ - Default: "Previous"
448
+ - Examples: "Go Back", "← Back", "Return", etc.
449
+
450
+ - **`handler: () => Promise<void> | void`** - Custom click behavior
451
+ - When provided, overrides the default previous step navigation
452
+ - Can be async for complex operations
453
+ - Useful for confirmations, data saving, custom navigation logic
454
+
455
+ **Use Cases:**
456
+ - **Hide on first content step**: Prevent users from going back to welcome screen
457
+ - **Custom confirmations**: Ask users to confirm before losing progress
458
+ - **Data validation**: Save or validate data before allowing navigation
459
+ - **Custom navigation**: Jump to specific steps instead of sequential navigation
460
+ - **Analytics tracking**: Log user navigation patterns
461
+ - **Conditional logic**: Show different behavior based on user state
462
+
463
+ **Default Behavior:**
464
+ - Previous button is automatically shown on all steps except the first step
465
+ - Clicking previous navigates to the immediately preceding step
466
+ - Button is disabled when navigation is not possible (e.g., during step transitions)
467
+
282
468
  ### Custom Integrations
283
469
 
284
470
  Extend the tour system with custom integrations:
@@ -1 +1 @@
1
- {"version":3,"file":"TourOverlay.d.ts","sourceRoot":"","sources":["../../src/components/TourOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+B,MAAM,OAAO,CAAC;AAIpD,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CA4EtB,CAAC"}
1
+ {"version":3,"file":"TourOverlay.d.ts","sourceRoot":"","sources":["../../src/components/TourOverlay.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAoD,MAAM,OAAO,CAAC;AAIzE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CAsQtB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"TourPopover.d.ts","sourceRoot":"","sources":["../../src/components/TourPopover.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAOjF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CAgTtB,CAAC"}
1
+ {"version":3,"file":"TourPopover.d.ts","sourceRoot":"","sources":["../../src/components/TourPopover.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4D,MAAM,OAAO,CAAC;AAOjF,MAAM,WAAW,gBAAgB;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,WAAW,8CA2WtB,CAAC"}
package/dist/index.d.ts CHANGED
@@ -15,6 +15,9 @@ interface TourStep {
15
15
  canSkip?: boolean;
16
16
  waitForElement?: boolean;
17
17
  waitTimeout?: number;
18
+ blockInteractions?: boolean;
19
+ clickToAdvance?: boolean;
20
+ previousButton?: PreviousButtonConfig;
18
21
  }
19
22
  interface TourAction {
20
23
  type: 'click' | 'navigate' | 'highlight' | 'tab-switch' | 'wizard-step' | 'custom';
@@ -23,6 +26,11 @@ interface TourAction {
23
26
  handler?: () => Promise<void> | void;
24
27
  delay?: number;
25
28
  }
29
+ interface PreviousButtonConfig {
30
+ show?: boolean;
31
+ label?: string;
32
+ handler?: () => Promise<void> | void;
33
+ }
26
34
  interface HighlightConfig {
27
35
  selector?: string;
28
36
  element?: HTMLElement;
@@ -52,6 +60,8 @@ interface TourConfig {
52
60
  onStepChange?: (step: TourStep, index: number) => void;
53
61
  allowKeyboardNavigation?: boolean;
54
62
  allowClickOutside?: boolean;
63
+ blockInteractions?: boolean;
64
+ clickToAdvance?: boolean;
55
65
  showProgress?: boolean;
56
66
  storage?: {
57
67
  key?: string;
package/dist/index.esm.js CHANGED
@@ -1414,8 +1414,21 @@ function useTourHighlight(step) {
1414
1414
  }
1415
1415
 
1416
1416
  const TourOverlay = React.memo(function TourOverlay({ className }) {
1417
- const { state, theme, stop } = useTour();
1417
+ const { state, theme, stop, config, next } = useTour();
1418
1418
  const { targetElement, highlightStyle, isVisible } = useTourHighlight(state.currentStep);
1419
+ const [scrollTrigger, setScrollTrigger] = useState(0);
1420
+ // Determine if interactions should be blocked
1421
+ const shouldBlockInteractions = useMemo(() => {
1422
+ const stepBlocking = state.currentStep?.blockInteractions;
1423
+ const globalBlocking = config.blockInteractions;
1424
+ return stepBlocking !== undefined ? stepBlocking : globalBlocking;
1425
+ }, [state.currentStep?.blockInteractions, config.blockInteractions]);
1426
+ // Determine if click-to-advance is enabled
1427
+ const shouldClickToAdvance = useMemo(() => {
1428
+ const stepClickToAdvance = state.currentStep?.clickToAdvance;
1429
+ const globalClickToAdvance = config.clickToAdvance;
1430
+ return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
1431
+ }, [state.currentStep?.clickToAdvance, config.clickToAdvance]);
1419
1432
  const overlayStyle = useMemo(() => ({
1420
1433
  position: 'fixed',
1421
1434
  top: 0,
@@ -1427,15 +1440,148 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
1427
1440
  zIndex: (theme.zIndex || 9999) - 1,
1428
1441
  pointerEvents: 'auto',
1429
1442
  }), [theme.overlay?.backgroundColor, theme.overlay?.opacity, theme.zIndex]);
1430
- const handleOverlayClick = useCallback(() => {
1431
- if (state.currentStep?.canSkip !== false) {
1443
+ // Create cutout style for the highlighted element
1444
+ const cutoutOverlayStyle = useMemo(() => {
1445
+ if ((!shouldBlockInteractions && !shouldClickToAdvance) || !targetElement || !isVisible) {
1446
+ return overlayStyle;
1447
+ }
1448
+ // Use getBoundingClientRect for viewport-relative coordinates (perfect for fixed overlay)
1449
+ const rect = targetElement.getBoundingClientRect();
1450
+ const padding = state.currentStep?.highlight?.padding || 8;
1451
+ // Ensure coordinates are within viewport bounds
1452
+ const left = Math.max(0, rect.left - padding);
1453
+ const top = Math.max(0, rect.top - padding);
1454
+ const right = Math.min(window.innerWidth, rect.right + padding);
1455
+ const bottom = Math.min(window.innerHeight, rect.bottom + padding);
1456
+ return {
1457
+ ...overlayStyle,
1458
+ clipPath: `polygon(
1459
+ 0% 0%,
1460
+ 0% 100%,
1461
+ ${left}px 100%,
1462
+ ${left}px ${top}px,
1463
+ ${right}px ${top}px,
1464
+ ${right}px ${bottom}px,
1465
+ ${left}px ${bottom}px,
1466
+ ${left}px 100%,
1467
+ 100% 100%,
1468
+ 100% 0%
1469
+ )`,
1470
+ };
1471
+ }, [overlayStyle, shouldBlockInteractions, shouldClickToAdvance, targetElement, isVisible, state.currentStep?.highlight?.padding, scrollTrigger]); // eslint-disable-line react-hooks/exhaustive-deps
1472
+ // Force re-render of cutout overlay on scroll to maintain proper positioning
1473
+ useEffect(() => {
1474
+ if (!shouldBlockInteractions && !shouldClickToAdvance) {
1475
+ return;
1476
+ }
1477
+ const handleScroll = () => {
1478
+ // Force recalculation by updating scroll trigger
1479
+ setScrollTrigger(prev => prev + 1);
1480
+ };
1481
+ window.addEventListener('scroll', handleScroll, { passive: true });
1482
+ window.addEventListener('resize', handleScroll, { passive: true });
1483
+ return () => {
1484
+ window.removeEventListener('scroll', handleScroll);
1485
+ window.removeEventListener('resize', handleScroll);
1486
+ };
1487
+ }, [shouldBlockInteractions, shouldClickToAdvance]);
1488
+ const handleOverlayClick = useCallback((e) => {
1489
+ // If interactions are blocked, prevent the click from propagating
1490
+ if (shouldBlockInteractions) {
1491
+ e.preventDefault();
1492
+ e.stopPropagation();
1493
+ return;
1494
+ }
1495
+ // Allow clicking outside to close tour if not blocking interactions
1496
+ if (config.allowClickOutside !== false && state.currentStep?.canSkip !== false) {
1432
1497
  stop();
1433
1498
  }
1434
- }, [state.currentStep?.canSkip, stop]);
1499
+ }, [shouldBlockInteractions, config.allowClickOutside, state.currentStep?.canSkip, stop]);
1500
+ // Handle click-to-advance functionality
1501
+ useEffect(() => {
1502
+ if (!shouldClickToAdvance || !targetElement || !state.isRunning) {
1503
+ return;
1504
+ }
1505
+ // Ensure target element is above the overlay when click-to-advance is enabled
1506
+ const originalZIndex = targetElement.style.zIndex;
1507
+ const originalPosition = targetElement.style.position;
1508
+ targetElement.style.position = targetElement.style.position || 'relative';
1509
+ targetElement.style.zIndex = String((theme.zIndex || 9999) + 1);
1510
+ const handleTargetClick = (_e) => {
1511
+ setTimeout(() => {
1512
+ next();
1513
+ }, 100);
1514
+ };
1515
+ targetElement.addEventListener('click', handleTargetClick, true);
1516
+ return () => {
1517
+ targetElement.removeEventListener('click', handleTargetClick, true);
1518
+ // Restore original styles
1519
+ targetElement.style.zIndex = originalZIndex;
1520
+ targetElement.style.position = originalPosition;
1521
+ };
1522
+ }, [shouldClickToAdvance, targetElement, state.isRunning, next, theme.zIndex]);
1523
+ // Block interactions on the entire page when blocking is enabled
1524
+ useEffect(() => {
1525
+ if (!shouldBlockInteractions || !state.isRunning) {
1526
+ return;
1527
+ }
1528
+ const handleGlobalClick = (e) => {
1529
+ const target = e.target;
1530
+ // Allow clicks on the tour target element and its children
1531
+ if (targetElement && (targetElement.contains(target) || targetElement === target)) {
1532
+ // If click-to-advance is enabled, let the target click handler deal with it
1533
+ if (shouldClickToAdvance) {
1534
+ return;
1535
+ }
1536
+ return;
1537
+ }
1538
+ // Allow clicks on tour UI elements (popover, buttons, etc.)
1539
+ if (target.closest('[data-tour-popover]') ||
1540
+ target.closest('[data-tour-highlight]') ||
1541
+ target.closest('[data-tour-overlay]')) {
1542
+ return;
1543
+ }
1544
+ // Block all other clicks
1545
+ e.preventDefault();
1546
+ e.stopPropagation();
1547
+ };
1548
+ const handleGlobalKeydown = (e) => {
1549
+ // Allow tour navigation keys
1550
+ if (['Escape', 'ArrowLeft', 'ArrowRight', 'Enter', 'Space'].includes(e.key)) {
1551
+ return;
1552
+ }
1553
+ // Block other keyboard interactions
1554
+ e.preventDefault();
1555
+ e.stopPropagation();
1556
+ };
1557
+ // Add event listeners to capture phase to block interactions early
1558
+ document.addEventListener('click', handleGlobalClick, true);
1559
+ document.addEventListener('keydown', handleGlobalKeydown, true);
1560
+ document.addEventListener('mousedown', handleGlobalClick, true);
1561
+ document.addEventListener('touchstart', handleGlobalClick, true);
1562
+ return () => {
1563
+ document.removeEventListener('click', handleGlobalClick, true);
1564
+ document.removeEventListener('keydown', handleGlobalKeydown, true);
1565
+ document.removeEventListener('mousedown', handleGlobalClick, true);
1566
+ document.removeEventListener('touchstart', handleGlobalClick, true);
1567
+ };
1568
+ }, [shouldBlockInteractions, state.isRunning, targetElement, shouldClickToAdvance]);
1435
1569
  if (!state.isRunning || !state.currentStep) {
1436
1570
  return null;
1437
1571
  }
1438
- return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("div", { style: overlayStyle, onClick: handleOverlayClick, className: className, "data-tour-overlay": true }), isVisible && targetElement && (jsxRuntimeExports.jsx("div", { style: highlightStyle, "data-tour-highlight": true })), jsxRuntimeExports.jsx("style", { children: `
1572
+ return (jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [jsxRuntimeExports.jsx("div", { style: (shouldBlockInteractions || shouldClickToAdvance) ? cutoutOverlayStyle : overlayStyle, onClick: handleOverlayClick, className: className, "data-tour-overlay": true }), isVisible && targetElement && (jsxRuntimeExports.jsx("div", { style: highlightStyle, "data-tour-highlight": true })), shouldBlockInteractions && (jsxRuntimeExports.jsx("div", { style: {
1573
+ position: 'fixed',
1574
+ top: '20px',
1575
+ right: '20px',
1576
+ backgroundColor: 'rgba(0, 0, 0, 0.8)',
1577
+ color: 'white',
1578
+ padding: '8px 12px',
1579
+ borderRadius: '4px',
1580
+ fontSize: '12px',
1581
+ zIndex: (theme.zIndex || 9999) + 1,
1582
+ pointerEvents: 'none',
1583
+ animation: 'tour-fade-in 0.3s ease-out',
1584
+ }, "data-tour-blocking-indicator": true, children: "\uD83D\uDD12 Interactions blocked" })), jsxRuntimeExports.jsx("style", { children: `
1439
1585
  @keyframes tour-highlight-pulse {
1440
1586
  0%, 100% {
1441
1587
  box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.2), 0 0 20px rgba(59, 130, 246, 0.3);
@@ -1463,24 +1609,53 @@ const TourOverlay = React.memo(function TourOverlay({ className }) {
1463
1609
  [data-tour-highlight] {
1464
1610
  animation: tour-fade-in 0.3s ease-out;
1465
1611
  }
1612
+
1613
+ [data-tour-blocking-indicator] {
1614
+ animation: tour-fade-in 0.3s ease-out;
1615
+ }
1466
1616
  ` })] }));
1467
1617
  });
1468
1618
 
1469
1619
  function r(e){var t,f,n="";if("string"==typeof e||"number"==typeof e)n+=e;else if("object"==typeof e)if(Array.isArray(e)){var o=e.length;for(t=0;t<o;t++)e[t]&&(f=r(e[t]))&&(n&&(n+=" "),n+=f);}else for(f in e)e[f]&&(n&&(n+=" "),n+=f);return n}function clsx(){for(var e,t,f=0,n="",o=arguments.length;f<o;f++)(e=arguments[f])&&(t=r(e))&&(n&&(n+=" "),n+=t);return n}
1470
1620
 
1471
1621
  const TourPopover = React.memo(function TourPopover({ className }) {
1472
- const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious } = useTour();
1622
+ const { state, theme, next, previous, skip, stop, isFirstStep, isLastStep, canGoNext, canGoPrevious, config } = useTour();
1473
1623
  const { targetElement } = useTourHighlight(state.currentStep);
1474
1624
  const popoverRef = useRef(null);
1475
1625
  const [position, setPosition] = useState({ top: 0, left: 0, placement: 'top' });
1476
1626
  const updatePosition = useCallback(() => {
1477
- if (!popoverRef.current || !targetElement || !state.currentStep)
1627
+ if (!popoverRef.current || !state.currentStep)
1628
+ return;
1629
+ // For center placement without target element, calculate center position directly
1630
+ if (state.currentStep.placement === 'center' && !targetElement) {
1631
+ const popoverRect = popoverRef.current.getBoundingClientRect();
1632
+ const viewport = {
1633
+ width: window.innerWidth,
1634
+ height: window.innerHeight,
1635
+ scrollTop: window.pageYOffset || document.documentElement.scrollTop,
1636
+ scrollLeft: window.pageXOffset || document.documentElement.scrollLeft,
1637
+ };
1638
+ setPosition({
1639
+ top: viewport.scrollTop + (viewport.height - popoverRect.height) / 2,
1640
+ left: viewport.scrollLeft + (viewport.width - popoverRect.width) / 2,
1641
+ placement: 'center',
1642
+ });
1643
+ return;
1644
+ }
1645
+ // For other placements, require target element
1646
+ if (!targetElement)
1478
1647
  return;
1479
1648
  const newPosition = calculatePopoverPosition(targetElement, popoverRef.current, state.currentStep.placement || 'top');
1480
1649
  setPosition(newPosition);
1481
1650
  }, [targetElement, state.currentStep]);
1482
1651
  const step = state.currentStep;
1483
1652
  const popoverConfig = useMemo(() => step?.popover || {}, [step?.popover]);
1653
+ // Determine if click-to-advance is enabled for this step
1654
+ const shouldClickToAdvance = useMemo(() => {
1655
+ const stepClickToAdvance = step?.clickToAdvance;
1656
+ const globalClickToAdvance = config.clickToAdvance;
1657
+ return stepClickToAdvance !== undefined ? stepClickToAdvance : globalClickToAdvance;
1658
+ }, [step?.clickToAdvance, config.clickToAdvance]);
1484
1659
  const popoverStyle = useMemo(() => ({
1485
1660
  position: 'absolute',
1486
1661
  top: position.top,
@@ -1513,10 +1688,14 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1513
1688
  }
1514
1689
  }, [canGoNext, isLastStep, next]);
1515
1690
  const handlePrevious = useCallback(async () => {
1691
+ if (step?.previousButton?.handler) {
1692
+ await step.previousButton.handler();
1693
+ return;
1694
+ }
1516
1695
  if (canGoPrevious) {
1517
1696
  await previous();
1518
1697
  }
1519
- }, [canGoPrevious, previous]);
1698
+ }, [canGoPrevious, previous, step?.previousButton]);
1520
1699
  const handleSkip = useCallback(async () => {
1521
1700
  await skip();
1522
1701
  }, [skip]);
@@ -1524,12 +1703,16 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1524
1703
  await stop();
1525
1704
  }, [stop]);
1526
1705
  useEffect(() => {
1527
- updatePosition();
1706
+ // Small delay to ensure popover is rendered and has dimensions
1707
+ const timeoutId = setTimeout(() => {
1708
+ updatePosition();
1709
+ }, 10);
1528
1710
  const handleResize = () => updatePosition();
1529
1711
  const handleScroll = () => updatePosition();
1530
1712
  window.addEventListener('resize', handleResize);
1531
1713
  window.addEventListener('scroll', handleScroll, { passive: true });
1532
1714
  return () => {
1715
+ clearTimeout(timeoutId);
1533
1716
  window.removeEventListener('resize', handleResize);
1534
1717
  window.removeEventListener('scroll', handleScroll);
1535
1718
  };
@@ -1604,7 +1787,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1604
1787
  pointerEvents: 'auto',
1605
1788
  position: 'relative',
1606
1789
  zIndex: 99999,
1607
- }, children: popoverConfig.skipLabel || 'Skip Tour' })), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px', marginLeft: 'auto' }, children: [!isFirstStep && (jsxRuntimeExports.jsx("button", { onClick: handlePrevious, disabled: !canGoPrevious, style: {
1790
+ }, children: popoverConfig.skipLabel || 'Skip Tour' })), jsxRuntimeExports.jsxs("div", { style: { display: 'flex', gap: '8px', marginLeft: 'auto' }, children: [!isFirstStep && (step?.previousButton?.show !== false) && (jsxRuntimeExports.jsx("button", { onClick: handlePrevious, disabled: !canGoPrevious, style: {
1608
1791
  backgroundColor: 'transparent',
1609
1792
  borderTopWidth: '1px',
1610
1793
  borderRightWidth: '1px',
@@ -1626,7 +1809,7 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1626
1809
  fontWeight: '500',
1627
1810
  opacity: canGoPrevious ? 1 : 0.5,
1628
1811
  transition: 'all 0.2s ease',
1629
- }, children: "Previous" })), jsxRuntimeExports.jsx("button", { onClick: (e) => {
1812
+ }, children: step?.previousButton?.label || 'Previous' })), !shouldClickToAdvance && (jsxRuntimeExports.jsx("button", { onClick: (e) => {
1630
1813
  e.preventDefault();
1631
1814
  e.stopPropagation();
1632
1815
  handleNext();
@@ -1649,7 +1832,18 @@ const TourPopover = React.memo(function TourPopover({ className }) {
1649
1832
  zIndex: 99999,
1650
1833
  }, children: isLastStep
1651
1834
  ? (popoverConfig.finishLabel || 'Finish')
1652
- : (popoverConfig.nextLabel || 'Next') })] })] }), position.placement !== 'center' && (jsxRuntimeExports.jsx("div", { style: {
1835
+ : (popoverConfig.nextLabel || 'Next') })), shouldClickToAdvance && (jsxRuntimeExports.jsx("div", { style: {
1836
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
1837
+ border: '1px solid rgba(59, 130, 246, 0.3)',
1838
+ color: theme.primaryColor || '#3b82f6',
1839
+ padding: '8px 16px',
1840
+ borderRadius: '8px',
1841
+ fontSize: '14px',
1842
+ fontWeight: '500',
1843
+ display: 'flex',
1844
+ alignItems: 'center',
1845
+ gap: '6px',
1846
+ }, children: "\uD83D\uDC46 Click the highlighted element to continue" }))] })] }), position.placement !== 'center' && (jsxRuntimeExports.jsx("div", { style: {
1653
1847
  position: 'absolute',
1654
1848
  width: '12px',
1655
1849
  height: '12px',