@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.
- package/.turbo/turbo-build.log +6 -6
- package/CHANGELOG.md +120 -0
- package/README.md +141 -79
- package/dist/index.d.ts +206 -35
- package/dist/index.js +1229 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/banner/banner.ts +63 -62
- package/src/exit-intent/exit-intent.ts +2 -3
- package/src/index.ts +2 -0
- package/src/inline/index.ts +3 -0
- package/src/inline/inline.test.ts +620 -0
- package/src/inline/inline.ts +269 -0
- package/src/inline/insertion.ts +66 -0
- package/src/inline/types.ts +52 -0
- package/src/integration.test.ts +356 -297
- package/src/modal/form-rendering.ts +262 -0
- package/src/modal/form-styles.ts +212 -0
- package/src/modal/form-validation.test.ts +413 -0
- package/src/modal/form-validation.ts +126 -0
- package/src/modal/index.ts +3 -0
- package/src/modal/modal-styles.ts +204 -0
- package/src/modal/modal.browser.test.ts +164 -0
- package/src/modal/modal.test.ts +1294 -0
- package/src/modal/modal.ts +685 -0
- package/src/modal/types.ts +114 -0
- package/src/scroll-depth/scroll-depth.test.ts +35 -0
- package/src/scroll-depth/scroll-depth.ts +2 -4
- package/src/time-delay/time-delay.test.ts +2 -2
- package/src/time-delay/time-delay.ts +2 -3
- package/src/types.ts +20 -36
- package/src/utils/sanitize.ts +4 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modal styling with CSS variables for theming
|
|
3
|
+
*
|
|
4
|
+
* Design tokens for modal dialogs, fully customizable via CSS variables.
|
|
5
|
+
* Users can override by setting CSS variables in their stylesheet.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```css
|
|
9
|
+
* :root {
|
|
10
|
+
* --xp-modal-backdrop-bg: rgba(0, 0, 0, 0.7);
|
|
11
|
+
* --xp-modal-dialog-bg: #ffffff;
|
|
12
|
+
* --xp-modal-dialog-radius: 12px;
|
|
13
|
+
* }
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get CSS for modal backdrop
|
|
19
|
+
*/
|
|
20
|
+
export function getBackdropStyles(): string {
|
|
21
|
+
return `
|
|
22
|
+
position: absolute;
|
|
23
|
+
inset: 0;
|
|
24
|
+
background-color: var(--xp-modal-backdrop-bg, rgba(0, 0, 0, 0.5));
|
|
25
|
+
`.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get CSS for modal dialog
|
|
30
|
+
*/
|
|
31
|
+
export function getDialogStyles(params: {
|
|
32
|
+
width: string;
|
|
33
|
+
maxWidth: string;
|
|
34
|
+
height: string;
|
|
35
|
+
maxHeight: string;
|
|
36
|
+
borderRadius: string;
|
|
37
|
+
padding: string;
|
|
38
|
+
}): string {
|
|
39
|
+
return `
|
|
40
|
+
position: relative;
|
|
41
|
+
background: var(--xp-modal-dialog-bg, white);
|
|
42
|
+
border-radius: var(--xp-modal-dialog-radius, ${params.borderRadius});
|
|
43
|
+
box-shadow: var(--xp-modal-dialog-shadow, 0 4px 6px rgba(0, 0, 0, 0.1));
|
|
44
|
+
max-width: ${params.width};
|
|
45
|
+
width: ${params.maxWidth};
|
|
46
|
+
height: ${params.height};
|
|
47
|
+
max-height: ${params.maxHeight};
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
padding: ${params.padding};
|
|
50
|
+
`.trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get CSS for hero image
|
|
55
|
+
*/
|
|
56
|
+
export function getHeroImageStyles(params: { maxHeight: number; borderRadius: string }): string {
|
|
57
|
+
return `
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: auto;
|
|
60
|
+
max-height: ${params.maxHeight}px;
|
|
61
|
+
object-fit: cover;
|
|
62
|
+
border-radius: ${params.borderRadius};
|
|
63
|
+
display: block;
|
|
64
|
+
margin: 0;
|
|
65
|
+
`.trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get CSS for close button
|
|
70
|
+
*/
|
|
71
|
+
export function getCloseButtonStyles(): string {
|
|
72
|
+
return `
|
|
73
|
+
position: absolute;
|
|
74
|
+
top: var(--xp-modal-close-top, 16px);
|
|
75
|
+
right: var(--xp-modal-close-right, 16px);
|
|
76
|
+
background: none;
|
|
77
|
+
border: none;
|
|
78
|
+
font-size: var(--xp-modal-close-size, 24px);
|
|
79
|
+
line-height: 1;
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
padding: var(--xp-modal-close-padding, 4px 8px);
|
|
82
|
+
color: var(--xp-modal-close-color, #666);
|
|
83
|
+
opacity: var(--xp-modal-close-opacity, 0.7);
|
|
84
|
+
transition: opacity 0.2s;
|
|
85
|
+
`.trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Get close button hover opacity
|
|
90
|
+
*/
|
|
91
|
+
export function getCloseButtonHoverOpacity(): string {
|
|
92
|
+
return 'var(--xp-modal-close-hover-opacity, 1)';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Get close button default opacity
|
|
97
|
+
*/
|
|
98
|
+
export function getCloseButtonDefaultOpacity(): string {
|
|
99
|
+
return 'var(--xp-modal-close-opacity, 0.7)';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get CSS for content wrapper
|
|
104
|
+
*/
|
|
105
|
+
export function getContentWrapperStyles(padding: string): string {
|
|
106
|
+
return `padding: ${padding};`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get CSS for modal title
|
|
111
|
+
*/
|
|
112
|
+
export function getTitleStyles(): string {
|
|
113
|
+
return `
|
|
114
|
+
margin: 0 0 var(--xp-modal-title-margin-bottom, 16px) 0;
|
|
115
|
+
font-size: var(--xp-modal-title-size, 20px);
|
|
116
|
+
font-weight: var(--xp-modal-title-weight, 600);
|
|
117
|
+
color: var(--xp-modal-title-color, #111);
|
|
118
|
+
`.trim();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get CSS for modal message
|
|
123
|
+
*/
|
|
124
|
+
export function getMessageStyles(): string {
|
|
125
|
+
return `
|
|
126
|
+
margin: 0 0 var(--xp-modal-message-margin-bottom, 20px) 0;
|
|
127
|
+
font-size: var(--xp-modal-message-size, 14px);
|
|
128
|
+
line-height: var(--xp-modal-message-line-height, 1.5);
|
|
129
|
+
color: var(--xp-modal-message-color, #444);
|
|
130
|
+
`.trim();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get CSS for button container
|
|
135
|
+
*/
|
|
136
|
+
export function getButtonContainerStyles(): string {
|
|
137
|
+
return `
|
|
138
|
+
display: flex;
|
|
139
|
+
gap: var(--xp-modal-buttons-gap, 8px);
|
|
140
|
+
flex-wrap: wrap;
|
|
141
|
+
`.trim();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Get CSS for primary button
|
|
146
|
+
*/
|
|
147
|
+
export function getPrimaryButtonStyles(): string {
|
|
148
|
+
return `
|
|
149
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
150
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
151
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
152
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
transition: all 0.2s;
|
|
155
|
+
border: none;
|
|
156
|
+
background: var(--xp-button-primary-bg, #2563eb);
|
|
157
|
+
color: var(--xp-button-primary-color, white);
|
|
158
|
+
`.trim();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get primary button hover background
|
|
163
|
+
*/
|
|
164
|
+
export function getPrimaryButtonHoverBg(): string {
|
|
165
|
+
return 'var(--xp-button-primary-bg-hover, #1d4ed8)';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Get primary button default background
|
|
170
|
+
*/
|
|
171
|
+
export function getPrimaryButtonDefaultBg(): string {
|
|
172
|
+
return 'var(--xp-button-primary-bg, #2563eb)';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get CSS for secondary button
|
|
177
|
+
*/
|
|
178
|
+
export function getSecondaryButtonStyles(): string {
|
|
179
|
+
return `
|
|
180
|
+
padding: var(--xp-button-padding, 10px 20px);
|
|
181
|
+
font-size: var(--xp-button-font-size, 14px);
|
|
182
|
+
font-weight: var(--xp-button-font-weight, 500);
|
|
183
|
+
border-radius: var(--xp-button-radius, 6px);
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: all 0.2s;
|
|
186
|
+
border: none;
|
|
187
|
+
background: var(--xp-button-secondary-bg, #f3f4f6);
|
|
188
|
+
color: var(--xp-button-secondary-color, #374151);
|
|
189
|
+
`.trim();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Get secondary button hover background
|
|
194
|
+
*/
|
|
195
|
+
export function getSecondaryButtonHoverBg(): string {
|
|
196
|
+
return 'var(--xp-button-secondary-bg-hover, #e5e7eb)';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Get secondary button default background
|
|
201
|
+
*/
|
|
202
|
+
export function getSecondaryButtonDefaultBg(): string {
|
|
203
|
+
return 'var(--xp-button-secondary-bg, #f3f4f6)';
|
|
204
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-specific tests for modal plugin
|
|
3
|
+
* Run with: pnpm test:browser
|
|
4
|
+
*
|
|
5
|
+
* These tests run in a real browser (Chromium via Playwright) to test
|
|
6
|
+
* features that depend on actual browser APIs like window.innerWidth.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { SDK } from '@lytics/sdk-kit';
|
|
10
|
+
import { beforeEach, describe, expect, it } from 'vitest';
|
|
11
|
+
import { modalPlugin } from './modal';
|
|
12
|
+
|
|
13
|
+
// Helper to initialize SDK with modal plugin
|
|
14
|
+
function initPlugin(config = {}) {
|
|
15
|
+
const sdk = new SDK({
|
|
16
|
+
name: 'test-sdk',
|
|
17
|
+
...config,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
sdk.use(modalPlugin);
|
|
21
|
+
return sdk;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('Modal Plugin - Browser Tests', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
// Clean up any existing modals
|
|
27
|
+
document.querySelectorAll('.xp-modal').forEach((el) => {
|
|
28
|
+
el.remove();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Mobile Viewport Detection', () => {
|
|
33
|
+
it('should detect mobile viewport (width < 640px)', () => {
|
|
34
|
+
// This test verifies that window.innerWidth works in the browser environment
|
|
35
|
+
// The actual viewport size is set by the test runner config
|
|
36
|
+
expect(window.innerWidth).toBeGreaterThan(0);
|
|
37
|
+
expect(typeof window.innerWidth).toBe('number');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should auto-enable fullscreen for lg size on mobile viewport @mobile', async () => {
|
|
41
|
+
// Note: @mobile tag would be used with test.describe.each for different viewports
|
|
42
|
+
// For now, we'll test the logic assuming the viewport is set externally
|
|
43
|
+
|
|
44
|
+
// Mock small viewport
|
|
45
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
46
|
+
writable: true,
|
|
47
|
+
configurable: true,
|
|
48
|
+
value: 375,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const sdk = initPlugin({
|
|
52
|
+
modal: {
|
|
53
|
+
size: 'lg',
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await sdk.init();
|
|
57
|
+
|
|
58
|
+
const experience = {
|
|
59
|
+
id: 'lg-mobile',
|
|
60
|
+
type: 'modal' as const,
|
|
61
|
+
targeting: {},
|
|
62
|
+
content: {
|
|
63
|
+
message: 'Large on mobile',
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
sdk.modal.show(experience);
|
|
68
|
+
|
|
69
|
+
// In a real mobile viewport, lg should become fullscreen
|
|
70
|
+
const modal = document.querySelector('.xp-modal');
|
|
71
|
+
const hasFullscreenClass = modal?.classList.contains('xp-modal--fullscreen');
|
|
72
|
+
const hasLgClass = modal?.classList.contains('xp-modal--lg');
|
|
73
|
+
|
|
74
|
+
// Since we're mocking innerWidth, this tests the check logic
|
|
75
|
+
// With innerWidth=375, isMobile() should return true
|
|
76
|
+
expect(modal).toBeTruthy();
|
|
77
|
+
expect(hasFullscreenClass || hasLgClass).toBe(true);
|
|
78
|
+
|
|
79
|
+
// Cleanup
|
|
80
|
+
await sdk.destroy();
|
|
81
|
+
|
|
82
|
+
// Restore
|
|
83
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
84
|
+
writable: true,
|
|
85
|
+
configurable: true,
|
|
86
|
+
value: 1024,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should respect mobileFullscreen: false configuration', async () => {
|
|
91
|
+
// Mock mobile viewport
|
|
92
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
93
|
+
writable: true,
|
|
94
|
+
configurable: true,
|
|
95
|
+
value: 375,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const sdk = initPlugin({
|
|
99
|
+
modal: {
|
|
100
|
+
size: 'lg',
|
|
101
|
+
mobileFullscreen: false,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
await sdk.init();
|
|
105
|
+
|
|
106
|
+
const experience = {
|
|
107
|
+
id: 'lg-no-fullscreen',
|
|
108
|
+
type: 'modal' as const,
|
|
109
|
+
targeting: {},
|
|
110
|
+
content: {
|
|
111
|
+
message: 'Large without fullscreen',
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
sdk.modal.show(experience);
|
|
116
|
+
|
|
117
|
+
const modal = document.querySelector('.xp-modal');
|
|
118
|
+
expect(modal?.classList.contains('xp-modal--lg')).toBe(true);
|
|
119
|
+
|
|
120
|
+
// Cleanup
|
|
121
|
+
await sdk.destroy();
|
|
122
|
+
|
|
123
|
+
// Restore
|
|
124
|
+
Object.defineProperty(window, 'innerWidth', {
|
|
125
|
+
writable: true,
|
|
126
|
+
configurable: true,
|
|
127
|
+
value: 1024,
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('Real Browser APIs', () => {
|
|
133
|
+
it('should access window object', () => {
|
|
134
|
+
expect(window).toBeDefined();
|
|
135
|
+
expect(window.document).toBeDefined();
|
|
136
|
+
expect(window.innerWidth).toBeGreaterThan(0);
|
|
137
|
+
expect(window.innerHeight).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should manipulate DOM', async () => {
|
|
141
|
+
const sdk = initPlugin();
|
|
142
|
+
await sdk.init();
|
|
143
|
+
|
|
144
|
+
const experience = {
|
|
145
|
+
id: 'dom-test',
|
|
146
|
+
type: 'modal' as const,
|
|
147
|
+
targeting: {},
|
|
148
|
+
content: {
|
|
149
|
+
message: 'DOM test',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
sdk.modal.show(experience);
|
|
154
|
+
|
|
155
|
+
// Modal should be in the DOM
|
|
156
|
+
const modal = document.querySelector('.xp-modal');
|
|
157
|
+
expect(modal).toBeInstanceOf(HTMLElement);
|
|
158
|
+
expect(modal?.getAttribute('data-xp-id')).toBe('dom-test');
|
|
159
|
+
|
|
160
|
+
// Cleanup
|
|
161
|
+
await sdk.destroy();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|