cf-pagetree-parser 1.0.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,376 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Media Element Parsers
4
+ * ============================================================================
5
+ *
6
+ * Image/V2, Icon/V1, Video/V1, Divider/V1
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ import {
12
+ generateId,
13
+ generateFractionalIndex,
14
+ parseInlineStyle,
15
+ parseValueWithUnit,
16
+ normalizeColor,
17
+ parseAnimationAttrs,
18
+ } from '../utils.js';
19
+
20
+ import {
21
+ parseSpacing,
22
+ spacingToAttrsAndParams,
23
+ parseBorderRadius,
24
+ parseBorder,
25
+ borderToParams,
26
+ parseShadow,
27
+ shadowToParams,
28
+ parseTextAlign,
29
+ parseBackground,
30
+ } from '../styles.js';
31
+
32
+ /**
33
+ * Parse Image/V2
34
+ */
35
+ export function parseImage(element, parentId, index) {
36
+ const id = generateId();
37
+
38
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
39
+ const spacing = parseSpacing(wrapperStyles);
40
+ const textAlign = parseTextAlign(wrapperStyles['text-align']);
41
+
42
+ // Find the img element
43
+ const img = element.querySelector('img');
44
+ const imgStyles = img ? parseInlineStyle(img.getAttribute('style') || '') : {};
45
+
46
+ const src = img ? img.getAttribute('src') : '';
47
+ const alt = img ? img.getAttribute('alt') : '';
48
+
49
+ const width = parseValueWithUnit(imgStyles.width || '100%', '%');
50
+ const height = parseValueWithUnit(imgStyles.height, 'px');
51
+ const borderRadius = parseBorderRadius(imgStyles);
52
+ const border = parseBorder(imgStyles);
53
+ const shadow = parseShadow(imgStyles['box-shadow']);
54
+ const objectFit = imgStyles['object-fit'] || 'cover';
55
+
56
+ // Parse animation attributes
57
+ const { attrs: animationAttrs, params: animationParams } = parseAnimationAttrs(element);
58
+
59
+ const node = {
60
+ type: 'Image/V2',
61
+ id,
62
+ version: 0,
63
+ parentId,
64
+ fractionalIndex: generateFractionalIndex(index),
65
+ attrs: {
66
+ alt,
67
+ style: {
68
+ 'text-align': textAlign,
69
+ },
70
+ ...animationAttrs,
71
+ },
72
+ params: {
73
+ imageUrl: [{ type: 'text', innerText: src }],
74
+ 'padding-top--unit': 'px',
75
+ 'padding-bottom--unit': 'px',
76
+ '--style-padding-horizontal--unit': 'px',
77
+ '--style-padding-horizontal': 0,
78
+ ...animationParams,
79
+ },
80
+ selectors: {
81
+ '.elImage': {
82
+ attrs: {
83
+ style: {
84
+ width: width ? width.value : 100,
85
+ 'object-fit': objectFit,
86
+ },
87
+ 'data-image-quality': 100,
88
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
89
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
90
+ },
91
+ params: {
92
+ 'width--unit': width ? width.unit : '%',
93
+ },
94
+ },
95
+ },
96
+ };
97
+
98
+ // Apply spacing
99
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
100
+ Object.assign(node.attrs.style, spacingAttrs.style);
101
+ Object.assign(node.params, spacingParams);
102
+
103
+ // Apply height if specified
104
+ if (height) {
105
+ node.selectors['.elImage'].attrs.style.height = height.value;
106
+ node.selectors['.elImage'].params['height--unit'] = height.unit;
107
+ }
108
+
109
+ // Apply border-radius
110
+ if (borderRadius) {
111
+ node.selectors['.elImage'].attrs.style['border-radius'] = borderRadius.value;
112
+ node.selectors['.elImage'].params['border-radius--unit'] = borderRadius.unit;
113
+ }
114
+
115
+ // Apply border
116
+ if (border.width || border.style || border.color) {
117
+ Object.assign(node.selectors['.elImage'].params, borderToParams(border));
118
+ }
119
+
120
+ // Apply shadow
121
+ if (shadow) {
122
+ Object.assign(node.selectors['.elImage'].params, shadowToParams(shadow));
123
+ }
124
+
125
+ return node;
126
+ }
127
+
128
+ /**
129
+ * Parse Icon/V1
130
+ */
131
+ export function parseIcon(element, parentId, index) {
132
+ const id = generateId();
133
+
134
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
135
+ const spacing = parseSpacing(wrapperStyles);
136
+ const textAlign = parseTextAlign(wrapperStyles['text-align']);
137
+
138
+ // Find the icon element
139
+ const icon = element.querySelector('i');
140
+ // Prefer data-icon attribute, fallback to class from <i> element
141
+ const iconAttr = element.getAttribute('data-icon');
142
+ const iconClass = iconAttr || (icon ? icon.getAttribute('class') : 'fas fa-star');
143
+ const iconStyles = icon ? parseInlineStyle(icon.getAttribute('style') || '') : {};
144
+
145
+ // Read from data attributes first (most reliable), fallback to inline styles
146
+ const sizeAttr = element.getAttribute('data-size');
147
+ const fontSize = sizeAttr
148
+ ? parseValueWithUnit(sizeAttr)
149
+ : parseValueWithUnit(iconStyles['font-size'] || '48px');
150
+
151
+ const colorAttr = element.getAttribute('data-color');
152
+ const color = colorAttr
153
+ ? normalizeColor(colorAttr)
154
+ : normalizeColor(iconStyles.color || '#3b82f6');
155
+
156
+ const opacityAttr = element.getAttribute('data-opacity');
157
+ const opacity = opacityAttr || iconStyles.opacity;
158
+
159
+ // Parse animation attributes
160
+ const { attrs: animationAttrs, params: animationParams } = parseAnimationAttrs(element);
161
+
162
+ const node = {
163
+ type: 'Icon/V1',
164
+ id,
165
+ version: 0,
166
+ parentId,
167
+ fractionalIndex: generateFractionalIndex(index),
168
+ attrs: {
169
+ style: {},
170
+ ...animationAttrs,
171
+ },
172
+ params: {
173
+ ...animationParams,
174
+ },
175
+ selectors: {
176
+ '.fa_icon': {
177
+ attrs: {
178
+ className: iconClass,
179
+ style: {
180
+ 'font-size': fontSize ? fontSize.value : 48,
181
+ color,
182
+ },
183
+ },
184
+ params: {
185
+ 'font-size--unit': fontSize ? fontSize.unit : 'px',
186
+ },
187
+ },
188
+ '.iconElement': {
189
+ attrs: {
190
+ style: {
191
+ 'text-align': textAlign,
192
+ },
193
+ },
194
+ },
195
+ },
196
+ };
197
+
198
+ // Apply opacity if present
199
+ if (opacity) {
200
+ node.selectors['.fa_icon'].attrs.style.opacity = parseFloat(opacity);
201
+ }
202
+
203
+ // Apply spacing
204
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
205
+ Object.assign(node.attrs.style, spacingAttrs.style);
206
+ Object.assign(node.params, spacingParams);
207
+
208
+ return node;
209
+ }
210
+
211
+ /**
212
+ * Parse Video/V1
213
+ */
214
+ export function parseVideo(element, parentId, index) {
215
+ const id = generateId();
216
+
217
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
218
+ const spacing = parseSpacing(wrapperStyles);
219
+
220
+ // Get video URL and type from data attributes
221
+ const videoUrl = element.getAttribute('data-video-url') || '';
222
+ const videoType = element.getAttribute('data-video-type') || 'youtube';
223
+
224
+ // Find container div for styling
225
+ const container = element.querySelector('div');
226
+ const containerStyles = container ? parseInlineStyle(container.getAttribute('style') || '') : {};
227
+
228
+ const borderRadius = parseBorderRadius(containerStyles);
229
+ const shadow = parseShadow(containerStyles['box-shadow']);
230
+ const border = parseBorder(containerStyles);
231
+ const background = parseBackground(containerStyles);
232
+
233
+ const node = {
234
+ type: 'Video/V1',
235
+ id,
236
+ version: 0,
237
+ parentId,
238
+ fractionalIndex: generateFractionalIndex(index),
239
+ attrs: {
240
+ 'data-video-type': videoType,
241
+ 'data-skip-background-settings': background.color ? 'false' : 'true',
242
+ 'data-skip-shadow-settings': shadow ? 'false' : 'true',
243
+ 'data-skip-corners-settings': borderRadius ? 'false' : 'true',
244
+ style: {},
245
+ },
246
+ params: {
247
+ 'video_url': videoUrl,
248
+ 'padding-top--unit': 'px',
249
+ 'padding-bottom--unit': 'px',
250
+ '--style-padding-horizontal--unit': 'px',
251
+ '--style-padding-horizontal': 0,
252
+ },
253
+ selectors: {},
254
+ };
255
+
256
+ // Apply spacing
257
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
258
+ Object.assign(node.attrs.style, spacingAttrs.style);
259
+ Object.assign(node.params, spacingParams);
260
+
261
+ // Apply border-radius
262
+ if (borderRadius) {
263
+ node.attrs.style['border-radius'] = borderRadius.value;
264
+ node.params['border-radius--unit'] = borderRadius.unit;
265
+ }
266
+
267
+ // Apply shadow
268
+ if (shadow) {
269
+ Object.assign(node.params, shadowToParams(shadow));
270
+ }
271
+
272
+ // Apply border
273
+ if (border.width || border.style || border.color) {
274
+ Object.assign(node.params, borderToParams(border));
275
+ }
276
+
277
+ // Apply background color
278
+ if (background.color) {
279
+ node.params['--style-background-color'] = background.color;
280
+ }
281
+
282
+ return node;
283
+ }
284
+
285
+ /**
286
+ * Parse Divider/V1
287
+ */
288
+ export function parseDivider(element, parentId, index) {
289
+ const id = generateId();
290
+
291
+ const wrapperStyles = parseInlineStyle(element.getAttribute('style') || '');
292
+ const spacing = parseSpacing(wrapperStyles);
293
+
294
+ // Find the divider line element
295
+ const line = element.querySelector('div');
296
+ const lineStyles = line ? parseInlineStyle(line.getAttribute('style') || '') : {};
297
+
298
+ // Parse border-top for thickness, style, and color
299
+ // Format: "1px solid #e2e8f0" or "3px dashed rgb(181, 69, 69)"
300
+ let borderWidth = 1;
301
+ let borderStyle = 'solid';
302
+ let borderColor = '#e2e8f0';
303
+
304
+ if (lineStyles['border-top']) {
305
+ // Match: thickness style color (where color can be hex, rgb, rgba, or named)
306
+ const match = lineStyles['border-top'].match(/^(\d+(?:\.\d+)?px)\s+(solid|dashed|dotted)\s+(.+)$/i);
307
+ if (match) {
308
+ borderWidth = parseFloat(match[1]);
309
+ borderStyle = match[2].toLowerCase();
310
+ borderColor = normalizeColor(match[3].trim());
311
+ }
312
+ }
313
+
314
+ const width = parseValueWithUnit(lineStyles.width || '100%', '%');
315
+
316
+ // Parse alignment from margin
317
+ // "0 auto" = center, "0 auto 0 0" = left, "0 0 0 auto" = right
318
+ let margin = '0 auto';
319
+ if (lineStyles.margin) {
320
+ margin = lineStyles.margin;
321
+ }
322
+
323
+ // Parse box shadow
324
+ const shadow = parseShadow(lineStyles['box-shadow']);
325
+
326
+ // Check data attribute for shadow settings
327
+ const skipShadowSettings = element.getAttribute('data-skip-shadow-settings') !== 'false';
328
+
329
+ const node = {
330
+ type: 'Divider/V1',
331
+ id,
332
+ version: 0,
333
+ parentId,
334
+ fractionalIndex: generateFractionalIndex(index),
335
+ attrs: {
336
+ style: {},
337
+ },
338
+ params: {
339
+ 'padding-top--unit': 'px',
340
+ 'padding-bottom--unit': 'px',
341
+ '--style-padding-horizontal--unit': 'px',
342
+ '--style-padding-horizontal': 0,
343
+ 'margin-top--unit': 'px',
344
+ },
345
+ selectors: {
346
+ '.elDivider': {
347
+ attrs: {
348
+ style: {
349
+ width: width ? width.value : 100,
350
+ margin: margin,
351
+ },
352
+ 'data-skip-shadow-settings': skipShadowSettings ? 'true' : 'false',
353
+ },
354
+ params: {
355
+ 'width--unit': width ? width.unit : '%',
356
+ '--style-border-top-width': borderWidth,
357
+ '--style-border-top-width--unit': 'px',
358
+ '--style-border-style': borderStyle,
359
+ '--style-border-color': borderColor,
360
+ },
361
+ },
362
+ },
363
+ };
364
+
365
+ // Apply spacing
366
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
367
+ Object.assign(node.attrs.style, spacingAttrs.style);
368
+ Object.assign(node.params, spacingParams);
369
+
370
+ // Apply shadow if present
371
+ if (shadow) {
372
+ Object.assign(node.selectors['.elDivider'].params, shadowToParams(shadow));
373
+ }
374
+
375
+ return node;
376
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Placeholder Parsers
4
+ * ============================================================================
5
+ *
6
+ * Parsers for placeholder elements that output actual ClickFunnels element types.
7
+ * These placeholders render as visual boxes in FunnelWind but output the
8
+ * proper ClickFunnels element types in the pagetree for seamless import.
9
+ *
10
+ * - CheckoutPlaceholder → Checkout/V2
11
+ * - OrderSummaryPlaceholder → CheckoutOrderSummary/V1
12
+ * - ConfirmationPlaceholder → OrderConfirmation/V1
13
+ *
14
+ * ============================================================================
15
+ */
16
+
17
+ import {
18
+ generateId,
19
+ generateFractionalIndex,
20
+ parseInlineStyle,
21
+ } from '../utils.js';
22
+
23
+ import { parseSpacing, spacingToAttrsAndParams } from '../styles.js';
24
+
25
+ /**
26
+ * Parse CheckoutPlaceholder
27
+ * Outputs a Checkout/V2 element - the actual ClickFunnels checkout form
28
+ */
29
+ export function parseCheckoutPlaceholder(element, parentId, index) {
30
+ const id = generateId();
31
+ const tosTextId = generateId();
32
+ const tosTextInner = generateId();
33
+ const ctaHeaderId = generateId();
34
+ const ctaHeaderInner = generateId();
35
+
36
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
37
+ const spacing = parseSpacing(styles);
38
+
39
+ const node = {
40
+ type: 'Checkout/V2',
41
+ id,
42
+ version: 0,
43
+ parentId,
44
+ fractionalIndex: generateFractionalIndex(index),
45
+ attrs: {
46
+ style: {
47
+ '--container-font-family': 'var(--style-guide-font-family-content)',
48
+ '--input-headline-font-family': 'var(--style-guide-font-family-subheadline)',
49
+ '--multiple-payments-font-family': 'sans-serif',
50
+ '--input-background-color': '#FFFFFF',
51
+ },
52
+ },
53
+ selectors: {
54
+ '.elButton': {
55
+ params: {
56
+ '--style-border-style': 'solid',
57
+ 'border-radius--unit': 'px',
58
+ },
59
+ attrs: {
60
+ style: {
61
+ 'border-style': 'none',
62
+ 'border-radius': 6,
63
+ },
64
+ 'data-skip-corners-settings': 'false',
65
+ },
66
+ },
67
+ },
68
+ children: [
69
+ {
70
+ type: 'p',
71
+ slotName: 'tos-text',
72
+ id: tosTextId,
73
+ version: 0,
74
+ parentId: id,
75
+ fractionalIndex: 'a0',
76
+ children: [
77
+ {
78
+ type: 'text',
79
+ innerText: 'By completing this purchase, you agree to our terms of service.',
80
+ id: tosTextInner,
81
+ version: 0,
82
+ parentId: tosTextId,
83
+ fractionalIndex: 'a0',
84
+ },
85
+ ],
86
+ },
87
+ {
88
+ type: 'p',
89
+ slotName: 'cta-header',
90
+ id: ctaHeaderId,
91
+ version: 0,
92
+ parentId: id,
93
+ fractionalIndex: 'a1',
94
+ children: [
95
+ {
96
+ type: 'text',
97
+ innerText: 'Complete Your Order Today!',
98
+ id: ctaHeaderInner,
99
+ version: 0,
100
+ parentId: ctaHeaderId,
101
+ fractionalIndex: 'a0',
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ };
107
+
108
+ // Apply spacing
109
+ const { attrs: spacingAttrs } = spacingToAttrsAndParams(spacing);
110
+ if (spacingAttrs.style) {
111
+ Object.assign(node.attrs.style, spacingAttrs.style);
112
+ }
113
+
114
+ return node;
115
+ }
116
+
117
+ /**
118
+ * Parse OrderSummaryPlaceholder
119
+ * Outputs a CheckoutOrderSummary/V1 element - displays cart items and totals
120
+ */
121
+ export function parseOrderSummaryPlaceholder(element, parentId, index) {
122
+ const id = generateId();
123
+
124
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
125
+ const spacing = parseSpacing(styles);
126
+
127
+ const node = {
128
+ type: 'CheckoutOrderSummary/V1',
129
+ id,
130
+ version: 0,
131
+ parentId,
132
+ fractionalIndex: generateFractionalIndex(index),
133
+ params: {
134
+ open: true,
135
+ state: 'ok',
136
+ linkWithCheckout: true,
137
+ // linkedCheckoutId will need to be set manually in ClickFunnels to link to the checkout
138
+ },
139
+ };
140
+
141
+ // Apply spacing if present
142
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
143
+ if (spacingAttrs.style && Object.keys(spacingAttrs.style).length > 0) {
144
+ node.attrs = { style: spacingAttrs.style };
145
+ }
146
+ if (Object.keys(spacingParams).length > 0) {
147
+ Object.assign(node.params, spacingParams);
148
+ }
149
+
150
+ return node;
151
+ }
152
+
153
+ /**
154
+ * Parse ConfirmationPlaceholder
155
+ * Outputs an OrderConfirmation/V1 element - shows purchase receipt
156
+ */
157
+ export function parseConfirmationPlaceholder(element, parentId, index) {
158
+ const id = generateId();
159
+
160
+ const styles = parseInlineStyle(element.getAttribute('style') || '');
161
+ const spacing = parseSpacing(styles);
162
+
163
+ const node = {
164
+ type: 'OrderConfirmation/V1',
165
+ id,
166
+ version: 0,
167
+ parentId,
168
+ fractionalIndex: generateFractionalIndex(index),
169
+ selectors: {
170
+ '.elOrderConfirmationV1': {
171
+ attrs: {
172
+ 'data-skip-corners-settings': 'false',
173
+ style: {
174
+ 'border-radius': '8px',
175
+ },
176
+ },
177
+ params: {
178
+ '--style-border-width': '1px',
179
+ '--style-border-style': 'solid',
180
+ '--style-border-color': '#ECF0F5',
181
+ },
182
+ },
183
+ },
184
+ };
185
+
186
+ // Apply spacing if present
187
+ const { attrs: spacingAttrs, params: spacingParams } = spacingToAttrsAndParams(spacing);
188
+ if (spacingAttrs.style && Object.keys(spacingAttrs.style).length > 0) {
189
+ node.attrs = { style: spacingAttrs.style };
190
+ }
191
+ if (Object.keys(spacingParams).length > 0) {
192
+ node.params = spacingParams;
193
+ }
194
+
195
+ return node;
196
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * ============================================================================
3
+ * PAGETREE PARSER - Popup Parser
4
+ * ============================================================================
5
+ *
6
+ * Parses cf-popup (ModalContainer/V1) elements for the popup property.
7
+ *
8
+ * ============================================================================
9
+ */
10
+
11
+ import {
12
+ generateId,
13
+ parseInlineStyle,
14
+ parseValueWithUnit,
15
+ } from '../utils.js';
16
+
17
+ import {
18
+ parseShadow,
19
+ shadowToParams,
20
+ } from '../styles.js';
21
+
22
+ /**
23
+ * Parse ModalContainer (popup) from cf-popup rendered HTML
24
+ *
25
+ * @param {HTMLElement} element - The popup wrapper element with data-type="ModalContainer/V1"
26
+ * @param {function} parseChildren - Callback to parse child elements
27
+ * @returns {Object} The ModalContainer/V1 pagetree node
28
+ */
29
+ export function parseModalContainer(element, parseChildren) {
30
+ const id = generateId();
31
+
32
+ // Get popup settings from data attributes
33
+ const width = element.getAttribute('data-popup-width') || '750px';
34
+ const overlay = element.getAttribute('data-popup-overlay') || 'rgba(0,0,0,0.5)';
35
+ const rounded = element.getAttribute('data-popup-rounded') || '16px';
36
+ const border = element.getAttribute('data-popup-border');
37
+ const borderColor = element.getAttribute('data-popup-border-color') || '#000000';
38
+ const shadowAttr = element.getAttribute('data-popup-shadow');
39
+
40
+ // Find the modal container to get actual styles
41
+ const modalEl = element.querySelector('.cf-popup-modal, .containerModal');
42
+ const modalStyles = modalEl ? parseInlineStyle(modalEl.getAttribute('style') || '') : {};
43
+
44
+ // Find the inner container for width
45
+ const innerEl = element.querySelector('.elModalInnerContainer');
46
+
47
+ // Parse values
48
+ const parsedWidth = parseValueWithUnit(width, 'px');
49
+ const parsedRounded = parseValueWithUnit(rounded, 'px');
50
+ const parsedMt = parseValueWithUnit(modalStyles['margin-top'] || '45px', 'px');
51
+ const parsedMb = parseValueWithUnit(modalStyles['margin-bottom'] || '10px', 'px');
52
+
53
+ // Build the node
54
+ const node = {
55
+ id,
56
+ version: 0,
57
+ type: 'ModalContainer/V1',
58
+ selectors: {
59
+ '.containerModal': {
60
+ attrs: {
61
+ 'data-skip-corners-settings': parsedRounded ? 'false' : 'true',
62
+ 'data-skip-shadow-settings': shadowAttr ? 'false' : 'true',
63
+ style: {
64
+ 'margin-bottom': parsedMb ? parsedMb.value : 10,
65
+ 'margin-top': parsedMt ? parsedMt.value : 45,
66
+ },
67
+ },
68
+ params: {
69
+ 'margin-bottom--unit': parsedMb ? parsedMb.unit : 'px',
70
+ 'margin-top--unit': parsedMt ? parsedMt.unit : 'px',
71
+ },
72
+ },
73
+ '.modal-wrapper': {
74
+ params: {
75
+ '--style-background-color': overlay,
76
+ '--style-padding-horizontal--unit': 'px',
77
+ '--style-padding-horizontal': 0,
78
+ },
79
+ },
80
+ '.elModalInnerContainer': {
81
+ params: {
82
+ 'width--unit': parsedWidth ? parsedWidth.unit : 'px',
83
+ },
84
+ attrs: {
85
+ style: {
86
+ width: parsedWidth ? parsedWidth.value : 750,
87
+ },
88
+ },
89
+ },
90
+ },
91
+ children: [],
92
+ };
93
+
94
+ // Apply border-radius
95
+ if (parsedRounded) {
96
+ node.selectors['.containerModal'].attrs.style['border-radius'] = parsedRounded.value;
97
+ node.selectors['.containerModal'].params['border-radius--unit'] = parsedRounded.unit;
98
+ }
99
+
100
+ // Apply border if present
101
+ if (border) {
102
+ const parsedBorder = parseValueWithUnit(border, 'px');
103
+ if (parsedBorder) {
104
+ node.selectors['.containerModal'].params['--style-border-style'] = 'solid';
105
+ node.selectors['.containerModal'].params['--style-border-width'] = parsedBorder.value;
106
+ node.selectors['.containerModal'].params['--style-border-width--unit'] = parsedBorder.unit;
107
+ node.selectors['.containerModal'].params['--style-border-color'] = borderColor;
108
+ }
109
+ }
110
+
111
+ // Apply shadow if present
112
+ if (shadowAttr) {
113
+ const shadow = parseShadow(modalStyles['box-shadow']);
114
+ if (shadow) {
115
+ Object.assign(node.selectors['.containerModal'].params, shadowToParams(shadow));
116
+ }
117
+ }
118
+
119
+ // Parse children (sections inside the popup)
120
+ // Look for sections inside the inner container
121
+ const sectionsContainer = innerEl || modalEl;
122
+ if (sectionsContainer) {
123
+ const sectionChildren = sectionsContainer.querySelectorAll(':scope > section, :scope > [data-type="SectionContainer/V1"]');
124
+ sectionChildren.forEach((child, index) => {
125
+ const childNode = parseChildren(child, id, index);
126
+ if (childNode) {
127
+ node.children.push(childNode);
128
+ }
129
+ });
130
+ }
131
+
132
+ return node;
133
+ }