2026-04-05 18:31:16 -07:00
import React , { useCallback , useState } from 'react' ;
import { useEditor } from '@craftjs/core' ;
2026-07-14 06:47:58 -07:00
import { SHADOW_PRESETS } from '../../../constants/presets' ;
2026-04-05 18:31:16 -07:00
import {
StylePanelProps ,
CollapsibleSection ,
ColorPickerField ,
2026-07-14 06:47:58 -07:00
SectionLabel ,
PresetButtonGrid ,
2026-04-05 18:31:16 -07:00
labelStyle ,
inputStyle ,
smallInputStyle ,
btnActiveStyle ,
sectionGap ,
2026-07-14 06:47:58 -07:00
useNodeProp ,
SpacingControl ,
BorderControl ,
BorderValue ,
buildBorderShorthand ,
AnimationControl ,
VisibilityControl ,
2026-04-05 18:31:16 -07:00
} from './shared' ;
2026-07-14 06:47:58 -07:00
/** Parses a border shorthand string ("2px solid #hex") produced by
* buildBorderShorthand() back into its parts for round-tripping through
* BorderControl. See SectionTypePanel.tsx for the identical helper. */
function parseBorderShorthand ( v : string | undefined ) : BorderValue {
if ( ! v || v === 'none' ) return { width : '' , style : 'none' , color : '#000000' };
const m = String ( v ). match ( /^([\d.]+[a-z%]*)\s+(\w+)\s+(.+)$/ );
if ( ! m ) return { width : '' , style : 'none' , color : '#000000' };
return { width : m [ 1 ], style : m [ 2 ], color : m [ 3 ] };
}
const capSide = ( s : string ) : string => s . charAt ( 0 ). toUpperCase () + s . slice ( 1 );
2026-04-05 18:31:16 -07:00
const bulletOptions = [
{ label : '✓' , value : 'check' },
{ label : '●' , value : 'dot' },
{ label : '→' , value : 'arrow' },
{ label : '★' , value : 'star' },
{ label : '—' , value : 'dash' },
{ label : 'None' , value : 'none' },
];
const bulletChar : Record < string , string > = {
check : '✓' , dot : '●' , arrow : '→' , star : '★' , dash : '—' , none : '' ,
};
export const PricingStylePanel : React.FC < StylePanelProps > = ({ selectedId , nodeProps }) => {
const { actions } = useEditor ();
2026-07-14 06:47:58 -07:00
const { setProp , setPropStyle } = useNodeProp ( selectedId );
2026-04-05 18:31:16 -07:00
const [ expandedPlan , setExpandedPlan ] = useState < number >( 0 );
const plans : any [] = Array . isArray ( nodeProps . plans ) ? nodeProps . plans : [];
const currentBullet = nodeProps . bulletType || 'check' ;
2026-07-14 06:47:58 -07:00
const style = nodeProps . style || {};
2026-04-05 18:31:16 -07:00
const updatePlan = useCallback (( planIndex : number , field : string , value : any ) => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
updated [ planIndex ] = { ... updated [ planIndex ], [ field ] : value };
props . plans = updated ;
});
}, [ actions , selectedId ]);
const addPlan = useCallback (() => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
updated . push ({
name : 'New Plan' ,
price : '$0' ,
period : '/month' ,
features : [ 'Feature 1' ],
buttonText : 'Choose Plan' ,
buttonHref : '#' ,
isFeatured : false ,
});
props . plans = updated ;
});
}, [ actions , selectedId ]);
const removePlan = useCallback (( index : number ) => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
updated . splice ( index , 1 );
props . plans = updated ;
});
}, [ actions , selectedId ]);
const addFeature = useCallback (( planIndex : number ) => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
const features = [...( Array . isArray ( updated [ planIndex ] ? . features ) ? updated [ planIndex ]. features : [])];
features . push ( 'New feature' );
updated [ planIndex ] = { ... updated [ planIndex ], features };
props . plans = updated ;
});
}, [ actions , selectedId ]);
const updateFeature = useCallback (( planIndex : number , featureIndex : number , value : string ) => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
const features = [...( Array . isArray ( updated [ planIndex ] ? . features ) ? updated [ planIndex ]. features : [])];
features [ featureIndex ] = value ;
updated [ planIndex ] = { ... updated [ planIndex ], features };
props . plans = updated ;
});
}, [ actions , selectedId ]);
const removeFeature = useCallback (( planIndex : number , featureIndex : number ) => {
actions . setProp ( selectedId , ( props : any ) => {
const updated = [...( Array . isArray ( props . plans ) ? props . plans : [])];
const features = [...( Array . isArray ( updated [ planIndex ] ? . features ) ? updated [ planIndex ]. features : [])];
features . splice ( featureIndex , 1 );
updated [ planIndex ] = { ... updated [ planIndex ], features };
props . plans = updated ;
});
}, [ actions , selectedId ]);
return (
<>
{ /* Bullet type */ }
< CollapsibleSection title = "Bullet Style" >
< div style = {{ display : 'flex' , gap : 4 }}>
{ bulletOptions . map (( b ) => (
< button key = { b . value } onClick = {() => actions . setProp ( selectedId , ( p : any ) => { p . bulletType = b . value ; })}
style = {{ ... btnActiveStyle ( currentBullet === b . value ), flex : 1 , fontSize : 14 }}>
{ b . label }
</ button >
))}
</ div >
</ CollapsibleSection >
{ /* Plans */ }
< CollapsibleSection title = { `Plans ( ${ plans . length } )` }>
{ plans . map (( plan , i ) => {
const isExpanded = expandedPlan === i ;
const features : string [] = Array . isArray ( plan . features ) ? plan . features : [];
return (
< div key = { i } style = {{
marginBottom : 8 , background : '#18181b' , borderRadius : 6 ,
border : plan.isFeatured ? '1px solid #3b82f6' : '1px solid #27272a' ,
}}>
{ /* Plan header - click to expand */ }
< div onClick = {() => setExpandedPlan ( isExpanded ? - 1 : i )} style = {{
padding : '8px 10px' , cursor : 'pointer' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' ,
}}>
< span style = {{ fontSize : 12 , fontWeight : 600 , color : '#e4e4e7' }}>
{ plan . name || 'Plan' } { plan . isFeatured && < span style = {{ fontSize : 9 , background : '#3b82f6' , color : '#fff' , padding : '1px 5px' , borderRadius : 3 , marginLeft : 4 }}> Featured </ span >}
</ span >
< div style = {{ display : 'flex' , gap : 4 , alignItems : 'center' }}>
< span style = {{ fontSize : 11 , color : '#71717a' }}>{ plan . price }</ span >
< i className = { `fa fa-chevron- ${ isExpanded ? 'up' : 'down' } ` } style = {{ fontSize : 10 , color : '#71717a' }} />
</ div >
</ div >
{ /* Expanded plan settings */ }
{ isExpanded && (
< div style = {{ padding : '0 10px 10px' , display : 'flex' , flexDirection : 'column' , gap : 6 }}>
< div style = {{ display : 'flex' , gap : 4 }}>
< div style = {{ flex : 1 }}>
< label style = {{ fontSize : 9 , color : '#52525b' }}> Name </ label >
< input type = "text" value = { plan . name || '' } onChange = {( e ) => updatePlan ( i , 'name' , e . target . value )} style = { smallInputStyle } />
</ div >
< div style = {{ flex : 1 }}>
< label style = {{ fontSize : 9 , color : '#52525b' }}> Price </ label >
< input type = "text" value = { plan . price || '' } onChange = {( e ) => updatePlan ( i , 'price' , e . target . value )} style = { smallInputStyle } />
</ div >
</ div >
< div >
< label style = {{ fontSize : 9 , color : '#52525b' }}> Period </ label >
< input type = "text" value = { plan . period || '' } onChange = {( e ) => updatePlan ( i , 'period' , e . target . value )} placeholder = "/month" style = { smallInputStyle } />
</ div >
< div style = {{ display : 'flex' , gap : 4 }}>
< div style = {{ flex : 1 }}>
< label style = {{ fontSize : 9 , color : '#52525b' }}> Button Text </ label >
< input type = "text" value = { plan . buttonText || '' } onChange = {( e ) => updatePlan ( i , 'buttonText' , e . target . value )} style = { smallInputStyle } />
</ div >
< div style = {{ flex : 1 }}>
< label style = {{ fontSize : 9 , color : '#52525b' }}> Button URL </ label >
< input type = "text" value = { plan . buttonHref || '' } onChange = {( e ) => updatePlan ( i , 'buttonHref' , e . target . value )} style = { smallInputStyle } />
</ div >
</ div >
< label style = {{ fontSize : 10 , color : '#71717a' , display : 'flex' , alignItems : 'center' , gap : 4 , cursor : 'pointer' }}>
< input type = "checkbox" checked = { !! plan . isFeatured } onChange = {( e ) => updatePlan ( i , 'isFeatured' , e . target . checked )} />
Featured ( highlighted )
</ label >
{ /* Features list */ }
< div >
< label style = {{ fontSize : 9 , color : '#52525b' , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' }}>
< span > Features ({ features . length })</ span >
< button onClick = {() => addFeature ( i )} style = {{ fontSize : 9 , background : '#3b82f6' , color : '#fff' , border : 'none' , borderRadius : 3 , padding : '2px 6px' , cursor : 'pointer' }}>
+ Add
</ button >
</ label >
< div style = {{ display : 'flex' , flexDirection : 'column' , gap : 2 , marginTop : 4 }}>
{ features . map (( feat , fi ) => (
< div key = { fi } style = {{ display : 'flex' , gap : 2 , alignItems : 'center' }}>
< span style = {{ fontSize : 11 , color : '#10b981' , width : 14 , textAlign : 'center' }}>{ bulletChar [ currentBullet ] || '✓' }</ span >
< input type = "text" value = { feat } onChange = {( e ) => updateFeature ( i , fi , e . target . value )} style = {{ ... smallInputStyle , flex : 1 }} />
< button onClick = {() => removeFeature ( i , fi )} style = {{ fontSize : 9 , background : '#ef4444' , color : '#fff' , border : 'none' , borderRadius : 3 , padding : '1px 4px' , cursor : 'pointer' , lineHeight : 1 }}>
×
</ button >
</ div >
))}
</ div >
</ div >
{ /* Remove plan */ }
{ plans . length > 1 && (
< button onClick = {() => removePlan ( i )} style = {{ fontSize : 10 , background : 'none' , color : '#ef4444' , border : '1px solid #ef4444' , borderRadius : 4 , padding : '3px 8px' , cursor : 'pointer' , marginTop : 4 }}>
Remove Plan
</ button >
)}
</ div >
)}
</ div >
);
})}
< button onClick = { addPlan } style = {{ width : '100%' , padding : '6px' , fontSize : 11 , background : '#27272a' , color : '#e4e4e7' , border : '1px solid #3f3f46' , borderRadius : 4 , cursor : 'pointer' , marginTop : 4 }}>
+ Add Plan
</ button >
</ CollapsibleSection >
{ /* Colors */ }
< CollapsibleSection title = "Colors" defaultOpen = { false }>
< ColorPickerField label = "Featured Plan Color" value = { nodeProps . featuredBg || '#3b82f6' } onChange = {( v ) => actions . setProp ( selectedId , ( p : any ) => { p . featuredBg = v ; })} />
2026-07-14 06:47:58 -07:00
{ /* Regular (non-featured) card colors -- built into PricingTable's
render/toHtml but previously hard-coded literals with no control
surfacing them. Each falls back to the prior literal when blank. */ }
< ColorPickerField label = "Card Background" value = { nodeProps . cardBg || '#ffffff' } onChange = {( v ) => setProp ( 'cardBg' , v )} />
< ColorPickerField label = "Heading / Price Color" value = { nodeProps . textColor || '#18181b' } onChange = {( v ) => setProp ( 'textColor' , v )} />
< ColorPickerField label = "Period Text Color" value = { nodeProps . subColor || '#64748b' } onChange = {( v ) => setProp ( 'subColor' , v )} />
< ColorPickerField label = "Feature Text Color" value = { nodeProps . featColor || '#4b5563' } onChange = {( v ) => setProp ( 'featColor' , v )} />
< ColorPickerField label = "Checkmark Color" value = { nodeProps . checkColor || '#10b981' } onChange = {( v ) => setProp ( 'checkColor' , v )} />
< ColorPickerField label = "Button Background" value = { nodeProps . btnBg || nodeProps . featuredBg || '#3b82f6' } onChange = {( v ) => setProp ( 'btnBg' , v )} />
< ColorPickerField label = "Button Text Color" value = { nodeProps . btnColor || '#ffffff' } onChange = {( v ) => setProp ( 'btnColor' , v )} />
</ CollapsibleSection >
{ /* Box model: margin, padding, border, shadow, opacity */ }
< CollapsibleSection title = "Spacing & Border" defaultOpen = { false }>
< SpacingControl
label = "Margin"
value = {{
top : style.marginTop as string , right : style.marginRight as string ,
bottom : style.marginBottom as string , left : style.marginLeft as string ,
}}
onChange = {( side , v ) => setPropStyle ( `margin ${ capSide ( side ) } ` , v )}
/>
< SpacingControl
label = "Padding"
value = {{
top : style.paddingTop as string , right : style.paddingRight as string ,
bottom : style.paddingBottom as string , left : style.paddingLeft as string ,
}}
onChange = {( side , v ) => setPropStyle ( `padding ${ capSide ( side ) } ` , v )}
/>
< BorderControl
value = { parseBorderShorthand ( style . border as string )}
onChange = {( v ) => setPropStyle ( 'border' , buildBorderShorthand ( v ))}
/>
< div className = "guided-section" >
< SectionLabel > Shadow </ SectionLabel >
< PresetButtonGrid presets = { SHADOW_PRESETS } activeValue = { style . boxShadow as string } onSelect = {( v ) => setPropStyle ( 'boxShadow' , v )} />
</ div >
< div className = "guided-section" >
< SectionLabel > Opacity </ SectionLabel >
< input
type = "range"
min = { 0 }
max = { 100 }
value = { style . opacity !== undefined ? Math . round ( Number ( style . opacity ) * 100 ) : 100 }
onChange = {( e ) => setPropStyle ( 'opacity' , String ( Number ( e . target . value ) / 100 ))}
style = {{ width : '100%' }}
/>
</ div >
</ CollapsibleSection >
{ /* Entrance animation */ }
< CollapsibleSection title = "Animation" defaultOpen = { false }>
< AnimationControl
value = {{ animation : nodeProps.animation || 'none' , animationDelay : nodeProps.animationDelay || '0' }}
onChange = {( v ) => { setProp ( 'animation' , v . animation ); setProp ( 'animationDelay' , v . animationDelay ); }}
/>
</ CollapsibleSection >
{ /* Responsive visibility */ }
< CollapsibleSection title = "Visibility" defaultOpen = { false }>
< VisibilityControl
value = {{
hideOnDesktop : !! nodeProps . hideOnDesktop ,
hideOnTablet : !! nodeProps . hideOnTablet ,
hideOnMobile : !! nodeProps . hideOnMobile ,
}}
onChange = {( v ) => {
setProp ( 'hideOnDesktop' , !! v . hideOnDesktop );
setProp ( 'hideOnTablet' , !! v . hideOnTablet );
setProp ( 'hideOnMobile' , !! v . hideOnMobile );
}}
/>
2026-04-05 18:31:16 -07:00
</ CollapsibleSection >
</>
);
};