Site builder: security & data-loss hardening + unified asset picker + audit backlog #3
@@ -2,6 +2,7 @@ import React, { CSSProperties, useCallback, useRef, useState } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
/* ---------- Types ---------- */
|
/* ---------- Types ---------- */
|
||||||
|
|
||||||
@@ -37,12 +38,6 @@ async function uploadToWhp(file: File): Promise<string | null> {
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Helper: escape HTML ---------- */
|
|
||||||
function esc(str: any): string {
|
|
||||||
str = String(str ?? "");
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Component ---------- */
|
/* ---------- Component ---------- */
|
||||||
|
|
||||||
export const Logo: UserComponent<LogoProps> = ({
|
export const Logo: UserComponent<LogoProps> = ({
|
||||||
@@ -394,7 +389,7 @@ Logo.craft = {
|
|||||||
let innerHtml: string;
|
let innerHtml: string;
|
||||||
if (props.type === 'image' && props.imageSrc) {
|
if (props.type === 'image' && props.imageSrc) {
|
||||||
const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' });
|
const imgStyle = cssPropsToString({ width: props.imageWidth || '120px', height: 'auto', display: 'block' });
|
||||||
innerHtml = `<img src="${esc(props.imageSrc)}" alt="${esc(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`;
|
innerHtml = `<img src="${escapeAttr(props.imageSrc)}" alt="${escapeAttr(props.text || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} />`;
|
||||||
} else {
|
} else {
|
||||||
const spanStyle = cssPropsToString({
|
const spanStyle = cssPropsToString({
|
||||||
fontWeight: props.fontWeight || '700',
|
fontWeight: props.fontWeight || '700',
|
||||||
@@ -402,7 +397,7 @@ Logo.craft = {
|
|||||||
fontFamily: props.fontFamily || 'Inter, sans-serif',
|
fontFamily: props.fontFamily || 'Inter, sans-serif',
|
||||||
color: props.color || '#1f2937',
|
color: props.color || '#1f2937',
|
||||||
});
|
});
|
||||||
innerHtml = `<span${spanStyle ? ` style="${spanStyle}"` : ''}>${esc(props.text || 'MySite')}</span>`;
|
innerHtml = `<span${spanStyle ? ` style="${spanStyle}"` : ''}>${escapeHtml(props.text || 'MySite')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const aStyle = cssPropsToString({
|
const aStyle = cssPropsToString({
|
||||||
@@ -414,6 +409,6 @@ Logo.craft = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<a href="${esc(href)}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`,
|
html: `<a href="${escapeAttr(href)}"${aStyle ? ` style="${aStyle}"` : ''}>${innerHtml}</a>`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { usePages } from '../../state/PageContext';
|
import { usePages } from '../../state/PageContext';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
/* ---------- Types ---------- */
|
/* ---------- Types ---------- */
|
||||||
|
|
||||||
@@ -34,12 +35,6 @@ const defaultLinks: MenuLink[] = [
|
|||||||
{ text: 'Contact', href: '#contact', isCta: true },
|
{ text: 'Contact', href: '#contact', isCta: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
/* ---------- Helper: escape HTML ---------- */
|
|
||||||
function esc(str: any): string {
|
|
||||||
str = String(str ?? "");
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Component ---------- */
|
/* ---------- Component ---------- */
|
||||||
|
|
||||||
export const Menu: UserComponent<MenuProps> = ({
|
export const Menu: UserComponent<MenuProps> = ({
|
||||||
@@ -494,7 +489,7 @@ Menu.craft = {
|
|||||||
borderRadius: link.isCta ? '6px' : '0',
|
borderRadius: link.isCta ? '6px' : '0',
|
||||||
transition: 'color 0.15s, background-color 0.15s',
|
transition: 'color 0.15s, background-color 0.15s',
|
||||||
});
|
});
|
||||||
return `<a href="${esc(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`;
|
return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
const hoverCss = `<style>
|
const hoverCss = `<style>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
|||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { usePages } from '../../state/PageContext';
|
import { usePages } from '../../state/PageContext';
|
||||||
import { useSiteDesign } from '../../state/SiteDesignContext';
|
import { useSiteDesign } from '../../state/SiteDesignContext';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
/* ---------- Types ---------- */
|
/* ---------- Types ---------- */
|
||||||
|
|
||||||
@@ -70,12 +71,6 @@ async function uploadToWhp(file: File): Promise<string | null> {
|
|||||||
} catch { return null; }
|
} catch { return null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- Helper: escape HTML ---------- */
|
|
||||||
function esc(str: any): string {
|
|
||||||
str = String(str ?? "");
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- Component ---------- */
|
/* ---------- Component ---------- */
|
||||||
|
|
||||||
export const Navbar: UserComponent<NavbarProps> = ({
|
export const Navbar: UserComponent<NavbarProps> = ({
|
||||||
@@ -853,7 +848,7 @@ Navbar.craft = {
|
|||||||
let logoHtml: string;
|
let logoHtml: string;
|
||||||
if (props.logoType === 'image' && props.logoImage) {
|
if (props.logoType === 'image' && props.logoImage) {
|
||||||
const imgStyle = cssPropsToString({ width: props.logoWidth || '120px', height: 'auto', display: 'block' });
|
const imgStyle = cssPropsToString({ width: props.logoWidth || '120px', height: 'auto', display: 'block' });
|
||||||
logoHtml = `<a href="${esc(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${esc(props.logoImage)}" alt="${esc(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`;
|
logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><img src="${escapeAttr(props.logoImage)}" alt="${escapeAttr(props.logoText || 'Logo')}"${imgStyle ? ` style="${imgStyle}"` : ''} /></a>`;
|
||||||
} else {
|
} else {
|
||||||
const logoStyle = cssPropsToString({
|
const logoStyle = cssPropsToString({
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
@@ -861,7 +856,7 @@ Navbar.craft = {
|
|||||||
fontFamily: props.logoFontFamily || 'Inter, sans-serif',
|
fontFamily: props.logoFontFamily || 'Inter, sans-serif',
|
||||||
color: props.logoColor || textCol,
|
color: props.logoColor || textCol,
|
||||||
});
|
});
|
||||||
logoHtml = `<a href="${esc(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${esc(props.logoText || 'MySite')}</span></a>`;
|
logoHtml = `<a href="${escapeAttr(logoUrl)}" style="text-decoration:none;display:flex;align-items:center;flex-shrink:0"><span${logoStyle ? ` style="${logoStyle}"` : ''}>${escapeHtml(props.logoText || 'MySite')}</span></a>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Links HTML
|
// Links HTML
|
||||||
@@ -878,15 +873,15 @@ Navbar.craft = {
|
|||||||
borderRadius: link.isCta ? '6px' : '0',
|
borderRadius: link.isCta ? '6px' : '0',
|
||||||
transition: 'color 0.15s, background-color 0.15s',
|
transition: 'color 0.15s, background-color 0.15s',
|
||||||
});
|
});
|
||||||
return `<a href="${esc(link.href)}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`;
|
return `<a href="${escapeAttr(link.href)}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
// Hamburger HTML for mobile
|
// Hamburger HTML for mobile
|
||||||
const hamburgerHtml = mobile
|
const hamburgerHtml = mobile
|
||||||
? `\n <button class="navbar-hamburger" onclick="this.parentElement.querySelector('.navbar-links').classList.toggle('navbar-open')" style="display:none;background:none;border:none;cursor:pointer;padding:4px;flex-direction:column;gap:4px">
|
? `\n <button class="navbar-hamburger" onclick="this.parentElement.querySelector('.navbar-links').classList.toggle('navbar-open')" style="display:none;background:none;border:none;cursor:pointer;padding:4px;flex-direction:column;gap:4px">
|
||||||
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span>
|
<span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
|
||||||
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span>
|
<span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
|
||||||
<span style="display:block;width:24px;height:2px;background-color:${esc(textCol)}"></span>
|
<span style="display:block;width:24px;height:2px;background-color:${escapeAttr(textCol)}"></span>
|
||||||
</button>`
|
</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
@@ -915,7 +910,7 @@ Navbar.craft = {
|
|||||||
borderRadius: link.isCta ? '6px' : '0',
|
borderRadius: link.isCta ? '6px' : '0',
|
||||||
transition: 'color 0.15s, background-color 0.15s',
|
transition: 'color 0.15s, background-color 0.15s',
|
||||||
});
|
});
|
||||||
return `<a href="${esc(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${esc(link.text)}</a>`;
|
return `<a href="${escapeAttr(link.href)}" class="${cls}"${target}${linkStyle ? ` style="${linkStyle}"` : ''}>${escapeHtml(link.text)}</a>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface SearchBarProps {
|
interface SearchBarProps {
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
@@ -171,7 +172,6 @@ SearchBar.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(SearchBar as any).toHtml = (props: SearchBarProps, _childrenHtml: string) => {
|
(SearchBar as any).toHtml = (props: SearchBarProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
placeholder = 'Search...',
|
placeholder = 'Search...',
|
||||||
buttonText = 'Search',
|
buttonText = 'Search',
|
||||||
@@ -189,14 +189,14 @@ SearchBar.craft = {
|
|||||||
const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`;
|
const inputStyleStr = `width:100%;padding:12px 16px 12px 40px;font-size:15px;font-family:Inter,sans-serif;border:1px solid #d1d5db;border-radius:${showButton ? '8px 0 0 8px' : '8px'};background-color:#ffffff;color:#1f2937;outline:none;box-sizing:border-box`;
|
||||||
|
|
||||||
const btnHtml = showButton
|
const btnHtml = showButton
|
||||||
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:#3b82f6;border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px"></i>${esc(buttonText)}</button>`
|
? `<button type="submit" style="padding:12px 20px;font-size:15px;font-weight:600;font-family:Inter,sans-serif;color:#ffffff;background-color:#3b82f6;border:none;border-radius:0 8px 8px 0;cursor:pointer;white-space:nowrap;display:flex;align-items:center;gap:6px"><i class="fa fa-search" style="font-size:13px"></i>${escapeHtml(buttonText)}</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<form role="search"${formStyle ? ` style="${formStyle}"` : ''}>
|
html: `<form role="search"${formStyle ? ` style="${formStyle}"` : ''}>
|
||||||
<div style="position:relative;flex:1">
|
<div style="position:relative;flex:1">
|
||||||
<i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:14px;pointer-events:none"></i>
|
<i class="fa fa-search" style="position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#9ca3af;font-size:14px;pointer-events:none"></i>
|
||||||
<input type="search" placeholder="${esc(placeholder)}" style="${inputStyleStr}" />
|
<input type="search" placeholder="${escapeAttr(placeholder)}" style="${inputStyleStr}" />
|
||||||
</div>
|
</div>
|
||||||
${btnHtml}
|
${btnHtml}
|
||||||
</form>`,
|
</form>`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { relayFormWiring } from '../../utils/form-relay-wiring';
|
import { relayFormWiring } from '../../utils/form-relay-wiring';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface ContactFormField {
|
interface ContactFormField {
|
||||||
type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
|
type: 'text' | 'email' | 'tel' | 'textarea' | 'select';
|
||||||
@@ -394,7 +395,6 @@ ContactForm.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string) => {
|
(ContactForm as any).toHtml = (props: ContactFormProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const formStyle = cssPropsToString({
|
const formStyle = cssPropsToString({
|
||||||
padding: '32px',
|
padding: '32px',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -409,16 +409,16 @@ ContactForm.craft = {
|
|||||||
|
|
||||||
const fieldsHtml = (props.fields || defaultFields).map((field) => {
|
const fieldsHtml = (props.fields || defaultFields).map((field) => {
|
||||||
const reqStar = field.required ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
|
const reqStar = field.required ? '<span style="color:#ef4444;margin-left:2px">*</span>' : '';
|
||||||
const labelHtml = `<label style="font-size:14px;font-weight:500;color:${labelColor}">${esc(field.label)}${reqStar}</label>`;
|
const labelHtml = `<label style="font-size:14px;font-weight:500;color:${labelColor}">${escapeHtml(field.label)}${reqStar}</label>`;
|
||||||
const reqAttr = field.required ? ' required' : '';
|
const reqAttr = field.required ? ' required' : '';
|
||||||
let inputHtml = '';
|
let inputHtml = '';
|
||||||
if (field.type === 'textarea') {
|
if (field.type === 'textarea') {
|
||||||
inputHtml = `<textarea name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" rows="4" style="${inputStyleStr};resize:vertical"${reqAttr}></textarea>`;
|
inputHtml = `<textarea name="${escapeAttr(field.name)}" placeholder="${escapeAttr(field.placeholder)}" rows="4" style="${inputStyleStr};resize:vertical"${reqAttr}></textarea>`;
|
||||||
} else if (field.type === 'select') {
|
} else if (field.type === 'select') {
|
||||||
const opts = (field.options || []).map((o) => `<option value="${esc(o)}">${esc(o)}</option>`).join('');
|
const opts = (field.options || []).map((o) => `<option value="${escapeAttr(o)}">${escapeHtml(o)}</option>`).join('');
|
||||||
inputHtml = `<select name="${esc(field.name)}" style="${inputStyleStr};cursor:pointer"${reqAttr}><option value="">${esc(field.placeholder || 'Select...')}</option>${opts}</select>`;
|
inputHtml = `<select name="${escapeAttr(field.name)}" style="${inputStyleStr};cursor:pointer"${reqAttr}><option value="">${escapeHtml(field.placeholder || 'Select...')}</option>${opts}</select>`;
|
||||||
} else {
|
} else {
|
||||||
inputHtml = `<input type="${field.type}" name="${esc(field.name)}" placeholder="${esc(field.placeholder)}" style="${inputStyleStr}"${reqAttr} />`;
|
inputHtml = `<input type="${field.type}" name="${escapeAttr(field.name)}" placeholder="${escapeAttr(field.placeholder)}" style="${inputStyleStr}"${reqAttr} />`;
|
||||||
}
|
}
|
||||||
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}${inputHtml}</div>`;
|
return `<div style="display:flex;flex-direction:column;gap:6px">${labelHtml}${inputHtml}</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
@@ -440,7 +440,7 @@ ContactForm.craft = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
html: `${marker}<form action="${actionAttr}" method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
|
html: `${marker}<form action="${actionAttr}" method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
|
||||||
${honeypot ? ` ${honeypot}\n` : ''}${fieldsHtml ? ` ${fieldsHtml}\n` : ''} <button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${esc(props.submitText || 'Send Message')}</button>
|
${honeypot ? ` ${honeypot}\n` : ''}${fieldsHtml ? ` ${fieldsHtml}\n` : ''} <button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${escapeHtml(props.submitText || 'Send Message')}</button>
|
||||||
</form>`,
|
</form>`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface InputFieldProps {
|
interface InputFieldProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -165,7 +166,6 @@ InputField.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(InputField as any).toHtml = (props: InputFieldProps, _childrenHtml: string) => {
|
(InputField as any).toHtml = (props: InputFieldProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const wrapStyle = cssPropsToString({
|
const wrapStyle = cssPropsToString({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -174,12 +174,12 @@ InputField.craft = {
|
|||||||
});
|
});
|
||||||
const reqAttr = props.required ? ' required' : '';
|
const reqAttr = props.required ? ' required' : '';
|
||||||
const labelHtml = props.label
|
const labelHtml = props.label
|
||||||
? `<label style="font-size:14px;font-weight:500;color:#18181b">${esc(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
|
? `<label style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
|
||||||
: '';
|
: '';
|
||||||
return {
|
return {
|
||||||
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
|
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
|
||||||
${labelHtml}
|
${labelHtml}
|
||||||
<input type="${props.type || 'text'}" name="${esc(props.name || 'field')}" placeholder="${esc(props.placeholder || '')}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box" />
|
<input type="${props.type || 'text'}" name="${escapeAttr(props.name || 'field')}" placeholder="${escapeAttr(props.placeholder || '')}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box" />
|
||||||
</div>`,
|
</div>`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface SubscribeFormProps {
|
interface SubscribeFormProps {
|
||||||
heading?: string;
|
heading?: string;
|
||||||
@@ -249,7 +250,6 @@ SubscribeForm.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(SubscribeForm as any).toHtml = (props: SubscribeFormProps, _childrenHtml: string) => {
|
(SubscribeForm as any).toHtml = (props: SubscribeFormProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
heading = 'Subscribe to our newsletter',
|
heading = 'Subscribe to our newsletter',
|
||||||
placeholder = 'Enter your email',
|
placeholder = 'Enter your email',
|
||||||
@@ -268,7 +268,7 @@ SubscribeForm.craft = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const headingHtml = heading
|
const headingHtml = heading
|
||||||
? `<h3 style="font-size:22px;font-weight:600;color:#1f2937;margin-bottom:20px;font-family:Inter,sans-serif">${esc(heading)}</h3>`
|
? `<h3 style="font-size:22px;font-weight:600;color:#1f2937;margin-bottom:20px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h3>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const formStyle = cssPropsToString({
|
const formStyle = cssPropsToString({
|
||||||
@@ -299,8 +299,8 @@ SubscribeForm.craft = {
|
|||||||
html: `<div${wrapperStyle ? ` style="${wrapperStyle}"` : ''}>
|
html: `<div${wrapperStyle ? ` style="${wrapperStyle}"` : ''}>
|
||||||
${headingHtml}
|
${headingHtml}
|
||||||
<form method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
|
<form method="POST"${formStyle ? ` style="${formStyle}"` : ''}>
|
||||||
<input type="email" name="email" placeholder="${esc(placeholder)}" required style="${inputStyleStr}" />
|
<input type="email" name="email" placeholder="${escapeAttr(placeholder)}" required style="${inputStyleStr}" />
|
||||||
<button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${esc(buttonText)}</button>
|
<button type="submit"${btnStyle ? ` style="${btnStyle}"` : ''}>${escapeHtml(buttonText)}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>`,
|
</div>`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface TextareaFieldProps {
|
interface TextareaFieldProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
@@ -167,7 +168,6 @@ TextareaField.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(TextareaField as any).toHtml = (props: TextareaFieldProps, _childrenHtml: string) => {
|
(TextareaField as any).toHtml = (props: TextareaFieldProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const wrapStyle = cssPropsToString({
|
const wrapStyle = cssPropsToString({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -176,12 +176,12 @@ TextareaField.craft = {
|
|||||||
});
|
});
|
||||||
const reqAttr = props.required ? ' required' : '';
|
const reqAttr = props.required ? ' required' : '';
|
||||||
const labelHtml = props.label
|
const labelHtml = props.label
|
||||||
? `<label style="font-size:14px;font-weight:500;color:#18181b">${esc(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
|
? `<label style="font-size:14px;font-weight:500;color:#18181b">${escapeHtml(props.label)}${props.required ? '<span style="color:#ef4444"> *</span>' : ''}</label>`
|
||||||
: '';
|
: '';
|
||||||
return {
|
return {
|
||||||
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
|
html: `<div${wrapStyle ? ` style="${wrapStyle}"` : ''}>
|
||||||
${labelHtml}
|
${labelHtml}
|
||||||
<textarea name="${esc(props.name || 'message')}" placeholder="${esc(props.placeholder || '')}" rows="${props.rows || 4}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea>
|
<textarea name="${escapeAttr(props.name || 'message')}" placeholder="${escapeAttr(props.placeholder || '')}" rows="${props.rows || 4}"${reqAttr} style="padding:10px 12px;border:1px solid #d4d4d8;border-radius:6px;font-size:14px;color:#18181b;background-color:#ffffff;width:100%;box-sizing:border-box;resize:vertical;font-family:inherit"></textarea>
|
||||||
</div>`,
|
</div>`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
|
|||||||
import { Container } from './Container';
|
import { Container } from './Container';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface BackgroundSectionProps {
|
interface BackgroundSectionProps {
|
||||||
bgImage?: string;
|
bgImage?: string;
|
||||||
@@ -182,7 +183,6 @@ BackgroundSection.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(BackgroundSection as any).toHtml = (props: BackgroundSectionProps, childrenHtml: string) => {
|
(BackgroundSection as any).toHtml = (props: BackgroundSectionProps, childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const outerStyle = cssPropsToString({
|
const outerStyle = cssPropsToString({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -207,7 +207,7 @@ BackgroundSection.craft = {
|
|||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
padding: '60px 20px',
|
padding: '60px 20px',
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
return {
|
return {
|
||||||
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}><div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></section>`,
|
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}><div${overlayStyle ? ` style="${overlayStyle}"` : ''}></div><div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div></section>`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
|
|||||||
import { Container } from './Container';
|
import { Container } from './Container';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
type SplitOption =
|
type SplitOption =
|
||||||
| '100'
|
| '100'
|
||||||
@@ -290,7 +291,6 @@ ColumnLayout.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
(ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const gap = props.gap || '16px';
|
const gap = props.gap || '16px';
|
||||||
const outerStyle = cssPropsToString({
|
const outerStyle = cssPropsToString({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@@ -299,7 +299,7 @@ ColumnLayout.craft = {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
return {
|
return {
|
||||||
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
html: `<div${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${childrenHtml}</div>`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { SettingsTabs } from '../../ui/SettingsTabs';
|
|||||||
import { BorderControl } from '../../ui/BorderControl';
|
import { BorderControl } from '../../ui/BorderControl';
|
||||||
import { AdvancedTab } from '../../ui/AdvancedTab';
|
import { AdvancedTab } from '../../ui/AdvancedTab';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface ContainerProps {
|
interface ContainerProps {
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
@@ -324,7 +325,6 @@ Container.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => {
|
(Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const tag = props.tag || 'div';
|
const tag = props.tag || 'div';
|
||||||
const isBoxed = props.contentWidth === 'boxed';
|
const isBoxed = props.contentWidth === 'boxed';
|
||||||
const flexStyles = flexAlignFromTextAlign(props.style?.textAlign);
|
const flexStyles = flexAlignFromTextAlign(props.style?.textAlign);
|
||||||
@@ -340,7 +340,7 @@ Container.craft = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styleStr = cssPropsToString(outerCss);
|
const styleStr = cssPropsToString(outerCss);
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
if (isBoxed) {
|
if (isBoxed) {
|
||||||
const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles });
|
const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles });
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, Element, UserComponent } from '@craftjs/core';
|
|||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { Container } from './Container';
|
import { Container } from './Container';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
/* ---------- Shape Divider SVG Paths ---------- */
|
/* ---------- Shape Divider SVG Paths ---------- */
|
||||||
|
|
||||||
@@ -383,7 +384,6 @@ function buildDividerHtml(
|
|||||||
}
|
}
|
||||||
|
|
||||||
(Section as any).toHtml = (props: SectionProps, childrenHtml: string) => {
|
(Section as any).toHtml = (props: SectionProps, childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const hasTopDivider = props.topDivider && props.topDivider !== 'none';
|
const hasTopDivider = props.topDivider && props.topDivider !== 'none';
|
||||||
const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none';
|
const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none';
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ function buildDividerHtml(
|
|||||||
|
|
||||||
const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top');
|
const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top');
|
||||||
const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom');
|
const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom');
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`,
|
html: `<section${idAttr}${outerStyle ? ` style="${outerStyle}"` : ''}>${topHtml}<div${innerStyle ? ` style="${innerStyle}"` : ''}>${childrenHtml}</div>${bottomHtml}</section>`,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface AccordionItem {
|
interface AccordionItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -299,12 +300,11 @@ Accordion.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => {
|
(Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
padding: '60px 20px',
|
padding: '60px 20px',
|
||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
const headerBg = props.headerBg || '#f8fafc';
|
const headerBg = props.headerBg || '#f8fafc';
|
||||||
const headerColor = props.headerColor || '#18181b';
|
const headerColor = props.headerColor || '#18181b';
|
||||||
const contentBg = props.contentBg || '#ffffff';
|
const contentBg = props.contentBg || '#ffffff';
|
||||||
@@ -318,10 +318,10 @@ Accordion.craft = {
|
|||||||
const borderBottom = i === items.length - 1 ? `border:1px solid ${borderColor};` : `border:1px solid ${borderColor};border-bottom:none;`;
|
const borderBottom = i === items.length - 1 ? `border:1px solid ${borderColor};` : `border:1px solid ${borderColor};border-bottom:none;`;
|
||||||
return `<details${openAttr} style="${borderBottom}${topRadius}${bottomRadius}">
|
return `<details${openAttr} style="${borderBottom}${topRadius}${bottomRadius}">
|
||||||
<summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center">
|
<summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center">
|
||||||
${esc(item.title)}
|
${escapeHtml(item.title)}
|
||||||
</summary>
|
</summary>
|
||||||
<div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
|
<div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
|
||||||
${esc(item.content)}
|
${escapeHtml(item.content)}
|
||||||
</div>
|
</div>
|
||||||
</details>`;
|
</details>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
|||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface CTASectionProps {
|
interface CTASectionProps {
|
||||||
heading?: string;
|
heading?: string;
|
||||||
@@ -169,7 +170,6 @@ CTASection.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => {
|
(CTASection as any).toHtml = (props: CTASectionProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
background: props.gradient || defaultGradient,
|
background: props.gradient || defaultGradient,
|
||||||
padding: '80px 20px',
|
padding: '80px 20px',
|
||||||
@@ -178,12 +178,12 @@ CTASection.craft = {
|
|||||||
});
|
});
|
||||||
const ctas = normalizeCtas(props);
|
const ctas = normalizeCtas(props);
|
||||||
const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' });
|
const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' });
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
return {
|
return {
|
||||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||||
<div style="max-width:700px;margin:0 auto">
|
<div style="max-width:700px;margin:0 auto">
|
||||||
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${esc(props.heading || '')}</h2>
|
<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
|
||||||
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p>
|
<p style="font-size:18px;color:rgba(255,255,255,0.85);margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
|
||||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`,
|
</section>`,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
|||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface CallToActionProps {
|
interface CallToActionProps {
|
||||||
heading?: string;
|
heading?: string;
|
||||||
@@ -355,8 +356,6 @@ CallToAction.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
|
(CallToAction as any).toHtml = (props: CallToActionProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
|
|
||||||
const bgType = props.bgType || 'gradient';
|
const bgType = props.bgType || 'gradient';
|
||||||
const bgValue = props.bgValue || defaultGradient;
|
const bgValue = props.bgValue || defaultGradient;
|
||||||
const textColor = props.textColor || '#ffffff';
|
const textColor = props.textColor || '#ffffff';
|
||||||
@@ -397,13 +396,13 @@ CallToAction.craft = {
|
|||||||
|
|
||||||
const ctas = normalizeCtas(props);
|
const ctas = normalizeCtas(props);
|
||||||
const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor });
|
const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor });
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||||
${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1">
|
${overlayHtml}<div style="max-width:700px;margin:0 auto;position:relative;z-index:1">
|
||||||
<h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${esc(props.heading || '')}</h2>
|
<h2 style="font-size:36px;font-weight:700;color:${textColor};margin-bottom:12px">${escapeHtml(props.heading || '')}</h2>
|
||||||
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${esc(props.description || '')}</p>
|
<p style="font-size:18px;color:${textColor};opacity:0.85;margin-bottom:28px;line-height:1.6">${escapeHtml(props.description || '')}</p>
|
||||||
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap">${buttonsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`,
|
</section>`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
|
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface Slide {
|
interface Slide {
|
||||||
type: 'image' | 'content';
|
type: 'image' | 'content';
|
||||||
@@ -443,7 +444,6 @@ ContentSlider.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
|
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
slides = defaultSlides,
|
slides = defaultSlides,
|
||||||
autoplay = true,
|
autoplay = true,
|
||||||
@@ -468,20 +468,20 @@ ContentSlider.craft = {
|
|||||||
const slidesHtml = items.map((slide, i) => {
|
const slidesHtml = items.map((slide, i) => {
|
||||||
const hasBgImage = slide.imageSrc;
|
const hasBgImage = slide.imageSrc;
|
||||||
const bgStyle = hasBgImage
|
const bgStyle = hasBgImage
|
||||||
? `background-image:url(${esc(slide.imageSrc!)});background-size:cover;background-position:center`
|
? `background-image:url(${escapeAttr(slide.imageSrc!)});background-size:cover;background-position:center`
|
||||||
: slide.bgColor?.startsWith('linear-gradient')
|
: slide.bgColor?.startsWith('linear-gradient')
|
||||||
? `background-image:${slide.bgColor}`
|
? `background-image:${slide.bgColor}`
|
||||||
: `background-color:${slide.bgColor || '#3b82f6'}`;
|
: `background-color:${slide.bgColor || '#3b82f6'}`;
|
||||||
|
|
||||||
const contentParts: string[] = [];
|
const contentParts: string[] = [];
|
||||||
if (slide.heading) {
|
if (slide.heading) {
|
||||||
contentParts.push(`<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px;font-family:Inter,sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.3)">${esc(slide.heading)}</h2>`);
|
contentParts.push(`<h2 style="font-size:36px;font-weight:700;color:#ffffff;margin-bottom:12px;font-family:Inter,sans-serif;text-shadow:0 2px 8px rgba(0,0,0,0.3)">${escapeHtml(slide.heading)}</h2>`);
|
||||||
}
|
}
|
||||||
if (slide.text) {
|
if (slide.text) {
|
||||||
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${esc(slide.text)}</p>`);
|
contentParts.push(`<p style="font-size:18px;color:rgba(255,255,255,0.9);margin-bottom:20px;font-family:Inter,sans-serif;text-shadow:0 1px 4px rgba(0,0,0,0.3)">${escapeHtml(slide.text)}</p>`);
|
||||||
}
|
}
|
||||||
if (slide.buttonText) {
|
if (slide.buttonText) {
|
||||||
contentParts.push(`<a href="${esc(slide.buttonHref || '#')}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${esc(slide.buttonText)}</a>`);
|
contentParts.push(`<a href="${escapeAttr(slide.buttonHref || '#')}" style="display:inline-block;padding:12px 28px;background:#ffffff;color:#18181b;text-decoration:none;border-radius:8px;font-weight:600;font-size:15px;font-family:Inter,sans-serif">${escapeHtml(slide.buttonText)}</a>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const innerHtml = contentParts.length > 0
|
const innerHtml = contentParts.length > 0
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useEffect, useState, useCallback } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface CountdownProps {
|
interface CountdownProps {
|
||||||
targetDate?: string;
|
targetDate?: string;
|
||||||
@@ -255,7 +256,6 @@ Countdown.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => {
|
(Countdown as any).toHtml = (props: CountdownProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
targetDate = DEFAULT_TARGET,
|
targetDate = DEFAULT_TARGET,
|
||||||
heading = 'Coming Soon',
|
heading = 'Coming Soon',
|
||||||
@@ -271,10 +271,10 @@ Countdown.craft = {
|
|||||||
backgroundColor: bgColor,
|
backgroundColor: bgColor,
|
||||||
...style,
|
...style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
const headingHtml = heading
|
const headingHtml = heading
|
||||||
? `<h2 style="font-size:32px;font-weight:700;color:${digitColor};margin-bottom:32px;font-family:Inter,sans-serif">${esc(heading)}</h2>`
|
? `<h2 style="font-size:32px;font-weight:700;color:${digitColor};margin-bottom:32px;font-family:Inter,sans-serif">${escapeHtml(heading)}</h2>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
const boxStyle = 'display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px';
|
const boxStyle = 'display:flex;flex-direction:column;align-items:center;gap:4px;min-width:80px';
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface FeatureItem {
|
interface FeatureItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -278,23 +279,22 @@ FeaturesGrid.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => {
|
(FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
padding: '80px 20px',
|
padding: '80px 20px',
|
||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
const cards = (props.features || defaultFeatures).map((feat) => {
|
const cards = (props.features || defaultFeatures).map((feat) => {
|
||||||
const media = feat.image
|
const media = feat.image
|
||||||
? `<img src="${esc(feat.image)}" alt="${esc(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
|
? `<img src="${escapeAttr(feat.image)}" alt="${escapeAttr(feat.imageAlt || feat.title || '')}" style="max-width:100%;height:auto;margin-bottom:16px;border-radius:8px">`
|
||||||
: `<div style="font-size:36px;margin-bottom:16px">${esc(feat.icon)}</div>`;
|
: `<div style="font-size:36px;margin-bottom:16px">${escapeHtml(feat.icon)}</div>`;
|
||||||
const button = feat.buttonText
|
const button = feat.buttonText
|
||||||
? `\n <a href="${esc(feat.buttonUrl || '#')}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${esc(feat.buttonText)}</a>`
|
? `\n <a href="${escapeAttr(feat.buttonUrl || '#')}" style="display:inline-block;margin-top:16px;padding:10px 24px;background:#3b82f6;color:#fff;border-radius:8px;text-decoration:none;font-size:14px;font-weight:600">${escapeHtml(feat.buttonText)}</a>`
|
||||||
: '';
|
: '';
|
||||||
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
|
return `<div style="text-align:center;padding:32px 24px;border-radius:12px;background-color:#f8fafc;border:1px solid #e2e8f0">
|
||||||
${media}
|
${media}
|
||||||
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${esc(feat.title)}</h3>
|
<h3 style="font-size:20px;font-weight:600;color:#18181b;margin-bottom:8px">${escapeHtml(feat.title)}</h3>
|
||||||
<p style="font-size:14px;color:#64748b;line-height:1.6">${esc(feat.description)}</p>${button}
|
<p style="font-size:14px;color:#64748b;line-height:1.6">${escapeHtml(feat.description)}</p>${button}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface GalleryImage {
|
interface GalleryImage {
|
||||||
src: string;
|
src: string;
|
||||||
@@ -277,7 +278,6 @@ Gallery.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => {
|
(Gallery as any).toHtml = (props: GalleryProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
padding: '60px 20px',
|
padding: '60px 20px',
|
||||||
...props.style,
|
...props.style,
|
||||||
@@ -291,11 +291,11 @@ Gallery.craft = {
|
|||||||
|
|
||||||
const items = images.map((img) => {
|
const items = images.map((img) => {
|
||||||
const caption = img.caption
|
const caption = img.caption
|
||||||
? `<div style="position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,0.7));color:#ffffff;font-size:12px;border-bottom-left-radius:8px;border-bottom-right-radius:8px">${esc(img.caption)}</div>`
|
? `<div style="position:absolute;bottom:0;left:0;right:0;padding:8px 12px;background:linear-gradient(transparent,rgba(0,0,0,0.7));color:#ffffff;font-size:12px;border-bottom-left-radius:8px;border-bottom-right-radius:8px">${escapeHtml(img.caption)}</div>`
|
||||||
: '';
|
: '';
|
||||||
const clickAttr = lightbox ? ` onclick="${galleryId}_open('${esc(img.src)}')" style="cursor:pointer;position:relative;overflow:hidden;border-radius:8px"` : ' style="position:relative;overflow:hidden;border-radius:8px"';
|
const clickAttr = lightbox ? ` onclick="${galleryId}_open('${escapeAttr(img.src)}')" style="cursor:pointer;position:relative;overflow:hidden;border-radius:8px"` : ' style="position:relative;overflow:hidden;border-radius:8px"';
|
||||||
return `<div${clickAttr}>
|
return `<div${clickAttr}>
|
||||||
<img src="${esc(img.src)}" alt="${esc(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
|
<img src="${escapeAttr(img.src)}" alt="${escapeAttr(img.alt)}" style="width:100%;height:200px;object-fit:cover;display:block;border-radius:8px;background-color:#f1f5f9" />
|
||||||
${caption}
|
${caption}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useNode, UserComponent } from '@craftjs/core';
|
|||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface HeroProps {
|
interface HeroProps {
|
||||||
heading?: string;
|
heading?: string;
|
||||||
@@ -412,7 +413,6 @@ HeroSimple.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => {
|
(HeroSimple as any).toHtml = (props: HeroProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
const bg = buildBackground(props);
|
const bg = buildBackground(props);
|
||||||
const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
|
const justifyMap: Record<string, string> = { top: 'flex-start', center: 'center', bottom: 'flex-end' };
|
||||||
|
|
||||||
@@ -451,13 +451,13 @@ HeroSimple.craft = {
|
|||||||
outlineText: props.textColor || '#fff',
|
outlineText: props.textColor || '#fff',
|
||||||
});
|
});
|
||||||
|
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
return {
|
return {
|
||||||
html: `<section${idAttr} style="${sectionStyle}">
|
html: `<section${idAttr} style="${sectionStyle}">
|
||||||
${videoHtml}${overlayHtml}
|
${videoHtml}${overlayHtml}
|
||||||
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}">
|
<div style="max-width:800px;width:100%;position:relative;z-index:2;text-align:${textAlign}">
|
||||||
<h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${esc(props.heading || '')}</h1>
|
<h1 style="font-size:48px;font-weight:700;color:${props.textColor || '#fff'};margin-bottom:16px;line-height:1.2">${escapeHtml(props.heading || '')}</h1>
|
||||||
<p style="font-size:20px;color:${props.textColor || '#fff'};opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">${esc(props.subtitle || '')}</p>
|
<p style="font-size:20px;color:${props.textColor || '#fff'};opacity:0.85;margin-bottom:32px;line-height:1.6;white-space:pre-line">${escapeHtml(props.subtitle || '')}</p>
|
||||||
<div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div>
|
<div style="display:flex;gap:12px;justify-content:${justifyBtn};flex-wrap:wrap">${buttonsHtml}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`,
|
</section>`,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface Counter {
|
interface Counter {
|
||||||
number: number;
|
number: number;
|
||||||
@@ -305,7 +306,6 @@ NumberCounter.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string) => {
|
(NumberCounter as any).toHtml = (props: NumberCounterProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
counters = defaultCounters,
|
counters = defaultCounters,
|
||||||
columns = 4,
|
columns = 4,
|
||||||
@@ -326,8 +326,8 @@ NumberCounter.craft = {
|
|||||||
|
|
||||||
const countersHtml = items.map((counter, i) => {
|
const countersHtml = items.map((counter, i) => {
|
||||||
return `<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
return `<div style="display:flex;flex-direction:column;align-items:center;gap:8px">
|
||||||
<span id="${uid}_n${i}" data-target="${counter.number}" data-suffix="${esc(counter.suffix)}" style="font-size:${numberSize};font-weight:700;color:${numberColor};line-height:1.1;font-family:Inter,sans-serif">0${esc(counter.suffix)}</span>
|
<span id="${uid}_n${i}" data-target="${counter.number}" data-suffix="${escapeAttr(counter.suffix)}" style="font-size:${numberSize};font-weight:700;color:${numberColor};line-height:1.1;font-family:Inter,sans-serif">0${escapeHtml(counter.suffix)}</span>
|
||||||
<span style="font-size:15px;color:${labelColor};font-family:Inter,sans-serif;font-weight:500">${esc(counter.label)}</span>
|
<span style="font-size:15px;color:${labelColor};font-family:Inter,sans-serif;font-weight:500">${escapeHtml(counter.label)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface PricingPlan {
|
interface PricingPlan {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -404,13 +405,12 @@ PricingTable.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => {
|
(PricingTable as any).toHtml = (props: PricingTableProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const bulletType = props.bulletType || 'check';
|
const bulletType = props.bulletType || 'check';
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
padding: '80px 20px',
|
padding: '80px 20px',
|
||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
const plans = props.plans || defaultPlans;
|
const plans = props.plans || defaultPlans;
|
||||||
const featuredBg = props.featuredBg || '#3b82f6';
|
const featuredBg = props.featuredBg || '#3b82f6';
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ PricingTable.craft = {
|
|||||||
const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);';
|
const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);';
|
||||||
|
|
||||||
const featuresHtml = (Array.isArray(plan.features) ? plan.features : []).map((f) =>
|
const featuresHtml = (Array.isArray(plan.features) ? plan.features : []).map((f) =>
|
||||||
`<li style="font-size:14px;color:${featColor};display:flex;align-items:center;gap:8px"><span style="color:${checkColor};font-weight:700">${bulletChars[bulletType] || '✓'}</span>${esc(f)}</li>`
|
`<li style="font-size:14px;color:${featColor};display:flex;align-items:center;gap:8px"><span style="color:${checkColor};font-weight:700">${bulletChars[bulletType] || '✓'}</span>${escapeHtml(f)}</li>`
|
||||||
).join('\n ');
|
).join('\n ');
|
||||||
|
|
||||||
const badge = plan.isFeatured
|
const badge = plan.isFeatured
|
||||||
@@ -436,15 +436,15 @@ PricingTable.craft = {
|
|||||||
|
|
||||||
return `<div style="flex:1 1 280px;max-width:360px;background-color:${cardBg};${cardBorder}border-radius:16px;padding:40px 32px;display:flex;flex-direction:column;align-items:center;text-align:center;position:relative;${scale}${shadow}">
|
return `<div style="flex:1 1 280px;max-width:360px;background-color:${cardBg};${cardBorder}border-radius:16px;padding:40px 32px;display:flex;flex-direction:column;align-items:center;text-align:center;position:relative;${scale}${shadow}">
|
||||||
${badge}
|
${badge}
|
||||||
<h3 style="font-size:20px;font-weight:600;color:${textColor};margin-bottom:8px;${plan.isFeatured ? 'margin-top:8px;' : ''}">${esc(plan.name)}</h3>
|
<h3 style="font-size:20px;font-weight:600;color:${textColor};margin-bottom:8px;${plan.isFeatured ? 'margin-top:8px;' : ''}">${escapeHtml(plan.name)}</h3>
|
||||||
<div style="margin-bottom:24px">
|
<div style="margin-bottom:24px">
|
||||||
<span style="font-size:48px;font-weight:700;color:${textColor};line-height:1">${esc(plan.price)}</span>
|
<span style="font-size:48px;font-weight:700;color:${textColor};line-height:1">${escapeHtml(plan.price)}</span>
|
||||||
<span style="font-size:16px;color:${subColor}">${esc(plan.period)}</span>
|
<span style="font-size:16px;color:${subColor}">${escapeHtml(plan.period)}</span>
|
||||||
</div>
|
</div>
|
||||||
<ul style="list-style:none;padding:0;margin:0 0 32px 0;width:100%;display:flex;flex-direction:column;gap:12px">
|
<ul style="list-style:none;padding:0;margin:0 0 32px 0;width:100%;display:flex;flex-direction:column;gap:12px">
|
||||||
${featuresHtml}
|
${featuresHtml}
|
||||||
</ul>
|
</ul>
|
||||||
<a href="${plan.buttonHref || '#'}" style="margin-top:auto;display:inline-block;padding:14px 32px;background-color:${btnBg};color:${btnColor};text-decoration:none;border-radius:8px;font-weight:600;font-size:14px;width:100%;text-align:center">${esc(plan.buttonText)}</a>
|
<a href="${plan.buttonHref || '#'}" style="margin-top:auto;display:inline-block;padding:14px 32px;background-color:${btnBg};color:${btnColor};text-decoration:none;border-radius:8px;font-weight:600;font-size:14px;width:100%;text-align:center">${escapeHtml(plan.buttonText)}</a>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface TabItem {
|
interface TabItem {
|
||||||
label: string;
|
label: string;
|
||||||
@@ -296,12 +297,11 @@ Tabs.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => {
|
(Tabs as any).toHtml = (props: TabsProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const sectionStyle = cssPropsToString({
|
const sectionStyle = cssPropsToString({
|
||||||
padding: '60px 20px',
|
padding: '60px 20px',
|
||||||
...props.style,
|
...props.style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
const tabs = props.tabs || defaultTabs;
|
const tabs = props.tabs || defaultTabs;
|
||||||
const activeTabBg = props.activeTabBg || '#3b82f6';
|
const activeTabBg = props.activeTabBg || '#3b82f6';
|
||||||
const activeTabColor = props.activeTabColor || '#ffffff';
|
const activeTabColor = props.activeTabColor || '#ffffff';
|
||||||
@@ -313,11 +313,11 @@ Tabs.craft = {
|
|||||||
|
|
||||||
const tabButtons = tabs.map((tab, i) => {
|
const tabButtons = tabs.map((tab, i) => {
|
||||||
const isActive = i === 0;
|
const isActive = i === 0;
|
||||||
return `<button onclick="${tabId}_switch(${i})" id="${tabId}_btn_${i}" style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color:${isActive ? activeTabBg : inactiveTabBg};color:${isActive ? activeTabColor : inactiveTabColor}">${esc(tab.label)}</button>`;
|
return `<button onclick="${tabId}_switch(${i})" id="${tabId}_btn_${i}" style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color:${isActive ? activeTabBg : inactiveTabBg};color:${isActive ? activeTabColor : inactiveTabColor}">${escapeHtml(tab.label)}</button>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
const tabPanels = tabs.map((tab, i) => {
|
const tabPanels = tabs.map((tab, i) => {
|
||||||
return `<div id="${tabId}_panel_${i}" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${esc(tab.content)}</div>`;
|
return `<div id="${tabId}_panel_${i}" style="padding:24px;background-color:${contentBg};border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px;${i !== 0 ? 'display:none' : ''}">${escapeHtml(tab.content)}</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
const switchScript = `<script>
|
const switchScript = `<script>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react';
|
|||||||
import { useNode, UserComponent } from '@craftjs/core';
|
import { useNode, UserComponent } from '@craftjs/core';
|
||||||
import { cssPropsToString } from '../../utils/style-helpers';
|
import { cssPropsToString } from '../../utils/style-helpers';
|
||||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
interface Testimonial {
|
interface Testimonial {
|
||||||
quote: string;
|
quote: string;
|
||||||
@@ -377,7 +378,6 @@ Testimonials.craft = {
|
|||||||
/* ---------- HTML export ---------- */
|
/* ---------- HTML export ---------- */
|
||||||
|
|
||||||
(Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => {
|
(Testimonials as any).toHtml = (props: TestimonialsProps, _childrenHtml: string) => {
|
||||||
const esc = (s: any) => String(s ?? "").replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
const {
|
const {
|
||||||
testimonials = defaultTestimonials,
|
testimonials = defaultTestimonials,
|
||||||
layout = 'grid',
|
layout = 'grid',
|
||||||
@@ -394,16 +394,16 @@ Testimonials.craft = {
|
|||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
...style,
|
...style,
|
||||||
});
|
});
|
||||||
const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : '';
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
||||||
|
|
||||||
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
|
const cardCss = `background-color:${cardBg};border-radius:12px;padding:32px 24px;text-align:center;border:1px solid #e2e8f0`;
|
||||||
|
|
||||||
const cards = items.map((t) => {
|
const cards = items.map((t) => {
|
||||||
return `<div style="${cardCss}">
|
return `<div style="${cardCss}">
|
||||||
${starsHtml(t.rating, starColor)}
|
${starsHtml(t.rating, starColor)}
|
||||||
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">“${esc(t.quote)}”</p>
|
<p style="font-size:15px;color:#374151;line-height:1.7;margin-bottom:16px;font-style:italic;font-family:Inter,sans-serif">“${escapeHtml(t.quote)}”</p>
|
||||||
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${esc(t.name)}</div>
|
<div style="font-weight:600;font-size:14px;color:#18181b;font-family:Inter,sans-serif">${escapeHtml(t.name)}</div>
|
||||||
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${esc(t.title)}</div>
|
<div style="font-size:13px;color:#64748b;font-family:Inter,sans-serif">${escapeHtml(t.title)}</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('\n ');
|
}).join('\n ');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||||
|
|
||||||
export type CtaVariant = 'primary' | 'outline' | 'ghost';
|
export type CtaVariant = 'primary' | 'outline' | 'ghost';
|
||||||
|
|
||||||
@@ -78,12 +79,10 @@ export function ctaCssString(cta: CtaButton, defaults: CtaStyleDefaults): string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const esc = (s: any) => String(s ?? '').replace(/</g, '<').replace(/>/g, '>');
|
|
||||||
|
|
||||||
export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string {
|
export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): string {
|
||||||
return ctas.map((c) => {
|
return ctas.map((c) => {
|
||||||
const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
|
const target = c.target === '_blank' ? ' target="_blank" rel="noopener noreferrer"' : '';
|
||||||
return `<a href="${esc(c.href || '#')}"${target} style="${ctaCssString(c, defaults)}">${esc(c.text || '')}</a>`;
|
return `<a href="${escapeAttr(c.href || '#')}"${target} style="${ctaCssString(c, defaults)}">${escapeHtml(c.text || '')}</a>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
* HTML — see PR #47 review).
|
* HTML — see PR #47 review).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const esc = (s: unknown): string =>
|
import { escapeAttr } from './escape';
|
||||||
String(s ?? '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
|
|
||||||
export interface RelayWiring {
|
export interface RelayWiring {
|
||||||
/** true when a recipient is set (relay path); false = legacy formAction fallback */
|
/** true when a recipient is set (relay path); false = legacy formAction fallback */
|
||||||
@@ -34,12 +33,12 @@ export function relayFormWiring(
|
|||||||
fallbackAction: string | undefined,
|
fallbackAction: string | undefined,
|
||||||
): RelayWiring {
|
): RelayWiring {
|
||||||
if (!recipientEmail) {
|
if (!recipientEmail) {
|
||||||
return { useRelay: false, marker: '', actionAttr: esc(fallbackAction || '#'), honeypot: '' };
|
return { useRelay: false, marker: '', actionAttr: escapeAttr(fallbackAction || '#'), honeypot: '' };
|
||||||
}
|
}
|
||||||
const fid = 'F' + Math.random().toString(36).slice(2, 8);
|
const fid = 'F' + Math.random().toString(36).slice(2, 8);
|
||||||
return {
|
return {
|
||||||
useRelay: true,
|
useRelay: true,
|
||||||
marker: `<!--WHP-FORM id="${fid}" recipient="${esc(recipientEmail)}" thankyou="${esc(thankYouUrl || '')}"-->`,
|
marker: `<!--WHP-FORM id="${fid}" recipient="${escapeAttr(recipientEmail)}" thankyou="${escapeAttr(thankYouUrl || '')}"-->`,
|
||||||
actionAttr: `__WHP_FORM_ACTION__${fid}__`,
|
actionAttr: `__WHP_FORM_ACTION__${fid}__`,
|
||||||
honeypot: `<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">`,
|
honeypot: `<input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="position:absolute;left:-9999px" aria-hidden="true">`,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user