Skip to content

refactor(uve): explicit iframe sizing with DevTools-style canvas controls#35539

Open
fmontes wants to merge 62 commits intomainfrom
issue-35514-uve-iframe-sizing-phase-1
Open

refactor(uve): explicit iframe sizing with DevTools-style canvas controls#35539
fmontes wants to merge 62 commits intomainfrom
issue-35514-uve-iframe-sizing-phase-1

Conversation

@fmontes
Copy link
Copy Markdown
Member

@fmontes fmontes commented May 2, 2026

Summary

Closes #35514. Replaces the iframe height-observation pipeline with explicit, user-controllable iframe sizing modeled after Chrome DevTools' device toolbar.

The previous architecture derived the canvas height from a ResizeObserver + MutationObserver pipeline running inside the iframe document. Pages with viewport-relative units on a layout container (e.g. min-height: 100dvh) entered an infinite growth loop because the iframe's height was both an input to and an output of layout.

What changed

  • Iframe size is now state, not a measurement. New store fields viewIframeWidth/viewIframeHeight are user-driven (resize handles, numeric inputs, device presets).
  • Zoom scales the iframe's internal viewport, not its on-screen size. The iframe element's CSS box stays the user-set dimensions; transform: scale() adjusts what the page inside renders. Resize handles never move with zoom; the canvas never scrolls.
  • Resize handles on the right/bottom edges + corner; drag is clamped to the available canvas in responsive mode. Dragging from a device preset exits to responsive without snapping.
  • Numeric W × H inputs in the toolbar; editing while a device is active flips back to responsive.
  • Zoom select (50/75/100/150/200% + the current value when off-preset) replaces the +/- buttons.
  • Device presets auto-fit the iframe to the canvas with a computed zoom; orientation flip recomputes.
  • Reset button restores 100% zoom + canvas-size in responsive mode.
  • Side-panel toggling preserves the user's iframe size — only the auto-fill responsive default reflows when the canvas grows/shrinks.
  • Contentlet selection moved to the SDK. The SDK posts CONTENTLET_CLICKED (capture-phase, with preventDefault to suppress page navigation) and the editor uses it to drive selection. The hover overlay is now pointer-events: none so wheel/scroll/click pass through to the iframe — fixes the "blue overlay blocks scroll" and "page link fires on click" bugs. Second click on the same selected contentlet passes through, so internal links remain usable.
  • Hover overlay is now an outline + content-type label tab in the top-left, replacing the transparent-blue fill.
  • Selection clears on canvas resize and scroll so stale tool positions don't linger.
  • SDK cleanup — removes reportIframeHeight / observeDocumentHeight (no longer consumed). Editor keeps the IFRAME_HEIGHT no-op handler for backward compatibility with external SDK builds.
  • Toolbar polish — center slot is no-wrap with auto-margin centering; URL pill collapses to icon-only via container query when narrow; editor grid uses minmax(0, 1fr) so it doesn't bleed when both sidebars are open.

Test plan

  • Customer's min-height: 100dvh page renders without runaway growth — iframe scrolls internally
  • A page with a hero min-height: 100vh renders correctly
  • Zoom (10–300%) preserves on-screen iframe size; size inputs stay stable across zoom
  • Resize handles never move with zoom; clamp to canvas in responsive mode
  • Picking a device fits to canvas at auto-fit zoom; orientation recomputes
  • Picking desktop snaps back to canvas at 100% zoom
  • Resizing during a device preset exits without a visual jump; desktop highlights
  • Reset resets both zoom and iframe size in responsive mode
  • User-set iframe size survives toggling left/right side panels
  • Hovering shows outline + content-type label; mouse wheel scrolls the iframe through it
  • Clicking a contentlet selects it without firing page links inside
  • Internal links inside a selected contentlet remain clickable on a second click
  • Contentlet tools clear on canvas resize / scroll instead of lingering at stale positions
  • Both side panels open on a 14" laptop — editor fits without horizontal bleed
  • Toolbar overflow scrolls to start; URL pill collapses to icon below ~48rem
  • Contentlet tools / dropzone hide while resizing and reappear on release
  • Section-jump from the palette scrolls inside the iframe

Tests added

  • `withView.spec.ts` (~30 cases): iframe-size clamping, device-fit math, orientation refit, exit-preset semantics, zoom reset/snap, canvas-available-size rounding.
  • `dot-uve-iframe-resize-handles.component.spec.ts`: pointerdown/move/up + destroy-mid-drag cleanup.
  • `dot-uve-zoom-controls.component.spec.ts`: option list including off-preset values, change handler.
  • `dot-uve-contentlet-tools.component.spec.ts`: hover overlay rendering + content-type label.
  • `edit-ema-editor.component.spec.ts`: `handleSectionOffset` regression coverage.

🤖 Generated with Claude Code

fmontes added 30 commits April 30, 2026 09:49
Phase 1 of #35514. Removes the ResizeObserver/MutationObserver pipeline
that derived the canvas height from the iframe document. Pages with
viewport-relative units on layout containers (e.g. min-height: 100dvh)
caused an infinite growth loop because the iframe's height was both an
input to and an output of layout.

The iframe now has explicit dimensions (viewIframeWidth, viewIframeHeight)
that are user-controlled inputs, defaulting to 1520 x 1080. Content
taller than the iframe scrolls inside the iframe. Inside the iframe,
vh/dvh resolve against a fixed viewport, so there is no feedback loop.

Overlays will glitch on iframe scroll until phase 2 wires scroll-aware
clearing of contentlet bounds.
The iframe element used h-full but its host (<dot-uve-iframe>) had no
height, so 100% resolved against zero. Add w-full h-full to the host
class so it fills .iframe-wrapper.
…-sizing

WIP — more tweaks pending.

Adds DevTools-style explicit iframe sizing:
- viewIframeWidth / viewIframeHeight are now device-preset and user-driven
  (resize handles, numeric inputs), not derived from page content.
- New $viewIsResponsiveMode computed (true when no specific device preset).
- viewSetDevice / viewSetOrientation now set explicit pixel dimensions.
- viewSetIframeSize clamps to MIN_IFRAME_WIDTH / MIN_IFRAME_HEIGHT.

Editor:
- ResizeObserver on .canvas-viewport syncs iframe size to viewport in
  responsive mode. Effect resyncs when user exits a device preset.
- Removed legacy $iframeWrapperStyles override; wrapper always fills the
  store-sized .canvas-inner.
- Initial state width/height = 0; ngAfterViewInit applies real size before
  first paint to avoid scrollbar flash on load.

New components (Tailwind, no SCSS):
- <dot-uve-iframe-resize-handles>: full-length right/bottom bars + corner
  ball. Hidden in device mode. Drag delta divided by zoom.
- <dot-uve-iframe-size-input>: width × height numeric inputs in toolbar.
  Editing while a device is active flips back to responsive mode.
Previously the iframe could grow beyond what the canvas could show at the
current zoom — e.g. drag to 2000px at 50% zoom (visually fits in 1000px),
then zoom back to 100% and the canvas had to scroll.

Now in responsive mode the iframe size is clamped so iframe * zoom never
exceeds the canvas viewport. The clamp re-applies on every zoom change
(viewZoomSetLevel, in/out/reset all delegate to it) and on every drag /
numeric-input update. Device-mode keeps preset dimensions unchanged and
lets the canvas scroll if needed.

Adds viewCanvasAvailableWidth/Height to UVEState and
viewSetCanvasAvailableSize action; the editor's ResizeObserver feeds the
real (padding/gutter-excluded) canvas size into the store.
Reuses the existing scroll plumbing — updateEditorScrollState on pointer
down, updateEditorOnScrollEnd on pointer up — so editorBounds and
editorContentArea clear during the drag and restore on release.
The canvas row uses margin: 0 auto, so growing the iframe shifted the
right/bottom edge by only half the size delta — the cursor visibly
outpaced the handle.

Switch the drag math to measure the handle's current edge each frame
and grow the iframe by the cursor's distance from that edge. The handle
stays under the cursor whether the canvas is centered, left-anchored
after overflow, or transitioning between the two.
Drag and numeric-input updates now cap the iframe so its zoomed size
never exceeds the available canvas — no horizontal/vertical canvas
scrollbars in responsive mode. Device mode is unaffected (preset sizes
keep their dimensions; canvas scrolls if needed).
Switch .browser-url-bar-container from align-items: center to
align-items: stretch so all toolbar items match the tallest one. Drop
the redundant h-full on dot-uve-iframe-size-input — flex stretch
handles it.
- viewZoomSetLevel no longer reshapes the iframe — zooming in past the
  canvas just makes the canvas scroll (DevTools behavior). Size inputs
  stay stable while zooming.
- viewZoomReset now also snaps the iframe to the canvas viewport in
  responsive mode (device mode keeps its preset size). Reset becomes a
  one-click 'fit and zoom 100%'.
The iframe's on-screen size is now always the user-set dimensions
(handles never move with zoom). Zoom changes only the iframe's internal
viewport via inverse scaling: at 70% zoom the iframe is laid out at
dimensions/0.7 CSS pixels and scaled by 0.7 to fill the same on-screen
box. The page inside the iframe sees a larger or smaller viewport
depending on zoom; canvas never scrolls.
viewIframeWidth/Height now represent on-screen size (since zoom adjusts
the iframe's internal viewport, not its on-screen extent). Update both
the resize-handle drag math and the responsive-mode canvas clamp to
work directly in on-screen pixels — no more dividing by zoom — so the
iframe stays inside the canvas at any zoom.
…mode

- Picking a device computes a fit zoom so device dims × zoom fit the
  canvas; iframe gets the on-screen size and viewZoomLevel is set to
  match. Same logic on orientation flip.
- Resize handles now render in device mode too. Dragging from a device
  preset switches back to responsive (clears the device) so the canvas
  clamp and user-driven sizing take over.
The default device left zoom and size untouched, so it inherited values
from a previously-selected device. Picking it now snaps the iframe to
the available canvas viewport at 100% zoom — the expected responsive
default.
The zoom controls now expose a borderless p-select (50/75/100/150/200%)
instead of the discrete +/- buttons. Borders, background, padding and
focus ring are zeroed via [dt] design-token overrides so only the value
and chevron icon remain in the toolbar.
Add size="small" and zero the label's right padding so the value sits
flush against the chevron.
The size input and zoom select share a single rounded gray container
with thin dividers between sections. The reset button moved to the left
of the pill (inside it) since it now resets both zoom and canvas size.
Device presets compute an auto-fit zoom (e.g. 67%) that isn't in the
predefined list, so p-select rendered an empty label. The options list
now appends the current zoom when it isn't a preset, and the bound
value is rounded to match the option label exactly.
- dot-uve-device-controls now renders the device buttons inside
  p-buttongroup; the active device is filled and the rest are outlined.
- Drop the gray pill background from dot-uve-device-controls so the
  group sits flat on the toolbar.
- Tighten left padding on the merged size+zoom pill (the reset button
  brings its own padding).
Lock .browser-toolbar to flex-wrap: nowrap and prevent the start/end
slots from shrinking. The center slot, the URL container, and the URL
pill all get min-width: 0 so the URL bar can compress and ellipsis its
text on narrow toolbars instead of wrapping to a second row.
- Toolbar center now flex+gap and never wraps; uses auto-margin
  centering so its overflow scrolls all the way to the start.
- Browser-toolbar contains its scroll instead of bleeding the layout.
- :host grid uses minmax(0, 1fr) and .editor-content min-width: 0 so
  the editor track shrinks under both side panels at narrow widths.
- Device-controls reverts to plain rounded buttons in a gray pill
  (active = bg-gray-200), no host padding so buttons sit edge-to-edge.
- Round all toolbar p-button icons to text-gray-900 (copy-url, zoom
  reset, orientation, device buttons).
- Browser URL pill gets pr-3; size+zoom select padding zeroed via dt
  and label !p-0.
Use a container query on .p-toolbar-center: under 48rem the URL text
hides and the pill drops its padding/max-width so only the copy-link
icon button remains. Move the pill's right padding from HTML utility
class into SCSS so the query can clear it.
Switch .browser-url-pill from flex: 1 1 0 (always grow) to flex: 0 1 auto
so it sits at its content width up to max-width: 28rem and only shrinks
under toolbar pressure.
Restructure the copy-URL popover so each row stacks the (lighter
gray-700) label above the URL with a small gap, and places the copy
button to the right vertically centered with both. List items get more
breathing room and a divider line beneath each row.
Inject GlobalStore in the editor and append a third entry in
$pageURLS using the current site's hostname (from globalStore.siteDetails)
plus the current page path. Uses the i18n key
uve.toolbar.page.current.site.url.
Move the progress bar out of the toolbar's center template into a
zero-height sibling wrapper. The bar inside is absolutely positioned
just above its slot so it overlays the bottom edge of the toolbar
without pushing the toolbar contents around.
viewSetDevice(DEFAULT_DEVICE) snapped the iframe back to canvas at 100%
zoom, causing a visible jump the moment the user grabbed a resize
handle on a device preview.

Add viewExitDevicePreset that just clears the device flag without
changing size or zoom, and call it from the resize handles after
flipping editorState to SCROLLING — the responsive-mode sync effect
now skips its canvas-snap while scrolling, so the iframe stays
exactly where the user grabbed it.
The active-state check only matched on inode equality, so after exiting
a device preset (viewDevice = null) no button highlighted. Treat a
null device as the desktop preset so the desktop button reflects
'responsive mode' both on explicit selection and on auto-exit.
Switch handle elements from divs to <button> for semantics and a11y,
bump the bar thickness to 1rem (w-4 / h-4), and use a quieter gray
palette: base bg-gray-300, hover bg-gray-500, active bg-gray-600 for
all three handles.
Drop the .edit-panel-wrapper clamp min from 400px to 360px so the
sidebar fits more comfortably alongside the iframe canvas on smaller
displays.
Drop the per-tab utility classes and styleClass overrides on both the
palette and the right sidebar tablist; the design tokens already size
and lay out the tabs. Add a tablist background design token override
(var(--gray-100)) for both.
fmontes added 6 commits May 1, 2026 21:15
The canvas viewport is overflow:hidden after the iframe-sizing
refactor, so canvasViewport.nativeElement.scrollTo was a no-op and
clicking a section node in the palette didn't move the editor.

Move the scroll target to the iframe's contentWindow (the iframe is
what scrolls now) and drop the zoom-factor multiplication — offsetTop
is already in the iframe's CSS coordinate space.

Extract the inline onSectionOffset callback into a protected
handleSectionOffset method and add three tests covering the iframe
target, negative-clamp, and missing-iframe paths.
If the component was destroyed while the user was actively dragging a
resize handle (e.g. navigation away mid-drag), the pointermove/up/cancel
listeners stayed attached to the handle and editorState stayed RESIZING.

Switch to AbortController-scoped listeners and tie aborting to
DestroyRef.onDestroy so the in-flight drag is torn down cleanly:
listeners detach via the signal, pointer capture is released if still
held, and editorState flips back to IDLE through updateEditorOnResizeEnd.

Adds a component spec covering pointerdown ordering, pointermove math,
pointerup release, and the destroy-mid-drag cleanup path.
…zoom

computeDeviceFit clamped the zoom to 10% but kept the on-screen size at
device * rawFit. With raw fit < 0.1 (canvas much smaller than the device)
the iframe ended up at e.g. device * 0.05 with zoom = 10, so the page
inside saw iframe / zoom = device * 0.5 — half of the device's CSS
dimensions. Device previews were silently broken on tiny canvases.

Re-derive the on-screen size from the *clamped* fit so iframe / zoom
always equals the device's CSS dimensions exactly. The trade-off:
the iframe may visually exceed the canvas (clipped by overflow:hidden)
when the canvas is too small to fit the device at the minimum zoom —
better than rendering the wrong page layout.

Adds a regression test for the clamped-zoom case.
The interface declared device, orientation and seo as non-nullable
strings/Orientation, but viewExitDevicePreset, viewSetSEO, and
viewClearDeviceAndSocialMedia all set them to null at runtime to clear
the corresponding preview mode. The mismatch was masked because no
strict-null check is enforced project-wide, but it was a real type
contract bug that could surface on any future strictness bump.

Make all three fields nullable and update the actions to:
- coerce undefined orientation to null in viewSetDevice
- read viewParams() once per action and produce a fully-typed object
- drop the '...store.viewParams() || {}' fallback that produced
  partial objects which couldn't satisfy the new type.
The hook only ran a self-contradicting null-check on uveStore — inject()
throws before lifecycle hooks fire, so the branch was unreachable. Drop
the method and the OnInit interface implementation.
Add a spec for DotUveZoomControlsComponent covering:
- Standard preset list when current zoom matches a preset
- Off-preset (e.g. 67% from device auto-fit) appended sorted
- Reactivity to zoom signal changes
- onZoomChange forwards to viewZoomSetLevel
- $viewZoomLevelPct passes through unmultiplied
fmontes added 2 commits May 1, 2026 21:41
When a side panel opened or closed the canvas viewport changed width,
which fired the ResizeObserver — the editor unconditionally called
viewSetIframeSize(canvasSize) and clobbered any custom size the user
had typed in the inputs or dragged via the resize handles.

Snapshot the previous canvas size before updating it. If the iframe
size matches the previous canvas, treat it as auto-filling and follow
the new canvas. Otherwise the user has resized — keep their size, but
re-apply it through viewSetIframeSize so the canvas-clamp can shrink
it when the canvas shrinks below the user's size.
The size-input component had to call viewExitDevicePreset and
viewSetIframeSize back-to-back to handle the user-types-while-device-active
case. That orchestration belongs in the store, not the caller.

Add viewResizeIframe(size) that delegates to the existing actions
internally — exits the device preset (size-preserving) and applies the
new dimension. Keep viewSetIframeSize as the low-level setter for paths
that legitimately should not exit the device preset (canvas-viewport
sync, resize-handles drag where exit already happened on pointerdown).

Update the size input to use the new method and add two tests covering
the exit-and-set flow and the responsive-mode no-op.
The editor no longer derives canvas size from the iframe document
(phase 1 replaced that pipeline with explicit user-controlled iframe
sizing). The SDK was still observing the document height and posting
IFRAME_HEIGHT messages that the editor now ignores — pure overhead.

Remove the SDK side of the contract:
- Delete reportIframeHeight, shouldReportIframeHeightToParent, and the
  observeDocumentHeight observer (+ its spec).
- Drop the call sites in sdk-editor.ts and lib/editor/public.ts.
- Drop the document-height-observer barrel re-export in internal.ts.
- Update script/utils.spec.ts and lib/editor/public.spec.ts.

The editor keeps its IFRAME_HEIGHT no-op postMessage handler so any
external SDK consumers still emitting this action remain harmless.
@github-actions github-actions Bot added Area : SDK PR changes SDK libraries and removed AI: Safe To Rollback labels May 2, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 2, 2026

Pull Request Unsafe to Rollback!!!

  • Category: M-3 — REST / GraphQL / Headless API Contract Change
  • Risk Level: 🟡 MEDIUM
  • Why it's unsafe: Two content REST endpoints had their authentication requirement relaxed from AnonymousAccess.NONE (always requires an authenticated user) to AnonymousAccess.READ (allows anonymous access). The 403 "Forbidden" response code was also removed from the OpenAPI spec for one of these endpoints. If this release is rolled back to N-1, any client that deployed against the new contract and relies on anonymous access to these endpoints will immediately receive 401 errors. The Angular UVE frontend is updated in the same PR to operate in contexts (e.g. preview mode) where the viewer may not be authenticated, so rolling back the backend without the frontend would break those flows.
  • Code that makes it unsafe:
    • dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java line ~459: .requiredAnonAccess(AnonymousAccess.NONE).requiredAnonAccess(AnonymousAccess.READ) (content counts endpoint)
    • dotCMS/src/main/java/com/dotcms/rest/api/v1/content/ContentResource.java line ~512: .requiredAnonAccess(AnonymousAccess.NONE).requiredAnonAccess(AnonymousAccess.READ) (content references endpoint)
    • dotCMS/src/main/webapp/WEB-INF/openapi/openapi.yaml ~line 7374: removed 403 response entry for the references endpoint
  • Alternative (if possible): Use the two-phase approach — in N, keep both auth paths working (e.g. allow anonymous but also handle authenticated requests identically). In N+1, remove the AnonymousAccess.NONE fallback once N-1 is outside the rollback window. Alternatively, keep AnonymousAccess.NONE on the server but let the Angular frontend supply a session cookie / token so the authenticated-only path is never hit anonymously.

fmontes and others added 8 commits May 1, 2026 23:15
When a side panel toggled, the canvas changed size and the iframe
reflowed internally — but the floating contentlet-tools stayed pinned
to the old coordinates. The user's quick-edit selection (preserved in
the store as editorActiveContentlet) was fine, but the visible tools
sat at the wrong place.

Two changes:
- Editor's canvas observer now flips editorState to RESIZING when the
  canvas size actually changes, then to IDLE on the next animation
  frame and re-requests bounds — so containers/dropzone re-anchor.
- DotUveContentletToolsComponent listens to editorState and clears its
  local selectedContentletArea on RESIZING/SCROLLING. The tools
  disappear during the transition and re-anchor on the next hover or
  click; the quick-edit panel data is untouched.
…-through

The contentlet hover overlay used to capture clicks (and wheels), which
blocked iframe scrolling and the page's native click behavior. Move
selection to the SDK side instead:

- New CONTENTLET_CLICKED UVE event in @dotcms/types + handler in the SDK
  (capture phase, preventDefault) that posts SET_SELECTED_CONTENTLET to
  the editor.
- New editorSelectedContentletArea state + setSelectedContentletArea
  store action; the action handler also calls setActiveContentlet so
  the quick-edit panel opens.
- DotUveContentletToolsComponent reads selectedContentletArea from the
  store; local signal + handleClick deleted. Editor's
  handleSelectedContentlet callback removed.
- .bounds.hover gets pointer-events: none so wheel and clicks pass
  through to the iframe.
- SDK suppresses the page's click on first selection but lets a second
  click on the SAME contentlet through, so users can interact with
  links/buttons inside the selected contentlet.
- updateEditorScrollState / updateEditorResizeState also clear
  editorSelectedContentletArea since the bounds go stale.
- Document SET_CONTENTLET (hover) vs SET_SELECTED_CONTENTLET (click).

Includes a rebuilt dot-uve.js.
Selection used to be triggered by spectator.click(hoverBounds); the
click flow moved to the SDK in the previous commit, so the spec now
mocks UVEStore with a writable editorSelectedContentletArea signal and
sets it directly to drive selected-state assertions. Drops the now-dead
'click hoverBounds to re-select' boilerplate from each test.
Hover overlay now shows a 2px primary outline with a small label tab in
the top-left corner displaying the contentlet's content type, instead of
the transparent-blue fill. The label is offset -2px to align with the
outline's outer edge.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switching device presets reflows the iframe and moves contentlets, but
viewSetDevice patches state directly without flipping editorState. The
selected/hover overlays were left pointing at stale coordinates.

Mirror the canvas-resize behavior: an effect on viewDevice/orientation
flips to RESIZING (hides overlays + clears editorSelectedContentletArea)
then back to IDLE on the next frame so REQUEST_BOUNDS re-emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the stale-overlay cleanup for device/orientation switches into the
store's atomic patchState calls (viewSetDevice, viewSetOrientation)
instead of the editor component's effect. The store is the right place
to mutate editor state; the component only needs to handle the IDLE
recovery + REQUEST_BOUNDS round-trip on the next frame.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related defensive fixes around the click→select path:

- $showContentletControls now also renders the contentlet-tools host
  when a selected area exists, not only when a hovered area does. This
  was breaking selection for headless contentlets that re-render their
  hovered DOM node on click — the click set the selection, but the host
  component was unmounted because editorContentArea was null.

- $reloadEditorContent gets a structural equal so downstream effects
  don't fire when none of (code, pageType, enableInlineEdit) actually
  changed. Click handlers don't write any of these, but the upstream
  pageAsset signal version still bumps in some flows, which used to
  re-fire UVE_SET_PAGE_DATA on every selection. The follow-up bug —
  where that reload also produced a one-frame empty image identifier
  in the consumer's React tree — is tracked separately.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Not Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code Area : SDK PR changes SDK libraries

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

refactor(uve): adopt DevTools-style iframe sizing to eliminate height feedback loop

1 participant