From dc45fb7f11af209e50cbe56c84708ee093cd6d0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 31 Aug 2026 04:56:26 +0100 Subject: [PATCH] Layout shows the measure too The content column's edges were drawn only by the page builder, and they answer a question the Layout section has just as much: the page is held to max-w-7xl and centred, so on a wide screen the content stops well short of the window and nothing marks where. `paintMeasure` is its own function now, called by both. The builder also draws sections, columns and a drop zone; none of those belong in Layout, whose bands are code and cannot be rearranged from there. What the two share is exactly the measure and nothing else. Nothing in View, for the same reason as the builder: chrome of any kind means you are not looking at the page. Co-Authored-By: Claude Opus 5 (1M context) --- web/canvas.ts | 159 +++++++++++++++++++---------------- web/sections/LayoutPanel.tsx | 18 +++- 2 files changed, 104 insertions(+), 73 deletions(-) diff --git a/web/canvas.ts b/web/canvas.ts index 2cab013..8dfc691 100644 --- a/web/canvas.ts +++ b/web/canvas.ts @@ -299,78 +299,8 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void { }); } - /* - The measure, drawn as two lines. - - The page is held to `max-w-7xl` and centred, so on a wide screen the content stops well short of the - window and there is nothing to say where. That is invisible until something is placed against it and - turns out to be in the wrong place, which is the moment it is expensive. - - `position:fixed` inside the frame, so they stay put while the page scrolls — a guide that scrolls - away is a guide for the top of the page only. Redrawn on resize, because the frame's width changes - when the device does. - */ - const column = root.querySelector('[data-content-column]'); - if (column) { - const guides: HTMLElement[] = []; - - const place = () => { - for (const guide of guides) guide.remove(); - guides.length = 0; - - const box = column.getBoundingClientRect(); - // Below the frame's own width there is no measure to show — the column simply IS the page, and - // two lines against the edges would be noise claiming to be information. - if (box.width >= (document.documentElement.clientWidth || 0) - 1) return; - - for (const [side, x] of [ - ["left", box.left], - ["right", box.right], - ] as const) { - const guide = document.createElement("div"); - guide.setAttribute(MARK, `measure-${side}`); - style(guide, [ - "position:fixed", - "top:0", - "bottom:0", - `left:${Math.round(x)}px`, - "width:1px", - `background:color-mix(in srgb, ${ACCENT} 35%, transparent)`, - // Behind nothing and in front of nothing that matters: it must not swallow a click meant for - // the page underneath it. - "pointer-events:none", - "z-index:2147483000", - ]); - document.body.append(guide); - guides.push(guide); - } - - const label = document.createElement("div"); - label.setAttribute(MARK, "measure-label"); - label.textContent = `${Math.round(box.width)}px`; - style(label, [ - "position:fixed", - "top:0.25rem", - `left:${Math.round(box.left + 4)}px`, - "font-size:0.625rem", - "font-family:ui-sans-serif, system-ui, sans-serif", - `color:${ACCENT}`, - "opacity:0.8", - "pointer-events:none", - "z-index:2147483000", - ]); - document.body.append(label); - guides.push(label); - }; - - place(); - const frameWindow = document.defaultView; - frameWindow?.addEventListener("resize", place); - undo.push(() => { - frameWindow?.removeEventListener("resize", place); - for (const guide of guides) guide.remove(); - }); - } + // The measure. Drawn for anything that shows a page — see paintMeasure. + undo.push(paintMeasure(document)); // ── Add section, always last ───────────────────────────────────────────────────────────────── /* @@ -427,3 +357,88 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void { for (const step of undo.reverse()) step(); }; } + +/** + * The content column's edges, as two lines and a width. + * + * ── Why this is on its own ── + * + * Every preview wants it and only one of them wants the rest. The page builder draws sections, columns + * and a drop zone; the Layout section draws none of those — its bands are code and nothing there can be + * rearranged — but it has exactly the same question about where the measure ends. + * + * ── Why it needs saying at all ── + * + * The page is held to `max-w-7xl` and centred, so on a wide screen the content stops well short of the + * window and nothing marks where. That is invisible until something is placed against the edge and + * turns out to be in the wrong place, which is the moment it becomes expensive. + * + * Returns a cleanup, like everything else injected here. + */ +export function paintMeasure(document: Document): () => void { + const root = document.querySelector('main'); + const column = root?.querySelector('[data-content-column]'); + if (!column) return () => {}; + + const guides: HTMLElement[] = []; + + const place = () => { + for (const guide of guides) guide.remove(); + guides.length = 0; + + const box = column.getBoundingClientRect(); + // Below the frame's own width there is no measure to show — the column simply IS the page, and two + // lines against the edges would be noise claiming to be information. + if (box.width >= (document.documentElement.clientWidth || 0) - 1) return; + + for (const [side, x] of [ + ['left', box.left], + ['right', box.right], + ] as const) { + const guide = document.createElement('div'); + guide.setAttribute(MARK, `measure-${side}`); + style(guide, [ + // `fixed`, so they stay while the page scrolls. A guide that scrolls away is a guide for the + // top of the page only. + 'position:fixed', + 'top:0', + 'bottom:0', + `left:${Math.round(x)}px`, + 'width:1px', + `background:color-mix(in srgb, ${ACCENT} 35%, transparent)`, + // It must not swallow a click meant for the page underneath it. + 'pointer-events:none', + 'z-index:2147483000', + ]); + document.body.append(guide); + guides.push(guide); + } + + const label = document.createElement('div'); + label.setAttribute(MARK, 'measure-label'); + label.textContent = `${Math.round(box.width)}px`; + style(label, [ + 'position:fixed', + 'top:0.25rem', + `left:${Math.round(box.left + 4)}px`, + 'font-size:0.625rem', + 'font-family:ui-sans-serif, system-ui, sans-serif', + `color:${ACCENT}`, + 'opacity:0.8', + 'pointer-events:none', + 'z-index:2147483000', + ]); + document.body.append(label); + guides.push(label); + }; + + place(); + // The frame's width changes when the device does, and a remount is not the only way that happens. + const frameWindow = document.defaultView; + frameWindow?.addEventListener('resize', place); + + return () => { + frameWindow?.removeEventListener('resize', place); + for (const guide of guides) guide.remove(); + }; +} diff --git a/web/sections/LayoutPanel.tsx b/web/sections/LayoutPanel.tsx index 4952f19..ac43dc1 100644 --- a/web/sections/LayoutPanel.tsx +++ b/web/sections/LayoutPanel.tsx @@ -4,6 +4,7 @@ import { readableError } from '../errors'; import { useFlash } from '../useFlash'; import { useSaveTarget } from '../SaveTarget'; import { usePreviewable, useViewMode } from '../ViewMode'; +import { paintMeasure } from '../canvas'; import { SitePreview, useDevPort } from '../SitePreview'; /** @@ -175,6 +176,21 @@ export const LayoutPanel = () => { usePreviewable(); const { mode } = useViewMode(); + /* + The measure, and nothing else. + + The page builder also draws sections, columns and a drop zone. None of those belong here: the + layout's bands are code and nothing in this panel can rearrange them. What it shares is the + question of where the content column ends, which is just as invisible on this page as on any other. + + Nothing in View, for the same reason as the builder — chrome of any kind means you are not looking + at the page. + */ + const onDocument = useCallback( + (frameDocument: Document) => (mode === 'view' ? undefined : paintMeasure(frameDocument)), + [mode], + ); + if (!draft) return
{error ?? 'Loading…'}
; /* @@ -202,7 +218,7 @@ export const LayoutPanel = () => { that sits in the content column. Worse, it was a second implementation of the layout, so it would have gone on being wrong differently every time either side changed. */} - + {/* The controls, and only in Edit.