the preview is as tall as the page

The frame took all the room the panel had, so a short page left a band of white
under the footer with the measure guides running down through it. An iframe has
no height: fit-content, so the page is measured inside it and the frame follows.

The wrapper, not the body — body is min-h-screen, so measuring it would ask the
frame how tall the frame is.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 07:29:08 +01:00
co-authored by Claude Opus 5
parent 6242fb03a6
commit 6717402331
+44 -8
View File
@@ -232,17 +232,53 @@ export const SitePreview = ({
/*
All the room there is. WIDTH is the device; height is just how much of the page you can see.
The page's own height, measured inside the frame.
It was capped at the device's own height, on the reasoning that a 390x844 phone drawn 1200 pixels
tall is not a phone. That was wrong, and the empty band under the preview was the tell: media
queries answer WIDTH, so the layout at 390 is the phone's layout whatever the height. A taller frame
shows more page, which is a scroll position rather than a different device.
── Why it is measured and not just "the room" ──
One real consequence, and it is the right one: the footer is `min-h-dvh`, so it sits at the bottom
of the FRAME. A taller frame therefore puts it lower — exactly as a taller browser window does.
It was `available.height` — all the room the panel had — which is right for a page longer than the
panel and wrong for every other one: a short page left a band of white below the footer with the
measure guides running down through it. Nothing was broken; the frame was simply taller than the
document, and an iframe has no `height: fit-content`.
── The WRAPPER, not the body ──
The site's `<body>` is `min-h-screen`, so its height is the FRAME's height and measuring it would
ask the frame how tall the frame is. Its one child is the page — header, main, footer — and that is
a real content height. It stopped being `min-h-dvh` on the same day this changed, for the same
reason: the stretch belonged to nothing anybody could see or set.
*/
const shownHeight = Math.max(0, available.height);
const [contentHeight, setContentHeight] = useState(0);
useEffect(() => {
if (!loaded) return;
const frameDocument = frame.current?.contentDocument;
const page = frameDocument?.body.firstElementChild as HTMLElement | null;
const view = frameDocument?.defaultView;
if (!page || !view) return;
const measure = () => setContentHeight(Math.ceil(page.getBoundingClientRect().height));
measure();
/*
Observed, because the page changes height without reloading: choosing a spacing rewrites classes
in this document, and the device buttons reflow it at a new width.
No feedback loop — the frame's height is set from this, and the wrapper's height does not depend
on the frame's.
*/
const observer = new view.ResizeObserver(measure);
observer.observe(page);
return () => observer.disconnect();
}, [loaded]);
/*
As tall as the page, or as tall as the room, whichever is less.
A long page still fills the panel and scrolls, exactly as it did. WIDTH is the device and height is
not: media queries answer width, so the layout at 390 is the phone's layout whatever the height.
*/
const shownHeight = contentHeight ? Math.min(contentHeight, Math.max(0, available.height)) : Math.max(0, available.height);
/*
When the frame is wider than the room, show its MIDDLE.