From cdfb40ce195e637012495e1d8db17e327516faa5 Mon Sep 17 00:00:00 2001 From: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Date: Tue, 27 Oct 2026 12:54:46 +0000 Subject: [PATCH] fix: fit expanded docs diagrams to mobile screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Start with the complete diagram visible, using Pele’s existing zoom and pan owner for readable detail. Preserve focus, dismissal, navigation, source fallback and touch targets without changing the diagram engine or font settings. Co-authored-by: vincentkoc <25068+vincentkoc@users.noreply.github.com> --- .../docs-site/diagram-interactions.test.mjs | 50 ++++++++++++++++--- scripts/docs-site/docs-layout.css | 10 ++-- scripts/docs-site/site-css.mjs | 8 +-- scripts/docs-site/site-js.mjs | 7 +-- scripts/docs-site/smoke.mjs | 4 ++ scripts/docs-site/visual-smoke.mjs | 10 ++-- 6 files changed, 67 insertions(+), 22 deletions(-) diff --git a/scripts/docs-site/diagram-interactions.test.mjs b/scripts/docs-site/diagram-interactions.test.mjs index 6ebdb64fbc..d05409bbd3 100644 --- a/scripts/docs-site/diagram-interactions.test.mjs +++ b/scripts/docs-site/diagram-interactions.test.mjs @@ -5,7 +5,7 @@ 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 source = "flowchart LR\n A[\"Chat apps + plugins\"] --> B[Gateway]\n B --> C[\"OpenClaw agent\"]\n B --> D[CLI]\n B --> E[\"Web Control UI\"]\n B --> F[\"macOS app\"]\n B --> G[\"iOS and Android nodes\"]"; 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]]) { @@ -17,7 +17,7 @@ for (const [name, engine] of [["Chromium", chromium], ["WebKit", webkit]]) { 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 } }); + const page = await browser.newPage({ viewport: { width: 390, height: 844 }, hasTouch: true }); page.setDefaultTimeout(10000); await page.route("**/*", route => { const url = new URL(route.request().url()); @@ -37,16 +37,54 @@ for (const [name, engine] of [["Chromium", chromium], ["WebKit", webkit]]) { 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"); + async function assertFits() { + assert.ok(await expanded.evaluate(svg => { + const box = svg.getBoundingClientRect(); + const viewport = svg.parentElement.getBoundingClientRect(); + return box.left >= viewport.left && box.top >= viewport.top + && box.right <= viewport.right + 1 && box.bottom <= viewport.bottom + 1; + }), "the complete expanded diagram must fit the viewport before zooming"); + } + await assertFits(); + const fittedViewBox = await expanded.getAttribute("viewBox"); + await canvas.getByRole("button", { name: "Zoom in", exact: true }).tap(); + await canvas.getByRole("button", { name: "Zoom in", exact: true }).tap(); + assert.notEqual(await expanded.getAttribute("viewBox"), fittedViewBox, "zoom must reveal readable detail"); + assert.ok(await canvas.evaluate(element => element.scrollWidth <= element.clientWidth + 1 + && element.scrollHeight <= element.clientHeight + 1), "zoom must not replace pan with clipped outer scrolling"); + const beforePan = await expanded.getAttribute("viewBox"); + const viewport = await canvas.boundingBox(); + await page.mouse.move(viewport.x + viewport.width / 2, viewport.y + viewport.height / 2); + await page.mouse.down(); + await page.mouse.move(viewport.x + viewport.width / 2 - 60, viewport.y + viewport.height / 2, { steps: 5 }); + await page.mouse.up(); + assert.notEqual(await expanded.getAttribute("viewBox"), beforePan, "zoomed diagram must pan with real pointer input"); + await canvas.getByRole("button", { name: "Fit diagram", exact: true }).click(); + assert.equal(await expanded.getAttribute("viewBox"), fittedViewBox); + await canvas.getByRole("button", { name: "Zoom in", exact: true }).tap(); + await page.setViewportSize({ width: 844, height: 390 }); + await page.waitForFunction(() => { + const svg = document.querySelector("[data-mermaid-overlay-canvas] > svg"); + return svg.viewBox.baseVal.width === Number(svg.getAttribute("width")); + }); + await assertFits(); 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"))); + await page.locator("[data-mermaid-expand]").click(); + await expanded.waitFor({ state: "visible" }); + assert.equal(await canvas.locator(".pele-zoom").count(), 1, "reopening must not retain old viewer controls"); + await page.keyboard.press("Tab"); + assert.ok(await expanded.evaluate(svg => svg === document.activeElement)); + const keyboardFit = await expanded.getAttribute("viewBox"); + await page.keyboard.press("+"); + assert.notEqual(await expanded.getAttribute("viewBox"), keyboardFit); + await page.keyboard.press("0"); + assert.equal(await expanded.getAttribute("viewBox"), keyboardFit); + await page.keyboard.press("Escape"); }); } diff --git a/scripts/docs-site/docs-layout.css b/scripts/docs-site/docs-layout.css index 60edcd8889..c451ee73fe 100644 --- a/scripts/docs-site/docs-layout.css +++ b/scripts/docs-site/docs-layout.css @@ -433,13 +433,16 @@ --pele-font-mono: var(--oc-font-mono); --pele-radius: 8px; } -.docs-layout .oc-mermaid-overlay-canvas { border-color: var(--docs-border); background: var(--paper); box-shadow: none; padding: 32px; } +.docs-layout .oc-mermaid-overlay-canvas { border-color: var(--docs-border); background: var(--paper); box-shadow: none; padding: 32px 32px 72px; } .docs-layout .doc .oc-mermaid.is-error { padding: 18px 20px; border-color: var(--docs-border); } .docs-layout .oc-mermaid-error { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; color: var(--muted); font: 400 12px/1.5 var(--oc-font-body); } .docs-layout .oc-mermaid-error svg { flex: 0 0 16px; color: var(--oc-status-warning-fg); } .docs-layout .doc .oc-mermaid.is-error pre { margin: 0; padding: 0; border: 0; border-radius: 0; background: transparent; box-shadow: none; color: var(--text); font-size: 12px; line-height: 1.65; } @media (max-width: 520px) { - .docs-layout .oc-mermaid { padding: 44px 16px 20px; }} + .docs-layout .oc-mermaid { padding: 44px 16px 20px; } + .docs-layout .oc-mermaid-overlay { padding: 12px; } + .docs-layout .oc-mermaid-overlay-canvas { padding: 16px 16px 64px; } +} /* A section title belongs to its first block, regardless of that block's default margin. */ .docs-layout .article .doc > :is(h2,h3,h4,h5,h6) + :where(p,ul,ol,blockquote,figure,aside,details,div,section) { margin-block-start: 0; } .docs-layout .site-footer { margin-top: 0; border-bottom: 0; background: var(--bg); } @@ -777,6 +780,3 @@ @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-css.mjs b/scripts/docs-site/site-css.mjs index e05ae8fe1f..bdb3166081 100644 --- a/scripts/docs-site/site-css.mjs +++ b/scripts/docs-site/site-css.mjs @@ -192,9 +192,11 @@ html{scroll-padding-top:calc(var(--sticky-header-h) + 54px)} .oc-mermaid-overlay-close{display:grid;place-items:center;width:38px;height:38px;border:1px solid var(--line-strong);border-radius:var(--oc-radius-control);background:color-mix(in srgb,var(--paper) 90%,transparent);color:var(--muted);cursor:pointer} .oc-mermaid-overlay-close:hover,.oc-mermaid-overlay-close:focus-visible{outline:0;border-color:color-mix(in srgb,var(--brand) 46%,var(--line-strong));background:color-mix(in srgb,var(--soft) 64%,var(--paper) 36%);color:var(--ink)} .oc-mermaid-overlay-close .icon{width:18px;height:18px;min-width:18px;max-width:18px} -.oc-mermaid-overlay-canvas{min-height:0;overflow:auto;overscroll-behavior:contain;border:1px solid var(--line-strong);border-radius:var(--oc-radius-surface);background:color-mix(in srgb,var(--paper) 94%,transparent);padding:var(--oc-space-5);box-shadow:var(--shadow)} -.oc-mermaid-overlay-canvas svg{display:block;width:max-content;min-width:min(1040px,100%);max-width:none;height:auto;margin:auto} -.oc-mermaid-overlay-canvas pre{margin:0;white-space:pre;min-width:max-content} +.oc-mermaid-overlay-canvas{position:relative;container-type:size;display:grid;place-items:center;min-height:0;overflow:auto;overscroll-behavior:contain;border:1px solid var(--line-strong);border-radius:var(--oc-radius-surface);background:color-mix(in srgb,var(--paper) 94%,transparent);padding:var(--oc-space-5);box-shadow:var(--shadow)} +.oc-mermaid-overlay-canvas>svg{display:block;min-width:0;max-width:none;height:auto;margin:auto} +.oc-mermaid-overlay-canvas .pele-zoom{flex-direction:row!important;--pele-zoom-top:auto;--pele-zoom-bottom:10px;--pele-zoom-right:10px} +.oc-mermaid-overlay-canvas .pele-zoom button{min-width:44px;min-height:44px} +.oc-mermaid-overlay-canvas pre{align-self:start;justify-self:start;margin:0;white-space:pre;min-width:max-content} .has-mermaid-overlay{overflow:hidden} .language-menu{top:calc(100% + 8px);width:min(270px,calc(100vw - 32px));max-height:min(62vh,430px);padding:6px;border-radius:var(--oc-radius-surface);scrollbar-width:thin;scrollbar-color:color-mix(in srgb,var(--brand) 58%,transparent) transparent;box-shadow:var(--oc-shadow-lg)} .language-menu::-webkit-scrollbar{width:8px} diff --git a/scripts/docs-site/site-js.mjs b/scripts/docs-site/site-js.mjs index 501cc7f62d..fc63db5a7d 100644 --- a/scripts/docs-site/site-js.mjs +++ b/scripts/docs-site/site-js.mjs @@ -207,7 +207,7 @@ function initAccordionMotion(){ function initCodeGroups(){document.querySelectorAll(".oc-code-group").forEach(group=>{if(group.dataset.codeGroupReady)return;const blocks=[...group.querySelectorAll(":scope > .oc-code")];if(!blocks.length)return;group.dataset.codeGroupReady="true";const preferred=localStorage.getItem("preferredCodeTab")||"";const preferredIndex=Math.max(0,blocks.findIndex(block=>(block.dataset.codeLabel||"Code")===preferred));const activeIndex=preferredIndex||0;const tabs=document.createElement("div");tabs.className="oc-code-tabs";tabs.setAttribute("role","tablist");blocks.forEach((block,i)=>{const label=block.dataset.codeLabel||"Code";const button=document.createElement("button");button.type="button";button.className="oc-code-tab"+(i===activeIndex?" active":"");button.textContent=label;button.setAttribute("role","tab");button.setAttribute("aria-selected",String(i===activeIndex));button.addEventListener("click",()=>{tabs.querySelectorAll(".oc-code-tab").forEach(tab=>{tab.classList.remove("active");tab.setAttribute("aria-selected","false")});blocks.forEach(item=>item.classList.remove("active"));button.classList.add("active");button.setAttribute("aria-selected","true");block.classList.add("active");localStorage.setItem("preferredCodeTab",label)});tabs.appendChild(button);block.classList.toggle("active",i===activeIndex)});group.prepend(tabs)})} const mermaidExpandIcon=''; const mermaidCloseIcon=''; -let mermaidReady;let lastMermaidTrigger=null;let mermaidOverlayRender=0; +let mermaidReady;let lastMermaidTrigger=null;let mermaidOverlayRender=0;let mermaidZoom; function resetMermaidBlock(block,source){block.dataset.mermaidReady="";block.classList.remove("is-rendered","is-error");block.innerHTML="
"+escapeHtml(source)+""}
async function renderMermaidSvg(source,renderId){
const pele=await (mermaidReady??=import(withBase("/assets/pele-1.6.0.js")));
@@ -220,9 +220,10 @@ async function renderMermaidSvg(source,renderId){
}
function mermaidExpandButton(){return ''}
function ensureMermaidOverlay(){let overlay=document.querySelector("[data-mermaid-overlay]");if(overlay)return overlay;overlay=document.createElement("div");overlay.className="oc-mermaid-overlay";overlay.dataset.mermaidOverlay="";overlay.setAttribute("role","dialog");overlay.setAttribute("aria-modal","true");overlay.setAttribute("aria-label","Expanded diagram");overlay.innerHTML='';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 closeMermaidOverlay(){const overlay=document.querySelector("[data-mermaid-overlay]");if(!overlay?.classList.contains("open"))return;mermaidOverlayRender+=1;mermaidZoom?.destroy();mermaidZoom=undefined;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")){return}canvas.innerHTML=svg;const diagram=canvas.firstElementChild;diagram.style.width=diagram.viewBox.baseVal.width+"px"}catch{}}
+// Fit both viewport axes; Pele owns all zoom, pan and resize behavior.
+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]");mermaidZoom?.destroy();mermaidZoom=undefined;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);const pele=await mermaidReady;if(renderToken!==mermaidOverlayRender||!overlay.classList.contains("open")){return}canvas.innerHTML=svg;const diagram=canvas.firstElementChild;const bounds=diagram.viewBox.baseVal;diagram.style.width="min(max("+bounds.width+"px,1040px),100cqw,calc(100cqh * "+bounds.width/bounds.height+"))";mermaidZoom=pele.enableZoom(canvas,{always:true,labels:{reset:"Fit diagram"}})}catch{}}
async function initMermaid(){const blocks=[...document.querySelectorAll("[data-mermaid]")].filter(block=>!block.dataset.mermaidReady);if(!blocks.length)return;let i=0;for(const block of blocks){const source=block.dataset.mermaid||"";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){resetMermaidBlock(block,source);block.dataset.mermaidReady="true";block.classList.add("is-error");block.insertAdjacentHTML("afterbegin",'