import { Link } from 'react-router-dom' const MOON_IMAGE = '/assets/img/hero-moon.png' // Font family tokens a line may opt into; default is the page serif. const FONT = { display: 'var(--display)', sans: 'var(--sans)' } // fontSize may be a number (px, from the editor) or a CSS string (e.g. a clamp() // used by the pre-populated default so the hero stays responsive until edited). function sizeToCss(v) { return typeof v === 'number' ? `${v}px` : v } function lineStyle(line) { return { display: 'block', // each line stacks (so a span line behaves like the others) margin: line.marginTop != null ? `${line.marginTop}px 0 0` : '0', fontFamily: FONT[line.font] || undefined, fontSize: sizeToCss(line.fontSize), color: line.color || 'inherit', fontWeight: line.weight || undefined, fontStyle: line.italic ? 'italic' : undefined, letterSpacing: line.letterSpacing || undefined, textTransform: line.transform || undefined, lineHeight: line.lineHeight || undefined, maxWidth: line.maxWidth ? `${line.maxWidth}px` : undefined, marginLeft: line.maxWidth ? 'auto' : undefined, marginRight: line.maxWidth ? 'auto' : undefined, } } function TextBlock({ props }) { const align = props.align || 'center' return (
{(props.lines || []).map((line, i) => { const Tag = /^(h1|h2|h3|p|span)$/.test(line.tag) ? line.tag : 'p' return ( {line.text} ) })}
) } function Buttons({ props }) { const justify = props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center' return (
{(props.items || []).map((b, i) => ( {b.label} ))}
) } function Badge({ props }) { return ( {props.text} ) } function HeroImage({ props }) { if (!props.src) { // Editor placeholder until an image is chosen (a srcless image never ships live). return (
Upload an image
) } return ( {props.alt ) } function content(element) { switch (element.type) { case 'text_block': return case 'buttons': return case 'moon': { const props = element.props || {} const size = props.size || 96 const glow = props.glow ?? 0.45 // Image source is configurable; old layouts with no src fall back to the // default hero moon so they render exactly as before. Size/glow unchanged. return ( {props.alt ) } case 'badge': return case 'image': return default: return null } } // Absolute-positioned wrapper + type-specific content. In `editor` mode the inner // content is made non-interactive (so clicks select/drag the wrapper) and the // wrapper takes selection styling + an onPointerDown handler. export default function HeroElement({ element, wrapperStyle, editor = false, selected = false, onPointerDown, children, }) { const anchor = element.anchor || 'center' const transform = anchor === 'center' ? 'translate(-50%, -50%)' : anchor === 'top-right' ? 'translateX(-100%)' : undefined // text_block/buttons may set a box width (px); kept within the containing block // (the hero section live, or the editor canvas) with small side gutters. const boxWidth = (element.type === 'text_block' || element.type === 'buttons') && element.props?.width ? `min(${element.props.width}px, calc(100% - 36px))` : undefined const cls = [editor ? 'hero-el-editable' : '', selected ? 'is-selected' : ''].filter(Boolean).join(' ') return (
{content(element)}
{children}
) }