diff --git a/client/src/routes/admin/views/HeroEditor.jsx b/client/src/routes/admin/views/HeroEditor.jsx index 4eab07f..701fc03 100644 --- a/client/src/routes/admin/views/HeroEditor.jsx +++ b/client/src/routes/admin/views/HeroEditor.jsx @@ -39,6 +39,20 @@ function newElement(type, z) { const RESIZABLE = { text_block: 'width', image: 'width', moon: 'size' } +// Scale a text line's font size by a ratio when its box is resized, so the corner +// handle acts as a WYSIWYG zoom that keeps the h1/h2/p ratios intact. Numeric px +// sizes (editor-authored) and simple rem/em/px strings scale; responsive strings +// like clamp()/vw are left alone so they keep adapting to the viewport. +const FONT_UNIT_RE = /^(\d*\.?\d+)(rem|em|px)$/ +function scaleFontSize(v, ratio) { + if (typeof v === 'number') return Math.max(6, Math.round(v * ratio)) + if (typeof v === 'string') { + const m = FONT_UNIT_RE.exec(v.trim()) + if (m) return `${round2(parseFloat(m[1]) * ratio)}${m[2]}` + } + return v +} + export default function HeroEditor() { const [layout, setLayout] = useState(null) const [live, setLive] = useState(null) @@ -190,6 +204,10 @@ export default function HeroEditor() { const rect = canvasRef.current.getBoundingClientRect() const sx = e.clientX const orig = el.props?.[dim] ?? (dim === 'width' && el.type === 'image' ? 40 : dim === 'width' ? 600 : 64) + // Snapshot the starting width + lines for text blocks so font scaling is always + // computed against the drag origin (no rounding drift as the pointer moves). + const baseWidth = el.type === 'text_block' ? orig : 0 + const baseLines = el.type === 'text_block' ? el.props?.lines || [] : null const node = e.currentTarget try { node.setPointerCapture(e.pointerId) @@ -199,11 +217,17 @@ 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 + dxLogical, 24, 400) // px - else val = clamp(orig + dxLogical, 120, 1180) // text_block box px - updateProps(el.id, { [dim]: Math.round(val) }) + if (el.type === 'image') { + updateProps(el.id, { width: Math.round(clamp(orig + (dxPx / rect.width) * 100, 5, 100)) }) // % + } else if (el.type === 'moon') { + updateProps(el.id, { size: Math.round(clamp(orig + dxLogical, 24, 400)) }) // px + } else { + // text_block: resize the box and scale every line's font proportionally. + const width = Math.round(clamp(orig + dxLogical, 120, 1180)) + const ratio = baseWidth ? width / baseWidth : 1 + const lines = baseLines.map((l) => ({ ...l, fontSize: scaleFontSize(l.fontSize, ratio) })) + updateProps(el.id, { width, lines }) + } } const up = () => { node.removeEventListener('pointermove', move) @@ -324,7 +348,7 @@ export default function HeroEditor() {
- Click to select · drag to move · Delete key removes the selected element. + Click to select · drag to move · drag the corner handle to resize (text scales with the box) · Delete key removes the selected element.