2026-04-05 18:31:16 -07:00
import React , { CSSProperties , useState } from 'react' ;
import { useNode , UserComponent } from '@craftjs/core' ;
import { cssPropsToString } from '../../utils/style-helpers' ;
2026-07-12 11:49:10 -07:00
import { escapeHtml , escapeAttr } from '../../utils/escape' ;
2026-04-05 18:31:16 -07:00
interface AccordionItem {
title : string ;
content : string ;
isOpen? : boolean ;
}
interface AccordionProps {
items? : AccordionItem [];
style? : CSSProperties ;
headerBg? : string ;
headerColor? : string ;
contentBg? : string ;
borderColor? : string ;
2026-05-25 12:43:28 -07:00
anchorId? : string ;
2026-04-05 18:31:16 -07:00
}
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' ,
2026-05-25 12:43:28 -07:00
anchorId ,
2026-04-05 18:31:16 -07:00
}) => {
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 )); }}
2026-05-25 12:43:28 -07:00
id = { anchorId || undefined }
2026-04-05 18:31:16 -07:00
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)' }}> & # 9660 ;</ 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' ,
2026-05-25 12:43:28 -07:00
anchorId : '' ,
2026-04-05 18:31:16 -07:00
},
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 ,
});
2026-07-12 11:49:10 -07:00
const idAttr = props . anchorId ? ` id=" ${ escapeAttr ( props . anchorId ) } "` : '' ;
2026-04-05 18:31:16 -07:00
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">
2026-07-12 11:49:10 -07:00
${ escapeHtml ( item . title ) }
2026-04-05 18:31:16 -07:00
</summary>
<div style="padding:16px 20px;background-color: ${ contentBg } ;color:#4b5563;font-size:14px;line-height:1.6;border-top:1px solid ${ borderColor } ">
2026-07-12 11:49:10 -07:00
${ escapeHtml ( item . content ) }
2026-04-05 18:31:16 -07:00
</div>
</details>` ;
}). join ( '\n ' );
return {
2026-05-25 12:43:28 -07:00
html : `<section ${ idAttr }${ sectionStyle ? ` style=" ${ sectionStyle } "` : '' } >
2026-04-05 18:31:16 -07:00
<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>` ,
};
};