refactor(builder): remove dead component settings UI
Each component defined a .craft.related.settings panel that was never rendered -- the right panel renders only GuidedStyles (per-type *StylePanel components), never .related.settings. Removed all dead settings components across every component, their settings-only helpers (including the dead uploadToWhp/showBrowser/handleBrowse asset-browse blocks in Logo/Navbar/VideoBlock/FeaturesGrid, and CtasEditor in _cta-helpers), and dropped the now-empty related keys. Render output, .craft props/rules, and toHtml statics are unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface AccordionItem {
|
||||
@@ -124,156 +123,6 @@ export const Accordion: UserComponent<AccordionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const AccordionSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as AccordionProps,
|
||||
}));
|
||||
|
||||
const items = props.items || defaultItems;
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
|
||||
const updateItem = (index: number, field: keyof AccordionItem, value: string | boolean) => {
|
||||
setProp((p: AccordionProps) => {
|
||||
const updated = [...(p.items || defaultItems)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.items = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addItem = () => {
|
||||
setProp((p: AccordionProps) => {
|
||||
p.items = [...(p.items || defaultItems), { title: 'New Question', content: 'Answer goes here.', isOpen: false }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeItem = (index: number) => {
|
||||
setProp((p: AccordionProps) => {
|
||||
const updated = [...(p.items || defaultItems)];
|
||||
updated.splice(index, 1);
|
||||
p.items = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const colorSwatches = ['#f8fafc', '#f1f5f9', '#e2e8f0', '#ffffff', '#18181b', '#1e293b', '#3b82f6', '#8b5cf6'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={labelStyle}>Header Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{colorSwatches.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: AccordionProps) => { p.headerBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.headerBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Header Text Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#18181b', '#1f2937', '#374151', '#ffffff', '#e2e8f0', '#3b82f6'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: AccordionProps) => { p.headerColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.headerColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Content Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#1e293b'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: AccordionProps) => { p.contentBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.contentBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Border Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#e2e8f0', '#cbd5e1', '#d1d5db', '#3f3f46', '#52525b'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: AccordionProps) => { p.borderColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.borderColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Items</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{items.map((item, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={item.title} onChange={(e) => updateItem(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeItem(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={item.content}
|
||||
onChange={(e) => updateItem(i, 'content', e.target.value)}
|
||||
placeholder="Content"
|
||||
rows={2}
|
||||
style={{ ...inputStyle, resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addItem}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
Accordion.craft = {
|
||||
@@ -292,9 +141,6 @@ Accordion.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: AccordionSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { CtaButton, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface CTASectionProps {
|
||||
@@ -71,78 +70,6 @@ export const CTASection: UserComponent<CTASectionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const CTASectionSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as CTASectionProps,
|
||||
}));
|
||||
|
||||
const gradientPresets = [
|
||||
{ label: 'Blue-Purple', value: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)' },
|
||||
{ label: 'Purple', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
{ label: 'Teal', value: 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)' },
|
||||
{ label: 'Sunset', value: 'linear-gradient(135deg, #f97316 0%, #ec4899 100%)' },
|
||||
{ label: 'Dark', value: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)' },
|
||||
{ label: 'Ocean', value: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)' },
|
||||
];
|
||||
|
||||
const effectiveCtas = normalizeCtas(props);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.heading || ''}
|
||||
onChange={(e) => setProp((p: CTASectionProps) => { p.heading = e.target.value; })}
|
||||
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Description</label>
|
||||
<textarea
|
||||
value={props.description || ''}
|
||||
onChange={(e) => setProp((p: CTASectionProps) => { p.description = e.target.value; })}
|
||||
rows={2}
|
||||
style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12, resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CtasEditor
|
||||
ctas={effectiveCtas}
|
||||
onChange={(next) => setProp((p: CTASectionProps) => {
|
||||
p.ctas = next;
|
||||
p.buttonText = undefined;
|
||||
p.buttonHref = undefined;
|
||||
})}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Gradient</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{gradientPresets.map((g) => (
|
||||
<button
|
||||
key={g.label}
|
||||
onClick={() => setProp((p: CTASectionProps) => { p.gradient = g.value; })}
|
||||
title={g.label}
|
||||
style={{
|
||||
width: 32, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
background: g.value, cursor: 'pointer',
|
||||
outline: props.gradient === g.value ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
CTASection.craft = {
|
||||
@@ -162,9 +89,6 @@ CTASection.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: CTASectionSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { CtaButton, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface CallToActionProps {
|
||||
@@ -113,216 +112,6 @@ export const CallToAction: UserComponent<CallToActionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const CallToActionSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as CallToActionProps,
|
||||
}));
|
||||
|
||||
const gradientPresets = [
|
||||
{ label: 'Blue-Purple', value: 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)' },
|
||||
{ label: 'Purple', value: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
|
||||
{ label: 'Teal', value: 'linear-gradient(135deg, #0d9488 0%, #0f766e 100%)' },
|
||||
{ label: 'Sunset', value: 'linear-gradient(135deg, #f97316 0%, #ec4899 100%)' },
|
||||
{ label: 'Dark', value: 'linear-gradient(135deg, #1e293b 0%, #0f172a 100%)' },
|
||||
{ label: 'Ocean', value: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)' },
|
||||
];
|
||||
|
||||
const colorPresets = ['#2563eb', '#7c3aed', '#0d9488', '#18181b', '#0f172a', '#1e293b', '#dc2626', '#f97316'];
|
||||
const buttonColorPresets = ['#ffffff', '#18181b', '#3b82f6', '#10b981', '#ef4444', '#8b5cf6', '#f59e0b', '#ec4899'];
|
||||
const textColorPresets = ['#ffffff', '#f8fafc', '#e2e8f0', '#18181b', '#1e293b', '#fef3c7'];
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
|
||||
};
|
||||
|
||||
const effectiveCtas = normalizeCtas(props);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.heading || ''}
|
||||
onChange={(e) => setProp((p: CallToActionProps) => { p.heading = e.target.value; })}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Description</label>
|
||||
<textarea
|
||||
value={props.description || ''}
|
||||
onChange={(e) => setProp((p: CallToActionProps) => { p.description = e.target.value; })}
|
||||
rows={2}
|
||||
style={{ ...inputStyle, resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CtasEditor
|
||||
ctas={effectiveCtas}
|
||||
onChange={(next) => setProp((p: CallToActionProps) => {
|
||||
p.ctas = next;
|
||||
p.buttonText = undefined;
|
||||
p.buttonHref = undefined;
|
||||
p.secondaryButtonText = undefined;
|
||||
p.secondaryButtonHref = undefined;
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Background Type */}
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background Type</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['color', 'gradient', 'image'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => {
|
||||
setProp((p: CallToActionProps) => {
|
||||
p.bgType = t;
|
||||
if (t === 'color') p.bgValue = '#2563eb';
|
||||
if (t === 'gradient') p.bgValue = defaultGradient;
|
||||
if (t === 'image') p.bgValue = '';
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.bgType === t ? '#3b82f6' : '#27272a',
|
||||
color: '#e4e4e7',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background sub-controls */}
|
||||
{props.bgType === 'color' && (
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{colorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CallToActionProps) => { p.bgValue = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.bgValue === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.bgType === 'gradient' && (
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Gradient</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{gradientPresets.map((g) => (
|
||||
<button
|
||||
key={g.label}
|
||||
onClick={() => setProp((p: CallToActionProps) => { p.bgValue = g.value; })}
|
||||
title={g.label}
|
||||
style={{
|
||||
width: 32, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
background: g.value, cursor: 'pointer',
|
||||
outline: props.bgValue === g.value ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.bgType === 'image' && (
|
||||
<>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Image URL</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.bgValue || ''}
|
||||
onChange={(e) => setProp((p: CallToActionProps) => { p.bgValue = e.target.value; })}
|
||||
placeholder="https://..."
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>
|
||||
Overlay Opacity: {props.overlayOpacity ?? 0}%
|
||||
</label>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={props.overlayOpacity ?? 0}
|
||||
onChange={(e) => setProp((p: CallToActionProps) => { p.overlayOpacity = parseInt(e.target.value); })}
|
||||
style={{ width: '100%', accentColor: '#3b82f6' }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Overlay Color</label>
|
||||
<input
|
||||
type="color"
|
||||
value={props.overlayColor || '#000000'}
|
||||
onChange={(e) => setProp((p: CallToActionProps) => { p.overlayColor = e.target.value; })}
|
||||
style={{ width: 32, height: 24, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer', background: 'none', padding: 0 }}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Text Color */}
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Text Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{textColorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CallToActionProps) => { p.textColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.textColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Button Color */}
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Button Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{buttonColorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CallToActionProps) => { p.buttonColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.buttonColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
CallToAction.craft = {
|
||||
@@ -348,9 +137,6 @@ CallToAction.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: CallToActionSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -216,208 +216,6 @@ export const ContentSlider: UserComponent<ContentSliderProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const ContentSliderSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as ContentSliderProps,
|
||||
}));
|
||||
|
||||
const items = props.slides || defaultSlides;
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const heightPresets = ['300px', '400px', '500px', '600px', '80vh'];
|
||||
const intervalPresets = [3000, 4000, 5000, 7000, 10000];
|
||||
const bgPresets = [
|
||||
'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
|
||||
'linear-gradient(135deg, #10b981 0%, #059669 100%)',
|
||||
'linear-gradient(135deg, #f59e0b 0%, #ef4444 100%)',
|
||||
'linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%)',
|
||||
'#18181b',
|
||||
'#0f172a',
|
||||
'#1e293b',
|
||||
'#3b82f6',
|
||||
];
|
||||
|
||||
const updateSlide = (index: number, field: keyof Slide, value: string) => {
|
||||
setProp((p: ContentSliderProps) => {
|
||||
const updated = [...(p.slides || defaultSlides)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.slides = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addSlide = () => {
|
||||
setProp((p: ContentSliderProps) => {
|
||||
const current = p.slides || defaultSlides;
|
||||
p.slides = [...current, {
|
||||
type: 'image',
|
||||
imageSrc: '',
|
||||
heading: 'New Slide',
|
||||
text: 'Add your content here',
|
||||
buttonText: '',
|
||||
buttonHref: '#',
|
||||
bgColor: 'linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%)',
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
const removeSlide = (index: number) => {
|
||||
setProp((p: ContentSliderProps) => {
|
||||
const updated = [...(p.slides || defaultSlides)];
|
||||
updated.splice(index, 1);
|
||||
p.slides = updated;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
{/* Height */}
|
||||
<div>
|
||||
<label style={labelStyle}>Height</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{heightPresets.map((h) => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setProp((p: ContentSliderProps) => { p.height = h; })}
|
||||
style={{
|
||||
padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.height === h ? '#3b82f6' : '#27272a',
|
||||
color: props.height === h ? '#fff' : '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{h}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Autoplay */}
|
||||
<div>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.autoplay !== false}
|
||||
onChange={(e) => setProp((p: ContentSliderProps) => { p.autoplay = e.target.checked; })}
|
||||
/>
|
||||
Autoplay
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Interval */}
|
||||
{props.autoplay !== false && (
|
||||
<div>
|
||||
<label style={labelStyle}>Interval (ms)</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{intervalPresets.map((ms) => (
|
||||
<button
|
||||
key={ms}
|
||||
onClick={() => setProp((p: ContentSliderProps) => { p.interval = ms; })}
|
||||
style={{
|
||||
padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: (props.interval || 5000) === ms ? '#3b82f6' : '#27272a',
|
||||
color: (props.interval || 5000) === ms ? '#fff' : '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{ms / 1000}s
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Show Arrows */}
|
||||
<div>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.showArrows !== false}
|
||||
onChange={(e) => setProp((p: ContentSliderProps) => { p.showArrows = e.target.checked; })}
|
||||
/>
|
||||
Show Arrows
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Show Dots */}
|
||||
<div>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.showDots !== false}
|
||||
onChange={(e) => setProp((p: ContentSliderProps) => { p.showDots = e.target.checked; })}
|
||||
/>
|
||||
Show Dots
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Slides */}
|
||||
<div>
|
||||
<label style={labelStyle}>Slides</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{items.map((slide, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 11, color: '#a1a1aa', flex: 'none', width: 18 }}>{i + 1}.</span>
|
||||
<select
|
||||
value={slide.type}
|
||||
onChange={(e) => updateSlide(i, 'type', e.target.value)}
|
||||
style={{ ...inputStyle, width: 70, flex: 'none', cursor: 'pointer' }}
|
||||
>
|
||||
<option value="image">Image</option>
|
||||
<option value="content">Content</option>
|
||||
</select>
|
||||
<input type="text" value={slide.heading || ''} onChange={(e) => updateSlide(i, 'heading', e.target.value)} placeholder="Heading" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeSlide(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" value={slide.imageSrc || ''} onChange={(e) => updateSlide(i, 'imageSrc', e.target.value)} placeholder="Image URL (optional)" style={inputStyle} />
|
||||
<input type="text" value={slide.text || ''} onChange={(e) => updateSlide(i, 'text', e.target.value)} placeholder="Text" style={inputStyle} />
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={slide.buttonText || ''} onChange={(e) => updateSlide(i, 'buttonText', e.target.value)} placeholder="Button text" style={{ ...inputStyle, flex: 1 }} />
|
||||
<input type="text" value={slide.buttonHref || ''} onChange={(e) => updateSlide(i, 'buttonHref', e.target.value)} placeholder="Button URL" style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
<div>
|
||||
<span style={{ fontSize: 10, color: '#a1a1aa', display: 'block', marginBottom: 2 }}>Background</span>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((bg) => (
|
||||
<button
|
||||
key={bg}
|
||||
onClick={() => updateSlide(i, 'bgColor', bg)}
|
||||
style={{
|
||||
width: 22, height: 22, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
background: bg, cursor: 'pointer',
|
||||
outline: slide.bgColor === bg ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addSlide}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Slide
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
ContentSlider.craft = {
|
||||
@@ -436,9 +234,6 @@ ContentSlider.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: ContentSliderSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties, useEffect, useState, useCallback } from 'react';
|
||||
import React, { CSSProperties, useEffect, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface CountdownProps {
|
||||
@@ -128,108 +127,6 @@ export const Countdown: UserComponent<CountdownProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const CountdownSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as CountdownProps,
|
||||
}));
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
|
||||
};
|
||||
|
||||
const colorPresets = ['#ffffff', '#f8fafc', '#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
|
||||
const bgPresets = ['#18181b', '#0f172a', '#1e293b', '#1e1b4b', '#042f2e', '#27272a', '#ffffff', '#f8fafc'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
{/* Target date */}
|
||||
<div>
|
||||
<label style={labelStyle}>Target Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={props.targetDate || DEFAULT_TARGET}
|
||||
onChange={(e) => setProp((p: CountdownProps) => { p.targetDate = e.target.value; })}
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Heading */}
|
||||
<div>
|
||||
<label style={labelStyle}>Heading</label>
|
||||
<input
|
||||
type="text"
|
||||
value={props.heading || ''}
|
||||
onChange={(e) => setProp((p: CountdownProps) => { p.heading = e.target.value; })}
|
||||
placeholder="Coming Soon"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Digit color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Digit Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{colorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CountdownProps) => { p.digitColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.digitColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Label Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{colorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CountdownProps) => { p.labelColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.labelColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: CountdownProps) => { p.bgColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.bgColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
Countdown.craft = {
|
||||
@@ -248,9 +145,6 @@ Countdown.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: CountdownSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface FeatureItem {
|
||||
@@ -15,25 +14,6 @@ interface FeatureItem {
|
||||
buttonUrl?: string;
|
||||
}
|
||||
|
||||
/* ---------- Image upload helper (same as Navbar/ImageBlock) ---------- */
|
||||
|
||||
async function uploadToWhp(file: File): Promise<string | null> {
|
||||
const cfg = (window as any).WHP_CONFIG;
|
||||
if (!cfg) return URL.createObjectURL(file);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
try {
|
||||
const resp = await fetch(`${cfg.apiUrl}?action=upload_asset&site_id=${cfg.siteId}`, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-Token': cfg.csrfToken },
|
||||
body: formData,
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.success && data.url) return data.url;
|
||||
return null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
interface FeaturesGridProps {
|
||||
features?: FeatureItem[];
|
||||
style?: CSSProperties;
|
||||
@@ -111,152 +91,6 @@ export const FeaturesGrid: UserComponent<FeaturesGridProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const FeaturesGridSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as FeaturesGridProps,
|
||||
}));
|
||||
|
||||
const features = props.features || defaultFeatures;
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const updateFeature = (index: number, field: keyof FeatureItem, value: string) => {
|
||||
setProp((p: FeaturesGridProps) => {
|
||||
const updated = [...(p.features || defaultFeatures)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.features = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const handleImageUpload = async (index: number, file: File) => {
|
||||
const url = await uploadToWhp(file);
|
||||
if (url) updateFeature(index, 'image', url);
|
||||
};
|
||||
|
||||
const addFeature = () => {
|
||||
setProp((p: FeaturesGridProps) => {
|
||||
p.features = [...(p.features || defaultFeatures), { title: 'New Feature', description: 'Describe this feature.', icon: '🔧', image: '', imageAlt: '', buttonText: '', buttonUrl: '' }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeFeature = (index: number) => {
|
||||
setProp((p: FeaturesGridProps) => {
|
||||
const updated = [...(p.features || defaultFeatures)];
|
||||
updated.splice(index, 1);
|
||||
p.features = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const bgPresets = ['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: FeaturesGridProps) => { p.style = { ...p.style, backgroundColor: c }; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.style?.backgroundColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 }}>Features</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{(Array.isArray(features) ? features : []).map((feat, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={feat.icon} onChange={(e) => updateFeature(i, 'icon', e.target.value)} placeholder="Icon" style={{ ...inputStyle, width: 40, flex: 'none', textAlign: 'center' }} />
|
||||
<input type="text" value={feat.title} onChange={(e) => updateFeature(i, 'title', e.target.value)} placeholder="Title" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeFeature(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={feat.description}
|
||||
onChange={(e) => updateFeature(i, 'description', e.target.value)}
|
||||
placeholder="Description"
|
||||
rows={2}
|
||||
style={{ ...inputStyle, resize: 'vertical' }}
|
||||
/>
|
||||
|
||||
{/* Icon / Image toggle */}
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['icon', 'image'] as const).map((mt) => {
|
||||
const active = (feat.mediaType || 'icon') === mt;
|
||||
return (
|
||||
<button
|
||||
key={mt}
|
||||
onClick={() => updateFeature(i, 'mediaType', mt)}
|
||||
style={{
|
||||
flex: 1, padding: '3px 6px', fontSize: 11, cursor: 'pointer',
|
||||
background: active ? '#3b82f6' : '#27272a',
|
||||
color: active ? '#fff' : '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
{mt === 'icon' ? 'Icon' : 'Image'}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{(feat.mediaType || 'icon') === 'image' ? (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<label
|
||||
style={{ padding: '3px 6px', fontSize: 11, cursor: 'pointer', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, flex: 'none', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
Upload
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={(e) => { const f = e.target.files?.[0]; if (f) void handleImageUpload(i, f); }}
|
||||
/>
|
||||
</label>
|
||||
<input type="text" value={feat.image || ''} onChange={(e) => updateFeature(i, 'image', e.target.value)} placeholder="Image URL" style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
<input type="text" value={feat.imageAlt || ''} onChange={(e) => updateFeature(i, 'imageAlt', e.target.value)} placeholder="Alt text (optional)" style={inputStyle} />
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Button (optional) */}
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'block', marginTop: 2 }}>Button (optional)</label>
|
||||
<input type="text" value={feat.buttonText || ''} onChange={(e) => updateFeature(i, 'buttonText', e.target.value)} placeholder="Button text" style={inputStyle} />
|
||||
<input type="text" value={feat.buttonUrl || ''} onChange={(e) => updateFeature(i, 'buttonUrl', e.target.value)} placeholder="https://... or /page" style={inputStyle} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addFeature}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Feature
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
FeaturesGrid.craft = {
|
||||
@@ -271,9 +105,6 @@ FeaturesGrid.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: FeaturesGridSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -104,156 +104,6 @@ export const Gallery: UserComponent<GalleryProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const GallerySettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as GalleryProps,
|
||||
}));
|
||||
|
||||
const images = props.images || defaultImages;
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
|
||||
const updateImage = (index: number, field: keyof GalleryImage, value: string) => {
|
||||
setProp((p: GalleryProps) => {
|
||||
const updated = [...(p.images || defaultImages)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.images = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addImage = () => {
|
||||
setProp((p: GalleryProps) => {
|
||||
const current = p.images || defaultImages;
|
||||
p.images = [...current, { src: placeholderSvg(current.length), alt: `Gallery image ${current.length + 1}`, caption: '' }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeImage = (index: number) => {
|
||||
setProp((p: GalleryProps) => {
|
||||
const updated = [...(p.images || defaultImages)];
|
||||
updated.splice(index, 1);
|
||||
p.images = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const columnOptions = [2, 3, 4, 5, 6];
|
||||
const gapOptions = ['8px', '12px', '16px', '24px', '32px'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<div>
|
||||
<label style={labelStyle}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: GalleryProps) => { p.style = { ...p.style, backgroundColor: c }; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.style?.backgroundColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Columns</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{columnOptions.map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setProp((p: GalleryProps) => { p.columns = n; })}
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.columns === n ? '#3b82f6' : '#27272a',
|
||||
color: props.columns === n ? '#fff' : '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Gap</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{gapOptions.map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
onClick={() => setProp((p: GalleryProps) => { p.gap = g; })}
|
||||
style={{
|
||||
padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.gap === g ? '#3b82f6' : '#27272a',
|
||||
color: props.gap === g ? '#fff' : '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{g}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={{ ...labelStyle, display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.lightbox || false}
|
||||
onChange={(e) => setProp((p: GalleryProps) => { p.lightbox = e.target.checked; })}
|
||||
/>
|
||||
Lightbox (click to enlarge in exported HTML)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Images</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{images.map((img, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<img
|
||||
src={img.src}
|
||||
alt=""
|
||||
style={{ width: 32, height: 32, objectFit: 'cover', borderRadius: 4, flexShrink: 0, backgroundColor: '#27272a' }}
|
||||
/>
|
||||
<input type="text" value={img.src} onChange={(e) => updateImage(i, 'src', e.target.value)} placeholder="Image URL" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeImage(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={img.alt} onChange={(e) => updateImage(i, 'alt', e.target.value)} placeholder="Alt text" style={{ ...inputStyle, flex: 1 }} />
|
||||
<input type="text" value={img.caption || ''} onChange={(e) => updateImage(i, 'caption', e.target.value)} placeholder="Caption" style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addImage}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Image
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
Gallery.craft = {
|
||||
@@ -270,9 +120,6 @@ Gallery.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: GallerySettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { CtaButton, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface HeroProps {
|
||||
@@ -163,215 +162,6 @@ export const HeroSimple: UserComponent<HeroProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const inputStyle: React.CSSProperties = {
|
||||
width: '100%', padding: '6px 8px', background: '#27272a',
|
||||
color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
|
||||
};
|
||||
const labelStyle: React.CSSProperties = {
|
||||
fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4,
|
||||
};
|
||||
const btnStyle = (active: boolean): React.CSSProperties => ({
|
||||
flex: 1, padding: '6px 4px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: active ? '#3b82f6' : '#27272a',
|
||||
color: active ? '#fff' : '#a1a1aa',
|
||||
fontWeight: active ? 600 : 400,
|
||||
});
|
||||
|
||||
const HeroSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as HeroProps,
|
||||
}));
|
||||
|
||||
const effectiveCtas = normalizeCtas(props);
|
||||
|
||||
return (
|
||||
<div style={{ padding: 12, display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||||
<AnchorIdField />
|
||||
{/* Content */}
|
||||
<div>
|
||||
<label style={labelStyle}>Heading</label>
|
||||
<input type="text" value={props.heading || ''} onChange={(e) => setProp((p: HeroProps) => { p.heading = e.target.value; })} style={inputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Subtitle</label>
|
||||
<textarea value={props.subtitle || ''} onChange={(e) => setProp((p: HeroProps) => { p.subtitle = e.target.value; })} rows={3} style={{ ...inputStyle, resize: 'vertical' as const }} />
|
||||
</div>
|
||||
|
||||
{/* Dynamic CTAs */}
|
||||
<CtasEditor
|
||||
ctas={effectiveCtas}
|
||||
onChange={(next) => setProp((p: HeroProps) => {
|
||||
p.ctas = next;
|
||||
// Once the user touches CTAs, the legacy fields are no longer
|
||||
// authoritative — clear them so the array is the only source.
|
||||
p.buttonText = undefined;
|
||||
p.buttonHref = undefined;
|
||||
p.secondaryButtonText = undefined;
|
||||
p.secondaryButtonHref = undefined;
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Background Type */}
|
||||
<div>
|
||||
<label style={labelStyle}>Background Type</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['color', 'gradient', 'image', 'video'] as const).map((t) => (
|
||||
<button key={t} onClick={() => setProp((p: HeroProps) => { p.bgType = t; })}
|
||||
style={btnStyle(props.bgType === t)}>
|
||||
{t === 'color' ? 'Color' : t === 'gradient' ? 'Gradient' : t === 'image' ? 'Image' : 'Video'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background controls based on type */}
|
||||
{props.bgType === 'color' && (
|
||||
<div>
|
||||
<label style={labelStyle}>Background Color</label>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="color" value={props.bgColor || '#1e293b'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgColor = e.target.value; })}
|
||||
style={{ width: 36, height: 30, border: 'none', cursor: 'pointer', background: 'none' }} />
|
||||
<input type="text" value={props.bgColor || '#1e293b'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgColor = e.target.value; })}
|
||||
style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.bgType === 'gradient' && (
|
||||
<>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>From</label>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<input type="color" value={props.bgGradientFrom || '#667eea'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgGradientFrom = e.target.value; })}
|
||||
style={{ width: 30, height: 26, border: 'none', cursor: 'pointer', background: 'none' }} />
|
||||
<input type="text" value={props.bgGradientFrom || '#667eea'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgGradientFrom = e.target.value; })}
|
||||
style={{ ...inputStyle, fontSize: 10 }} />
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>To</label>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<input type="color" value={props.bgGradientTo || '#764ba2'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgGradientTo = e.target.value; })}
|
||||
style={{ width: 30, height: 26, border: 'none', cursor: 'pointer', background: 'none' }} />
|
||||
<input type="text" value={props.bgGradientTo || '#764ba2'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgGradientTo = e.target.value; })}
|
||||
style={{ ...inputStyle, fontSize: 10 }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Angle: {props.bgGradientAngle || 135}°</label>
|
||||
<input type="range" min={0} max={360} value={props.bgGradientAngle || 135}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgGradientAngle = parseInt(e.target.value); })}
|
||||
style={{ width: '100%' }} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{props.bgType === 'image' && (
|
||||
<div>
|
||||
<label style={labelStyle}>Background Image URL</label>
|
||||
<input type="text" value={props.bgImage || ''} placeholder="https://..."
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgImage = e.target.value; })} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.bgType === 'video' && (
|
||||
<div>
|
||||
<label style={labelStyle}>Background Video URL</label>
|
||||
<input type="text" value={props.bgVideo || ''} placeholder="https://...mp4"
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.bgVideo = e.target.value; })} style={inputStyle} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Overlay */}
|
||||
{(props.bgType === 'image' || props.bgType === 'video') && (
|
||||
<div>
|
||||
<label style={labelStyle}>Overlay ({props.overlayOpacity || 0}%)</label>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="color" value={props.overlayColor || '#000000'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.overlayColor = e.target.value; })}
|
||||
style={{ width: 30, height: 26, border: 'none', cursor: 'pointer', background: 'none' }} />
|
||||
<input type="range" min={0} max={100} value={props.overlayOpacity || 0}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.overlayOpacity = parseInt(e.target.value); })}
|
||||
style={{ flex: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Text & Button Colors */}
|
||||
<div>
|
||||
<label style={labelStyle}>Text Color</label>
|
||||
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
|
||||
<input type="color" value={props.textColor || '#ffffff'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.textColor = e.target.value; })}
|
||||
style={{ width: 36, height: 30, border: 'none', cursor: 'pointer', background: 'none' }} />
|
||||
<input type="text" value={props.textColor || '#ffffff'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.textColor = e.target.value; })}
|
||||
style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>Button BG</label>
|
||||
<input type="color" value={props.buttonBgColor || '#3b82f6'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.buttonBgColor = e.target.value; })}
|
||||
style={{ width: '100%', height: 30, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }} />
|
||||
</div>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>Button Text</label>
|
||||
<input type="color" value={props.buttonTextColor || '#ffffff'}
|
||||
onChange={(e) => setProp((p: HeroProps) => { p.buttonTextColor = e.target.value; })}
|
||||
style={{ width: '100%', height: 30, border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layout */}
|
||||
<div>
|
||||
<label style={labelStyle}>Min Height</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{['300px', '400px', '500px', '600px', '100vh'].map((h) => (
|
||||
<button key={h} onClick={() => setProp((p: HeroProps) => { p.minHeight = h; })}
|
||||
style={btnStyle(props.minHeight === h)}>{h === '100vh' ? 'Full' : h}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Vertical Align</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['top', 'center', 'bottom'] as const).map((v) => (
|
||||
<button key={v} onClick={() => setProp((p: HeroProps) => { p.verticalAlign = v; })}
|
||||
style={btnStyle(props.verticalAlign === v)}>{v}</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Text Align</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{(['left', 'center', 'right'] as const).map((a) => (
|
||||
<button key={a} onClick={() => setProp((p: HeroProps) => { p.textAlign = a; })}
|
||||
style={btnStyle(props.textAlign === a)}>
|
||||
<i className={`fa fa-align-${a}`} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
HeroSimple.craft = {
|
||||
@@ -405,9 +195,6 @@ HeroSimple.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: HeroSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -88,199 +88,6 @@ export const NumberCounter: UserComponent<NumberCounterProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const NumberCounterSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as NumberCounterProps,
|
||||
}));
|
||||
|
||||
const items = props.counters || defaultCounters;
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const columnOptions = [2, 3, 4, 5, 6];
|
||||
const sizePresets = ['32px', '40px', '48px', '56px', '64px'];
|
||||
const numberColorPresets = ['#3b82f6', '#10b981', '#8b5cf6', '#ef4444', '#f59e0b', '#18181b', '#ec4899', '#0ea5e9'];
|
||||
const labelColorPresets = ['#6b7280', '#374151', '#9ca3af', '#a1a1aa', '#64748b', '#18181b'];
|
||||
const bgPresets = ['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'];
|
||||
|
||||
const updateCounter = (index: number, field: keyof Counter, value: string | number) => {
|
||||
setProp((p: NumberCounterProps) => {
|
||||
const updated = [...(p.counters || defaultCounters)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.counters = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addCounter = () => {
|
||||
setProp((p: NumberCounterProps) => {
|
||||
p.counters = [...(p.counters || defaultCounters), { number: 100, suffix: '+', label: 'New Stat' }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeCounter = (index: number) => {
|
||||
setProp((p: NumberCounterProps) => {
|
||||
const updated = [...(p.counters || defaultCounters)];
|
||||
updated.splice(index, 1);
|
||||
p.counters = updated;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
{/* Columns */}
|
||||
<div>
|
||||
<label style={labelStyle}>Columns</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{columnOptions.map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setProp((p: NumberCounterProps) => { p.columns = n; })}
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: (props.columns || 4) === n ? '#3b82f6' : '#27272a',
|
||||
color: '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Number Size */}
|
||||
<div>
|
||||
<label style={labelStyle}>Number Size</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{sizePresets.map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onClick={() => setProp((p: NumberCounterProps) => { p.numberSize = s; })}
|
||||
style={{
|
||||
padding: '4px 8px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: (props.numberSize || '48px') === s ? '#3b82f6' : '#27272a',
|
||||
color: (props.numberSize || '48px') === s ? '#fff' : '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Number Color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Number Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{numberColorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: NumberCounterProps) => { p.numberColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: (props.numberColor || '#3b82f6') === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Label Color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Label Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{labelColorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: NumberCounterProps) => { p.labelColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: (props.labelColor || '#6b7280') === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Background */}
|
||||
<div>
|
||||
<label style={labelStyle}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: NumberCounterProps) => { p.style = { ...p.style, backgroundColor: c }; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.style?.backgroundColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Counters */}
|
||||
<div>
|
||||
<label style={labelStyle}>Counters</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{items.map((counter, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<input
|
||||
type="number"
|
||||
value={counter.number}
|
||||
onChange={(e) => updateCounter(i, 'number', parseInt(e.target.value) || 0)}
|
||||
placeholder="Number"
|
||||
style={{ ...inputStyle, width: 70, flex: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={counter.suffix}
|
||||
onChange={(e) => updateCounter(i, 'suffix', e.target.value)}
|
||||
placeholder="Suffix"
|
||||
style={{ ...inputStyle, width: 40, flex: 'none' }}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={counter.label}
|
||||
onChange={(e) => updateCounter(i, 'label', e.target.value)}
|
||||
placeholder="Label"
|
||||
style={{ ...inputStyle, flex: 1 }}
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeCounter(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer', flex: 'none' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addCounter}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Counter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
NumberCounter.craft = {
|
||||
@@ -298,9 +105,6 @@ NumberCounter.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: NumberCounterSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
interface PricingPlan {
|
||||
@@ -198,189 +197,6 @@ export const PricingTable: UserComponent<PricingTableProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const PricingTableSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as PricingTableProps,
|
||||
}));
|
||||
|
||||
const plans = props.plans || defaultPlans;
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
|
||||
const updatePlan = (index: number, field: keyof PricingPlan, value: any) => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
const updated = [...(p.plans || defaultPlans)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.plans = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const updateFeature = (planIndex: number, featureIndex: number, value: string) => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
const updated = [...(p.plans || defaultPlans)];
|
||||
const features = [...(Array.isArray(updated[planIndex].features) ? updated[planIndex].features : [])];
|
||||
features[featureIndex] = value;
|
||||
updated[planIndex] = { ...updated[planIndex], features };
|
||||
p.plans = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addFeature = (planIndex: number) => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
const updated = [...(p.plans || defaultPlans)];
|
||||
updated[planIndex] = { ...updated[planIndex], features: [...updated[planIndex].features, 'New Feature'] };
|
||||
p.plans = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const removeFeature = (planIndex: number, featureIndex: number) => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
const updated = [...(p.plans || defaultPlans)];
|
||||
const features = [...(Array.isArray(updated[planIndex].features) ? updated[planIndex].features : [])];
|
||||
features.splice(featureIndex, 1);
|
||||
updated[planIndex] = { ...updated[planIndex], features };
|
||||
p.plans = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addPlan = () => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
p.plans = [...(p.plans || defaultPlans), {
|
||||
name: 'New Plan',
|
||||
price: '$19',
|
||||
period: '/month',
|
||||
features: ['Feature 1', 'Feature 2'],
|
||||
buttonText: 'Get Started',
|
||||
buttonHref: '#',
|
||||
isFeatured: false,
|
||||
}];
|
||||
});
|
||||
};
|
||||
|
||||
const removePlan = (index: number) => {
|
||||
setProp((p: PricingTableProps) => {
|
||||
const updated = [...(p.plans || defaultPlans)];
|
||||
updated.splice(index, 1);
|
||||
p.plans = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const bgPresets = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#18181b', '#0f172a', '#7c3aed'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={labelStyle}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: PricingTableProps) => { p.style = { ...p.style, backgroundColor: c }; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.style?.backgroundColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Featured Plan Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: PricingTableProps) => { p.featuredBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.featuredBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Plans</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
{plans.map((plan, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={plan.name} onChange={(e) => updatePlan(i, 'name', e.target.value)} placeholder="Plan Name" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removePlan(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={plan.price} onChange={(e) => updatePlan(i, 'price', e.target.value)} placeholder="$29" style={{ ...inputStyle, width: '60px', flex: 'none' }} />
|
||||
<input type="text" value={plan.period} onChange={(e) => updatePlan(i, 'period', e.target.value)} placeholder="/month" style={{ ...inputStyle, width: '70px', flex: 'none' }} />
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: 11, color: '#a1a1aa', marginLeft: 'auto' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={plan.isFeatured}
|
||||
onChange={(e) => updatePlan(i, 'isFeatured', e.target.checked)}
|
||||
/>
|
||||
Featured
|
||||
</label>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={plan.buttonText} onChange={(e) => updatePlan(i, 'buttonText', e.target.value)} placeholder="Button Text" style={{ ...inputStyle, flex: 1 }} />
|
||||
<input type="text" value={plan.buttonHref} onChange={(e) => updatePlan(i, 'buttonHref', e.target.value)} placeholder="URL" style={{ ...inputStyle, flex: 1 }} />
|
||||
</div>
|
||||
|
||||
{/* Features */}
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<span style={{ fontSize: 10, color: '#71717a' }}>Features:</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 2, marginTop: 2 }}>
|
||||
{(Array.isArray(plan.features) ? plan.features : []).map((feat, fi) => (
|
||||
<div key={fi} style={{ display: 'flex', gap: 2 }}>
|
||||
<input type="text" value={feat} onChange={(e) => updateFeature(i, fi, e.target.value)} style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeFeature(i, fi)}
|
||||
style={{ padding: '1px 4px', fontSize: 10, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 3, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={() => addFeature(i)}
|
||||
style={{ marginTop: 2, width: '100%', padding: '3px', fontSize: 10, background: '#27272a', color: '#a1a1aa', border: '1px solid #3f3f46', borderRadius: 3, cursor: 'pointer' }}
|
||||
>
|
||||
+ Feature
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addPlan}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Plan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
PricingTable.craft = {
|
||||
@@ -397,9 +213,6 @@ PricingTable.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: PricingTableSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface TabItem {
|
||||
@@ -102,174 +101,6 @@ export const Tabs: UserComponent<TabsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const TabsSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as TabsProps,
|
||||
}));
|
||||
|
||||
const tabs = props.tabs || defaultTabs;
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
|
||||
const updateTab = (index: number, field: keyof TabItem, value: string) => {
|
||||
setProp((p: TabsProps) => {
|
||||
const updated = [...(p.tabs || defaultTabs)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.tabs = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addTab = () => {
|
||||
setProp((p: TabsProps) => {
|
||||
p.tabs = [...(p.tabs || defaultTabs), { label: 'New Tab', content: 'Tab content goes here.' }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeTab = (index: number) => {
|
||||
setProp((p: TabsProps) => {
|
||||
const updated = [...(p.tabs || defaultTabs)];
|
||||
updated.splice(index, 1);
|
||||
p.tabs = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const colorSwatches = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#18181b', '#ffffff', '#f1f5f9'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
<div>
|
||||
<label style={labelStyle}>Active Tab Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{colorSwatches.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TabsProps) => { p.activeTabBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.activeTabBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Active Tab Text Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#ffffff', '#18181b', '#1f2937', '#e2e8f0'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TabsProps) => { p.activeTabColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.activeTabColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Inactive Tab Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#f1f5f9', '#e2e8f0', '#f8fafc', '#ffffff', '#27272a', '#18181b'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TabsProps) => { p.inactiveTabBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.inactiveTabBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Inactive Tab Text Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#64748b', '#94a3b8', '#18181b', '#ffffff'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TabsProps) => { p.inactiveTabColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.inactiveTabColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Content Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#1e293b'].map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TabsProps) => { p.contentBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.contentBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label style={labelStyle}>Tabs</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{tabs.map((tab, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<input type="text" value={tab.label} onChange={(e) => updateTab(i, 'label', e.target.value)} placeholder="Label" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeTab(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
value={tab.content}
|
||||
onChange={(e) => updateTab(i, 'content', e.target.value)}
|
||||
placeholder="Content"
|
||||
rows={2}
|
||||
style={{ ...inputStyle, resize: 'vertical' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addTab}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Tab
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
Tabs.craft = {
|
||||
@@ -289,9 +120,6 @@ Tabs.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: TabsSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { CSSProperties, useState } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { AnchorIdField } from '../../ui/AnchorIdField';
|
||||
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
||||
|
||||
interface Testimonial {
|
||||
@@ -149,209 +148,6 @@ export const Testimonials: UserComponent<TestimonialsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Settings panel ---------- */
|
||||
|
||||
const TestimonialsSettings: React.FC = () => {
|
||||
const { actions: { setProp }, props } = useNode((node) => ({
|
||||
props: node.data.props as TestimonialsProps,
|
||||
}));
|
||||
|
||||
const items = props.testimonials || defaultTestimonials;
|
||||
|
||||
const labelStyle: CSSProperties = { fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 6 };
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '3px 6px', background: '#27272a', color: '#e4e4e7',
|
||||
border: '1px solid #3f3f46', borderRadius: 4, fontSize: 11,
|
||||
};
|
||||
|
||||
const updateTestimonial = (index: number, field: keyof Testimonial, value: string | number) => {
|
||||
setProp((p: TestimonialsProps) => {
|
||||
const updated = [...(p.testimonials || defaultTestimonials)];
|
||||
updated[index] = { ...updated[index], [field]: value };
|
||||
p.testimonials = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const addTestimonial = () => {
|
||||
setProp((p: TestimonialsProps) => {
|
||||
p.testimonials = [...(p.testimonials || defaultTestimonials), { quote: 'Great experience!', name: 'New Person', title: 'Role', rating: 5 }];
|
||||
});
|
||||
};
|
||||
|
||||
const removeTestimonial = (index: number) => {
|
||||
setProp((p: TestimonialsProps) => {
|
||||
const updated = [...(p.testimonials || defaultTestimonials)];
|
||||
updated.splice(index, 1);
|
||||
p.testimonials = updated;
|
||||
});
|
||||
};
|
||||
|
||||
const bgPresets = ['#ffffff', '#f8fafc', '#f1f5f9', '#18181b', '#0f172a'];
|
||||
const cardBgPresets = ['#f8fafc', '#ffffff', '#f1f5f9', '#e2e8f0', '#27272a', '#1e293b'];
|
||||
const starColorPresets = ['#f59e0b', '#eab308', '#ef4444', '#3b82f6', '#10b981', '#8b5cf6'];
|
||||
|
||||
return (
|
||||
<div style={{ padding: '12px', display: 'flex', flexDirection: 'column', gap: '14px' }}>
|
||||
<AnchorIdField />
|
||||
{/* Layout */}
|
||||
<div>
|
||||
<label style={labelStyle}>Layout</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
<button
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.layout = 'grid'; })}
|
||||
style={{
|
||||
flex: 1, padding: '6px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.layout === 'grid' ? '#3b82f6' : '#27272a',
|
||||
color: props.layout === 'grid' ? '#fff' : '#a1a1aa',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Grid
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.layout = 'single'; })}
|
||||
style={{
|
||||
flex: 1, padding: '6px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.layout === 'single' ? '#3b82f6' : '#27272a',
|
||||
color: props.layout === 'single' ? '#fff' : '#a1a1aa',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Single
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Columns (only for grid) */}
|
||||
{props.layout === 'grid' && (
|
||||
<div>
|
||||
<label style={labelStyle}>Columns</label>
|
||||
<div style={{ display: 'flex', gap: 4 }}>
|
||||
{[1, 2, 3, 4].map((n) => (
|
||||
<button
|
||||
key={n}
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.columns = n; })}
|
||||
style={{
|
||||
padding: '4px 10px', fontSize: 11, borderRadius: 4, cursor: 'pointer',
|
||||
border: '1px solid #3f3f46',
|
||||
background: props.columns === n ? '#3b82f6' : '#27272a',
|
||||
color: '#e4e4e7',
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Section background */}
|
||||
<div>
|
||||
<label style={labelStyle}>Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{bgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.style = { ...p.style, backgroundColor: c }; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.style?.backgroundColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card background */}
|
||||
<div>
|
||||
<label style={labelStyle}>Card Background</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{cardBgPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.cardBg = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.cardBg === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Star color */}
|
||||
<div>
|
||||
<label style={labelStyle}>Star Color</label>
|
||||
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
||||
{starColorPresets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setProp((p: TestimonialsProps) => { p.starColor = c; })}
|
||||
style={{
|
||||
width: 24, height: 24, borderRadius: 4, border: '1px solid #3f3f46',
|
||||
backgroundColor: c, cursor: 'pointer',
|
||||
outline: props.starColor === c ? '2px solid #3b82f6' : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Testimonials list */}
|
||||
<div>
|
||||
<label style={labelStyle}>Testimonials</label>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{items.map((t, i) => (
|
||||
<div key={i} style={{ background: '#1e1e22', borderRadius: 6, padding: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||
<input type="text" value={t.name} onChange={(e) => updateTestimonial(i, 'name', e.target.value)} placeholder="Name" style={{ ...inputStyle, flex: 1 }} />
|
||||
<button
|
||||
onClick={() => removeTestimonial(i)}
|
||||
style={{ padding: '2px 6px', fontSize: 11, background: '#ef4444', color: '#fff', border: 'none', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
<input type="text" value={t.title} onChange={(e) => updateTestimonial(i, 'title', e.target.value)} placeholder="Title/Role" style={inputStyle} />
|
||||
<textarea
|
||||
value={t.quote}
|
||||
onChange={(e) => updateTestimonial(i, 'quote', e.target.value)}
|
||||
placeholder="Quote..."
|
||||
rows={2}
|
||||
style={{ ...inputStyle, resize: 'vertical' }}
|
||||
/>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ fontSize: 11, color: '#a1a1aa' }}>Rating:</span>
|
||||
{[1, 2, 3, 4, 5].map((n) => (
|
||||
<i
|
||||
key={n}
|
||||
className={`fa ${n <= t.rating ? 'fa-star' : 'fa-star-o'}`}
|
||||
onClick={() => updateTestimonial(i, 'rating', n)}
|
||||
style={{ color: '#f59e0b', cursor: 'pointer', fontSize: 14 }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
onClick={addTestimonial}
|
||||
style={{ marginTop: 6, width: '100%', padding: '6px', fontSize: 11, background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, cursor: 'pointer' }}
|
||||
>
|
||||
+ Add Testimonial
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/* ---------- Craft config ---------- */
|
||||
|
||||
Testimonials.craft = {
|
||||
@@ -370,9 +166,6 @@ Testimonials.craft = {
|
||||
canMoveIn: () => false,
|
||||
canMoveOut: () => true,
|
||||
},
|
||||
related: {
|
||||
settings: TestimonialsSettings,
|
||||
},
|
||||
};
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { CSSProperties } from 'react';
|
||||
import { CSSProperties } from 'react';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
|
||||
export type CtaVariant = 'primary' | 'outline' | 'ghost';
|
||||
@@ -86,115 +86,3 @@ export function ctasToHtml(ctas: CtaButton[], defaults: CtaStyleDefaults): strin
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ---------- CTAs editor (settings UI) ---------- */
|
||||
|
||||
interface CtasEditorProps {
|
||||
ctas: CtaButton[];
|
||||
/** Called whenever the user mutates the array. Sections wire this via setProp. */
|
||||
onChange: (next: CtaButton[]) => void;
|
||||
/** Max items the user can add. Default 4. */
|
||||
max?: number;
|
||||
}
|
||||
|
||||
const inputStyle: CSSProperties = {
|
||||
width: '100%', padding: '6px 8px', background: '#27272a',
|
||||
color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12,
|
||||
};
|
||||
const labelStyle: CSSProperties = {
|
||||
fontSize: 11, color: '#a1a1aa', display: 'block', marginBottom: 4,
|
||||
};
|
||||
|
||||
export const CtasEditor: React.FC<CtasEditorProps> = ({ ctas, onChange, max = 4 }) => {
|
||||
const update = (i: number, patch: Partial<CtaButton>) => {
|
||||
const next = ctas.slice();
|
||||
next[i] = { ...next[i], ...patch };
|
||||
onChange(next);
|
||||
};
|
||||
const remove = (i: number) => onChange(ctas.filter((_, j) => j !== i));
|
||||
const add = () => {
|
||||
if (ctas.length >= max) return;
|
||||
onChange([...ctas, { text: 'New button', href: '#', variant: ctas.length === 0 ? 'primary' : 'outline' }]);
|
||||
};
|
||||
const move = (i: number, dir: -1 | 1) => {
|
||||
const j = i + dir;
|
||||
if (j < 0 || j >= ctas.length) return;
|
||||
const next = ctas.slice();
|
||||
[next[i], next[j]] = [next[j], next[i]];
|
||||
onChange(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
|
||||
<div style={{ fontSize: 11, color: '#a1a1aa', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
Buttons ({ctas.length})
|
||||
</div>
|
||||
{ctas.length === 0 && (
|
||||
<div style={{ fontSize: 11, color: '#71717a', fontStyle: 'italic', padding: '8px 0' }}>
|
||||
No buttons. Click "Add button" to insert one.
|
||||
</div>
|
||||
)}
|
||||
{ctas.map((cta, i) => (
|
||||
<div key={i} style={{
|
||||
background: '#18181b', border: '1px solid #3f3f46', borderRadius: 6,
|
||||
padding: 10, display: 'flex', flexDirection: 'column', gap: 6,
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<span style={{ fontSize: 10, color: '#a1a1aa', fontWeight: 600, flex: 1 }}>Button {i + 1}</span>
|
||||
<button onClick={() => move(i, -1)} disabled={i === 0} title="Move up"
|
||||
style={iconBtn(i === 0)}>↑</button>
|
||||
<button onClick={() => move(i, 1)} disabled={i === ctas.length - 1} title="Move down"
|
||||
style={iconBtn(i === ctas.length - 1)}>↓</button>
|
||||
<button onClick={() => remove(i)} title="Remove"
|
||||
style={{ ...iconBtn(false), color: '#fca5a5' }}>✕</button>
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>Text</label>
|
||||
<input type="text" value={cta.text} onChange={(e) => update(i, { text: e.target.value })} style={inputStyle} />
|
||||
</div>
|
||||
<div>
|
||||
<label style={labelStyle}>URL</label>
|
||||
<input type="text" value={cta.href} onChange={(e) => update(i, { href: e.target.value })}
|
||||
placeholder="https://… or #anchor" style={inputStyle} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 6 }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<label style={labelStyle}>Style</label>
|
||||
<select value={cta.variant || 'primary'}
|
||||
onChange={(e) => update(i, { variant: e.target.value as CtaVariant })}
|
||||
style={{ ...inputStyle, padding: '5px 6px' }}>
|
||||
<option value="primary">Primary (filled)</option>
|
||||
<option value="outline">Outline</option>
|
||||
<option value="ghost">Ghost (text)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div style={{ flex: '0 0 auto', display: 'flex', alignItems: 'flex-end' }}>
|
||||
<label style={{ fontSize: 11, color: '#a1a1aa', display: 'inline-flex', alignItems: 'center', gap: 4, cursor: 'pointer', whiteSpace: 'nowrap' }}>
|
||||
<input type="checkbox" checked={cta.target === '_blank'}
|
||||
onChange={(e) => update(i, { target: e.target.checked ? '_blank' : undefined })} />
|
||||
New tab
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{ctas.length < max && (
|
||||
<button onClick={add} style={{
|
||||
padding: '8px 12px', fontSize: 12, fontWeight: 600,
|
||||
color: '#3b82f6', background: 'rgba(59,130,246,0.1)',
|
||||
border: '1px dashed #3b82f6', borderRadius: 4, cursor: 'pointer',
|
||||
}}>
|
||||
+ Add button{ctas.length === 0 ? '' : ` (${max - ctas.length} more)`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function iconBtn(disabled: boolean): CSSProperties {
|
||||
return {
|
||||
width: 22, height: 22, fontSize: 11,
|
||||
background: '#27272a', color: disabled ? '#52525b' : '#a1a1aa',
|
||||
border: '1px solid #3f3f46', borderRadius: 4,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user