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
30 changes: 29 additions & 1 deletion scripts/docs-site/assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,34 @@ ${designSystemCss}
.language-option{grid-template-columns:24px minmax(0,1fr) 18px;gap:var(--oc-space-2);padding:var(--oc-space-2) 10px;border-radius:var(--oc-radius-control);font-size:var(--oc-font-size-md);line-height:1.15}
.language-check{font-size:16px}
@media(max-width:820px){.language-menu{width:min(300px,calc(100vw - 28px));max-height:min(62vh,430px)}.language-native{display:block;position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;border:0;opacity:0;font-size:16px;z-index:2;cursor:pointer;-webkit-appearance:none;appearance:none}}
.sidebar-head,.mobile-section-switcher,.sidebar-section-label{display:none}
@media(max-width:820px){
.header-row{position:relative;grid-template-columns:auto minmax(0,1fr) auto}
.header-left{position:absolute;grid-area:auto;top:30px;left:50%;width:max-content;transform:translate(-50%,-50%)}
.brand-tag,.header-links .top-icon-link{display:none}
.theme-toggle{border:0!important;background:transparent!important}
.sidebar{height:100vh;height:100dvh;max-height:100vh;max-height:100dvh;padding:0 18px 28px}
.sidebar-head{position:sticky;top:0;z-index:2;display:flex;align-items:center;justify-content:space-between;min-height:64px;margin:0 -2px 14px;padding:10px 2px;border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--bg) 94%,transparent);backdrop-filter:blur(16px)}
.sidebar-head>strong{color:var(--ink);font:720 var(--oc-font-size-base)/1.2 var(--oc-font-body)}
.sidebar-close{display:inline-grid;place-items:center;width:40px;height:40px;margin:0;padding:0;border:1px solid var(--line-strong);border-radius:var(--oc-radius-control);background:var(--paper);color:var(--muted);cursor:pointer;transition:color var(--oc-duration-fast) var(--oc-ease-out),border-color var(--oc-duration-fast) var(--oc-ease-out),background var(--oc-duration-fast) var(--oc-ease-out)}
.sidebar-close:hover,.sidebar-close:focus-visible{border-color:color-mix(in srgb,var(--brand) 44%,var(--line-strong));background:var(--paper-2);color:var(--ink);outline:0}
.sidebar-close .icon{width:18px;height:18px}
.mobile-section-switcher{display:block;margin:0 0 24px}
.mobile-section-switcher summary{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:12px;min-height:56px;padding:9px 12px;border:1px solid var(--line-strong);border-radius:var(--oc-radius-control);background:color-mix(in srgb,var(--paper) 72%,transparent);cursor:pointer;list-style:none;transition:background var(--oc-duration-fast) var(--oc-ease-out),border-color var(--oc-duration-fast) var(--oc-ease-out)}
.mobile-section-switcher summary::-webkit-details-marker{display:none}
.mobile-section-switcher summary:hover,.mobile-section-switcher summary:focus-visible{border-color:color-mix(in srgb,var(--brand) 44%,var(--line-strong));background:var(--paper-2);outline:0}
.mobile-section-copy{display:grid;gap:2px;min-width:0}
.mobile-section-label{color:var(--muted);font:750 10px/1.2 var(--oc-font-mono);letter-spacing:.08em;text-transform:uppercase}
.mobile-section-copy>strong{overflow:hidden;color:var(--ink);font:700 var(--oc-font-size-base)/1.25 var(--oc-font-body);text-overflow:ellipsis;white-space:nowrap}
.mobile-section-chevron{display:grid;place-items:center;color:var(--muted);transition:transform var(--oc-duration-fast) var(--oc-ease-out),color var(--oc-duration-fast) var(--oc-ease-out)}
.mobile-section-switcher[open] .mobile-section-chevron{transform:rotate(180deg);color:var(--brand)}
.mobile-tabs{display:none}
.mobile-section-switcher[open] .mobile-tabs{display:grid;gap:2px;max-height:min(56vh,500px);margin:8px 0 0;padding:6px;overflow:auto;overscroll-behavior:contain;border:1px solid var(--line-strong);border-radius:var(--oc-radius-control);background:color-mix(in srgb,var(--paper) 88%,var(--bg) 12%);box-shadow:var(--oc-shadow-md)}
.mobile-tab-link{display:flex;align-items:center;min-width:0;min-height:40px;padding:9px 10px;border-radius:var(--oc-radius-control);color:var(--text);font:650 var(--oc-font-size-sm)/1.3 var(--oc-font-body);overflow-wrap:anywhere;transition:background var(--oc-duration-fast) var(--oc-ease-out),color var(--oc-duration-fast) var(--oc-ease-out)}
.mobile-tab-link:hover,.mobile-tab-link:focus-visible{background:var(--paper-2);color:var(--ink);outline:0}
.mobile-tab-link.active{background:var(--soft);color:var(--brand)}
.sidebar-section-label{display:block;margin:0 0 14px;color:var(--muted);font:750 var(--oc-font-size-xs)/1.2 var(--oc-font-mono);letter-spacing:.08em;text-transform:uppercase}
}
@media(max-width:640px){.sidebar{width:100vw;border-right:0}}
.site-footer{border-top:1px solid var(--line);margin-top:var(--oc-space-5)}
.site-footer-inner{max-width:1780px;margin:0 auto;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--oc-space-3) var(--oc-space-5);padding:26px 200px 26px 56px;color:var(--muted);font:500 var(--oc-font-size-xs)/1.6 var(--oc-font-mono)}
Expand Down Expand Up @@ -97,7 +125,7 @@ function updateSidebarFade(){const sidebar=document.querySelector(".sidebar");if
function scrollActiveNavLink(){const sidebar=document.querySelector(".sidebar");const active=sidebar?.querySelector(".nav-link.active");if(!sidebar||!active){updateSidebarFade();return}const target=active.offsetTop-sidebar.clientHeight/2+active.offsetHeight/2;const max=sidebar.scrollHeight-sidebar.clientHeight;sidebar.scrollTop=Math.max(0,Math.min(max,target));updateSidebarFade()}
function syncSidebar(nextDoc){const current=document.querySelector(".sidebar");const incoming=nextDoc.querySelector(".sidebar");if(!current||!incoming)return;const scroll=current.scrollTop;if(sameSidebarLinks(current,incoming)){const nextLinks=[...incoming.querySelectorAll("a")];current.querySelectorAll("a").forEach((link,i)=>link.className=nextLinks[i]?.className||link.className);current.scrollTop=scroll}else{current.replaceWith(incoming)}scrollActiveNavLink()}
const compactTocQuery=matchMedia("(max-width:1120px)");function syncStickyHeaderOffset(){const header=document.querySelector(".site-header");const height=Math.ceil(header?.getBoundingClientRect().height||0);if(height)root.style.setProperty("--sticky-header-h",height+"px")}function syncTocDisclosure(){const visible=Math.max(scrollY,document.scrollingElement?.scrollTop||0)>8;document.querySelectorAll(".toc").forEach(toc=>{if(compactTocQuery.matches){if(!toc.dataset.compactTocReady){toc.open=false;toc.dataset.compactTocReady="true"}toc.classList.toggle("is-visible",visible)}else{toc.open=true;toc.classList.remove("is-visible");delete toc.dataset.compactTocReady}})}
function setNavOpen(open){document.body.classList.toggle("nav-open",open);const sidebar=document.querySelector(".sidebar");const toggle=document.querySelector("[data-nav-toggle]");sidebar?.classList.toggle("open",open);toggle?.setAttribute("aria-expanded",String(open))}
function setNavOpen(open){document.body.classList.toggle("nav-open",open);const sidebar=document.querySelector(".sidebar");const toggle=document.querySelector("[data-nav-toggle]");sidebar?.classList.toggle("open",open);toggle?.setAttribute("aria-expanded",String(open));if(open){sidebar?.scrollTo({top:0});requestAnimationFrame(()=>sidebar?.querySelector("[data-nav-close]")?.focus())}else if(sidebar?.contains(document.activeElement)){toggle?.focus()}}
function swap(selector,nextDoc){const current=document.querySelector(selector);const incoming=nextDoc.querySelector(selector);if(current&&incoming)current.replaceWith(incoming)}
function placeTabUnderline(animate){const tabs=document.querySelector(".tabs");if(!tabs)return;const underline=tabs.querySelector(".tab-underline");const active=tabs.querySelector(".tab-link.active");if(!underline)return;if(!active){tabs.classList.remove("is-ready");underline.style.setProperty("--tu-w","0px");return}if(!animate)underline.classList.add("no-transition");underline.style.setProperty("--tu-x",active.offsetLeft+"px");underline.style.setProperty("--tu-w",active.offsetWidth+"px");tabs.classList.add("is-ready");if(!animate)requestAnimationFrame(()=>requestAnimationFrame(()=>underline.classList.remove("no-transition")))}
function swapTabs(nextDoc){const current=document.querySelector(".tabs");const incoming=nextDoc.querySelector(".tabs");if(!current||!incoming)return;const oldUnderline=current.querySelector(".tab-underline");const oldX=oldUnderline?.style.getPropertyValue("--tu-x");const oldW=oldUnderline?.style.getPropertyValue("--tu-w");const wasReady=current.classList.contains("is-ready");current.replaceWith(incoming);const newUnderline=incoming.querySelector(".tab-underline");if(newUnderline&&oldX&&oldW&&wasReady){newUnderline.classList.add("no-transition");newUnderline.style.setProperty("--tu-x",oldX);newUnderline.style.setProperty("--tu-w",oldW);incoming.classList.add("is-ready");requestAnimationFrame(()=>{newUnderline.classList.remove("no-transition");placeTabUnderline(true)})}else{placeTabUnderline(false)}}
Expand Down
16 changes: 12 additions & 4 deletions scripts/docs-site/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ function siteHeader(page, nav, activeTab) {
const tabs = nav.map((tab) => {
const href = pageUrl(firstPage(tab));
const active = tab.title === activeTab ? " active" : "";
return `<a class="tab-link${active}" href="${href}">${escapeHtml(tab.title)}</a>`;
const current = tab.title === activeTab ? ' aria-current="location"' : "";
return `<a class="tab-link${active}" href="${href}"${current}>${escapeHtml(tab.title)}</a>`;
}).join("");
return `<header class="site-header">
<div class="header-row">
Expand Down Expand Up @@ -370,10 +371,17 @@ function siteFooter() {
}

function sidebar(page, nav, activeTab) {
const groups = (nav.find((tab) => tab.title === activeTab) ?? nav[0])?.groups ?? [];
const currentTab = nav.find((tab) => tab.title === activeTab) ?? nav[0];
const groups = currentTab?.groups ?? [];
const mobileTabs = nav.map((tab) => {
const active = tab.title === currentTab?.title;
return `<a class="mobile-tab-link${active ? " active" : ""}" href="${pageUrl(firstPage(tab))}"${active ? ' aria-current="location"' : ""}>${escapeHtml(tab.title)}</a>`;
}).join("");
return `<aside class="sidebar">
<button class="sidebar-close" type="button" data-nav-close aria-label="Close menu">Close</button>
<nav>${groups.map((group) => navGroupHtml(page, group)).join("")}</nav>
<div class="sidebar-head"><strong>Browse docs</strong><button class="sidebar-close" type="button" data-nav-close aria-label="Close menu">${icon("x")}</button></div>
<details class="mobile-section-switcher"><summary><span class="mobile-section-copy"><span class="mobile-section-label">Section</span><strong>${escapeHtml(currentTab?.title ?? "Docs")}</strong></span><span class="mobile-section-chevron" aria-hidden="true">${icon("chevron-down")}</span></summary><nav class="mobile-tabs" aria-label="Docs sections">${mobileTabs}</nav></details>
<p class="sidebar-section-label">In this section</p>
<nav aria-label="${escapeAttr(`${currentTab?.title ?? "Docs"} pages`)}">${groups.map((group) => navGroupHtml(page, group)).join("")}</nav>
</aside>`;
}

Expand Down
7 changes: 7 additions & 0 deletions scripts/docs-site/smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,13 @@ if (!/let tocObserver=null/.test(siteJs)
if (!/function setNavOpen/.test(siteJs) || !/body\.nav-open:before/.test(siteCss) || !/data-nav-close/.test(index)) {
throw new Error("assets: mobile navigation drawer state is missing");
}
if (!/class="mobile-tabs" aria-label="Docs sections"/.test(index)
|| !/class="mobile-tab-link active"[^>]*aria-current="location"/.test(index)
|| !/class="mobile-section-switcher"><summary>/.test(index)
|| !/\.mobile-section-switcher\[open\] \.mobile-tabs\{display:grid;gap:2px/.test(siteCss)
|| !/\.mobile-tab-link\.active\{background:var\(--soft\);color:var\(--brand\)/.test(siteCss)) {
throw new Error("assets: mobile global docs navigation is missing");
}
if (/data-locale/.test(siteJs)) {
throw new Error("assets: stale native language select handler is still present");
}
Expand Down
63 changes: 60 additions & 3 deletions scripts/docs-site/visual-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -508,19 +508,35 @@ async function checkMobile() {
const header = document.querySelector(".site-header")?.getBoundingClientRect();
const menu = document.querySelector("[data-nav-toggle]")?.getBoundingClientRect();
const theme = document.querySelector("[data-theme-toggle]")?.getBoundingClientRect();
const themeStyle = getComputedStyle(document.querySelector("[data-theme-toggle]"));
const search = document.querySelector("[data-search-open]")?.getBoundingClientRect();
const brand = document.querySelector(".brand")?.getBoundingClientRect();
const github = document.querySelector('.top-icon-link[aria-label="GitHub"]')?.getBoundingClientRect();
const discord = document.querySelector('.top-icon-link[aria-label="Discord"]')?.getBoundingClientRect();
const code = document.querySelector(".oc-code")?.getBoundingClientRect();
const step = document.querySelector(".oc-step")?.getBoundingClientRect();
const overlap = (a, b) => Boolean(a && b && a.left < b.right && a.right > b.left && a.top < b.bottom && a.bottom > b.top);
return {
headerWidth: header?.width,
menuThemeOverlap: overlap(menu, theme),
themeHasChrome: parseFloat(themeStyle.borderTopWidth) > 0 || themeStyle.backgroundColor !== "rgba(0, 0, 0, 0)",
brandCenterDelta: brand ? Math.abs((brand.left + brand.right) / 2 - innerWidth / 2) : null,
githubVisible: Boolean(github && github.width > 0 && github.height > 0),
discordVisible: Boolean(discord && discord.width > 0 && discord.height > 0),
searchInViewport: search ? search.left >= 0 && search.right <= innerWidth : false,
codeInViewport: code ? code.left >= 0 && code.right <= innerWidth + 1 : false,
stepInset: step?.left,
};
});
if (geometry.menuThemeOverlap || !geometry.searchInViewport || !geometry.codeInViewport || geometry.stepInset < 0) {
if (geometry.menuThemeOverlap
|| geometry.themeHasChrome
|| geometry.brandCenterDelta === null
|| geometry.brandCenterDelta > 2
|| geometry.githubVisible
|| geometry.discordVisible
|| !geometry.searchInViewport
|| !geometry.codeInViewport
|| geometry.stepInset < 0) {
throw new Error(`mobile visual geometry failed: ${JSON.stringify(geometry)}`);
}
const mobileCardColumns = await page.evaluate(() => {
Expand All @@ -538,30 +554,71 @@ async function checkMobile() {
await page.click("[data-nav-toggle]");
await page.locator(".sidebar.open").waitFor({ state: "visible" });
await page.waitForFunction(() => Math.abs(document.querySelector(".sidebar")?.getBoundingClientRect().left ?? -999) < 1);
await page.screenshot({ path: path.join(artifacts, "elements-mobile-menu.png"), fullPage: false });
const menu = await page.evaluate(() => {
const sidebar = document.querySelector(".sidebar")?.getBoundingClientRect();
const close = document.querySelector("[data-nav-close]")?.getBoundingClientRect();
const toggle = document.querySelector("[data-nav-toggle]");
const closeStyle = getComputedStyle(document.querySelector("[data-nav-close]"));
const sectionSwitcher = document.querySelector(".mobile-section-switcher");
const sectionSwitcherRect = sectionSwitcher?.getBoundingClientRect();
return {
bodyOpen: document.body.classList.contains("nav-open"),
ariaExpanded: toggle?.getAttribute("aria-expanded"),
sidebarLeft: sidebar?.left,
sidebarRight: sidebar?.right,
sidebarWidth: sidebar?.width,
sidebarTop: sidebar?.top,
sidebarBottom: sidebar?.bottom,
closeVisible: closeStyle.display !== "none" && close && close.height >= 36,
sectionSwitcherVisible: Boolean(sectionSwitcherRect && sectionSwitcherRect.width > 0 && sectionSwitcherRect.height > 0),
sectionSwitcherOpen: sectionSwitcher?.hasAttribute("open"),
sectionSwitcherCurrent: sectionSwitcher?.querySelector(".mobile-section-copy > strong")?.textContent?.trim(),
drawerAtTop: (document.querySelector(".sidebar")?.scrollTop ?? -1) <= 1,
closeFocused: document.activeElement === document.querySelector("[data-nav-close]"),
viewport: innerWidth,
viewportHeight: innerHeight,
};
});
if (!menu.bodyOpen
|| menu.ariaExpanded !== "true"
|| Math.abs(menu.sidebarLeft) >= 1
|| Math.abs(menu.sidebarRight - menu.viewport) >= 1
|| Math.abs(menu.sidebarWidth - menu.viewport) >= 1
|| !menu.closeVisible) {
|| Math.abs(menu.sidebarTop) >= 1
|| Math.abs(menu.sidebarBottom - menu.viewportHeight) >= 1
|| !menu.closeVisible
|| !menu.sectionSwitcherVisible
|| menu.sectionSwitcherOpen
|| !menu.sectionSwitcherCurrent
|| !menu.drawerAtTop
|| !menu.closeFocused) {
throw new Error(`mobile menu drawer failed (expected full-bleed on phones): ${JSON.stringify(menu)}`);
}
await page.click(".mobile-section-switcher > summary");
await page.locator(".mobile-tabs").waitFor({ state: "visible" });
await page.screenshot({ path: path.join(artifacts, "elements-mobile-menu.png"), fullPage: false });
const sections = await page.evaluate(() => {
const container = document.querySelector(".mobile-tabs")?.getBoundingClientRect();
const links = [...document.querySelectorAll(".mobile-tab-link")];
const rects = links.map((link) => link.getBoundingClientRect());
return {
open: document.querySelector(".mobile-section-switcher")?.hasAttribute("open"),
mobileTabCount: links.length,
desktopTabCount: document.querySelectorAll(".tab-link").length,
activeCount: document.querySelectorAll('.mobile-tab-link[aria-current="location"]').length,
linksVisible: rects.every((rect) => rect.width > 0 && rect.height > 0),
linksInViewport: rects.every((rect) => rect.left >= 0 && rect.right <= innerWidth),
lastLinkVisible: Boolean(container && rects.at(-1)?.bottom <= container.bottom + 1),
};
});
if (!sections.open
|| sections.mobileTabCount !== sections.desktopTabCount
|| sections.activeCount !== 1
|| !sections.linksVisible
|| !sections.linksInViewport
|| !sections.lastLinkVisible) {
throw new Error(`mobile docs section switcher failed: ${JSON.stringify(sections)}`);
}
await page.keyboard.press("Escape");
const closed = await page.evaluate(() => ({
bodyOpen: document.body.classList.contains("nav-open"),
Expand Down