@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,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure form rendering functions
|
|
3
|
+
*
|
|
4
|
+
* These functions are intentionally pure (return DOM elements, no side effects) to make them:
|
|
5
|
+
* - Easy to test
|
|
6
|
+
* - Easy to extract into a separate form plugin later
|
|
7
|
+
* - Reusable across different contexts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExperienceButton } from '../types';
|
|
11
|
+
import {
|
|
12
|
+
getErrorMessageStyles,
|
|
13
|
+
getErrorStateStyles,
|
|
14
|
+
getFieldStyles,
|
|
15
|
+
getFormStateStyles,
|
|
16
|
+
getFormStyles,
|
|
17
|
+
getInputStyles,
|
|
18
|
+
getLabelStyles,
|
|
19
|
+
getRequiredStyles,
|
|
20
|
+
getStateButtonsStyles,
|
|
21
|
+
getStateMessageStyles,
|
|
22
|
+
getStateTitleStyles,
|
|
23
|
+
getSubmitButtonHoverBg,
|
|
24
|
+
getSubmitButtonStyles,
|
|
25
|
+
getSuccessStateStyles,
|
|
26
|
+
} from './form-styles';
|
|
27
|
+
import type { FormConfig, FormField } from './types';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Render complete form element
|
|
31
|
+
*
|
|
32
|
+
* @param experienceId - Experience ID for namespacing field IDs
|
|
33
|
+
* @param config - Form configuration
|
|
34
|
+
* @returns Form HTML element
|
|
35
|
+
*/
|
|
36
|
+
export function renderForm(experienceId: string, config: FormConfig): HTMLFormElement {
|
|
37
|
+
const form = document.createElement('form');
|
|
38
|
+
form.className = 'xp-modal__form';
|
|
39
|
+
form.style.cssText = getFormStyles();
|
|
40
|
+
form.dataset.xpExperienceId = experienceId;
|
|
41
|
+
form.setAttribute('novalidate', ''); // Use custom validation instead of browser default
|
|
42
|
+
|
|
43
|
+
// Render each field
|
|
44
|
+
config.fields.forEach((field) => {
|
|
45
|
+
const fieldElement = renderFormField(experienceId, field);
|
|
46
|
+
form.appendChild(fieldElement);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Render submit button
|
|
50
|
+
const submitButton = renderSubmitButton(config.submitButton);
|
|
51
|
+
form.appendChild(submitButton);
|
|
52
|
+
|
|
53
|
+
return form;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Render a single form field with label and error container
|
|
58
|
+
*
|
|
59
|
+
* @param experienceId - Experience ID for namespacing field ID
|
|
60
|
+
* @param field - Field configuration
|
|
61
|
+
* @returns Field wrapper HTML element
|
|
62
|
+
*/
|
|
63
|
+
export function renderFormField(experienceId: string, field: FormField): HTMLElement {
|
|
64
|
+
const wrapper = document.createElement('div');
|
|
65
|
+
wrapper.className = 'xp-form__field';
|
|
66
|
+
wrapper.style.cssText = getFieldStyles();
|
|
67
|
+
|
|
68
|
+
// Label (optional)
|
|
69
|
+
if (field.label) {
|
|
70
|
+
const label = document.createElement('label');
|
|
71
|
+
label.className = 'xp-form__label';
|
|
72
|
+
label.style.cssText = getLabelStyles();
|
|
73
|
+
label.htmlFor = `${experienceId}-${field.name}`;
|
|
74
|
+
label.textContent = field.label;
|
|
75
|
+
|
|
76
|
+
// Required indicator
|
|
77
|
+
if (field.required) {
|
|
78
|
+
const required = document.createElement('span');
|
|
79
|
+
required.className = 'xp-form__required';
|
|
80
|
+
required.style.cssText = getRequiredStyles();
|
|
81
|
+
required.textContent = ' *';
|
|
82
|
+
required.setAttribute('aria-label', 'required');
|
|
83
|
+
label.appendChild(required);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
wrapper.appendChild(label);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Input element (input or textarea)
|
|
90
|
+
const input =
|
|
91
|
+
field.type === 'textarea'
|
|
92
|
+
? document.createElement('textarea')
|
|
93
|
+
: document.createElement('input');
|
|
94
|
+
|
|
95
|
+
input.className = 'xp-form__input';
|
|
96
|
+
input.style.cssText = getInputStyles();
|
|
97
|
+
input.id = `${experienceId}-${field.name}`;
|
|
98
|
+
input.name = field.name;
|
|
99
|
+
|
|
100
|
+
// Set type for input elements
|
|
101
|
+
if (input instanceof HTMLInputElement) {
|
|
102
|
+
input.type = field.type;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Placeholder
|
|
106
|
+
if (field.placeholder) {
|
|
107
|
+
input.placeholder = field.placeholder;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Required attribute (for screen readers)
|
|
111
|
+
if (field.required) {
|
|
112
|
+
input.required = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Pattern attribute (for HTML5 validation as fallback)
|
|
116
|
+
if (field.pattern && input instanceof HTMLInputElement) {
|
|
117
|
+
input.setAttribute('pattern', field.pattern);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Accessibility attributes
|
|
121
|
+
input.setAttribute('aria-invalid', 'false');
|
|
122
|
+
input.setAttribute('aria-describedby', `${experienceId}-${field.name}-error`);
|
|
123
|
+
|
|
124
|
+
// Custom styling
|
|
125
|
+
if (field.className) {
|
|
126
|
+
input.className += ` ${field.className}`;
|
|
127
|
+
}
|
|
128
|
+
if (field.style) {
|
|
129
|
+
Object.assign(input.style, field.style);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
wrapper.appendChild(input);
|
|
133
|
+
|
|
134
|
+
// Error message container
|
|
135
|
+
const error = document.createElement('div');
|
|
136
|
+
error.className = 'xp-form__error';
|
|
137
|
+
error.style.cssText = getErrorMessageStyles();
|
|
138
|
+
error.id = `${experienceId}-${field.name}-error`;
|
|
139
|
+
error.setAttribute('role', 'alert');
|
|
140
|
+
error.setAttribute('aria-live', 'polite');
|
|
141
|
+
wrapper.appendChild(error);
|
|
142
|
+
|
|
143
|
+
return wrapper;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Render submit button
|
|
148
|
+
*
|
|
149
|
+
* @param buttonConfig - Button configuration
|
|
150
|
+
* @returns Button HTML element
|
|
151
|
+
*/
|
|
152
|
+
export function renderSubmitButton(buttonConfig: ExperienceButton): HTMLButtonElement {
|
|
153
|
+
const button = document.createElement('button');
|
|
154
|
+
button.type = 'submit';
|
|
155
|
+
button.className = 'xp-form__submit xp-modal__button';
|
|
156
|
+
button.style.cssText = getSubmitButtonStyles();
|
|
157
|
+
|
|
158
|
+
// Variant styling
|
|
159
|
+
if (buttonConfig.variant) {
|
|
160
|
+
button.className += ` xp-modal__button--${buttonConfig.variant}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Custom class
|
|
164
|
+
if (buttonConfig.className) {
|
|
165
|
+
button.className += ` ${buttonConfig.className}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Button text
|
|
169
|
+
button.textContent = buttonConfig.text;
|
|
170
|
+
|
|
171
|
+
// Hover effect using CSS variable
|
|
172
|
+
const hoverBg = getSubmitButtonHoverBg();
|
|
173
|
+
button.onmouseover = () => {
|
|
174
|
+
button.style.backgroundColor = hoverBg;
|
|
175
|
+
};
|
|
176
|
+
button.onmouseout = () => {
|
|
177
|
+
button.style.backgroundColor = ''; // Reset to CSS variable default
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Custom styling (applied last to allow overrides)
|
|
181
|
+
if (buttonConfig.style) {
|
|
182
|
+
Object.assign(button.style, buttonConfig.style);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return button;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Render form state (success or error)
|
|
190
|
+
*
|
|
191
|
+
* @param state - State configuration ('success' or 'error')
|
|
192
|
+
* @param stateConfig - State content configuration
|
|
193
|
+
* @returns State HTML element
|
|
194
|
+
*/
|
|
195
|
+
export function renderFormState(
|
|
196
|
+
state: 'success' | 'error',
|
|
197
|
+
stateConfig: { title?: string; message: string; buttons?: ExperienceButton[] }
|
|
198
|
+
): HTMLElement {
|
|
199
|
+
const stateEl = document.createElement('div');
|
|
200
|
+
stateEl.className = `xp-form__state xp-form__state--${state}`;
|
|
201
|
+
|
|
202
|
+
// Base styles + state-specific styles
|
|
203
|
+
const baseStyles = getFormStateStyles();
|
|
204
|
+
const stateStyles = state === 'success' ? getSuccessStateStyles() : getErrorStateStyles();
|
|
205
|
+
stateEl.style.cssText = `${baseStyles}; ${stateStyles}`;
|
|
206
|
+
|
|
207
|
+
// Title (optional)
|
|
208
|
+
if (stateConfig.title) {
|
|
209
|
+
const title = document.createElement('h3');
|
|
210
|
+
title.className = 'xp-form__state-title';
|
|
211
|
+
title.style.cssText = getStateTitleStyles();
|
|
212
|
+
title.textContent = stateConfig.title;
|
|
213
|
+
stateEl.appendChild(title);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Message
|
|
217
|
+
const message = document.createElement('div');
|
|
218
|
+
message.className = 'xp-form__state-message';
|
|
219
|
+
message.style.cssText = getStateMessageStyles();
|
|
220
|
+
message.textContent = stateConfig.message;
|
|
221
|
+
stateEl.appendChild(message);
|
|
222
|
+
|
|
223
|
+
// Buttons (optional)
|
|
224
|
+
if (stateConfig.buttons && stateConfig.buttons.length > 0) {
|
|
225
|
+
const buttonContainer = document.createElement('div');
|
|
226
|
+
buttonContainer.className = 'xp-form__state-buttons';
|
|
227
|
+
buttonContainer.style.cssText = getStateButtonsStyles();
|
|
228
|
+
|
|
229
|
+
stateConfig.buttons.forEach((btnConfig) => {
|
|
230
|
+
const btn = document.createElement('button');
|
|
231
|
+
btn.type = 'button';
|
|
232
|
+
btn.className = 'xp-modal__button';
|
|
233
|
+
|
|
234
|
+
if (btnConfig.variant) {
|
|
235
|
+
btn.className += ` xp-modal__button--${btnConfig.variant}`;
|
|
236
|
+
}
|
|
237
|
+
if (btnConfig.className) {
|
|
238
|
+
btn.className += ` ${btnConfig.className}`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
btn.textContent = btnConfig.text;
|
|
242
|
+
|
|
243
|
+
if (btnConfig.style) {
|
|
244
|
+
Object.assign(btn.style, btnConfig.style);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Store action/dismiss metadata for event handlers
|
|
248
|
+
if (btnConfig.action) {
|
|
249
|
+
btn.dataset.action = btnConfig.action;
|
|
250
|
+
}
|
|
251
|
+
if (btnConfig.dismiss) {
|
|
252
|
+
btn.dataset.dismiss = 'true';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
buttonContainer.appendChild(btn);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
stateEl.appendChild(buttonContainer);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return stateEl;
|
|
262
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form styling with CSS variables for theming
|
|
3
|
+
*
|
|
4
|
+
* Design tokens inspired by Tailwind CSS, 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-form-input-border: #3b82f6;
|
|
11
|
+
* --xp-form-input-focus-ring: rgba(59, 130, 246, 0.2);
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get CSS for form container
|
|
18
|
+
*/
|
|
19
|
+
export function getFormStyles(): string {
|
|
20
|
+
return `
|
|
21
|
+
margin-top: var(--xp-form-spacing, 16px);
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: column;
|
|
24
|
+
gap: var(--xp-form-gap, 16px);
|
|
25
|
+
`.trim();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get CSS for form field wrapper
|
|
30
|
+
*/
|
|
31
|
+
export function getFieldStyles(): string {
|
|
32
|
+
return `
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: var(--xp-field-gap, 6px);
|
|
36
|
+
`.trim();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get CSS for form label
|
|
41
|
+
*/
|
|
42
|
+
export function getLabelStyles(): string {
|
|
43
|
+
return `
|
|
44
|
+
font-size: var(--xp-label-font-size, 14px);
|
|
45
|
+
font-weight: var(--xp-label-font-weight, 500);
|
|
46
|
+
color: var(--xp-label-color, #374151);
|
|
47
|
+
line-height: 1.5;
|
|
48
|
+
`.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get CSS for required indicator
|
|
53
|
+
*/
|
|
54
|
+
export function getRequiredStyles(): string {
|
|
55
|
+
return `
|
|
56
|
+
color: var(--xp-required-color, #ef4444);
|
|
57
|
+
`.trim();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get CSS for form input/textarea
|
|
62
|
+
*/
|
|
63
|
+
export function getInputStyles(): string {
|
|
64
|
+
return `
|
|
65
|
+
padding: var(--xp-input-padding, 8px 12px);
|
|
66
|
+
font-size: var(--xp-input-font-size, 14px);
|
|
67
|
+
line-height: 1.5;
|
|
68
|
+
color: var(--xp-input-color, #111827);
|
|
69
|
+
background-color: var(--xp-input-bg, white);
|
|
70
|
+
border: var(--xp-input-border-width, 1px) solid var(--xp-input-border-color, #d1d5db);
|
|
71
|
+
border-radius: var(--xp-input-radius, 6px);
|
|
72
|
+
transition: all 0.15s ease-in-out;
|
|
73
|
+
outline: none;
|
|
74
|
+
width: 100%;
|
|
75
|
+
box-sizing: border-box;
|
|
76
|
+
`.trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get CSS for input focus state (applies via :focus)
|
|
81
|
+
*/
|
|
82
|
+
export function getInputFocusStyles(): string {
|
|
83
|
+
return `
|
|
84
|
+
border-color: var(--xp-input-focus-border, #3b82f6);
|
|
85
|
+
box-shadow: 0 0 0 var(--xp-input-focus-ring-width, 3px) var(--xp-input-focus-ring, rgba(59, 130, 246, 0.1));
|
|
86
|
+
`.trim();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get CSS for input error state
|
|
91
|
+
*/
|
|
92
|
+
export function getInputErrorStyles(): string {
|
|
93
|
+
return `
|
|
94
|
+
border-color: var(--xp-input-error-border, #ef4444);
|
|
95
|
+
`.trim();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get CSS for error message
|
|
100
|
+
*/
|
|
101
|
+
export function getErrorMessageStyles(): string {
|
|
102
|
+
return `
|
|
103
|
+
font-size: var(--xp-error-font-size, 13px);
|
|
104
|
+
color: var(--xp-error-color, #ef4444);
|
|
105
|
+
line-height: 1.4;
|
|
106
|
+
min-height: 18px;
|
|
107
|
+
`.trim();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get CSS for submit button
|
|
112
|
+
*/
|
|
113
|
+
export function getSubmitButtonStyles(): string {
|
|
114
|
+
return `
|
|
115
|
+
margin-top: var(--xp-submit-margin-top, 8px);
|
|
116
|
+
padding: var(--xp-submit-padding, 10px 20px);
|
|
117
|
+
font-size: var(--xp-submit-font-size, 14px);
|
|
118
|
+
font-weight: var(--xp-submit-font-weight, 500);
|
|
119
|
+
color: var(--xp-submit-color, white);
|
|
120
|
+
background-color: var(--xp-submit-bg, #2563eb);
|
|
121
|
+
border: none;
|
|
122
|
+
border-radius: var(--xp-submit-radius, 6px);
|
|
123
|
+
cursor: pointer;
|
|
124
|
+
transition: all 0.2s;
|
|
125
|
+
width: 100%;
|
|
126
|
+
`.trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get CSS for submit button hover background
|
|
131
|
+
*/
|
|
132
|
+
export function getSubmitButtonHoverBg(): string {
|
|
133
|
+
return 'var(--xp-submit-bg-hover, #1d4ed8)';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get CSS for submit button disabled state
|
|
138
|
+
*/
|
|
139
|
+
export function getSubmitButtonDisabledStyles(): string {
|
|
140
|
+
return `
|
|
141
|
+
opacity: var(--xp-submit-disabled-opacity, 0.6);
|
|
142
|
+
cursor: not-allowed;
|
|
143
|
+
`.trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get CSS for form state container (success/error)
|
|
148
|
+
*/
|
|
149
|
+
export function getFormStateStyles(): string {
|
|
150
|
+
return `
|
|
151
|
+
padding: var(--xp-state-padding, 16px);
|
|
152
|
+
border-radius: var(--xp-state-radius, 8px);
|
|
153
|
+
text-align: center;
|
|
154
|
+
`.trim();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get CSS for success state
|
|
159
|
+
*/
|
|
160
|
+
export function getSuccessStateStyles(): string {
|
|
161
|
+
return `
|
|
162
|
+
background-color: var(--xp-success-bg, #f0fdf4);
|
|
163
|
+
border: var(--xp-state-border-width, 1px) solid var(--xp-success-border, #86efac);
|
|
164
|
+
`.trim();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get CSS for error state
|
|
169
|
+
*/
|
|
170
|
+
export function getErrorStateStyles(): string {
|
|
171
|
+
return `
|
|
172
|
+
background-color: var(--xp-error-bg, #fef2f2);
|
|
173
|
+
border: var(--xp-state-border-width, 1px) solid var(--xp-error-border, #fca5a5);
|
|
174
|
+
`.trim();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get CSS for state title
|
|
179
|
+
*/
|
|
180
|
+
export function getStateTitleStyles(): string {
|
|
181
|
+
return `
|
|
182
|
+
font-size: var(--xp-state-title-font-size, 16px);
|
|
183
|
+
font-weight: var(--xp-state-title-font-weight, 600);
|
|
184
|
+
margin: 0 0 var(--xp-state-title-margin-bottom, 8px) 0;
|
|
185
|
+
color: var(--xp-state-title-color, #111827);
|
|
186
|
+
`.trim();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get CSS for state message
|
|
191
|
+
*/
|
|
192
|
+
export function getStateMessageStyles(): string {
|
|
193
|
+
return `
|
|
194
|
+
font-size: var(--xp-state-message-font-size, 14px);
|
|
195
|
+
line-height: 1.5;
|
|
196
|
+
color: var(--xp-state-message-color, #374151);
|
|
197
|
+
margin: 0;
|
|
198
|
+
`.trim();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get CSS for state buttons container
|
|
203
|
+
*/
|
|
204
|
+
export function getStateButtonsStyles(): string {
|
|
205
|
+
return `
|
|
206
|
+
margin-top: var(--xp-state-buttons-margin-top, 16px);
|
|
207
|
+
display: flex;
|
|
208
|
+
gap: var(--xp-state-buttons-gap, 8px);
|
|
209
|
+
justify-content: center;
|
|
210
|
+
flex-wrap: wrap;
|
|
211
|
+
`.trim();
|
|
212
|
+
}
|