Skip to content

fix: keep html/body sized to the composition root, warn when a scaffold's copies drift - #4005

Merged
miguel-heygen merged 1 commit into
mainfrom
fix-portrait-body-clip-4001
Sep 16, 2026
Merged

miguel-heygen merged 1 commit into
mainfrom
fix-portrait-body-clip-4001

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

Summary

A composition whose root declares portrait data-width/data-height (e.g. 1080/1920) renders at the correct output size — that always comes from the root's own attributes — but a project scaffolded landscape (hyperframes init without --resolution) still ships html, body CSS and a <meta viewport> at the old landscape size. The stale body's overflow: hidden then visually clips the correctly-sized root at the old height, so everything past it renders as the page background instead of real content. hyperframes lint/check don't catch it: their layout audits measure against the root's own (already-correct) rect, not the body's.

Runtime fix (packages/core/src/runtime/init.ts): applyCompositionSizing already computes forcedWidth/forcedHeight from the root's data attributes to size the root element itself. This change mirrors the SAME already-computed values onto document.documentElement/document.body too, reusing rather than recomputing them. overflow: hidden on html/body is left untouched — it's set unconditionally earlier in the same init pass specifically to keep browser-default margins from bleeding into renders as white bars, and once body's own size agrees with the root it contains, that clip doesn't hide anything real. findRootCompositionEl resolves the outermost [data-root="true"] composition, so a sub-composition's own CSS (compiler-scoped elsewhere) is never touched. Both the preview/Studio path and the render/capture path load the one bundled runtime artifact this file builds into, so a single fix covers both by construction.

The <meta name="viewport"> tag has no effect on the capture path: frameCapture.ts sets the headless Chromium capture viewport directly via CDP (page.setViewport()), independent of any HTML viewport meta, which is a mobile-browser/touch-emulation hint that a non-mobile-emulated headless context ignores.

Lint rule (packages/lint/src/rules/core.ts): new warning-level root_dimensions_mismatch, next to root_missing_dimensions. Compares the root's data-width/data-height against the html, body CSS block and the viewport meta — the two other places a scaffold's resolution lives. Fires on a document whose scaffold copies have drifted; a sub-composition fragment has neither an html/body nor a viewport meta to compare against, so this is naturally top-level-only without a separate scoping guard.

The two regex patterns this rule reads are extracted into packages/parsers/src/canvasScaffoldPatterns.ts (re-exported from that package's root) rather than duplicated inline, since @hyperframes/lint cannot depend on @hyperframes/cli (the reverse is true), and @hyperframes/parsers is a dependency both already share. @hyperframes/cli's applyResolutionPreset keeps its own prefix/suffix-capturing regexes for its REPLACE operation on these same two locations — a different shape than these read-only, value-capturing patterns — so the two aren't literally the same regex objects, but they track the same two locations and the new file's doc comment says so explicitly rather than overclaiming a shared implementation.

Cost: both checks run once — the runtime mirroring during the composition's own init pass (no new browser session, no new DOM pass beyond what applyCompositionSizing already does), the lint rule once per hyperframes lint invocation on the top-level file. Neither touches a hot/per-frame path.

Out of scope, follow-up: skills/general-video/SKILL.md and skills/motion-graphics/SKILL.md calling hyperframes init without passing --resolution from the brief — that's the other half of preventing this class of mistake at the source, not touched here.

Test plan

  • packages/core/src/runtime/init.test.ts: new test constructs a portrait root (1080x1920) inside a document whose html/body are still sized 1920x1080 (mimicking the scaffolded landscape template), calls the runtime init, and asserts documentElement/body end up at 1080x1920 to match the root.
  • packages/lint/src/rules/core.test.ts: 3 new tests — fires when html/body CSS and the viewport meta are still the scaffolded landscape size (including the height-before-width CSS ordering); does not fire on a sub-composition fragment with no html/body/viewport to compare against; does not fire when they already agree.
  • Manually reproduced the reported symptom end-to-end with a real render, before and after the runtime fix, using a fixture shaped exactly like the reporter's gist (portrait root, red background, a bottom-anchored green bar, landscape html/body left unedited): before the fix, sampling the output frame's center column showed the root's red (#c0392b) through y≈1080 and then the page's own dark background color (#0a0a0a) for every y beyond that — the green bar never appears. After the fix, the same column shows red through the full portrait height and the green bar (#27ae60) correctly visible at the bottom, with overflow: hidden left in place throughout.
  • oxlint/oxfmt clean on all touched files. packages/lint's full core.test.ts suite passes (81/81) locally. packages/core's runtime typecheck (tsc --noEmit -p tsconfig.runtime.json) is clean.

Closes #4001.

@develoverli

Copy link
Copy Markdown

Thanks for the quick fix! Happy to pick up the follow-up (passing --resolution from the brief in the workflow init calls) once this lands, if that's useful.

…en a scaffold's copies drift

A composition whose root declares portrait data-width/data-height renders
at the correct output size, but a project scaffolded landscape (init
without --resolution) still ships html/body CSS and a viewport meta at the
old landscape size. The stale body's overflow:hidden then clips the
correctly-sized root at the old height, so everything past it renders as
page background instead of real content.

applyCompositionSizing now mirrors the SAME forced width/height it already
computes for the root onto documentElement/body, reusing rather than
recomputing them. overflow:hidden is left untouched (a deliberate
anti-white-bar guard set earlier in the same init pass): once body's own
size agrees with the root it contains, it clips nothing that matters.
Runs in both preview and render, since both load the one bundled runtime
artifact this file builds into.

Adds a warning-level root_dimensions_mismatch lint rule alongside
root_missing_dimensions, comparing the root's dims against the html/body
CSS block and the viewport meta - the two other places a scaffold's
resolution lives besides the root itself. The read-only regexes live in
@hyperframes/parsers (packages/lint cannot depend on packages/cli, which
owns the closest existing pattern in applyResolutionPreset) so a future
caller has one place to update if the scaffold's shape changes, rather
than a rule silently comparing against a guess.

Closes #4001.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
@miga-heygen
miga-heygen force-pushed the fix-portrait-body-clip-4001 branch from ae8828e to b4fbc3e Compare September 16, 2026 16:14
@miguel-heygen
miguel-heygen merged commit eb0957a into main Sep 16, 2026
59 checks passed
@miguel-heygen
miguel-heygen deleted the fix-portrait-body-clip-4001 branch September 16, 2026 16:37
miga-heygen added a commit that referenced this pull request Sep 16, 2026
- Portrait compositions render the full frame when the project was
  scaffolded at a different size: html/body are sized to the composition
  root at runtime; new lint warning root_dimensions_mismatch (#4005,
  fixes #4001)
- render_complete telemetry adds composition_element_tags,
  aroll_video_count, heygen_video_count; render_error adds error_name and
  failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
  search no longer shows [object Object] (#4006)

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
miguel-heygen added a commit that referenced this pull request Sep 16, 2026
- Portrait compositions render the full frame when the project was
  scaffolded at a different size: html/body are sized to the composition
  root at runtime; new lint warning root_dimensions_mismatch (#4005,
  fixes #4001)
- render_complete telemetry adds composition_element_tags,
  aroll_video_count, heygen_video_count; render_error adds error_name and
  failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
  search no longer shows [object Object] (#4006)

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
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.

Portrait render shows only the top ~1080px when html/body CSS keeps the scaffold's 1920×1080 size

3 participants