diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9c1fb831..16db0d2c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -134,11 +134,11 @@ jobs: with: instance: ${{ matrix.language.module }}/asu - # Everything the build profile carries — the structured data, the canonical - # link, the language switcher, the search widget — is easy to lose silently: - # it was missing from the site for a while and nobody noticed. This assembles - # the five builds the way the deploy does and fails when a page comes out - # without them. + # What every page has to carry — the structured data, the canonical link, the + # row of links to the other languages, the notice at the end — is written in + # after the build, and it is easy to lose silently: it was missing from the + # site for a while and nobody noticed. This assembles the five builds the way + # the deploy does and fails when a page comes out without them. assembled: needs: build runs-on: ubuntu-latest @@ -183,7 +183,7 @@ jobs: missing=0 for page in dir/overview.html dir/ru/overview.html dir/appendcolumns.html; do - for marker in 'rel="canonical"' 'asu-fab' 'asu-language-open' 'SoftwareSourceCode'; do + for marker in 'rel="canonical"' 'asu-languages' 'asu-ai-notice' 'SoftwareSourceCode'; do if ! grep -qF "$marker" "$page"; then echo "::error::${page} is missing ${marker}" missing=1 @@ -234,18 +234,6 @@ jobs: unzip -qq "artifacts/docs-${code}/${ARTIFACT}" -d "dir/${code}" done - # Feeds the search widget injected by each project's search.html. Every - # language gets its own index, next to its own pages. - - name: Build the search indexes - run: | - set -euo pipefail - - node scripts/docs/build-search-index.mjs dir - - for code in ru uk de fr; do - node scripts/docs/build-search-index.mjs "dir/${code}" - done - # Writes the hreflang links into the served HTML, one sitemap covering # every language, and robots.txt. It has to run after all five builds are # in place, because that is when the alternates are known. diff --git a/AGENTS.md b/AGENTS.md index d9daee56..9c3931a4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -201,12 +201,14 @@ deploy and an archive that lives only there disappears with the next one. `npm run docs:help-versions -- ` writes the file the switcher reads. -Two scripts take an argument and run against a built site rather than the -sources: `npm run docs:search-index -- ` writes that directory's -`search-index.json`, once per language, and `npm run docs:finalize-site -- ` -writes the `hreflang` links into the served HTML, the sitemap covering every -language, and `robots.txt`. The second one has to run after all five builds sit -in one directory — that is when it can know which pages exist in which language. +`npm run docs:finalize-site -- ` runs against a built site +rather than the sources. It writes into every page what the build profile does +not: the structured data, the canonical link, the `og:image` address, the row of +links to the other languages under the breadcrumbs, and the notice at the end +saying the documentation was written by AI. It also writes the `hreflang` links, +the sitemap covering every language and `robots.txt`, which is why it has to run +after all five builds sit in one directory — that is when it can know which +pages exist in which language. The languages, and every label around the prose, are declared in `scripts/docs/languages.mjs`. Adding a language is a change to that file and a diff --git a/docs/writerside-de/search.html b/docs/writerside-de/search.html deleted file mode 100644 index 3cf9da14..00000000 --- a/docs/writerside-de/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-fr/search.html b/docs/writerside-fr/search.html deleted file mode 100644 index 64ca7bef..00000000 --- a/docs/writerside-fr/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-ru/search.html b/docs/writerside-ru/search.html deleted file mode 100644 index 43c0c34b..00000000 --- a/docs/writerside-ru/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside-uk/search.html b/docs/writerside-uk/search.html deleted file mode 100644 index 773bcbfe..00000000 --- a/docs/writerside-uk/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/docs/writerside/search.html b/docs/writerside/search.html deleted file mode 100644 index 0f87cb97..00000000 --- a/docs/writerside/search.html +++ /dev/null @@ -1,635 +0,0 @@ - - - - - - - - - - - diff --git a/package.json b/package.json index 9e0abe96..fe11fccf 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "build:gas": "npm run build && node scripts/gas/bundle.mjs scripts/gas/Library.js", "docs:generate": "node scripts/docs/generate-topics.mjs", "docs:check": "node scripts/docs/generate-topics.mjs --check", - "docs:search-index": "node scripts/docs/build-search-index.mjs", "docs:finalize-site": "node scripts/docs/finalize-site.mjs", "docs:help-versions": "node scripts/docs/write-help-versions.mjs", "docs:author": "node scripts/docs/author-guards.mjs && node scripts/docs/author-exceptions.mjs && node scripts/docs/author-batch.mjs", diff --git a/scripts/docs/assets/search-widget.js b/scripts/docs/assets/search-widget.js deleted file mode 100644 index 2a24c6bc..00000000 --- a/scripts/docs/assets/search-widget.js +++ /dev/null @@ -1,365 +0,0 @@ -/** - * The search widget and language switcher injected into every published page. - * - * `scripts/docs/site.mjs` reads this file and replaces `__ASU_STRINGS__` with - * the labels of the language being built. Keeping it a real file, rather than a - * string inside the generator, is what keeps its regular expressions intact. - */ - -/* global __ASU_STRINGS__ */ - -(function () { - var strings = __ASU_STRINGS__; - - var dialog = document.getElementById("asu-search-dialog"); - - var panel = document.getElementById("asu-search-panel"); - - var input = document.getElementById("asu-search-input"); - - var list = document.getElementById("asu-search-results"); - - var status = document.getElementById("asu-search-status"); - - var open = document.getElementById("asu-search-open"); - - var languageButton = document.getElementById("asu-language-open"); - - var languageMenu = document.getElementById("asu-language-menu"); - - var index = null; - - var loading = null; - - var hits = []; - - var cursor = 0; - - // Every language publishes the same file names, so a language link keeps the - // reader on the page they were reading. - Array.prototype.forEach.call(languageMenu.querySelectorAll("a"), function (link) { - var file = window.location.pathname.split("/").pop(); - - if (file && file.indexOf(".html") !== -1) { - link.href = link.getAttribute("href") + file; - } - }); - - function base() { - var path = window.location.pathname; - - return path.slice(0, path.lastIndexOf("/") + 1); - } - - function load() { - if (loading) { - return loading; - } - - loading = fetch(base() + "search-index.json") - .then(function (response) { - if (!response.ok) { - throw new Error(String(response.status)); - } - - return response.json(); - }) - .then(function (data) { - index = data; - - return data; - }) - .catch(function () { - index = []; - status.textContent = strings.missing; - - return []; - }); - - return loading; - } - - function escapeHtml(text) { - return text.replace(/[&<>"]/g, function (character) { - return { "&": "&", "<": "<", ">": ">", '"': """ }[character]; - }); - } - - function highlight(text, terms) { - var html = escapeHtml(text); - - terms.forEach(function (term) { - var pattern = new RegExp("(" + term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", "ig"); - - html = html.replace(pattern, "$1"); - }); - - return html; - } - - function snippetOf(entry, terms) { - var text = entry.text || ""; - - var at = -1; - - terms.some(function (term) { - at = text.toLowerCase().indexOf(term); - - return at !== -1; - }); - - if (at === -1) { - return entry.summary || text.slice(0, 140); - } - - var from = Math.max(0, at - 60); - - return (from > 0 ? "…" : "") + text.slice(from, from + 160) + "…"; - } - - function score(entry, terms) { - var title = (entry.title || "").toLowerCase(); - - var text = ((entry.summary || "") + " " + (entry.text || "")).toLowerCase(); - - var total = 0; - - for (var i = 0; i < terms.length; i += 1) { - var term = terms[i]; - - if (title === term) { - total += 120; - } else if (title.indexOf(term) === 0) { - total += 60; - } else if (title.indexOf(term) !== -1) { - total += 30; - } else if (text.indexOf(term) !== -1) { - total += 5; - } else { - return 0; - } - } - - return total; - } - - function render(terms) { - list.innerHTML = ""; - - hits.forEach(function (entry, position) { - var item = document.createElement("li"); - - item.setAttribute("role", "option"); - item.setAttribute("aria-selected", position === cursor ? "true" : "false"); - - var link = document.createElement("a"); - - link.href = base() + entry.url; - link.innerHTML = - '' + - highlight(entry.title, terms) + - "" + - '
' + - highlight(snippetOf(entry, terms), terms) + - "
"; - - item.appendChild(link); - list.appendChild(item); - }); - } - - function search() { - var query = input.value.trim().toLowerCase(); - - if (!query) { - hits = []; - list.innerHTML = ""; - status.textContent = strings.hint; - - return; - } - - load().then(function () { - var terms = query.split(/\s+/).filter(Boolean); - - hits = (index || []) - .map(function (entry) { - return { entry: entry, score: score(entry, terms) }; - }) - .filter(function (row) { - return row.score > 0; - }) - .sort(function (a, b) { - return b.score - a.score; - }) - .slice(0, 25) - .map(function (row) { - return row.entry; - }); - - cursor = 0; - status.textContent = hits.length - ? hits.length + " " + strings.matches - : strings.empty + " “" + query + "”"; - - render(terms); - }); - } - - function show() { - dialog.setAttribute("open", ""); - input.value = ""; - input.focus(); - status.textContent = strings.hint; - list.innerHTML = ""; - load(); - } - - function hide() { - dialog.removeAttribute("open"); - } - - function move(step) { - if (hits.length === 0) { - return; - } - - cursor = (cursor + step + hits.length) % hits.length; - - Array.prototype.forEach.call(list.children, function (item, position) { - item.setAttribute("aria-selected", position === cursor ? "true" : "false"); - }); - - var selected = list.children[cursor]; - - if (selected) { - selected.scrollIntoView({ block: "nearest" }); - } - } - - /** - * The header is drawn by the help application after the page loads, so the - * two controls wait for it and then sit beside the theme switcher, wearing - * its own classes. Until it appears — or if it never does — they stay the - * floating pair they were. - */ - function mountInHeader() { - var switchers = document.querySelector(".wh-header__switchers"); - - if (!switchers || switchers.contains(languageButton)) { - return Boolean(switchers); - } - - var theme = switchers.querySelector('[data-test="theme-switcher"]'); - - if (!theme) { - return false; - } - - [open, languageButton].forEach(function (button) { - button.className = theme.className; - button.classList.add("asu-in-header"); - }); - - switchers.insertBefore(open, theme); - switchers.insertBefore(languageButton, theme); - - return true; - } - - if (!mountInHeader()) { - var watcher = new MutationObserver(function () { - if (mountInHeader()) { - watcher.disconnect(); - } - }); - - watcher.observe(document.body, { childList: true, subtree: true }); - - // A header that has not arrived within a few seconds is not coming. - window.setTimeout(function () { - watcher.disconnect(); - }, 8000); - } - - /** - * The menu hangs under its button wherever the button ended up. - */ - function placeMenu() { - var rect = languageButton.getBoundingClientRect(); - - if (!languageButton.classList.contains("asu-in-header")) { - languageMenu.style.top = ""; - languageMenu.style.right = ""; - - return; - } - - languageMenu.style.top = rect.bottom + 8 + "px"; - languageMenu.style.right = Math.max(8, window.innerWidth - rect.right) + "px"; - } - - languageButton.addEventListener("click", function () { - var opened = languageMenu.hasAttribute("open"); - - if (opened) { - languageMenu.removeAttribute("open"); - } else { - placeMenu(); - languageMenu.setAttribute("open", ""); - } - - languageButton.setAttribute("aria-expanded", String(!opened)); - }); - - window.addEventListener("resize", function () { - if (languageMenu.hasAttribute("open")) { - placeMenu(); - } - }); - - document.addEventListener("click", function (event) { - if (!languageMenu.contains(event.target) && event.target !== languageButton) { - languageMenu.removeAttribute("open"); - languageButton.setAttribute("aria-expanded", "false"); - } - }); - - open.addEventListener("click", show); - input.addEventListener("input", search); - - dialog.addEventListener("click", function (event) { - if (!panel.contains(event.target)) { - hide(); - } - }); - - document.addEventListener("keydown", function (event) { - var typing = /^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement.tagName); - - if (!dialog.hasAttribute("open")) { - var shortcut = - (event.key === "/" && !typing) || ((event.metaKey || event.ctrlKey) && event.key === "k"); - - if (shortcut) { - event.preventDefault(); - show(); - } - - return; - } - - if (event.key === "Escape") { - hide(); - } else if (event.key === "ArrowDown") { - event.preventDefault(); - move(1); - } else if (event.key === "ArrowUp") { - event.preventDefault(); - move(-1); - } else if (event.key === "Enter" && hits[cursor]) { - event.preventDefault(); - window.location.href = base() + hits[cursor].url; - } - }); -})(); diff --git a/scripts/docs/build-search-index.mjs b/scripts/docs/build-search-index.mjs deleted file mode 100644 index 9e5474b1..00000000 --- a/scripts/docs/build-search-index.mjs +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Builds `search-index.json` for the search widget in `docs/writerside/search.html`. - * - * It reads the built site rather than the Markdown sources, so the URLs are the - * ones the builder actually produced and the text is the text a reader sees. - * - * Usage: `node scripts/docs/build-search-index.mjs ` - */ - -import { readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; -import { join, relative } from "node:path"; - -const root = process.argv[2]; - -if (!root) { - console.error("Usage: node scripts/docs/build-search-index.mjs "); - process.exit(1); -} - -function listPages(dir) { - const out = []; - - for (const entry of readdirSync(dir)) { - const full = join(dir, entry); - - if (statSync(full).isDirectory()) { - // The builder keeps its assets and the llms.txt copies in their own folders. - if (["_llms", "assets", "images", "webhelp-assets"].includes(entry)) { - continue; - } - - out.push(...listPages(full)); - - continue; - } - - if (entry.endsWith(".html")) { - out.push(full); - } - } - - return out; -} - -function attribute(html, pattern) { - const match = html.match(pattern); - - return match ? match[1].trim() : ""; -} - -function decode(text) { - return text - .replace(/ /g, " ") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/&/g, "&"); -} - -const index = []; - -for (const page of listPages(root)) { - const html = readFileSync(page, "utf8"); - - const url = relative(root, page).split("\\").join("/"); - - // The redirect stub the builder writes at the site root is not a page. - if (/]+http-equiv=["']refresh/i.test(html)) { - continue; - } - - const rawTitle = attribute(html, /([^<]*)<\/title>/i); - - const title = decode(rawTitle.replace(/\s*\|\s*apps-script-utils\s*$/i, "")).trim(); - - if (!title) { - continue; - } - - const summary = decode(attribute(html, /<meta name="description" content="([^"]*)"/i)); - - const body = html - .replace(/<head[\s\S]*?<\/head>/gi, " ") - .replace(/<script[\s\S]*?<\/script>/gi, " ") - .replace(/<style[\s\S]*?<\/style>/gi, " ") - .replace(/<nav[\s\S]*?<\/nav>/gi, " ") - .replace(/<[^>]+>/g, " "); - - const text = decode(body).replace(/\s+/g, " ").trim().slice(0, 4000); - - index.push({ url, title, summary, text }); -} - -index.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); - -writeFileSync(join(root, "search-index.json"), JSON.stringify(index)); - -console.log(`search-index.json: ${index.length} pages`); diff --git a/scripts/docs/finalize-site.mjs b/scripts/docs/finalize-site.mjs index 03101c31..41da0608 100644 --- a/scripts/docs/finalize-site.mjs +++ b/scripts/docs/finalize-site.mjs @@ -75,15 +75,28 @@ function urlOf(language, page) { * or the language switcher — so the same files are put in here instead. */ function injections(language) { - const read = (name) => { - try { - return readFileSync(join(ROOT, language.root, name), "utf8").trim(); - } catch { - return ""; - } - }; + try { + return { head: readFileSync(join(ROOT, language.root, "head.html"), "utf8").trim() }; + } catch { + return { head: "" }; + } +} - return { head: read("head.html"), body: read("search.html") }; +/** + * The same page in the other languages, as a row of links. + * + * It goes at the top of the article, which puts it directly under the + * breadcrumbs, and it is plain HTML: a reader with no JavaScript, and a crawler + * that runs none, both still see every translation. + */ +function languageRow(language, page, present) { + const links = present.map((other) => + other === language + ? `<span aria-current="true" style="opacity:0.6">${other.name}</span>` + : `<a href="${urlOf(other, page)}" hreflang="${other.code}">${other.name}</a>` + ); + + return `<nav class="asu-languages" aria-label="${language.strings.language}" style="margin:0 0 24px;font-size:13px;line-height:1.6;opacity:0.85">${links.join(' <span aria-hidden="true">·</span> ')}</nav>`; } const pages = pagesOf(root); @@ -147,22 +160,29 @@ for (const page of pages) { `$1${SITE}/images/banner-1280x640.jpg$2` ); - if (injected.body && !html.includes("asu-fab")) { - const body = html.lastIndexOf("</body>"); + // Directly under the breadcrumbs, which sit above the article. + if (!html.includes("asu-languages")) { + const article = html.match(/<article[^>]*>/); - if (body !== -1) { - html = `${html.slice(0, body)}${injected.body}\n${html.slice(body)}`; + if (article) { + const at = html.indexOf(article[0]) + article[0].length; + + html = `${html.slice(0, at)}${languageRow(language, page, present)}${html.slice(at)}`; } } - // Said once at the end of every page, in that page's language. + // Said once at the end of every page, in that page's language. It goes + // inside the article: a paragraph left at the end of `body` is laid out by + // the application's own grid and lands at the top of the page. if (!html.includes("asu-ai-notice")) { - const body = html.lastIndexOf("</body>"); + const notice = `<p class="asu-ai-notice" style="margin:40px 0 0;padding-top:16px;border-top:1px solid rgba(39,40,44,0.16);font-size:13px;line-height:1.5;opacity:0.7">${language.strings.aiNotice}</p>`; + + const article = html.lastIndexOf("</article>"); - if (body !== -1) { - const notice = `<p class="asu-ai-notice" style="margin:32px 0 16px;padding-top:16px;border-top:1px solid rgba(39,40,44,0.16);font-size:13px;line-height:1.5;opacity:0.7;text-align:center">${language.strings.aiNotice}</p>`; + const at = article === -1 ? html.lastIndexOf("</body>") : article; - html = `${html.slice(0, body)}${notice}\n${html.slice(body)}`; + if (at !== -1) { + html = `${html.slice(0, at)}${notice}\n${html.slice(at)}`; } } diff --git a/scripts/docs/generate-topics.mjs b/scripts/docs/generate-topics.mjs index 74c57700..4ef87e86 100644 --- a/scripts/docs/generate-topics.mjs +++ b/scripts/docs/generate-topics.mjs @@ -36,7 +36,7 @@ import prettier from "prettier"; import ts from "typescript"; import { LANGUAGES, SOURCE_LANGUAGE } from "./languages.mjs"; -import { buildProfiles, headHtml, searchHtml, writersideCfg } from "./site.mjs"; +import { buildProfiles, headHtml, writersideCfg } from "./site.mjs"; const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); @@ -975,7 +975,6 @@ for (const language of LANGUAGES) { // 1. Project configuration, the search widget and the structured head. emit(join(root, "buildprofiles.xml"), buildProfiles(language)); emit(join(root, "head.html"), await pretty(headHtml(language), join(root, "head.html"))); - emit(join(root, "search.html"), await pretty(searchHtml(language), join(root, "search.html"))); // The configuration is the same everywhere but for the version, which comes // from the package, so even the English project is written rather than kept. diff --git a/scripts/docs/site.mjs b/scripts/docs/site.mjs index 0c1aeb37..9ae6d3a3 100644 --- a/scripts/docs/site.mjs +++ b/scripts/docs/site.mjs @@ -133,260 +133,3 @@ export function headHtml(language) { * page. Writerside publishes no search of its own, and has no notion of a * translated site, so both live here. */ -export function searchHtml(language) { - const strings = language.strings; - - const roots = LANGUAGES.map((other) => ({ - code: other.code, - name: other.name, - url: other.webRoot, - current: other.code === language.code - })); - - return `<style> - .asu-fab { - position: fixed; - bottom: 24px; - z-index: 2147483000; - display: flex; - gap: 8px; - align-items: center; - padding: 10px 16px; - border: 1px solid rgba(39, 40, 44, 0.16); - border-radius: 999px; - background: #fff; - box-shadow: 0 6px 20px rgba(39, 40, 44, 0.18); - color: #27282c; - font: 500 14px/1.2 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - cursor: pointer; - } - - #asu-search-open { - right: 24px; - } - - #asu-language-open { - right: 160px; - } - - .asu-fab kbd { - padding: 2px 6px; - border: 1px solid rgba(39, 40, 44, 0.2); - border-radius: 4px; - font: 500 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace; - opacity: 0.7; - } - - /* In the header the button is the help application's own, so everything the - floating pair sets has to come back off. */ - .asu-in-header { - position: static; - margin: 0 2px; - bottom: auto; - right: auto; - left: auto; - box-shadow: none; - } - - #asu-language-menu { - position: fixed; - right: 160px; - bottom: 76px; - z-index: 2147483000; - display: none; - min-width: 160px; - padding: 6px; - border: 1px solid rgba(39, 40, 44, 0.16); - border-radius: 10px; - background: #fff; - box-shadow: 0 12px 32px rgba(39, 40, 44, 0.22); - font: 14px/1.4 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - } - - #asu-language-menu[open] { - display: block; - } - - #asu-language-menu a { - display: block; - padding: 8px 10px; - border-radius: 6px; - color: #27282c; - text-decoration: none; - } - - #asu-language-menu a:hover { - background: rgba(48, 127, 255, 0.12); - } - - #asu-language-menu a[aria-current="true"] { - font-weight: 600; - } - - #asu-search-dialog { - position: fixed; - inset: 0; - z-index: 2147483001; - display: none; - padding: 10vh 16px 16px; - background: rgba(19, 20, 22, 0.55); - } - - #asu-search-dialog[open] { - display: block; - } - - #asu-search-panel { - max-width: 680px; - margin: 0 auto; - overflow: hidden; - border-radius: 12px; - background: #fff; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.35); - color: #27282c; - font: 14px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; - } - - #asu-search-input { - width: 100%; - box-sizing: border-box; - padding: 18px 20px; - border: 0; - border-bottom: 1px solid rgba(39, 40, 44, 0.12); - background: transparent; - color: inherit; - font: 16px/1.4 inherit; - outline: none; - } - - #asu-search-results { - max-height: 60vh; - margin: 0; - padding: 8px; - overflow-y: auto; - list-style: none; - } - - #asu-search-results li a { - display: block; - padding: 10px 12px; - border-radius: 8px; - color: inherit; - text-decoration: none; - } - - #asu-search-results li[aria-selected="true"] a, - #asu-search-results li a:hover { - background: rgba(48, 127, 255, 0.12); - } - - #asu-search-results .asu-title { - font-weight: 600; - } - - #asu-search-results .asu-snippet { - margin-top: 2px; - opacity: 0.75; - font-size: 13px; - } - - #asu-search-results mark { - padding: 0 1px; - border-radius: 2px; - background: rgba(255, 214, 0, 0.45); - color: inherit; - } - - #asu-search-status { - padding: 14px 20px; - opacity: 0.7; - } - - @media (max-width: 640px) { - #asu-language-open { - right: 24px; - bottom: 76px; - } - - #asu-language-menu { - right: 24px; - bottom: 128px; - } - } - - @media (prefers-color-scheme: dark) { - .asu-fab, - #asu-language-menu, - #asu-search-panel { - border-color: rgba(255, 255, 255, 0.16); - background: #1e1f22; - color: #dfe1e5; - } - - #asu-language-menu a { - color: #dfe1e5; - } - - #asu-search-input { - border-bottom-color: rgba(255, 255, 255, 0.12); - } - } - - html[data-theme="dark"] .asu-fab, - html[data-theme="dark"] #asu-language-menu, - html[data-theme="dark"] #asu-search-panel { - border-color: rgba(255, 255, 255, 0.16); - background: #1e1f22; - color: #dfe1e5; - } - - html[data-theme="dark"] #asu-language-menu a { - color: #dfe1e5; - } - - @media print { - .asu-fab, - #asu-language-menu, - #asu-search-dialog { - display: none !important; - } - } -</style> - -<button id="asu-language-open" class="asu-fab" type="button" aria-haspopup="true" aria-expanded="false"> - <span aria-hidden="true">🌐</span> ${language.name} -</button> - -<nav id="asu-language-menu" aria-label="${strings.language}"> -${roots - .map( - (root) => - ` <a href="${root.url}/" hreflang="${root.code}"${root.current ? ' aria-current="true"' : ""}>${root.name}</a>` - ) - .join("\n")} -</nav> - -<button id="asu-search-open" class="asu-fab" type="button" aria-label="${strings.searchLabel}"> - <span aria-hidden="true">🔍</span> ${strings.searchLabel} <kbd>/</kbd> -</button> - -<div id="asu-search-dialog" role="dialog" aria-modal="true" aria-label="${strings.searchLabel}"> - <div id="asu-search-panel"> - <input - id="asu-search-input" - type="search" - autocomplete="off" - spellcheck="false" - placeholder="${strings.searchPlaceholder}" - aria-controls="asu-search-results" - /> - <p id="asu-search-status">${strings.searchHint}</p> - <ul id="asu-search-results" role="listbox"></ul> - </div> -</div> - -<script> -${widget(language)} -</script> -`; -}