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() {