feat(builder): surface hidden section/pricing/social props + box-model rollout
SectionTypePanel (Accordion/Tabs/Testimonials/Countdown/NumberCounter/ CTASection/CallToAction/FeaturesGrid), PricingStylePanel, and SocialStylePanel all gain a Spacing & Border section (margin/padding per-side, border, box-shadow, opacity), an Animation section, and a Visibility section, wired to the shared SpacingControl/BorderControl/ AnimationControl/VisibilityControl. PricingTable's per-card colors (cardBg/textColor/subColor/featColor/ checkColor/btnBg/btnColor) were previously hard-coded literals computed from featuredBg inside toHtml -- promoted to real optional props (each falling back to the exact prior literal when unset) and exposed via ColorPickerField in PricingStylePanel. SocialStylePanel now exposes SocialLinks' iconShape/gap (already-built props with no control), plus Icon's bgColor/bgShape/bgSize/link and StarRating's filledColor/emptyColor, which the panel's generic iconBgColor/starColor checks never matched since those aren't Icon's or StarRating's real prop names. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +1,38 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useEditor } from '@craftjs/core';
|
||||
import { SHADOW_PRESETS } from '../../../constants/presets';
|
||||
import {
|
||||
StylePanelProps,
|
||||
CollapsibleSection,
|
||||
ColorPickerField,
|
||||
SectionLabel,
|
||||
PresetButtonGrid,
|
||||
labelStyle,
|
||||
inputStyle,
|
||||
smallInputStyle,
|
||||
btnActiveStyle,
|
||||
sectionGap,
|
||||
useNodeProp,
|
||||
SpacingControl,
|
||||
BorderControl,
|
||||
BorderValue,
|
||||
buildBorderShorthand,
|
||||
AnimationControl,
|
||||
VisibilityControl,
|
||||
} from './shared';
|
||||
|
||||
/** 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);
|
||||
|
||||
const bulletOptions = [
|
||||
{ label: '✓', value: 'check' },
|
||||
{ label: '●', value: 'dot' },
|
||||
@@ -26,10 +48,12 @@ const bulletChar: Record<string, string> = {
|
||||
|
||||
export const PricingStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeProps }) => {
|
||||
const { actions } = useEditor();
|
||||
const { setProp, setPropStyle } = useNodeProp(selectedId);
|
||||
const [expandedPlan, setExpandedPlan] = useState<number>(0);
|
||||
|
||||
const plans: any[] = Array.isArray(nodeProps.plans) ? nodeProps.plans : [];
|
||||
const currentBullet = nodeProps.bulletType || 'check';
|
||||
const style = nodeProps.style || {};
|
||||
|
||||
const updatePlan = useCallback((planIndex: number, field: string, value: any) => {
|
||||
actions.setProp(selectedId, (props: any) => {
|
||||
@@ -204,6 +228,79 @@ export const PricingStylePanel: React.FC<StylePanelProps> = ({ selectedId, nodeP
|
||||
{/* 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; })} />
|
||||
{/* 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);
|
||||
}}
|
||||
/>
|
||||
</CollapsibleSection>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user