File size: 7,123 Bytes
befb893 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 | // Debug grid overlay. Draws the live tracks of the page grid (read from the
// computed grid-template-columns of .content-grid, so it always reflects the
// tokens in src/styles/_grid.css), the named lines, the column spans, the
// sticky offset and a baseline. Loaded on demand by DebugGrid.astro.
// G toggle the overlay
// Shift+G toggle the baseline
import { onLayoutChange } from "./headings";
const CSS = `
.dg-root { position: fixed; inset: 0; pointer-events: none; z-index: 2147483000; display: none;
font: 10px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; color: #111; }
html.debug-grid .dg-root { display: block; }
.dg-track { position: absolute; top: 0; bottom: 0; border-left: 1px dashed rgba(127, 127, 127, .55); }
.dg-track > span { position: absolute; top: 45%; left: 4px; right: 4px; padding: 2px 4px; border-radius: 3px;
text-align: center; overflow: hidden; background: rgba(255, 255, 255, .85); }
.dg-track.toc { background: rgba(78, 121, 167, .10); }
.dg-track.body { background: rgba(89, 161, 79, .10); }
.dg-track.margin { background: rgba(240, 140, 60, .12); }
.dg-track.gap { background: repeating-linear-gradient(45deg, rgba(127, 127, 127, .12) 0 4px, transparent 4px 8px); }
.dg-track.outer { background: rgba(225, 87, 89, .06); }
.dg-line { position: absolute; top: 70px; color: #c00; writing-mode: vertical-rl; transform: translateX(-12px); white-space: nowrap; }
.dg-span { position: absolute; height: 14px; line-height: 12px; text-align: center; border: 1px solid rgba(0, 0, 0, .5);
border-top: 0; background: rgba(255, 255, 255, .85); }
.dg-sticky { position: absolute; left: 0; right: 0; border-top: 1px dashed rgba(200, 0, 0, .6); }
.dg-sticky > span { position: absolute; right: 8px; top: 2px; color: #c00; }
.dg-badge { position: absolute; left: 8px; bottom: 8px; padding: 4px 8px; border-radius: 4px; background: #111; color: #fff; }
.dg-baseline { position: absolute; inset: 0; display: none;
background-image: linear-gradient(rgba(0, 160, 255, .22) 1px, transparent 1px); }
html.debug-grid-baseline .dg-baseline { display: block; }
[data-theme="dark"] .dg-root { color: #eee; }
[data-theme="dark"] .dg-track > span, [data-theme="dark"] .dg-span { background: rgba(15, 17, 21, .85); border-color: rgba(255, 255, 255, .5); }
[data-theme="dark"] .dg-line, [data-theme="dark"] .dg-sticky > span { color: #ff7a7a; }
`;
const SPANS: Array<[string, string, string]> = [
["body", "body-start", "body-end"],
["outset", "outset-start", "outset-end"],
["wide", "body-start", "margin-end"],
["page", "page-start", "page-end"],
["screen", "screen-start", "screen-end"],
];
const root = document.documentElement;
let overlay: HTMLElement | null = null;
let baseline: HTMLElement | null = null;
function setUrl(on: boolean) {
try {
const u = new URL(location.href);
u.searchParams.delete("debug");
if (on) u.searchParams.set("grid", "1");
else u.searchParams.delete("grid");
history.replaceState(history.state, "", u);
} catch {}
}
function parseTracks(template: string) {
const names: string[][] = [];
const sizes: number[] = [];
let current: string[] = [];
const re = /\[([^\]]*)\]|([\d.]+)px/g;
let m: RegExpExecArray | null;
while ((m = re.exec(template))) {
if (m[1] !== undefined) current.push(...m[1].trim().split(/\s+/).filter(Boolean));
else {
names.push(current);
current = [];
sizes.push(parseFloat(m[2]));
}
}
names.push(current);
return { names, sizes };
}
function el(cls: string, style: Partial<CSSStyleDeclaration>, html = "") {
const d = document.createElement("div");
d.className = cls;
Object.assign(d.style, style);
d.innerHTML = html;
return d;
}
function draw() {
if (!overlay || !root.classList.contains("debug-grid")) return;
overlay.querySelectorAll(":scope > :not(.dg-baseline)").forEach((n) => n.remove());
const grid = document.querySelector<HTMLElement>(".content-grid");
if (!grid) return;
const cs = getComputedStyle(grid);
const rect = grid.getBoundingClientRect();
const { names, sizes } = parseTracks(cs.gridTemplateColumns);
const gap = parseFloat(cs.columnGap) || 0;
const lineX: Record<string, number> = {};
let x = rect.left + parseFloat(cs.paddingLeft);
sizes.forEach((w, i) => {
const start = names[i];
const cls = start.includes("body-start")
? "body"
: start.includes("margin-start")
? "margin"
: start.includes("toc-start")
? "toc"
: start.includes("screen-start") || start.includes("page-end") || w > 48
? "outer"
: "gap";
start.forEach((n) => (lineX[n] = x));
overlay!.appendChild(
el(`dg-track ${cls}`, { left: `${x}px`, width: `${w}px` }, `<span>${w >= 70 ? `${cls}<br>` : ""}${Math.round(w)}</span>`),
);
if (start.length) overlay!.appendChild(el("dg-line", { left: `${x}px` }, start.join(" ")));
x += w + (i < sizes.length - 1 ? gap : 0);
});
names[sizes.length].forEach((n) => (lineX[n] = x));
SPANS.forEach(([label, a, b], i) => {
if (lineX[a] === undefined || lineX[b] === undefined) return;
const w = lineX[b] - lineX[a];
overlay!.appendChild(
el("dg-span", { left: `${lineX[a]}px`, width: `${w}px`, bottom: `${40 + i * 18}px` }, `${label} ${Math.round(w)}px`),
);
});
const stickyTop = getComputedStyle(root).getPropertyValue("--layout-sticky-top").trim();
overlay.appendChild(el("dg-sticky", { top: stickyTop }, `<span>sticky ${stickyTop}</span>`));
const single = sizes.length <= 3;
const tier = single ? "single column" : window.matchMedia("(min-width: 1280px)").matches ? "rail + margin" : "compact rail";
overlay.appendChild(el("dg-badge", {}, `${root.clientWidth}px | ${tier} | G grid, Shift+G baseline`));
syncBaseline();
}
function syncBaseline() {
if (!baseline) return;
const step = parseFloat(getComputedStyle(root).getPropertyValue("--layout-baseline")) || 8;
baseline.style.backgroundSize = `100% ${step}px`;
baseline.style.backgroundPosition = `0 ${-(window.scrollY % step)}px`;
}
function mount() {
if (overlay) return;
const style = document.createElement("style");
style.dataset.debugGrid = "";
style.textContent = CSS;
document.head.appendChild(style);
overlay = el("dg-root", {});
overlay.setAttribute("aria-hidden", "true");
baseline = el("dg-baseline", {});
overlay.appendChild(baseline);
root.classList.add("debug-grid-baseline");
document.body.appendChild(overlay);
onLayoutChange(draw);
window.addEventListener("scroll", syncBaseline, { passive: true });
document.addEventListener("keydown", (e) => {
if (e.key.toLowerCase() !== "g" || e.metaKey || e.ctrlKey || e.altKey) return;
if ((e.target as Element | null)?.closest?.("input, textarea, select, [contenteditable]")) return;
if (e.shiftKey) root.classList.toggle("debug-grid-baseline");
else toggleDebugGrid();
});
}
export function toggleDebugGrid(force?: boolean) {
mount();
const on = force ?? !root.classList.contains("debug-grid");
root.classList.toggle("debug-grid", on);
setUrl(on);
draw();
}
|