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