@prosdevlab/experience-sdk-plugins 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +6 -6
  2. package/CHANGELOG.md +150 -0
  3. package/README.md +141 -79
  4. package/dist/index.d.ts +813 -35
  5. package/dist/index.js +1910 -66
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/banner/banner.ts +63 -62
  9. package/src/exit-intent/exit-intent.test.ts +423 -0
  10. package/src/exit-intent/exit-intent.ts +371 -0
  11. package/src/exit-intent/index.ts +6 -0
  12. package/src/exit-intent/types.ts +59 -0
  13. package/src/index.ts +7 -0
  14. package/src/inline/index.ts +3 -0
  15. package/src/inline/inline.test.ts +620 -0
  16. package/src/inline/inline.ts +269 -0
  17. package/src/inline/insertion.ts +66 -0
  18. package/src/inline/types.ts +52 -0
  19. package/src/integration.test.ts +421 -0
  20. package/src/modal/form-rendering.ts +262 -0
  21. package/src/modal/form-styles.ts +212 -0
  22. package/src/modal/form-validation.test.ts +413 -0
  23. package/src/modal/form-validation.ts +126 -0
  24. package/src/modal/index.ts +3 -0
  25. package/src/modal/modal-styles.ts +204 -0
  26. package/src/modal/modal.browser.test.ts +164 -0
  27. package/src/modal/modal.test.ts +1294 -0
  28. package/src/modal/modal.ts +685 -0
  29. package/src/modal/types.ts +114 -0
  30. package/src/page-visits/index.ts +6 -0
  31. package/src/page-visits/page-visits.test.ts +562 -0
  32. package/src/page-visits/page-visits.ts +314 -0
  33. package/src/page-visits/types.ts +119 -0
  34. package/src/scroll-depth/index.ts +6 -0
  35. package/src/scroll-depth/scroll-depth.test.ts +580 -0
  36. package/src/scroll-depth/scroll-depth.ts +398 -0
  37. package/src/scroll-depth/types.ts +122 -0
  38. package/src/time-delay/index.ts +6 -0
  39. package/src/time-delay/time-delay.test.ts +477 -0
  40. package/src/time-delay/time-delay.ts +296 -0
  41. package/src/time-delay/types.ts +89 -0
  42. package/src/types.ts +20 -36
  43. package/src/utils/sanitize.ts +5 -2
package/dist/index.d.ts CHANGED
@@ -1,57 +1,187 @@
1
- import { PluginFunction } from '@lytics/sdk-kit';
1
+ import { PluginFunction, SDK } from '@lytics/sdk-kit';
2
+ export { PluginFunction } from '@lytics/sdk-kit';
2
3
 
3
4
  /**
4
- * Shared types for Experience SDK plugins
5
- * These types are re-exported by core for user convenience
5
+ * Inline plugin configuration
6
6
  */
7
+ interface InlinePluginConfig {
8
+ inline?: {
9
+ /** Retry selector lookup if not found (default: false) */
10
+ retry?: boolean;
11
+ /** Retry timeout in ms (default: 5000) */
12
+ retryTimeout?: number;
13
+ };
14
+ }
7
15
  /**
8
- * Experience content - varies by type
9
- */
10
- type ExperienceContent = BannerContent | ModalContent | TooltipContent;
11
- /**
12
- * Banner content configuration
16
+ * Inline content configuration
13
17
  */
14
- interface BannerContent {
15
- title?: string;
18
+ interface InlineContent$1 {
19
+ /** CSS selector for target element */
20
+ selector: string;
21
+ /** Where to insert content (default: 'replace') */
22
+ position?: 'replace' | 'append' | 'prepend' | 'before' | 'after';
23
+ /** HTML content to insert */
16
24
  message: string;
17
- buttons?: Array<{
18
- text: string;
19
- action?: string;
20
- url?: string;
21
- variant?: 'primary' | 'secondary' | 'link';
22
- metadata?: Record<string, any>;
23
- className?: string;
24
- style?: Record<string, string>;
25
- }>;
25
+ /** Show close button (default: false) */
26
26
  dismissable?: boolean;
27
- position?: 'top' | 'bottom';
27
+ /** Remember dismissal in localStorage (default: false) */
28
+ persist?: boolean;
29
+ /** Custom CSS class */
28
30
  className?: string;
31
+ /** Inline styles */
29
32
  style?: Record<string, string>;
30
33
  }
31
34
  /**
32
- * Modal content configuration
35
+ * Inline plugin API
33
36
  */
34
- interface ModalContent {
35
- title: string;
36
- message: string;
37
- confirmText?: string;
38
- cancelText?: string;
37
+ interface InlinePlugin {
38
+ /** Show an inline experience */
39
+ show(experience: any): void;
40
+ /** Remove a specific inline experience */
41
+ remove(experienceId: string): void;
42
+ /** Check if an inline experience is showing */
43
+ isShowing(experienceId?: string): boolean;
39
44
  }
40
45
  /**
41
- * Modal content configuration
46
+ * Insertion position for inline content
42
47
  */
43
- interface ModalContent {
44
- title: string;
48
+ type InsertionPosition = 'replace' | 'append' | 'prepend' | 'before' | 'after';
49
+
50
+ interface ModalConfig {
51
+ modal?: {
52
+ /** Allow dismissal via close button (default: true) */
53
+ dismissable?: boolean;
54
+ /** Allow dismissal via backdrop click (default: true) */
55
+ backdropDismiss?: boolean;
56
+ /** Z-index for modal (default: 10001) */
57
+ zIndex?: number;
58
+ /** Modal size (default: 'md') */
59
+ size?: 'sm' | 'md' | 'lg' | 'fullscreen' | 'auto';
60
+ /** Auto-fullscreen on mobile screens <640px (default: true for 'lg', false for others) */
61
+ mobileFullscreen?: boolean;
62
+ /** Modal position (default: 'center') */
63
+ position?: 'center' | 'bottom';
64
+ /** Animation type (default: 'fade') */
65
+ animation?: 'fade' | 'slide-up' | 'none';
66
+ /** Animation duration in ms (default: 200) */
67
+ animationDuration?: number;
68
+ };
69
+ }
70
+ interface ModalContent$1 {
71
+ /** Optional hero image at top of modal */
72
+ image?: {
73
+ /** Image source URL */
74
+ src: string;
75
+ /** Alt text for accessibility */
76
+ alt: string;
77
+ /** Max height in pixels (default: 300, 200 on mobile) */
78
+ maxHeight?: number;
79
+ };
80
+ /** Modal title */
81
+ title?: string;
82
+ /** Modal message (supports HTML via sanitizer) */
83
+ message: string;
84
+ /** Array of action buttons */
85
+ buttons?: ExperienceButton[];
86
+ /** Optional form configuration */
87
+ form?: FormConfig;
88
+ /** Custom CSS class */
89
+ className?: string;
90
+ /** Inline styles */
91
+ style?: Record<string, string>;
92
+ }
93
+ interface FormConfig {
94
+ /** Array of form fields */
95
+ fields: FormField[];
96
+ /** Submit button configuration */
97
+ submitButton: ExperienceButton;
98
+ /** Success state after submission */
99
+ successState?: FormState;
100
+ /** Error state on submission failure */
101
+ errorState?: FormState;
102
+ /** Custom validation function (optional) */
103
+ validate?: (data: Record<string, string>) => ValidationResult;
104
+ }
105
+ interface FormField {
106
+ /** Field name (used in form data) */
107
+ name: string;
108
+ /** Field type */
109
+ type: 'email' | 'text' | 'textarea' | 'tel' | 'url' | 'number';
110
+ /** Label text (optional) */
111
+ label?: string;
112
+ /** Placeholder text */
113
+ placeholder?: string;
114
+ /** Required field (default: false) */
115
+ required?: boolean;
116
+ /** Custom validation pattern (regex) */
117
+ pattern?: string;
118
+ /** Error message for validation failure */
119
+ errorMessage?: string;
120
+ /** Custom CSS class */
121
+ className?: string;
122
+ /** Inline styles */
123
+ style?: Record<string, string>;
124
+ }
125
+ interface FormState {
126
+ /** Title to show in success/error state */
127
+ title?: string;
128
+ /** Message to show */
45
129
  message: string;
46
- confirmText?: string;
47
- cancelText?: string;
130
+ /** Optional buttons (e.g., "Close", "Try Again") */
131
+ buttons?: ExperienceButton[];
132
+ }
133
+ interface ValidationResult {
134
+ /** Whether validation passed */
135
+ valid: boolean;
136
+ /** Validation errors by field name */
137
+ errors?: Record<string, string>;
48
138
  }
139
+ interface ModalPlugin {
140
+ /** Show a modal experience */
141
+ show(experience: any): void;
142
+ /** Remove a specific modal */
143
+ remove(experienceId: string): void;
144
+ /** Check if a modal is showing */
145
+ isShowing(experienceId?: string): boolean;
146
+ /** Show form success or error state */
147
+ showFormState(experienceId: string, state: 'success' | 'error'): void;
148
+ /** Reset form to initial state */
149
+ resetForm(experienceId: string): void;
150
+ /** Get current form data */
151
+ getFormData(experienceId: string): Record<string, string> | null;
152
+ }
153
+
49
154
  /**
50
- * Tooltip content configuration
155
+ * Shared types for Experience SDK plugins
156
+ * These types are re-exported by core for user convenience
51
157
  */
52
- interface TooltipContent {
158
+
159
+ type ModalContent = ModalContent$1;
160
+ type InlineContent = InlineContent$1;
161
+ /**
162
+ * Experience button configuration (used across all experience types)
163
+ */
164
+ interface ExperienceButton {
165
+ text: string;
166
+ action?: string;
167
+ url?: string;
168
+ variant?: 'primary' | 'secondary' | 'link';
169
+ dismiss?: boolean;
170
+ metadata?: Record<string, any>;
171
+ className?: string;
172
+ style?: Record<string, string>;
173
+ }
174
+ /**
175
+ * Banner content configuration
176
+ */
177
+ interface BannerContent {
178
+ title?: string;
53
179
  message: string;
54
- position?: 'top' | 'bottom' | 'left' | 'right';
180
+ buttons?: ExperienceButton[];
181
+ dismissable?: boolean;
182
+ position?: 'top' | 'bottom';
183
+ className?: string;
184
+ style?: Record<string, string>;
55
185
  }
56
186
  /**
57
187
  * Tooltip content configuration
@@ -60,6 +190,10 @@ interface TooltipContent {
60
190
  message: string;
61
191
  position?: 'top' | 'bottom' | 'left' | 'right';
62
192
  }
193
+ /**
194
+ * Experience content - varies by type
195
+ */
196
+ type ExperienceContent = BannerContent | ModalContent | InlineContent | TooltipContent;
63
197
  /**
64
198
  * Experience definition
65
199
  */
@@ -192,6 +326,140 @@ interface DebugPlugin {
192
326
  */
193
327
  declare const debugPlugin: PluginFunction;
194
328
 
329
+ /**
330
+ * Exit Intent Plugin Configuration
331
+ */
332
+ interface ExitIntentPluginConfig {
333
+ exitIntent?: {
334
+ /**
335
+ * Maximum Y position (px) where exit intent can trigger
336
+ * @default 50
337
+ */
338
+ sensitivity?: number;
339
+ /**
340
+ * Minimum time on page (ms) before exit intent is active
341
+ * Prevents immediate triggers on page load
342
+ * @default 2000
343
+ */
344
+ minTimeOnPage?: number;
345
+ /**
346
+ * Delay (ms) between detection and trigger
347
+ * @default 0
348
+ */
349
+ delay?: number;
350
+ /**
351
+ * Number of mouse positions to track for velocity calculation
352
+ * @default 30
353
+ */
354
+ positionHistorySize?: number;
355
+ /**
356
+ * Disable exit intent on mobile devices
357
+ * @default true
358
+ */
359
+ disableOnMobile?: boolean;
360
+ };
361
+ }
362
+ /**
363
+ * Exit Intent Event Payload
364
+ */
365
+ interface ExitIntentEvent {
366
+ timestamp: number;
367
+ lastY: number;
368
+ previousY: number;
369
+ velocity: number;
370
+ timeOnPage: number;
371
+ }
372
+ /**
373
+ * Exit Intent Plugin API
374
+ */
375
+ interface ExitIntentPlugin {
376
+ isTriggered(): boolean;
377
+ reset(): void;
378
+ getPositions(): Array<{
379
+ x: number;
380
+ y: number;
381
+ }>;
382
+ }
383
+
384
+ /**
385
+ * Exit Intent Plugin
386
+ *
387
+ * Detects when users are about to leave the page by tracking upward mouse movement
388
+ * near the top of the viewport. Inspired by Pathfora's showOnExitIntent.
389
+ *
390
+ * **Event-Driven Architecture:**
391
+ * This plugin emits `trigger:exitIntent` events when exit intent is detected.
392
+ * The core runtime listens for these events and automatically re-evaluates experiences.
393
+ *
394
+ * **Usage Pattern:**
395
+ * Use `targeting.custom` to check if exit intent has triggered:
396
+ *
397
+ * @example Basic usage
398
+ * ```typescript
399
+ * import { init, register } from '@prosdevlab/experience-sdk';
400
+ * import { exitIntentPlugin } from '@prosdevlab/experience-sdk-plugins';
401
+ *
402
+ * init({
403
+ * plugins: [exitIntentPlugin],
404
+ * exitIntent: {
405
+ * sensitivity: 20, // Trigger within 20px of top (default: 50)
406
+ * minTimeOnPage: 2000, // Wait 2s before enabling (default: 2000)
407
+ * delay: 0, // Delay after trigger (default: 0)
408
+ * disableOnMobile: true // Disable on mobile (default: true)
409
+ * }
410
+ * });
411
+ *
412
+ * // Show banner only when exit intent is detected
413
+ * register('exit-offer', {
414
+ * type: 'banner',
415
+ * content: {
416
+ * title: 'Wait! Don't leave yet!',
417
+ * message: 'Get 15% off your first order',
418
+ * buttons: [{ text: 'Claim Offer', variant: 'primary' }]
419
+ * },
420
+ * targeting: {
421
+ * custom: (context) => context.triggers?.exitIntent?.triggered === true
422
+ * },
423
+ * frequency: { max: 1, per: 'session' } // Only show once per session
424
+ * });
425
+ * ```
426
+ *
427
+ * @example Combining with other conditions
428
+ * ```typescript
429
+ * // Show exit offer only on shop pages with items in cart
430
+ * register('cart-recovery', {
431
+ * type: 'banner',
432
+ * content: { message: 'Complete your purchase and save!' },
433
+ * targeting: {
434
+ * url: { contains: '/shop' },
435
+ * custom: (context) => {
436
+ * return (
437
+ * context.triggers?.exitIntent?.triggered === true &&
438
+ * getCart().items.length > 0
439
+ * );
440
+ * }
441
+ * }
442
+ * });
443
+ * ```
444
+ *
445
+ * @example Combining multiple triggers (exit intent + scroll depth)
446
+ * ```typescript
447
+ * // Show offer on exit intent OR after 70% scroll
448
+ * register('engaged-exit', {
449
+ * type: 'banner',
450
+ * content: { message: 'You're almost there!' },
451
+ * targeting: {
452
+ * custom: (context) => {
453
+ * const exitIntent = context.triggers?.exitIntent?.triggered;
454
+ * const scrolled = (context.triggers?.scrollDepth?.percent || 0) >= 70;
455
+ * return exitIntent || scrolled;
456
+ * }
457
+ * }
458
+ * });
459
+ * ```
460
+ */
461
+ declare const exitIntentPlugin: PluginFunction;
462
+
195
463
  /**
196
464
  * Frequency Capping Plugin
197
465
  *
@@ -227,4 +495,514 @@ interface FrequencyPlugin {
227
495
  */
228
496
  declare const frequencyPlugin: PluginFunction;
229
497
 
230
- export { type BannerContent, type BannerPlugin, type BannerPluginConfig, type DebugPlugin, type DebugPluginConfig, type Decision, type DecisionMetadata, type Experience, type ExperienceContent, type FrequencyPlugin, type FrequencyPluginConfig, type ModalContent, type TooltipContent, type TraceStep, bannerPlugin, debugPlugin, frequencyPlugin };
498
+ /**
499
+ * Inline Plugin
500
+ *
501
+ * Embeds experiences directly within page content using DOM selectors.
502
+ * Supports multiple insertion positions and dismissal with persistence.
503
+ */
504
+ declare const inlinePlugin: (plugin: any, instance: SDK, config: any) => void;
505
+
506
+ /**
507
+ * Insert content into a target element using specified position
508
+ *
509
+ * @param selector - CSS selector for target element
510
+ * @param content - HTML content to insert
511
+ * @param position - Where to insert the content
512
+ * @param experienceId - Unique identifier for the experience
513
+ * @returns The created wrapper element, or null if target not found
514
+ */
515
+ declare function insertContent(selector: string, content: string, position: InsertionPosition, experienceId: string): HTMLElement | null;
516
+ /**
517
+ * Remove inline content by experience ID
518
+ *
519
+ * @param experienceId - Unique identifier for the experience
520
+ * @returns True if element was found and removed, false otherwise
521
+ */
522
+ declare function removeContent(experienceId: string): boolean;
523
+
524
+ /**
525
+ * Modal Plugin for @prosdevlab/experience-sdk
526
+ *
527
+ * Renders experiences as accessible modal dialogs with:
528
+ * - Focus trap and keyboard handling
529
+ * - ARIA attributes for screen readers
530
+ * - Backdrop and close button
531
+ * - Responsive design
532
+ */
533
+ declare const modalPlugin: (plugin: any, instance: SDK) => void;
534
+
535
+ /**
536
+ * Page Visits Plugin Types
537
+ *
538
+ * Generic page visit tracking for any SDK built on sdk-kit.
539
+ * Tracks session and lifetime visit counts with first-visit detection.
540
+ */
541
+ /**
542
+ * Page visits plugin configuration
543
+ */
544
+ interface PageVisitsPluginConfig {
545
+ pageVisits?: {
546
+ /**
547
+ * Enable/disable page visit tracking
548
+ * @default true
549
+ */
550
+ enabled?: boolean;
551
+ /**
552
+ * Honor Do Not Track browser setting
553
+ * @default true
554
+ */
555
+ respectDNT?: boolean;
556
+ /**
557
+ * Storage key for session count
558
+ * @default 'pageVisits:session'
559
+ */
560
+ sessionKey?: string;
561
+ /**
562
+ * Storage key for lifetime data
563
+ * @default 'pageVisits:total'
564
+ */
565
+ totalKey?: string;
566
+ /**
567
+ * TTL for lifetime data in seconds (GDPR compliance)
568
+ * @default undefined (no expiration)
569
+ */
570
+ ttl?: number;
571
+ /**
572
+ * Automatically increment on plugin load
573
+ * @default true
574
+ */
575
+ autoIncrement?: boolean;
576
+ };
577
+ }
578
+ /**
579
+ * Page visits event payload
580
+ */
581
+ interface PageVisitsEvent {
582
+ /** Whether this is the user's first visit ever */
583
+ isFirstVisit: boolean;
584
+ /** Total visits across all sessions (lifetime) */
585
+ totalVisits: number;
586
+ /** Visits in current session */
587
+ sessionVisits: number;
588
+ /** Timestamp of first visit (unix ms) */
589
+ firstVisitTime?: number;
590
+ /** Timestamp of last visit (unix ms) */
591
+ lastVisitTime?: number;
592
+ /** Timestamp of current visit (unix ms) */
593
+ timestamp: number;
594
+ }
595
+ /**
596
+ * Page visits plugin API
597
+ */
598
+ interface PageVisitsPlugin {
599
+ /**
600
+ * Get total visit count (lifetime)
601
+ */
602
+ getTotalCount(): number;
603
+ /**
604
+ * Get session visit count
605
+ */
606
+ getSessionCount(): number;
607
+ /**
608
+ * Check if this is the first visit
609
+ */
610
+ isFirstVisit(): boolean;
611
+ /**
612
+ * Get timestamp of first visit
613
+ */
614
+ getFirstVisitTime(): number | undefined;
615
+ /**
616
+ * Get timestamp of last visit
617
+ */
618
+ getLastVisitTime(): number | undefined;
619
+ /**
620
+ * Manually increment page visit
621
+ * (useful if autoIncrement is disabled)
622
+ */
623
+ increment(): void;
624
+ /**
625
+ * Reset all counters and data
626
+ * (useful for testing or user opt-out)
627
+ */
628
+ reset(): void;
629
+ /**
630
+ * Get full page visits state
631
+ */
632
+ getState(): PageVisitsEvent;
633
+ }
634
+
635
+ /**
636
+ * Page Visits Plugin
637
+ *
638
+ * Generic page visit tracking for any SDK built on sdk-kit.
639
+ *
640
+ * Features:
641
+ * - Session-scoped counter (sessionStorage)
642
+ * - Lifetime counter with timestamps (localStorage)
643
+ * - First-visit detection
644
+ * - DNT (Do Not Track) support
645
+ * - GDPR-compliant expiration
646
+ * - Auto-loads storage plugin if missing
647
+ *
648
+ * Events emitted:
649
+ * - 'pageVisits:incremented' with PageVisitsEvent
650
+ * - 'pageVisits:reset'
651
+ * - 'pageVisits:disabled' with { reason: 'dnt' | 'config' }
652
+ *
653
+ * @example
654
+ * ```typescript
655
+ * import { SDK } from '@lytics/sdk-kit';
656
+ * import { storagePlugin, pageVisitsPlugin } from '@lytics/sdk-kit-plugins';
657
+ *
658
+ * const sdk = new SDK({
659
+ * pageVisits: {
660
+ * enabled: true,
661
+ * respectDNT: true,
662
+ * ttl: 31536000 // 1 year
663
+ * }
664
+ * });
665
+ *
666
+ * sdk.use(storagePlugin);
667
+ * sdk.use(pageVisitsPlugin);
668
+ *
669
+ * // Listen to visit events
670
+ * sdk.on('pageVisits:incremented', (event) => {
671
+ * console.log('Visit count:', event.totalVisits);
672
+ * if (event.isFirstVisit) {
673
+ * console.log('Welcome, first-time visitor!');
674
+ * }
675
+ * });
676
+ *
677
+ * // API methods
678
+ * console.log(sdk.pageVisits.getTotalCount()); // 5
679
+ * console.log(sdk.pageVisits.getSessionCount()); // 2
680
+ * console.log(sdk.pageVisits.isFirstVisit()); // false
681
+ * ```
682
+ */
683
+
684
+ declare const pageVisitsPlugin: PluginFunction;
685
+
686
+ /** @module scrollDepthPlugin */
687
+
688
+ /**
689
+ * Scroll Depth Plugin
690
+ *
691
+ * Tracks scroll depth and emits `trigger:scrollDepth` events when thresholds are crossed.
692
+ *
693
+ * ## How It Works
694
+ *
695
+ * 1. **Detection**: Listens to `scroll` events (throttled)
696
+ * 2. **Calculation**: Calculates current scroll percentage
697
+ * 3. **Tracking**: Tracks maximum scroll depth and threshold crossings
698
+ * 4. **Emission**: Emits `trigger:scrollDepth` events when thresholds are crossed
699
+ *
700
+ * ## Configuration
701
+ *
702
+ * ```typescript
703
+ * init({
704
+ * scrollDepth: {
705
+ * thresholds: [25, 50, 75, 100], // Percentages to track
706
+ * throttle: 100, // Throttle interval (ms)
707
+ * includeViewportHeight: true, // Calculation method
708
+ * recalculateOnResize: true // Recalculate on resize
709
+ * }
710
+ * });
711
+ * ```
712
+ *
713
+ * ## Experience Targeting
714
+ *
715
+ * ```typescript
716
+ * register('mid-article-cta', {
717
+ * type: 'banner',
718
+ * content: { message: 'Enjoying the article?' },
719
+ * targeting: {
720
+ * custom: (ctx) => (ctx.triggers?.scrollDepth?.percent || 0) >= 50
721
+ * }
722
+ * });
723
+ * ```
724
+ *
725
+ * ## API Methods
726
+ *
727
+ * ```typescript
728
+ * // Get maximum scroll percentage reached
729
+ * instance.scrollDepth.getMaxPercent(); // 73
730
+ *
731
+ * // Get current scroll percentage
732
+ * instance.scrollDepth.getCurrentPercent(); // 50
733
+ *
734
+ * // Get all crossed thresholds
735
+ * instance.scrollDepth.getThresholdsCrossed(); // [25, 50]
736
+ *
737
+ * // Reset tracking (useful for testing)
738
+ * instance.scrollDepth.reset();
739
+ * ```
740
+ *
741
+ * @param plugin Plugin interface from sdk-kit
742
+ * @param instance SDK instance
743
+ * @param config SDK configuration
744
+ */
745
+ declare const scrollDepthPlugin: PluginFunction;
746
+
747
+ /** @module scrollDepthPlugin */
748
+ /**
749
+ * Scroll Depth Plugin API
750
+ */
751
+ interface ScrollDepthPlugin {
752
+ getMaxPercent(): number;
753
+ getCurrentPercent(): number;
754
+ getThresholdsCrossed(): number[];
755
+ getDevice(): 'mobile' | 'tablet' | 'desktop';
756
+ getAdvancedMetrics(): {
757
+ timeOnPage: number;
758
+ directionChanges: number;
759
+ timeScrollingUp: number;
760
+ thresholdTimes: Record<number, number>;
761
+ } | null;
762
+ reset(): void;
763
+ }
764
+ /**
765
+ * Scroll Depth Plugin Configuration
766
+ *
767
+ * Tracks scroll depth and emits trigger:scrollDepth events when thresholds are crossed.
768
+ */
769
+ interface ScrollDepthPluginConfig {
770
+ scrollDepth?: {
771
+ /**
772
+ * Array of scroll percentage thresholds to track (0-100).
773
+ * When user scrolls past a threshold, a trigger:scrollDepth event is emitted.
774
+ * @default [25, 50, 75, 100]
775
+ * @example [50, 100]
776
+ */
777
+ thresholds?: number[];
778
+ /**
779
+ * Throttle interval in milliseconds for scroll event handler.
780
+ * Lower values are more responsive but impact performance.
781
+ * @default 100
782
+ * @example 200
783
+ */
784
+ throttle?: number;
785
+ /**
786
+ * Include viewport height in scroll percentage calculation.
787
+ *
788
+ * - true: (scrollTop + viewportHeight) / totalHeight
789
+ * More intuitive: 100% when bottom of viewport reaches end
790
+ * - false: scrollTop / (totalHeight - viewportHeight)
791
+ * Pathfora's method: 100% when top of viewport reaches end
792
+ *
793
+ * @default true
794
+ */
795
+ includeViewportHeight?: boolean;
796
+ /**
797
+ * Recalculate scroll on window resize.
798
+ * Useful for responsive layouts where content height changes.
799
+ * @default true
800
+ */
801
+ recalculateOnResize?: boolean;
802
+ /**
803
+ * Track advanced metrics (velocity, direction, time-to-threshold).
804
+ * Enables advanced engagement quality analysis.
805
+ * Slight performance overhead but provides rich insights.
806
+ * @default false
807
+ */
808
+ trackAdvancedMetrics?: boolean;
809
+ /**
810
+ * Velocity threshold (px/ms) to consider "fast scrolling".
811
+ * Fast scrolling often indicates skimming rather than reading.
812
+ * Only used when trackAdvancedMetrics is true.
813
+ * @default 3
814
+ */
815
+ fastScrollVelocityThreshold?: number;
816
+ /**
817
+ * Disable scroll tracking on mobile devices.
818
+ * Useful since mobile scroll behavior differs significantly from desktop.
819
+ * @default false
820
+ */
821
+ disableOnMobile?: boolean;
822
+ };
823
+ }
824
+ /**
825
+ * Scroll Depth Event Payload
826
+ *
827
+ * Emitted as `trigger:scrollDepth` when a threshold is crossed.
828
+ */
829
+ interface ScrollDepthEvent {
830
+ /** Whether the trigger has fired */
831
+ triggered: boolean;
832
+ /** Timestamp when the event was emitted */
833
+ timestamp: number;
834
+ /** Current scroll percentage (0-100) */
835
+ percent: number;
836
+ /** Maximum scroll percentage reached during session */
837
+ maxPercent: number;
838
+ /** The threshold that was just crossed */
839
+ threshold: number;
840
+ /** All thresholds that have been triggered */
841
+ thresholdsCrossed: number[];
842
+ /** Device type (mobile, tablet, desktop) */
843
+ device: 'mobile' | 'tablet' | 'desktop';
844
+ /** Advanced metrics (only present when trackAdvancedMetrics is enabled) */
845
+ advanced?: {
846
+ /** Time in milliseconds to reach this threshold from page load */
847
+ timeToThreshold: number;
848
+ /** Current scroll velocity in pixels per millisecond */
849
+ velocity: number;
850
+ /** Whether user is scrolling fast (indicates skimming) */
851
+ isFastScrolling: boolean;
852
+ /** Number of direction changes (up/down) since last threshold */
853
+ directionChanges: number;
854
+ /** Total time spent scrolling up (indicates seeking behavior) */
855
+ timeScrollingUp: number;
856
+ /** Scroll quality score (0-100, higher = more engaged) */
857
+ engagementScore: number;
858
+ };
859
+ }
860
+
861
+ /** @module timeDelayPlugin */
862
+ /**
863
+ * Time Delay Plugin Configuration
864
+ *
865
+ * Tracks time elapsed since SDK initialization and emits trigger:timeDelay events.
866
+ */
867
+ interface TimeDelayPluginConfig {
868
+ timeDelay?: {
869
+ /**
870
+ * Delay before emitting trigger event (milliseconds).
871
+ * Set to 0 to disable (immediate trigger on init).
872
+ * @default 0
873
+ * @example 5000 // 5 seconds
874
+ */
875
+ delay?: number;
876
+ /**
877
+ * Pause timer when tab is hidden (Page Visibility API).
878
+ * When true, only counts "active viewing time".
879
+ * When false, timer runs even when tab is hidden.
880
+ * @default true
881
+ */
882
+ pauseWhenHidden?: boolean;
883
+ };
884
+ }
885
+ /**
886
+ * Time Delay Event Payload
887
+ *
888
+ * Emitted via 'trigger:timeDelay' when the configured delay is reached.
889
+ */
890
+ interface TimeDelayEvent {
891
+ /** Timestamp when the trigger event was emitted */
892
+ timestamp: number;
893
+ /** Total elapsed time since init (milliseconds, includes paused time) */
894
+ elapsed: number;
895
+ /** Active elapsed time (milliseconds, excludes time when tab was hidden) */
896
+ activeElapsed: number;
897
+ /** Whether the timer was paused at any point */
898
+ wasPaused: boolean;
899
+ /** Number of times visibility changed (hidden/visible) */
900
+ visibilityChanges: number;
901
+ }
902
+ /**
903
+ * Time Delay Plugin API
904
+ */
905
+ interface TimeDelayPlugin {
906
+ /**
907
+ * Get total elapsed time since init (includes paused time)
908
+ * @returns Time in milliseconds
909
+ */
910
+ getElapsed(): number;
911
+ /**
912
+ * Get active elapsed time (excludes paused time)
913
+ * @returns Time in milliseconds
914
+ */
915
+ getActiveElapsed(): number;
916
+ /**
917
+ * Get remaining time until trigger
918
+ * @returns Time in milliseconds, or 0 if already triggered
919
+ */
920
+ getRemaining(): number;
921
+ /**
922
+ * Check if timer is currently paused (tab hidden)
923
+ * @returns True if paused
924
+ */
925
+ isPaused(): boolean;
926
+ /**
927
+ * Check if trigger has fired
928
+ * @returns True if triggered
929
+ */
930
+ isTriggered(): boolean;
931
+ /**
932
+ * Reset timer to initial state
933
+ * Clears trigger flag and restarts timing
934
+ */
935
+ reset(): void;
936
+ }
937
+
938
+ /** @module timeDelayPlugin */
939
+
940
+ /**
941
+ * Time Delay Plugin
942
+ *
943
+ * Tracks time elapsed since SDK initialization and emits trigger:timeDelay events
944
+ * when the configured delay is reached.
945
+ *
946
+ * **Features:**
947
+ * - Millisecond precision timing
948
+ * - Pause/resume on tab visibility change (optional)
949
+ * - Tracks active vs total elapsed time
950
+ * - Full timer lifecycle management
951
+ *
952
+ * **Event-Driven Architecture:**
953
+ * This plugin emits `trigger:timeDelay` events when the delay threshold is reached.
954
+ * The core runtime listens for these events and automatically re-evaluates experiences.
955
+ *
956
+ * **Usage Pattern:**
957
+ * Use `targeting.custom` to check if time delay has triggered:
958
+ *
959
+ * @example Basic usage
960
+ * ```typescript
961
+ * import { init, register } from '@prosdevlab/experience-sdk';
962
+ *
963
+ * init({
964
+ * timeDelay: {
965
+ * delay: 5000, // 5 seconds
966
+ * pauseWhenHidden: true // Pause when tab hidden (default)
967
+ * }
968
+ * });
969
+ *
970
+ * // Show banner after 5 seconds of active viewing time
971
+ * register('timed-offer', {
972
+ * type: 'banner',
973
+ * content: {
974
+ * message: 'Limited time offer!',
975
+ * buttons: [{ text: 'Claim Now', variant: 'primary' }]
976
+ * },
977
+ * targeting: {
978
+ * custom: (context) => {
979
+ * const active = context.triggers?.timeDelay?.activeElapsed || 0;
980
+ * return active >= 5000;
981
+ * }
982
+ * }
983
+ * });
984
+ * ```
985
+ *
986
+ * @example Combining with other triggers
987
+ * ```typescript
988
+ * // Show after 10s OR on exit intent (whichever comes first)
989
+ * register('engaged-offer', {
990
+ * type: 'banner',
991
+ * content: { message: 'Special offer for engaged users!' },
992
+ * targeting: {
993
+ * custom: (context) => {
994
+ * const timeElapsed = (context.triggers?.timeDelay?.activeElapsed || 0) >= 10000;
995
+ * const exitIntent = context.triggers?.exitIntent?.triggered;
996
+ * return timeElapsed || exitIntent;
997
+ * }
998
+ * }
999
+ * });
1000
+ * ```
1001
+ *
1002
+ * @param plugin Plugin interface from sdk-kit
1003
+ * @param instance SDK instance
1004
+ * @param config SDK configuration
1005
+ */
1006
+ declare const timeDelayPlugin: PluginFunction;
1007
+
1008
+ export { type BannerContent, type BannerPlugin, type BannerPluginConfig, type DebugPlugin, type DebugPluginConfig, type Decision, type DecisionMetadata, type ExitIntentEvent, type ExitIntentPlugin, type ExitIntentPluginConfig, type Experience, type ExperienceContent, type FormConfig, type FormField, type FormState, type FrequencyPlugin, type FrequencyPluginConfig, type InlineContent$1 as InlineContent, type InlinePlugin, type InlinePluginConfig, type InsertionPosition, type ModalConfig, type ModalContent, type ModalPlugin, type PageVisitsEvent, type PageVisitsPlugin, type PageVisitsPluginConfig, type ScrollDepthEvent, type ScrollDepthPlugin, type ScrollDepthPluginConfig, type TimeDelayEvent, type TimeDelayPlugin, type TimeDelayPluginConfig, type TooltipContent, type TraceStep, type ValidationResult, bannerPlugin, debugPlugin, exitIntentPlugin, frequencyPlugin, inlinePlugin, insertContent, modalPlugin, pageVisitsPlugin, removeContent, scrollDepthPlugin, timeDelayPlugin };