Skip to content

Fix/paragraph margins and full document html - #36

Merged
ivoIturrieta merged 11 commits into
mainfrom
fix/paragraph-margins-and-full-document-html
Jul 4, 2026
Merged

Fix/paragraph margins and full document html#36
ivoIturrieta merged 11 commits into
mainfrom
fix/paragraph-margins-and-full-document-html

Conversation

@ivoIturrieta

@ivoIturrieta ivoIturrieta commented Jul 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

What does this PR do? Keep it brief.

Changes

Test Plan

  • pnpm build passes
  • pnpm test passes
  • Tested in Storybook (if UI change)
  • Added/updated tests for new behavior

Notes

Any additional context for reviewers.


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

ivoIturrieta and others added 4 commits July 4, 2026 09:24
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>
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>
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>
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>
Copilot AI review requested due to automatic review settings July 4, 2026 07:28

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 updates the rendering pipeline to (1) ensure paragraphs render with email-client-safe zero margins and (2) make renderToHtml() return a complete HTML document (doctype → </html>) with per-mode shells aligned to the editor’s exported HTML, plus adds a Chromium-based E2E gate to validate the rendered output.

Changes:

  • Add Lexical paragraph HTML serialization that enforces inline <p> margin resets and supports alignment/indent/dir.
  • Change renderToHtml() to emit full document shells (email/web/document), add RenderToHtmlOptions (title, fonts), and adjust related tests/snapshots/docs.
  • Update Row responsive CSS behavior/breakpoint and add a Playwright-driven browser E2E test + baseline + CI step.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
README.md Updates docs to reflect full-document renderToHtml() output and mention renderToHtmlParts().
packages/shared/src/utils/lexical-helpers.ts Adds paragraph HTML serialization with margin resets, alignment/indent handling.
packages/shared/src/utils/lexical-helpers.test.ts Adds unit coverage for updated Lexical → HTML behavior.
packages/react/src/utils/render-to-html.tsx Implements full-document renderToHtml() + options, refactors body rendering, updates renderToPlainText/parts behavior.
packages/react/src/utils/render-to-html.test.tsx Adds tests for full document shells and mode resolution behavior.
packages/react/src/utils/extract-head.test.tsx Updates assertions to reflect full-document wrapping behavior.
packages/react/src/utils/document-layouts.ts Introduces per-mode document shell builders (email/web/document).
packages/react/src/index.ts Exports RenderToHtmlOptions type.
packages/react/src/golden-template.test.tsx Adds Document-mode golden template coverage.
packages/react/src/dx-behaviors.test.tsx Updates/extends DX expectations around paragraph output.
packages/react/src/components/Row.tsx Updates responsive flex stacking CSS and default breakpoint.
packages/react/src/components/snapshots/snapshots.test.tsx.snap Snapshot updates for paragraph margin resets and responsive CSS.
packages/react/src/snapshots/golden-template.test.tsx.snap Snapshot updates to lock full-document HTML output.
packages/react/scripts/browser-e2e.mjs Adds Playwright Chromium E2E gate validating rendered DOM/computed styles and regressions.
packages/react/scripts/browser-e2e-baseline.json Adds deterministic computed-style baseline for the E2E gate.
packages/react/README.md Updates package docs for full-document renderToHtml() and new options.
packages/react/package.json Adds test:e2e script.
CLAUDE.md Updates internal docs for new layouts, E2E gate, and updated size budget.
.github/workflows/test.yml Adds Browser E2E gate step and adjusts bundle size budget.

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

Comment thread packages/shared/src/utils/lexical-helpers.ts Outdated
Comment thread packages/react/src/components/Row.tsx
ivoIturrieta and others added 2 commits July 4, 2026 09:57
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>
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>

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 19 out of 19 changed files in this pull request and generated 1 comment.

Comment thread packages/react/scripts/browser-e2e.mjs Outdated
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>

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 19 out of 19 changed files in this pull request and generated 2 comments.

Comment thread packages/react/src/utils/render-to-html.tsx
Comment thread packages/react/src/utils/render-to-html.tsx
ivoIturrieta and others added 4 commits July 4, 2026 10:17
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>
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>
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>
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>
@ivoIturrieta
ivoIturrieta merged commit 49bab75 into main Jul 4, 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants