cf-elements 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.
Files changed (3) hide show
  1. package/README.md +104 -0
  2. package/cf-elements.js +4607 -0
  3. package/package.json +33 -0
package/cf-elements.js ADDED
@@ -0,0 +1,4607 @@
1
+ /**
2
+ * ============================================================================
3
+ * FUNNELWIND WEB COMPONENTS - cf-elements.js
4
+ * ============================================================================
5
+ *
6
+ * A zero-dependency Web Components library that generates ClickFunnels
7
+ * compatible HTML with inline styles. No CSS framework required.
8
+ *
9
+ * USAGE:
10
+ * <script src="cf-elements.js"></script>
11
+ *
12
+ * <cf-page bg="#ffffff">
13
+ * <cf-section pt="80px" pb="80px" bg="#0f172a">
14
+ * <cf-row width="wide">
15
+ * <cf-col span="12">
16
+ * <cf-headline size="48px" color="#ffffff" align="center">
17
+ * Hello World
18
+ * </cf-headline>
19
+ * </cf-col>
20
+ * </cf-row>
21
+ * </cf-section>
22
+ * </cf-page>
23
+ *
24
+ * OUTPUT: Clean HTML with data-type attributes for ClickFunnels conversion
25
+ *
26
+ * ============================================================================
27
+ * CLICKFUNNELS LIMITATIONS (enforced by this library)
28
+ * ============================================================================
29
+ *
30
+ * - NO margin-bottom (only margin-top via mt attribute)
31
+ * - Horizontal padding is unified (px, not pl/pr separately)
32
+ * - FontAwesome OLD format only: "fas fa-check" not "fa-solid fa-check"
33
+ * - Line heights as percentages: 100%, 110%, 120%, 140%, 160%, 180%
34
+ * - FlexContainer always centered (margin: 0 auto)
35
+ * - All borders share same width/style on all sides
36
+ * - Only ONE shadow allowed (no multiple shadows)
37
+ * - Sections always horizontally centered, can use 100% width fullContainer
38
+ *
39
+ * ============================================================================
40
+ */
41
+
42
+ (function () {
43
+ "use strict";
44
+
45
+ // ==========================================================================
46
+ // LOADING STATE - Prevent Flash of Unstyled Content (FOUC)
47
+ // ==========================================================================
48
+
49
+ // Inject loading state CSS immediately to hide content until rendered
50
+ (function injectLoadingStyles() {
51
+ const style = document.createElement('style');
52
+ style.id = 'cf-loading-styles';
53
+ style.textContent = `
54
+ /* Hide cf-page until rendered to prevent FOUC */
55
+ cf-page:not([data-rendered="true"]) {
56
+ opacity: 0 !important;
57
+ }
58
+ cf-page[data-rendered="true"] {
59
+ opacity: 1;
60
+ transition: opacity 0.1s ease-in;
61
+ }
62
+ `;
63
+ // Insert at the start of head to ensure it's applied immediately
64
+ if (document.head) {
65
+ document.head.insertBefore(style, document.head.firstChild);
66
+ } else {
67
+ document.addEventListener('DOMContentLoaded', () => {
68
+ document.head.insertBefore(style, document.head.firstChild);
69
+ });
70
+ }
71
+ })();
72
+
73
+ // ==========================================================================
74
+ // CONSTANTS & MAPPINGS
75
+ // ==========================================================================
76
+
77
+ /**
78
+ * Shadow presets matching ClickFunnels
79
+ */
80
+ const SHADOWS = {
81
+ none: "none",
82
+ sm: "0 1px 2px rgba(0,0,0,0.05)",
83
+ default: "0 1px 3px rgba(0,0,0,0.1)",
84
+ md: "0 4px 6px rgba(0,0,0,0.1)",
85
+ lg: "0 10px 15px rgba(0,0,0,0.1)",
86
+ xl: "0 20px 25px rgba(0,0,0,0.1)",
87
+ "2xl": "0 25px 50px rgba(0,0,0,0.25)",
88
+ };
89
+
90
+ /**
91
+ * Border radius presets
92
+ */
93
+ const RADIUS = {
94
+ none: "0",
95
+ sm: "4px",
96
+ default: "8px",
97
+ md: "12px",
98
+ lg: "16px",
99
+ xl: "20px",
100
+ "2xl": "24px",
101
+ "3xl": "32px",
102
+ full: "9999px",
103
+ };
104
+
105
+ /**
106
+ * Line height presets (percentages for ClickFunnels)
107
+ */
108
+ const LINE_HEIGHTS = {
109
+ none: "100%",
110
+ tight: "110%",
111
+ snug: "120%",
112
+ normal: "140%",
113
+ relaxed: "160%",
114
+ loose: "180%",
115
+ };
116
+
117
+ /**
118
+ * Font weight mappings
119
+ */
120
+ const FONT_WEIGHTS = {
121
+ thin: "100",
122
+ extralight: "200",
123
+ light: "300",
124
+ normal: "400",
125
+ medium: "500",
126
+ semibold: "600",
127
+ bold: "700",
128
+ extrabold: "800",
129
+ black: "900",
130
+ };
131
+
132
+ /**
133
+ * Font size presets for text elements
134
+ * xl = extra large (headlines), l = large, m = medium, s = small
135
+ */
136
+ const FONT_SIZES = {
137
+ xs: "12px",
138
+ s: "14px",
139
+ sm: "14px",
140
+ m: "16px",
141
+ md: "16px",
142
+ l: "20px",
143
+ lg: "20px",
144
+ xl: "24px",
145
+ "2xl": "32px",
146
+ "3xl": "40px",
147
+ "4xl": "48px",
148
+ "5xl": "64px",
149
+ };
150
+
151
+ /**
152
+ * Row width presets
153
+ */
154
+ const ROW_WIDTHS = {
155
+ narrow: "800px",
156
+ medium: "960px",
157
+ wide: "1170px",
158
+ extra: "1400px",
159
+ };
160
+
161
+ /**
162
+ * Container width presets
163
+ */
164
+ const CONTAINER_WIDTHS = {
165
+ small: "550px",
166
+ mid: "720px",
167
+ midWide: "960px",
168
+ wide: "1170px",
169
+ full: "100%",
170
+ };
171
+
172
+ /**
173
+ * Border width mappings
174
+ */
175
+ const BORDER_WIDTHS = {
176
+ 0: "0",
177
+ 1: "1px",
178
+ 2: "2px",
179
+ 3: "3px",
180
+ 4: "4px",
181
+ 6: "6px",
182
+ 8: "8px",
183
+ };
184
+
185
+ const BG_STYLE_CLASSES = {
186
+ cover: "bgCover",
187
+ "cover-center": "bgCoverCenter",
188
+ parallax: "bgCoverV2",
189
+ w100: "bgW100",
190
+ w100h100: "bgW100H100",
191
+ "no-repeat": "bgNoRepeat",
192
+ repeat: "bgRepeat",
193
+ "repeat-x": "bgRepeatX",
194
+ "repeat-y": "bgRepeatY",
195
+ };
196
+
197
+ /**
198
+ * Animation types available in ClickFunnels
199
+ */
200
+ const ANIMATION_TYPES = [
201
+ 'fade-in', 'glide-in', 'expand-in', 'bounce-in', 'fold-in', 'puff-in',
202
+ 'spin-in', 'flip-in', 'slide-in', 'turn-in', 'float-in', 'blink',
203
+ 'reveal', 'rocking', 'bouncing', 'wooble', 'elevate',
204
+ ];
205
+
206
+ // ==========================================================================
207
+ // STYLEGUIDE MANAGER
208
+ // ==========================================================================
209
+
210
+ /**
211
+ * StyleguideManager - Handles styleguide CSS generation and attribute mapping
212
+ *
213
+ * Reads styleguide data from embedded JSON or external source and generates
214
+ * CSS for paint themes, buttons, shadows, borders, and corners.
215
+ */
216
+ class StyleguideManager {
217
+ constructor() {
218
+ this.data = null;
219
+ this.styleElement = null;
220
+ }
221
+
222
+ /**
223
+ * Initialize from embedded JSON or external data
224
+ * @param {Object} styleguideData - Optional styleguide data object
225
+ */
226
+ init(styleguideData = null) {
227
+ if (styleguideData) {
228
+ this.data = styleguideData;
229
+ } else {
230
+ // Try to load from embedded script
231
+ const scriptEl = document.getElementById("cf-styleguide-data");
232
+ if (scriptEl) {
233
+ try {
234
+ this.data = JSON.parse(scriptEl.textContent);
235
+ } catch (e) {
236
+ console.warn("FunnelWind: Failed to parse styleguide data:", e);
237
+ return;
238
+ }
239
+ }
240
+ }
241
+
242
+ if (this.data) {
243
+ this.injectCSS();
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Generate and inject CSS for browser preview
249
+ */
250
+ injectCSS() {
251
+ if (!this.data) return;
252
+
253
+ const css = this.generateCSS();
254
+
255
+ // Remove existing styleguide styles
256
+ if (this.styleElement) {
257
+ this.styleElement.remove();
258
+ }
259
+
260
+ // Inject new styles
261
+ this.styleElement = document.createElement("style");
262
+ this.styleElement.id = "cf-styleguide-styles";
263
+ this.styleElement.textContent = css;
264
+ document.head.appendChild(this.styleElement);
265
+
266
+ // Apply font data attributes to elements for pagetree parsing
267
+ this.applyFontDataAttributes();
268
+ }
269
+
270
+ /**
271
+ * Apply font and color data attributes to elements for pagetree parsing.
272
+ * This allows the parser to capture styleguide fonts and paint theme colors.
273
+ */
274
+ applyFontDataAttributes() {
275
+ if (!this.data) return;
276
+
277
+ const { typography, paintThemes } = this.data;
278
+
279
+ // Apply typography fonts to elements without explicit fonts
280
+ if (typography) {
281
+ const { headlineFont, subheadlineFont, contentFont } = typography;
282
+
283
+ // Apply headline font to headlines without explicit font
284
+ if (headlineFont) {
285
+ document.querySelectorAll('[data-type="Headline/V1"]:not([data-font])').forEach(el => {
286
+ el.setAttribute('data-font', headlineFont);
287
+ });
288
+ }
289
+
290
+ // Apply subheadline font to subheadlines without explicit font
291
+ if (subheadlineFont) {
292
+ document.querySelectorAll('[data-type="SubHeadline/V1"]:not([data-font])').forEach(el => {
293
+ el.setAttribute('data-font', subheadlineFont);
294
+ });
295
+ }
296
+
297
+ // Apply content font to paragraphs without explicit font
298
+ if (contentFont) {
299
+ document.querySelectorAll('[data-type="Paragraph/V1"]:not([data-font])').forEach(el => {
300
+ el.setAttribute('data-font', contentFont);
301
+ });
302
+ }
303
+ }
304
+
305
+ // Helper to check if element's closest paint-themed ancestor is the given container
306
+ // This prevents applying colors to elements inside nested non-paint containers
307
+ const isDirectPaintDescendant = (el, paintContainer) => {
308
+ const closestPaint = el.closest('[data-paint-colors]');
309
+ return closestPaint === paintContainer;
310
+ };
311
+
312
+ // Apply paint theme colors to elements for pagetree parsing
313
+ // IMPORTANT: Paint themes OVERRIDE inline colors (CSS uses !important), so we must
314
+ // override data attributes too for the parser to capture the correct colors
315
+ // Only apply to elements whose closest paint ancestor is this container
316
+ if (paintThemes?.length) {
317
+ paintThemes.forEach(theme => {
318
+ const containers = document.querySelectorAll(`[data-paint-colors="${theme.id}"]`);
319
+ containers.forEach(container => {
320
+ const headlineColor = this.getColorHex(theme.headlineColorId);
321
+ const subheadlineColor = this.getColorHex(theme.subheadlineColorId);
322
+ const contentColor = this.getColorHex(theme.contentColorId);
323
+ const iconColor = this.getColorHex(theme.iconColorId);
324
+ const linkColor = theme.linkColorId ? this.getColorHex(theme.linkColorId) : null;
325
+
326
+ // Apply headline color (only to direct paint descendants)
327
+ container.querySelectorAll('[data-type="Headline/V1"]').forEach(el => {
328
+ if (isDirectPaintDescendant(el, container)) {
329
+ if (!el.hasAttribute('data-color-explicit')) {
330
+ el.setAttribute('data-color', headlineColor);
331
+ }
332
+ if (linkColor) el.setAttribute('data-link-color', linkColor);
333
+ }
334
+ });
335
+
336
+ // Apply subheadline color (only to direct paint descendants)
337
+ container.querySelectorAll('[data-type="SubHeadline/V1"]').forEach(el => {
338
+ if (isDirectPaintDescendant(el, container)) {
339
+ if (!el.hasAttribute('data-color-explicit')) {
340
+ el.setAttribute('data-color', subheadlineColor);
341
+ }
342
+ if (linkColor) el.setAttribute('data-link-color', linkColor);
343
+ }
344
+ });
345
+
346
+ // Apply content/paragraph color (only to direct paint descendants)
347
+ container.querySelectorAll('[data-type="Paragraph/V1"]').forEach(el => {
348
+ if (isDirectPaintDescendant(el, container)) {
349
+ if (!el.hasAttribute('data-color-explicit')) {
350
+ el.setAttribute('data-color', contentColor);
351
+ }
352
+ if (linkColor) el.setAttribute('data-link-color', linkColor);
353
+ }
354
+ });
355
+
356
+ // Apply icon color (only to direct paint descendants)
357
+ container.querySelectorAll('[data-type="Icon/V1"]').forEach(el => {
358
+ if (isDirectPaintDescendant(el, container)) {
359
+ if (!el.hasAttribute('data-color-explicit')) {
360
+ el.setAttribute('data-color', iconColor);
361
+ }
362
+ }
363
+ });
364
+
365
+ // Apply text color to bullet lists (only to direct paint descendants)
366
+ container.querySelectorAll('[data-type="BulletList/V1"]').forEach(el => {
367
+ if (isDirectPaintDescendant(el, container)) {
368
+ if (!el.hasAttribute('data-text-color-explicit')) {
369
+ el.setAttribute('data-text-color', contentColor);
370
+ }
371
+ if (!el.hasAttribute('data-icon-color-explicit')) {
372
+ el.setAttribute('data-icon-color', iconColor);
373
+ }
374
+ if (linkColor) el.setAttribute('data-link-color', linkColor);
375
+ }
376
+ });
377
+ });
378
+ });
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Get hex color by ID from the color palette
384
+ */
385
+ getColorHex(colorId) {
386
+ if (!this.data?.colors) return "#000000";
387
+ const color = this.data.colors.find((c) => c.id === colorId);
388
+ return color ? color.hex : "#000000";
389
+ }
390
+
391
+ /**
392
+ * Get typescale sizes calculated from baseSize and scaleRatio
393
+ * Returns object with scales for each element type (headline, subheadline, paragraph)
394
+ *
395
+ * The scale is designed so that within each size category (xl, l, m, s):
396
+ * - Headlines use the largest size
397
+ * - Subheadlines use one step down
398
+ * - Paragraphs use two steps down
399
+ *
400
+ * This matches StyleguideEditor.tsx preview layout
401
+ */
402
+ getTypescale() {
403
+ if (!this.data?.typography) return null;
404
+
405
+ const { baseSize = 16, scaleRatio = 1.25 } = this.data.typography;
406
+
407
+ // Calculate the base scale steps
408
+ const r = scaleRatio;
409
+ const b = baseSize;
410
+
411
+ // Scale values (each is one step apart)
412
+ // With base=16, ratio=1.25: ~10, 13, 16, 20, 25, 31, 39, 49, 61, 76, 95
413
+ const scale = {
414
+ n3: Math.round(b / Math.pow(r, 3)), // ~8
415
+ n2: Math.round(b / Math.pow(r, 2)), // ~10
416
+ n1: Math.round(b / r), // ~13
417
+ base: b, // 16
418
+ p1: Math.round(b * r), // ~20
419
+ p2: Math.round(b * Math.pow(r, 2)), // ~25
420
+ p3: Math.round(b * Math.pow(r, 3)), // ~31
421
+ p4: Math.round(b * Math.pow(r, 4)), // ~39
422
+ p5: Math.round(b * Math.pow(r, 5)), // ~49
423
+ p6: Math.round(b * Math.pow(r, 6)), // ~61
424
+ p7: Math.round(b * Math.pow(r, 7)), // ~76
425
+ p8: Math.round(b * Math.pow(r, 8)), // ~95
426
+ };
427
+
428
+ return {
429
+ // Headlines get largest sizes
430
+ headline: {
431
+ "5xl": `${scale.p8}px`,
432
+ "4xl": `${scale.p7}px`,
433
+ "3xl": `${scale.p6}px`,
434
+ "2xl": `${scale.p5}px`,
435
+ xl: `${scale.p4}px`,
436
+ l: `${scale.p3}px`,
437
+ lg: `${scale.p3}px`,
438
+ m: `${scale.p2}px`,
439
+ md: `${scale.p2}px`,
440
+ s: `${scale.p1}px`,
441
+ sm: `${scale.p1}px`,
442
+ xs: `${scale.base}px`,
443
+ },
444
+ // Subheadlines get one step smaller
445
+ subheadline: {
446
+ "5xl": `${scale.p7}px`,
447
+ "4xl": `${scale.p6}px`,
448
+ "3xl": `${scale.p5}px`,
449
+ "2xl": `${scale.p4}px`,
450
+ xl: `${scale.p3}px`,
451
+ l: `${scale.p2}px`,
452
+ lg: `${scale.p2}px`,
453
+ m: `${scale.p1}px`,
454
+ md: `${scale.p1}px`,
455
+ s: `${scale.base}px`,
456
+ sm: `${scale.base}px`,
457
+ xs: `${scale.n1}px`,
458
+ },
459
+ // Paragraphs get two steps smaller
460
+ paragraph: {
461
+ "5xl": `${scale.p6}px`,
462
+ "4xl": `${scale.p5}px`,
463
+ "3xl": `${scale.p4}px`,
464
+ "2xl": `${scale.p3}px`,
465
+ xl: `${scale.p2}px`,
466
+ l: `${scale.p1}px`,
467
+ lg: `${scale.p1}px`,
468
+ m: `${scale.base}px`,
469
+ md: `${scale.base}px`,
470
+ s: `${scale.n1}px`,
471
+ sm: `${scale.n1}px`,
472
+ xs: `${scale.n2}px`,
473
+ },
474
+ };
475
+ }
476
+
477
+ /**
478
+ * Resolve a size preset (xl, l, m, s, xs) to pixel value using typescale
479
+ * Falls back to static FONT_SIZES if no styleguide or not a preset
480
+ *
481
+ * @param {string} size - Size preset (xl, l, m, s, xs) or pixel value
482
+ * @param {string} elementType - Element type: 'headline', 'subheadline', or 'paragraph'
483
+ */
484
+ resolveSize(size, elementType = 'headline') {
485
+ // First try styleguide typescale
486
+ const typescale = this.getTypescale();
487
+ if (typescale) {
488
+ const elementScale = typescale[elementType] || typescale.headline;
489
+ if (elementScale && elementScale[size]) {
490
+ return elementScale[size];
491
+ }
492
+ }
493
+ // Return null to let caller use static fallback
494
+ return null;
495
+ }
496
+
497
+ /**
498
+ * Check if a value is a styleguide reference
499
+ * @param {string} value - The attribute value to check
500
+ * @param {string} type - Type of styleguide reference (paint, shadow, border, corner, button)
501
+ */
502
+ isStyleguideRef(value, type) {
503
+ if (!value || !this.data) return false;
504
+
505
+ switch (type) {
506
+ case "paint":
507
+ return this.data.paintThemes?.some((t) => t.id === value);
508
+ case "shadow":
509
+ return this.data.shadows?.some((s) => s.id === value);
510
+ case "border":
511
+ return this.data.borders?.some((b) => b.id === value);
512
+ case "corner":
513
+ return this.data.corners?.some((c) => c.id === value);
514
+ case "button":
515
+ return this.data.buttons?.some((b) => b.id === value);
516
+ default:
517
+ return false;
518
+ }
519
+ }
520
+
521
+ /**
522
+ * Generate CSS from styleguide data
523
+ */
524
+ generateCSS() {
525
+ const { colors, paintThemes, shadows, borders, corners, buttons, typography } =
526
+ this.data;
527
+ let css = "/* FunnelWind Styleguide CSS */\n\n";
528
+
529
+ // 1. CSS Variables for colors
530
+ if (colors?.length) {
531
+ css += ":root {\n";
532
+ colors.forEach((color) => {
533
+ css += ` --sg-color-${color.id}: ${color.hex};\n`;
534
+ });
535
+ css += "}\n\n";
536
+ }
537
+
538
+ // 1.5 Typography styles (fonts for headline, subheadline, content)
539
+ // Only apply when element doesn't have explicit data-font attribute
540
+ if (typography) {
541
+ css += "/* Typography styles */\n";
542
+ const { headlineFont, subheadlineFont, contentFont, headlineWeight, subheadlineWeight, contentWeight } = typography;
543
+
544
+ // Headline font (when no explicit font set)
545
+ if (headlineFont) {
546
+ css += `[data-type="Headline/V1"]:not([data-font]) h1,\n`;
547
+ css += `[data-type="Headline/V1"]:not([data-font]) h2 {\n`;
548
+ css += ` font-family: "${headlineFont}", sans-serif !important;\n`;
549
+ css += "}\n";
550
+ }
551
+
552
+ // Subheadline font (when no explicit font set)
553
+ if (subheadlineFont) {
554
+ css += `[data-type="SubHeadline/V1"]:not([data-font]) h2,\n`;
555
+ css += `[data-type="SubHeadline/V1"]:not([data-font]) h3 {\n`;
556
+ css += ` font-family: "${subheadlineFont}", sans-serif !important;\n`;
557
+ css += "}\n";
558
+ }
559
+
560
+ // Content/paragraph font (when no explicit font set)
561
+ if (contentFont) {
562
+ css += `[data-type="Paragraph/V1"]:not([data-font]) p {\n`;
563
+ css += ` font-family: "${contentFont}", sans-serif !important;\n`;
564
+ css += "}\n";
565
+
566
+ // Also apply to bullet lists (they use content font)
567
+ css += `[data-type="BulletList/V1"] li {\n`;
568
+ css += ` font-family: "${contentFont}", sans-serif !important;\n`;
569
+ css += "}\n";
570
+ }
571
+ css += "\n";
572
+ }
573
+
574
+ // 2. Paint theme styles (for sections/rows)
575
+ if (paintThemes?.length) {
576
+ paintThemes.forEach((theme) => {
577
+ const bgColor = this.getColorHex(theme.backgroundColorId);
578
+ const headlineColor = this.getColorHex(theme.headlineColorId);
579
+ const subheadlineColor = this.getColorHex(theme.subheadlineColorId);
580
+ const contentColor = this.getColorHex(theme.contentColorId);
581
+ const linkColor = this.getColorHex(theme.linkColorId);
582
+ const iconColor = this.getColorHex(theme.iconColorId);
583
+
584
+ // Container styles - set CSS variables for this paint theme
585
+ css += `[data-paint-colors="${theme.id}"] {\n`;
586
+ css += ` background-color: ${bgColor} !important;\n`;
587
+ css += ` --sg-headline-color: ${headlineColor};\n`;
588
+ css += ` --sg-subheadline-color: ${subheadlineColor};\n`;
589
+ css += ` --sg-content-color: ${contentColor};\n`;
590
+ css += ` --sg-link-color: ${linkColor};\n`;
591
+ css += ` --sg-icon-color: ${iconColor};\n`;
592
+ css += "}\n\n";
593
+ });
594
+
595
+ // Text color rules using CSS variables - these inherit from CLOSEST paint ancestor
596
+ // Only one rule needed per element type (no theme-specific selectors)
597
+ css += `/* Paint theme text colors - inherit from closest paint ancestor */\n`;
598
+
599
+ // Headline
600
+ css += `[data-paint-colors] [data-type="Headline/V1"] h1,\n`;
601
+ css += `[data-paint-colors] [data-type="Headline/V1"] h2 {\n`;
602
+ css += ` color: var(--sg-headline-color) !important;\n`;
603
+ css += "}\n";
604
+
605
+ // Subheadline
606
+ css += `[data-paint-colors] [data-type="SubHeadline/V1"] h2,\n`;
607
+ css += `[data-paint-colors] [data-type="SubHeadline/V1"] h3 {\n`;
608
+ css += ` color: var(--sg-subheadline-color) !important;\n`;
609
+ css += "}\n";
610
+
611
+ // Paragraph
612
+ css += `[data-paint-colors] [data-type="Paragraph/V1"] p {\n`;
613
+ css += ` color: var(--sg-content-color) !important;\n`;
614
+ css += "}\n";
615
+
616
+ // Links (not buttons)
617
+ css += `[data-paint-colors] a:not([data-type="Button/V1"] a) {\n`;
618
+ css += ` color: var(--sg-link-color) !important;\n`;
619
+ css += "}\n";
620
+
621
+ // Icons and bullet list icons
622
+ css += `[data-paint-colors] [data-type="Icon/V1"] i,\n`;
623
+ css += `[data-paint-colors] [data-type="BulletList/V1"] .fa_icon {\n`;
624
+ css += ` color: var(--sg-icon-color) !important;\n`;
625
+ css += "}\n";
626
+
627
+ // Bullet list text (target both li and span for proper inheritance)
628
+ css += `[data-paint-colors] [data-type="BulletList/V1"] li,\n`;
629
+ css += `[data-paint-colors] [data-type="BulletList/V1"] li span {\n`;
630
+ css += ` color: var(--sg-content-color) !important;\n`;
631
+ css += "}\n\n";
632
+ }
633
+
634
+ // 3. Shadow styles
635
+ if (shadows?.length) {
636
+ shadows.forEach((shadow) => {
637
+ const value = `${shadow.x}px ${shadow.y}px ${shadow.blur}px ${shadow.spread}px ${shadow.color}`;
638
+ css += `[data-style-guide-shadow="${shadow.id}"] {\n`;
639
+ css += ` box-shadow: ${value} !important;\n`;
640
+ css += "}\n";
641
+ });
642
+ css += "\n";
643
+ }
644
+
645
+ // 4. Border styles
646
+ if (borders?.length) {
647
+ borders.forEach((border) => {
648
+ css += `[data-style-guide-border="${border.id}"] {\n`;
649
+ css += ` border: ${border.width}px ${border.style} ${border.color} !important;\n`;
650
+ css += "}\n";
651
+ });
652
+ css += "\n";
653
+ }
654
+
655
+ // 5. Corner styles
656
+ if (corners?.length) {
657
+ corners.forEach((corner) => {
658
+ css += `[data-style-guide-corner="${corner.id}"] {\n`;
659
+ css += ` border-radius: ${corner.radius}px !important;\n`;
660
+ css += "}\n";
661
+ });
662
+ css += "\n";
663
+ }
664
+
665
+ // 6. Button styles
666
+ if (buttons?.length) {
667
+ buttons.forEach((btn) => {
668
+ // Base button style
669
+ css += `[data-style-guide-button="${btn.id}"] a {\n`;
670
+ css += ` background-color: ${
671
+ btn.regular?.bg || "#3b82f6"
672
+ } !important;\n`;
673
+ if (btn.borderRadius)
674
+ css += ` border-radius: ${btn.borderRadius}px !important;\n`;
675
+ if (btn.borderWidth > 0) {
676
+ css += ` border: ${btn.borderWidth}px ${
677
+ btn.borderStyle || "solid"
678
+ } ${btn.borderColor || "transparent"} !important;\n`;
679
+ }
680
+ if (btn.shadow?.enabled) {
681
+ const s = btn.shadow;
682
+ css += ` box-shadow: ${s.x || 0}px ${s.y || 0}px ${
683
+ s.blur || 0
684
+ }px ${s.spread || 0}px ${
685
+ s.color || "rgba(0,0,0,0.1)"
686
+ } !important;\n`;
687
+ }
688
+ css += "}\n";
689
+
690
+ // Button text color
691
+ css += `[data-style-guide-button="${btn.id}"] a span {\n`;
692
+ css += ` color: ${btn.regular?.color || "#ffffff"} !important;\n`;
693
+ css += "}\n";
694
+
695
+ // Hover state
696
+ if (btn.hover) {
697
+ css += `[data-style-guide-button="${btn.id}"] a:hover {\n`;
698
+ css += ` background-color: ${
699
+ btn.hover.bg || btn.regular?.bg || "#3b82f6"
700
+ } !important;\n`;
701
+ css += "}\n";
702
+ css += `[data-style-guide-button="${btn.id}"] a:hover span {\n`;
703
+ css += ` color: ${
704
+ btn.hover.color || btn.regular?.color || "#ffffff"
705
+ } !important;\n`;
706
+ css += "}\n";
707
+ }
708
+
709
+ // Active state
710
+ if (btn.active) {
711
+ css += `[data-style-guide-button="${btn.id}"] a:active {\n`;
712
+ css += ` background-color: ${
713
+ btn.active.bg || btn.regular?.bg || "#3b82f6"
714
+ } !important;\n`;
715
+ css += "}\n";
716
+ }
717
+ });
718
+ }
719
+
720
+ return css;
721
+ }
722
+ }
723
+
724
+ // Create global instance
725
+ const styleguideManager = new StyleguideManager();
726
+
727
+ // ==========================================================================
728
+ // BRAND ASSETS MANAGER
729
+ // ==========================================================================
730
+
731
+ /**
732
+ * Manages brand assets for dynamic image swapping
733
+ * Brand assets can be attached to cf-image elements using the brand-asset attribute
734
+ * Also supports bg-image on cf-section, cf-row, cf-col
735
+ * Valid types: logo, logo_light, logo_dark, background, pattern, icon, product_image
736
+ */
737
+ class BrandAssetsManager {
738
+ constructor() {
739
+ this.data = null;
740
+ }
741
+
742
+ /**
743
+ * Initialize from embedded JSON or external data
744
+ * @param {Object} brandAssets - Optional brand assets data object
745
+ */
746
+ init(brandAssets = null) {
747
+ if (brandAssets) {
748
+ this.data = brandAssets;
749
+ } else {
750
+ // Try to load from embedded script
751
+ const scriptEl = document.getElementById("cf-brand-assets");
752
+ if (scriptEl) {
753
+ try {
754
+ this.data = JSON.parse(scriptEl.textContent);
755
+ } catch (e) {
756
+ console.warn("FunnelWind: Failed to parse brand assets data:", e);
757
+ return;
758
+ }
759
+ }
760
+ }
761
+ }
762
+
763
+ /**
764
+ * Get the first active asset URL for a given type
765
+ * @param {string} assetType - Type of asset: logo, background, pattern, icon, product_image
766
+ * @returns {string|null} - The URL of the first active asset or null
767
+ */
768
+ getAssetUrl(assetType) {
769
+ if (
770
+ !this.data ||
771
+ !this.data[assetType] ||
772
+ this.data[assetType].length === 0
773
+ ) {
774
+ return null;
775
+ }
776
+ return this.data[assetType][0];
777
+ }
778
+
779
+ /**
780
+ * Get all active asset URLs for a given type
781
+ * @param {string} assetType - Type of asset
782
+ * @returns {string[]} - Array of URLs
783
+ */
784
+ getAssetUrls(assetType) {
785
+ if (!this.data || !this.data[assetType]) {
786
+ return [];
787
+ }
788
+ return this.data[assetType];
789
+ }
790
+
791
+ /**
792
+ * Check if we have any assets of a given type
793
+ * @param {string} assetType - Type of asset
794
+ * @returns {boolean}
795
+ */
796
+ hasAsset(assetType) {
797
+ return (
798
+ this.data && this.data[assetType] && this.data[assetType].length > 0
799
+ );
800
+ }
801
+ }
802
+
803
+ // Create global instance
804
+ const brandAssetsManager = new BrandAssetsManager();
805
+
806
+ // ==========================================================================
807
+ // UTILITY FUNCTIONS
808
+ // ==========================================================================
809
+
810
+ /**
811
+ * Get attribute with fallback
812
+ */
813
+ function attr(el, name, fallback = null) {
814
+ const val = el.getAttribute(name);
815
+ return val !== null ? val : fallback;
816
+ }
817
+
818
+ /**
819
+ * Resolve a value - check if it's a preset key or use as-is
820
+ */
821
+ function resolve(value, presets) {
822
+ if (!value) return null;
823
+ return presets[value] || value;
824
+ }
825
+
826
+ /**
827
+ * Build inline style string from object
828
+ */
829
+ function buildStyle(styleObj) {
830
+ return Object.entries(styleObj)
831
+ .filter(([_, v]) => v !== null && v !== undefined && v !== "")
832
+ .map(([k, v]) => `${k}: ${v}`)
833
+ .join("; ");
834
+ }
835
+
836
+ /**
837
+ * Parse content with slot handling for Web Components
838
+ */
839
+ function getContent(el) {
840
+ return el.innerHTML;
841
+ }
842
+
843
+ function getBgStyleClass(bgStyle) {
844
+ if (!bgStyle) return "bgCoverCenter";
845
+ return BG_STYLE_CLASSES[bgStyle] || bgStyle;
846
+ }
847
+
848
+ /**
849
+ * Helper to extract YouTube video ID from URL
850
+ */
851
+ function extractYouTubeVideoId(url) {
852
+ if (!url) return null;
853
+ const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\s?]+)/);
854
+ return match ? match[1] : null;
855
+ }
856
+
857
+ /**
858
+ * Build animation data attributes string from element attributes
859
+ * Returns empty string if no animation specified
860
+ */
861
+ function buildAnimationAttrs(el) {
862
+ const animation = attr(el, 'animation');
863
+ if (!animation) return '';
864
+
865
+ const time = attr(el, 'animation-time', '1000');
866
+ const delay = attr(el, 'animation-delay', '0');
867
+ const trigger = attr(el, 'animation-trigger', 'load');
868
+ const timing = attr(el, 'animation-timing', 'ease');
869
+ const direction = attr(el, 'animation-direction', 'normal');
870
+ const once = attr(el, 'animation-once', 'true');
871
+ const loop = attr(el, 'animation-loop', 'false');
872
+
873
+ return ` data-skip-animation-settings="false" data-animation-type="${animation}" data-animation-time="${time}" data-animation-delay="${delay}" data-animation-trigger="${trigger}" data-animation-timing-function="${timing}" data-animation-direction="${direction}" data-animation-once="${once === 'true'}" data-animation-loop="${loop === 'true'}"`;
874
+ }
875
+
876
+ // ==========================================================================
877
+ // BASE COMPONENT CLASS
878
+ // ==========================================================================
879
+
880
+ class CFElement extends HTMLElement {
881
+ constructor() {
882
+ super();
883
+ this._rendered = false;
884
+ }
885
+
886
+ connectedCallback() {
887
+ // Defer rendering to allow children to be parsed
888
+ if (!this._rendered) {
889
+ requestAnimationFrame(() => {
890
+ // Check if element is still in DOM before rendering
891
+ // (parent may have already replaced itself via outerHTML)
892
+ if (this.parentNode) {
893
+ this.render();
894
+ this._rendered = true;
895
+ }
896
+ });
897
+ }
898
+ }
899
+
900
+ render() {
901
+ // Override in subclasses
902
+ }
903
+ }
904
+
905
+ // ==========================================================================
906
+ // LAYOUT COMPONENTS
907
+ // ==========================================================================
908
+
909
+ /**
910
+ * <cf-page> - Root content node
911
+ *
912
+ * Attributes:
913
+ * bg - Background color
914
+ * bg-image - Background image URL
915
+ * bg-style - Background image style: cover, cover-center (default), parallax, w100, w100h100, no-repeat, repeat, repeat-x, repeat-y
916
+ * gradient - CSS gradient
917
+ * overlay - Overlay color (rgba)
918
+ * text-color - Default text color
919
+ * link-color - Default link color
920
+ * font-family - Default font family (e.g., '"Roboto", sans-serif')
921
+ * font-weight - Default font weight
922
+ * header-code - Custom header HTML/scripts
923
+ * footer-code - Custom footer HTML/scripts
924
+ * css - Custom CSS
925
+ */
926
+ class CFPage extends CFElement {
927
+ render() {
928
+ const bg = attr(this, "bg", "#ffffff");
929
+ const bgImage = attr(this, "bg-image");
930
+ const bgStyle = attr(this, "bg-style");
931
+ const gradient = attr(this, "gradient");
932
+ const overlay = attr(this, "overlay");
933
+ const textColor = attr(this, "text-color", "#334155");
934
+ const linkColor = attr(this, "link-color", "#3b82f6");
935
+ const fontFamily = attr(this, "font-family");
936
+ const fontWeight = attr(this, "font-weight");
937
+ const headerCode = attr(this, "header-code");
938
+ const footerCode = attr(this, "footer-code");
939
+ const customCss = attr(this, "css");
940
+
941
+ const styles = {
942
+ width: "100%",
943
+ "min-height": "100vh",
944
+ position: "relative",
945
+ };
946
+
947
+ if (gradient) {
948
+ styles["background"] = gradient;
949
+ } else if (bg) {
950
+ styles["background-color"] = bg;
951
+ }
952
+ if (bgImage) {
953
+ styles["background-image"] = `url(${bgImage})`;
954
+ // Don't set bg-size/position/repeat inline - let CSS class handle it
955
+ }
956
+
957
+ // Determine background style class
958
+ const bgStyleClass = bgImage ? getBgStyleClass(bgStyle) : "bgCoverCenter";
959
+
960
+ // Build overlay element and content wrapper if overlay specified
961
+ let overlayHtml = "";
962
+ let overlayDataAttr = "";
963
+ let contentHtml = getContent(this);
964
+ if (overlay) {
965
+ overlayHtml = `<div class="cf-overlay" style="position:absolute;inset:0;background:${overlay};pointer-events:none;z-index:1;"></div>`;
966
+ overlayDataAttr = ` data-overlay="${overlay}"`;
967
+ contentHtml = `<div style="position:relative;z-index:2;">${contentHtml}</div>`;
968
+ }
969
+
970
+ // Build optional data attributes for settings
971
+ let optionalAttrs = "";
972
+ if (fontFamily) optionalAttrs += ` data-font-family="${fontFamily}"`;
973
+ if (fontWeight) optionalAttrs += ` data-font-weight="${fontWeight}"`;
974
+ if (headerCode)
975
+ optionalAttrs += ` data-header-code="${encodeURIComponent(
976
+ headerCode
977
+ )}"`;
978
+ if (footerCode)
979
+ optionalAttrs += ` data-footer-code="${encodeURIComponent(
980
+ footerCode
981
+ )}"`;
982
+ if (customCss)
983
+ optionalAttrs += ` data-custom-css="${encodeURIComponent(customCss)}"`;
984
+
985
+ // Build custom CSS style tag for live preview
986
+ const customCssStyle = customCss
987
+ ? `<style id="custom-css">${customCss}</style>`
988
+ : `<style id="custom-css"></style>`;
989
+
990
+ this.outerHTML = `
991
+ <div
992
+ class="content-node ${bgStyleClass}"
993
+ data-type="ContentNode"
994
+ data-text-color="${textColor}"
995
+ data-link-color="${linkColor}"
996
+ data-bg-style="${bgStyleClass}"${overlayDataAttr}${optionalAttrs}
997
+ style="${buildStyle(styles)}"
998
+ >${overlayHtml}${contentHtml}</div>${customCssStyle}
999
+ `;
1000
+ }
1001
+ }
1002
+
1003
+ /**
1004
+ * <cf-section> - Section container
1005
+ *
1006
+ * Attributes:
1007
+ * container - Width: small, mid, midWide, wide, full
1008
+ * bg - Background color
1009
+ * bg-image - Background image URL
1010
+ * brand-asset - Brand asset type for bg-image: background, pattern
1011
+ * gradient - CSS gradient
1012
+ * overlay - Overlay color
1013
+ * paint - Styleguide paint theme: lightest, light, colored, dark, darkest
1014
+ * pt - Padding top (e.g., "80px", "64px")
1015
+ * pb - Padding bottom
1016
+ * mt - Margin top (NO margin-bottom!)
1017
+ * shadow - Shadow preset, styleguide ref (style1-3), or custom
1018
+ * rounded - Border radius preset or value
1019
+ * corner - Styleguide corner ref (style1-3)
1020
+ * border - Border width or styleguide ref (style1-3)
1021
+ * border-style - Border style (solid, dashed, dotted)
1022
+ * border-color - Border color
1023
+ * show - Visibility: desktop, mobile
1024
+ * video-bg - YouTube URL for video background
1025
+ * video-bg-overlay - Overlay color for video (rgba format, defaults to bg if rgba)
1026
+ * video-bg-hide-mobile - Hide video on mobile (true/false, default true)
1027
+ * video-bg-style - Video style: fill (default), fit
1028
+ */
1029
+ class CFSection extends CFElement {
1030
+ render() {
1031
+ const elementId = attr(this, "element-id");
1032
+ const container = attr(this, "container", "full");
1033
+ const bg = attr(this, "bg");
1034
+ let bgImage = attr(this, "bg-image");
1035
+ const bgStyle = attr(this, "bg-style");
1036
+ const gradient = attr(this, "gradient");
1037
+ const overlay = attr(this, "overlay");
1038
+ const paint = attr(this, "paint");
1039
+ const pt = attr(this, "pt", "64px");
1040
+ const pb = attr(this, "pb", "64px");
1041
+ const px = attr(this, "px", "0");
1042
+ const mt = attr(this, "mt");
1043
+ const shadow = attr(this, "shadow");
1044
+ // Check for popup-rounded (inherited from parent cf-popup) if no explicit rounded
1045
+ const popupRounded = attr(this, "data-popup-rounded");
1046
+ const rounded = attr(this, "rounded") || popupRounded;
1047
+ const corner = attr(this, "corner");
1048
+ const border = attr(this, "border");
1049
+ const borderStyle = attr(this, "border-style", "solid");
1050
+ const borderColor = attr(this, "border-color");
1051
+ const show = attr(this, "show");
1052
+ const brandAsset = attr(this, "brand-asset");
1053
+
1054
+ // Video background attributes
1055
+ const videoBg = attr(this, "video-bg");
1056
+ const videoBgOverlay = attr(this, "video-bg-overlay");
1057
+ const videoBgHideMobile = attr(this, "video-bg-hide-mobile", "true");
1058
+ const videoBgStyle = attr(this, "video-bg-style", "fill");
1059
+
1060
+ // If brand-asset is specified, try to get the asset URL from brand assets manager
1061
+ if (brandAsset && brandAssetsManager.hasAsset(brandAsset)) {
1062
+ const brandAssetUrl = brandAssetsManager.getAssetUrl(brandAsset);
1063
+ if (brandAssetUrl) {
1064
+ bgImage = brandAssetUrl;
1065
+ }
1066
+ }
1067
+
1068
+ const containerWidth = resolve(container, CONTAINER_WIDTHS) || "1170px";
1069
+
1070
+ // Check if shadow/border/corner are styleguide references
1071
+ const isShadowStyleguide =
1072
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
1073
+ const isBorderStyleguide =
1074
+ border && styleguideManager.isStyleguideRef(border, "border");
1075
+ const isCornerStyleguide =
1076
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
1077
+
1078
+ const styles = {
1079
+ width: "100%",
1080
+ "max-width": containerWidth,
1081
+ "margin-left": "auto",
1082
+ "margin-right": "auto",
1083
+ position: "relative",
1084
+ "padding-top": pt,
1085
+ "padding-bottom": pb,
1086
+ "padding-left": px,
1087
+ "padding-right": px,
1088
+ "box-sizing": "border-box",
1089
+ };
1090
+
1091
+ // Only apply bg if not using paint (styleguide handles paint backgrounds)
1092
+ if (!paint) {
1093
+ if (gradient) {
1094
+ styles["background"] = gradient;
1095
+ } else if (bg) {
1096
+ styles["background-color"] = bg;
1097
+ }
1098
+ }
1099
+ if (bgImage) {
1100
+ styles["background-image"] = `url(${bgImage})`;
1101
+ // Don't set bg-size/position/repeat inline - let CSS class handle it
1102
+ }
1103
+ if (mt) styles["margin-top"] = mt;
1104
+
1105
+ // Shadow: use inline if not styleguide ref
1106
+ if (shadow && !isShadowStyleguide) {
1107
+ styles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1108
+ }
1109
+
1110
+ // Rounded/corner: prefer corner styleguide ref, fallback to rounded
1111
+ if (corner && isCornerStyleguide) {
1112
+ // Styleguide CSS will handle it
1113
+ } else if (rounded) {
1114
+ styles["border-radius"] = resolve(rounded, RADIUS) || rounded;
1115
+ }
1116
+
1117
+ // Border: use inline if not styleguide ref
1118
+ if (border && !isBorderStyleguide) {
1119
+ styles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1120
+ styles["border-style"] = borderStyle;
1121
+ }
1122
+ if (borderColor) styles["border-color"] = borderColor;
1123
+
1124
+ // Determine background style class
1125
+ const bgStyleClass = bgImage ? getBgStyleClass(bgStyle) : "";
1126
+
1127
+ let dataAttrs = 'data-type="SectionContainer/V1"';
1128
+ dataAttrs += ` data-container="${container}"`;
1129
+ if (show) dataAttrs += ` data-show="${show}"`;
1130
+ if (bgStyleClass) dataAttrs += ` data-bg-style="${bgStyleClass}"`;
1131
+ if (overlay) dataAttrs += ` data-overlay="${overlay}"`;
1132
+ // Store original bg-image for conversion (not the swapped brand asset URL)
1133
+ const originalBgImage = attr(this, "bg-image");
1134
+ if (originalBgImage) dataAttrs += ` data-bg-image="${originalBgImage}"`;
1135
+ if (brandAsset) dataAttrs += ` data-brand-asset="${brandAsset}"`;
1136
+
1137
+ // Styleguide data attributes
1138
+ if (paint) dataAttrs += ` data-paint-colors="${paint}"`;
1139
+ if (isShadowStyleguide)
1140
+ dataAttrs += ` data-style-guide-shadow="${shadow}"`;
1141
+ if (isBorderStyleguide)
1142
+ dataAttrs += ` data-style-guide-border="${border}"`;
1143
+ if (isCornerStyleguide)
1144
+ dataAttrs += ` data-style-guide-corner="${corner}"`;
1145
+
1146
+ // Video background handling
1147
+ if (videoBg) {
1148
+ const videoId = extractYouTubeVideoId(videoBg);
1149
+ if (videoId) {
1150
+ const videoThumbnailUrl = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
1151
+ dataAttrs += ` data-video-bg-url="${videoBg}"`;
1152
+ dataAttrs += ` data-video-bg-type="youtube"`;
1153
+ dataAttrs += ` data-video-bg-thumbnail="${videoThumbnailUrl}"`;
1154
+ dataAttrs += ` data-video-bg-hide-mobile="${videoBgHideMobile === "true" || videoBgHideMobile === true}"`;
1155
+ dataAttrs += ` data-video-bg-style="${videoBgStyle}"`;
1156
+
1157
+ // Determine overlay color for video - use explicit overlay, video-bg-overlay, or bg if rgba
1158
+ let videoOverlayColor = videoBgOverlay || overlay;
1159
+ if (!videoOverlayColor && bg && bg.startsWith("rgba")) {
1160
+ videoOverlayColor = bg;
1161
+ }
1162
+ if (videoOverlayColor) {
1163
+ dataAttrs += ` data-video-bg-overlay="${videoOverlayColor}"`;
1164
+ }
1165
+ }
1166
+ }
1167
+
1168
+ // Build overlay element and content wrapper if overlay specified
1169
+ let overlayHtml = "";
1170
+ let contentHtml = getContent(this);
1171
+ if (overlay) {
1172
+ overlayHtml = `<div class="cf-overlay" style="position:absolute;inset:0;background:${overlay};pointer-events:none;z-index:1;border-radius:inherit;"></div>`;
1173
+ contentHtml = `<div style="position:relative;z-index:2;">${contentHtml}</div>`;
1174
+ }
1175
+
1176
+ // Build class attribute (apply bg-style class for CSS effect)
1177
+ const classAttr = bgStyleClass ? ` class="${bgStyleClass}"` : "";
1178
+
1179
+ // Build ID attribute for scroll-to and show-hide targeting
1180
+ const idAttr = elementId ? ` id="${elementId}"` : "";
1181
+
1182
+ this.outerHTML = `
1183
+ <section${idAttr}${classAttr} ${dataAttrs} style="${buildStyle(styles)}">
1184
+ ${overlayHtml}${contentHtml}
1185
+ </section>
1186
+ `;
1187
+ }
1188
+ }
1189
+
1190
+ /**
1191
+ * <cf-row> - Row container for columns
1192
+ *
1193
+ * Attributes:
1194
+ * width - Max width: narrow, medium, wide, extra, or px value
1195
+ * bg - Background color
1196
+ * bg-image - Background image URL
1197
+ * brand-asset - Brand asset type for bg-image: background, pattern
1198
+ * bg-style - Background image style: cover, cover-center, parallax, etc.
1199
+ * gradient - CSS gradient
1200
+ * overlay - Overlay color
1201
+ * paint - Styleguide paint theme: lightest, light, colored, dark, darkest
1202
+ * pt - Padding top
1203
+ * pb - Padding bottom
1204
+ * px - Padding horizontal (left & right)
1205
+ * mt - Margin top
1206
+ * shadow - Shadow preset, styleguide ref (style1-3), or custom
1207
+ * rounded - Border radius
1208
+ * corner - Styleguide corner ref (style1-3)
1209
+ * border - Border width or styleguide ref (style1-3)
1210
+ * border-style - Border style
1211
+ * border-color - Border color
1212
+ */
1213
+ class CFRow extends CFElement {
1214
+ render() {
1215
+ const elementId = attr(this, "element-id");
1216
+ const width = attr(this, "width", "wide");
1217
+ const bg = attr(this, "bg");
1218
+ let bgImage = attr(this, "bg-image");
1219
+ const bgStyle = attr(this, "bg-style");
1220
+ const gradient = attr(this, "gradient");
1221
+ const overlay = attr(this, "overlay");
1222
+ const paint = attr(this, "paint");
1223
+ const pt = attr(this, "pt");
1224
+ const pb = attr(this, "pb");
1225
+ const px = attr(this, "px");
1226
+ const mt = attr(this, "mt");
1227
+ const shadow = attr(this, "shadow");
1228
+ const rounded = attr(this, "rounded");
1229
+ const corner = attr(this, "corner");
1230
+ const border = attr(this, "border");
1231
+ const borderStyle = attr(this, "border-style", "solid");
1232
+ const borderColor = attr(this, "border-color");
1233
+ const brandAsset = attr(this, "brand-asset");
1234
+ const show = attr(this, "show");
1235
+
1236
+ // If brand-asset is specified, try to get the asset URL from brand assets manager
1237
+ if (brandAsset && brandAssetsManager.hasAsset(brandAsset)) {
1238
+ const brandAssetUrl = brandAssetsManager.getAssetUrl(brandAsset);
1239
+ if (brandAssetUrl) {
1240
+ bgImage = brandAssetUrl;
1241
+ }
1242
+ }
1243
+
1244
+ const maxWidth = resolve(width, ROW_WIDTHS) || width;
1245
+
1246
+ // Check if shadow/border/corner are styleguide references
1247
+ const isShadowStyleguide =
1248
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
1249
+ const isBorderStyleguide =
1250
+ border && styleguideManager.isStyleguideRef(border, "border");
1251
+ const isCornerStyleguide =
1252
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
1253
+
1254
+ const styles = {
1255
+ display: "flex",
1256
+ "flex-wrap": "wrap",
1257
+ width: maxWidth,
1258
+ "max-width": "100%",
1259
+ "margin-left": "auto",
1260
+ "margin-right": "auto",
1261
+ position: "relative",
1262
+ "box-sizing": "border-box",
1263
+ "margin-top": mt || "0",
1264
+ };
1265
+
1266
+ // Only apply bg if not using paint (styleguide handles paint backgrounds)
1267
+ if (!paint) {
1268
+ if (gradient) {
1269
+ styles["background"] = gradient;
1270
+ } else if (bg) {
1271
+ styles["background-color"] = bg;
1272
+ }
1273
+ }
1274
+ if (bgImage) {
1275
+ styles["background-image"] = `url(${bgImage})`;
1276
+ // Don't set bg-size/position/repeat inline - let CSS class handle it
1277
+ }
1278
+ if (pt) styles["padding-top"] = pt;
1279
+ if (pb) styles["padding-bottom"] = pb;
1280
+ if (px) {
1281
+ styles["padding-left"] = px;
1282
+ styles["padding-right"] = px;
1283
+ }
1284
+
1285
+ // Shadow: use inline if not styleguide ref
1286
+ if (shadow && !isShadowStyleguide) {
1287
+ styles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1288
+ }
1289
+
1290
+ // Rounded/corner: prefer corner styleguide ref, fallback to rounded
1291
+ if (corner && isCornerStyleguide) {
1292
+ // Styleguide CSS will handle it
1293
+ } else if (rounded) {
1294
+ styles["border-radius"] = resolve(rounded, RADIUS) || rounded;
1295
+ }
1296
+
1297
+ // Border: use inline if not styleguide ref
1298
+ if (border && !isBorderStyleguide) {
1299
+ styles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1300
+ styles["border-style"] = borderStyle;
1301
+ }
1302
+ if (borderColor) styles["border-color"] = borderColor;
1303
+
1304
+ // Determine background style class
1305
+ const bgStyleClass = bgImage ? getBgStyleClass(bgStyle) : "";
1306
+
1307
+ let dataAttrs = 'data-type="RowContainer/V1"';
1308
+ dataAttrs += ` data-width="${width}"`;
1309
+ if (show) dataAttrs += ` data-show="${show}"`;
1310
+ if (bgStyleClass) dataAttrs += ` data-bg-style="${bgStyleClass}"`;
1311
+ if (overlay) dataAttrs += ` data-overlay="${overlay}"`;
1312
+ // Store original bg-image for conversion (not the swapped brand asset URL)
1313
+ const originalBgImage = attr(this, "bg-image");
1314
+ if (originalBgImage) dataAttrs += ` data-bg-image="${originalBgImage}"`;
1315
+ if (brandAsset) dataAttrs += ` data-brand-asset="${brandAsset}"`;
1316
+
1317
+ // Styleguide data attributes
1318
+ if (paint) dataAttrs += ` data-paint-colors="${paint}"`;
1319
+ if (isShadowStyleguide)
1320
+ dataAttrs += ` data-style-guide-shadow="${shadow}"`;
1321
+ if (isBorderStyleguide)
1322
+ dataAttrs += ` data-style-guide-border="${border}"`;
1323
+ if (isCornerStyleguide)
1324
+ dataAttrs += ` data-style-guide-corner="${corner}"`;
1325
+
1326
+ // Animation attributes
1327
+ const animationAttrs = buildAnimationAttrs(this);
1328
+ dataAttrs += animationAttrs;
1329
+
1330
+ // Build overlay element and content wrapper if overlay specified
1331
+ let overlayHtml = "";
1332
+ let contentHtml = getContent(this);
1333
+ if (overlay) {
1334
+ overlayHtml = `<div class="cf-overlay" style="position:absolute;inset:0;background:${overlay};pointer-events:none;z-index:1;border-radius:inherit;"></div>`;
1335
+ contentHtml = `<div style="position:relative;z-index:2;display:flex;flex-wrap:wrap;width:100%;">${contentHtml}</div>`;
1336
+ }
1337
+
1338
+ // Build class attribute (apply bg-style class for CSS effect)
1339
+ const classAttr = bgStyleClass ? ` class="${bgStyleClass}"` : "";
1340
+
1341
+ // Build ID attribute for scroll-to and show-hide targeting
1342
+ const idAttr = elementId ? ` id="${elementId}"` : "";
1343
+
1344
+ this.outerHTML = `
1345
+ <div${idAttr}${classAttr} ${dataAttrs} style="${buildStyle(styles)}">
1346
+ ${overlayHtml}${contentHtml}
1347
+ </div>
1348
+ `;
1349
+ }
1350
+ }
1351
+
1352
+ /**
1353
+ * <cf-col> - Column container (12-column grid)
1354
+ *
1355
+ * Attributes:
1356
+ * span - Column width 1-12 (default: 12)
1357
+ * align - Column alignment: left, center, right (default: left)
1358
+ * bg - Background color
1359
+ * bg-image - Background image URL
1360
+ * brand-asset - Brand asset type for bg-image: background, pattern
1361
+ * bg-style - Background image style: cover, cover-center (default), parallax, etc.
1362
+ * gradient - CSS gradient
1363
+ * overlay - Overlay color
1364
+ * pt - Padding top
1365
+ * pb - Padding bottom
1366
+ * px - Padding horizontal (left & right unified)
1367
+ * mx - Margin horizontal (creates column gaps)
1368
+ * shadow - Shadow
1369
+ * rounded - Border radius (all corners)
1370
+ * rounded-tl - Border radius top-left
1371
+ * rounded-tr - Border radius top-right
1372
+ * rounded-bl - Border radius bottom-left
1373
+ * rounded-br - Border radius bottom-right
1374
+ * border - Border width
1375
+ * border-style - Border style
1376
+ * border-color - Border color
1377
+ */
1378
+ class CFCol extends CFElement {
1379
+ render() {
1380
+ const elementId = attr(this, "element-id");
1381
+ const span = parseInt(attr(this, "span", "12"), 10);
1382
+ const align = attr(this, "align", "left");
1383
+ const show = attr(this, "show");
1384
+ const widthPercent = ((span / 12) * 100).toFixed(6) + "%";
1385
+
1386
+ // Col-inner styling attributes
1387
+ const bg = attr(this, "bg");
1388
+ let bgImage = attr(this, "bg-image");
1389
+ const bgStyle = attr(this, "bg-style");
1390
+ const gradient = attr(this, "gradient");
1391
+ const overlay = attr(this, "overlay");
1392
+ const paint = attr(this, "paint");
1393
+ const pt = attr(this, "pt");
1394
+ const pb = attr(this, "pb");
1395
+ const px = attr(this, "px");
1396
+ const mx = attr(this, "mx", "16px");
1397
+ const shadow = attr(this, "shadow");
1398
+ const rounded = attr(this, "rounded");
1399
+ const corner = attr(this, "corner");
1400
+ const roundedTl = attr(this, "rounded-tl");
1401
+ const roundedTr = attr(this, "rounded-tr");
1402
+ const roundedBl = attr(this, "rounded-bl");
1403
+ const roundedBr = attr(this, "rounded-br");
1404
+ const border = attr(this, "border");
1405
+ const borderStyle = attr(this, "border-style", "solid");
1406
+ const borderColor = attr(this, "border-color");
1407
+ const brandAsset = attr(this, "brand-asset");
1408
+
1409
+ // If brand-asset is specified, try to get the asset URL from brand assets manager
1410
+ if (brandAsset && brandAssetsManager.hasAsset(brandAsset)) {
1411
+ const brandAssetUrl = brandAssetsManager.getAssetUrl(brandAsset);
1412
+ if (brandAssetUrl) {
1413
+ bgImage = brandAssetUrl;
1414
+ }
1415
+ }
1416
+
1417
+ // Check if shadow/border/corner are styleguide references
1418
+ const isShadowStyleguide =
1419
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
1420
+ const isBorderStyleguide =
1421
+ border && styleguideManager.isStyleguideRef(border, "border");
1422
+ const isCornerStyleguide =
1423
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
1424
+
1425
+ // Check if any col-inner styling is present
1426
+ const hasColInnerStyling =
1427
+ bg ||
1428
+ bgImage ||
1429
+ gradient ||
1430
+ overlay ||
1431
+ paint ||
1432
+ pt ||
1433
+ pb ||
1434
+ px ||
1435
+ mx ||
1436
+ shadow ||
1437
+ rounded ||
1438
+ corner ||
1439
+ roundedTl ||
1440
+ roundedTr ||
1441
+ roundedBl ||
1442
+ roundedBr ||
1443
+ border ||
1444
+ borderColor;
1445
+
1446
+ const colStyles = {
1447
+ width: widthPercent,
1448
+ position: "relative",
1449
+ "min-height": "1px",
1450
+ "box-sizing": "border-box",
1451
+ "z-index": "2",
1452
+ };
1453
+
1454
+ // Build col-inner styles
1455
+ const innerStyles = {
1456
+ height: "100%",
1457
+ position: "relative",
1458
+ "text-align": align,
1459
+ "box-sizing": "border-box",
1460
+ };
1461
+
1462
+ if (pt) innerStyles["padding-top"] = pt;
1463
+ if (pb) innerStyles["padding-bottom"] = pb;
1464
+ if (px) {
1465
+ innerStyles["padding-left"] = px;
1466
+ innerStyles["padding-right"] = px;
1467
+ }
1468
+ if (mx) {
1469
+ innerStyles["margin-left"] = mx;
1470
+ innerStyles["margin-right"] = mx;
1471
+ }
1472
+
1473
+ // Only apply bg/gradient if not using paint (styleguide handles paint backgrounds)
1474
+ if (!paint) {
1475
+ if (gradient) {
1476
+ innerStyles["background"] = gradient;
1477
+ } else if (bg) {
1478
+ innerStyles["background-color"] = bg;
1479
+ }
1480
+ }
1481
+ if (bgImage) {
1482
+ innerStyles["background-image"] = `url(${bgImage})`;
1483
+ }
1484
+
1485
+ // Apply shadow (styleguide refs handled via CSS variables)
1486
+ if (shadow && !isShadowStyleguide) {
1487
+ innerStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1488
+ }
1489
+
1490
+ // Handle border radius - styleguide corner refs handled via CSS variables
1491
+ if (corner && isCornerStyleguide) {
1492
+ // Styleguide corner - handled by CSS
1493
+ } else if (rounded) {
1494
+ innerStyles["border-radius"] = resolve(rounded, RADIUS) || rounded;
1495
+ }
1496
+ if (roundedTl)
1497
+ innerStyles["border-top-left-radius"] =
1498
+ resolve(roundedTl, RADIUS) || roundedTl;
1499
+ if (roundedTr)
1500
+ innerStyles["border-top-right-radius"] =
1501
+ resolve(roundedTr, RADIUS) || roundedTr;
1502
+ if (roundedBl)
1503
+ innerStyles["border-bottom-left-radius"] =
1504
+ resolve(roundedBl, RADIUS) || roundedBl;
1505
+ if (roundedBr)
1506
+ innerStyles["border-bottom-right-radius"] =
1507
+ resolve(roundedBr, RADIUS) || roundedBr;
1508
+
1509
+ // Apply border (styleguide refs handled via CSS variables)
1510
+ if (border && !isBorderStyleguide) {
1511
+ innerStyles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1512
+ innerStyles["border-style"] = borderStyle;
1513
+ }
1514
+ if (borderColor) innerStyles["border-color"] = borderColor;
1515
+
1516
+ // Determine background style class
1517
+ const bgStyleClass = bgImage ? getBgStyleClass(bgStyle) : "";
1518
+
1519
+ // Build class attribute for col-inner
1520
+ const classes = ["col-inner"];
1521
+ if (bgStyleClass) classes.push(bgStyleClass);
1522
+ // Add styleguide CSS classes
1523
+ if (isShadowStyleguide) classes.push(`sg-shadow-${shadow}`);
1524
+ if (isBorderStyleguide) classes.push(`sg-border-${border}`);
1525
+ if (isCornerStyleguide) classes.push(`sg-corner-${corner}`);
1526
+ const classAttr = `class="${classes.join(" ")}"`;
1527
+
1528
+ // Build data attributes for parser
1529
+ let dataAttrs = classAttr;
1530
+ if (overlay) dataAttrs += ` data-overlay="${overlay}"`;
1531
+ if (bg && !paint) dataAttrs += ` data-bg="${bg}"`;
1532
+ // Store original bg-image for conversion (not the swapped brand asset URL)
1533
+ const originalBgImage = attr(this, "bg-image");
1534
+ if (originalBgImage) dataAttrs += ` data-bg-image="${originalBgImage}"`;
1535
+ if (brandAsset) dataAttrs += ` data-brand-asset="${brandAsset}"`;
1536
+ if (bgStyleClass) dataAttrs += ` data-bg-style="${bgStyleClass}"`;
1537
+ // Add paint attribute for ClickFunnels export
1538
+ if (paint) dataAttrs += ` data-paint-colors="${paint}"`;
1539
+ // Add styleguide data attributes for ClickFunnels export
1540
+ if (isShadowStyleguide)
1541
+ dataAttrs += ` data-style-guide-shadow="${shadow}"`;
1542
+ if (isBorderStyleguide)
1543
+ dataAttrs += ` data-style-guide-border="${border}"`;
1544
+ if (isCornerStyleguide)
1545
+ dataAttrs += ` data-style-guide-corner="${corner}"`;
1546
+
1547
+ // Check if we have separate corners
1548
+ const hasSeparateCorners =
1549
+ roundedTl || roundedTr || roundedBl || roundedBr;
1550
+ if (hasSeparateCorners) dataAttrs += ' data-separate-corners="true"';
1551
+
1552
+ // Build overlay element and content wrapper if overlay specified
1553
+ let overlayHtml = "";
1554
+ let contentHtml = getContent(this);
1555
+ if (overlay) {
1556
+ overlayHtml = `<div class="cf-overlay" style="position:absolute;inset:0;background:${overlay};pointer-events:none;z-index:1;border-radius:inherit;"></div>`;
1557
+ contentHtml = `<div style="position:relative;z-index:2;">${contentHtml}</div>`;
1558
+ }
1559
+
1560
+ // Build ID attribute for scroll-to and custom CSS targeting
1561
+ // ID goes on col-inner so custom CSS targets the styled element, not the structural wrapper
1562
+ const idAttr = elementId ? ` id="${elementId}"` : "";
1563
+ const showAttr = show ? ` data-show="${show}"` : "";
1564
+
1565
+ // Only render col-inner if there's styling, otherwise just wrap content
1566
+ let innerHtml;
1567
+ if (hasColInnerStyling) {
1568
+ innerHtml = `<div${idAttr} ${dataAttrs} style="${buildStyle(
1569
+ innerStyles
1570
+ )}">${overlayHtml}${contentHtml}</div>`;
1571
+ } else {
1572
+ innerHtml = `<div${idAttr} class="col-inner" style="${buildStyle(
1573
+ innerStyles
1574
+ )}">${contentHtml}</div>`;
1575
+ }
1576
+
1577
+ this.outerHTML = `
1578
+ <div data-type="ColContainer/V1" data-span="${span}" data-col-direction="${align}"${showAttr} style="${buildStyle(
1579
+ colStyles
1580
+ )}">
1581
+ ${innerHtml}
1582
+ </div>
1583
+ `;
1584
+ }
1585
+ }
1586
+
1587
+ /**
1588
+ * <cf-col-inner> - Inner column wrapper (required inside cf-col)
1589
+ *
1590
+ * Attributes:
1591
+ * bg - Background color
1592
+ * bg-image - Background image URL
1593
+ * gradient - CSS gradient
1594
+ * overlay - Overlay color
1595
+ * pt - Padding top
1596
+ * pb - Padding bottom
1597
+ * px - Padding horizontal (left & right unified)
1598
+ * mx - Margin horizontal (creates column gaps)
1599
+ * align - Text alignment: left, center, right
1600
+ * shadow - Shadow
1601
+ * rounded - Border radius
1602
+ * border - Border width
1603
+ * border-style - Border style
1604
+ * border-color - Border color
1605
+ */
1606
+ class CFColInner extends CFElement {
1607
+ render() {
1608
+ const bg = attr(this, "bg");
1609
+ const bgImage = attr(this, "bg-image");
1610
+ const bgStyle = attr(this, "bg-style");
1611
+ const gradient = attr(this, "gradient");
1612
+ const overlay = attr(this, "overlay");
1613
+ const pt = attr(this, "pt", "20px");
1614
+ const pb = attr(this, "pb", "20px");
1615
+ const px = attr(this, "px");
1616
+ const mx = attr(this, "mx", "16px");
1617
+ const align = attr(this, "align", "left");
1618
+ const shadow = attr(this, "shadow");
1619
+ const rounded = attr(this, "rounded");
1620
+ const roundedTl = attr(this, "rounded-tl");
1621
+ const roundedTr = attr(this, "rounded-tr");
1622
+ const roundedBl = attr(this, "rounded-bl");
1623
+ const roundedBr = attr(this, "rounded-br");
1624
+ const border = attr(this, "border");
1625
+ const borderStyle = attr(this, "border-style", "solid");
1626
+ const borderColor = attr(this, "border-color");
1627
+
1628
+ const styles = {
1629
+ height: "100%",
1630
+ position: "relative",
1631
+ "background-size": "cover",
1632
+ "background-position": "center",
1633
+ "background-repeat": "no-repeat",
1634
+ "padding-top": pt,
1635
+ "padding-bottom": pb,
1636
+ "text-align": align,
1637
+ "box-sizing": "border-box",
1638
+ "margin-left": mx,
1639
+ "margin-right": mx,
1640
+ };
1641
+
1642
+ if (gradient) {
1643
+ styles["background"] = gradient;
1644
+ } else if (bg) {
1645
+ styles["background-color"] = bg;
1646
+ }
1647
+ if (bgImage) styles["background-image"] = `url(${bgImage})`;
1648
+ if (px) {
1649
+ styles["padding-left"] = px;
1650
+ styles["padding-right"] = px;
1651
+ }
1652
+ if (shadow) styles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1653
+
1654
+ if (rounded)
1655
+ styles["border-radius"] = resolve(rounded, RADIUS) || rounded;
1656
+ if (roundedTl)
1657
+ styles["border-top-left-radius"] =
1658
+ resolve(roundedTl, RADIUS) || roundedTl;
1659
+ if (roundedTr)
1660
+ styles["border-top-right-radius"] =
1661
+ resolve(roundedTr, RADIUS) || roundedTr;
1662
+ if (roundedBl)
1663
+ styles["border-bottom-left-radius"] =
1664
+ resolve(roundedBl, RADIUS) || roundedBl;
1665
+ if (roundedBr)
1666
+ styles["border-bottom-right-radius"] =
1667
+ resolve(roundedBr, RADIUS) || roundedBr;
1668
+
1669
+ if (border) {
1670
+ styles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1671
+ styles["border-style"] = borderStyle;
1672
+ }
1673
+ if (borderColor) styles["border-color"] = borderColor;
1674
+
1675
+ const bgStyleClass = bgImage ? getBgStyleClass(bgStyle) : "";
1676
+ const classes = ["col-inner"];
1677
+ if (bgStyleClass) classes.push(bgStyleClass);
1678
+ let dataAttrs = `class="${classes.join(" ")}" data-type="ColInner/V1"`;
1679
+ if (overlay) dataAttrs += ` data-overlay="${overlay}"`;
1680
+ if (bg) dataAttrs += ` data-bg="${bg}"`;
1681
+ if (bgImage) dataAttrs += ` data-bg-image="${bgImage}"`;
1682
+ if (bgStyleClass) dataAttrs += ` data-bg-style="${bgStyleClass}"`;
1683
+ const hasSeparateCorners =
1684
+ roundedTl || roundedTr || roundedBl || roundedBr;
1685
+ if (hasSeparateCorners) dataAttrs += ' data-separate-corners="true"';
1686
+
1687
+ let overlayHtml = "";
1688
+ let contentHtml = getContent(this);
1689
+ if (overlay) {
1690
+ overlayHtml = `<div class="cf-overlay" style="position:absolute;inset:0;background:${overlay};pointer-events:none;z-index:1;border-radius:inherit;"></div>`;
1691
+ contentHtml = `<div style="position:relative;z-index:2;">${contentHtml}</div>`;
1692
+ }
1693
+
1694
+ this.outerHTML = `
1695
+ <div ${dataAttrs} style="${buildStyle(styles)}">
1696
+ ${overlayHtml}${contentHtml}
1697
+ </div>
1698
+ `;
1699
+ }
1700
+ }
1701
+
1702
+ /**
1703
+ * <cf-flex> - Flex container for advanced layouts
1704
+ *
1705
+ * Attributes:
1706
+ * direction - row, col, row-reverse, col-reverse
1707
+ * justify - start, center, end, between, around, evenly
1708
+ * items - start, center, end, stretch, baseline
1709
+ * wrap - true/false
1710
+ * gap - Gap between items (e.g., "16px")
1711
+ * bg - Background color
1712
+ * gradient - CSS gradient
1713
+ * p - Padding all sides
1714
+ * px - Padding horizontal
1715
+ * py - Padding vertical
1716
+ * pt - Padding top
1717
+ * pb - Padding bottom
1718
+ * mt - Margin top (NO margin-bottom!)
1719
+ * shadow - Shadow preset or styleguide ref (style1-3)
1720
+ * rounded - Border radius
1721
+ * corner - Styleguide corner ref (style1-3)
1722
+ * border - Border width or styleguide ref (style1-3)
1723
+ * border-style - Border style
1724
+ * border-color - Border color
1725
+ * width - Width (percentage or px)
1726
+ * height - Height (px)
1727
+ * paint - Styleguide paint theme: lightest, light, colored, dark, darkest
1728
+ */
1729
+ class CFFlex extends CFElement {
1730
+ render() {
1731
+ const elementId = attr(this, "element-id");
1732
+ const direction = attr(this, "direction", "row");
1733
+ const justify = attr(this, "justify", "start");
1734
+ const items = attr(this, "items", "start");
1735
+ const wrap = attr(this, "wrap");
1736
+ const gap = attr(this, "gap");
1737
+ const bg = attr(this, "bg");
1738
+ const gradient = attr(this, "gradient");
1739
+ const paint = attr(this, "paint");
1740
+ const p = attr(this, "p");
1741
+ const px = attr(this, "px");
1742
+ const py = attr(this, "py");
1743
+ const pt = attr(this, "pt");
1744
+ const pb = attr(this, "pb");
1745
+ const mt = attr(this, "mt");
1746
+ const shadow = attr(this, "shadow");
1747
+ const rounded = attr(this, "rounded");
1748
+ const corner = attr(this, "corner");
1749
+ const border = attr(this, "border");
1750
+ const borderStyle = attr(this, "border-style", "solid");
1751
+ const borderColor = attr(this, "border-color");
1752
+ const width = attr(this, "width");
1753
+ const height = attr(this, "height");
1754
+ const show = attr(this, "show");
1755
+
1756
+ // Check if shadow/border/corner are styleguide references
1757
+ const isShadowStyleguide =
1758
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
1759
+ const isBorderStyleguide =
1760
+ border && styleguideManager.isStyleguideRef(border, "border");
1761
+ const isCornerStyleguide =
1762
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
1763
+
1764
+ // Map direction
1765
+ const flexDirection =
1766
+ {
1767
+ row: "row",
1768
+ col: "column",
1769
+ "row-reverse": "row-reverse",
1770
+ "col-reverse": "column-reverse",
1771
+ }[direction] || "row";
1772
+
1773
+ // Map justify
1774
+ const justifyContent =
1775
+ {
1776
+ start: "flex-start",
1777
+ center: "center",
1778
+ end: "flex-end",
1779
+ between: "space-between",
1780
+ around: "space-around",
1781
+ evenly: "space-evenly",
1782
+ }[justify] || "flex-start";
1783
+
1784
+ // Map align items
1785
+ const alignItems =
1786
+ {
1787
+ start: "flex-start",
1788
+ center: "center",
1789
+ end: "flex-end",
1790
+ stretch: "stretch",
1791
+ baseline: "baseline",
1792
+ }[items] || "center";
1793
+
1794
+ // NOTE: Always centered horizontally with margin-left/right auto
1795
+ const styles = {
1796
+ display: "flex",
1797
+ "flex-direction": flexDirection,
1798
+ "justify-content": justifyContent,
1799
+ "align-items": alignItems,
1800
+ "box-sizing": "border-box",
1801
+ "margin-left": "auto",
1802
+ "margin-right": "auto",
1803
+ };
1804
+
1805
+ if (wrap === "true" || wrap === "") styles["flex-wrap"] = "wrap";
1806
+ // Convert gap to em (16px = 1em), default to 1.5em (ClickFunnels default)
1807
+ if (gap) {
1808
+ if (gap.endsWith("px")) {
1809
+ const pxValue = parseFloat(gap);
1810
+ styles["gap"] = pxValue / 16 + "em";
1811
+ } else {
1812
+ styles["gap"] = gap;
1813
+ }
1814
+ } else {
1815
+ styles["gap"] = "1.5em";
1816
+ }
1817
+ // Only apply bg if not using paint (styleguide handles paint backgrounds)
1818
+ if (!paint) {
1819
+ if (gradient) {
1820
+ styles["background"] = gradient;
1821
+ } else if (bg) {
1822
+ styles["background-color"] = bg;
1823
+ }
1824
+ }
1825
+ // Handle padding - always use individual properties for parser compatibility
1826
+ if (p) {
1827
+ styles["padding-top"] = p;
1828
+ styles["padding-bottom"] = p;
1829
+ styles["padding-left"] = p;
1830
+ styles["padding-right"] = p;
1831
+ }
1832
+ if (px) {
1833
+ styles["padding-left"] = px;
1834
+ styles["padding-right"] = px;
1835
+ }
1836
+ if (py) {
1837
+ styles["padding-top"] = py;
1838
+ styles["padding-bottom"] = py;
1839
+ }
1840
+ if (pt) styles["padding-top"] = pt;
1841
+ if (pb) styles["padding-bottom"] = pb;
1842
+ if (mt) styles["margin-top"] = mt;
1843
+
1844
+ // Shadow: use inline if not styleguide ref
1845
+ if (shadow && !isShadowStyleguide) {
1846
+ styles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1847
+ }
1848
+
1849
+ // Rounded/corner: prefer corner styleguide ref, fallback to rounded
1850
+ if (!isCornerStyleguide && (rounded || corner)) {
1851
+ styles["border-radius"] = resolve(rounded || corner, RADIUS) || rounded || corner;
1852
+ }
1853
+
1854
+ // Border: use inline if not styleguide ref
1855
+ if (border && !isBorderStyleguide) {
1856
+ styles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1857
+ styles["border-style"] = borderStyle;
1858
+ }
1859
+ if (borderColor) styles["border-color"] = borderColor;
1860
+ // Only set width if explicitly provided - let flex shrink to fit content by default
1861
+ if (width) styles["width"] = width;
1862
+ if (height) styles["height"] = height;
1863
+
1864
+ // Data attributes - include all flex properties for pagetree parser
1865
+ let dataAttrs = 'data-type="FlexContainer/V1"';
1866
+ if (show) dataAttrs += ` data-show="${show}"`;
1867
+ dataAttrs += ` data-direction="${direction}"`;
1868
+ dataAttrs += ` data-justify="${justify}"`;
1869
+ dataAttrs += ` data-items="${items}"`;
1870
+ if (wrap === "true" || wrap === "") dataAttrs += ' data-wrap="true"';
1871
+ // Store gap in em format for parser
1872
+ const gapValue = styles["gap"];
1873
+ dataAttrs += ` data-gap="${gapValue}"`;
1874
+ // Store width/height with units for proper roundtrip
1875
+ if (width) dataAttrs += ` data-width="${width}"`;
1876
+ if (height) dataAttrs += ` data-height="${height}"`;
1877
+
1878
+ // Add styleguide data attributes
1879
+ if (paint) dataAttrs += ` data-paint-colors="${paint}"`;
1880
+ if (isShadowStyleguide) dataAttrs += ` data-style-guide-shadow="${shadow}"`;
1881
+ if (isBorderStyleguide) dataAttrs += ` data-style-guide-border="${border}"`;
1882
+ if (isCornerStyleguide) dataAttrs += ` data-style-guide-corner="${corner}"`;
1883
+
1884
+ // Build ID attribute for scroll-to and show-hide targeting
1885
+ const idAttr = elementId ? ` id="${elementId}"` : "";
1886
+
1887
+ this.outerHTML = `
1888
+ <div${idAttr} ${dataAttrs} style="${buildStyle(styles)}">
1889
+ ${getContent(this)}
1890
+ </div>
1891
+ `;
1892
+ }
1893
+ }
1894
+
1895
+ /**
1896
+ * <cf-popup> - Popup/Modal container
1897
+ *
1898
+ * Each ClickFunnels page can have one popup that can be triggered to show.
1899
+ * Renders as ModalContainer/V1 in pagetree.
1900
+ *
1901
+ * Attributes:
1902
+ * width - Modal width (default: 750px)
1903
+ * overlay - Overlay background color (default: rgba(0,0,0,0.5))
1904
+ * rounded - Border radius for modal (default: 16px)
1905
+ * border - Border width
1906
+ * border-color - Border color
1907
+ * shadow - Box shadow
1908
+ * mt - Margin top (default: 45px)
1909
+ * mb - Margin bottom (default: 10px)
1910
+ * px - Horizontal padding on overlay wrapper
1911
+ *
1912
+ * Children: cf-section elements that form the popup content
1913
+ */
1914
+ class CFPopup extends CFElement {
1915
+ render() {
1916
+ const width = attr(this, "width", "750px");
1917
+ const overlay = attr(this, "overlay", "rgba(0,0,0,0.5)");
1918
+ const rounded = attr(this, "rounded", "16px");
1919
+ const border = attr(this, "border");
1920
+ const borderColor = attr(this, "border-color", "#000000");
1921
+ const shadow = attr(this, "shadow");
1922
+ const mt = attr(this, "mt", "45px");
1923
+ const mb = attr(this, "mb", "10px");
1924
+ const px = attr(this, "px", "0");
1925
+
1926
+ // Modal container styles (the actual popup box)
1927
+ const modalStyles = {
1928
+ "max-width": "100%",
1929
+ "margin-top": mt,
1930
+ "margin-bottom": mb,
1931
+ "margin-left": "auto",
1932
+ "margin-right": "auto",
1933
+ "border-radius": resolve(rounded, RADIUS) || rounded,
1934
+ "background-color": "#ffffff",
1935
+ "position": "relative",
1936
+ "box-sizing": "border-box",
1937
+ };
1938
+
1939
+ if (border) {
1940
+ modalStyles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
1941
+ modalStyles["border-style"] = "solid";
1942
+ modalStyles["border-color"] = borderColor;
1943
+ }
1944
+ if (shadow) {
1945
+ modalStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
1946
+ }
1947
+
1948
+ // Inner container styles (holds the width)
1949
+ const innerStyles = {
1950
+ width: width,
1951
+ "max-width": "100%",
1952
+ "margin-left": "auto",
1953
+ "margin-right": "auto",
1954
+ };
1955
+
1956
+ // Wrapper styles (overlay background)
1957
+ const wrapperStyles = {
1958
+ position: "fixed",
1959
+ inset: "0",
1960
+ "background-color": overlay,
1961
+ display: "none",
1962
+ "align-items": "flex-start",
1963
+ "justify-content": "center",
1964
+ "overflow-y": "auto",
1965
+ "z-index": "9999",
1966
+ "padding-left": px,
1967
+ "padding-right": px,
1968
+ };
1969
+
1970
+ // Build data attributes
1971
+ let dataAttrs = 'data-type="ModalContainer/V1"';
1972
+ dataAttrs += ` data-popup-width="${width}"`;
1973
+ dataAttrs += ` data-popup-overlay="${overlay}"`;
1974
+ dataAttrs += ` data-popup-rounded="${rounded}"`;
1975
+ if (border) dataAttrs += ` data-popup-border="${border}"`;
1976
+ if (borderColor) dataAttrs += ` data-popup-border-color="${borderColor}"`;
1977
+ if (shadow) dataAttrs += ` data-popup-shadow="${shadow}"`;
1978
+
1979
+ // Get the resolved border radius value for the first section
1980
+ const resolvedRounded = resolve(rounded, RADIUS) || rounded;
1981
+
1982
+ // Process content to add rounded to first cf-section
1983
+ let content = getContent(this);
1984
+ // Add data attribute to pass rounded value to first section
1985
+ const contentWithRounded = content.replace(
1986
+ /<cf-section/i,
1987
+ `<cf-section data-popup-rounded="${resolvedRounded}"`
1988
+ );
1989
+
1990
+ this.outerHTML = `
1991
+ <div class="cf-popup-wrapper" ${dataAttrs} style="${buildStyle(wrapperStyles)}">
1992
+ <div class="cf-popup-modal containerModal" style="${buildStyle(modalStyles)}">
1993
+ <button class="cf-popup-close" style="position:absolute;top:-12px;right:-12px;width:28px;height:28px;border:none;background:#000000;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;z-index:10;box-shadow:0 2px 8px rgba(0,0,0,0.3);" onclick="this.closest('.cf-popup-wrapper').style.display='none'">
1994
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1995
+ <line x1="18" y1="6" x2="6" y2="18"></line>
1996
+ <line x1="6" y1="6" x2="18" y2="18"></line>
1997
+ </svg>
1998
+ </button>
1999
+ <div class="elModalInnerContainer" style="${buildStyle(innerStyles)}">
2000
+ ${contentWithRounded}
2001
+ </div>
2002
+ </div>
2003
+ </div>
2004
+ `;
2005
+ }
2006
+ }
2007
+
2008
+ // ==========================================================================
2009
+ // TEXT ELEMENTS
2010
+ // ==========================================================================
2011
+
2012
+ /**
2013
+ * <cf-headline> - Primary heading
2014
+ *
2015
+ * Attributes:
2016
+ * size - Font size (e.g., "48px", "36px")
2017
+ * weight - Font weight: thin, light, normal, medium, semibold, bold, extrabold, black
2018
+ * color - Text color
2019
+ * align - Text alignment: left, center, right
2020
+ * leading - Line height: none, tight, snug, normal, relaxed, loose, or percentage
2021
+ * tracking - Letter spacing (e.g., "-0.02em", "0.05em")
2022
+ * transform - Text transform: uppercase, lowercase, capitalize
2023
+ * pt - Wrapper padding top
2024
+ * pb - Wrapper padding bottom
2025
+ * mt - Wrapper margin top
2026
+ * tag - HTML tag: h1, h2, h3, span (default: h1)
2027
+ * icon - FontAwesome icon (e.g., "fas fa-star") - OLD format only
2028
+ * icon-align - Icon position: left, right (default: left)
2029
+ */
2030
+ class CFHeadline extends CFElement {
2031
+ render() {
2032
+ const size = attr(this, "size", "48px");
2033
+ const weight = attr(this, "weight", "bold");
2034
+ const font = attr(this, "font");
2035
+ const color = attr(this, "color");
2036
+ const hasExplicitColor = this.hasAttribute("color");
2037
+ const align = attr(this, "align", "center");
2038
+ const leading = attr(this, "leading", "tight");
2039
+ const tracking = attr(this, "tracking");
2040
+ const transform = attr(this, "transform");
2041
+ const pt = attr(this, "pt", "0");
2042
+ const pb = attr(this, "pb", "0");
2043
+ const mt = attr(this, "mt", "0");
2044
+ const tag = attr(this, "tag", "h1");
2045
+ const icon = attr(this, "icon");
2046
+ const iconAlign = attr(this, "icon-align", "left");
2047
+
2048
+ // NOTE: No width set - allows flex layout to work properly
2049
+ // In columns, block elements naturally take 100% width
2050
+ // In flex containers, they'll size to content
2051
+ const wrapperStyles = {
2052
+ "padding-top": pt,
2053
+ "padding-bottom": pb,
2054
+ "margin-top": mt || "20px",
2055
+ "box-sizing": "border-box",
2056
+ };
2057
+
2058
+ // Resolve size preset to pixel value for CSS, keep original for data attr
2059
+ // First try styleguide typescale, then static FONT_SIZES, then use as-is
2060
+ const resolvedSize = styleguideManager.resolveSize(size, 'headline') || resolve(size, FONT_SIZES) || size;
2061
+
2062
+ const textStyles = {
2063
+ margin: "0",
2064
+ "font-size": resolvedSize,
2065
+ "font-weight": resolve(weight, FONT_WEIGHTS) || weight,
2066
+ "text-align": align,
2067
+ "line-height": resolve(leading, LINE_HEIGHTS) || leading,
2068
+ };
2069
+ if (hasExplicitColor && color) textStyles.color = `${color} !important`;
2070
+ if (font) textStyles["font-family"] = font;
2071
+ if (tracking) textStyles["letter-spacing"] = tracking;
2072
+ if (transform) textStyles["text-transform"] = transform;
2073
+
2074
+ // Build data attributes for round-trip conversion
2075
+ // Store original size (preset or px) for reliable roundtrip
2076
+ let dataAttrs = 'data-type="Headline/V1"';
2077
+ dataAttrs += ` data-size="${size}"`;
2078
+ dataAttrs += ` data-weight="${weight}"`;
2079
+ if (hasExplicitColor && color) {
2080
+ dataAttrs += ` data-color="${color}" data-color-explicit="true"`;
2081
+ }
2082
+ dataAttrs += ` data-align="${align}"`;
2083
+ dataAttrs += ` data-leading="${leading}"`;
2084
+ if (font) dataAttrs += ` data-font="${font}"`;
2085
+ if (tracking) dataAttrs += ` data-tracking="${tracking}"`;
2086
+ if (transform) dataAttrs += ` data-transform="${transform}"`;
2087
+ if (pt !== "0") dataAttrs += ` data-pt="${pt}"`;
2088
+ if (pb !== "0") dataAttrs += ` data-pb="${pb}"`;
2089
+ if (mt !== "0") dataAttrs += ` data-mt="${mt}"`;
2090
+ if (icon) dataAttrs += ` data-icon="${icon}"`;
2091
+ if (icon && iconAlign !== "left") dataAttrs += ` data-icon-align="${iconAlign}"`;
2092
+
2093
+ // Build icon HTML if present
2094
+ let iconHtml = "";
2095
+ if (icon) {
2096
+ const iconSpacing = iconAlign === "left" ? "margin-right: 8px;" : "margin-left: 8px;";
2097
+ iconHtml = `<i class="${icon}" style="${iconSpacing}"></i>`;
2098
+ }
2099
+
2100
+ // Combine text with icon
2101
+ const textWithIcon = iconAlign === "left"
2102
+ ? iconHtml + getContent(this)
2103
+ : getContent(this) + iconHtml;
2104
+
2105
+ // Animation attributes
2106
+ const animationAttrs = buildAnimationAttrs(this);
2107
+ dataAttrs += animationAttrs;
2108
+
2109
+ this.outerHTML = `
2110
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2111
+ <${tag} style="${buildStyle(textStyles)}">${textWithIcon}</${tag}>
2112
+ </div>
2113
+ `;
2114
+ }
2115
+ }
2116
+
2117
+ /**
2118
+ * <cf-subheadline> - Secondary heading
2119
+ * Same attributes as cf-headline (including icon and icon-align)
2120
+ */
2121
+ class CFSubheadline extends CFElement {
2122
+ render() {
2123
+ const size = attr(this, "size", "24px");
2124
+ const weight = attr(this, "weight", "normal");
2125
+ const font = attr(this, "font");
2126
+ const color = attr(this, "color");
2127
+ const hasExplicitColor = this.hasAttribute("color");
2128
+ const align = attr(this, "align", "center");
2129
+ const leading = attr(this, "leading", "relaxed");
2130
+ const tracking = attr(this, "tracking");
2131
+ const transform = attr(this, "transform");
2132
+ const pt = attr(this, "pt", "0");
2133
+ const pb = attr(this, "pb", "0");
2134
+ const mt = attr(this, "mt", "0");
2135
+ const tag = attr(this, "tag", "h2");
2136
+ const icon = attr(this, "icon");
2137
+ const iconAlign = attr(this, "icon-align", "left");
2138
+
2139
+ // NOTE: No width set - allows flex layout to work properly
2140
+ const wrapperStyles = {
2141
+ "padding-top": pt,
2142
+ "padding-bottom": pb,
2143
+ "margin-top": mt,
2144
+ "box-sizing": "border-box",
2145
+ };
2146
+
2147
+ // Resolve size preset to pixel value for CSS, keep original for data attr
2148
+ // First try styleguide typescale, then static FONT_SIZES, then use as-is
2149
+ const resolvedSize = styleguideManager.resolveSize(size, 'subheadline') || resolve(size, FONT_SIZES) || size;
2150
+
2151
+ const textStyles = {
2152
+ margin: "0",
2153
+ "font-size": resolvedSize,
2154
+ "font-weight": resolve(weight, FONT_WEIGHTS) || weight,
2155
+ "text-align": align,
2156
+ "line-height": resolve(leading, LINE_HEIGHTS) || leading,
2157
+ };
2158
+ if (hasExplicitColor && color) textStyles.color = `${color} !important`;
2159
+ if (font) textStyles["font-family"] = font;
2160
+ if (tracking) textStyles["letter-spacing"] = tracking;
2161
+ if (transform) textStyles["text-transform"] = transform;
2162
+
2163
+ // Build data attributes for round-trip conversion
2164
+ // Store original size (preset or px) for reliable roundtrip
2165
+ let dataAttrs = 'data-type="SubHeadline/V1"';
2166
+ dataAttrs += ` data-size="${size}"`;
2167
+ dataAttrs += ` data-weight="${weight}"`;
2168
+ if (hasExplicitColor && color) {
2169
+ dataAttrs += ` data-color="${color}" data-color-explicit="true"`;
2170
+ }
2171
+ dataAttrs += ` data-align="${align}"`;
2172
+ dataAttrs += ` data-leading="${leading}"`;
2173
+ if (font) dataAttrs += ` data-font="${font}"`;
2174
+ if (tracking) dataAttrs += ` data-tracking="${tracking}"`;
2175
+ if (transform) dataAttrs += ` data-transform="${transform}"`;
2176
+ if (pt !== "0") dataAttrs += ` data-pt="${pt}"`;
2177
+ if (pb !== "0") dataAttrs += ` data-pb="${pb}"`;
2178
+ if (mt !== "0") dataAttrs += ` data-mt="${mt}"`;
2179
+ if (icon) dataAttrs += ` data-icon="${icon}"`;
2180
+ if (icon && iconAlign !== "left") dataAttrs += ` data-icon-align="${iconAlign}"`;
2181
+
2182
+ // Build icon HTML if present
2183
+ let iconHtml = "";
2184
+ if (icon) {
2185
+ const iconSpacing = iconAlign === "left" ? "margin-right: 8px;" : "margin-left: 8px;";
2186
+ iconHtml = `<i class="${icon}" style="${iconSpacing}"></i>`;
2187
+ }
2188
+
2189
+ // Combine text with icon
2190
+ const textWithIcon = iconAlign === "left"
2191
+ ? iconHtml + getContent(this)
2192
+ : getContent(this) + iconHtml;
2193
+
2194
+ // Animation attributes
2195
+ const animationAttrs = buildAnimationAttrs(this);
2196
+ dataAttrs += animationAttrs;
2197
+
2198
+ this.outerHTML = `
2199
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2200
+ <${tag} style="${buildStyle(textStyles)}">${textWithIcon}</${tag}>
2201
+ </div>
2202
+ `;
2203
+ }
2204
+ }
2205
+
2206
+ /**
2207
+ * <cf-paragraph> - Body text
2208
+ * Same attributes as cf-headline (including icon and icon-align)
2209
+ */
2210
+ class CFParagraph extends CFElement {
2211
+ render() {
2212
+ const size = attr(this, "size", "16px");
2213
+ const weight = attr(this, "weight", "normal");
2214
+ const font = attr(this, "font");
2215
+ const color = attr(this, "color");
2216
+ const hasExplicitColor = this.hasAttribute("color");
2217
+ const align = attr(this, "align", "center");
2218
+ const leading = attr(this, "leading", "relaxed");
2219
+ const tracking = attr(this, "tracking");
2220
+ const transform = attr(this, "transform");
2221
+ const pt = attr(this, "pt", "0");
2222
+ const pb = attr(this, "pb", "0");
2223
+ const px = attr(this, "px");
2224
+ const mt = attr(this, "mt", "0");
2225
+ const bg = attr(this, "bg");
2226
+ const icon = attr(this, "icon");
2227
+ const iconAlign = attr(this, "icon-align", "left");
2228
+
2229
+ // NOTE: No width set - allows flex layout to work properly
2230
+ const wrapperStyles = {
2231
+ "padding-top": pt,
2232
+ "padding-bottom": pb,
2233
+ "margin-top": mt,
2234
+ "box-sizing": "border-box",
2235
+ };
2236
+ if (bg) {
2237
+ wrapperStyles["background-color"] = bg;
2238
+ }
2239
+ if (px) {
2240
+ wrapperStyles["padding-left"] = px;
2241
+ wrapperStyles["padding-right"] = px;
2242
+ }
2243
+
2244
+ // Resolve size preset to pixel value for CSS, keep original for data attr
2245
+ // First try styleguide typescale, then static FONT_SIZES, then use as-is
2246
+ const resolvedSize = styleguideManager.resolveSize(size, 'paragraph') || resolve(size, FONT_SIZES) || size;
2247
+
2248
+ const textStyles = {
2249
+ margin: "0",
2250
+ "font-size": resolvedSize,
2251
+ "font-weight": resolve(weight, FONT_WEIGHTS) || weight,
2252
+ "text-align": align,
2253
+ "line-height": resolve(leading, LINE_HEIGHTS) || leading,
2254
+ };
2255
+ if (hasExplicitColor && color) textStyles.color = `${color} !important`;
2256
+ if (font) textStyles["font-family"] = font;
2257
+ if (tracking) textStyles["letter-spacing"] = tracking;
2258
+ if (transform) textStyles["text-transform"] = transform;
2259
+
2260
+ // Build data attributes for round-trip conversion
2261
+ // Store original size (preset or px) for reliable roundtrip
2262
+ let dataAttrs = 'data-type="Paragraph/V1"';
2263
+ dataAttrs += ` data-size="${size}"`;
2264
+ dataAttrs += ` data-weight="${weight}"`;
2265
+ if (hasExplicitColor && color) {
2266
+ dataAttrs += ` data-color="${color}" data-color-explicit="true"`;
2267
+ }
2268
+ dataAttrs += ` data-align="${align}"`;
2269
+ dataAttrs += ` data-leading="${leading}"`;
2270
+ if (font) dataAttrs += ` data-font="${font}"`;
2271
+ if (tracking) dataAttrs += ` data-tracking="${tracking}"`;
2272
+ if (transform) dataAttrs += ` data-transform="${transform}"`;
2273
+ if (pt !== "0") dataAttrs += ` data-pt="${pt}"`;
2274
+ if (pb !== "0") dataAttrs += ` data-pb="${pb}"`;
2275
+ if (mt !== "0") dataAttrs += ` data-mt="${mt}"`;
2276
+ if (px) dataAttrs += ` data-px="${px}"`;
2277
+ if (bg) dataAttrs += ` data-bg="${bg}"`;
2278
+ if (icon) dataAttrs += ` data-icon="${icon}"`;
2279
+ if (icon && iconAlign !== "left") dataAttrs += ` data-icon-align="${iconAlign}"`;
2280
+
2281
+ // Build icon HTML if present
2282
+ let iconHtml = "";
2283
+ if (icon) {
2284
+ const iconSpacing = iconAlign === "left" ? "margin-right: 8px;" : "margin-left: 8px;";
2285
+ iconHtml = `<i class="${icon}" style="${iconSpacing}"></i>`;
2286
+ }
2287
+
2288
+ // Combine text with icon
2289
+ const textWithIcon = iconAlign === "left"
2290
+ ? iconHtml + getContent(this)
2291
+ : getContent(this) + iconHtml;
2292
+
2293
+ // Animation attributes
2294
+ const animationAttrs = buildAnimationAttrs(this);
2295
+ dataAttrs += animationAttrs;
2296
+
2297
+ this.outerHTML = `
2298
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2299
+ <p style="${buildStyle(textStyles)}">${textWithIcon}</p>
2300
+ </div>
2301
+ `;
2302
+ }
2303
+ }
2304
+
2305
+ // ==========================================================================
2306
+ // INTERACTIVE ELEMENTS
2307
+ // ==========================================================================
2308
+
2309
+ /**
2310
+ * <cf-button> - CTA Button
2311
+ *
2312
+ * Attributes:
2313
+ * href - Link URL
2314
+ * style - Styleguide button style: style1, style2, style3
2315
+ * bg - Background color (ignored if style is set)
2316
+ * color - Text color (ignored if style is set)
2317
+ * size - Font size
2318
+ * weight - Font weight
2319
+ * px - Horizontal padding
2320
+ * py - Vertical padding
2321
+ * pt - Wrapper padding top
2322
+ * pb - Wrapper padding bottom
2323
+ * mt - Wrapper margin top
2324
+ * rounded - Border radius (ignored if style is set)
2325
+ * shadow - Box shadow (ignored if style is set)
2326
+ * align - Button alignment: left, center, right
2327
+ * full-width - Full width button (true/false)
2328
+ * subtext - Optional subtext below main text
2329
+ * subtext-color - Subtext color
2330
+ * icon - FontAwesome icon (e.g., "fas fa-arrow-right")
2331
+ * icon-position - Icon position: left, right
2332
+ */
2333
+ class CFButton extends CFElement {
2334
+ render() {
2335
+ const action = attr(this, "action", "link");
2336
+ const target = attr(this, "target", "_self");
2337
+ let href = attr(this, "href", "#");
2338
+
2339
+ // Action-specific attributes
2340
+ const scrollTarget = attr(this, "scroll-target");
2341
+ const showIds = attr(this, "show-ids");
2342
+ const hideIds = attr(this, "hide-ids");
2343
+
2344
+ // Map action to href
2345
+ if (action === "submit") {
2346
+ href = "#submit-form";
2347
+ } else if (action === "popup") {
2348
+ href = "#open-popup";
2349
+ } else if (action === "scroll" && scrollTarget) {
2350
+ href = `#scroll-${scrollTarget}`;
2351
+ } else if (action === "show-hide") {
2352
+ href = "#show-hide";
2353
+ } else if (action === "next-step") {
2354
+ href = "?next_funnel_step=true";
2355
+ } else if (action === "oto" || action === "one-click-upsell") {
2356
+ href = "#submit-oto";
2357
+ }
2358
+
2359
+ const buttonStyleRef = attr(this, "style");
2360
+ const isStyleguideButton =
2361
+ buttonStyleRef &&
2362
+ styleguideManager.isStyleguideRef(buttonStyleRef, "button");
2363
+
2364
+ // Always read attribute values - we need them for data attributes even if using styleguide
2365
+ const bg = attr(this, "bg", "#3b82f6");
2366
+ const color = attr(this, "color", "#ffffff");
2367
+ const size = attr(this, "size", "20px");
2368
+ const weight = attr(this, "weight", "bold");
2369
+ const px = attr(this, "px", "32px");
2370
+ const py = attr(this, "py", "16px");
2371
+ const pt = attr(this, "pt", "0");
2372
+ const pb = attr(this, "pb");
2373
+ const mt = attr(this, "mt", "0");
2374
+ const rounded = attr(this, "rounded", "default");
2375
+ const shadow = attr(this, "shadow");
2376
+ const borderColor = attr(this, "border-color");
2377
+ const borderWidth = attr(this, "border-width", "0");
2378
+ const align = attr(this, "align", "center");
2379
+ const fullWidth = attr(this, "full-width");
2380
+ const subtext = attr(this, "subtext");
2381
+ const subtextColor = attr(this, "subtext-color", "rgba(255,255,255,0.8)");
2382
+ const icon = attr(this, "icon");
2383
+ const iconPosition = attr(this, "icon-position", "left");
2384
+ const iconColor = attr(this, "icon-color", color);
2385
+
2386
+ // NOTE: No width set - allows flex layout to work properly
2387
+ const wrapperStyles = {
2388
+ "text-align": align,
2389
+ "padding-top": pt,
2390
+ "margin-top": mt,
2391
+ "box-sizing": "border-box",
2392
+ };
2393
+ if (pb) wrapperStyles["padding-bottom"] = pb;
2394
+
2395
+ const buttonStyles = {
2396
+ display: "inline-flex",
2397
+ "flex-direction": "column",
2398
+ "align-items": "center",
2399
+ "justify-content": "center",
2400
+ "text-decoration": "none",
2401
+ cursor: "pointer",
2402
+ "padding-left": px,
2403
+ "padding-right": px,
2404
+ "padding-top": py,
2405
+ "padding-bottom": py,
2406
+ "box-sizing": "border-box",
2407
+ };
2408
+
2409
+ // Only add inline styling if not using styleguide button
2410
+ if (!isStyleguideButton) {
2411
+ if (bg) buttonStyles["background-color"] = bg;
2412
+ if (rounded)
2413
+ buttonStyles["border-radius"] = resolve(rounded, RADIUS) || rounded;
2414
+ buttonStyles["border-style"] = "solid";
2415
+ buttonStyles["border-width"] = borderWidth;
2416
+ buttonStyles["border-color"] = borderColor || "transparent";
2417
+ if (shadow)
2418
+ buttonStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
2419
+ }
2420
+
2421
+ if (fullWidth === "true" || fullWidth === "")
2422
+ buttonStyles["width"] = "100%";
2423
+
2424
+ const textStyles = {
2425
+ display: "inline-flex",
2426
+ "align-items": "center",
2427
+ "justify-content": "center",
2428
+ "font-size": size,
2429
+ "font-weight": resolve(weight, FONT_WEIGHTS) || weight,
2430
+ };
2431
+
2432
+ // Only add color if not using styleguide button
2433
+ if (!isStyleguideButton && color) {
2434
+ textStyles.color = color;
2435
+ }
2436
+
2437
+ const iconStyles = {};
2438
+ if (!isStyleguideButton && iconColor) {
2439
+ iconStyles.color = iconColor;
2440
+ }
2441
+
2442
+ const subtextStyles = {
2443
+ display: "block",
2444
+ "text-align": "center",
2445
+ "font-size": "14px",
2446
+ "margin-top": "4px",
2447
+ };
2448
+ if (!isStyleguideButton) {
2449
+ subtextStyles.color = subtextColor;
2450
+ }
2451
+
2452
+ // Build icon HTML with margin
2453
+ let iconLeftHtml = "";
2454
+ let iconRightHtml = "";
2455
+ if (icon && iconPosition === "left") {
2456
+ const iconStyle = Object.keys(iconStyles).length
2457
+ ? `style="${buildStyle(iconStyles)}; margin-right: 10px;"`
2458
+ : 'style="margin-right: 10px;"';
2459
+ iconLeftHtml = `<i class="${icon}" ${iconStyle}></i>`;
2460
+ }
2461
+ if (icon && iconPosition === "right") {
2462
+ const iconStyle = Object.keys(iconStyles).length
2463
+ ? `style="${buildStyle(iconStyles)}; margin-left: 10px;"`
2464
+ : 'style="margin-left: 10px;"';
2465
+ iconRightHtml = `<i class="${icon}" ${iconStyle}></i>`;
2466
+ }
2467
+
2468
+ const subtextHtml = subtext
2469
+ ? `<span style="${buildStyle(subtextStyles)}">${subtext}</span>`
2470
+ : "";
2471
+
2472
+ // Build data attributes - include all styling values for round-trip conversion
2473
+ let wrapperDataAttrs = `data-type="Button/V1" data-href="${href}" data-target="${target}" data-action="${action}"`;
2474
+
2475
+ // Styling data attributes (for parser to read back)
2476
+ // Always include styling attrs so parser can output them
2477
+ if (isStyleguideButton) {
2478
+ wrapperDataAttrs += ` data-style-guide-button="${buttonStyleRef}"`;
2479
+ }
2480
+ if (bg) wrapperDataAttrs += ` data-bg="${bg}"`;
2481
+ if (color) wrapperDataAttrs += ` data-color="${color}"`;
2482
+ if (rounded) wrapperDataAttrs += ` data-rounded="${rounded}"`;
2483
+ if (shadow) wrapperDataAttrs += ` data-shadow="${shadow}"`;
2484
+ if (borderColor) wrapperDataAttrs += ` data-border-color="${borderColor}"`;
2485
+ if (borderWidth !== "0") wrapperDataAttrs += ` data-border-width="${borderWidth}"`;
2486
+ if (iconColor && iconColor !== color) wrapperDataAttrs += ` data-icon-color="${iconColor}"`;
2487
+
2488
+ // Common attributes
2489
+ wrapperDataAttrs += ` data-size="${size}"`;
2490
+ wrapperDataAttrs += ` data-weight="${weight}"`;
2491
+ wrapperDataAttrs += ` data-px="${px}"`;
2492
+ wrapperDataAttrs += ` data-py="${py}"`;
2493
+ if (align !== "center") wrapperDataAttrs += ` data-align="${align}"`;
2494
+ if (fullWidth === "true" || fullWidth === "") wrapperDataAttrs += ` data-full-width="true"`;
2495
+ if (subtext) wrapperDataAttrs += ` data-subtext="${subtext}"`;
2496
+ if (subtextColor !== "rgba(255,255,255,0.8)") wrapperDataAttrs += ` data-subtext-color="${subtextColor}"`;
2497
+ if (icon) {
2498
+ wrapperDataAttrs += ` data-icon="${icon}"`;
2499
+ wrapperDataAttrs += ` data-icon-position="${iconPosition}"`;
2500
+ }
2501
+
2502
+ // Action-specific data attributes for parser
2503
+ if (action === "show-hide") {
2504
+ wrapperDataAttrs += ` data-elbuttontype="showHide"`;
2505
+ if (showIds) wrapperDataAttrs += ` data-show-ids="${showIds}"`;
2506
+ if (hideIds) wrapperDataAttrs += ` data-hide-ids="${hideIds}"`;
2507
+ }
2508
+ if (action === "scroll" && scrollTarget) {
2509
+ wrapperDataAttrs += ` data-scroll-target="${scrollTarget}"`;
2510
+ }
2511
+
2512
+ // Animation attributes
2513
+ const animationAttrs = buildAnimationAttrs(this);
2514
+ wrapperDataAttrs += animationAttrs;
2515
+
2516
+ this.outerHTML = `
2517
+ <div ${wrapperDataAttrs} style="${buildStyle(wrapperStyles)}">
2518
+ <a href="${href}" target="${target}" style="${buildStyle(
2519
+ buttonStyles
2520
+ )}">
2521
+ <span style="${buildStyle(textStyles)}">
2522
+ ${iconLeftHtml}${getContent(this)}${iconRightHtml}
2523
+ </span>
2524
+ ${subtextHtml}
2525
+ </a>
2526
+ </div>
2527
+ `;
2528
+ }
2529
+ }
2530
+
2531
+ // ==========================================================================
2532
+ // MEDIA ELEMENTS
2533
+ // ==========================================================================
2534
+
2535
+ /**
2536
+ * <cf-image> - Image element
2537
+ *
2538
+ * Attributes:
2539
+ * src - Image URL (required)
2540
+ * alt - Alt text
2541
+ * width - Width (e.g., "400px", "100%")
2542
+ * height - Height
2543
+ * align - Alignment: left, center, right
2544
+ * rounded - Border radius
2545
+ * corner - Styleguide corner ref (style1-3)
2546
+ * shadow - Box shadow or styleguide ref (style1-3)
2547
+ * border - Border width or styleguide ref (style1-3)
2548
+ * border-style - Border style
2549
+ * border-color - Border color
2550
+ * object-fit - Object fit: cover, contain, fill
2551
+ * pt - Wrapper padding top
2552
+ * pb - Wrapper padding bottom
2553
+ * mt - Wrapper margin top
2554
+ * brand-asset - Brand asset type to use: logo, background, pattern, icon, product_image
2555
+ * When set, the src will be replaced with the active brand asset URL if available
2556
+ */
2557
+ class CFImage extends CFElement {
2558
+ render() {
2559
+ let src = attr(this, "src", "");
2560
+ const alt = attr(this, "alt", "");
2561
+ const width = attr(this, "width", "100%");
2562
+ const height = attr(this, "height");
2563
+ const align = attr(this, "align", "center");
2564
+ const rounded = attr(this, "rounded");
2565
+ const corner = attr(this, "corner");
2566
+ const shadow = attr(this, "shadow");
2567
+ const border = attr(this, "border");
2568
+ const borderStyle = attr(this, "border-style", "solid");
2569
+ const borderColor = attr(this, "border-color");
2570
+ const objectFit = attr(this, "object-fit");
2571
+ const pt = attr(this, "pt", "0");
2572
+ const pb = attr(this, "pb", "0");
2573
+ const mt = attr(this, "mt", "0");
2574
+ const brandAsset = attr(this, "brand-asset");
2575
+
2576
+ // Check if shadow/border/corner are styleguide references
2577
+ const isShadowStyleguide =
2578
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
2579
+ const isBorderStyleguide =
2580
+ border && styleguideManager.isStyleguideRef(border, "border");
2581
+ const isCornerStyleguide =
2582
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
2583
+
2584
+ // If brand-asset is specified, try to get the asset URL from brand assets manager
2585
+ if (brandAsset && brandAssetsManager.hasAsset(brandAsset)) {
2586
+ const brandAssetUrl = brandAssetsManager.getAssetUrl(brandAsset);
2587
+ if (brandAssetUrl) {
2588
+ src = brandAssetUrl;
2589
+ }
2590
+ }
2591
+
2592
+ // NOTE: No width set - allows flex layout to work properly
2593
+ const wrapperStyles = {
2594
+ "text-align": align,
2595
+ "padding-top": pt,
2596
+ "padding-bottom": pb,
2597
+ "margin-top": mt,
2598
+ "box-sizing": "border-box",
2599
+ };
2600
+
2601
+ // When shadow + rounded/corner are both present with inline styles, use inner container
2602
+ // This ensures box-shadow follows the border-radius properly
2603
+ const hasInlineRadius = !isCornerStyleguide && (rounded || corner);
2604
+ const hasInlineShadow = shadow && !isShadowStyleguide;
2605
+ const needsInnerContainer = hasInlineRadius && hasInlineShadow;
2606
+
2607
+ const resolvedRadius = (rounded || corner) ? (resolve(rounded || corner, RADIUS) || rounded || corner) : null;
2608
+ const resolvedShadow = shadow ? (resolve(shadow, SHADOWS) || shadow) : null;
2609
+
2610
+ const innerContainerStyles = needsInnerContainer ? {
2611
+ display: "inline-block",
2612
+ "border-radius": resolvedRadius,
2613
+ overflow: "hidden",
2614
+ "box-shadow": resolvedShadow,
2615
+ "max-width": "100%",
2616
+ } : null;
2617
+
2618
+ const imgStyles = {
2619
+ display: needsInnerContainer ? "block" : "inline-block",
2620
+ "vertical-align": "top",
2621
+ "max-width": "100%",
2622
+ width: width,
2623
+ };
2624
+ if (height) imgStyles["height"] = height;
2625
+
2626
+ // Rounded/corner: apply to img only if no inner container
2627
+ if (!needsInnerContainer && !isCornerStyleguide && (rounded || corner)) {
2628
+ imgStyles["border-radius"] = resolvedRadius;
2629
+ }
2630
+
2631
+ // Shadow: apply to img only if no inner container (and not styleguide ref)
2632
+ if (!needsInnerContainer && shadow && !isShadowStyleguide) {
2633
+ imgStyles["box-shadow"] = resolvedShadow;
2634
+ }
2635
+
2636
+ // Border: use inline if not styleguide ref
2637
+ if (border && !isBorderStyleguide) {
2638
+ imgStyles["border-width"] = resolve(border, BORDER_WIDTHS) || border;
2639
+ imgStyles["border-style"] = borderStyle;
2640
+ }
2641
+ if (borderColor) imgStyles["border-color"] = borderColor;
2642
+ if (objectFit) imgStyles["object-fit"] = objectFit;
2643
+
2644
+ // Build data attributes for roundtrip conversion
2645
+ // Note: We store the original src, not the swapped brand asset URL
2646
+ const originalSrc = attr(this, "src", "");
2647
+ let dataAttrs = 'data-type="Image/V2"';
2648
+ dataAttrs += ` data-src="${originalSrc}"`;
2649
+ if (alt) dataAttrs += ` data-alt="${alt}"`;
2650
+ if (width !== "100%") dataAttrs += ` data-width="${width}"`;
2651
+ if (height) dataAttrs += ` data-height="${height}"`;
2652
+ if (align !== "center") dataAttrs += ` data-align="${align}"`;
2653
+ if (rounded) dataAttrs += ` data-rounded="${rounded}"`;
2654
+ if (corner) dataAttrs += ` data-corner="${corner}"`;
2655
+ if (shadow) dataAttrs += ` data-shadow="${shadow}"`;
2656
+ if (border) dataAttrs += ` data-border="${border}"`;
2657
+ if (borderStyle !== "solid")
2658
+ dataAttrs += ` data-border-style="${borderStyle}"`;
2659
+ if (borderColor) dataAttrs += ` data-border-color="${borderColor}"`;
2660
+ if (objectFit) dataAttrs += ` data-object-fit="${objectFit}"`;
2661
+ if (brandAsset) dataAttrs += ` data-brand-asset="${brandAsset}"`;
2662
+
2663
+ // Add styleguide data attributes
2664
+ if (isShadowStyleguide) dataAttrs += ` data-style-guide-shadow="${shadow}"`;
2665
+ if (isBorderStyleguide) dataAttrs += ` data-style-guide-border="${border}"`;
2666
+ if (isCornerStyleguide) dataAttrs += ` data-style-guide-corner="${corner}"`;
2667
+
2668
+ // Animation attributes
2669
+ const animationAttrs = buildAnimationAttrs(this);
2670
+ dataAttrs += animationAttrs;
2671
+
2672
+ const imgHtml = `<img src="${src}" alt="${alt}" style="${buildStyle(imgStyles)}" />`;
2673
+ const innerHtml = needsInnerContainer
2674
+ ? `<span style="${buildStyle(innerContainerStyles)}">${imgHtml}</span>`
2675
+ : imgHtml;
2676
+
2677
+ this.outerHTML = `
2678
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2679
+ ${innerHtml}
2680
+ </div>
2681
+ `;
2682
+ }
2683
+ }
2684
+
2685
+ /**
2686
+ * <cf-icon> - FontAwesome icon
2687
+ *
2688
+ * IMPORTANT: Use OLD FontAwesome format: "fas fa-check" NOT "fa-solid fa-check"
2689
+ *
2690
+ * Attributes:
2691
+ * icon - FontAwesome classes (e.g., "fas fa-rocket")
2692
+ * size - Font size
2693
+ * color - Icon color
2694
+ * align - Alignment: left, center, right (default: center)
2695
+ * opacity - Opacity value 0-1 (e.g., "0.5", "0.8")
2696
+ * pt - Padding top
2697
+ * pb - Padding bottom
2698
+ * mt - Margin top
2699
+ */
2700
+ class CFIcon extends CFElement {
2701
+ render() {
2702
+ const icon = attr(this, "icon", "fas fa-star");
2703
+ const size = attr(this, "size", "48px");
2704
+ const color = attr(this, "color");
2705
+ const hasExplicitColor = this.hasAttribute("color");
2706
+ const align = attr(this, "align", "center");
2707
+ const opacity = attr(this, "opacity");
2708
+ const pt = attr(this, "pt", "12px");
2709
+ const pb = attr(this, "pb", "12px");
2710
+ const mt = attr(this, "mt", "0");
2711
+
2712
+ // NOTE: No width set - allows flex layout to work properly
2713
+ const wrapperStyles = {
2714
+ "text-align": align,
2715
+ "padding-top": pt,
2716
+ "padding-bottom": pb,
2717
+ "margin-top": mt,
2718
+ "box-sizing": "border-box",
2719
+ };
2720
+
2721
+ const iconStyles = {
2722
+ display: "inline-block",
2723
+ "font-size": size,
2724
+ };
2725
+ if (hasExplicitColor && color) iconStyles.color = `${color} !important`;
2726
+ if (opacity) iconStyles["opacity"] = opacity;
2727
+
2728
+ // Build data attributes for roundtrip conversion
2729
+ let dataAttrs = 'data-type="Icon/V1"';
2730
+ dataAttrs += ` data-icon="${icon}"`;
2731
+ if (size !== "48px") dataAttrs += ` data-size="${size}"`;
2732
+ if (hasExplicitColor && color)
2733
+ dataAttrs += ` data-color="${color}" data-color-explicit="true"`;
2734
+ if (align !== "center") dataAttrs += ` data-align="${align}"`;
2735
+ if (opacity) dataAttrs += ` data-opacity="${opacity}"`;
2736
+ // Store padding/margin for reliable roundtrip
2737
+ if (pt !== "12px") dataAttrs += ` data-pt="${pt}"`;
2738
+ if (pb !== "12px") dataAttrs += ` data-pb="${pb}"`;
2739
+ if (mt !== "0") dataAttrs += ` data-mt="${mt}"`;
2740
+
2741
+ // Animation attributes
2742
+ const animationAttrs = buildAnimationAttrs(this);
2743
+ dataAttrs += animationAttrs;
2744
+
2745
+ this.outerHTML = `
2746
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2747
+ <i class="${icon}" style="${buildStyle(iconStyles)}"></i>
2748
+ </div>
2749
+ `;
2750
+ }
2751
+ }
2752
+
2753
+ /**
2754
+ * <cf-video> - YouTube video embed
2755
+ *
2756
+ * Attributes:
2757
+ * url - Full YouTube URL
2758
+ * rounded - Border radius
2759
+ * corner - Styleguide corner ref (style1-3)
2760
+ * shadow - Box shadow or styleguide ref (style1-3)
2761
+ * border - Border width or styleguide ref (style1-3)
2762
+ * border-style - Border style
2763
+ * border-color - Border color
2764
+ * bg - Background color (before video loads)
2765
+ * pt - Padding top (default: 0)
2766
+ * pb - Padding bottom (default: 0)
2767
+ * px - Padding horizontal (left + right)
2768
+ * mt - Margin top (default: 0)
2769
+ */
2770
+ class CFVideo extends CFElement {
2771
+ render() {
2772
+ const url = attr(this, "url", "");
2773
+ const rounded = attr(this, "rounded", "lg");
2774
+ const corner = attr(this, "corner");
2775
+ const shadow = attr(this, "shadow", "lg");
2776
+ const border = attr(this, "border");
2777
+ const borderStyle = attr(this, "border-style", "solid");
2778
+ const borderColor = attr(this, "border-color");
2779
+ const bg = attr(this, "bg", "#000");
2780
+ const pt = attr(this, "pt", "0");
2781
+ const pb = attr(this, "pb", "0");
2782
+ const px = attr(this, "px");
2783
+ const mt = attr(this, "mt", "0");
2784
+
2785
+ // Check if shadow/border/corner are styleguide references
2786
+ const isShadowStyleguide =
2787
+ shadow && styleguideManager.isStyleguideRef(shadow, "shadow");
2788
+ const isBorderStyleguide =
2789
+ border && styleguideManager.isStyleguideRef(border, "border");
2790
+ const isCornerStyleguide =
2791
+ corner && styleguideManager.isStyleguideRef(corner, "corner");
2792
+
2793
+ // Extract YouTube video ID
2794
+ const match = url.match(
2795
+ /(?:youtube\.com\/watch\?v=|youtu\.be\/)([^&\s]+)/
2796
+ );
2797
+ const videoId = match ? match[1] : "";
2798
+ const embedUrl = `https://www.youtube.com/embed/${videoId}`;
2799
+
2800
+ // NOTE: No width on wrapper - allows flex layout to work properly
2801
+ const wrapperStyles = {
2802
+ "padding-top": pt,
2803
+ "padding-bottom": pb,
2804
+ "margin-top": mt,
2805
+ "box-sizing": "border-box",
2806
+ };
2807
+ if (px) {
2808
+ wrapperStyles["padding-left"] = px;
2809
+ wrapperStyles["padding-right"] = px;
2810
+ }
2811
+
2812
+ const containerStyles = {
2813
+ width: "100%",
2814
+ "aspect-ratio": "16/9",
2815
+ position: "relative",
2816
+ overflow: "hidden",
2817
+ "background-color": bg,
2818
+ };
2819
+
2820
+ // Rounded/corner: prefer corner styleguide ref, fallback to rounded
2821
+ if (!isCornerStyleguide && (rounded || corner)) {
2822
+ containerStyles["border-radius"] = resolve(rounded || corner, RADIUS) || rounded || corner;
2823
+ }
2824
+
2825
+ // Shadow: use inline if not styleguide ref
2826
+ if (shadow && !isShadowStyleguide) {
2827
+ containerStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
2828
+ }
2829
+
2830
+ // Border: use inline if not styleguide ref
2831
+ if (border && !isBorderStyleguide) {
2832
+ containerStyles["border-width"] =
2833
+ resolve(border, BORDER_WIDTHS) || border;
2834
+ containerStyles["border-style"] = borderStyle;
2835
+ }
2836
+ if (borderColor) containerStyles["border-color"] = borderColor;
2837
+
2838
+ // Build data attributes for roundtrip conversion
2839
+ let dataAttrs = 'data-type="Video/V1" data-video-type="youtube"';
2840
+ dataAttrs += ` data-video-url="${url}"`;
2841
+ if (rounded !== "lg") dataAttrs += ` data-rounded="${rounded}"`;
2842
+ if (corner) dataAttrs += ` data-corner="${corner}"`;
2843
+ if (shadow !== "lg") dataAttrs += ` data-shadow="${shadow}"`;
2844
+ if (border) dataAttrs += ` data-border="${border}"`;
2845
+ if (borderStyle !== "solid")
2846
+ dataAttrs += ` data-border-style="${borderStyle}"`;
2847
+ if (borderColor) dataAttrs += ` data-border-color="${borderColor}"`;
2848
+ if (bg !== "#000") dataAttrs += ` data-bg="${bg}"`;
2849
+
2850
+ // Add styleguide data attributes
2851
+ if (isShadowStyleguide) dataAttrs += ` data-style-guide-shadow="${shadow}"`;
2852
+ if (isBorderStyleguide) dataAttrs += ` data-style-guide-border="${border}"`;
2853
+ if (isCornerStyleguide) dataAttrs += ` data-style-guide-corner="${corner}"`;
2854
+
2855
+ this.outerHTML = `
2856
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2857
+ <div style="${buildStyle(containerStyles)}">
2858
+ <iframe
2859
+ src="${embedUrl}"
2860
+ style="width: 100%; height: 100%; border: none;"
2861
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
2862
+ allowfullscreen
2863
+ ></iframe>
2864
+ </div>
2865
+ </div>
2866
+ `;
2867
+ }
2868
+ }
2869
+
2870
+ /**
2871
+ * <cf-divider> - Horizontal line separator
2872
+ *
2873
+ * Attributes:
2874
+ * color - Line color (default: "#e2e8f0")
2875
+ * width - Width (e.g., "100%", "200px", "50%")
2876
+ * thickness - Border thickness (e.g., "1px", "3px")
2877
+ * style - Border style: solid, dashed, dotted (default: solid)
2878
+ * align - Alignment: left, center, right (default: center)
2879
+ * shadow - Box shadow preset or custom
2880
+ * pt - Padding top
2881
+ * pb - Padding bottom
2882
+ * px - Padding horizontal
2883
+ * mt - Margin top
2884
+ */
2885
+ class CFDivider extends CFElement {
2886
+ render() {
2887
+ const color = attr(this, "color", "#e2e8f0");
2888
+ const width = attr(this, "width", "100%");
2889
+ const thickness = attr(this, "thickness", "1px");
2890
+ const borderStyle = attr(this, "style", "solid");
2891
+ const align = attr(this, "align", "center");
2892
+ const shadow = attr(this, "shadow");
2893
+ const pt = attr(this, "pt", "16px");
2894
+ const pb = attr(this, "pb", "16px");
2895
+ const px = attr(this, "px");
2896
+ const mt = attr(this, "mt");
2897
+
2898
+ // NOTE: No width set - allows flex layout to work properly
2899
+ const wrapperStyles = {
2900
+ "padding-top": pt,
2901
+ "padding-bottom": pb,
2902
+ "box-sizing": "border-box",
2903
+ };
2904
+ if (px) {
2905
+ wrapperStyles["padding-left"] = px;
2906
+ wrapperStyles["padding-right"] = px;
2907
+ }
2908
+ if (mt) wrapperStyles["margin-top"] = mt;
2909
+
2910
+ // Alignment determines margin
2911
+ const alignMargin =
2912
+ {
2913
+ left: "0 auto 0 0",
2914
+ center: "0 auto",
2915
+ right: "0 0 0 auto",
2916
+ }[align] || "0 auto";
2917
+
2918
+ const lineStyles = {
2919
+ "border-top": `${thickness} ${borderStyle} ${color}`,
2920
+ "border-right": "none",
2921
+ "border-bottom": "none",
2922
+ "border-left": "none",
2923
+ width: width,
2924
+ margin: alignMargin,
2925
+ };
2926
+ if (shadow) lineStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
2927
+
2928
+ // Build data attributes for roundtrip conversion
2929
+ let dataAttrs = 'data-type="Divider/V1"';
2930
+ if (color !== "#e2e8f0") dataAttrs += ` data-color="${color}"`;
2931
+ if (width !== "100%") dataAttrs += ` data-width="${width}"`;
2932
+ if (thickness !== "1px") dataAttrs += ` data-thickness="${thickness}"`;
2933
+ if (borderStyle !== "solid") dataAttrs += ` data-style="${borderStyle}"`;
2934
+ if (align !== "center") dataAttrs += ` data-align="${align}"`;
2935
+ if (shadow) dataAttrs += ` data-shadow="${shadow}"`;
2936
+ dataAttrs += ` data-skip-shadow-settings="${shadow ? "false" : "true"}"`;
2937
+
2938
+ this.outerHTML = `
2939
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
2940
+ <div style="${buildStyle(lineStyles)}"></div>
2941
+ </div>
2942
+ `;
2943
+ }
2944
+ }
2945
+
2946
+ // ==========================================================================
2947
+ // FORM ELEMENTS
2948
+ // ==========================================================================
2949
+
2950
+ /**
2951
+ * <cf-input> - Text input field
2952
+ *
2953
+ * Attributes:
2954
+ * type - Input type: email, name, first_name, last_name, phone_number,
2955
+ * shipping_address, shipping_city, shipping_zip, shipping_state,
2956
+ * shipping_country, custom_type
2957
+ * name - Custom field name (required when type="custom_type")
2958
+ * required - Required field (true/false)
2959
+ * bg - Background color
2960
+ * color - Text color
2961
+ * border-color - Border color
2962
+ * rounded - Border radius
2963
+ * shadow - Box shadow
2964
+ * border - Border width
2965
+ * border-style - Border style
2966
+ * px - Horizontal padding
2967
+ * py - Vertical padding
2968
+ * width - Width percentage
2969
+ * align - Alignment: left, center, right
2970
+ * pt - Wrapper padding top
2971
+ * mt - Wrapper margin top
2972
+ */
2973
+ class CFInput extends CFElement {
2974
+ render() {
2975
+ const type = attr(this, "type", "email");
2976
+ const name = attr(this, "name");
2977
+ const placeholder = attr(this, "placeholder", "");
2978
+ const required = attr(this, "required");
2979
+ const bg = attr(this, "bg", "#ffffff");
2980
+ const color = attr(this, "color");
2981
+ const fontSize = attr(this, "font-size", "16px");
2982
+ const borderColor = attr(this, "border-color", "#d1d5db");
2983
+ const rounded = attr(this, "rounded", "lg");
2984
+ const shadow = attr(this, "shadow");
2985
+ const border = attr(this, "border", "1");
2986
+ const borderStyle = attr(this, "border-style", "solid");
2987
+ const px = attr(this, "px", "16px");
2988
+ const py = attr(this, "py", "12px");
2989
+ const inputWidth = attr(this, "width");
2990
+ const align = attr(this, "align", "center");
2991
+ const pt = attr(this, "pt");
2992
+ const mt = attr(this, "mt");
2993
+
2994
+ // Build data attributes
2995
+ let dataAttrs = `data-type="Input/V1" data-input-type="${type}" data-align="${align}"`;
2996
+ dataAttrs += ` data-font-size="${fontSize}"`;
2997
+ if (color) dataAttrs += ` data-color="${color}"`;
2998
+ if (type === "custom_type" && name)
2999
+ dataAttrs += ` data-input-name="${name}"`;
3000
+ if (required === "true" || required === "")
3001
+ dataAttrs += ' data-required="true"';
3002
+ if (inputWidth) dataAttrs += ` data-width="${inputWidth}"`;
3003
+ if (placeholder) dataAttrs += ` data-placeholder="${placeholder}"`;
3004
+
3005
+ const wrapperStyles = {
3006
+ width: "100%",
3007
+ "box-sizing": "border-box",
3008
+ };
3009
+ if (pt) wrapperStyles["padding-top"] = pt;
3010
+ if (mt) wrapperStyles["margin-top"] = mt;
3011
+
3012
+ const containerStyles = {
3013
+ width: inputWidth ? `${inputWidth}%` : "100%",
3014
+ display: "block",
3015
+ "background-color": bg,
3016
+ "padding-left": px,
3017
+ "padding-right": px,
3018
+ "padding-top": py,
3019
+ "padding-bottom": py,
3020
+ "border-radius": resolve(rounded, RADIUS) || rounded,
3021
+ "border-width": resolve(border, BORDER_WIDTHS) || border,
3022
+ "border-style": borderStyle,
3023
+ "border-color": borderColor,
3024
+ "box-sizing": "border-box",
3025
+ };
3026
+ if (shadow)
3027
+ containerStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
3028
+ if (inputWidth && align === "center") {
3029
+ containerStyles["margin-left"] = "auto";
3030
+ containerStyles["margin-right"] = "auto";
3031
+ } else if (inputWidth && align === "right") {
3032
+ containerStyles["margin-left"] = "auto";
3033
+ }
3034
+
3035
+ const fieldStyles = {
3036
+ width: "100%",
3037
+ border: "none",
3038
+ outline: "none",
3039
+ background: "transparent",
3040
+ "font-family": "inherit",
3041
+ "font-size": fontSize,
3042
+ };
3043
+ if (color) fieldStyles["color"] = color;
3044
+
3045
+ const htmlType = { email: "email", phone_number: "tel" }[type] || "text";
3046
+ const placeholderAttr = placeholder ? ` placeholder="${placeholder}"` : "";
3047
+
3048
+ this.outerHTML = `
3049
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3050
+ <div style="${buildStyle(containerStyles)}">
3051
+ <input type="${htmlType}"${placeholderAttr} style="${buildStyle(fieldStyles)}" />
3052
+ </div>
3053
+ </div>
3054
+ `;
3055
+ }
3056
+ }
3057
+
3058
+ /**
3059
+ * <cf-textarea> - Multi-line text input
3060
+ *
3061
+ * Attributes:
3062
+ * name - Field name
3063
+ * required - Required field
3064
+ * bg - Background color
3065
+ * color - Text color
3066
+ * border-color - Border color
3067
+ * rounded - Border radius
3068
+ * shadow - Box shadow
3069
+ * border - Border width
3070
+ * border-style - Border style
3071
+ * px - Horizontal padding
3072
+ * py - Vertical padding
3073
+ * width - Width percentage
3074
+ * height - Height (e.g., "150px")
3075
+ * align - Alignment
3076
+ * pt - Wrapper padding top
3077
+ * mt - Wrapper margin top
3078
+ */
3079
+ class CFTextarea extends CFElement {
3080
+ render() {
3081
+ const name = attr(this, "name", "message");
3082
+ const placeholder = attr(this, "placeholder", "");
3083
+ const required = attr(this, "required");
3084
+ const bg = attr(this, "bg", "#ffffff");
3085
+ const color = attr(this, "color");
3086
+ const fontSize = attr(this, "font-size", "16px");
3087
+ const borderColor = attr(this, "border-color", "#d1d5db");
3088
+ const rounded = attr(this, "rounded", "lg");
3089
+ const shadow = attr(this, "shadow");
3090
+ const border = attr(this, "border", "1");
3091
+ const borderStyle = attr(this, "border-style", "solid");
3092
+ const px = attr(this, "px", "16px");
3093
+ const py = attr(this, "py", "12px");
3094
+ const textareaWidth = attr(this, "width");
3095
+ const height = attr(this, "height", "120px");
3096
+ const align = attr(this, "align", "center");
3097
+ const pt = attr(this, "pt");
3098
+ const mt = attr(this, "mt");
3099
+
3100
+ let dataAttrs = `data-type="TextArea/V1" data-textarea-name="${name}" data-align="${align}"`;
3101
+ dataAttrs += ` data-font-size="${fontSize}"`;
3102
+ if (color) dataAttrs += ` data-color="${color}"`;
3103
+ if (required === "true" || required === "")
3104
+ dataAttrs += ' data-required="true"';
3105
+ if (textareaWidth) dataAttrs += ` data-width="${textareaWidth}"`;
3106
+ if (height) dataAttrs += ` data-height="${height.replace("px", "")}"`;
3107
+ if (placeholder) dataAttrs += ` data-placeholder="${placeholder}"`;
3108
+
3109
+ const wrapperStyles = {
3110
+ width: "100%",
3111
+ "box-sizing": "border-box",
3112
+ };
3113
+ if (pt) wrapperStyles["padding-top"] = pt;
3114
+ if (mt) wrapperStyles["margin-top"] = mt;
3115
+
3116
+ const containerStyles = {
3117
+ width: textareaWidth ? `${textareaWidth}%` : "100%",
3118
+ display: "block",
3119
+ "background-color": bg,
3120
+ "padding-left": px,
3121
+ "padding-right": px,
3122
+ "padding-top": py,
3123
+ "padding-bottom": py,
3124
+ "border-radius": resolve(rounded, RADIUS) || rounded,
3125
+ "border-width": resolve(border, BORDER_WIDTHS) || border,
3126
+ "border-style": borderStyle,
3127
+ "border-color": borderColor,
3128
+ "box-sizing": "border-box",
3129
+ };
3130
+ if (shadow)
3131
+ containerStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
3132
+ if (textareaWidth && align === "center") {
3133
+ containerStyles["margin-left"] = "auto";
3134
+ containerStyles["margin-right"] = "auto";
3135
+ } else if (textareaWidth && align === "right") {
3136
+ containerStyles["margin-left"] = "auto";
3137
+ }
3138
+
3139
+ const fieldStyles = {
3140
+ width: "100%",
3141
+ height: height,
3142
+ border: "none",
3143
+ outline: "none",
3144
+ background: "transparent",
3145
+ "font-family": "inherit",
3146
+ "font-size": fontSize,
3147
+ resize: "vertical",
3148
+ };
3149
+ if (color) fieldStyles["color"] = color;
3150
+
3151
+ const placeholderAttr = placeholder ? ` placeholder="${placeholder}"` : "";
3152
+
3153
+ this.outerHTML = `
3154
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3155
+ <div style="${buildStyle(containerStyles)}">
3156
+ <textarea${placeholderAttr} style="${buildStyle(fieldStyles)}"></textarea>
3157
+ </div>
3158
+ </div>
3159
+ `;
3160
+ }
3161
+ }
3162
+
3163
+ /**
3164
+ * <cf-select> - Dropdown select
3165
+ *
3166
+ * Attributes:
3167
+ * name - Field name
3168
+ * placeholder - Placeholder text
3169
+ * required - Required field
3170
+ * bg - Background color
3171
+ * color - Text color
3172
+ * border-color - Border color
3173
+ * rounded - Border radius
3174
+ * shadow - Box shadow
3175
+ * border - Border width
3176
+ * border-style - Border style
3177
+ * px - Horizontal padding
3178
+ * py - Vertical padding
3179
+ * width - Width percentage
3180
+ * align - Alignment
3181
+ * pt - Wrapper padding top
3182
+ * mt - Wrapper margin top
3183
+ *
3184
+ * Children: <option value="...">Label</option>
3185
+ */
3186
+ class CFSelect extends CFElement {
3187
+ render() {
3188
+ const type = attr(this, "type", "custom_type");
3189
+ const name = attr(this, "name", "option");
3190
+ const placeholder = attr(this, "placeholder", "Select an option...");
3191
+ const required = attr(this, "required");
3192
+ const bg = attr(this, "bg", "#ffffff");
3193
+ const color = attr(this, "color");
3194
+ const fontSize = attr(this, "font-size", "16px");
3195
+ const borderColor = attr(this, "border-color", "#d1d5db");
3196
+ const rounded = attr(this, "rounded", "lg");
3197
+ const shadow = attr(this, "shadow");
3198
+ const border = attr(this, "border", "1");
3199
+ const borderStyle = attr(this, "border-style", "solid");
3200
+ const px = attr(this, "px", "16px");
3201
+ const py = attr(this, "py", "12px");
3202
+ const selectWidth = attr(this, "width");
3203
+ const align = attr(this, "align", "center");
3204
+ const pt = attr(this, "pt");
3205
+ const mt = attr(this, "mt", "0");
3206
+
3207
+ let dataAttrs = `data-type="SelectBox/V1" data-select-name="${name}" data-select-type="${type}" data-align="${align}"`;
3208
+ dataAttrs += ` data-font-size="${fontSize}"`;
3209
+ if (color) dataAttrs += ` data-color="${color}"`;
3210
+ if (required === "true" || required === "")
3211
+ dataAttrs += ' data-required="true"';
3212
+ if (selectWidth) dataAttrs += ` data-width="${selectWidth}"`;
3213
+ if (placeholder) dataAttrs += ` data-placeholder="${placeholder}"`;
3214
+
3215
+ const wrapperStyles = {
3216
+ width: selectWidth ? `${selectWidth}%` : "100%",
3217
+ "margin-top": mt,
3218
+ "box-sizing": "border-box",
3219
+ };
3220
+ if (pt) wrapperStyles["padding-top"] = pt;
3221
+ if (selectWidth && align === "center") {
3222
+ wrapperStyles["margin-left"] = "auto";
3223
+ wrapperStyles["margin-right"] = "auto";
3224
+ } else if (selectWidth && align === "right") {
3225
+ wrapperStyles["margin-left"] = "auto";
3226
+ }
3227
+
3228
+ const containerStyles = {
3229
+ width: "100%",
3230
+ display: "block",
3231
+ "background-color": bg,
3232
+ "padding-left": px,
3233
+ "padding-right": px,
3234
+ "padding-top": py,
3235
+ "padding-bottom": py,
3236
+ "border-radius": resolve(rounded, RADIUS) || rounded,
3237
+ "border-width": resolve(border, BORDER_WIDTHS) || border,
3238
+ "border-style": borderStyle,
3239
+ "border-color": borderColor,
3240
+ "box-sizing": "border-box",
3241
+ };
3242
+ if (shadow)
3243
+ containerStyles["box-shadow"] = resolve(shadow, SHADOWS) || shadow;
3244
+
3245
+ const fieldStyles = {
3246
+ width: "100%",
3247
+ border: "none",
3248
+ outline: "none",
3249
+ background: "transparent",
3250
+ "font-family": "inherit",
3251
+ "font-size": fontSize,
3252
+ cursor: "pointer",
3253
+ };
3254
+ if (color) fieldStyles["color"] = color;
3255
+
3256
+ // Get option elements from children
3257
+ const options = getContent(this);
3258
+
3259
+ this.outerHTML = `
3260
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3261
+ <div style="${buildStyle(containerStyles)}">
3262
+ <select style="${buildStyle(fieldStyles)}">
3263
+ <option value="">${placeholder}</option>
3264
+ ${options}
3265
+ </select>
3266
+ </div>
3267
+ </div>
3268
+ `;
3269
+ }
3270
+ }
3271
+
3272
+ /**
3273
+ * <cf-checkbox> - Checkbox with label
3274
+ *
3275
+ * Attributes:
3276
+ * name - Field name
3277
+ * checked - Pre-checked (true/false)
3278
+ * required - Required field
3279
+ * label-color - Label text color
3280
+ * label-size - Label font size
3281
+ * box-size - Checkbox box size
3282
+ * box-bg - Box background color
3283
+ * box-border-color - Box border color
3284
+ * check-color - Check mark color
3285
+ * gap - Gap between box and label
3286
+ * mt - Margin top
3287
+ *
3288
+ * Content: Label text (supports HTML)
3289
+ */
3290
+ class CFCheckbox extends CFElement {
3291
+ render() {
3292
+ const name = attr(this, "name", "agree");
3293
+ const checked = attr(this, "checked");
3294
+ const required = attr(this, "required");
3295
+ const labelColor = attr(this, "label-color", "#334155");
3296
+ const labelSize = attr(this, "label-size", "16px");
3297
+ const boxSize = attr(this, "box-size", "20px");
3298
+ const boxBg = attr(this, "box-bg", "#ffffff");
3299
+ const boxBorderColor = attr(this, "box-border-color", "#d1d5db");
3300
+ const checkColor = attr(this, "check-color", "#ffffff");
3301
+ const gap = attr(this, "gap", "12px");
3302
+ const mt = attr(this, "mt");
3303
+
3304
+ let dataAttrs = `data-type="Checkbox/V1" data-name="${name}"`;
3305
+ if (checked === "true" || checked === "")
3306
+ dataAttrs += ' data-checked="true"';
3307
+ if (required === "true" || required === "")
3308
+ dataAttrs += ' data-required="true"';
3309
+
3310
+ const wrapperStyles = {
3311
+ width: "100%",
3312
+ "box-sizing": "border-box",
3313
+ };
3314
+ if (mt) wrapperStyles["margin-top"] = mt;
3315
+
3316
+ const labelStyles = {
3317
+ display: "flex",
3318
+ "align-items": "center",
3319
+ gap: gap,
3320
+ cursor: "pointer",
3321
+ };
3322
+
3323
+ const boxStyles = {
3324
+ "flex-shrink": "0",
3325
+ width: boxSize,
3326
+ height: boxSize,
3327
+ border: `2px solid ${boxBorderColor}`,
3328
+ "border-radius": "4px",
3329
+ "background-color": boxBg,
3330
+ display: "flex",
3331
+ "align-items": "center",
3332
+ "justify-content": "center",
3333
+ };
3334
+
3335
+ const textStyles = {
3336
+ "font-size": labelSize,
3337
+ "line-height": "1.5",
3338
+ color: labelColor,
3339
+ };
3340
+
3341
+ const isChecked = checked === "true" || checked === "";
3342
+
3343
+ this.outerHTML = `
3344
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3345
+ <label style="${buildStyle(labelStyles)}">
3346
+ <input type="checkbox" style="position: absolute; opacity: 0; width: 0; height: 0;" ${
3347
+ isChecked ? "checked" : ""
3348
+ } />
3349
+ <span style="${buildStyle(boxStyles)}">
3350
+ <i class="fas fa-check" style="color: ${checkColor}; font-size: calc(${boxSize} * 0.6); display: none;"></i>
3351
+ </span>
3352
+ <span style="${buildStyle(textStyles)}">${getContent(this)}</span>
3353
+ </label>
3354
+ </div>
3355
+ `;
3356
+ }
3357
+ }
3358
+
3359
+ // ==========================================================================
3360
+ // LIST ELEMENTS
3361
+ // ==========================================================================
3362
+
3363
+ /**
3364
+ * <cf-bullet-list> - List with icons
3365
+ *
3366
+ * IMPORTANT: All items share the SAME icon and color
3367
+ *
3368
+ * Attributes:
3369
+ * icon - FontAwesome icon (OLD format: "fas fa-check")
3370
+ * icon-color - Icon color
3371
+ * text-color - Text color
3372
+ * icon-size - Icon size
3373
+ * size - Text size preset (s, m, l, xl) - uses paragraph typescale
3374
+ * gap - Gap between icon and text
3375
+ * item-gap - Gap between list items
3376
+ * pt - Padding top
3377
+ * pb - Padding bottom
3378
+ * mt - Margin top
3379
+ *
3380
+ * Children: <li>Item text</li>
3381
+ */
3382
+ class CFBulletList extends CFElement {
3383
+ render() {
3384
+ const icon = attr(this, "icon", "fas fa-check");
3385
+ const iconColor = attr(this, "icon-color");
3386
+ const hasExplicitIconColor = this.hasAttribute("icon-color");
3387
+ const textColor = attr(this, "text-color"); // No default - allows paint inheritance
3388
+ const hasExplicitTextColor = this.hasAttribute("text-color");
3389
+ const iconSize = attr(this, "icon-size", "16px");
3390
+ const size = attr(this, "size", "m"); // Size preset (s, m, l, xl) - uses paragraph scale
3391
+ const gap = attr(this, "gap", "12px");
3392
+ const itemGap = attr(this, "item-gap", "8px");
3393
+ const align = attr(this, "align", "left");
3394
+ const pt = attr(this, "pt", "0");
3395
+ const pb = attr(this, "pb", "0");
3396
+ const mt = attr(this, "mt", "0");
3397
+
3398
+ // Resolve size preset to pixel value using paragraph scale (bullet lists are body text)
3399
+ const resolvedSize = styleguideManager.resolveSize(size, 'paragraph') || resolve(size, FONT_SIZES) || size;
3400
+
3401
+ // Map align to justify-content
3402
+ const justifyMap = {
3403
+ left: "flex-start",
3404
+ center: "center",
3405
+ right: "flex-end",
3406
+ };
3407
+ const justifyContent = justifyMap[align] || "flex-start";
3408
+
3409
+ const wrapperStyles = {
3410
+ width: "100%",
3411
+ "padding-top": pt,
3412
+ "padding-bottom": pb,
3413
+ "margin-top": mt,
3414
+ "box-sizing": "border-box",
3415
+ };
3416
+
3417
+ const listStyles = {
3418
+ "list-style": "none",
3419
+ padding: "0",
3420
+ margin: "0",
3421
+ display: "flex",
3422
+ "flex-direction": "column",
3423
+ gap: itemGap,
3424
+ };
3425
+
3426
+ const itemStyles = {
3427
+ display: "flex",
3428
+ "align-items": "flex-start",
3429
+ "justify-content": justifyContent,
3430
+ };
3431
+
3432
+ const iconStyles = {
3433
+ "flex-shrink": "0",
3434
+ "font-size": iconSize,
3435
+ "line-height": "1.5",
3436
+ "margin-top": "2px",
3437
+ "margin-right": gap,
3438
+ };
3439
+ if (hasExplicitIconColor && iconColor) {
3440
+ iconStyles.color = `${iconColor} !important`;
3441
+ }
3442
+
3443
+ const textStyles = {
3444
+ "font-size": resolvedSize,
3445
+ "line-height": "1.5",
3446
+ };
3447
+ // Only apply inline color if explicitly set - allows paint inheritance
3448
+ if (hasExplicitTextColor && textColor) {
3449
+ textStyles.color = `${textColor} !important`;
3450
+ }
3451
+
3452
+ // Parse list items
3453
+ const tempDiv = document.createElement("div");
3454
+ tempDiv.innerHTML = getContent(this);
3455
+ const items = tempDiv.querySelectorAll("li");
3456
+
3457
+ let listContent = "";
3458
+ items.forEach((item) => {
3459
+ listContent += `
3460
+ <li style="${buildStyle(itemStyles)}">
3461
+ <i class="${icon} fa_icon" style="${buildStyle(iconStyles)}"></i>
3462
+ <span style="${buildStyle(textStyles)}">${item.innerHTML}</span>
3463
+ </li>
3464
+ `;
3465
+ });
3466
+
3467
+ // Build data attributes for roundtrip conversion
3468
+ let dataAttrs = 'data-type="BulletList/V1"';
3469
+ dataAttrs += ` data-icon="${icon}"`;
3470
+ if (hasExplicitIconColor && iconColor)
3471
+ dataAttrs += ` data-icon-color="${iconColor}" data-icon-color-explicit="true"`;
3472
+ if (hasExplicitTextColor && textColor)
3473
+ dataAttrs += ` data-text-color="${textColor}" data-text-color-explicit="true"`;
3474
+ dataAttrs += ` data-icon-size="${iconSize}"`;
3475
+ dataAttrs += ` data-size="${size}"`; // Store preset for roundtrip
3476
+ dataAttrs += ` data-gap="${gap}"`;
3477
+ dataAttrs += ` data-item-gap="${itemGap}"`;
3478
+ if (align !== "left") dataAttrs += ` data-align="${align}"`;
3479
+
3480
+ this.outerHTML = `
3481
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3482
+ <ul style="${buildStyle(listStyles)}">
3483
+ ${listContent}
3484
+ </ul>
3485
+ </div>
3486
+ `;
3487
+ }
3488
+ }
3489
+
3490
+ // ==========================================================================
3491
+ // INTERACTIVE ELEMENTS
3492
+ // ==========================================================================
3493
+
3494
+ /**
3495
+ * <cf-progress-bar> - Progress bar with optional label
3496
+ *
3497
+ * Attributes:
3498
+ * progress - Progress percentage 0-100 (default: 50)
3499
+ * text - Label text (default: "")
3500
+ * text-outside - Show text outside bar (default: false)
3501
+ * width - Bar width (default: 100%)
3502
+ * height - Bar height (default: 24px)
3503
+ * bg - Background/track color (default: #e2e8f0)
3504
+ * fill - Fill/progress color (default: #3b82f6)
3505
+ * text-color - Label text color
3506
+ * rounded - Border radius (default: full)
3507
+ * shadow - Box shadow
3508
+ * border - Border width
3509
+ * border-color - Border color
3510
+ * pt - Padding top
3511
+ * pb - Padding bottom
3512
+ * mt - Margin top
3513
+ * align - Alignment: left, center, right (default: center)
3514
+ */
3515
+ class CFProgressBar extends CFElement {
3516
+ render() {
3517
+ const progress = parseInt(attr(this, 'progress', '50'), 10);
3518
+ const text = attr(this, 'text', '');
3519
+ const textOutside = attr(this, 'text-outside', 'false') === 'true';
3520
+ const width = attr(this, 'width', '100%');
3521
+ const height = attr(this, 'height', '24px');
3522
+ const bg = attr(this, 'bg', '#e2e8f0');
3523
+ const fill = attr(this, 'fill', '#3b82f6');
3524
+ const textColor = attr(this, 'text-color', textOutside ? '#334155' : '#ffffff');
3525
+ const rounded = attr(this, 'rounded', 'full');
3526
+ const shadow = attr(this, 'shadow');
3527
+ const border = attr(this, 'border');
3528
+ const borderColor = attr(this, 'border-color', '#000000');
3529
+ const pt = attr(this, 'pt', '0');
3530
+ const pb = attr(this, 'pb', '0');
3531
+ const mt = attr(this, 'mt', '0');
3532
+ const alignAttr = attr(this, 'align', 'center');
3533
+
3534
+ const wrapperStyles = {
3535
+ 'width': '100%',
3536
+ 'text-align': alignAttr,
3537
+ 'padding-top': pt,
3538
+ 'padding-bottom': pb,
3539
+ 'margin-top': mt,
3540
+ 'box-sizing': 'border-box',
3541
+ };
3542
+
3543
+ const trackStyles = {
3544
+ 'width': width,
3545
+ 'height': height,
3546
+ 'background-color': bg,
3547
+ 'border-radius': resolve(rounded, RADIUS) || rounded,
3548
+ 'overflow': 'hidden',
3549
+ 'position': 'relative',
3550
+ 'display': 'inline-block',
3551
+ };
3552
+ if (shadow) trackStyles['box-shadow'] = resolve(shadow, SHADOWS) || shadow;
3553
+ if (border) {
3554
+ trackStyles['border-width'] = border;
3555
+ trackStyles['border-style'] = 'solid';
3556
+ trackStyles['border-color'] = borderColor;
3557
+ }
3558
+
3559
+ const fillStyles = {
3560
+ 'width': `${progress}%`,
3561
+ 'height': '100%',
3562
+ 'background-color': fill,
3563
+ 'border-radius': 'inherit',
3564
+ 'transition': 'width 0.3s ease',
3565
+ };
3566
+
3567
+ const labelInsideStyles = {
3568
+ 'position': 'absolute',
3569
+ 'top': '50%',
3570
+ 'left': '50%',
3571
+ 'transform': 'translate(-50%, -50%)',
3572
+ 'color': textColor,
3573
+ 'font-size': '14px',
3574
+ 'font-weight': '600',
3575
+ 'white-space': 'nowrap',
3576
+ };
3577
+
3578
+ const labelOutsideStyles = {
3579
+ 'display': 'block',
3580
+ 'color': textColor,
3581
+ 'font-size': '14px',
3582
+ 'font-weight': '500',
3583
+ 'margin-bottom': '8px',
3584
+ };
3585
+
3586
+ // Build data attributes
3587
+ let dataAttrs = 'data-type="ProgressBar/V1"';
3588
+ dataAttrs += ` data-progress="${progress}"`;
3589
+ if (text) dataAttrs += ` data-text="${text}"`;
3590
+ dataAttrs += ` data-text-outside="${textOutside}"`;
3591
+ dataAttrs += ` data-bg="${bg}"`;
3592
+ dataAttrs += ` data-fill="${fill}"`;
3593
+ dataAttrs += ` data-height="${height}"`;
3594
+ if (rounded !== 'full') dataAttrs += ` data-rounded="${rounded}"`;
3595
+ if (shadow) dataAttrs += ` data-shadow="${shadow}"`;
3596
+ if (border) dataAttrs += ` data-border="${border}"`;
3597
+ if (borderColor !== '#000000') dataAttrs += ` data-border-color="${borderColor}"`;
3598
+
3599
+ // Build label HTML
3600
+ let labelHtml = '';
3601
+ if (text && textOutside) {
3602
+ labelHtml = `<span class="progress-label" style="${buildStyle(labelOutsideStyles)}">${text}</span>`;
3603
+ } else if (text) {
3604
+ labelHtml = `<span class="progress-label" style="${buildStyle(labelInsideStyles)}">${text}</span>`;
3605
+ }
3606
+
3607
+ this.outerHTML = `
3608
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3609
+ ${textOutside ? labelHtml : ''}
3610
+ <div class="progress" style="${buildStyle(trackStyles)}">
3611
+ <div class="progress-bar" style="${buildStyle(fillStyles)}"></div>
3612
+ ${!textOutside ? labelHtml : ''}
3613
+ </div>
3614
+ </div>
3615
+ `;
3616
+ }
3617
+ }
3618
+
3619
+ /**
3620
+ * <cf-video-popup> - Clickable thumbnail that opens video in modal
3621
+ *
3622
+ * Attributes:
3623
+ * url - YouTube video URL (required)
3624
+ * thumbnail - Custom thumbnail URL (auto-generated from YouTube if not provided)
3625
+ * alt - Alt text for thumbnail
3626
+ * width - Thumbnail width (default: 100%)
3627
+ * align - Alignment: left, center, right (default: center)
3628
+ * rounded - Border radius (default: lg)
3629
+ * shadow - Box shadow (default: lg)
3630
+ * border - Border width
3631
+ * border-color - Border color
3632
+ * overlay-bg - Modal overlay background (default: rgba(0,0,0,0.8))
3633
+ * play-icon - Show play icon overlay (default: true)
3634
+ * play-icon-size - Play icon size (default: 64px)
3635
+ * play-icon-color - Play icon color (default: #ffffff)
3636
+ * pt - Padding top
3637
+ * pb - Padding bottom
3638
+ * mt - Margin top
3639
+ */
3640
+ class CFVideoPopup extends CFElement {
3641
+ render() {
3642
+ const url = attr(this, 'url', '');
3643
+ let thumbnail = attr(this, 'thumbnail', '');
3644
+ const alt = attr(this, 'alt', 'Video thumbnail');
3645
+ const width = attr(this, 'width', '100%');
3646
+ const alignAttr = attr(this, 'align', 'center');
3647
+ const rounded = attr(this, 'rounded', 'lg');
3648
+ const shadow = attr(this, 'shadow', 'lg');
3649
+ const border = attr(this, 'border');
3650
+ const borderColor = attr(this, 'border-color', '#000000');
3651
+ const overlayBg = attr(this, 'overlay-bg', 'rgba(0,0,0,0.8)');
3652
+ const playIcon = attr(this, 'play-icon', 'true') !== 'false';
3653
+ const playIconSize = attr(this, 'play-icon-size', '64px');
3654
+ const playIconColor = attr(this, 'play-icon-color', '#ffffff');
3655
+ const pt = attr(this, 'pt', '0');
3656
+ const pb = attr(this, 'pb', '0');
3657
+ const mt = attr(this, 'mt', '0');
3658
+
3659
+ // Extract YouTube video ID and generate thumbnail if not provided
3660
+ const videoId = extractYouTubeVideoId(url);
3661
+ if (!thumbnail && videoId) {
3662
+ thumbnail = `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
3663
+ }
3664
+
3665
+ const wrapperStyles = {
3666
+ 'width': '100%',
3667
+ 'text-align': alignAttr,
3668
+ 'padding-top': pt,
3669
+ 'padding-bottom': pb,
3670
+ 'margin-top': mt,
3671
+ 'box-sizing': 'border-box',
3672
+ };
3673
+
3674
+ const imageWrapperStyles = {
3675
+ 'display': 'inline-block',
3676
+ 'position': 'relative',
3677
+ 'cursor': 'pointer',
3678
+ 'width': width,
3679
+ 'max-width': '100%',
3680
+ };
3681
+
3682
+ const imgStyles = {
3683
+ 'display': 'block',
3684
+ 'width': '100%',
3685
+ 'height': 'auto',
3686
+ 'border-radius': resolve(rounded, RADIUS) || rounded,
3687
+ };
3688
+ if (shadow) imgStyles['box-shadow'] = resolve(shadow, SHADOWS) || shadow;
3689
+ if (border) {
3690
+ imgStyles['border-width'] = border;
3691
+ imgStyles['border-style'] = 'solid';
3692
+ imgStyles['border-color'] = borderColor;
3693
+ }
3694
+
3695
+ const playIconStyles = {
3696
+ 'position': 'absolute',
3697
+ 'top': '50%',
3698
+ 'left': '50%',
3699
+ 'transform': 'translate(-50%, -50%)',
3700
+ 'font-size': playIconSize,
3701
+ 'color': playIconColor,
3702
+ 'opacity': '0.9',
3703
+ 'text-shadow': '0 2px 8px rgba(0,0,0,0.3)',
3704
+ 'pointer-events': 'none',
3705
+ };
3706
+
3707
+ // Build data attributes
3708
+ let dataAttrs = 'data-type="VideoPopup/V1"';
3709
+ dataAttrs += ` data-video-url="${url}"`;
3710
+ dataAttrs += ` data-video-type="youtube"`;
3711
+ dataAttrs += ` data-thumbnail="${thumbnail}"`;
3712
+ dataAttrs += ` data-overlay-bg="${overlayBg}"`;
3713
+ if (rounded !== 'lg') dataAttrs += ` data-rounded="${rounded}"`;
3714
+ if (shadow && shadow !== 'lg') dataAttrs += ` data-shadow="${shadow}"`;
3715
+ if (border) dataAttrs += ` data-border="${border}"`;
3716
+ if (borderColor !== '#000000') dataAttrs += ` data-border-color="${borderColor}"`;
3717
+
3718
+ const playIconHtml = playIcon
3719
+ ? `<i class="fas fa-play-circle" style="${buildStyle(playIconStyles)}"></i>`
3720
+ : '';
3721
+
3722
+ this.outerHTML = `
3723
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3724
+ <div class="elImageWrapper" style="${buildStyle(imageWrapperStyles)}">
3725
+ <img class="elImage" src="${thumbnail}" alt="${alt}" style="${buildStyle(imgStyles)}" />
3726
+ ${playIconHtml}
3727
+ </div>
3728
+ </div>
3729
+ `;
3730
+ }
3731
+ }
3732
+
3733
+ /**
3734
+ * <cf-countdown> - Countdown timer to a specific date/time
3735
+ *
3736
+ * Attributes:
3737
+ * end-date - Target date (YYYY-MM-DD format, required)
3738
+ * end-time - Target time (HH:MM:SS format, default: 00:00:00)
3739
+ * timezone - Timezone (default: America/New_York)
3740
+ * show-days - Show days (default: true)
3741
+ * show-hours - Show hours (default: true)
3742
+ * show-minutes - Show minutes (default: true)
3743
+ * show-seconds - Show seconds (default: true)
3744
+ * redirect - URL to redirect when countdown ends
3745
+ * number-bg - Background color for number boxes (default: #1C65E1)
3746
+ * number-color - Number text color (default: #ffffff)
3747
+ * label-color - Label text color (default: #164EAD)
3748
+ * number-size - Number font size (default: 28px)
3749
+ * label-size - Label font size (default: 11px)
3750
+ * rounded - Border radius for number boxes (default: lg)
3751
+ * gap - Gap between countdown units (default: 0.65em)
3752
+ * align - Alignment: left, center, right (default: center)
3753
+ * pt - Padding top
3754
+ * pb - Padding bottom
3755
+ * mt - Margin top
3756
+ */
3757
+ class CFCountdown extends CFElement {
3758
+ render() {
3759
+ const endDate = attr(this, 'end-date', '');
3760
+ const endTime = attr(this, 'end-time', '00:00:00');
3761
+ const timezone = attr(this, 'timezone', 'America/New_York');
3762
+ const showDays = attr(this, 'show-days', 'true') === 'true';
3763
+ const showHours = attr(this, 'show-hours', 'true') === 'true';
3764
+ const showMinutes = attr(this, 'show-minutes', 'true') === 'true';
3765
+ const showSeconds = attr(this, 'show-seconds', 'true') === 'true';
3766
+ const redirect = attr(this, 'redirect', '');
3767
+ const numberBg = attr(this, 'number-bg', '#1C65E1');
3768
+ const numberColor = attr(this, 'number-color', '#ffffff');
3769
+ const labelColor = attr(this, 'label-color', '#164EAD');
3770
+ const numberSize = attr(this, 'number-size', '28px');
3771
+ const labelSize = attr(this, 'label-size', '11px');
3772
+ const rounded = attr(this, 'rounded', 'lg');
3773
+ const gap = attr(this, 'gap', '0.65em');
3774
+ const alignAttr = attr(this, 'align', 'center');
3775
+ const pt = attr(this, 'pt', '0');
3776
+ const pb = attr(this, 'pb', '0');
3777
+ const mt = attr(this, 'mt', '0');
3778
+
3779
+ const wrapperStyles = {
3780
+ 'width': '100%',
3781
+ 'padding-top': pt,
3782
+ 'padding-bottom': pb,
3783
+ 'margin-top': mt,
3784
+ 'box-sizing': 'border-box',
3785
+ };
3786
+
3787
+ const rowStyles = {
3788
+ 'display': 'flex',
3789
+ 'justify-content': alignAttr === 'left' ? 'flex-start' : alignAttr === 'right' ? 'flex-end' : 'center',
3790
+ 'gap': gap,
3791
+ 'flex-wrap': 'wrap',
3792
+ };
3793
+
3794
+ const columnStyles = {
3795
+ 'display': 'flex',
3796
+ 'flex-direction': 'column',
3797
+ 'align-items': 'center',
3798
+ 'gap': '0.5em',
3799
+ };
3800
+
3801
+ const numberContainerStyles = {
3802
+ 'background-color': numberBg,
3803
+ 'padding': '14px',
3804
+ 'border-radius': resolve(rounded, RADIUS) || rounded,
3805
+ 'min-width': '60px',
3806
+ 'text-align': 'center',
3807
+ };
3808
+
3809
+ const numberStyles = {
3810
+ 'color': numberColor,
3811
+ 'font-size': numberSize,
3812
+ 'font-weight': '700',
3813
+ 'line-height': '100%',
3814
+ };
3815
+
3816
+ const labelStyles = {
3817
+ 'color': labelColor,
3818
+ 'font-size': labelSize,
3819
+ 'font-weight': '600',
3820
+ 'text-transform': 'uppercase',
3821
+ };
3822
+
3823
+ // Build data attributes
3824
+ let dataAttrs = 'data-type="Countdown/V1"';
3825
+ dataAttrs += ` data-end-date="${endDate}"`;
3826
+ dataAttrs += ` data-end-time="${endTime}"`;
3827
+ dataAttrs += ` data-timezone="${timezone}"`;
3828
+ dataAttrs += ` data-show-days="${showDays}"`;
3829
+ dataAttrs += ` data-show-hours="${showHours}"`;
3830
+ dataAttrs += ` data-show-minutes="${showMinutes}"`;
3831
+ dataAttrs += ` data-show-seconds="${showSeconds}"`;
3832
+ if (redirect) dataAttrs += ` data-redirect="${redirect}"`;
3833
+ dataAttrs += ` data-number-bg="${numberBg}"`;
3834
+ dataAttrs += ` data-number-color="${numberColor}"`;
3835
+ dataAttrs += ` data-label-color="${labelColor}"`;
3836
+
3837
+ // Build countdown units
3838
+ const units = [];
3839
+ if (showDays) units.push({ value: '00', label: 'Days', unitClass: 'days' });
3840
+ if (showHours) units.push({ value: '00', label: 'Hours', unitClass: 'hours' });
3841
+ if (showMinutes) units.push({ value: '00', label: 'Minutes', unitClass: 'minutes' });
3842
+ if (showSeconds) units.push({ value: '00', label: 'Seconds', unitClass: 'seconds' });
3843
+
3844
+ const unitsHtml = units.map(unit => `
3845
+ <div class="elCountdownColumn" style="${buildStyle(columnStyles)}">
3846
+ <div class="elCountdownAmountContainer" style="${buildStyle(numberContainerStyles)}">
3847
+ <span class="elCountdownAmount" data-unit="${unit.unitClass}" style="${buildStyle(numberStyles)}">${unit.value}</span>
3848
+ </div>
3849
+ <span class="elCountdownPeriod" style="${buildStyle(labelStyles)}">${unit.label}</span>
3850
+ </div>
3851
+ `).join('');
3852
+
3853
+ this.outerHTML = `
3854
+ <div ${dataAttrs} style="${buildStyle(wrapperStyles)}">
3855
+ <div class="elCountdownRow" style="${buildStyle(rowStyles)}">
3856
+ ${unitsHtml}
3857
+ </div>
3858
+ </div>
3859
+ `;
3860
+ }
3861
+ }
3862
+
3863
+ // ==========================================================================
3864
+ // REGISTER ALL CUSTOM ELEMENTS
3865
+ // ==========================================================================
3866
+
3867
+ // ==========================================================================
3868
+ // PLACEHOLDER COMPONENTS
3869
+ // ==========================================================================
3870
+
3871
+ /**
3872
+ * <cf-checkout-placeholder> - Placeholder for ClickFunnels checkout form
3873
+ * Outputs: Checkout/V2
3874
+ */
3875
+ class CFCheckoutPlaceholder extends CFElement {
3876
+ render() {
3877
+ const width = attr(this, "width", "100%");
3878
+ const minHeight = attr(this, "min-height", "400px");
3879
+ const mt = attr(this, "mt");
3880
+
3881
+ const containerStyles = {
3882
+ display: "flex",
3883
+ "flex-direction": "column",
3884
+ "align-items": "center",
3885
+ "justify-content": "center",
3886
+ width: width,
3887
+ "min-height": minHeight,
3888
+ "background": "linear-gradient(135deg, #fef3c7 0%, #fde68a 100%)",
3889
+ "border": "3px dashed #f59e0b",
3890
+ "border-radius": "16px",
3891
+ padding: "32px",
3892
+ "box-sizing": "border-box",
3893
+ "text-align": "center",
3894
+ gap: "16px",
3895
+ };
3896
+
3897
+ if (mt) containerStyles["margin-top"] = mt;
3898
+
3899
+ this.outerHTML = `
3900
+ <div
3901
+ data-type="CheckoutPlaceholder"
3902
+ class="cf-placeholder cf-checkout-placeholder"
3903
+ style="${buildStyle(containerStyles)}"
3904
+ >
3905
+ <div style="display:inline-flex;align-items:center;gap:6px;background-color:#fbbf24;color:#78350f;padding:6px 12px;border-radius:9999px;font-size:12px;font-weight:600;">
3906
+ <i class="fas fa-info-circle"></i>
3907
+ <span>Checkout/V2</span>
3908
+ </div>
3909
+ <i class="fas fa-credit-card" style="font-size:48px;color:#d97706;"></i>
3910
+ <h3 style="font-size:20px;font-weight:700;color:#92400e;margin:0;">Checkout Form</h3>
3911
+ <p style="font-size:14px;color:#b45309;margin:0;max-width:400px;line-height:1.5;">
3912
+ This placeholder outputs a Checkout/V2 element. The checkout form collects
3913
+ payment information and processes orders in your funnel.
3914
+ </p>
3915
+ </div>
3916
+ `;
3917
+ }
3918
+ }
3919
+
3920
+ /**
3921
+ * <cf-order-summary-placeholder> - Placeholder for order summary display
3922
+ * Outputs: CheckoutOrderSummary/V1
3923
+ */
3924
+ class CFOrderSummaryPlaceholder extends CFElement {
3925
+ render() {
3926
+ const width = attr(this, "width", "100%");
3927
+ const minHeight = attr(this, "min-height", "200px");
3928
+ const mt = attr(this, "mt");
3929
+
3930
+ const containerStyles = {
3931
+ display: "flex",
3932
+ "flex-direction": "column",
3933
+ "align-items": "center",
3934
+ "justify-content": "center",
3935
+ width: width,
3936
+ "min-height": minHeight,
3937
+ "background": "linear-gradient(135deg, #e0e7ff 0%, #c7d2fe 100%)",
3938
+ "border": "3px dashed #6366f1",
3939
+ "border-radius": "16px",
3940
+ padding: "32px",
3941
+ "box-sizing": "border-box",
3942
+ "text-align": "center",
3943
+ gap: "16px",
3944
+ };
3945
+
3946
+ if (mt) containerStyles["margin-top"] = mt;
3947
+
3948
+ this.outerHTML = `
3949
+ <div
3950
+ data-type="OrderSummaryPlaceholder"
3951
+ class="cf-placeholder cf-order-summary-placeholder"
3952
+ style="${buildStyle(containerStyles)}"
3953
+ >
3954
+ <div style="display:inline-flex;align-items:center;gap:6px;background-color:#818cf8;color:#1e1b4b;padding:6px 12px;border-radius:9999px;font-size:12px;font-weight:600;">
3955
+ <i class="fas fa-info-circle"></i>
3956
+ <span>CheckoutOrderSummary/V1</span>
3957
+ </div>
3958
+ <i class="fas fa-list-alt" style="font-size:48px;color:#4f46e5;"></i>
3959
+ <h3 style="font-size:20px;font-weight:700;color:#3730a3;margin:0;">Order Summary</h3>
3960
+ <p style="font-size:14px;color:#4338ca;margin:0;max-width:400px;line-height:1.5;">
3961
+ This placeholder outputs a CheckoutOrderSummary/V1 element. It displays
3962
+ the cart items, prices, and totals linked to your checkout form.
3963
+ </p>
3964
+ </div>
3965
+ `;
3966
+ }
3967
+ }
3968
+
3969
+ /**
3970
+ * <cf-confirmation-placeholder> - Placeholder for order confirmation/receipt
3971
+ * Outputs: OrderConfirmation/V1
3972
+ */
3973
+ class CFConfirmationPlaceholder extends CFElement {
3974
+ render() {
3975
+ const width = attr(this, "width", "100%");
3976
+ const minHeight = attr(this, "min-height", "300px");
3977
+ const mt = attr(this, "mt");
3978
+
3979
+ const containerStyles = {
3980
+ display: "flex",
3981
+ "flex-direction": "column",
3982
+ "align-items": "center",
3983
+ "justify-content": "center",
3984
+ width: width,
3985
+ "min-height": minHeight,
3986
+ "background": "linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%)",
3987
+ "border": "3px dashed #10b981",
3988
+ "border-radius": "16px",
3989
+ padding: "32px",
3990
+ "box-sizing": "border-box",
3991
+ "text-align": "center",
3992
+ gap: "16px",
3993
+ };
3994
+
3995
+ if (mt) containerStyles["margin-top"] = mt;
3996
+
3997
+ this.outerHTML = `
3998
+ <div
3999
+ data-type="ConfirmationPlaceholder"
4000
+ class="cf-placeholder cf-confirmation-placeholder"
4001
+ style="${buildStyle(containerStyles)}"
4002
+ >
4003
+ <div style="display:inline-flex;align-items:center;gap:6px;background-color:#34d399;color:#064e3b;padding:6px 12px;border-radius:9999px;font-size:12px;font-weight:600;">
4004
+ <i class="fas fa-info-circle"></i>
4005
+ <span>OrderConfirmation/V1</span>
4006
+ </div>
4007
+ <i class="fas fa-receipt" style="font-size:48px;color:#059669;"></i>
4008
+ <h3 style="font-size:20px;font-weight:700;color:#065f46;margin:0;">Order Confirmation</h3>
4009
+ <p style="font-size:14px;color:#047857;margin:0;max-width:400px;line-height:1.5;">
4010
+ This placeholder outputs an OrderConfirmation/V1 element. It shows
4011
+ purchase details and receipt information on thank you pages.
4012
+ </p>
4013
+ </div>
4014
+ `;
4015
+ }
4016
+ }
4017
+
4018
+ const elements = {
4019
+ "cf-page": CFPage,
4020
+ "cf-section": CFSection,
4021
+ "cf-row": CFRow,
4022
+ "cf-col": CFCol,
4023
+ // "cf-col-inner": CFColInner,
4024
+ "cf-flex": CFFlex,
4025
+ "cf-popup": CFPopup,
4026
+ "cf-headline": CFHeadline,
4027
+ "cf-subheadline": CFSubheadline,
4028
+ "cf-paragraph": CFParagraph,
4029
+ "cf-button": CFButton,
4030
+ "cf-image": CFImage,
4031
+ "cf-icon": CFIcon,
4032
+ "cf-video": CFVideo,
4033
+ "cf-divider": CFDivider,
4034
+ "cf-input": CFInput,
4035
+ "cf-textarea": CFTextarea,
4036
+ "cf-select": CFSelect,
4037
+ "cf-checkbox": CFCheckbox,
4038
+ "cf-bullet-list": CFBulletList,
4039
+ "cf-progress-bar": CFProgressBar,
4040
+ "cf-video-popup": CFVideoPopup,
4041
+ "cf-countdown": CFCountdown,
4042
+ // Placeholders
4043
+ "cf-checkout-placeholder": CFCheckoutPlaceholder,
4044
+ "cf-order-summary-placeholder": CFOrderSummaryPlaceholder,
4045
+ "cf-confirmation-placeholder": CFConfirmationPlaceholder,
4046
+ };
4047
+
4048
+ // Register elements
4049
+ Object.entries(elements).forEach(([name, constructor]) => {
4050
+ if (!customElements.get(name)) {
4051
+ customElements.define(name, constructor);
4052
+ }
4053
+ });
4054
+
4055
+ // ==========================================================================
4056
+ // ANIMATIONS - Apply animate.css animations to elements
4057
+ // ==========================================================================
4058
+
4059
+ // Map ClickFunnels animation names to animate.css classes
4060
+ const ANIMATION_MAP = {
4061
+ // Entrance animations
4062
+ 'fade-in': 'fadeIn',
4063
+ 'glide-in': 'fadeInUp',
4064
+ 'slide-in': 'slideInLeft',
4065
+ 'bounce-in': 'bounceIn',
4066
+ 'expand-in': 'zoomIn',
4067
+ 'fold-in': 'fadeInDown',
4068
+ 'puff-in': 'zoomInUp',
4069
+ 'spin-in': 'rotateIn',
4070
+ 'flip-in': 'flipInX',
4071
+ 'turn-in': 'rotateInDownLeft',
4072
+ 'float-in': 'fadeInUp',
4073
+ 'reveal': 'fadeIn',
4074
+
4075
+ // Looping/attention animations (using subtle custom versions)
4076
+ 'blink': 'flash',
4077
+ 'rocking': 'subtleSwing',
4078
+ 'bouncing': 'subtleBounce',
4079
+ 'wooble': 'subtleWobble',
4080
+ 'elevate': 'subtlePulse',
4081
+ 'shake': 'shakeX',
4082
+ 'pulse': 'subtlePulse',
4083
+ 'tada': 'tada',
4084
+ 'jello': 'jello',
4085
+ 'heartbeat': 'heartBeat',
4086
+ 'rubber-band': 'rubberBand',
4087
+ };
4088
+
4089
+ // Load animate.css from CDN and inject custom subtle animations
4090
+ function loadAnimateCSS() {
4091
+ if (document.querySelector('link[href*="animate.css"]')) {
4092
+ injectCustomAnimations();
4093
+ return Promise.resolve();
4094
+ }
4095
+
4096
+ return new Promise((resolve, reject) => {
4097
+ const link = document.createElement('link');
4098
+ link.rel = 'stylesheet';
4099
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css';
4100
+ link.onload = () => {
4101
+ injectCustomAnimations();
4102
+ resolve();
4103
+ };
4104
+ link.onerror = reject;
4105
+ document.head.appendChild(link);
4106
+ });
4107
+ }
4108
+
4109
+ // Inject custom subtle animation keyframes
4110
+ function injectCustomAnimations() {
4111
+ if (document.getElementById('funnelwind-custom-animations')) return;
4112
+
4113
+ const style = document.createElement('style');
4114
+ style.id = 'funnelwind-custom-animations';
4115
+ style.textContent = `
4116
+ /* Subtle wobble - less rotation than default */
4117
+ @keyframes subtleWobble {
4118
+ 0%, 100% { transform: translateX(0); }
4119
+ 15% { transform: translateX(-3px) rotate(-1deg); }
4120
+ 30% { transform: translateX(2px) rotate(0.5deg); }
4121
+ 45% { transform: translateX(-2px) rotate(-0.5deg); }
4122
+ 60% { transform: translateX(1px) rotate(0.25deg); }
4123
+ 75% { transform: translateX(-1px) rotate(-0.25deg); }
4124
+ }
4125
+ .animate__subtleWobble {
4126
+ animation-name: subtleWobble;
4127
+ }
4128
+
4129
+ /* Subtle bounce - less dramatic vertical movement */
4130
+ @keyframes subtleBounce {
4131
+ 0%, 20%, 53%, 100% {
4132
+ animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
4133
+ transform: translateY(0);
4134
+ }
4135
+ 40%, 43% {
4136
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
4137
+ transform: translateY(-8px);
4138
+ }
4139
+ 70% {
4140
+ animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06);
4141
+ transform: translateY(-4px);
4142
+ }
4143
+ 80% {
4144
+ transform: translateY(0);
4145
+ }
4146
+ 90% {
4147
+ transform: translateY(-2px);
4148
+ }
4149
+ }
4150
+ .animate__subtleBounce {
4151
+ animation-name: subtleBounce;
4152
+ transform-origin: center bottom;
4153
+ }
4154
+
4155
+ /* Subtle pulse - less scale change */
4156
+ @keyframes subtlePulse {
4157
+ 0%, 100% { transform: scale(1); }
4158
+ 50% { transform: scale(1.03); }
4159
+ }
4160
+ .animate__subtlePulse {
4161
+ animation-name: subtlePulse;
4162
+ }
4163
+
4164
+ /* Subtle swing/rocking - less rotation */
4165
+ @keyframes subtleSwing {
4166
+ 0%, 100% { transform: rotate(0deg); }
4167
+ 20% { transform: rotate(3deg); }
4168
+ 40% { transform: rotate(-2deg); }
4169
+ 60% { transform: rotate(1deg); }
4170
+ 80% { transform: rotate(-1deg); }
4171
+ }
4172
+ .animate__subtleSwing {
4173
+ animation-name: subtleSwing;
4174
+ transform-origin: top center;
4175
+ }
4176
+ `;
4177
+ document.head.appendChild(style);
4178
+ }
4179
+
4180
+ // Initialize animations when DOM is ready
4181
+ function initAnimations() {
4182
+ // Find all elements with animation attributes
4183
+ const animatedElements = document.querySelectorAll('[data-animation-type]');
4184
+
4185
+ animatedElements.forEach(element => {
4186
+ const animationType = element.getAttribute('data-animation-type');
4187
+ const animationTime = parseInt(element.getAttribute('data-animation-time') || '1000', 10);
4188
+ const animationDelay = parseInt(element.getAttribute('data-animation-delay') || '0', 10);
4189
+ const animationTrigger = element.getAttribute('data-animation-trigger') || 'load';
4190
+ const animationLoop = element.getAttribute('data-animation-loop') === 'true';
4191
+ const animationOnce = element.getAttribute('data-animation-once') !== 'false';
4192
+ const animationTiming = element.getAttribute('data-animation-timing-function') || 'ease';
4193
+
4194
+ // Get animate.css class name
4195
+ const animateClass = ANIMATION_MAP[animationType] || animationType;
4196
+
4197
+ // Set CSS custom properties for duration and delay
4198
+ element.style.setProperty('--animate-duration', `${animationTime}ms`);
4199
+ element.style.setProperty('animation-timing-function', animationTiming);
4200
+
4201
+ // Initially hide element (unless it's a looping animation)
4202
+ if (!animationLoop) {
4203
+ element.style.opacity = '0';
4204
+ }
4205
+
4206
+ // Handle different triggers
4207
+ switch (animationTrigger) {
4208
+ case 'scroll':
4209
+ setupScrollTrigger(element, animateClass, animationDelay, animationLoop, animationOnce);
4210
+ break;
4211
+ case 'hover':
4212
+ setupHoverTrigger(element, animateClass, animationLoop);
4213
+ break;
4214
+ case 'load':
4215
+ default:
4216
+ setupLoadTrigger(element, animateClass, animationDelay, animationLoop);
4217
+ break;
4218
+ }
4219
+ });
4220
+ }
4221
+
4222
+ // Trigger animation on page load
4223
+ function setupLoadTrigger(element, animateClass, delay, loop) {
4224
+ setTimeout(() => {
4225
+ element.style.opacity = '1';
4226
+ element.classList.add('animate__animated', `animate__${animateClass}`);
4227
+
4228
+ if (loop) {
4229
+ element.classList.add('animate__infinite');
4230
+ }
4231
+ }, delay);
4232
+ }
4233
+
4234
+ // Trigger animation when element scrolls into view
4235
+ function setupScrollTrigger(element, animateClass, delay, loop, once) {
4236
+ const observer = new IntersectionObserver((entries) => {
4237
+ entries.forEach(entry => {
4238
+ if (entry.isIntersecting) {
4239
+ setTimeout(() => {
4240
+ element.style.opacity = '1';
4241
+ element.classList.add('animate__animated', `animate__${animateClass}`);
4242
+
4243
+ if (loop) {
4244
+ element.classList.add('animate__infinite');
4245
+ }
4246
+ }, delay);
4247
+
4248
+ if (once) {
4249
+ observer.unobserve(element);
4250
+ }
4251
+ } else if (!once) {
4252
+ // Reset animation when out of view
4253
+ element.style.opacity = '0';
4254
+ element.classList.remove('animate__animated', `animate__${animateClass}`);
4255
+ }
4256
+ });
4257
+ }, {
4258
+ threshold: 0.1,
4259
+ rootMargin: '0px 0px -50px 0px'
4260
+ });
4261
+
4262
+ observer.observe(element);
4263
+ }
4264
+
4265
+ // Trigger animation on hover
4266
+ function setupHoverTrigger(element, animateClass, loop) {
4267
+ // Show element initially for hover animations
4268
+ element.style.opacity = '1';
4269
+
4270
+ element.addEventListener('mouseenter', () => {
4271
+ element.classList.add('animate__animated', `animate__${animateClass}`);
4272
+ if (loop) {
4273
+ element.classList.add('animate__infinite');
4274
+ }
4275
+ });
4276
+
4277
+ element.addEventListener('mouseleave', () => {
4278
+ element.classList.remove('animate__animated', `animate__${animateClass}`, 'animate__infinite');
4279
+ });
4280
+
4281
+ // Also handle animation end for non-looping
4282
+ element.addEventListener('animationend', () => {
4283
+ if (!loop) {
4284
+ element.classList.remove('animate__animated', `animate__${animateClass}`);
4285
+ }
4286
+ });
4287
+ }
4288
+
4289
+ // ==========================================================================
4290
+ // VIDEO BACKGROUND INITIALIZATION
4291
+ // ==========================================================================
4292
+
4293
+ /**
4294
+ * Extract YouTube video ID from URL
4295
+ */
4296
+ function extractYouTubeVideoIdForBackground(url) {
4297
+ if (!url) return null;
4298
+ const match = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([^&\s?]+)/);
4299
+ return match ? match[1] : null;
4300
+ }
4301
+
4302
+ /**
4303
+ * Initialize video backgrounds on all sections with video-bg-url attribute
4304
+ */
4305
+ function initVideoBackgrounds() {
4306
+ const sections = document.querySelectorAll('[data-video-bg-url]');
4307
+
4308
+ sections.forEach(section => {
4309
+ const videoUrl = section.getAttribute('data-video-bg-url');
4310
+ const videoId = extractYouTubeVideoIdForBackground(videoUrl);
4311
+
4312
+ if (!videoId) return;
4313
+
4314
+ const overlay = section.getAttribute('data-video-bg-overlay') || section.getAttribute('data-overlay');
4315
+ const hideMobile = section.getAttribute('data-video-bg-hide-mobile') !== 'false';
4316
+
4317
+ // Create video background container
4318
+ const videoContainer = document.createElement('div');
4319
+ videoContainer.className = 'cf-video-background';
4320
+ videoContainer.style.cssText = `
4321
+ position: absolute;
4322
+ inset: 0;
4323
+ overflow: hidden;
4324
+ z-index: 0;
4325
+ pointer-events: none;
4326
+ `;
4327
+
4328
+ // Create iframe wrapper for scaling/positioning
4329
+ const iframeWrapper = document.createElement('div');
4330
+ iframeWrapper.className = 'cf-video-background-wrapper';
4331
+ iframeWrapper.style.cssText = `
4332
+ position: absolute;
4333
+ width: 100%;
4334
+ height: 100%;
4335
+ transform: translateY(-50%) scale(1.5);
4336
+ top: 50%;
4337
+ `;
4338
+
4339
+ // Create YouTube iframe with autoplay, mute, loop
4340
+ const iframe = document.createElement('iframe');
4341
+ iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1&mute=1&loop=1&playlist=${videoId}&controls=0&showinfo=0&rel=0&modestbranding=1&playsinline=1&enablejsapi=1`;
4342
+ iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
4343
+ iframe.allowFullscreen = true;
4344
+ iframe.style.cssText = `
4345
+ position: absolute;
4346
+ top: 50%;
4347
+ left: 50%;
4348
+ width: 100vw;
4349
+ height: 100vh;
4350
+ min-width: 100%;
4351
+ min-height: 100%;
4352
+ transform: translate(-50%, -50%);
4353
+ object-fit: cover;
4354
+ border: none;
4355
+ `;
4356
+
4357
+ iframeWrapper.appendChild(iframe);
4358
+ videoContainer.appendChild(iframeWrapper);
4359
+
4360
+ // Create overlay if specified
4361
+ if (overlay) {
4362
+ const overlayEl = document.createElement('div');
4363
+ overlayEl.className = 'cf-video-background-overlay';
4364
+ overlayEl.style.cssText = `
4365
+ position: absolute;
4366
+ inset: 0;
4367
+ background: ${overlay};
4368
+ z-index: 1;
4369
+ pointer-events: none;
4370
+ `;
4371
+ videoContainer.appendChild(overlayEl);
4372
+ }
4373
+
4374
+ // Ensure section has relative positioning
4375
+ const sectionStyle = window.getComputedStyle(section);
4376
+ if (sectionStyle.position === 'static') {
4377
+ section.style.position = 'relative';
4378
+ }
4379
+
4380
+ // Insert video container as first child
4381
+ section.insertBefore(videoContainer, section.firstChild);
4382
+
4383
+ // Ensure content is above video
4384
+ const contentWrapper = section.querySelector(':scope > div:not(.cf-video-background):not(.cf-overlay)');
4385
+ if (contentWrapper) {
4386
+ contentWrapper.style.position = 'relative';
4387
+ contentWrapper.style.zIndex = '2';
4388
+ }
4389
+
4390
+ // Handle mobile visibility
4391
+ if (hideMobile) {
4392
+ const style = document.createElement('style');
4393
+ style.textContent = `
4394
+ @media (max-width: 768px) {
4395
+ .cf-video-background {
4396
+ display: none;
4397
+ }
4398
+ }
4399
+ `;
4400
+ if (!document.getElementById('cf-video-bg-mobile-styles')) {
4401
+ style.id = 'cf-video-bg-mobile-styles';
4402
+ document.head.appendChild(style);
4403
+ }
4404
+ }
4405
+ });
4406
+ }
4407
+
4408
+ /**
4409
+ * Inject CSS for video background styling
4410
+ */
4411
+ function injectVideoBackgroundStyles() {
4412
+ if (document.getElementById('funnelwind-video-bg-styles')) return;
4413
+
4414
+ const style = document.createElement('style');
4415
+ style.id = 'funnelwind-video-bg-styles';
4416
+ style.textContent = `
4417
+ /* Video background container */
4418
+ .cf-video-background {
4419
+ position: absolute;
4420
+ inset: 0;
4421
+ overflow: hidden;
4422
+ z-index: 0;
4423
+ pointer-events: none;
4424
+ }
4425
+
4426
+ .cf-video-background iframe {
4427
+ border: none;
4428
+ }
4429
+
4430
+ /* Ensure section content stays above video */
4431
+ [data-video-bg-url] > *:not(.cf-video-background):not(.cf-overlay) {
4432
+ position: relative;
4433
+ z-index: 2;
4434
+ }
4435
+ `;
4436
+ document.head.appendChild(style);
4437
+ }
4438
+
4439
+ // ==========================================================================
4440
+ // INITIALIZATION - Process elements in correct order (leaf-first)
4441
+ // ==========================================================================
4442
+
4443
+ /**
4444
+ * Initialize FunnelWind - transforms all cf-* elements
4445
+ * Call this after DOM is ready or after dynamic content is added
4446
+ */
4447
+ function initFunnelWind() {
4448
+ // Process from innermost to outermost (reverse order)
4449
+ const tagOrder = [
4450
+ // Elements first (innermost)
4451
+ "cf-icon",
4452
+ "cf-divider",
4453
+ "cf-image",
4454
+ "cf-video",
4455
+ "cf-headline",
4456
+ "cf-subheadline",
4457
+ "cf-paragraph",
4458
+ "cf-button",
4459
+ "cf-input",
4460
+ "cf-textarea",
4461
+ "cf-select",
4462
+ "cf-checkbox",
4463
+ "cf-bullet-list",
4464
+ "cf-progress-bar",
4465
+ "cf-video-popup",
4466
+ "cf-countdown",
4467
+ // Placeholders
4468
+ "cf-checkout-placeholder",
4469
+ "cf-order-summary-placeholder",
4470
+ "cf-confirmation-placeholder",
4471
+ // Then containers
4472
+ "cf-flex",
4473
+ "cf-col-inner",
4474
+ "cf-col",
4475
+ "cf-row",
4476
+ "cf-section",
4477
+ "cf-popup",
4478
+ "cf-page",
4479
+ ];
4480
+
4481
+ tagOrder.forEach((tag) => {
4482
+ document.querySelectorAll(tag).forEach((el) => {
4483
+ if (el.render && !el._rendered) {
4484
+ el.render();
4485
+ el._rendered = true;
4486
+ }
4487
+ });
4488
+ });
4489
+
4490
+ // Mark all cf-page elements as rendered to show content (FOUC prevention)
4491
+ document.querySelectorAll('cf-page').forEach((page) => {
4492
+ page.setAttribute('data-rendered', 'true');
4493
+ });
4494
+
4495
+ // Initialize animations, popup, and other functionality after elements are rendered
4496
+ // Small delay to ensure DOM is fully updated
4497
+ requestAnimationFrame(() => {
4498
+ loadAnimateCSS().then(initAnimations);
4499
+ initPopup();
4500
+ });
4501
+ }
4502
+
4503
+ /**
4504
+ * Initialize popup functionality
4505
+ * - Buttons with action="show-popup" open the popup
4506
+ * - Close button and overlay click close the popup
4507
+ * - Escape key closes the popup
4508
+ */
4509
+ function initPopup() {
4510
+ const popup = document.querySelector('.cf-popup-wrapper');
4511
+ if (!popup) return;
4512
+
4513
+ // Show popup function
4514
+ const showPopup = () => {
4515
+ popup.style.display = 'flex';
4516
+ };
4517
+
4518
+ // Hide popup function
4519
+ const hidePopup = () => {
4520
+ popup.style.display = 'none';
4521
+ };
4522
+
4523
+ // Set up buttons with action="popup" (data attribute after rendering)
4524
+ document.querySelectorAll('[data-action="popup"]').forEach(btn => {
4525
+ btn.addEventListener('click', (e) => {
4526
+ e.preventDefault();
4527
+ showPopup();
4528
+ });
4529
+ });
4530
+
4531
+ // Close on overlay click (but not modal itself)
4532
+ popup.addEventListener('click', (e) => {
4533
+ if (e.target === popup) {
4534
+ hidePopup();
4535
+ }
4536
+ });
4537
+
4538
+ // Close on Escape key
4539
+ document.addEventListener('keydown', (e) => {
4540
+ if (e.key === 'Escape' && popup.style.display === 'flex') {
4541
+ hidePopup();
4542
+ }
4543
+ });
4544
+
4545
+ // Expose popup control functions globally
4546
+ window.FunnelWindPopup = {
4547
+ show: showPopup,
4548
+ hide: hidePopup,
4549
+ toggle: () => {
4550
+ if (popup.style.display === 'flex') {
4551
+ hidePopup();
4552
+ } else {
4553
+ showPopup();
4554
+ }
4555
+ }
4556
+ };
4557
+ }
4558
+
4559
+ // Auto-initialize when DOM is ready
4560
+ if (document.readyState === "loading") {
4561
+ document.addEventListener("DOMContentLoaded", () => {
4562
+ styleguideManager.init();
4563
+ brandAssetsManager.init();
4564
+ initFunnelWind();
4565
+ // Initialize video backgrounds after rendering
4566
+ requestAnimationFrame(() => {
4567
+ injectVideoBackgroundStyles();
4568
+ initVideoBackgrounds();
4569
+ });
4570
+ });
4571
+ } else {
4572
+ // DOM already ready, use requestAnimationFrame to ensure all elements are parsed
4573
+ requestAnimationFrame(() => {
4574
+ styleguideManager.init();
4575
+ brandAssetsManager.init();
4576
+ initFunnelWind();
4577
+ // Initialize video backgrounds after rendering
4578
+ requestAnimationFrame(() => {
4579
+ injectVideoBackgroundStyles();
4580
+ initVideoBackgrounds();
4581
+ });
4582
+ });
4583
+ }
4584
+
4585
+ // Expose for manual re-initialization and styleguide management
4586
+ window.FunnelWind = {
4587
+ init: initFunnelWind,
4588
+ initAnimations: initAnimations,
4589
+ loadAnimateCSS: loadAnimateCSS,
4590
+ initVideoBackgrounds: initVideoBackgrounds,
4591
+ elements: elements,
4592
+ StyleguideManager: styleguideManager,
4593
+ BrandAssetsManager: brandAssetsManager,
4594
+ initStyleguide: (data) => {
4595
+ styleguideManager.init(data);
4596
+ initFunnelWind();
4597
+ },
4598
+ initBrandAssets: (data) => {
4599
+ brandAssetsManager.init(data);
4600
+ initFunnelWind();
4601
+ },
4602
+ };
4603
+
4604
+ // Also expose StyleguideManager globally for direct access
4605
+ window.StyleguideManager = styleguideManager;
4606
+ window.BrandAssetsManager = brandAssetsManager;
4607
+ })();