Skip to content

DX follow-ups: renderToJson wrapper parity + Menu text inputs#31

Merged
ivoIturrieta merged 5 commits into
mainfrom
fix/dx-followup-renderjson-menu
Jun 28, 2026
Merged

DX follow-ups: renderToJson wrapper parity + Menu text inputs#31
ivoIturrieta merged 5 commits into
mainfrom
fix/dx-followup-renderjson-menu

Conversation

@ivoIturrieta

@ivoIturrieta ivoIturrieta commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Follow-ups from testing fresh Opus agents generating designs on 0.1.11. None blocked them, but these were the recurring sharp edges.

1. renderToJson accepts a wrapper component (parity with renderToHtml)

renderToHtml(<MyEmail/>) renders a custom wrapper through React, but renderToJson(<MyEmail/>) walked the element tree and rejected any root that wasn't <Body>/<Email>/<Page>/<Document> — so the two renderers disagreed. 3 of 4 agents hit this. Now renderToJson unwraps a plain function-component root to its returned element (bounded loop; class/forwardRef/memo still get the clear root-type error). Tests added for the wrapper case and the still-invalid case.

2. Menu text inputs relaxed to match the other text components

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>. Relaxed to the shared agent-friendly inputs (Menu has no color/lineHeight field, so only these four). Type-only — values normalize at render time the same way as the other text components, and the result renders faithfully (font-family, font-size px, letter-spacing em). Guarded in the typecheck contract.

Verification

build ✅ · typecheck ✅ · 331 tests ✅ (+2). Verified against a packed build: renderToJson(<MyEmail/>) returns valid JSON, and Menu's arial/14px/0.08em all reach the output with no [object Object]/undefined.

Backward-compatible, type-/parity-only — fine as a patch.

Not included (separate, needs investigation): image-in-narrow-column sizing — the column renderer doesn't thread column width to the image exporter, so a documented maxWidth:"100%" thumbnail resolves to the full content-width cap. Tracking separately.


📖 Storybook Preview: https://unlayer.github.io/elements/pr/31/

ivoIturrieta and others added 2 commits June 27, 2026 19:00
…oHtml

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>
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>
Copilot AI review requested due to automatic review settings June 27, 2026 17:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses two recurring developer-experience friction points: (1) aligning renderToJson behavior with renderToHtml by supporting a wrapper root component, and (2) relaxing Menu text-style prop input types to match other text components.

Changes:

  • Added bounded unwrapping for plain function-component wrapper roots in renderToJson to reach <Body>/<Email>/<Page>/<Document>.
  • Added tests covering the wrapper success case and a wrapper that still resolves to an invalid root.
  • Relaxed Menu’s fontFamily/fontWeight/fontSize/letterSpacing prop types to use the shared agent-friendly text input types, with a typecheck contract guard.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
packages/react/src/utils/render-to-json.ts Adds wrapper-root unwrapping and centralizes valid root checks for renderToJson.
packages/react/src/utils/render-to-json.test.tsx Adds regression tests for wrapper-root parity and invalid-wrapper behavior.
packages/react/src/dx-types.test-d.tsx Extends DX type contract to assert relaxed Menu text input types compile.
packages/react/src/components/Menu.tsx Updates Menu semantic prop typing to accept shared text-style inputs (excluding unsupported fields).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/react/src/utils/render-to-json.ts
ivoIturrieta and others added 2 commits June 27, 2026 19:25
…cern

Public-repo hygiene — the comment now describes only this file's local head
type, not anything about how the rendering dependency is structured.
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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread packages/react/src/utils/render-to-json.ts
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>
@ivoIturrieta ivoIturrieta merged commit bc868dd into main Jun 28, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants