@prosdevlab/experience-sdk-plugins 0.2.0 → 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.
@@ -0,0 +1,114 @@
1
+ import type { PluginFunction } from '@lytics/sdk-kit';
2
+ import type { ExperienceButton } from '../types';
3
+
4
+ export interface ModalConfig {
5
+ modal?: {
6
+ /** Allow dismissal via close button (default: true) */
7
+ dismissable?: boolean;
8
+ /** Allow dismissal via backdrop click (default: true) */
9
+ backdropDismiss?: boolean;
10
+ /** Z-index for modal (default: 10001) */
11
+ zIndex?: number;
12
+ /** Modal size (default: 'md') */
13
+ size?: 'sm' | 'md' | 'lg' | 'fullscreen' | 'auto';
14
+ /** Auto-fullscreen on mobile screens <640px (default: true for 'lg', false for others) */
15
+ mobileFullscreen?: boolean;
16
+ /** Modal position (default: 'center') */
17
+ position?: 'center' | 'bottom';
18
+ /** Animation type (default: 'fade') */
19
+ animation?: 'fade' | 'slide-up' | 'none';
20
+ /** Animation duration in ms (default: 200) */
21
+ animationDuration?: number;
22
+ };
23
+ }
24
+
25
+ export interface ModalContent {
26
+ /** Optional hero image at top of modal */
27
+ image?: {
28
+ /** Image source URL */
29
+ src: string;
30
+ /** Alt text for accessibility */
31
+ alt: string;
32
+ /** Max height in pixels (default: 300, 200 on mobile) */
33
+ maxHeight?: number;
34
+ };
35
+ /** Modal title */
36
+ title?: string;
37
+ /** Modal message (supports HTML via sanitizer) */
38
+ message: string;
39
+ /** Array of action buttons */
40
+ buttons?: ExperienceButton[];
41
+ /** Optional form configuration */
42
+ form?: FormConfig;
43
+ /** Custom CSS class */
44
+ className?: string;
45
+ /** Inline styles */
46
+ style?: Record<string, string>;
47
+ }
48
+
49
+ export interface FormConfig {
50
+ /** Array of form fields */
51
+ fields: FormField[];
52
+ /** Submit button configuration */
53
+ submitButton: ExperienceButton;
54
+ /** Success state after submission */
55
+ successState?: FormState;
56
+ /** Error state on submission failure */
57
+ errorState?: FormState;
58
+ /** Custom validation function (optional) */
59
+ validate?: (data: Record<string, string>) => ValidationResult;
60
+ }
61
+
62
+ export interface FormField {
63
+ /** Field name (used in form data) */
64
+ name: string;
65
+ /** Field type */
66
+ type: 'email' | 'text' | 'textarea' | 'tel' | 'url' | 'number';
67
+ /** Label text (optional) */
68
+ label?: string;
69
+ /** Placeholder text */
70
+ placeholder?: string;
71
+ /** Required field (default: false) */
72
+ required?: boolean;
73
+ /** Custom validation pattern (regex) */
74
+ pattern?: string;
75
+ /** Error message for validation failure */
76
+ errorMessage?: string;
77
+ /** Custom CSS class */
78
+ className?: string;
79
+ /** Inline styles */
80
+ style?: Record<string, string>;
81
+ }
82
+
83
+ export interface FormState {
84
+ /** Title to show in success/error state */
85
+ title?: string;
86
+ /** Message to show */
87
+ message: string;
88
+ /** Optional buttons (e.g., "Close", "Try Again") */
89
+ buttons?: ExperienceButton[];
90
+ }
91
+
92
+ export interface ValidationResult {
93
+ /** Whether validation passed */
94
+ valid: boolean;
95
+ /** Validation errors by field name */
96
+ errors?: Record<string, string>;
97
+ }
98
+
99
+ export interface ModalPlugin {
100
+ /** Show a modal experience */
101
+ show(experience: any): void;
102
+ /** Remove a specific modal */
103
+ remove(experienceId: string): void;
104
+ /** Check if a modal is showing */
105
+ isShowing(experienceId?: string): boolean;
106
+ /** Show form success or error state */
107
+ showFormState(experienceId: string, state: 'success' | 'error'): void;
108
+ /** Reset form to initial state */
109
+ resetForm(experienceId: string): void;
110
+ /** Get current form data */
111
+ getFormData(experienceId: string): Record<string, string> | null;
112
+ }
113
+
114
+ export type { PluginFunction };
@@ -501,6 +501,41 @@ describe('scrollDepthPlugin', () => {
501
501
  });
502
502
  });
503
503
 
504
+ describe('reset()', () => {
505
+ it('should clear triggered thresholds and max scroll', async () => {
506
+ const emitSpy = vi.fn();
507
+
508
+ await initPlugin({ thresholds: [25, 50, 75] });
509
+ sdk.on('trigger:scrollDepth', emitSpy);
510
+ vi.advanceTimersByTime(0);
511
+
512
+ // Scroll to 50%
513
+ simulateScroll(1000, 3000, 1000);
514
+ vi.advanceTimersByTime(200);
515
+
516
+ // Should have triggered 25% and 50%
517
+ expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
518
+ expect(sdk.scrollDepth.getMaxPercent()).toBeGreaterThan(0);
519
+ expect(emitSpy).toHaveBeenCalledTimes(2);
520
+
521
+ // Reset
522
+ sdk.scrollDepth.reset();
523
+
524
+ // Should clear state
525
+ expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([]);
526
+ expect(sdk.scrollDepth.getMaxPercent()).toBe(0);
527
+
528
+ // Scroll again to 50% should trigger again
529
+ emitSpy.mockClear();
530
+ simulateScroll(1000, 3000, 1000);
531
+ vi.advanceTimersByTime(200);
532
+
533
+ // Should trigger both 25% and 50% again
534
+ expect(emitSpy).toHaveBeenCalledTimes(2);
535
+ expect(sdk.scrollDepth.getThresholdsCrossed()).toEqual([25, 50]);
536
+ });
537
+ });
538
+
504
539
  describe('Pathfora compatibility tests', () => {
505
540
  it('should match Pathfora test: scrollPercentageToDisplay 50', async () => {
506
541
  const emitSpy = vi.fn();
@@ -329,10 +329,9 @@ export const scrollDepthPlugin: PluginFunction = (plugin, instance, config) => {
329
329
  }
330
330
 
331
331
  // Setup destroy handler
332
- const destroyHandler = () => {
332
+ instance.on('sdk:destroy', () => {
333
333
  cleanup();
334
- };
335
- instance.on('destroy', destroyHandler);
334
+ });
336
335
 
337
336
  // Expose API
338
337
  plugin.expose({
@@ -395,6 +394,5 @@ export const scrollDepthPlugin: PluginFunction = (plugin, instance, config) => {
395
394
  // Return cleanup function
396
395
  return () => {
397
396
  cleanup();
398
- instance.off('destroy', destroyHandler);
399
397
  };
400
398
  };
@@ -380,7 +380,7 @@ describe('Time Delay Plugin', () => {
380
380
  vi.advanceTimersByTime(2000);
381
381
 
382
382
  // Destroy SDK
383
- sdk.emit('destroy');
383
+ await sdk.destroy();
384
384
 
385
385
  // Advance past trigger time
386
386
  vi.advanceTimersByTime(5000);
@@ -396,7 +396,7 @@ describe('Time Delay Plugin', () => {
396
396
  const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
397
397
 
398
398
  // Destroy SDK
399
- sdk.emit('destroy');
399
+ await sdk.destroy();
400
400
 
401
401
  // Should have removed visibility listener
402
402
  expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
@@ -290,8 +290,7 @@ export const timeDelayPlugin: PluginFunction = (plugin, instance, config) => {
290
290
  initialize();
291
291
 
292
292
  // Cleanup on instance destroy
293
- const destroyHandler = () => {
293
+ instance.on('sdk:destroy', () => {
294
294
  cleanup();
295
- };
296
- instance.on('destroy', destroyHandler);
295
+ });
297
296
  };
package/src/types.ts CHANGED
@@ -3,10 +3,25 @@
3
3
  * These types are re-exported by core for user convenience
4
4
  */
5
5
 
6
+ import type { InlineContent as _InlineContent } from './inline/types';
7
+ // Import modal and inline content types from their plugins
8
+ import type { ModalContent as _ModalContent } from './modal/types';
9
+ export type ModalContent = _ModalContent;
10
+ export type InlineContent = _InlineContent;
11
+
6
12
  /**
7
- * Experience content - varies by type
13
+ * Experience button configuration (used across all experience types)
8
14
  */
9
- export type ExperienceContent = BannerContent | ModalContent | TooltipContent;
15
+ export interface ExperienceButton {
16
+ text: string;
17
+ action?: string;
18
+ url?: string;
19
+ variant?: 'primary' | 'secondary' | 'link';
20
+ dismiss?: boolean;
21
+ metadata?: Record<string, any>;
22
+ className?: string;
23
+ style?: Record<string, string>;
24
+ }
10
25
 
11
26
  /**
12
27
  * Banner content configuration
@@ -14,31 +29,13 @@ export type ExperienceContent = BannerContent | ModalContent | TooltipContent;
14
29
  export interface BannerContent {
15
30
  title?: string;
16
31
  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
- }>;
32
+ buttons?: ExperienceButton[];
26
33
  dismissable?: boolean;
27
34
  position?: 'top' | 'bottom';
28
35
  className?: string;
29
36
  style?: Record<string, string>;
30
37
  }
31
38
 
32
- /**
33
- * Modal content configuration
34
- */
35
- export interface ModalContent {
36
- title: string;
37
- message: string;
38
- confirmText?: string;
39
- cancelText?: string;
40
- }
41
-
42
39
  /**
43
40
  * Tooltip content configuration
44
41
  */
@@ -48,22 +45,9 @@ export interface TooltipContent {
48
45
  }
49
46
 
50
47
  /**
51
- * Modal content configuration
52
- */
53
- export interface ModalContent {
54
- title: string;
55
- message: string;
56
- confirmText?: string;
57
- cancelText?: string;
58
- }
59
-
60
- /**
61
- * Tooltip content configuration
48
+ * Experience content - varies by type
62
49
  */
63
- export interface TooltipContent {
64
- message: string;
65
- position?: 'top' | 'bottom' | 'left' | 'right';
66
- }
50
+ export type ExperienceContent = BannerContent | ModalContent | InlineContent | TooltipContent;
67
51
 
68
52
  /**
69
53
  * Experience definition
@@ -11,7 +11,7 @@
11
11
  * Allowed HTML tags for sanitization
12
12
  * Only safe formatting tags are permitted
13
13
  */
14
- const ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p'] as const;
14
+ const ALLOWED_TAGS = ['strong', 'em', 'a', 'br', 'span', 'b', 'i', 'p', 'div', 'ul', 'li'] as const;
15
15
 
16
16
  /**
17
17
  * Allowed attributes per tag
@@ -20,6 +20,9 @@ const ALLOWED_ATTRIBUTES: Record<string, string[]> = {
20
20
  a: ['href', 'class', 'style', 'title'],
21
21
  span: ['class', 'style'],
22
22
  p: ['class', 'style'],
23
+ div: ['class', 'style'],
24
+ ul: ['class', 'style'],
25
+ li: ['class', 'style'],
23
26
  // Other tags have no attributes allowed
24
27
  };
25
28