diff --git a/craft/src/App.tsx b/craft/src/App.tsx index aee8e4a..323ba61 100644 --- a/craft/src/App.tsx +++ b/craft/src/App.tsx @@ -6,18 +6,29 @@ import { WhpConfig } from './types'; import { EditorConfigProvider } from './state/EditorConfigContext'; import { SiteDesignProvider } from './state/SiteDesignContext'; import { PageProvider } from './state/PageContext'; +import { SitesmithProvider, useSitesmithModal } from './state/SitesmithContext'; +import { SitesmithModal } from './panels/sitesmith/SitesmithModal'; interface AppProps { whpConfig: WhpConfig | null; } +const SitesmithModalMount: React.FC = () => { + const { isOpen, target, close } = useSitesmithModal(); + if (!isOpen) return null; + return ; +}; + export const App: React.FC = ({ whpConfig }) => { return ( - + + + + diff --git a/craft/src/components/layout/BackgroundSection.tsx b/craft/src/components/layout/BackgroundSection.tsx index 03e1ef2..bffb039 100644 --- a/craft/src/components/layout/BackgroundSection.tsx +++ b/craft/src/components/layout/BackgroundSection.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, Element, UserComponent } from '@craftjs/core'; import { Container } from './Container'; import { cssPropsToString } from '../../utils/style-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface BackgroundSectionProps { bgImage?: string; @@ -11,6 +12,7 @@ interface BackgroundSectionProps { innerMaxWidth?: string; style?: CSSProperties; children?: React.ReactNode; + anchorId?: string; } export const BackgroundSection: UserComponent = ({ @@ -20,12 +22,14 @@ export const BackgroundSection: UserComponent = ({ overlayOpacity = 0.4, innerMaxWidth = '1200px', style = {}, + anchorId, }) => { const { connectors: { connect, drag } } = useNode(); return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ position: 'relative', width: '100%', @@ -77,6 +81,7 @@ const BackgroundSectionSettings: React.FC = () => { return (
+
true, @@ -176,6 +182,7 @@ BackgroundSection.craft = { /* ---------- HTML export ---------- */ (BackgroundSection as any).toHtml = (props: BackgroundSectionProps, childrenHtml: string) => { + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const outerStyle = cssPropsToString({ position: 'relative', width: '100%', @@ -200,7 +207,8 @@ BackgroundSection.craft = { margin: '0 auto', padding: '60px 20px', }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; return { - html: `
${childrenHtml}
`, + html: `${childrenHtml}`, }; }; diff --git a/craft/src/components/layout/ColumnLayout.tsx b/craft/src/components/layout/ColumnLayout.tsx index 22306b3..e9e1bac 100644 --- a/craft/src/components/layout/ColumnLayout.tsx +++ b/craft/src/components/layout/ColumnLayout.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties, useState } from 'react'; import { useNode, Element, UserComponent } from '@craftjs/core'; import { Container } from './Container'; import { cssPropsToString } from '../../utils/style-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; type SplitOption = | '100' @@ -18,6 +19,7 @@ interface ColumnLayoutProps { gap?: string; style?: CSSProperties; children?: React.ReactNode; + anchorId?: string; } const splitToWidths: Record = { @@ -59,6 +61,7 @@ export const ColumnLayout: UserComponent = ({ split = '50-50', gap = '16px', style = {}, + anchorId, }) => { const { connectors: { connect, drag } } = useNode(); const widths = getWidths(split, columns); @@ -66,6 +69,7 @@ export const ColumnLayout: UserComponent = ({ return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ display: 'flex', flexWrap: 'wrap', @@ -124,6 +128,7 @@ const ColumnLayoutSettings: React.FC = () => { return (
+ {/* Preset layouts */}
@@ -270,6 +275,7 @@ ColumnLayout.craft = { split: '50-50', gap: '16px', style: {}, + anchorId: '', }, rules: { canDrag: () => true, @@ -284,6 +290,7 @@ ColumnLayout.craft = { /* ---------- HTML export ---------- */ (ColumnLayout as any).toHtml = (props: ColumnLayoutProps, childrenHtml: string) => { + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const gap = props.gap || '16px'; const outerStyle = cssPropsToString({ display: 'flex', @@ -292,7 +299,8 @@ ColumnLayout.craft = { width: '100%', ...props.style, }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; return { - html: `${childrenHtml}
`, + html: `${childrenHtml}
`, }; }; diff --git a/craft/src/components/layout/Container.tsx b/craft/src/components/layout/Container.tsx index 7a2e7b9..b623ee7 100644 --- a/craft/src/components/layout/Container.tsx +++ b/craft/src/components/layout/Container.tsx @@ -4,6 +4,7 @@ import { cssPropsToString } from '../../utils/style-helpers'; import { SettingsTabs } from '../../ui/SettingsTabs'; import { BorderControl } from '../../ui/BorderControl'; import { AdvancedTab } from '../../ui/AdvancedTab'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface ContainerProps { style?: CSSProperties; @@ -11,6 +12,7 @@ interface ContainerProps { children?: React.ReactNode; cssId?: string; cssClass?: string; + anchorId?: string; hideOnDesktop?: boolean; hideOnTablet?: boolean; hideOnMobile?: boolean; @@ -37,6 +39,7 @@ export const Container: UserComponent = ({ children, fullWidth = false, contentWidth = 'full', + anchorId, }) => { const { connectors: { connect, drag } } = useNode(); @@ -56,6 +59,7 @@ export const Container: UserComponent = ({ ref: (ref: HTMLElement | null): void => { if (ref) connect(drag(ref)); }, style: outerStyle, 'data-craft-container': 'true', + id: anchorId || undefined, }, needsBoxedWrapper ? React.createElement('div', { style: { maxWidth: '1200px', margin: '0 auto', ...flexStyles } }, children) @@ -120,6 +124,7 @@ const ContainerSettings: React.FC = () => { + {/* Tag */}
@@ -304,6 +309,7 @@ Container.craft = { tag: 'div', fullWidth: false, contentWidth: 'full', + anchorId: '', }, rules: { canDrag: () => true, @@ -318,6 +324,7 @@ Container.craft = { /* ---------- HTML export ---------- */ (Container as any).toHtml = (props: ContainerProps, childrenHtml: string) => { + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const tag = props.tag || 'div'; const isBoxed = props.contentWidth === 'boxed'; const flexStyles = flexAlignFromTextAlign(props.style?.textAlign); @@ -333,11 +340,12 @@ Container.craft = { } const styleStr = cssPropsToString(outerCss); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; if (isBoxed) { const innerStyle = cssPropsToString({ maxWidth: '1200px', margin: '0 auto', ...flexStyles }); - return { html: `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}
` }; + return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}
` }; } - return { html: `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}` }; + return { html: `<${tag}${idAttr}${styleStr ? ` style="${styleStr}"` : ''}>${childrenHtml}` }; }; diff --git a/craft/src/components/layout/Section.tsx b/craft/src/components/layout/Section.tsx index d959875..2fb13ba 100644 --- a/craft/src/components/layout/Section.tsx +++ b/craft/src/components/layout/Section.tsx @@ -2,6 +2,7 @@ import React, { CSSProperties } from 'react'; import { useNode, Element, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { Container } from './Container'; +import { AnchorIdField } from '../../ui/AnchorIdField'; /* ---------- Shape Divider SVG Paths ---------- */ @@ -27,6 +28,7 @@ interface SectionProps { bottomDivider?: DividerShape; bottomDividerColor?: string; bottomDividerHeight?: string; + anchorId?: string; } /* ---------- Divider renderer ---------- */ @@ -85,6 +87,7 @@ export const Section: UserComponent = ({ bottomDivider = 'none', bottomDividerColor = '#ffffff', bottomDividerHeight = '50px', + anchorId, }) => { const { connectors: { connect, drag } } = useNode(); @@ -94,6 +97,7 @@ export const Section: UserComponent = ({ return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ width: '100%', position: (hasTopDivider || hasBottomDivider) ? 'relative' : undefined, @@ -229,6 +233,7 @@ const SectionSettings: React.FC = () => { return (
+
@@ -333,6 +338,7 @@ Section.craft = { bottomDivider: 'none', bottomDividerColor: '#ffffff', bottomDividerHeight: '50px', + anchorId: '', }, rules: { canDrag: () => true, @@ -377,6 +383,7 @@ function buildDividerHtml( } (Section as any).toHtml = (props: SectionProps, childrenHtml: string) => { + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const hasTopDivider = props.topDivider && props.topDivider !== 'none'; const hasBottomDivider = props.bottomDivider && props.bottomDivider !== 'none'; @@ -394,8 +401,9 @@ function buildDividerHtml( const topHtml = buildDividerHtml(props.topDivider, props.topDividerColor, props.topDividerHeight, 'top'); const bottomHtml = buildDividerHtml(props.bottomDivider, props.bottomDividerColor, props.bottomDividerHeight, 'bottom'); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; return { - html: `${topHtml}${childrenHtml}
${bottomHtml}
`, + html: `${topHtml}${childrenHtml}${bottomHtml}`, }; }; diff --git a/craft/src/components/sections/Accordion.tsx b/craft/src/components/sections/Accordion.tsx index 86bb0bd..2505813 100644 --- a/craft/src/components/sections/Accordion.tsx +++ b/craft/src/components/sections/Accordion.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties, useState } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface AccordionItem { title: string; @@ -15,6 +16,7 @@ interface AccordionProps { headerColor?: string; contentBg?: string; borderColor?: string; + anchorId?: string; } const defaultItems: AccordionItem[] = [ @@ -30,6 +32,7 @@ export const Accordion: UserComponent = ({ headerColor = '#18181b', contentBg = '#ffffff', borderColor = '#e2e8f0', + anchorId, }) => { const { connectors: { connect, drag }, @@ -56,6 +59,7 @@ export const Accordion: UserComponent = ({ return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ padding: '60px 20px', backgroundColor: '#ffffff', @@ -161,6 +165,7 @@ const AccordionSettings: React.FC = () => { return (
+
@@ -279,6 +284,7 @@ Accordion.craft = { headerColor: '#18181b', contentBg: '#ffffff', borderColor: '#e2e8f0', + anchorId: '', }, rules: { canDrag: () => true, @@ -293,11 +299,12 @@ Accordion.craft = { /* ---------- HTML export ---------- */ (Accordion as any).toHtml = (props: AccordionProps, _childrenHtml: string) => { - const esc = (s: any) => String(s ?? "").replace(//g, '>'); + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const sectionStyle = cssPropsToString({ padding: '60px 20px', ...props.style, }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const headerBg = props.headerBg || '#f8fafc'; const headerColor = props.headerColor || '#18181b'; const contentBg = props.contentBg || '#ffffff'; @@ -320,7 +327,7 @@ Accordion.craft = { }).join('\n '); return { - html: ` + html: `
${panels}
diff --git a/craft/src/components/sections/CTASection.tsx b/craft/src/components/sections/CTASection.tsx index 94eb78a..c0efdb4 100644 --- a/craft/src/components/sections/CTASection.tsx +++ b/craft/src/components/sections/CTASection.tsx @@ -1,13 +1,18 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface CTASectionProps { heading?: string; description?: string; + ctas?: CtaButton[]; + /** Legacy props kept for backward compat with saved projects. */ buttonText?: string; buttonHref?: string; gradient?: string; + anchorId?: string; style?: CSSProperties; } @@ -16,9 +21,11 @@ const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)'; export const CTASection: UserComponent = ({ heading = 'Ready to Get Started?', description = 'Join thousands of satisfied users and start building your dream website today.', - buttonText = 'Start Free Trial', - buttonHref = '#', + ctas, + buttonText, + buttonHref, gradient = defaultGradient, + anchorId, style = {}, }) => { const { @@ -28,9 +35,13 @@ export const CTASection: UserComponent = ({ selected: node.events.selected, })); + const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref }); + const ctaDefaults = { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' }; + return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ background: gradient, padding: '80px 20px', @@ -46,22 +57,14 @@ export const CTASection: UserComponent = ({

{description}

- e.preventDefault()} - style={{ - display: 'inline-block', - padding: '14px 36px', - backgroundColor: '#ffffff', - color: '#18181b', - textDecoration: 'none', - borderRadius: '8px', - fontWeight: '600', - fontSize: '16px', - }} - > - {buttonText} - +
); @@ -83,8 +86,11 @@ const CTASectionSettings: React.FC = () => { { label: 'Ocean', value: 'linear-gradient(135deg, #0ea5e9 0%, #6366f1 100%)' }, ]; + const effectiveCtas = normalizeCtas(props); + return (
+
{ />
-
- - setProp((p: CTASectionProps) => { p.buttonText = e.target.value; })} - style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }} - /> -
- -
- - setProp((p: CTASectionProps) => { p.buttonHref = e.target.value; })} - placeholder="https://..." - style={{ width: '100%', padding: '4px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12 }} - /> -
+ setProp((p: CTASectionProps) => { + p.ctas = next; + p.buttonText = undefined; + p.buttonHref = undefined; + })} + />
@@ -155,9 +149,11 @@ CTASection.craft = { props: { heading: 'Ready to Get Started?', description: 'Join thousands of satisfied users and start building your dream website today.', - buttonText: 'Start Free Trial', - buttonHref: '#', + ctas: [ + { text: 'Start Free Trial', href: '#', variant: 'primary' }, + ] as CtaButton[], gradient: defaultGradient, + anchorId: '', style: {}, }, rules: { @@ -180,12 +176,15 @@ CTASection.craft = { textAlign: 'center', ...props.style, }); + const ctas = normalizeCtas(props); + const buttonsHtml = ctasToHtml(ctas, { primaryBg: '#ffffff', primaryText: '#18181b', outlineText: '#ffffff' }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; return { - html: ` + html: `

${esc(props.heading || '')}

${esc(props.description || '')}

- ${esc(props.buttonText || '')} +
${buttonsHtml}
`, }; diff --git a/craft/src/components/sections/CallToAction.tsx b/craft/src/components/sections/CallToAction.tsx index 8e7b64f..26c1271 100644 --- a/craft/src/components/sections/CallToAction.tsx +++ b/craft/src/components/sections/CallToAction.tsx @@ -1,10 +1,14 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface CallToActionProps { heading?: string; description?: string; + ctas?: CtaButton[]; + /** Legacy props kept for backward compat with saved projects. */ buttonText?: string; buttonHref?: string; secondaryButtonText?: string; @@ -15,6 +19,7 @@ interface CallToActionProps { overlayOpacity?: number; textColor?: string; buttonColor?: string; + anchorId?: string; style?: CSSProperties; } @@ -23,16 +28,18 @@ const defaultGradient = 'linear-gradient(135deg, #2563eb 0%, #7c3aed 100%)'; export const CallToAction: UserComponent = ({ heading = 'Ready to Get Started?', description = 'Join thousands of satisfied users and start building your dream website today.', - buttonText = 'Get Started', - buttonHref = '#', - secondaryButtonText = '', - secondaryButtonHref = '#', + ctas, + buttonText, + buttonHref, + secondaryButtonText, + secondaryButtonHref, bgType = 'gradient', bgValue = defaultGradient, overlayColor = '#000000', overlayOpacity = 0, textColor = '#ffffff', buttonColor = '#ffffff', + anchorId, style = {}, }) => { const { @@ -56,9 +63,13 @@ export const CallToAction: UserComponent = ({ const isButtonDark = buttonColor === '#ffffff' || buttonColor === '#f8fafc'; const buttonTextColor = isButtonDark ? '#18181b' : '#ffffff'; + const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref }); + const ctaDefaults = { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor }; + return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ position: 'relative', padding: '80px 20px', @@ -89,41 +100,12 @@ export const CallToAction: UserComponent = ({ {description}

@@ -155,8 +137,11 @@ const CallToActionSettings: React.FC = () => { border: '1px solid #3f3f46', borderRadius: 4, fontSize: 12, }; + const effectiveCtas = normalizeCtas(props); + return (
+
{ />
- {/* Primary Button */} -
- - setProp((p: CallToActionProps) => { p.buttonText = e.target.value; })} - style={inputStyle} - /> -
- -
- - setProp((p: CallToActionProps) => { p.buttonHref = e.target.value; })} - placeholder="https://..." - style={inputStyle} - /> -
- - {/* Secondary Button */} -
- - setProp((p: CallToActionProps) => { p.secondaryButtonText = e.target.value; })} - placeholder="e.g. Learn More" - style={inputStyle} - /> -
- - {props.secondaryButtonText && ( -
- - setProp((p: CallToActionProps) => { p.secondaryButtonHref = e.target.value; })} - placeholder="https://..." - style={inputStyle} - /> -
- )} + setProp((p: CallToActionProps) => { + p.ctas = next; + p.buttonText = undefined; + p.buttonHref = undefined; + p.secondaryButtonText = undefined; + p.secondaryButtonHref = undefined; + })} + /> {/* Background Type */}
@@ -380,10 +329,11 @@ CallToAction.craft = { props: { heading: 'Ready to Get Started?', description: 'Join thousands of satisfied users and start building your dream website today.', - buttonText: 'Get Started', - buttonHref: '#', - secondaryButtonText: 'Learn More', - secondaryButtonHref: '#', + ctas: [ + { text: 'Get Started', href: '#', variant: 'primary' }, + { text: 'Learn More', href: '#', variant: 'outline' }, + ] as CtaButton[], + anchorId: '', bgType: 'gradient', bgValue: defaultGradient, overlayColor: '#000000', @@ -445,41 +395,16 @@ CallToAction.craft = { overlayHtml = `
`; } - let secondaryBtnHtml = ''; - if (props.secondaryButtonText) { - const secStyle = cssPropsToString({ - display: 'inline-block', - padding: '14px 36px', - backgroundColor: 'transparent', - color: textColor, - textDecoration: 'none', - borderRadius: '8px', - fontWeight: '600', - fontSize: '16px', - border: `2px solid ${textColor}`, - }); - secondaryBtnHtml = `\n ${esc(props.secondaryButtonText)}`; - } - - const btnStyle = cssPropsToString({ - display: 'inline-block', - padding: '14px 36px', - backgroundColor: buttonColor, - color: buttonTextColor, - textDecoration: 'none', - borderRadius: '8px', - fontWeight: '600', - fontSize: '16px', - }); + const ctas = normalizeCtas(props); + const buttonsHtml = ctasToHtml(ctas, { primaryBg: buttonColor, primaryText: buttonTextColor, outlineText: textColor }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; return { - html: ` + html: ` ${overlayHtml}

${esc(props.heading || '')}

${esc(props.description || '')}

-
- ${esc(props.buttonText || '')}${secondaryBtnHtml} -
+
${buttonsHtml}
`, }; diff --git a/craft/src/components/sections/Countdown.tsx b/craft/src/components/sections/Countdown.tsx index 42b981c..b6542cf 100644 --- a/craft/src/components/sections/Countdown.tsx +++ b/craft/src/components/sections/Countdown.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties, useEffect, useState, useCallback } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface CountdownProps { targetDate?: string; @@ -9,6 +10,7 @@ interface CountdownProps { digitColor?: string; labelColor?: string; bgColor?: string; + anchorId?: string; } interface TimeLeft { @@ -44,6 +46,7 @@ export const Countdown: UserComponent = ({ digitColor = '#ffffff', labelColor = 'rgba(255,255,255,0.7)', bgColor = '#18181b', + anchorId, }) => { const { connectors: { connect, drag }, @@ -98,6 +101,7 @@ export const Countdown: UserComponent = ({ return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ padding: '60px 20px', textAlign: 'center', @@ -141,6 +145,7 @@ const CountdownSettings: React.FC = () => { return (
+ {/* Target date */}
@@ -235,6 +240,7 @@ Countdown.craft = { digitColor: '#ffffff', labelColor: 'rgba(255,255,255,0.7)', bgColor: '#18181b', + anchorId: '', }, rules: { canDrag: () => true, @@ -265,6 +271,7 @@ Countdown.craft = { backgroundColor: bgColor, ...style, }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const headingHtml = heading ? `

${esc(heading)}

` @@ -278,7 +285,7 @@ Countdown.craft = { const uid = 'cd_' + Math.random().toString(36).slice(2, 8); return { - html: ` + html: ` ${headingHtml}
00Days
diff --git a/craft/src/components/sections/FeaturesGrid.tsx b/craft/src/components/sections/FeaturesGrid.tsx index 6313f98..6d26fcb 100644 --- a/craft/src/components/sections/FeaturesGrid.tsx +++ b/craft/src/components/sections/FeaturesGrid.tsx @@ -1,6 +1,7 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface FeatureItem { title: string; @@ -11,6 +12,7 @@ interface FeatureItem { interface FeaturesGridProps { features?: FeatureItem[]; style?: CSSProperties; + anchorId?: string; } const defaultFeatures: FeatureItem[] = [ @@ -22,6 +24,7 @@ const defaultFeatures: FeatureItem[] = [ export const FeaturesGrid: UserComponent = ({ features = defaultFeatures, style = {}, + anchorId, }) => { const { connectors: { connect, drag }, @@ -33,6 +36,7 @@ export const FeaturesGrid: UserComponent = ({ return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ padding: '80px 20px', backgroundColor: '#ffffff', @@ -102,6 +106,7 @@ const FeaturesGridSettings: React.FC = () => { return (
+
@@ -163,6 +168,7 @@ FeaturesGrid.craft = { props: { features: defaultFeatures, style: { backgroundColor: '#ffffff' }, + anchorId: '', }, rules: { canDrag: () => true, @@ -177,11 +183,12 @@ FeaturesGrid.craft = { /* ---------- HTML export ---------- */ (FeaturesGrid as any).toHtml = (props: FeaturesGridProps, _childrenHtml: string) => { - const esc = (s: any) => String(s ?? "").replace(//g, '>'); + const esc = (s: any) => String(s ?? "").replace(//g, '>').replace(/"/g, '"'); const sectionStyle = cssPropsToString({ padding: '80px 20px', ...props.style, }); + const idAttr = props.anchorId ? ` id="${esc(props.anchorId)}"` : ''; const cards = (props.features || defaultFeatures).map((feat) => { return `
${esc(feat.icon)}
@@ -191,7 +198,7 @@ FeaturesGrid.craft = { }).join('\n '); return { - html: ` + html: `
${cards}
diff --git a/craft/src/components/sections/HeroSimple.tsx b/craft/src/components/sections/HeroSimple.tsx index cac745b..d36bc8a 100644 --- a/craft/src/components/sections/HeroSimple.tsx +++ b/craft/src/components/sections/HeroSimple.tsx @@ -1,10 +1,15 @@ import React, { CSSProperties } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; +import { CtaButton, CtasEditor, normalizeCtas, ctaInlineStyle, ctasToHtml } from './_cta-helpers'; +import { AnchorIdField } from '../../ui/AnchorIdField'; interface HeroProps { heading?: string; subtitle?: string; + /** New dynamic CTAs. When set (length > 0), legacy primary/secondary fields are ignored. */ + ctas?: CtaButton[]; + /** Legacy — kept for backwards compatibility with saved projects. */ buttonText?: string; buttonHref?: string; secondaryButtonText?: string; @@ -24,6 +29,7 @@ interface HeroProps { minHeight?: string; verticalAlign?: 'top' | 'center' | 'bottom'; textAlign?: 'left' | 'center' | 'right'; + anchorId?: string; style?: CSSProperties; } @@ -43,10 +49,11 @@ function buildBackground(props: HeroProps): string { export const HeroSimple: UserComponent = ({ heading = 'Build Something Amazing', subtitle = 'Create beautiful websites without writing a single line of code.', - buttonText = 'Get Started', - buttonHref = '#', - secondaryButtonText = '', - secondaryButtonHref = '#', + ctas, + buttonText, + buttonHref, + secondaryButtonText, + secondaryButtonHref, bgType = 'color', bgColor = '#1e293b', bgGradientFrom = '#667eea', @@ -62,6 +69,7 @@ export const HeroSimple: UserComponent = ({ minHeight = '500px', verticalAlign = 'center', textAlign = 'center', + anchorId, style = {}, }) => { const { connectors: { connect, drag } } = useNode(); @@ -72,9 +80,17 @@ export const HeroSimple: UserComponent = ({ const justifyMap = { top: 'flex-start', center: 'center', bottom: 'flex-end' }; + const effectiveCtas = normalizeCtas({ ctas, buttonText, buttonHref, secondaryButtonText, secondaryButtonHref }); + const ctaDefaults = { + primaryBg: buttonBgColor, + primaryText: buttonTextColor, + outlineText: textColor, + }; + return (
{ if (ref) connect(drag(ref)); }} + id={anchorId || undefined} style={{ ...style, background: bgType !== 'image' ? bg : undefined, @@ -134,25 +150,12 @@ export const HeroSimple: UserComponent = ({ {subtitle}

@@ -181,8 +184,11 @@ const HeroSettings: React.FC = () => { props: node.data.props as HeroProps, })); + const effectiveCtas = normalizeCtas(props); + return (
+ {/* Content */}
@@ -192,18 +198,20 @@ const HeroSettings: React.FC = () => {