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.