65a10a1ef9
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>
184 lines
6.7 KiB
TypeScript
184 lines
6.7 KiB
TypeScript
import React, { CSSProperties, useState } from 'react';
|
|
import { useNode, UserComponent } from '@craftjs/core';
|
|
import { cssPropsToString } from '../../utils/style-helpers';
|
|
import { escapeHtml, escapeAttr } from '../../utils/escape';
|
|
|
|
interface AccordionItem {
|
|
title: string;
|
|
content: string;
|
|
isOpen?: boolean;
|
|
}
|
|
|
|
interface AccordionProps {
|
|
items?: AccordionItem[];
|
|
style?: CSSProperties;
|
|
headerBg?: string;
|
|
headerColor?: string;
|
|
contentBg?: string;
|
|
borderColor?: string;
|
|
anchorId?: string;
|
|
}
|
|
|
|
const defaultItems: AccordionItem[] = [
|
|
{ title: 'What is this product?', content: 'Our product is a powerful yet easy-to-use tool designed to help you build beautiful websites without writing a single line of code.', isOpen: true },
|
|
{ title: 'How do I get started?', content: 'Simply sign up for a free account, choose a template, and start customizing. Our drag-and-drop editor makes it easy to create professional pages in minutes.', isOpen: false },
|
|
{ title: 'Is there a free plan?', content: 'Yes! We offer a generous free tier that includes all core features. Upgrade anytime to unlock advanced capabilities like custom domains and analytics.', isOpen: false },
|
|
];
|
|
|
|
export const Accordion: UserComponent<AccordionProps> = ({
|
|
items = defaultItems,
|
|
style = {},
|
|
headerBg = '#f8fafc',
|
|
headerColor = '#18181b',
|
|
contentBg = '#ffffff',
|
|
borderColor = '#e2e8f0',
|
|
anchorId,
|
|
}) => {
|
|
const {
|
|
connectors: { connect, drag },
|
|
selected,
|
|
} = useNode((node) => ({
|
|
selected: node.events.selected,
|
|
}));
|
|
|
|
const [openIndexes, setOpenIndexes] = useState<Set<number>>(() => {
|
|
const initial = new Set<number>();
|
|
items.forEach((item, i) => { if (item.isOpen) initial.add(i); });
|
|
return initial;
|
|
});
|
|
|
|
const toggle = (index: number) => {
|
|
setOpenIndexes((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(index)) next.delete(index);
|
|
else next.add(index);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
return (
|
|
<section
|
|
ref={(ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }}
|
|
id={anchorId || undefined}
|
|
style={{
|
|
padding: '60px 20px',
|
|
backgroundColor: '#ffffff',
|
|
outline: selected ? '2px solid #3b82f6' : 'none',
|
|
...style,
|
|
}}
|
|
>
|
|
<div style={{ maxWidth: '800px', margin: '0 auto', display: 'flex', flexDirection: 'column', gap: '0px' }}>
|
|
{items.map((item, i) => {
|
|
const isOpen = openIndexes.has(i);
|
|
return (
|
|
<div
|
|
key={i}
|
|
style={{
|
|
border: `1px solid ${borderColor}`,
|
|
borderBottom: i === items.length - 1 ? `1px solid ${borderColor}` : 'none',
|
|
...(i === 0 ? { borderTopLeftRadius: '8px', borderTopRightRadius: '8px' } : {}),
|
|
...(i === items.length - 1 ? { borderBottomLeftRadius: '8px', borderBottomRightRadius: '8px', borderBottom: `1px solid ${borderColor}` } : {}),
|
|
}}
|
|
>
|
|
<div
|
|
onClick={() => toggle(i)}
|
|
style={{
|
|
padding: '16px 20px',
|
|
backgroundColor: headerBg,
|
|
color: headerColor,
|
|
cursor: 'pointer',
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'center',
|
|
fontWeight: '600',
|
|
fontSize: '16px',
|
|
userSelect: 'none',
|
|
...(i === 0 ? { borderTopLeftRadius: '7px', borderTopRightRadius: '7px' } : {}),
|
|
...(i === items.length - 1 && !isOpen ? { borderBottomLeftRadius: '7px', borderBottomRightRadius: '7px' } : {}),
|
|
}}
|
|
>
|
|
<span>{item.title}</span>
|
|
<span style={{ fontSize: '12px', transition: 'transform 0.2s', transform: isOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>▼</span>
|
|
</div>
|
|
{isOpen && (
|
|
<div
|
|
style={{
|
|
padding: '16px 20px',
|
|
backgroundColor: contentBg,
|
|
color: '#4b5563',
|
|
fontSize: '14px',
|
|
lineHeight: '1.6',
|
|
borderTop: `1px solid ${borderColor}`,
|
|
...(i === items.length - 1 ? { borderBottomLeftRadius: '7px', borderBottomRightRadius: '7px' } : {}),
|
|
}}
|
|
>
|
|
{item.content}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|
|
|
|
/* ---------- Craft config ---------- */
|
|
|
|
Accordion.craft = {
|
|
displayName: 'Accordion',
|
|
props: {
|
|
items: defaultItems,
|
|
style: { backgroundColor: '#ffffff' },
|
|
headerBg: '#f8fafc',
|
|
headerColor: '#18181b',
|
|
contentBg: '#ffffff',
|
|
borderColor: '#e2e8f0',
|
|
anchorId: '',
|
|
},
|
|
rules: {
|
|
canDrag: () => true,
|
|
canMoveIn: () => false,
|
|
canMoveOut: () => true,
|
|
},
|
|
};
|
|
|
|
/* ---------- HTML export ---------- */
|
|
|
|
(Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => {
|
|
const sectionStyle = cssPropsToString({
|
|
padding: '60px 20px',
|
|
...props.style,
|
|
});
|
|
const idAttr = props.anchorId ? ` id="${escapeAttr(props.anchorId)}"` : '';
|
|
const headerBg = props.headerBg || '#f8fafc';
|
|
const headerColor = props.headerColor || '#18181b';
|
|
const contentBg = props.contentBg || '#ffffff';
|
|
const borderColor = props.borderColor || '#e2e8f0';
|
|
const items = props.items || defaultItems;
|
|
|
|
const panels = items.map((item, i) => {
|
|
const openAttr = item.isOpen ? ' open' : '';
|
|
const topRadius = i === 0 ? 'border-top-left-radius:8px;border-top-right-radius:8px;' : '';
|
|
const bottomRadius = i === items.length - 1 ? 'border-bottom-left-radius:8px;border-bottom-right-radius:8px;' : '';
|
|
const borderBottom = i === items.length - 1 ? `border:1px solid ${borderColor};` : `border:1px solid ${borderColor};border-bottom:none;`;
|
|
return `<details${openAttr} style="${borderBottom}${topRadius}${bottomRadius}">
|
|
<summary style="padding:16px 20px;background-color:${headerBg};color:${headerColor};cursor:pointer;font-weight:600;font-size:16px;list-style:none;display:flex;justify-content:space-between;align-items:center">
|
|
${escapeHtml(item.title)}
|
|
</summary>
|
|
<div style="padding:16px 20px;background-color:${contentBg};color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${borderColor}">
|
|
${escapeHtml(item.content)}
|
|
</div>
|
|
</details>`;
|
|
}).join('\n ');
|
|
|
|
return {
|
|
html: `<section${idAttr}${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
|
<div style="max-width:800px;margin:0 auto;display:flex;flex-direction:column">
|
|
${panels}
|
|
</div>
|
|
<style>details summary::-webkit-details-marker{display:none}details summary::marker{display:none}</style>
|
|
</section>`,
|
|
};
|
|
};
|