Make the hero Moon image configurable via props.src

The Moon stays a dedicated, first-class hero element — only its image
source becomes configurable. Adds optional src/alt props alongside the
existing size/glow.

- HeroElement: the moon renders props.src when present, else falls back to
  the default /assets/img/hero-moon.png. Size, glow, and animation are
  unchanged. alt is now props.alt (default '', same as before).
- HeroEditor MoonPanel: adds an image upload (reusing the existing shared
  api.admin.upload workflow, same as the image/background panels) that sets
  props.src, an alt-text field, and a "Use default" reset. Size/glow
  controls unchanged.

Fully backwards compatible: existing layouts with only size/glow and no
src render exactly as today via the fallback. No DB, API, or hero-JSON
changes; no migration.
This commit is contained in:
2026-07-02 23:38:03 -05:00
parent ea46b5d346
commit 6ab3e47d38
2 changed files with 44 additions and 5 deletions

View File

@@ -107,12 +107,15 @@ function content(element) {
case 'buttons': case 'buttons':
return <Buttons props={element.props || {}} /> return <Buttons props={element.props || {}} />
case 'moon': { case 'moon': {
const size = element.props?.size || 96 const props = element.props || {}
const glow = element.props?.glow ?? 0.45 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 ( return (
<img <img
src={MOON_IMAGE} src={props.src || MOON_IMAGE}
alt="" alt={props.alt || ''}
draggable={false} draggable={false}
style={{ style={{
width: size, width: size,

View File

@@ -499,9 +499,45 @@ const swatch = { width: '100%', height: 38, padding: 2, border: '1px solid var(-
function MoonPanel({ element, onProps }) { function MoonPanel({ element, onProps }) {
const p = element.props || {} const p = element.props || {}
const [up, setUp] = useState(false)
// Reuses the shared admin upload endpoint (same as the image/background panels);
// a successful upload just points props.src at the returned URL.
async function onFile(e) {
const f = e.target.files?.[0]
e.target.value = ''
if (!f) return
if (f.size > 1024 * 1024 && !confirm('This image is over 1 MB and may slow the page. Upload anyway?')) return
setUp(true)
try {
const { url } = await api.admin.upload(f)
onProps({ src: url })
} catch {
/* ignore */
} finally {
setUp(false)
}
}
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
<p className="sans dim" style={{ margin: 0, fontSize: '0.8rem' }}>Uses the moon from the hero artwork.</p> <div>
<span className="field-label">Moon image</span>
{p.src ? (
<img src={p.src} alt="" style={{ width: '100%', maxHeight: 90, objectFit: 'contain', borderRadius: 6, border: '1px solid var(--line)', marginBottom: 8 }} />
) : (
<p className="sans dim" style={{ margin: '0 0 8px', fontSize: '0.8rem' }}>Using the default moon from the hero artwork.</p>
)}
<label className="btn btn-ghost btn-sq" style={{ display: 'inline-block', cursor: 'pointer' }}>
{up ? 'Uploading…' : p.src ? 'Replace' : 'Upload'}
<input type="file" accept="image/*" onChange={onFile} hidden disabled={up} />
</label>
{p.src && (
<span className="link-accent" style={{ fontSize: '0.8rem', marginLeft: 10 }} onClick={() => onProps({ src: '' })}>Use default</span>
)}
</div>
<label>
<span className="field-label">Alt text</span>
<input className="input" value={p.alt || ''} onChange={(e) => onProps({ alt: e.target.value })} />
</label>
<label> <label>
<span className="field-label">Size {p.size || 96}px</span> <span className="field-label">Size {p.size || 96}px</span>
<input type="range" min="24" max="320" step="1" value={p.size || 96} onChange={(e) => onProps({ size: Number(e.target.value) })} style={{ width: '100%' }} /> <input type="range" min="24" max="320" step="1" value={p.size || 96} onChange={(e) => onProps({ size: Number(e.target.value) })} style={{ width: '100%' }} />