diff --git a/assets/reader.css b/assets/reader.css index 36da0c0..d63751f 100644 --- a/assets/reader.css +++ b/assets/reader.css @@ -1248,13 +1248,15 @@ button { } /* The layout follows the width left for the zoomed document (set by - reader.js), not the window, so zooming in still gives the text room. */ -:root[data-layout="wide"] .content-grid { + reader.js), not the window, so zooming in still gives the text room. The + side columns are only for a document with an outline (data-outline, set by + reader.js): without one the text would go into the 220 px outline column. */ +:root[data-layout="wide"] .content-grid[data-outline] { grid-template-columns: 220px var(--column) 220px; } :root[data-layout="wide"] .outline:not([hidden]), -:root[data-layout="wide"] .layout-balance { +:root[data-layout="wide"] .content-grid[data-outline] .layout-balance { display: block; } diff --git a/assets/reader.js b/assets/reader.js index 2f61fd0..9b13c5f 100644 --- a/assets/reader.js +++ b/assets/reader.js @@ -612,14 +612,18 @@ } }); + // The wide layout has side columns only for a document with an outline; + // without one the text keeps the single column (reader.css). if (outlineEntries.length < 2) { outlineEntries = []; elements.outlineList.replaceChildren(); elements.outline.hidden = true; + elements.readerPane.removeAttribute("data-outline"); return; } elements.outlineList.replaceChildren(outline); elements.outline.hidden = false; + elements.readerPane.setAttribute("data-outline", ""); } function prepareDiagrams() { diff --git a/tests/Invoke-ReaderBrowserSmoke.mjs b/tests/Invoke-ReaderBrowserSmoke.mjs index 7193f40..ae741da 100644 --- a/tests/Invoke-ReaderBrowserSmoke.mjs +++ b/tests/Invoke-ReaderBrowserSmoke.mjs @@ -289,6 +289,33 @@ const THEME_DOCUMENT_HTML = [ " C -->|yes| D[Ship]\n", ].join("\n"); +// A short status note with one H1 and no H2 or H3, so it has no outline: +// four short paragraphs, three opening in bold, and one diagram. It has the +// shape of the 2.1 KB file that 0.7.0 showed in a 220 px column in a wide +// window; the text is made up. +const SHORT_STATUS_HTML = [ + "

Harbor relay 10.30.100.40 status

", + "

Checked Nov 2, 2026, at 09:15 UTC. This is the logical view: relay, " + + "then two workers, then two sensors each. Physically, the sensors all " + + "plug into the site switch, and each worker's software sets which " + + "sensors it reads.

", + "

Result: all hardware is connected and on its listed address, " + + "and all four sensors report directly to their worker.

", + "

Changed since the 08:50 UTC check: sensor_4 is online " + + "and sensor_3's main feed works on worker 2.

", + "

Note on sensor_3: its second feed is out of scope for " + + "this test, so the sensor is marked correct on its main feed. The monitor " + + "still reports it as offline overall because the second feed returns 404.

", + '
flowchart TD\n' +
+    '    hub["Hub"] -->|"VPN"| relay["Relay"]\n' +
+    '    relay --> w1["Worker 1"]\n' +
+    '    relay --> w2["Worker 2"]\n' +
+    '    w1 --> s1["Sensor 1"]\n' +
+    '    w1 --> s2["Sensor 2"]\n' +
+    '    w2 --> s3["Sensor 3"]\n' +
+    '    w2 --> s4["Sensor 4"]\n
', +].join("\n"); + // The softer dark themes from issue #6. The reader must apply exactly these // token values; they are the contract a later shared theme system reuses. const SOFT_THEME_TOKENS = { @@ -1083,6 +1110,71 @@ async function run() { check(Math.abs(column.width - column.full) < 1 && column.scroll <= 0, "Full fills a narrower window too, with no sideways scroll"); + // A document with no outline gets the same column as any other, in every + // layout. In 0.7.0 the wide layout kept its outline columns when there was + // no outline, so the text went into the 220 px outline column. + const SHORT_COLUMN = `(function () { + var pane = document.getElementById("readerPane"); + var style = getComputedStyle(pane); + var main = document.getElementById("mainContent").getBoundingClientRect(); + var box = pane.getBoundingClientRect(); + var left = box.left + pane.clientLeft + parseFloat(style.paddingLeft); + var space = pane.clientWidth - parseFloat(style.paddingLeft) - + parseFloat(style.paddingRight); + return { + width: main.width, + space: space, + leftGap: main.left - left, + rightGap: left + space - main.right, + rows: style.gridTemplateRows.split(" ").length, + outline: getComputedStyle(document.getElementById("outline")).display, + layout: document.documentElement.dataset.layout || "", + scroll: document.documentElement.scrollWidth - window.innerWidth + }; + }())`; + const COLUMN_FOR = { narrow: NARROW_PX, wide: WIDE_PX, full: Infinity }; + const showShortStatus = async (width) => { + await deliver({ type: "host", copySource: true, tabs: true, zoom: 100, width }); + await deliver({ + type: "document", tabId: 1, theme: "dusk", fileName: "status.md", + path: "/fixtures/status.md", documentBaseUrl: "", sourceBytes: 2144, + hasMermaid: true, html: SHORT_STATUS_HTML, + }); + await waitFor(client, 'document.documentElement.dataset.renderState === "ready" && ' + + '!!document.querySelector("#article figure.diagram svg")', "the short status note"); + }; + // 966 px is the window of the report; 760 and 761, 1179 and 1180 are the + // two sides of the compact and wide breakpoints; 1880 is a maximised window. + for (const width of ["narrow", "wide", "full"]) { + for (const [viewport, layout] of [[966, ""], [760, ""], [761, ""], + [1179, ""], [1180, "wide"], [1880, "wide"]]) { + await setViewport(viewport); + await showShortStatus(width); + const short = await client.evaluate(SHORT_COLUMN); + const expected = Math.min(COLUMN_FOR[width], short.space); + check(Math.abs(short.width - expected) < 1 && Math.abs(short.leftGap - short.rightGap) < 1 && + short.rows === 1 && short.outline === "none" && short.layout === layout && + short.scroll <= 0, + "with no outline, " + width + " at " + viewport + " px is a centred " + + Math.round(expected) + " px column in one row " + JSON.stringify(short)); + } + } + // Going from a document with an outline to one without, and back. + await showWidthDocument({ zoom: 100, width: "narrow" }); + check(await client.evaluate( + 'getComputedStyle(document.getElementById("outline")).display === "block" && ' + + 'getComputedStyle(document.getElementById("readerPane")).gridTemplateColumns === "220px 720px 220px"'), + "a document with an outline still gets the outline column at 1880 px"); + await showShortStatus("narrow"); + const switched = await client.evaluate(SHORT_COLUMN); + check(Math.round(switched.width) === NARROW_PX && switched.rows === 1, + "the next document without an outline gets the " + NARROW_PX + " px column " + + JSON.stringify(switched)); + await showWidthDocument({ zoom: 100, width: "narrow" }); + check(Math.round((await client.evaluate(COLUMN)).width) === NARROW_PX && await client.evaluate( + 'getComputedStyle(document.getElementById("outline")).display === "block"'), + "and the outline comes back with the next document that has one"); + // Zoom. The host keeps its web view at 100% and the page zooms only the // document, so the toolbar and tabs keep their size. await setViewport(1200);