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.
- package/README.md +104 -0
- package/cf-elements.js +4607 -0
- 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
|
+
})();
|