the preview is 1:1, and the measure is visible

TWO CHANGES, and the first is an argument I got wrong.

The frame was scaled to fit the panel with a transform. That made the preview change apparent size as
the panel was dragged, so the same page looked different at every panel width and nothing could be
judged against anything. A preview is 1:1 or it is not a preview.

So it stays at the device's real width and OVERFLOWS when there is not room — a scrollbar rather than a
lie. The width was never negotiable anyway: the site's media queries answer it, so a frame given the
panel's width renders the tablet layout and calls it desktop. What changes is only what happens when it
does not fit, and scrolling is the honest answer.

THE MEASURE, as two fixed lines and a width in pixels.

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 says 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.

Fixed rather than absolute, so they stay while the page scrolls; a guide that scrolls away is a guide
for the top of the page only. Not drawn at all when the column IS the full width, because two lines
against the window edges are noise claiming to be information.

One more site marker: data-content-column. Four attributes now, still no behaviour.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 04:23:14 +01:00
co-authored by Claude Opus 5
parent 66812587fd
commit 01fd0807c6
2 changed files with 95 additions and 26 deletions
+80 -4
View File
@@ -13,9 +13,10 @@
*
* ── What the SITE has to provide, and it is only this ──
*
* data-page-empty the placeholder shown when a page has no sections
* data-section-id which row a band came from
* data-column a column within a band
* data-page-empty the placeholder shown when a page has no sections
* data-section-id which row a band came from
* data-column a column within a band
* data-content-column the wrapper that holds the page to its measure
*
* Three attributes and no behaviour. Markers rather than classes, so they cannot be mistaken for
* styling, cannot be purged by a build that has never heard of this file, and read as what they are:
@@ -73,7 +74,9 @@ export function paintCanvas(document: Document, canvas: Canvas): () => void {
// Anything a previous injection left. Cheap insurance: the cleanup should have run, and if a reload
// raced it, a second Add section button is exactly the sort of thing nobody reports as a bug.
root.querySelectorAll(`[${MARK}]`).forEach((node) => node.remove());
// Across the whole document, not just `main`: the measure guides are appended to body so they can be
// `fixed` without a positioned ancestor interfering.
document.querySelectorAll(`[${MARK}]`).forEach((node) => node.remove());
/** Everything to undo, in the order it was done. */
const undo: (() => void)[] = [];
@@ -187,6 +190,79 @@ 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<HTMLElement>('[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();
});
}
// ── Add section, always last ─────────────────────────────────────────────────────────────────
const zone = document.createElement('div');
zone.setAttribute(MARK, 'add-section');