Publish multi-version documentation site (#1727)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bb4b44f0-f505-49c5-9878-76e2b40c7a15 Copilot-Session: 8eac1204-10ae-4319-9333-aee5dd43dcc8
This commit is contained in:
parent
a5179df39b
commit
ae36026695
2 changed files with 819 additions and 134 deletions
102
docs/version-picker/version-picker.css
Normal file
102
docs/version-picker/version-picker.css
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Version picker widget for the multi-version MCP C# SDK docs site.
|
||||
* Injected into every generated page by scripts/inject-version-picker.mjs.
|
||||
* Two placements: inline in the DocFX "modern" navbar, or a fixed floating
|
||||
* fallback when the navbar cannot be found.
|
||||
*/
|
||||
.dv-picker {
|
||||
--dv-fg: #24292f;
|
||||
--dv-bg: #ffffff;
|
||||
--dv-border: #d0d7de;
|
||||
--dv-accent: #0969da;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
color: var(--dv-fg);
|
||||
}
|
||||
|
||||
.dv-picker__label {
|
||||
opacity: 0.75;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dv-picker__select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
background-color: var(--dv-bg);
|
||||
color: var(--dv-fg);
|
||||
border: 1px solid var(--dv-border);
|
||||
border-radius: 6px;
|
||||
padding: 0.25rem 1.55rem 0.25rem 0.55rem;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23636c76'%3E%3Cpath d='M4.22 6.22a.75.75 0 0 1 1.06 0L8 8.94l2.72-2.72a.75.75 0 1 1 1.06 1.06l-3.25 3.25a.75.75 0 0 1-1.06 0L4.22 7.28a.75.75 0 0 1 0-1.06Z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 0.45rem center;
|
||||
background-size: 0.72rem;
|
||||
}
|
||||
|
||||
.dv-picker__select:hover {
|
||||
border-color: var(--dv-accent);
|
||||
}
|
||||
|
||||
.dv-picker__badge {
|
||||
display: inline-block;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.02em;
|
||||
color: #9a6700;
|
||||
background: #fff8c5;
|
||||
border: 1px solid #eac54f;
|
||||
}
|
||||
|
||||
.dv-prerelease-banner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.4rem 1rem;
|
||||
color: #24292f;
|
||||
background: #fff8c5;
|
||||
border-bottom: 1px solid #eac54f;
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Inline in the navbar (preferred placement). */
|
||||
.dv-picker--navbar {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
/* Fixed fallback when no navbar brand was found. */
|
||||
.dv-picker--floating {
|
||||
position: fixed;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
z-index: 2147483000;
|
||||
padding: 0.45rem 0.6rem;
|
||||
background: var(--dv-bg);
|
||||
border: 1px solid var(--dv-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* DocFX's explicit Bootstrap theme controls the widget appearance. */
|
||||
[data-bs-theme="dark"] .dv-picker {
|
||||
--dv-fg: #e6edf3;
|
||||
--dv-bg: #161b22;
|
||||
--dv-border: #30363d;
|
||||
--dv-accent: #4493f8;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] .dv-prerelease-banner {
|
||||
color: #e6edf3;
|
||||
background: #3d2e00;
|
||||
border-color: #9a6700;
|
||||
}
|
||||
159
docs/version-picker/version-picker.js
Normal file
159
docs/version-picker/version-picker.js
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Version picker widget for the multi-version MCP C# SDK docs site.
|
||||
*
|
||||
* Configuration is provided per-page via a `window.__DOCS__` object that
|
||||
* scripts/inject-version-picker.mjs writes into each page's <head>:
|
||||
*
|
||||
* window.__DOCS__ = {
|
||||
* version: "2.0", // slug of the version this page belongs to
|
||||
* base: "/", // site base path ("/" for a custom domain)
|
||||
* default: "1.x", // slug the site root redirects to
|
||||
* versions: [ // newest first; drives the dropdown
|
||||
* { slug: "2.0", label: "2.0 (preview)", prerelease: true },
|
||||
* { slug: "1.x", label: "1.x (latest)", prerelease: false }
|
||||
* ]
|
||||
* };
|
||||
*
|
||||
* The pure helpers (joinPath / stripVersionPrefix / targetPath) are exported so
|
||||
* they can be unit-tested in Node without a DOM.
|
||||
*/
|
||||
|
||||
export function joinPath(a, b) {
|
||||
const left = String(a || "").replace(/\/+$/, "");
|
||||
const right = String(b == null ? "" : b).replace(/^\/+/, "");
|
||||
return (left + "/" + right).replace(/\/{2,}/g, "/");
|
||||
}
|
||||
|
||||
// Given the current pathname, return the part after "<base>/<currentSlug>/".
|
||||
export function stripVersionPrefix(pathname, base, currentSlug) {
|
||||
const prefix = joinPath(joinPath(base, currentSlug), "");
|
||||
if (pathname.startsWith(prefix)) return pathname.slice(prefix.length);
|
||||
// Tolerate a missing trailing slash (e.g. "/2.0").
|
||||
const bare = joinPath(base, currentSlug);
|
||||
if (pathname === bare) return "";
|
||||
return "";
|
||||
}
|
||||
|
||||
// Derive the deployment base from the page URL. This keeps picker navigation
|
||||
// within project Pages sites, whose content is hosted below "/<repository>/".
|
||||
export function getBasePath(cfg, pathname) {
|
||||
const marker = `/${cfg.version}/`;
|
||||
const markerIndex = pathname.indexOf(marker);
|
||||
if (markerIndex !== -1) return pathname.slice(0, markerIndex + 1) || "/";
|
||||
|
||||
const bareMarker = `/${cfg.version}`;
|
||||
if (pathname.endsWith(bareMarker)) {
|
||||
return pathname.slice(0, -bareMarker.length + 1) || "/";
|
||||
}
|
||||
|
||||
return cfg.base || "/";
|
||||
}
|
||||
|
||||
// Compute the equivalent path in another version, preserving the sub-page.
|
||||
export function targetPath(cfg, targetSlug, pathname) {
|
||||
const base = getBasePath(cfg, pathname);
|
||||
const sub = stripVersionPrefix(pathname, base, cfg.version);
|
||||
return joinPath(joinPath(base, targetSlug), sub);
|
||||
}
|
||||
|
||||
function h(tag, attrs, children) {
|
||||
const el = document.createElement(tag);
|
||||
for (const k in attrs || {}) {
|
||||
if (k === "class") el.className = attrs[k];
|
||||
else el.setAttribute(k, attrs[k]);
|
||||
}
|
||||
for (const c of children || []) {
|
||||
el.appendChild(typeof c === "string" ? document.createTextNode(c) : c);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function versionRoot(cfg, slug, pathname) {
|
||||
return joinPath(joinPath(getBasePath(cfg, pathname), slug), "");
|
||||
}
|
||||
|
||||
async function navigate(cfg, slug) {
|
||||
const candidate = targetPath(cfg, slug, location.pathname);
|
||||
const home = versionRoot(cfg, slug, location.pathname);
|
||||
// Best effort: keep the reader on the same page if it exists in the target
|
||||
// version; otherwise fall back to that version's landing page.
|
||||
try {
|
||||
const res = await fetch(candidate, { method: "HEAD" });
|
||||
if (res.ok) {
|
||||
location.assign(candidate + location.hash);
|
||||
return;
|
||||
}
|
||||
} catch (_) {
|
||||
/* HEAD may be blocked (e.g. file://) -- fall through to the version home. */
|
||||
}
|
||||
location.assign(home);
|
||||
}
|
||||
|
||||
function previewBadge() {
|
||||
return h("span", { class: "dv-picker__badge" }, ["preview"]);
|
||||
}
|
||||
|
||||
function buildPicker(cfg) {
|
||||
const current = cfg.versions.find((v) => v.slug === cfg.version);
|
||||
|
||||
const select = h("select", {
|
||||
class: "dv-picker__select",
|
||||
"aria-label": "Select documentation version",
|
||||
});
|
||||
for (const v of cfg.versions) {
|
||||
const opt = h("option", { value: v.slug }, [v.label || v.slug]);
|
||||
if (v.slug === cfg.version) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
select.addEventListener("change", () => navigate(cfg, select.value));
|
||||
|
||||
const children = [h("span", { class: "dv-picker__label" }, ["Version"]), select];
|
||||
if (current && current.prerelease) {
|
||||
children.push(previewBadge());
|
||||
}
|
||||
return h("div", { class: "dv-picker" }, children);
|
||||
}
|
||||
|
||||
function buildPrereleaseBanner() {
|
||||
return h("div", {
|
||||
class: "dv-prerelease-banner",
|
||||
role: "status",
|
||||
}, [
|
||||
"You are currently viewing",
|
||||
previewBadge(),
|
||||
"documentation.",
|
||||
]);
|
||||
}
|
||||
|
||||
function mount(picker) {
|
||||
const brand = document.querySelector(".navbar-brand");
|
||||
if (brand && brand.parentNode) {
|
||||
picker.classList.add("dv-picker--navbar");
|
||||
brand.parentNode.insertBefore(picker, brand.nextSibling);
|
||||
return;
|
||||
}
|
||||
picker.classList.add("dv-picker--floating");
|
||||
document.body.appendChild(picker);
|
||||
}
|
||||
|
||||
function init() {
|
||||
const cfg = window.__DOCS__;
|
||||
if (!cfg || !Array.isArray(cfg.versions) || cfg.versions.length < 2) return;
|
||||
if (document.querySelector(".dv-picker, .dv-prerelease-banner")) return; // idempotent
|
||||
|
||||
const current = cfg.versions.find((v) => v.slug === cfg.version);
|
||||
if (!current) return;
|
||||
if (current.prerelease) {
|
||||
document.body.prepend(buildPrereleaseBanner());
|
||||
}
|
||||
|
||||
mount(buildPicker(cfg));
|
||||
}
|
||||
|
||||
if (typeof document !== "undefined" && typeof document.createElement === "function") {
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue