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-05-25 12:43:28 -07:00
import { AnchorIdField } from '../../ui/AnchorIdField' ;
2026-04-05 18:31:16 -07:00
interface TabItem {
label : string ;
content : string ;
}
interface TabsProps {
tabs? : TabItem [];
style? : CSSProperties ;
activeTabBg? : string ;
activeTabColor? : string ;
inactiveTabBg? : string ;
inactiveTabColor? : string ;
contentBg? : string ;
2026-05-25 12:43:28 -07:00
anchorId? : string ;
2026-04-05 18:31:16 -07:00
}
const defaultTabs : TabItem [] = [
{ label : 'Overview' , content : 'Welcome to our platform. We provide the tools you need to build, launch, and grow your online presence. Our intuitive interface makes it simple to get started in minutes.' },
{ label : 'Features' , content : 'Drag-and-drop editor, responsive templates, custom domains, analytics dashboard, SEO tools, and integrations with your favorite services. Everything you need in one place.' },
{ label : 'Support' , content : 'Our dedicated support team is available 24/7 to help you with any questions. Access our knowledge base, community forums, or reach out directly via live chat or email.' },
];
export const Tabs : UserComponent < TabsProps > = ({
tabs = defaultTabs ,
style = {},
activeTabBg = '#3b82f6' ,
activeTabColor = '#ffffff' ,
inactiveTabBg = '#f1f5f9' ,
inactiveTabColor = '#64748b' ,
contentBg = '#ffffff' ,
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 [ activeIndex , setActiveIndex ] = useState ( 0 );
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' }}>
{ /* Tab buttons */ }
< div style = {{ display : 'flex' , gap : '2px' , borderBottom : '2px solid #e2e8f0' }}>
{ tabs . map (( tab , i ) => (
< button
key = { i }
onClick = {() => setActiveIndex ( i )}
style = {{
padding : '12px 24px' ,
fontSize : '14px' ,
fontWeight : '600' ,
border : 'none' ,
borderTopLeftRadius : '8px' ,
borderTopRightRadius : '8px' ,
cursor : 'pointer' ,
backgroundColor : i === activeIndex ? activeTabBg : inactiveTabBg ,
color : i === activeIndex ? activeTabColor : inactiveTabColor ,
transition : 'background-color 0.2s, color 0.2s' ,
}}
>
{ tab . label }
</ button >
))}
</ div >
{ /* Content panel */ }
< div
style = {{
padding : '24px' ,
backgroundColor : contentBg ,
border : '1px solid #e2e8f0' ,
borderTop : 'none' ,
borderBottomLeftRadius : '8px' ,
borderBottomRightRadius : '8px' ,
fontSize : '14px' ,
lineHeight : '1.7' ,
color : '#4b5563' ,
minHeight : '100px' ,
}}
>
{ tabs [ activeIndex ] ? . content || '' }
</ div >
</ div >
</ section >
);
};
/* ---------- 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' }}>
2026-05-25 12:43:28 -07:00
< AnchorIdField />
2026-04-05 18:31:16 -07:00
< 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 = {
displayName : 'Tabs' ,
props : {
tabs : defaultTabs ,
style : { backgroundColor : '#ffffff' },
activeTabBg : '#3b82f6' ,
activeTabColor : '#ffffff' ,
inactiveTabBg : '#f1f5f9' ,
inactiveTabColor : '#64748b' ,
contentBg : '#ffffff' ,
2026-05-25 12:43:28 -07:00
anchorId : '' ,
2026-04-05 18:31:16 -07:00
},
rules : {
canDrag : () => true ,
canMoveIn : () => false ,
canMoveOut : () => true ,
},
related : {
settings : TabsSettings ,
},
};
/* ---------- HTML export ---------- */
( Tabs as any ). toHtml = ( props : TabsProps , _childrenHtml : string ) => {
2026-05-25 12:43:28 -07:00
const esc = ( s : any ) => String ( s ?? "" ). replace ( /</g , '<' ). replace ( />/g , '>' ). replace ( /"/g , '"' );
2026-04-05 18:31:16 -07:00
const sectionStyle = cssPropsToString ({
padding : '60px 20px' ,
... props . style ,
});
2026-05-25 12:43:28 -07:00
const idAttr = props . anchorId ? ` id=" ${ esc ( props . anchorId ) } "` : '' ;
2026-04-05 18:31:16 -07:00
const tabs = props . tabs || defaultTabs ;
const activeTabBg = props . activeTabBg || '#3b82f6' ;
const activeTabColor = props . activeTabColor || '#ffffff' ;
const inactiveTabBg = props . inactiveTabBg || '#f1f5f9' ;
const inactiveTabColor = props . inactiveTabColor || '#64748b' ;
const contentBg = props . contentBg || '#ffffff' ;
const tabId = 'tabs_' + Math . random (). toString ( 36 ). slice ( 2 , 8 );
const tabButtons = tabs . map (( tab , i ) => {
const isActive = i === 0 ;
return `<button onclick=" ${ tabId } _switch( ${ i } )" id=" ${ tabId } _btn_ ${ i } " style="padding:12px 24px;font-size:14px;font-weight:600;border:none;border-top-left-radius:8px;border-top-right-radius:8px;cursor:pointer;background-color: ${ isActive ? activeTabBg : inactiveTabBg } ;color: ${ isActive ? activeTabColor : inactiveTabColor } "> ${ esc ( tab . label ) } </button>` ;
}). join ( '\n ' );
const tabPanels = tabs . map (( tab , i ) => {
return `<div id=" ${ tabId } _panel_ ${ i } " style="padding:24px;background-color: ${ contentBg } ;border:1px solid #e2e8f0;border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px;font-size:14px;line-height:1.7;color:#4b5563;min-height:100px; ${ i !== 0 ? 'display:none' : '' } "> ${ esc ( tab . content ) } </div>` ;
}). join ( '\n ' );
const switchScript = `<script>
function ${ tabId } _switch(idx){
var total= ${ tabs . length } ;
for(var i=0;i<total;i++){
document.getElementById(' ${ tabId } _panel_'+i).style.display=i===idx?'':'none';
var btn=document.getElementById(' ${ tabId } _btn_'+i);
btn.style.backgroundColor=i===idx?' ${ activeTabBg } ':' ${ inactiveTabBg } ';
btn.style.color=i===idx?' ${ activeTabColor } ':' ${ inactiveTabColor } ';
}
}
</script>` ;
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">
<div style="display:flex;gap:2px;border-bottom:2px solid #e2e8f0">
${ tabButtons }
</div>
${ tabPanels }
</div>
${ switchScript }
</section>` ,
};
};