@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,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
+ });