* Pin fixed-width images so they survive the design-JSON round-trip
A fixed image width (px or number) was stored in the natural-size field
with autoWidth:true, so re-opening the exported design in an editor
reloaded the image's intrinsic dimensions and the explicit width was lost
(the image snapped back to its original size on selection).
Treat a fixed px/number width as display intent: emit autoWidth:false with
maxWidth as a percent of the column's content slot — the canonical
fixed-size shape, kept independent of the natural src.width/height. The
percent is computed from the same available-width geometry the renderers
use (contentWidth x column share, minus paddings/borders) by a width-aware
pass in both renderToHtml (via Column's threaded context) and renderToJson
(via the tree walk). A percent width/maxWidth already pinned and is
unchanged; a no-width image stays responsive (autoWidth:true).
- add utils/image-sizing.ts (slot geometry + px->percent conversion)
- Image propMapper: capture width/maxWidth as display intent, no longer
polluting the natural src.width field
- new Image.width-roundtrip tests; update stale assertions that encoded
the old natural-size behavior
Verified the emitted percent matches the renderer's own available-width
math and that the pin no longer jumps when intrinsic dimensions refresh.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: raise ESM bundle budget 60KB->68KB for image width-pinning
The bundle was already at 58.4KB on main (97% of the 60KB budget set when it
was ~49KB). The image width-pinning fix adds ~4.6KB of dependency-free local
geometry, so the budget no longer fits legitimate growth. Raise to 68KB; it
still flags accidental dependency bundling (any real dep is 10KB+).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Treat a bare numeric-string contentWidth as fixed px in slot geometry
fixedContentWidth only accepted numbers and px strings, but the renderer
(Row's toContentWidthPx parseInts any string) and the exporter's body-width
math treat a bare numeric string like "600" as 600px. The slot geometry
fell back to 500, producing a wrong pinned-image percent for that input.
Accept a numeric string with an optional px unit; still reject "%"/"auto".
Caught in review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Make toPx strict + correct fixedContentWidth doc
toPx() parseFloat'd any string, so a non-px maxWidth on a pinned src (e.g.
the escape hatch {autoWidth:false, maxWidth:'1.5em'}) was misread as px and
converted into a bogus percent. Accept only a number or numeric/px string;
leave other CSS units untouched. Add a test guarding it.
Also correct fixedContentWidth's doc: it mirrors the exporter's body-width
math (bare numeric string = px, %/auto -> fallback), not Row's parseInt
(which would misread '50%' as 50). Both caught in review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Share one strict contentWidth->px parse; default row cells to Column count
- contentWidth parsing: Row's grid CSS and the image slot geometry had
separate parsers that disagreed on non-px values (Row's parseInt read
"50%" as 50px; the slot math fell back to 500). Extract one shared
bodyContentWidthPx and use it in both, so a non-px contentWidth collapses
to the same base everywhere. No change for px/number widths; also fixes a
latent email-grid bug for % content widths.
- renderToJson default cells: counted all children, so a stray non-Column
child inflated the cells array beyond the column list and distorted the
column-share math (wrong pinned-image percent) and the row layout. Count
only <Column> children, matching the existing comment.
Both caught in review. +4 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Keep geometry parsers on parseFloat to mirror the renderer
The slot geometry must match the renderer's available-width math, and the
editor's explodePaddingsOrMargins / explodeBorder both parseFloat each token
(so '10%' is read as 10). edges() already did this; switch borderEdges() back
from strict toPx to parseFloat so the two are consistent and both mirror the
renderer. Strict px parsing (toPx) stays only for the display-pin value in
pinImageSrc, never for the geometry. Documented the rationale inline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>