From 458aa9abed1e4a4323ea89c1d7b06a85d1e0a0a1 Mon Sep 17 00:00:00 2001 From: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Date: Tue, 27 Oct 2026 06:39:38 +0000 Subject: [PATCH] fix: expanded diagrams stay readable on narrow screens Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> --- .../docs-site/diagram-interactions.test.mjs | 52 +++++++++++++++++++ scripts/docs-site/docs-layout.css | 3 ++ scripts/docs-site/site-js.mjs | 2 +- 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 scripts/docs-site/diagram-interactions.test.mjs diff --git a/scripts/docs-site/diagram-interactions.test.mjs b/scripts/docs-site/diagram-interactions.test.mjs new file mode 100644 index 0000000000..6ebdb64fbc --- /dev/null +++ b/scripts/docs-site/diagram-interactions.test.mjs @@ -0,0 +1,52 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { test } from "node:test"; +import { chromium, webkit } from "playwright"; +import { fixture } from "./test-helpers/redirect-fixture.mjs"; + +const source = "flowchart LR\n A[\"Chat apps and plugins\"] --> B[Gateway]\n B --> C[\"OpenClaw agent\"]\n B --> D[\"Web Control UI\"]"; +const markdown = "# Diagram interactions\n\n```mermaid\n" + source + "\n```\n\n```mermaid\nflowchart LR\n Broken -->\n```"; + +for (const [name, engine] of [["Chromium", chromium], ["WebKit", webkit]]) { + test(name + " preserves diagram source, error fallback and expansion controls", async t => { + const f = fixture(t, [], { "guide.md": markdown }); + fs.symlinkSync(path.resolve("node_modules"), path.join(f.root, "node_modules"), process.platform === "win32" ? "junction" : "dir"); + const built = f.build(); + assert.equal(built.status, 0, built.stderr); + const site = path.join(f.root, "dist/docs-site"); + const browser = await engine.launch({ headless: true }); + t.after(() => browser.close()); + const page = await browser.newPage({ viewport: { width: 390, height: 844 } }); + page.setDefaultTimeout(10000); + await page.route("**/*", route => { + const url = new URL(route.request().url()); + if (url.origin !== "https://docs.example") return route.abort(); + let file = path.join(site, url.pathname); + if (fs.existsSync(file) && fs.statSync(file).isDirectory()) file = path.join(file, "index.html"); + if (!fs.existsSync(file)) return route.fulfill({ status: 404 }); + const contentType = ({ ".html": "text/html", ".js": "text/javascript", ".mjs": "text/javascript", ".css": "text/css", ".woff2": "font/woff2", ".svg": "image/svg+xml" })[path.extname(file)] || "application/octet-stream"; + return route.fulfill({ contentType, body: fs.readFileSync(file) }); + }); + await page.goto("https://docs.example/guide"); + await page.locator(".oc-mermaid.is-rendered > svg").waitFor(); + assert.equal(await page.locator(".oc-mermaid").first().getAttribute("data-mermaid"), source); + assert.ok(fs.readFileSync(path.join(site, "guide.md"), "utf8").includes(source)); + assert.ok(JSON.parse(await page.locator("[data-page-markdown]").textContent()).includes(source)); + assert.match(await page.locator(".oc-mermaid.is-error code").textContent(), /Broken -->/); + await page.locator("[data-mermaid-expand]").click(); + const expanded = page.locator("[data-mermaid-overlay-canvas] > svg"); + await expanded.waitFor({ state: "visible" }); + const geometry = await expanded.evaluate(svg => ({ width: svg.getBoundingClientRect().width, naturalWidth: svg.viewBox.baseVal.width })); + assert.ok(geometry.width >= geometry.naturalWidth - 1, "expansion must not shrink labels to fit a narrow screen"); + const canvas = page.locator("[data-mermaid-overlay-canvas]"); + await canvas.evaluate(element => { element.scrollLeft = element.scrollWidth; }); + assert.ok(await expanded.evaluate(svg => svg.getBoundingClientRect().right <= svg.parentElement.getBoundingClientRect().right + 1), "the expanded diagram end must remain reachable by scrolling"); + await page.keyboard.press("Tab"); + assert.ok(await page.locator("[data-mermaid-overlay]").evaluate(overlay => overlay.contains(document.activeElement))); + await page.keyboard.press("Escape"); + assert.equal(await page.locator("[data-mermaid-overlay].open").count(), 0); + assert.ok(await page.locator("[data-mermaid-expand]").evaluate(button => button === document.activeElement)); + assert.ok(await page.evaluate(() => !document.body.classList.contains("has-mermaid-overlay"))); + }); +} diff --git a/scripts/docs-site/docs-layout.css b/scripts/docs-site/docs-layout.css index b68125775e..ef8c64f0ff 100644 --- a/scripts/docs-site/docs-layout.css +++ b/scripts/docs-site/docs-layout.css @@ -768,3 +768,6 @@ @media (min-width:821px) { .docs-layout:dir(rtl) :is(.docs-sidebar-directory, .docs-sidebar-context > .docs-sections) { padding-block-end: 88px; } } + +/* Renderers put responsive max-width inline; expansion must stay readable and scroll. */ +.docs-layout .oc-mermaid-overlay-canvas > svg { max-width: none !important; } diff --git a/scripts/docs-site/site-js.mjs b/scripts/docs-site/site-js.mjs index 2947abf692..078c4132b4 100644 --- a/scripts/docs-site/site-js.mjs +++ b/scripts/docs-site/site-js.mjs @@ -229,7 +229,7 @@ function mermaidExpandButton(){return '
';document.body.appendChild(overlay);return overlay} function closeMermaidOverlay(){const overlay=document.querySelector("[data-mermaid-overlay]");if(!overlay?.classList.contains("open"))return;mermaidOverlayRender+=1;overlay.classList.remove("open");document.body.classList.remove("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-canvas]").innerHTML="";lastMermaidTrigger?.focus?.();lastMermaidTrigger=null} function handleMermaidOverlayKeydown(e){const overlay=document.querySelector("[data-mermaid-overlay].open");if(!overlay)return false;if(e.key==="Escape"){e.preventDefault();e.stopImmediatePropagation();closeMermaidOverlay();return true}if(isSearchShortcutEvent(e)){e.preventDefault();e.stopImmediatePropagation();return true}if(e.key!=="Tab")return false;const controls=[...overlay.querySelectorAll("button:not([disabled]),[href],[tabindex]:not([tabindex='-1'])")].filter(control=>!control.hidden&&control.getAttribute("aria-hidden")!=="true"&&control.offsetParent!==null);const first=controls[0];const last=controls.at(-1);if(!first){e.preventDefault();return true}if(!overlay.contains(document.activeElement)||e.shiftKey&&document.activeElement===first||!e.shiftKey&&document.activeElement===last){e.preventDefault();(e.shiftKey?last:first).focus();return true}return false} -async function openMermaidOverlay(control){const block=control.closest("[data-mermaid]");const source=block?.dataset.mermaid||"";if(!source)return;const renderToken=++mermaidOverlayRender;lastMermaidTrigger=control;const overlay=ensureMermaidOverlay();const canvas=overlay.querySelector("[data-mermaid-overlay-canvas]");canvas.innerHTML=""+escapeHtml(source)+"";overlay.classList.add("open");document.body.classList.add("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-close]")?.focus();const renderId="oc_mermaid_overlay_"+Date.now();try{const {svg}=await renderMermaidSvg(source,renderId);if(renderToken!==mermaidOverlayRender||!overlay.classList.contains("open")){removeMermaidRenderArtifact(renderId);return}canvas.innerHTML=svg}catch{removeMermaidRenderArtifact(renderId)}}
+async function openMermaidOverlay(control){const block=control.closest("[data-mermaid]");const source=block?.dataset.mermaid||"";if(!source)return;const renderToken=++mermaidOverlayRender;lastMermaidTrigger=control;const overlay=ensureMermaidOverlay();const canvas=overlay.querySelector("[data-mermaid-overlay-canvas]");canvas.innerHTML=""+escapeHtml(source)+"";overlay.classList.add("open");document.body.classList.add("has-mermaid-overlay");overlay.querySelector("[data-mermaid-overlay-close]")?.focus();const renderId="oc_mermaid_overlay_"+Date.now();try{const {svg}=await renderMermaidSvg(source,renderId);if(renderToken!==mermaidOverlayRender||!overlay.classList.contains("open")){removeMermaidRenderArtifact(renderId);return}canvas.innerHTML=svg;const diagram=canvas.firstElementChild;diagram.style.width=diagram.viewBox.baseVal.width+"px"}catch{removeMermaidRenderArtifact(renderId)}}
async function initMermaid(force=false){const blocks=[...document.querySelectorAll("[data-mermaid]")].filter(block=>force||!block.dataset.mermaidReady);if(!blocks.length)return;let i=0;for(const block of blocks){const source=block.dataset.mermaid||"";if(force)resetMermaidBlock(block,source);const renderId="oc_mermaid_"+Date.now()+"_"+i++;try{const {svg}=await renderMermaidSvg(source,renderId);block.innerHTML=mermaidExpandButton()+svg;block.dataset.mermaidReady="true";block.classList.remove("is-error");block.classList.add("is-rendered")}catch(err){removeMermaidRenderArtifact(renderId);resetMermaidBlock(block,source);block.dataset.mermaidReady="true";block.classList.add("is-error");block.insertAdjacentHTML("afterbegin",'