diff --git a/client/public/assets/img/hero-moon.png b/client/public/assets/img/hero-moon.png new file mode 100644 index 0000000..94f54fa Binary files /dev/null and b/client/public/assets/img/hero-moon.png differ diff --git a/client/src/components/HeroElement.jsx b/client/src/components/HeroElement.jsx index 496719a..7d12d8d 100644 --- a/client/src/components/HeroElement.jsx +++ b/client/src/components/HeroElement.jsx @@ -1,5 +1,6 @@ import { Link } from 'react-router-dom' -import MoonDot from './MoonDot.jsx' + +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)' } @@ -105,8 +106,23 @@ function content(element) { return case 'buttons': return - case 'moon': - return + case 'moon': { + const size = element.props?.size || 96 + const glow = element.props?.glow ?? 0.45 + return ( + + ) + } case 'badge': return case 'image': diff --git a/client/src/routes/admin/AdminLayout.jsx b/client/src/routes/admin/AdminLayout.jsx index 3cf61d1..4fd70c9 100644 --- a/client/src/routes/admin/AdminLayout.jsx +++ b/client/src/routes/admin/AdminLayout.jsx @@ -41,6 +41,8 @@ export default function AdminLayout() { const navigate = useNavigate() const location = useLocation() const title = TITLES[location.pathname] || 'Admin' + // The hero canvas editor needs room — let it use the full content width. + const wide = location.pathname === '/admin/hero' const modeDot = mode === 'live' ? 'var(--mode-live)' : 'var(--mode-maint)' // Keep the admin out of search indexes (belt-and-suspenders with robots.txt). @@ -147,7 +149,7 @@ export default function AdminLayout() { -
+
diff --git a/client/src/routes/admin/views/HeroEditor.jsx b/client/src/routes/admin/views/HeroEditor.jsx index 8b3972d..b287780 100644 --- a/client/src/routes/admin/views/HeroEditor.jsx +++ b/client/src/routes/admin/views/HeroEditor.jsx @@ -20,7 +20,7 @@ function newElement(type, z) { if (type === 'buttons') { return { ...base, y: 60, props: { align: 'center', gap: 12, items: [{ label: 'Button', to: '/', variant: 'primary' }] } } } - if (type === 'moon') return { ...base, props: { size: 64, glow: 0.45, color: '#eef3f8' } } + if (type === 'moon') return { ...base, props: { size: 96, glow: 0.5 } } if (type === 'badge') return { ...base, props: { text: 'New badge', bgColor: '#1a2d4a', textColor: '#c2d2e6', borderRadius: 999 } } if (type === 'image') return { ...base, props: { src: '', width: 40, alt: '' } } return base @@ -37,9 +37,27 @@ export default function HeroEditor() { const [uploading, setUploading] = useState(false) const [selectedId, setSelectedId] = useState(null) const [snap, setSnap] = useState(false) + const [scale, setScale] = useState(1) const teaserRef = useRef('') const skipSave = useRef(true) - const canvasRef = useRef(null) + const canvasRef = useRef(null) // the 1280x720 stage (scaled to fit) + const colRef = useRef(null) // measures available width + + // Render the canvas as a scaled 1280x720 stage so it's a faithful miniature of + // the live hero (viewport-unit fonts + % positions all scale together). + useEffect(() => { + const el = colRef.current + if (!el) return + const recompute = () => setScale(Math.min(el.clientWidth / 1280, (window.innerHeight * 0.66) / 720)) + recompute() + const ro = new ResizeObserver(recompute) + ro.observe(el) + window.addEventListener('resize', recompute) + return () => { + ro.disconnect() + window.removeEventListener('resize', recompute) + } + }, [loading]) useEffect(() => { let active = true @@ -133,8 +151,8 @@ export default function HeroEditor() { } catch { /* ignore */ } - const stepX = (8 / rect.width) * 100 // 8px snap, as % of canvas - const stepY = (8 / rect.height) * 100 + const stepX = (8 / 1280) * 100 // 8px snap on the 1280x720 stage, as % + const stepY = (8 / 720) * 100 const move = (ev) => { let nx = clamp(ox + ((ev.clientX - sx) / rect.width) * 100, 0, 100) let ny = clamp(oy + ((ev.clientY - sy) / rect.height) * 100, 0, 100) @@ -169,10 +187,11 @@ export default function HeroEditor() { } const move = (ev) => { const dxPx = ev.clientX - sx + const dxLogical = dxPx / scale // client px → stage px let val if (el.type === 'image') val = clamp(orig + (dxPx / rect.width) * 100, 5, 100) // % - else if (el.type === 'moon') val = clamp(orig + dxPx, 8, 400) // px - else val = clamp(orig + dxPx, 120, 1080) // text_block box px + else if (el.type === 'moon') val = clamp(orig + dxLogical, 24, 400) // px + else val = clamp(orig + dxLogical, 120, 1180) // text_block box px updateProps(el.id, { [dim]: Math.round(val) }) } const up = () => { @@ -253,25 +272,27 @@ export default function HeroEditor() {
-
-
{ - if (e.target === e.currentTarget) setSelectedId(null) - }} - style={{ - position: 'relative', - width: '100%', - aspectRatio: '16 / 9', - borderRadius: 10, - overflow: 'hidden', - border: '1px solid var(--line)', - ...heroBackground(layout), - }} - > - {snap && ( -
- )} +
+
+
{ + if (e.target === e.currentTarget) setSelectedId(null) + }} + style={{ + position: 'absolute', + top: 0, + left: 0, + width: 1280, + height: 720, + transformOrigin: 'top left', + transform: `scale(${scale})`, + ...heroBackground(layout), + }} + > + {snap && ( +
+ )} {elements.map((el) => ( ))} +

Click to select · drag to move · Delete key removes the selected element.

-
)