the preview takes the room, the controls take what they need

Edit put a fixed 420px band above a scrolling column, so the preview was a letterbox in the mode where
changes are actually being made — and there was empty panel below the controls while the thing being
judged was squeezed.

One column now, for both modes, and it never scrolls as a whole. The preview is flex-1, so it is always
as tall as whatever is left. The controls are shrink-0, so they are exactly as tall as their content
and no taller. Edit takes height from the preview and only as much as it needs.

Capped at 55% with their own scroll, because shrink-0 alone would let a short window hand the controls
everything and leave the preview at nothing — the same failure in the other direction.

The frame is also capped at the device's real height. A 390x844 phone in a 1200px panel would otherwise
be drawn as a phone 1200 pixels tall, which is not a phone. Past that it stops growing and the space
stays empty, which is honest: there is no device that shape.

Two modes were two return paths; now switching modes hides a block rather than rendering a different
component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 02:43:42 +01:00
co-authored by Claude Opus 5
parent 97aa78204b
commit 7d43517be0
+43 -24
View File
@@ -158,24 +158,21 @@ export const LayoutPanel = () => {
if (!draft) return <div className="p-4 text-xs text-muted-foreground">{error ?? 'Loading…'}</div>; if (!draft) return <div className="p-4 text-xs text-muted-foreground">{error ?? 'Loading…'}</div>;
/* /*
Viewing: the preview and nothing else, filling the panel. ONE layout for both modes: the preview takes the room, the controls take what they need.
Not the same thing as hiding the controls. The preview is given the whole height, which is what It used to be two — View returned the preview alone, and Edit put a fixed 420px band above a
makes View worth having — at edit height a phone is a letterbox, and a phone is the width somebody scrolling column. That made the preview a letterbox in the mode where you are actually changing
most wants to look at properly. things, with empty panel below the controls, and it meant switching modes was a different component
rather than a smaller one.
Now the column fills the panel and never scrolls as a whole. The preview is `flex-1`, so it is
always as tall as whatever is left; the controls are `shrink-0`, so they are exactly as tall as
their content and no taller. Edit takes height from the preview, and only as much as it needs.
*/ */
if (mode === 'view') {
return (
<div className="flex size-full flex-col p-4">
<LayoutPreview port={devPort} fill />
</div>
);
}
return ( return (
<div className="flex size-full flex-col gap-4 overflow-auto p-4"> <div className="flex size-full flex-col gap-3 p-4">
{saved && <p className="text-xs text-success">Saved</p>} {saved && <p className="shrink-0 text-xs text-success">Saved</p>}
{error && <p className="text-xs text-destructive">{error}</p>} {error && <p className="shrink-0 text-xs text-destructive">{error}</p>}
{/* {/*
The REAL page, embedded — not a drawing of it. The REAL page, embedded — not a drawing of it.
@@ -187,6 +184,16 @@ export const LayoutPanel = () => {
*/} */}
<LayoutPreview port={devPort} /> <LayoutPreview port={devPort} />
{/*
The controls, and only in Edit.
`shrink-0` so they never give up height to the preview, `max-h-[55%]` so they can never take
the whole panel either — on a short window the preview would otherwise be squeezed to nothing,
which is the failure this whole change is about. Past that they scroll on their own rather than
scrolling the preview off the top.
*/}
{mode === 'edit' && (
<div className="flex max-h-[55%] shrink-0 flex-col gap-4 overflow-auto">
{/* ── Colours ──────────────────────────────────────────────────────────────────────────── */} {/* ── Colours ──────────────────────────────────────────────────────────────────────────── */}
<section className="flex flex-col gap-3 rounded-lg border border-border p-4"> <section className="flex flex-col gap-3 rounded-lg border border-border p-4">
<h3 className="text-sm font-medium">Colours</h3> <h3 className="text-sm font-medium">Colours</h3>
@@ -250,6 +257,8 @@ export const LayoutPanel = () => {
))} ))}
</div> </div>
</section> </section>
</div>
)}
</div> </div>
); );
}; };
@@ -273,7 +282,7 @@ export const LayoutPanel = () => {
* was the other option and it is a guess about where the browser kept it; the platform tried that and * was the other option and it is a guess about where the browser kept it; the platform tried that and
* says so in its own comments. * says so in its own comments.
*/ */
const LayoutPreview = ({ port, fill }: { port: number | null; fill?: boolean }) => { const LayoutPreview = ({ port }: { port: number | null }) => {
const client = useClient(); const client = useClient();
/* /*
Through a ref, and this is the whole reason the preview hung. Through a ref, and this is the whole reason the preview hung.
@@ -289,7 +298,7 @@ const LayoutPreview = ({ port, fill }: { port: number | null; fill?: boolean })
const clientRef = useRef(client); const clientRef = useRef(client);
clientRef.current = client; clientRef.current = client;
const { device } = useViewMode(); const { device, mode } = useViewMode();
const { width, height } = DEVICES[device]; const { width, height } = DEVICES[device];
const [ready, setReady] = useState(false); const [ready, setReady] = useState(false);
@@ -345,13 +354,21 @@ const LayoutPreview = ({ port, fill }: { port: number | null; fill?: boolean })
*/ */
const scale = available.width ? Math.min(1, available.width / width) : 1; const scale = available.width ? Math.min(1, available.width / width) : 1;
// In View the frame gets the panel's height; editing, a fixed band, so the controls below stay put /*
// instead of moving every time somebody switches device. Whatever room is left, capped at the device's own height.
const shownHeight = fill ? Math.max(0, available.height) : Math.min(height * scale, 420);
The cap matters: a 390x844 phone in a 1200px-tall panel would otherwise be drawn as a phone 1200
pixels tall, which is not a phone. Past its real height the frame stops growing and the space stays
empty, which is honest — there is no device that shape.
It was a fixed 420px band in Edit. That made the preview a letterbox in the mode where changes are
being made, and left empty panel below the controls.
*/
const shownHeight = Math.max(0, Math.min(available.height, height * scale));
return ( return (
<section className={`flex flex-col gap-2 ${fill ? 'min-h-0 flex-1' : ''}`}> <section className="flex min-h-0 flex-1 flex-col gap-2">
<div ref={box} className={`min-h-0 ${fill ? 'flex-1' : ''}`}> <div ref={box} className="min-h-0 flex-1">
<div <div
className="mx-auto overflow-hidden rounded-lg border border-border bg-white" className="mx-auto overflow-hidden rounded-lg border border-border bg-white"
// The scaled footprint, so the border hugs the frame and the page below does not jump when // The scaled footprint, so the border hugs the frame and the page below does not jump when
@@ -382,8 +399,10 @@ const LayoutPreview = ({ port, fill }: { port: number | null; fill?: boolean })
</div> </div>
</div> </div>
{!fill && ( {/* Only while editing. In View the preview is the whole point, and a caption is a line of height
<p className="text-[10px] text-muted-foreground"> taken from it to say something the device buttons already show. */}
{mode === 'edit' && (
<p className="shrink-0 text-[10px] text-muted-foreground">
The development server, live at {width}px. Save to see a change here. The development server, live at {width}px. Save to see a change here.
</p> </p>
)} )}