Skip to content

Tags: unlayer/elements

Tags

v0.1.21

Toggle v0.1.21's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(shared): decode common named HTML entities in plaintext output (#54)

htmlToPlainText (behind the public renderToPlainText, which builds the
text/plain MIME part of an email) only mapped ~17 named entities, so common
ones such as £, €, é, ° and × leaked into the
plaintext as literal entity text (e.g. "Price £5" instead of "Price
£5"). Expand the named-entity map to cover currency, typographic punctuation,
common symbols, and the Latin-1 accented letters used in European names/words.

Kept as a curated map (not the full HTML5 named-reference set) so the ESM
bundle stays within its CI size budget; anything omitted still decodes when
written as a numeric entity. Unknown entities are still left untouched.

Co-authored-by: eastagiletracker <310448263+eastagiletracker@users.noreply.github.com>

v0.1.20

Toggle v0.1.20's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(ci): support Node 24 releases and fork previews (#48)

* fix(ci): support Node 24 releases and fork previews

* revert(ci): defer fork Storybook previews

v0.1.19

Toggle v0.1.19's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
renderToHtmlParts: expose granular css/js/tags (#38)

* feat(react): expose granular css/js/tags on renderToHtmlParts

Non-breaking: head and body are unchanged; the pieces the assembled
head is built from are now also returned, for pipelines that need them
separately (CSS inlining, injecting into an existing style pipeline,
dropping scripts for email sends). No fonts field — Elements has no
font registry, and an always-empty array would misrepresent parity
with editor chunk exports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(react): position granular parts vs the documented chunk parameters

The editor's documented chunks are body/css/js/fonts. Elements exposes
tags additionally because component head functions can contribute
them — reassembling from css + js alone would silently drop tags —
and omits fonts (no font registry; renderToHtml's fonts option covers
font links). Both deviations are now stated in the types and README.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(react): remove stray prototype file from the branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

v0.1.18

Toggle v0.1.18's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Custom tools: registerElementsTool renders editor custom tools from c…

…ode (#37)

* feat(react): registerElementsTool — render custom tools from code

Accepts the same tool definition embedders already write for the
editor's registerTool (docs.unlayer.com Custom Tools) and returns a
React component. One definition powers both runtimes: the Builder uses
the panel/canvas half (label, icon, options, Viewer, validator — all
accepted and ignored here), while Elements calls renderer.exporters per
mode and collects renderer.head, so code output matches editor exports
by construction. renderToJson emits { type: 'custom', slug, values }
with u_content_custom_<slug> meta ids — the shape custom-tool designs
already use — so trees round-trip into the Builder.

Details: document mode falls back to the web exporter and email-only
tools render via email everywhere (matching export fallbacks); tool
output passes through toSafeHtml when configured (matching editor
exports, which sanitize custom tool HTML); the content wrapper, head
extraction, and JSON emission honor an explicit meta base so ids match
editor-saved designs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(react): custom tool edge cases from real-world tool shapes

Adds option-group default seeding — the common real-world custom tool
shape keeps values: {} and defines defaults on each property widget, so
registerElementsTool now folds those in (disabled properties skipped,
tool-level values winning, matching the editor's precedence).

22 edge-case tests modeled on realistic tools: a product card (nested
object values, rich-text defaults, editor-only fields at every level)
and a head-heavy tool (css keyed on values._meta.htmlID, per-mode
branching, js emission, undefined heads, tag dedupe). Failure modes:
throwing exporters drop only their block with a logged error, non-string
returns coerce, special characters pass through raw, the sanitizer sees
each mode's output. Composition: per-slug counters, containerPadding,
base props excluded from values, plaintext extraction.

Round-trip verified against a live editor: a renderToJson design
containing a custom tool loads with the tool registered via customJS,
saveDesign returns type/slug/values/_meta intact, and exportHtml
renders through the tool's own exporter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): review fixes + link-widget normalization + custom tool stories

Review feedback: exporter returns are coerced to a string before a
configured sanitizer sees them (a non-string return would crash a
sanitizer calling string methods), and the metaName doc now describes
the suffix rather than the full prefixed id.

Adding Storybook coverage surfaced a real parity gap the smoke test
caught: tools written for the editor read link-widget values in render
shape (values.<option>.url), but Elements only normalized fields named
href/action. Options declared with widget: 'link' are now normalized
to { url, target } before each render, matching what editor exporters
receive.

Four new stories under Custom Tools/Registered Tools: a countdown with
option-widget defaults, a customized variant, a product card with
nested image/link values, and both tools composed inside a full design.
Visual baseline extended (130 renders; all pre-existing renders
byte-identical to main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(react): autodocs + full source snippets for custom tool stories

The stories had no visible code: the meta lacked the autodocs tag the
other component stories carry, and even autodocs would only show the
JSX usage — for custom tools the definition object is the code worth
copying. Each story now ships an explicit source snippet with the tool
definition, the registerElementsTool call, and the usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(react): gallery-grade custom tool examples + per-instance meta ids

Ports the accordion and tab tools from the public examples gallery as
stories, with the upgrades the originals lack: head css scoped per
instance via values._meta.htmlID, idempotent head js, and email
exporters (panels render expanded; tabs become stacked sections — email
has no JS). Stories render through renderToHtmlParts with a head
injector so the interactivity works in the canvas.

Building the two-instance story exposed a real bug: the value-level
_meta.htmlID came from the factory's index default, so every instance
in a Column rendered as _1 while head extraction numbered _1.._N —
instance >= 2's scoped css targeted an id that didn't exist. Column now
allocates the content id before rendering and threads it into the item,
so the value meta, wrapper id, and head/json numbering all agree.
Verified in-browser: two accordions with different colors style and
toggle independently. All snapshots byte-identical — built-in output
is unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(react): port the product, QR, and map gallery tools

Three more tools from the public examples gallery, completing the set:

- product_library — the gallery's flagship card, ported faithfully:
  nested image/link values, per-part colors, rich-text description, and
  the split price/CTA footer, in both div (web) and table (email)
  markup. The link-widget action arrives in render shape (.url/.target)
  exactly as editor exporters receive it.
- qr_tool — in the Builder a property-editor widget generates the code
  client-side; from code any generator URL works. Adds the email
  exporter the original leaves empty (tickets and menus are the prime
  QR use case).
- map_tool — the original builds a Google Static Maps URL around an
  API key, which a public example should not ship. This port computes
  OpenStreetMap tile coordinates from lat/lon/zoom in the exporter
  (slippy-map math), keyless, with a marker overlay on web and the
  same tile table rendering in email.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(react): name the API registerTool — same term as the Builder

Per the naming discussion: terms stay the same across the Builder and
Elements (that shared vocabulary is the point of the one-definition
contract), and the docs bridge the mental model for Elements-first
users — a custom tool is the custom component you create.
registerElementsTool remains exported as an alias.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

v0.1.17

Toggle v0.1.17's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix/paragraph margins and full document html (#36)

* fix(shared): reset paragraph margins inline in generated text HTML

Generated <p> tags now carry margin: 0px plus the logical block-margin
longhands inline, matching the markup the Unlayer editor exports.
Browsers default <p> to 1em block margins and email clients strip
<head> resets, so without the inline reset multi-paragraph text gained
extra vertical spacing compared to the editor.

Also brings paragraph serialization to parity for custom styles,
indent, string/numeric alignment formats, and inline-tool spans.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): stack web-mode columns on mobile via flex wrapping

The web grid's mobile rules (display: block, width/min-width overrides)
are inert on flex items in a nowrap row — flex-basis governs their
size — so columns never stacked on small screens and overflowed the
viewport instead. Stack by letting the row wrap and growing each
column's flex-basis to 100% below the breakpoint, matching the
behavior of editor-exported web pages.

Also aligns two related behaviors: the web breakpoint is the 480px
mobile device breakpoint (email keeps contentWidth + 20), and
document/print output never stacks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(react)!: renderToHtml returns a complete HTML document

renderToHtml now returns a ready-to-use document from <!DOCTYPE ...>
to </html>, matching what the Unlayer editor exports: an XHTML
transitional shell with VML namespaces and MSO conditionals for email,
an HTML5 shell for web, and a print-friendly XHTML shell for document
mode. New options: title and fonts (stylesheet link URLs). Apps that
own their document shell keep using renderToHtmlParts, which still
returns embeddable { head, body } chunks.

Also fixes display-mode resolution for the Email/Page/Document
wrappers: they lock their mode internally, so head extraction
previously ran in web mode for all of them — renderToHtmlParts(<Email>)
returned web CSS instead of email CSS.

Golden snapshots now byte-lock the full documents for all three modes.

BREAKING CHANGE: renderToHtml previously returned only the body
markup. Callers that wrapped it in their own document shell should
switch to renderToHtmlParts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(react): browser E2E gate for rendered documents

Renders full documents with the built dist for all three modes and
asserts in headless Chromium: zero computed <p> margins, a single
<body>, responsive column stacking at mobile width, button hover
colors, RTL direction, no horizontal overflow, hidden preheader,
accessibility basics (img alt, link names, presentation tables),
image width pinning, CSS parseability, print media visibility, and a
computed-style baseline (deterministic across platforms, unlike pixel
screenshots; regenerate with UPDATE_E2E_BASELINE=1). A negative
control must fail, proving the gate detects what it guards.

Runs in CI after the existing Playwright Chromium install. The ESM
bundle budget rises 68KB -> 75KB for the per-mode document shells
(currently ~70KB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(shared): whitelist dir and heading tag in generated text HTML

Review feedback: node.direction was interpolated into the dir attribute
unescaped, so untrusted Lexical JSON could inject attribute markup.
Only ltr/rtl are emitted now — which is also exact parity, since the
editor ignores any other direction value. The same guard applies to
heading nodes, plus an h1-h6 whitelist for the heading tag name, which
was likewise interpolated unvalidated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): scope mobile .container rule under .u_row

Review feedback: the responsive rule targeted bare .container with
!important, which collides with host-page classes (Bootstrap et al.)
when Elements output is embedded. Every Elements .container sits inside
the .u_row wrapper, so scoping keeps our rendering identical while
making the rule inert outside our markup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(react): correct stale breakpoint note in browser E2E header

Review feedback: the header still said 620px; the gate tests the real
breakpoints (web: 480px device breakpoint, email: contentWidth + 20px).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): only strip the host div for Unlayer wrapper roots

Review feedback: stripOuterDiv ran on every root, so a bare item root
(e.g. renderToHtml(<Button className=...>)) lost its own wrapper div —
including author-supplied className/style. The strip now applies only
when the root is Body/Email/Page/Document, whose host div is renderer
plumbing rather than the element's own markup. Also documents what
HtmlParts.body actually contains per mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(react): storybook visual-drift gate

Fingerprints the computed styles of every story (126 renders) at
desktop and mobile widths in headless Chromium and diffs against a
committed, dictionary-encoded baseline. A PR that changes any story's
rendered styling fails naming the exact stories and property-level
diffs — no more paging through stories by hand to spot drift. Computed
styles rather than pixel screenshots keep the baseline deterministic
across macOS and Linux. Regenerate intentional changes with
UPDATE_VISUAL_BASELINE=1 pnpm test:visual.

Runs in CI after the Storybook smoke test, reusing its static build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): normalize UA default serif in visual fingerprints

First CI run caught the one environment-derived style: elements with
no explicit font inherit the browser default serif, reported as Times
on macOS and Times New Roman on Linux. Normalized to a stable token so
the baseline transfers across platforms; explicit font stacks compute
identically and stay covered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(react): normalize BlinkMacSystemFont in visual fingerprints

macOS Chromium canonicalizes BlinkMacSystemFont to system-ui in
computed font-family values; Linux keeps it literal. Normalize to one
spelling so story stacks using system-font fallbacks fingerprint
identically on both platforms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

v0.1.16

Toggle v0.1.16's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix multi-column Row NaN (default cells to column count) + border wid… (

#35)

* Fix multi-column Row NaN (default cells to column count) + border width px unit

Two type-checks-but-renders-broken footguns found by cold-start agent testing:

- A multi-column <Row> with no `layout`/`cells` defaulted `cells` to `[1]`
  regardless of column count, so the 2nd/3rd <Column> had no cell and rendered
  width="NaN". Default to one equal cell PER <Column> child, matching
  renderToJson. (Also makes a `layout` accidentally placed on <Column> harmless
  — the Row no longer NaNs.)

- A number border width (`borderTopWidth: 1`) rendered `border-top: 1 solid`
  (invalid CSS the browser drops), even though BorderInput + its JSDoc accept a
  number. Normalize border *Width fields (number / unit-less numeric string → px)
  in the mapper, for both nested `border` objects and gathered flat side props.

+6 tests (shared mapper normalization + react render-level for both).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Review: clone border before px-normalizing (keep mapSemanticProps pure)

The width normalization mutated final.border in place, but final.border can
alias the caller's object (the values escape hatch is a shallow clone; a nested
border prop passes by reference) — so a reused const HAIRLINE would be rewritten
to '1px' as a side effect. Clone before rewriting, reassign. +purity test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

v0.1.15

Toggle v0.1.15's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Fix/image objsrc width and dx footguns (#34)

* Complete image width round-trip (object-src form) + fix DX footguns

Image round-trip: a fixed width now pins whether set via the flat `width`
prop OR the object-src / documented `values` full-control form
(`src={{ width: 300 }}`). Previously only the flat prop pinned; the object
form serialized autoWidth:true and still resized to the original on click in
the Builder. Both now emit the editor's canonical pin (autoWidth:false +
percent of the column slot), verified against the editor's own
imageRendering functions (holds across the on-click natural-dimension
reload). An explicit `autoWidth` on `src` is still honored.

DX footguns surfaced by cold-start agent testing (valid, type-checking input
that produced broken output):
- Button: `{ name, attrs: { href } }` (the shape the canonical Href type
  advertises) rendered href="" — normalizeLinkValue now reads href/target
  from `attrs` too, and `||` lets the schema's empty default href fall
  through. Genuine custom attrs are still spread.
- Social: `iconSize`/`spacing` as px strings ("34px") rendered max-width:NaNpx
  — the exporter does arithmetic on them; relax the type to number|string and
  coerce to a number in the mapper.
- Image: a string-url image inherited the placeholder's 1600x400 aspect and
  emitted a wrong height attr; drop the default height so it's height:auto.

Polish:
- Export the input building-block types (SizeInput, BorderInput,
  TextStyleProps, FontFamilyInput, FontWeightInput, HeadingLevel,
  ImageSrcInput) — referenced by public prop types but not importable (TS2459).
- Fix a JSDoc import example (@unlayer-internal/shared-elements -> the public
  package) and add a Button href example.

Tests: new dx-footguns + object-src round-trip cases; updated the two tests
that encoded the old object-src=natural behavior; type guards for the exports;
refreshed two snapshots (only the wrong height attr removed). 369 pass,
typecheck clean, bundle 63.6KB < 68KB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Add tests: normalizeLinkValue attrs/empty-fallthrough + cross-component link consistency

- shared: normalizeLinkValue reads href from attrs, an empty values.href
  falls through to attrs (the || vs ?? fix), custom attrs preserved.
- react: the attrs-href fix works for Image action + Menu, not just Button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Canonicalize attrs href into values.href so it round-trips into the editor

The render-time fix made { name, attrs:{ href } } render a working anchor,
but renderToJson preserved the storage shape (empty values.href + attrs),
and the Builder reads values.href — so the link was lost on import. Move an
attrs href/target into values.href/target at the mapper (both flat prop and
values escape hatch), keeping genuine custom attrs. Found while loading a
test design into the live editor. +4 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix CallToAction story (malformed SVG data URI) + note Html is raw passthrough

The story embedded an inline-SVG data URI with double quotes (xmlns="…")
inside a double-quoted style="…", so the first inner quote closed the
attribute and '); opacity: 0.1; "> leaked as visible text. Replaced the grain
with a valid CSS radial-gradient dot pattern and dropped the onmouseover/out
inline JS (can't run in a rendered email; XSS pattern) from the affected
stories. Added a guard test over all Html story HTML (no inline handlers, no
url() with a raw double quote) and a security note: <Html> renders verbatim,
not sanitized — pass toSafeHtml via UnlayerProvider to sanitize like the editor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Review: tighten normalizeLinkValue guard + strict Social size coercion

- normalizeLinkValue: only normalize a {name} object when it actually carries
  values or attrs, so a bare {name} (or accidental {name:…}) falls through to
  undefined per the documented contract instead of becoming {url:""}.
- Social coerceSizes: parse iconSize/spacing strictly (number or px string);
  drop a non-px unit ("50%", "1.5em") so it falls back to the schema default
  rather than being silently parseFloat-ed to a wrong px count.
Both caught in review. +2 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

v0.1.14

Toggle v0.1.14's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Pin fixed-width images so they survive the design-JSON round-trip (#33)

* 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>

v0.1.13

Toggle v0.1.13's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Render parity: content-width image sizing, containerPadding, unique I…

…Ds (#32)

* fix(elements): size images against the real content width in columns

Item exporters received no column/body context, so the width-aware image exporter
fell back to a fixed ~500px regardless of contentWidth or column count — full-width
images rendered small and images in multi-column rows could overflow their column.

Column now threads its index, the row cells, and the row/column/body values to its
item children, and renderComponent surfaces them on the exporter `meta`, so the
exporter computes the available width (contentWidth × column fraction, minus
padding) the same way the editor does. A standalone item (no Body) now defaults
contentWidth to 500 to match the schema default. Result: a full-width image fills
the content width, an image in a 3-column row sizes to ~1/3, and nothing overflows.

Updates the golden snapshot to the corrected sizes; adds regression tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(elements): expose containerPadding as a typed item prop (number → px)

containerPadding (an item's content-wrapper padding) was threaded at runtime but
only typed as a string and never exposed on item props — so containerPadding="10px"
was a type error, and a bare number would render unitless. Type it as SizeInput on
the item base props and normalize a number to px in Column, matching the other size
props. Renders identically to the equivalent px string. Adds render + type tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(elements): unique element ids in renderToHtml (match renderToJson + editor)

renderToHtml generated ids per element position, so multi-row designs repeated
u_row_1 / u_column_1 / u_content_*_1 — invalid HTML5 and out of step with both
renderToJson (a global counter) and the editor (unique stored ids). Thread a
per-render id counter on _config (reset by Body, shared by reference down the
tree, SSR-safe) so every body/row/column/content id is unique. Updates the
multi-element snapshots (id attributes only); adds a uniqueness test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(elements): default standalone contentWidth to the schema-shaped "500px"

Use the CSS-string "500px" (matching BodyDefaults.contentWidth) for the
standalone-item contentWidth default instead of a bare number, so the value is a
CSS string everywhere it might be consumed. Behavior-neutral — the image exporter
parseFloats it either way; addresses a review note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

v0.1.12

Toggle v0.1.12's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
DX follow-ups: renderToJson wrapper parity + Menu text inputs (#31)

* fix(elements): renderToJson accepts a wrapper component, like renderToHtml

renderToHtml renders a custom wrapper component through React, but renderToJson
walked the element tree and rejected anything whose root wasn't <Body>/<Email>/
<Page>/<Document> — so renderToJson(<MyEmail/>) threw while renderToHtml(<MyEmail/>)
worked. Unwrap a plain function-component root to its returned element (bounded
loop; class/forwardRef/memo still hit the clear root-type error). Adds tests for
the wrapper case and the still-invalid case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(elements): relax Menu's text inputs to match Heading/Paragraph

Menu's fontFamily/fontWeight/fontSize/letterSpacing kept the canonical strict
types, so a string fontFamily or a number/em size that compiles on Heading
failed on Menu. Relax them to the shared agent-friendly inputs (Menu has no
color/lineHeight field, so only these four). Type-only — values are normalized
at render time the same way as the other text components. Guarded in the tsc
contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(elements): keep extract-head's type comment to its own local concern

Public-repo hygiene — the comment now describes only this file's local head
type, not anything about how the rendering dependency is structured.

* fix(elements): give a clear error when a renderToJson wrapper throws

Invoking a wrapper component that uses React hooks throws a bare "Invalid hook
call" that masked the intended guidance. Catch the invocation and rethrow an
actionable error: a wrapper must synchronously return a root (Email/Page/
Document/Body) and use no hooks — pass the root element or call the component.
Adds a test for the throwing-wrapper path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(elements): drop misleading workaround from the unwrap error message

The error fires only when invoking the wrapper itself threw, so calling it
manually (renderToJson(MyEmail())) would fail identically — suggesting it was
misleading. Point only to passing the root element directly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>