Render the column layout structure on the backend, cap at two columns, add a left-margin breakout.
render() now segments the body into .colseg wrappers and flags .multicol
itself, replacing the fragile colseg injection in pagerite.js. Columns
are capped at two; shrink-wrapped figures left-align inside columns.
New {.margin} breakout (and ::: aside) drops blocks into the left gutter
on wide viewports, falling back to in-column floats.
This commit is contained in:
@@ -240,9 +240,13 @@ function runScripts(root) {
|
||||
}
|
||||
}
|
||||
|
||||
function previewIntoArticle(html, hasH1) {
|
||||
function previewIntoArticle(html, hasH1, multicol) {
|
||||
const article = document.querySelector('#main article')
|
||||
if (!article) return
|
||||
// The server render owns the column layout: .multicol on the article,
|
||||
// the segmented .colseg/.cols structure inside .body. Both arrive with
|
||||
// the preview and must stay in sync as edits cross the thresholds.
|
||||
article.classList.toggle('multicol', multicol)
|
||||
const h1 = article.querySelector('h1')
|
||||
const body = article.querySelector('.body')
|
||||
// The edit pen may be tucked inside an h1 (title or markdown-owned);
|
||||
@@ -270,7 +274,7 @@ function onMessage(ev) {
|
||||
requestRender()
|
||||
dirty.value = false // just loaded from the server, nothing unsaved
|
||||
} else if (msg.type === 'html' && msg.path === path.value) {
|
||||
previewIntoArticle(msg.html, msg.has_h1)
|
||||
previewIntoArticle(msg.html, msg.has_h1, msg.multicol)
|
||||
} else if (msg.type === 'saved') {
|
||||
saveError.value = ''
|
||||
pendingSave = null
|
||||
|
||||
@@ -275,12 +275,13 @@ body {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 78rem) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
/* Long articles (.multicol is added by pagerite.js based on content length)
|
||||
lift the 78rem cap and scrap the right gutter: a 1fr left gutter (which
|
||||
holds the overlaying sidebar) and the article taking all the rest, out
|
||||
to the right viewport edge. The column count follows the width (see the
|
||||
`columns: 30rem` rule below). The .wide breakout is re-anchored to the
|
||||
left gutter below (the article is no longer viewport-centered). */
|
||||
/* Long articles (.multicol comes from the backend render, based on
|
||||
content length — code excluded) lift the 78rem cap and scrap the right
|
||||
gutter: a 1fr left gutter (which holds the overlaying sidebar and any
|
||||
margin-breakout boxes) and the article taking all the rest, out to the
|
||||
right viewport edge. Columns are capped at two (see the
|
||||
`columns: 30rem 2` rule below). The .wide breakout is re-anchored to
|
||||
the left gutter below (the article is no longer viewport-centered). */
|
||||
body:has(.multicol) #content {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 4fr);
|
||||
}
|
||||
@@ -561,20 +562,28 @@ article dd {
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
/* Multi-column reading, but only for long articles (pagerite.js adds
|
||||
.multicol based on content length — code blocks excluded — and splits
|
||||
the body into .colseg segments separated by full-width h2s and .wide
|
||||
elements; only segments with enough text get .cols, and a ::: nocols
|
||||
container opts its section out). No fixed breakpoint: `columns: 30rem` lets CSS fit as
|
||||
many columns of at least 30rem as the article's current width allows —
|
||||
since .multicol also uncaps the article width (see #content above), a
|
||||
wider window simply yields more columns. */
|
||||
/* Multi-column reading, but only for long articles: the backend render
|
||||
splits the body into .colseg segments (separated by full-width h2s and
|
||||
.wide elements), tags segments with enough text as .cols (a ::: nocols
|
||||
container opts its section out) and flags the article .multicol based
|
||||
on content length — code blocks excluded. Never more than two columns:
|
||||
`columns: 30rem 2` fits one or two columns of at least 30rem into the
|
||||
article's current width — since .multicol also uncaps the article
|
||||
width (see #content above), a wider window widens the two columns
|
||||
instead of adding more. */
|
||||
.multicol .colseg.cols {
|
||||
columns: 30rem;
|
||||
columns: 30rem 2;
|
||||
column-gap: 3.5rem;
|
||||
column-rule: 1px solid var(--line);
|
||||
}
|
||||
|
||||
/* A shrink-wrapped figure (explicit image width) centers in the plain
|
||||
layout; inside a column the centering looks adrift — left-align.
|
||||
Floated figures keep their own margins (the text gap). */
|
||||
.multicol .colseg.cols figure:has(img[width]):not(:has(.left), :has(.right), :has(.margin)) {
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.multicol .colseg {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@@ -725,13 +734,13 @@ blockquote p + p {
|
||||
--admonition-color: var(--accent3);
|
||||
}
|
||||
|
||||
/* Asides (::: aside): a floated side box in the floated-figure idiom;
|
||||
consecutive asides stack (clear: right). On wide single-column pages it
|
||||
leans into the empty right gutter (below 104rem the gutter cannot hold
|
||||
the box; multicol pages have no right gutter at all, and while editing
|
||||
the docked panel reshapes the gutters — in all these it stays a plain
|
||||
float). Headings already clear floats, so asides never bleed into the
|
||||
next section. */
|
||||
/* Side boxes: ::: aside is a muted floated box (consecutive asides stack
|
||||
via clear: right); {.margin} / ::: margin is a plainer margin note, and
|
||||
figures take {.margin} like {.left}. All of them drop into the left
|
||||
margin when the layout has room for it (the breakout rules live with
|
||||
the figure rules below); without room they stay in-column floats —
|
||||
asides on the right, margin boxes on the left. Headings already clear
|
||||
floats, so boxes never bleed into the next section. */
|
||||
.aside {
|
||||
float: right;
|
||||
clear: right;
|
||||
@@ -745,15 +754,19 @@ blockquote p + p {
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
|
||||
.aside> :last-child {
|
||||
.aside> :last-child,
|
||||
.margin> :last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 104rem) {
|
||||
body:not(.editing):not(:has(.multicol)) .aside {
|
||||
width: 12rem;
|
||||
margin-right: -13rem;
|
||||
}
|
||||
.margin {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 30%;
|
||||
max-width: 20rem;
|
||||
margin: 0.3rem 1.2rem 1rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -917,6 +930,52 @@ figure:has(img[width]) {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* {.margin} figures float left like {.left} ones — until the margin
|
||||
breakout below pulls them into the left gutter. */
|
||||
figure:has(.margin) {
|
||||
float: left;
|
||||
width: 30%;
|
||||
max-width: 50%;
|
||||
margin: 0.3rem 1em 1rem 0;
|
||||
}
|
||||
|
||||
/* The left-margin breakout: margin boxes ({.margin} / ::: margin blocks,
|
||||
::: aside, {.margin} figures) leave the text column for the left
|
||||
gutter, hugging the article's left edge (a 12rem box on a 13.25rem
|
||||
pull: the gutter plus main's padding). The sticky sidebar shares the
|
||||
gutter and slides over them translucently — the same overlap full-bleed
|
||||
.wide images get. Scoped to direct .body children: the backend render
|
||||
keeps breakout blocks out of the column segments, and nested ones keep
|
||||
the in-column float fallback. Room exists once the single-column
|
||||
layout's symmetric gutters fit the box (≥104rem), and on multicol
|
||||
pages whose 1fr left gutter fits it (≥65rem) — but never while editing
|
||||
(the docked panel owns the left edge), and not below 102rem with a
|
||||
sidebar (the fixed 12rem sidebar track, see #content, leaves no free
|
||||
gutter there). */
|
||||
@media (min-width: 104rem) {
|
||||
body:not(.editing) .body>.margin,
|
||||
body:not(.editing) .body>.aside,
|
||||
body:not(.editing) .body>figure:has(.margin) {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 12rem;
|
||||
max-width: none;
|
||||
margin: 0.3rem 0 1rem -13.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 65rem) {
|
||||
body:not(.editing):has(.multicol):not(:has(#sidebar)) .body>.margin,
|
||||
body:not(.editing):has(.multicol):not(:has(#sidebar)) .body>.aside,
|
||||
body:not(.editing):has(.multicol):not(:has(#sidebar)) .body>figure:has(.margin) {
|
||||
float: left;
|
||||
clear: left;
|
||||
width: 12rem;
|
||||
max-width: none;
|
||||
margin: 0.3rem 0 1rem -13.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* .wide is full bleed: edge to edge of the viewport (or of the space right
|
||||
of the docked editor), while staying in flow so it keeps its vertical
|
||||
space. The article column is centered in the available space, so negative
|
||||
@@ -1038,9 +1097,9 @@ article h2 {
|
||||
|
||||
/* Phones and other narrow viewports: single-column layout with the
|
||||
sidebar lifted above the article as a wrapping link strip, and no
|
||||
floated figures — .left/.right fall back to plain centered figures
|
||||
(explicit img widths still shrink-wrap), while .wide keeps its full
|
||||
viewport bleed. */
|
||||
floated figures — .left/.right/.margin fall back to plain centered
|
||||
figures (explicit img widths still shrink-wrap), while .wide keeps its
|
||||
full viewport bleed. */
|
||||
@media (max-width: 48rem) {
|
||||
|
||||
/* Nav type shrinks fluidly as space runs out. The nav font-size is
|
||||
@@ -1105,7 +1164,8 @@ article h2 {
|
||||
}
|
||||
|
||||
figure:has(.right),
|
||||
figure:has(.left) {
|
||||
figure:has(.left),
|
||||
figure:has(.margin) {
|
||||
float: none;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
|
||||
@@ -286,66 +286,8 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
// created page has no pen for commitPending's handover click.
|
||||
renderAuthUi();
|
||||
placeEditPen();
|
||||
// Preview swaps also wipe the .colseg wrappers (the server render has
|
||||
// none), which would drop the multi-column layout until a full reload;
|
||||
// re-split so columns survive both live editing and closing the editor.
|
||||
const main = document.getElementById("main");
|
||||
if (main) applyMulticol(main);
|
||||
});
|
||||
|
||||
// Multi-column layout only when there is enough text to justify it.
|
||||
// Split the body into columned segments: h1s, h2s and wide elements are
|
||||
// full-width separators and never go inside columns.
|
||||
function applyMulticol(main) {
|
||||
const article = main.querySelector("article");
|
||||
if (!article) return;
|
||||
const body = article.querySelector(".body");
|
||||
// Code blocks don't read as flowing text and are often generated
|
||||
// filler; exclude them when measuring whether the text justifies
|
||||
// columns.
|
||||
const textLen = (el) => {
|
||||
let n = el.textContent.trim().length;
|
||||
for (const pre of el.querySelectorAll("pre")) n -= pre.textContent.length;
|
||||
return n;
|
||||
};
|
||||
article.classList.toggle(
|
||||
"multicol",
|
||||
!!body && textLen(body) > 1800,
|
||||
);
|
||||
if (body && article.classList.contains("multicol")
|
||||
&& !body.querySelector(".colseg")) {
|
||||
// h1s, h2s and wide elements (a {.wide} block or anything holding
|
||||
// one, e.g. a figure with a wide image) are full-width separators
|
||||
const isSeparator = (el) =>
|
||||
el.tagName === "H1" || el.tagName === "H2"
|
||||
|| el.classList.contains("wide")
|
||||
|| el.querySelector(".wide") !== null;
|
||||
let seg = null;
|
||||
for (const el of [...body.children]) {
|
||||
if (isSeparator(el)) {
|
||||
seg = null;
|
||||
body.append(el);
|
||||
} else {
|
||||
if (!seg) {
|
||||
seg = document.createElement("div");
|
||||
seg.className = "colseg";
|
||||
body.append(seg);
|
||||
}
|
||||
seg.append(el);
|
||||
}
|
||||
}
|
||||
// Columns are per section: only segments with enough text get them,
|
||||
// so a short ingress or a brief section stays single-column. A
|
||||
// .nocols container (::: nocols) opts its whole section out.
|
||||
for (const s of body.querySelectorAll(".colseg")) {
|
||||
s.classList.toggle(
|
||||
"cols",
|
||||
s.querySelector(".nocols") === null && textLen(s) > 600,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyEffects() {
|
||||
(window.requestIdleCallback || setTimeout)(preload);
|
||||
const main = document.getElementById("main");
|
||||
@@ -355,7 +297,6 @@ import "overlayscrollbars/overlayscrollbars.css";
|
||||
renderAuthUi();
|
||||
placeEditPen();
|
||||
fitNav();
|
||||
applyMulticol(main);
|
||||
if (reduceMotion.matches) return;
|
||||
for (const el of main.querySelectorAll(
|
||||
"h2, h3, figure, img, pre, blockquote, table, dl, .task-list-item",
|
||||
|
||||
Reference in New Issue
Block a user