From 8bf525c6009b5cca70949dcd8b93cd28507aa36b Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 14 Jul 2026 06:47:58 -0700 Subject: [PATCH] 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) --- .../src/components/basic/Icon.toHtml.test.ts | 26 +++ craft/src/components/basic/Icon.tsx | 10 ++ .../basic/SocialLinks.toHtml.test.ts | 40 +++++ craft/src/components/basic/SocialLinks.tsx | 10 ++ .../basic/StarRating.toHtml.test.ts | 11 ++ craft/src/components/basic/StarRating.tsx | 10 ++ .../sections/Accordion.toHtml.test.ts | 38 +++++ craft/src/components/sections/Accordion.tsx | 10 ++ .../sections/CTASection.toHtml.test.ts | 37 +++++ craft/src/components/sections/CTASection.tsx | 10 ++ .../sections/CallToAction.toHtml.test.ts | 37 +++++ .../src/components/sections/CallToAction.tsx | 10 ++ .../sections/Countdown.toHtml.test.ts | 11 ++ craft/src/components/sections/Countdown.tsx | 10 ++ .../sections/FeaturesGrid.toHtml.test.ts | 11 ++ .../src/components/sections/FeaturesGrid.tsx | 10 ++ .../sections/NumberCounter.toHtml.test.ts | 11 ++ .../src/components/sections/NumberCounter.tsx | 10 ++ .../sections/PricingTable.toHtml.test.ts | 105 ++++++++++++ .../src/components/sections/PricingTable.tsx | 86 ++++++++-- .../components/sections/Tabs.toHtml.test.ts | 11 ++ craft/src/components/sections/Tabs.tsx | 10 ++ .../sections/Testimonials.toHtml.test.ts | 11 ++ .../src/components/sections/Testimonials.tsx | 10 ++ .../panels/right/styles/PricingStylePanel.tsx | 97 +++++++++++ .../panels/right/styles/SectionTypePanel.tsx | 92 ++++++++++- .../panels/right/styles/SocialStylePanel.tsx | 155 ++++++++++++++++++ 27 files changed, 873 insertions(+), 16 deletions(-) create mode 100644 craft/src/components/sections/Accordion.toHtml.test.ts create mode 100644 craft/src/components/sections/CTASection.toHtml.test.ts create mode 100644 craft/src/components/sections/CallToAction.toHtml.test.ts create mode 100644 craft/src/components/sections/PricingTable.toHtml.test.ts diff --git a/craft/src/components/basic/Icon.toHtml.test.ts b/craft/src/components/basic/Icon.toHtml.test.ts index cac27f9..38b134d 100644 --- a/craft/src/components/basic/Icon.toHtml.test.ts +++ b/craft/src/components/basic/Icon.toHtml.test.ts @@ -44,3 +44,29 @@ describe('Icon.toHtml XSS hardening', () => { expect(html).not.toMatch(/"\s+onclick="/); }); }); + +describe('Icon.toHtml bgShape/bgColor/bgSize rendering (built but, until this panel update, unexposed)', () => { + test('bgShape="circle" + bgColor render a colored 50%-radius background box', () => { + const { html } = toHtml({ icon: 'fa-star', bgShape: 'circle', bgColor: '#3b82f6', bgSize: '64px' }, ''); + expect(html).toContain('background-color:#3b82f6'); + expect(html).toContain('border-radius:50%'); + expect(html).toContain('width:64px'); + expect(html).toContain('height:64px'); + }); + + test('bgShape="none" (default) renders the bare icon with no background wrapper', () => { + const { html } = toHtml({ icon: 'fa-star' }, ''); + expect(html).not.toContain('background-color'); + }); +}); + +describe('Icon.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (Icon as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/basic/Icon.tsx b/craft/src/components/basic/Icon.tsx index b36adb5..ce11185 100644 --- a/craft/src/components/basic/Icon.tsx +++ b/craft/src/components/basic/Icon.tsx @@ -12,6 +12,11 @@ interface IconProps { bgSize?: string; link?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } function getBgBorderRadius(shape: string): string { @@ -96,6 +101,11 @@ Icon.craft = { bgSize: '56px', link: '', style: {}, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/basic/SocialLinks.toHtml.test.ts b/craft/src/components/basic/SocialLinks.toHtml.test.ts index 85aa97a..6eb82b2 100644 --- a/craft/src/components/basic/SocialLinks.toHtml.test.ts +++ b/craft/src/components/basic/SocialLinks.toHtml.test.ts @@ -51,3 +51,43 @@ describe('SocialLinks.toHtml XSS hardening (iconSize/iconColor/iconBgColor/gap i expect(html).not.toContain('javascript:alert(1)'); }); }); + +describe('SocialLinks.toHtml iconShape/gap emission (previously built but unexposed in SocialStylePanel)', () => { + test('iconShape="circle" emits a 50% border-radius on the wrapping ', () => { + const { html } = toHtml({ links: [{ platform: 'facebook', url: '#' }], iconShape: 'circle' }, ''); + expect(html).toMatch(/]*style="[^"]*border-radius:50%[^"]*"/); + }); + + test('iconShape="square" emits border-radius:0', () => { + const { html } = toHtml({ links: [{ platform: 'facebook', url: '#' }], iconShape: 'square' }, ''); + expect(html).toMatch(/]*style="[^"]*border-radius:0[^"]*"/); + }); + + test('iconShape="rounded" emits a 6px border-radius', () => { + const { html } = toHtml({ links: [{ platform: 'facebook', url: '#' }], iconShape: 'rounded' }, ''); + expect(html).toMatch(/]*style="[^"]*border-radius:6px[^"]*"/); + }); + + test('iconShape="none" omits the background box entirely (transparent bg, no fixed box size)', () => { + const { html } = toHtml({ links: [{ platform: 'facebook', url: '#' }], iconShape: 'none' }, ''); + const aTag = html.match(/]*>/)![0]; + expect(aTag).toContain('background-color:transparent'); + expect(aTag).not.toContain('border-radius'); + }); + + test('gap emits on the wrapper
style', () => { + const { html } = toHtml({ links: [{ platform: 'facebook', url: '#' }], gap: '24px' }, ''); + expect(html).toMatch(/]*style="[^"]*gap:24px[^"]*"/); + }); +}); + +describe('SocialLinks.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (SocialLinks as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/basic/SocialLinks.tsx b/craft/src/components/basic/SocialLinks.tsx index a92b046..d13585c 100644 --- a/craft/src/components/basic/SocialLinks.tsx +++ b/craft/src/components/basic/SocialLinks.tsx @@ -17,6 +17,11 @@ interface SocialLinksProps { gap?: string; alignment?: 'left' | 'center' | 'right'; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const platformIcons: Record = { @@ -151,6 +156,11 @@ SocialLinks.craft = { gap: '10px', alignment: 'center', style: {}, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/basic/StarRating.toHtml.test.ts b/craft/src/components/basic/StarRating.toHtml.test.ts index 632259f..680c256 100644 --- a/craft/src/components/basic/StarRating.toHtml.test.ts +++ b/craft/src/components/basic/StarRating.toHtml.test.ts @@ -75,3 +75,14 @@ describe('StarRating.toHtml XSS hardening (rating/maxStars into aria-label, F2.2 expect(html).toMatch(/ { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (StarRating as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/basic/StarRating.tsx b/craft/src/components/basic/StarRating.tsx index 58829ba..f678ff3 100644 --- a/craft/src/components/basic/StarRating.tsx +++ b/craft/src/components/basic/StarRating.tsx @@ -10,6 +10,11 @@ interface StarRatingProps { filledColor?: string; emptyColor?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } export const StarRating: UserComponent = ({ @@ -87,6 +92,11 @@ StarRating.craft = { filledColor: '#f59e0b', emptyColor: '#d1d5db', style: {}, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/Accordion.toHtml.test.ts b/craft/src/components/sections/Accordion.toHtml.test.ts new file mode 100644 index 0000000..6d31080 --- /dev/null +++ b/craft/src/components/sections/Accordion.toHtml.test.ts @@ -0,0 +1,38 @@ +import { describe, test, expect } from 'vitest'; +import { Accordion } from './Accordion'; + +const toHtml = (Accordion as any).toHtml; + +const items = [ + { title: 'Q1', content: 'A1', isOpen: true }, + { title: 'Q2', content: 'A2', isOpen: false }, +]; + +describe('Accordion.toHtml basic export', () => { + test('renders a
per item with the open attribute honored', () => { + const { html } = toHtml({ items }, ''); + const detailsBlocks = html.match(/]*>/g) || []; + expect(detailsBlocks.length).toBe(2); + expect(detailsBlocks[0]).toContain(' open'); + expect(detailsBlocks[1]).not.toContain(' open'); + }); + + test('headerBg/headerColor/contentBg/borderColor emit into the panel styles', () => { + const { html } = toHtml({ items, headerBg: '#111111', headerColor: '#222222', contentBg: '#333333', borderColor: '#444444' }, ''); + expect(html).toContain('#111111'); + expect(html).toContain('#222222'); + expect(html).toContain('#333333'); + expect(html).toContain('#444444'); + }); +}); + +describe('Accordion.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (Accordion as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/Accordion.tsx b/craft/src/components/sections/Accordion.tsx index 02cbdba..d21112a 100644 --- a/craft/src/components/sections/Accordion.tsx +++ b/craft/src/components/sections/Accordion.tsx @@ -17,6 +17,11 @@ interface AccordionProps { contentBg?: string; borderColor?: string; anchorId?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultItems: AccordionItem[] = [ @@ -135,6 +140,11 @@ Accordion.craft = { contentBg: '#ffffff', borderColor: '#e2e8f0', anchorId: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/CTASection.toHtml.test.ts b/craft/src/components/sections/CTASection.toHtml.test.ts new file mode 100644 index 0000000..8c6f2bc --- /dev/null +++ b/craft/src/components/sections/CTASection.toHtml.test.ts @@ -0,0 +1,37 @@ +import { describe, test, expect } from 'vitest'; +import { CTASection } from './CTASection'; + +const toHtml = (CTASection as any).toHtml; + +describe('CTASection.toHtml basic export', () => { + test('renders heading, description, and CTA buttons', () => { + const { html } = toHtml({ heading: 'Hi', description: 'Sub', ctas: [{ text: 'Go', href: '#', variant: 'primary' }] }, ''); + expect(html).toContain('Hi'); + expect(html).toContain('Sub'); + expect(html).toContain('Go'); + }); + + test('box-model style props (margin/padding/border/boxShadow/opacity) flow through to the section style=""', () => { + const { html } = toHtml({ + heading: 'Hi', + description: 'Sub', + style: { marginTop: '10px', paddingLeft: '5px', border: '1px solid #000', boxShadow: '0 1px 2px rgba(0,0,0,0.1)', opacity: '0.5' }, + }, ''); + expect(html).toContain('margin-top:10px'); + expect(html).toContain('padding-left:5px'); + expect(html).toContain('border:1px solid #000'); + expect(html).toContain('box-shadow:0 1px 2px rgba(0,0,0,0.1)'); + expect(html).toContain('opacity:0.5'); + }); +}); + +describe('CTASection.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (CTASection as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/CTASection.tsx b/craft/src/components/sections/CTASection.tsx index 080b6d6..258a7d7 100644 --- a/craft/src/components/sections/CTASection.tsx +++ b/craft/src/components/sections/CTASection.tsx @@ -14,6 +14,11 @@ interface CTASectionProps { gradient?: string; anchorId?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)'; @@ -83,6 +88,11 @@ CTASection.craft = { gradient: defaultGradient, anchorId: '', style: {}, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/CallToAction.toHtml.test.ts b/craft/src/components/sections/CallToAction.toHtml.test.ts new file mode 100644 index 0000000..9558445 --- /dev/null +++ b/craft/src/components/sections/CallToAction.toHtml.test.ts @@ -0,0 +1,37 @@ +import { describe, test, expect } from 'vitest'; +import { CallToAction } from './CallToAction'; + +const toHtml = (CallToAction as any).toHtml; + +describe('CallToAction.toHtml basic export', () => { + test('renders heading, description, and CTA buttons', () => { + const { html } = toHtml({ heading: 'Hi', description: 'Sub', ctas: [{ text: 'Go', href: '#', variant: 'primary' }] }, ''); + expect(html).toContain('Hi'); + expect(html).toContain('Sub'); + expect(html).toContain('Go'); + }); + + test('box-model style props (margin/padding/border/boxShadow/opacity) flow through to the section style=""', () => { + const { html } = toHtml({ + heading: 'Hi', + description: 'Sub', + style: { marginBottom: '12px', paddingRight: '6px', border: '2px dashed #333', boxShadow: '0 4px 8px rgba(0,0,0,0.12)', opacity: '0.75' }, + }, ''); + expect(html).toContain('margin-bottom:12px'); + expect(html).toContain('padding-right:6px'); + expect(html).toContain('border:2px dashed #333'); + expect(html).toContain('box-shadow:0 4px 8px rgba(0,0,0,0.12)'); + expect(html).toContain('opacity:0.75'); + }); +}); + +describe('CallToAction.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (CallToAction as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/CallToAction.tsx b/craft/src/components/sections/CallToAction.tsx index 4489bb1..d2b103e 100644 --- a/craft/src/components/sections/CallToAction.tsx +++ b/craft/src/components/sections/CallToAction.tsx @@ -21,6 +21,11 @@ interface CallToActionProps { buttonColor?: string; anchorId?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)'; @@ -131,6 +136,11 @@ CallToAction.craft = { textColor: '#ffffff', buttonColor: '#ffffff', style: {}, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/Countdown.toHtml.test.ts b/craft/src/components/sections/Countdown.toHtml.test.ts index b7544b9..dfc3f00 100644 --- a/craft/src/components/sections/Countdown.toHtml.test.ts +++ b/craft/src/components/sections/Countdown.toHtml.test.ts @@ -62,3 +62,14 @@ describe('Countdown.toHtml script nit: ticking interval stops at zero', () => { expect(html).toMatch(/if\s*\(\s*target\s*-\s*Date\.now\(\)\s*>\s*0\s*\)\s*\{/); }); }); + +describe('Countdown.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (Countdown as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/Countdown.tsx b/craft/src/components/sections/Countdown.tsx index 7c4400a..ea4d00b 100644 --- a/craft/src/components/sections/Countdown.tsx +++ b/craft/src/components/sections/Countdown.tsx @@ -11,6 +11,11 @@ interface CountdownProps { labelColor?: string; bgColor?: string; anchorId?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } interface TimeLeft { @@ -139,6 +144,11 @@ Countdown.craft = { labelColor: 'rgba(255,255,255,0.7)', bgColor: '#18181b', anchorId: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/FeaturesGrid.toHtml.test.ts b/craft/src/components/sections/FeaturesGrid.toHtml.test.ts index db72fe5..73c520c 100644 --- a/craft/src/components/sections/FeaturesGrid.toHtml.test.ts +++ b/craft/src/components/sections/FeaturesGrid.toHtml.test.ts @@ -21,3 +21,14 @@ describe('FeaturesGrid.toHtml image sink uses safeImageUrl (data:image/svg+xml a expect(html).toMatch(/ { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (FeaturesGrid as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/FeaturesGrid.tsx b/craft/src/components/sections/FeaturesGrid.tsx index 72cbb8f..c8176be 100644 --- a/craft/src/components/sections/FeaturesGrid.tsx +++ b/craft/src/components/sections/FeaturesGrid.tsx @@ -17,6 +17,11 @@ interface FeaturesGridProps { features?: FeatureItem[]; style?: CSSProperties; anchorId?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } // Keys image/imageAlt/buttonText/buttonUrl are present (blank) on the defaults so @@ -98,6 +103,11 @@ FeaturesGrid.craft = { features: defaultFeatures, style: { backgroundColor: '#ffffff' }, anchorId: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/NumberCounter.toHtml.test.ts b/craft/src/components/sections/NumberCounter.toHtml.test.ts index 869018c..e20a5e3 100644 --- a/craft/src/components/sections/NumberCounter.toHtml.test.ts +++ b/craft/src/components/sections/NumberCounter.toHtml.test.ts @@ -73,3 +73,14 @@ describe('NumberCounter.toHtml counter.number is NOT runtime-type-checked -- mus expect(html).toContain('data-target="150"'); }); }); + +describe('NumberCounter.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (NumberCounter as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/NumberCounter.tsx b/craft/src/components/sections/NumberCounter.tsx index 26eed26..81fee8c 100644 --- a/craft/src/components/sections/NumberCounter.tsx +++ b/craft/src/components/sections/NumberCounter.tsx @@ -16,6 +16,11 @@ interface NumberCounterProps { labelColor?: string; numberSize?: string; style?: CSSProperties; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultCounters: Counter[] = [ @@ -99,6 +104,11 @@ NumberCounter.craft = { labelColor: '#6b7280', numberSize: '48px', style: { backgroundColor: '#ffffff' }, + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/PricingTable.toHtml.test.ts b/craft/src/components/sections/PricingTable.toHtml.test.ts new file mode 100644 index 0000000..52463cb --- /dev/null +++ b/craft/src/components/sections/PricingTable.toHtml.test.ts @@ -0,0 +1,105 @@ +import { describe, test, expect } from 'vitest'; +import { PricingTable } from './PricingTable'; + +const toHtml = (PricingTable as any).toHtml; + +const plans = [ + { name: 'Basic', price: '$9', period: '/month', features: ['Feature A'], buttonText: 'Buy', buttonHref: '#', isFeatured: false }, + { name: 'Pro', price: '$29', period: '/month', features: ['Feature B'], buttonText: 'Buy', buttonHref: '#', isFeatured: true }, +]; + +describe('PricingTable.toHtml regular-card color overrides (previously hard-coded, now real props)', () => { + test('cardBg emits as the non-featured card background', () => { + const { html } = toHtml({ plans, cardBg: '#f0f0f0' }, ''); + expect(html).toContain('background-color:#f0f0f0'); + }); + + test('textColor emits as the non-featured heading/price color', () => { + const { html } = toHtml({ plans, textColor: '#123456' }, ''); + expect(html).toContain('color:#123456'); + }); + + test('subColor emits as the non-featured period text color', () => { + const { html } = toHtml({ plans, subColor: '#abcdef' }, ''); + expect(html).toContain('color:#abcdef'); + }); + + test('featColor emits as the non-featured feature list text color', () => { + const { html } = toHtml({ plans, featColor: '#334455' }, ''); + expect(html).toContain('color:#334455'); + }); + + test('checkColor emits as the non-featured bullet color', () => { + const { html } = toHtml({ plans, checkColor: '#00ff00' }, ''); + expect(html).toContain('color:#00ff00'); + }); + + test('btnBg/btnColor emit as the non-featured button colors', () => { + const { html } = toHtml({ plans, btnBg: '#111111', btnColor: '#eeeeee' }, ''); + expect(html).toContain('background-color:#111111'); + expect(html).toContain('color:#eeeeee'); + }); + + test('unset overrides fall back to the exact prior literals (backward compatible)', () => { + const { html } = toHtml({ plans }, ''); + // Regular (non-featured) card literals unchanged from before these props existed. + expect(html).toContain('background-color:#ffffff'); + expect(html).toContain('color:#18181b'); + expect(html).toContain('color:#64748b'); + expect(html).toContain('color:#4b5563'); + expect(html).toContain('color:#10b981'); + }); + + test('unset btnBg falls back to featuredBg (original derivation)', () => { + const { html } = toHtml({ plans, featuredBg: '#654321' }, ''); + expect(html).toContain('background-color:#654321'); + }); +}); + +describe('PricingTable.toHtml XSS hardening (new card color props into style=)', () => { + test('a cardBg breakout string cannot escape style=""', () => { + const malicious = '#fff" onmouseover="alert(1)'; + const { html } = toHtml({ plans, cardBg: malicious }, ''); + expect(html).not.toMatch(/"\s+onmouseover="/); + }); + + test('a textColor breakout string cannot escape style=""', () => { + const malicious = '#000" onmouseover="alert(1)'; + const { html } = toHtml({ plans, textColor: malicious }, ''); + expect(html).not.toMatch(/"\s+onmouseover="/); + }); + + test('a btnBg breakout string cannot escape style=""', () => { + const malicious = '#000" onmouseover="alert(1)'; + const { html } = toHtml({ plans, btnBg: malicious }, ''); + expect(html).not.toMatch(/"\s+onmouseover="/); + }); + + test('a btnColor breakout string cannot escape style=""', () => { + const malicious = '#000" onmouseover="alert(1)'; + const { html } = toHtml({ plans, btnColor: malicious }, ''); + expect(html).not.toMatch(/"\s+onmouseover="/); + }); +}); + +describe('PricingTable.craft.props includes the box-model/animation/visibility rollout props', () => { + test('the new regular-card color props are declared blank by default', () => { + const props = (PricingTable as any).craft.props; + expect(props).toHaveProperty('cardBg', ''); + expect(props).toHaveProperty('textColor', ''); + expect(props).toHaveProperty('subColor', ''); + expect(props).toHaveProperty('featColor', ''); + expect(props).toHaveProperty('checkColor', ''); + expect(props).toHaveProperty('btnBg', ''); + expect(props).toHaveProperty('btnColor', ''); + }); + + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (PricingTable as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/PricingTable.tsx b/craft/src/components/sections/PricingTable.tsx index c4f4ecb..986bc1b 100644 --- a/craft/src/components/sections/PricingTable.tsx +++ b/craft/src/components/sections/PricingTable.tsx @@ -19,6 +19,23 @@ interface PricingTableProps { featuredBg?: string; bulletType?: string; anchorId?: string; + /* ---- Regular (non-featured) card colors ---- + All optional; each falls back to the exact literal the card was + previously hard-coded to (or, for the button, to featuredBg -- the + button's original derivation) when left unset, so existing saved + projects render pixel-identical until a color is explicitly picked. */ + cardBg?: string; + textColor?: string; + subColor?: string; + featColor?: string; + checkColor?: string; + btnBg?: string; + btnColor?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const bulletChars: Record = { @@ -61,6 +78,13 @@ export const PricingTable: UserComponent = ({ featuredBg = '#3b82f6', bulletType = 'check', anchorId, + cardBg, + textColor, + subColor, + featColor, + checkColor, + btnBg, + btnColor, }) => { const { connectors: { connect, drag }, @@ -69,6 +93,14 @@ export const PricingTable: UserComponent = ({ selected: node.events.selected, })); + const regCardBg = cardBg || '#ffffff'; + const regTextColor = textColor || '#18181b'; + const regSubColor = subColor || '#64748b'; + const regFeatColor = featColor || '#4b5563'; + const regCheckColor = checkColor || '#10b981'; + const regBtnBg = btnBg || featuredBg; + const regBtnColor = btnColor || '#ffffff'; + return (
{ if (ref) connect(drag(ref)); }} @@ -95,7 +127,7 @@ export const PricingTable: UserComponent = ({ style={{ flex: '1 1 280px', maxWidth: '360px', - backgroundColor: plan.isFeatured ? featuredBg : '#ffffff', + backgroundColor: plan.isFeatured ? featuredBg : regCardBg, border: plan.isFeatured ? 'none' : '1px solid #e2e8f0', borderRadius: '16px', padding: '40px 32px', @@ -127,7 +159,7 @@ export const PricingTable: UserComponent = ({

@@ -137,14 +169,14 @@ export const PricingTable: UserComponent = ({ {plan.price} {plan.period} @@ -161,12 +193,12 @@ export const PricingTable: UserComponent = ({ {(Array.isArray(plan.features) ? plan.features : []).map((feature, fi) => (
  • - {bulletChars[bulletType] || '✓'} + {bulletChars[bulletType] || '✓'} {feature}
  • ))} @@ -178,8 +210,8 @@ export const PricingTable: UserComponent = ({ marginTop: 'auto', display: 'inline-block', padding: '14px 32px', - backgroundColor: plan.isFeatured ? '#ffffff' : featuredBg, - color: plan.isFeatured ? featuredBg : '#ffffff', + backgroundColor: plan.isFeatured ? '#ffffff' : regBtnBg, + color: plan.isFeatured ? featuredBg : regBtnColor, textDecoration: 'none', borderRadius: '8px', fontWeight: '600', @@ -207,6 +239,18 @@ PricingTable.craft = { featuredBg: '#3b82f6', bulletType: 'check', anchorId: '', + cardBg: '', + textColor: '', + subColor: '', + featColor: '', + checkColor: '', + btnBg: '', + btnColor: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, @@ -228,16 +272,28 @@ PricingTable.craft = { // Sanitized -- featuredBg is a raw string-interpolation sink below (drives // cardBg/btnBg/btnColor, all raw-interpolated into style="..."). const featuredBg = cssValue(props.featuredBg) || '#3b82f6'; + // Sanitized -- regular (non-featured) card color overrides, all raw + // string-interpolation sinks into style="..." below. Each falls back to + // the exact literal the card was previously hard-coded to (or, for the + // button, to featuredBg) when unset, so unmodified pricing tables render + // identically to before these props existed. + const regCardBg = cssValue(props.cardBg) || '#ffffff'; + const regTextColor = cssValue(props.textColor) || '#18181b'; + const regSubColor = cssValue(props.subColor) || '#64748b'; + const regFeatColor = cssValue(props.featColor) || '#4b5563'; + const regCheckColor = cssValue(props.checkColor) || '#10b981'; + const regBtnBg = cssValue(props.btnBg) || featuredBg; + const regBtnColor = cssValue(props.btnColor) || '#ffffff'; const cards = plans.map((plan) => { - const cardBg = plan.isFeatured ? featuredBg : '#ffffff'; + const cardBg = plan.isFeatured ? featuredBg : regCardBg; const cardBorder = plan.isFeatured ? 'border:none;' : 'border:1px solid #e2e8f0;'; - const textColor = plan.isFeatured ? '#ffffff' : '#18181b'; - const subColor = plan.isFeatured ? 'rgba(255,255,255,0.8)' : '#64748b'; - const featColor = plan.isFeatured ? 'rgba(255,255,255,0.9)' : '#4b5563'; - const checkColor = plan.isFeatured ? '#bbf7d0' : '#10b981'; - const btnBg = plan.isFeatured ? '#ffffff' : featuredBg; - const btnColor = plan.isFeatured ? featuredBg : '#ffffff'; + const textColor = plan.isFeatured ? '#ffffff' : regTextColor; + const subColor = plan.isFeatured ? 'rgba(255,255,255,0.8)' : regSubColor; + const featColor = plan.isFeatured ? 'rgba(255,255,255,0.9)' : regFeatColor; + const checkColor = plan.isFeatured ? '#bbf7d0' : regCheckColor; + const btnBg = plan.isFeatured ? '#ffffff' : regBtnBg; + const btnColor = plan.isFeatured ? featuredBg : regBtnColor; const scale = plan.isFeatured ? 'transform:scale(1.05);' : ''; const shadow = plan.isFeatured ? 'box-shadow:0 20px 60px rgba(59,130,246,0.3);' : 'box-shadow:0 1px 3px rgba(0,0,0,0.06);'; diff --git a/craft/src/components/sections/Tabs.toHtml.test.ts b/craft/src/components/sections/Tabs.toHtml.test.ts index 399ef46..807f3dc 100644 --- a/craft/src/components/sections/Tabs.toHtml.test.ts +++ b/craft/src/components/sections/Tabs.toHtml.test.ts @@ -80,3 +80,14 @@ describe('Tabs.toHtml deterministic + unique ids (thread node id, resolves id-co expect(html1).toBe(html2); }); }); + +describe('Tabs.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (Tabs as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/Tabs.tsx b/craft/src/components/sections/Tabs.tsx index 6dc04d7..1cce208 100644 --- a/craft/src/components/sections/Tabs.tsx +++ b/craft/src/components/sections/Tabs.tsx @@ -17,6 +17,11 @@ interface TabsProps { inactiveTabColor?: string; contentBg?: string; anchorId?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultTabs: TabItem[] = [ @@ -114,6 +119,11 @@ Tabs.craft = { inactiveTabColor: '#64748b', contentBg: '#ffffff', anchorId: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/components/sections/Testimonials.toHtml.test.ts b/craft/src/components/sections/Testimonials.toHtml.test.ts index f721139..fb083d6 100644 --- a/craft/src/components/sections/Testimonials.toHtml.test.ts +++ b/craft/src/components/sections/Testimonials.toHtml.test.ts @@ -71,3 +71,14 @@ describe('Testimonials.toHtml rating aria-label sink (attacker-controlled `ratin expect(html).toContain('aria-label="Rating: 4 out of 5"'); }); }); + +describe('Testimonials.craft.props includes the box-model/animation/visibility rollout props', () => { + test('animation, animationDelay, and all 3 hideOn* flags are declared (blank/false defaults)', () => { + const props = (Testimonials as any).craft.props; + expect(props).toHaveProperty('animation', ''); + expect(props).toHaveProperty('animationDelay', ''); + expect(props).toHaveProperty('hideOnDesktop', false); + expect(props).toHaveProperty('hideOnTablet', false); + expect(props).toHaveProperty('hideOnMobile', false); + }); +}); diff --git a/craft/src/components/sections/Testimonials.tsx b/craft/src/components/sections/Testimonials.tsx index 0308aec..68db73b 100644 --- a/craft/src/components/sections/Testimonials.tsx +++ b/craft/src/components/sections/Testimonials.tsx @@ -18,6 +18,11 @@ interface TestimonialsProps { cardBg?: string; starColor?: string; anchorId?: string; + animation?: string; + animationDelay?: string; + hideOnDesktop?: boolean; + hideOnTablet?: boolean; + hideOnMobile?: boolean; } const defaultTestimonials: Testimonial[] = [ @@ -131,6 +136,11 @@ Testimonials.craft = { cardBg: '#f8fafc', starColor: '#f59e0b', anchorId: '', + animation: '', + animationDelay: '', + hideOnDesktop: false, + hideOnTablet: false, + hideOnMobile: false, }, rules: { canDrag: () => true, diff --git a/craft/src/panels/right/styles/PricingStylePanel.tsx b/craft/src/panels/right/styles/PricingStylePanel.tsx index 21f63e9..7cd02bf 100644 --- a/craft/src/panels/right/styles/PricingStylePanel.tsx +++ b/craft/src/panels/right/styles/PricingStylePanel.tsx @@ -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 = { export const PricingStylePanel: React.FC = ({ selectedId, nodeProps }) => { const { actions } = useEditor(); + const { setProp, setPropStyle } = useNodeProp(selectedId); const [expandedPlan, setExpandedPlan] = useState(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 = ({ selectedId, nodeP {/* Colors */} 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. */} + setProp('cardBg', v)} /> + setProp('textColor', v)} /> + setProp('subColor', v)} /> + setProp('featColor', v)} /> + setProp('checkColor', v)} /> + setProp('btnBg', v)} /> + setProp('btnColor', v)} /> + + + {/* Box model: margin, padding, border, shadow, opacity */} + + setPropStyle(`margin${capSide(side)}`, v)} + /> + setPropStyle(`padding${capSide(side)}`, v)} + /> + setPropStyle('border', buildBorderShorthand(v))} + /> +
    + Shadow + setPropStyle('boxShadow', v)} /> +
    +
    + Opacity + setPropStyle('opacity', String(Number(e.target.value) / 100))} + style={{ width: '100%' }} + /> +
    +
    + + {/* Entrance animation */} + + { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }} + /> + + + {/* Responsive visibility */} + + { + setProp('hideOnDesktop', !!v.hideOnDesktop); + setProp('hideOnTablet', !!v.hideOnTablet); + setProp('hideOnMobile', !!v.hideOnMobile); + }} + /> ); diff --git a/craft/src/panels/right/styles/SectionTypePanel.tsx b/craft/src/panels/right/styles/SectionTypePanel.tsx index cf2fc32..e3b8e23 100644 --- a/craft/src/panels/right/styles/SectionTypePanel.tsx +++ b/craft/src/panels/right/styles/SectionTypePanel.tsx @@ -3,6 +3,7 @@ import { BG_COLORS, SPACING_PRESETS, RADIUS_PRESETS, + SHADOW_PRESETS, } from '../../../constants/presets'; import { StylePanelProps, @@ -15,10 +16,31 @@ import { inputStyle, sectionGap, useNodeProp, + SpacingControl, + BorderControl, + BorderValue, + buildBorderShorthand, + AnimationControl, + VisibilityControl, } from './shared'; import { FeaturesEditor } from './FeaturesEditor'; import { ArrayItemFieldsEditor } from './ArrayItemFields'; +/** Parses a border shorthand string ("2px solid #hex") produced by + * buildBorderShorthand() back into its parts for round-tripping through + * BorderControl. Anything that doesn't match (undefined/'none'/legacy + * hand-authored values) falls back to an empty/none border rather than + * throwing, since this is display-only -- the next edit always re-emits a + * clean shorthand via buildBorderShorthand(). */ +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); + /* ---------- SECTION-TYPE (Accordion, Tabs, Pricing, Testimonials, etc.) ---------- */ export const SectionTypePanel: React.FC = ({ selectedId, nodeProps, typeName }) => { const { setProp, setPropStyle } = useNodeProp(selectedId); @@ -26,7 +48,12 @@ export const SectionTypePanel: React.FC const style = nodeProps.style || {}; // Find all string/number/boolean props - const SKIP_PROPS = new Set(['style', 'children', 'cssId', 'cssClass']); + const SKIP_PROPS = new Set([ + 'style', 'children', 'cssId', 'cssClass', + // Rendered via dedicated controls below (Animation/Visibility), not the + // generic Content field dump -- otherwise they'd show up twice. + 'animation', 'animationDelay', 'hideOnDesktop', 'hideOnTablet', 'hideOnMobile', + ]); const scalarProps = Object.entries(nodeProps).filter( ([key, val]) => !SKIP_PROPS.has(key) && (typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean') ); @@ -126,6 +153,69 @@ export const SectionTypePanel: React.FC setPropStyle('borderRadius', v)} />

    + + {/* Box model: margin, padding, border, shadow, opacity */} + + setPropStyle(`margin${capSide(side)}`, v)} + /> + setPropStyle(`padding${capSide(side)}`, v)} + /> + setPropStyle('border', buildBorderShorthand(v))} + /> +
    + Shadow + setPropStyle('boxShadow', v)} /> +
    +
    + Opacity + setPropStyle('opacity', String(Number(e.target.value) / 100))} + style={{ width: '100%' }} + /> +
    +
    + + {/* Entrance animation */} + + { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }} + /> + + + {/* Responsive visibility */} + + { + setProp('hideOnDesktop', !!v.hideOnDesktop); + setProp('hideOnTablet', !!v.hideOnTablet); + setProp('hideOnMobile', !!v.hideOnMobile); + }} + /> + ); }; diff --git a/craft/src/panels/right/styles/SocialStylePanel.tsx b/craft/src/panels/right/styles/SocialStylePanel.tsx index 06af9d9..52ea003 100644 --- a/craft/src/panels/right/styles/SocialStylePanel.tsx +++ b/craft/src/panels/right/styles/SocialStylePanel.tsx @@ -3,6 +3,7 @@ import { useEditor } from '@craftjs/core'; import { BG_COLORS, SPACING_PRESETS, + SHADOW_PRESETS, } from '../../../constants/presets'; import { StylePanelProps, @@ -17,8 +18,33 @@ import { 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 ICON_SHAPES = [ + { label: 'None', value: 'none' }, + { label: 'Circle', value: 'circle' }, + { label: 'Square', value: 'square' }, + { label: 'Rounded', value: 'rounded' }, +]; + /* ---------- SOCIAL / ICON / STAR RATING ---------- */ export const SocialStylePanel: React.FC = ({ selectedId, nodeProps }) => { const { actions } = useEditor(); @@ -111,6 +137,72 @@ export const SocialStylePanel: React.FC = ({ selectedId, nodePr )} + {/* Icon shape (SocialLinks) -- exists on the component (iconShape prop + drives the circle/square/rounded background box) but previously had + no control. */} + {nodeProps.iconShape !== undefined && ( +
    + +
    + {ICON_SHAPES.map((s) => ( + + ))} +
    +
    + )} + {/* Gap between links (SocialLinks) -- exists on the component but + previously had no control. */} + {nodeProps.gap !== undefined && ( +
    + + setProp('gap', e.target.value)} placeholder="10px" style={inputStyle} /> +
    + )} + + {/* Background shape / size / link (Icon component) -- bgColor, bgShape, + bgSize, link all already exist on Icon.craft.props but had no + control; the generic `color`/`iconColor` checks above never matched + Icon's actual prop names. */} + {nodeProps.bgShape !== undefined && ( +
    + +
    + {ICON_SHAPES.map((s) => ( + + ))} +
    +
    + )} + {nodeProps.bgColor !== undefined && ( + setProp('bgColor', v)} /> + )} + {nodeProps.bgSize !== undefined && ( +
    + + setProp('bgSize', e.target.value)} placeholder="56px" style={inputStyle} /> +
    + )} + {nodeProps.link !== undefined && ( +
    + + setProp('link', e.target.value)} placeholder="https://..." style={inputStyle} /> +
    + )} + + {/* Star colors (StarRating) -- exist on the component as filledColor / + emptyColor but had no control (the generic `starColor` check above + never matched StarRating's actual prop names). */} + {nodeProps.filledColor !== undefined && ( + setProp('filledColor', v)} /> + )} + {nodeProps.emptyColor !== undefined && ( + setProp('emptyColor', v)} /> + )} + {/* Colors */} {nodeProps.iconColor !== undefined && ( setProp('iconColor', v)} /> @@ -164,6 +256,69 @@ export const SocialStylePanel: React.FC = ({ selectedId, nodePr setPropStyle('padding', v)} /> + + {/* Box model: margin, padding (per-side), border, shadow, opacity */} + + setPropStyle(`margin${capSide(side)}`, v)} + /> + setPropStyle(`padding${capSide(side)}`, v)} + /> + setPropStyle('border', buildBorderShorthand(v))} + /> +
    + Shadow + setPropStyle('boxShadow', v)} /> +
    +
    + Opacity + setPropStyle('opacity', String(Number(e.target.value) / 100))} + style={{ width: '100%' }} + /> +
    +
    + + {/* Entrance animation */} + + { setProp('animation', v.animation); setProp('animationDelay', v.animationDelay); }} + /> + + + {/* Responsive visibility */} + + { + setProp('hideOnDesktop', !!v.hideOnDesktop); + setProp('hideOnTablet', !!v.hideOnTablet); + setProp('hideOnMobile', !!v.hideOnMobile); + }} + /> + ); }; -- 2.52.0