Hero editor: scale text block fonts with the resize handle (#25)

The text_block corner handle previously only changed the wrap width, so
the font size never tracked the box — making the editor un-WYSIWYG and
awkward to tune. Now dragging the handle scales every line's font
proportionally with the box, acting as a zoom that preserves the
h1/h2/p size ratios and keeps each line's manually-set baseline.

- Add scaleFontSize(): numeric px sizes (floored at 6px) and simple
  rem/em/px strings scale by the box ratio; responsive clamp()/vw
  strings are left untouched so the default hero stays fluid.
- Snapshot the box width + lines at drag start so scaling is computed
  against the origin (no rounding drift mid-drag).
- Update the canvas hint to note the handle scales text.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 14:58:45 -05:00
parent 86e44a94a2
commit 6af85c30b6

View File

@@ -39,6 +39,20 @@ function newElement(type, z) {
const RESIZABLE = { text_block: 'width', image: 'width', moon: 'size' } 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() { export default function HeroEditor() {
const [layout, setLayout] = useState(null) const [layout, setLayout] = useState(null)
const [live, setLive] = useState(null) const [live, setLive] = useState(null)
@@ -190,6 +204,10 @@ export default function HeroEditor() {
const rect = canvasRef.current.getBoundingClientRect() const rect = canvasRef.current.getBoundingClientRect()
const sx = e.clientX const sx = e.clientX
const orig = el.props?.[dim] ?? (dim === 'width' && el.type === 'image' ? 40 : dim === 'width' ? 600 : 64) 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 const node = e.currentTarget
try { try {
node.setPointerCapture(e.pointerId) node.setPointerCapture(e.pointerId)
@@ -199,11 +217,17 @@ export default function HeroEditor() {
const move = (ev) => { const move = (ev) => {
const dxPx = ev.clientX - sx const dxPx = ev.clientX - sx
const dxLogical = dxPx / scale // client px → stage px const dxLogical = dxPx / scale // client px → stage px
let val if (el.type === 'image') {
if (el.type === 'image') val = clamp(orig + (dxPx / rect.width) * 100, 5, 100) // % updateProps(el.id, { width: Math.round(clamp(orig + (dxPx / rect.width) * 100, 5, 100)) }) // %
else if (el.type === 'moon') val = clamp(orig + dxLogical, 24, 400) // px } else if (el.type === 'moon') {
else val = clamp(orig + dxLogical, 120, 1180) // text_block box px updateProps(el.id, { size: Math.round(clamp(orig + dxLogical, 24, 400)) }) // px
updateProps(el.id, { [dim]: Math.round(val) }) } 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 = () => { const up = () => {
node.removeEventListener('pointermove', move) node.removeEventListener('pointermove', move)
@@ -324,7 +348,7 @@ export default function HeroEditor() {
</div> </div>
</div> </div>
<p className="sans dim" style={{ fontSize: '0.76rem', marginTop: 8 }}> <p className="sans dim" style={{ fontSize: '0.76rem', marginTop: 8 }}>
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.
</p> </p>
</div> </div>