Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 44 additions & 6 deletions scripts/docs-site/diagram-interactions.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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]]) {
Expand All @@ -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());
Expand All @@ -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");
});
}
10 changes: 5 additions & 5 deletions scripts/docs-site/docs-layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
Expand Down Expand Up @@ -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; }
8 changes: 5 additions & 3 deletions scripts/docs-site/site-css.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
7 changes: 4 additions & 3 deletions scripts/docs-site/site-js.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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='<svg class="icon icon-maximize-2" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="m21 3-7 7"/><path d="M9 21H3v-6"/><path d="m3 21 7-7"/></svg>';
const mermaidCloseIcon='<svg class="icon icon-x" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>';
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="<pre><code>"+escapeHtml(source)+"</code></pre>"}
async function renderMermaidSvg(source,renderId){
const pele=await (mermaidReady??=import(withBase("/assets/pele-1.6.0.js")));
Expand All @@ -220,9 +220,10 @@ async function renderMermaidSvg(source,renderId){
}
function mermaidExpandButton(){return '<div class="oc-mermaid-actions"><button type="button" class="oc-mermaid-expand" data-mermaid-expand aria-label="Expand diagram" title="Expand diagram">'+mermaidExpandIcon+'<span class="oc-visually-hidden">Expand diagram</span></button></div>'}
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='<div class="oc-mermaid-overlay-head"><button type="button" class="oc-mermaid-overlay-close" data-mermaid-overlay-close aria-label="Close expanded diagram">'+mermaidCloseIcon+'</button></div><div class="oc-mermaid-overlay-canvas" data-mermaid-overlay-canvas></div>';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="<pre><code>"+escapeHtml(source)+"</code></pre>";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="<pre><code>"+escapeHtml(source)+"</code></pre>";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",'<div class="oc-mermaid-error"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4M12 17h.01"/></svg><span>Diagram could not be rendered</span></div>')}}}
function setCopyFeedback(control,state){if(!control)return;if(control.matches("[data-code-copy]")||control.matches("[data-prompt-copy]")||control.matches("[data-chat-copy]")||control.matches("[data-copy-page]")||control.matches("[data-heading-anchor]")){const label=control.dataset.copyLabel||control.getAttribute("aria-label")||"Copy";control.dataset.copyLabel=label;const feedback=control.querySelector("[data-copy-feedback]");const old=control.dataset.copyDefaultText||feedback?.textContent||label;control.dataset.copyDefaultText=old;clearTimeout(control._copyStateTimer);if(state==="copying"){control.dataset.copyState="copying";control.setAttribute("aria-label","Copying");if(feedback)feedback.textContent="Copying...";return}const ok=state==="copied";control.dataset.copyState=ok?"copied":"failed";control.setAttribute("aria-label",ok?"Copied":"Copy failed");if(control.hasAttribute("title"))control.title=ok?"Copied":"Copy failed";if(feedback)feedback.textContent=ok?"Copied":"Copy failed";control._copyStateTimer=setTimeout(()=>{delete control.dataset.copyState;control.setAttribute("aria-label",label);if(control.hasAttribute("title"))control.title=label;if(feedback)feedback.textContent=old},1300);return}const old=control.textContent;control.textContent=state==="copied"?"Copied":"Copy failed";setTimeout(()=>control.textContent=old||"Copy",1300)}
function showCopyFeedback(control,ok){setCopyFeedback(control,ok?"copied":"failed")}
Expand Down
4 changes: 4 additions & 0 deletions scripts/docs-site/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ if (!fs.existsSync(path.join(site, "assets/pele-1.6.0.js"))
|| !fs.existsSync(path.join(site, "assets/pele-LICENSE.txt"))) {
throw new Error("assets: Pele runtime and license were not copied");
}
if (!siteJs.includes("pele.enableZoom(canvas") || !siteCss.includes("container-type:size")
|| siteCss.includes("min-width:min(1040px,100%)")) {
throw new Error("assets: expanded diagrams must fit the viewport and retain zoom controls");
}
if (!/\.sidebar\{[^}]*padding:0 6px var\(--oc-space-8\) 0;[^}]*scrollbar-gutter:stable/.test(siteCss)) {
throw new Error("assets: sidebar scroll-end padding is missing");
}
Expand Down
Loading
Loading