diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index aedd964a578..0c196c9c8c0 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -41,6 +41,10 @@ jobs: with: fetch-depth: 0 # the git-revision-date plugin reads each page's history + # The design system in bindings/python/docs/brand is vendored, never edited here: it must still match the hash in its VERSION.md. + - name: Check the vendored design system copy + run: bash bindings/python/docs/brand/verify.sh + - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: diff --git a/bindings/python/docs/brand/VERSION.md b/bindings/python/docs/brand/VERSION.md index c5d20ea3660..90368c2dd8b 100644 --- a/bindings/python/docs/brand/VERSION.md +++ b/bindings/python/docs/brand/VERSION.md @@ -3,6 +3,6 @@ Do not edit these files here. Change them in github.com/humemai/design-system, then vendor again. `./verify.sh` checks this copy against the hash below. -Commit: 79b1a8b2318ada932dcdebdf57efb414d35b3119 -Date: 2026-11-06 -Hash: 0071e50a6c4a2714d4b8cf3b3edb75211214b2781bf17468712f4005b9acdf65 +Commit: 19edee040a025ba0551bd48dcb30d8b35046afa1 +Date: 2026-11-11 +Hash: 9d9b6f2c78a75ec59c0cfafc6b7984c258804c97cc8d2c66af98e23b6528af41 diff --git a/bindings/python/docs/brand/css/base.css b/bindings/python/docs/brand/css/base.css new file mode 100644 index 00000000000..1483851fe28 --- /dev/null +++ b/bindings/python/docs/brand/css/base.css @@ -0,0 +1,71 @@ +/* HumemAI base rules every page wants and none should write twice. Opt-in: + load it after tokens.css; nothing here sets a colour, a font or a margin on + an element of its own. The focus ring was written twice (the Humem Tower's + stylesheet and the website's globals.css), and the Tower's skip link and its + motion rule had no counterpart on the site (humemai/design-system#14). The + structure follows Stateloop's base and motion files; the skip link is a + HumemAI original, since Stateloop's design system has none. */ + +/* The focus ring: 2px in --hm-focus, which is 3:1 on every page surface + (scripts/check.py), pulled 2px off the edge so it never covers a border. */ +:focus-visible { + outline: 2px solid var(--hm-focus); + outline-offset: 2px; +} + +/* A page with a sticky header sets --hm-sticky-offset on :root to the header's + height, so a jump to #main or any anchor lands below the header and a focused + control is not scrolled under it (WCAG 2.4.11). */ +html { + scroll-padding-block-start: var(--hm-sticky-offset, 0px); +} + +/* Skip link: the first focusable thing on the page, Skip to content, and the target needs id="main". Hidden + until a keyboard focuses it, so it never shows to a mouse. Focused, it is + fixed to the top left corner with a high z-index, which puts it above a sticky + header (the website's is 40) instead of beneath it, in link ink on the + surface with a focus-colour border. */ +.hm-skip:not(:focus) { + position: absolute; + inline-size: 1px; + block-size: 1px; + padding: 0; + border: 0; + overflow: hidden; + white-space: nowrap; + clip-path: inset(50%); +} + +.hm-skip:focus { + position: fixed; + inset-block-start: var(--hm-space-2); + inset-inline-start: var(--hm-space-2); + z-index: 100; + padding: var(--hm-space-2) var(--hm-space-4); + background: var(--hm-surface); + border: 2px solid var(--hm-focus); + border-radius: var(--hm-radius-sm); + color: var(--hm-link); + font-weight: var(--hm-weight-semibold); + text-decoration: underline; + text-underline-offset: 0.2em; +} + +/* Reduced motion: a reader who asked for less gets no transition and no + animation, whoever wrote them. A duration of 0.01ms and not 0s, so an + animation still ends in its last frame and its animationend still fires. + Hover lifts that a component writes as a transform are zeroed beside the + component (.hm-card, in components.css). */ +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0s !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + transition-delay: 0s !important; + scroll-behavior: auto !important; + } +} diff --git a/bindings/python/docs/brand/css/components.css b/bindings/python/docs/brand/css/components.css index 5a8d6b3c857..bcbd570fe77 100644 --- a/bindings/python/docs/brand/css/components.css +++ b/bindings/python/docs/brand/css/components.css @@ -49,4 +49,230 @@ .hm-card { transition: none; } + + .hm-card:hover, + .hm-card:focus-visible { + transform: none; + } +} + +/* Hint: a term's short explanation. A small ? beside the word, opened by hover + with a mouse and by click, tap or Enter everywhere (humemai/design-system #12, + mirrored from Stateloop's .stl-hint of 2026-11-06). A native title="" tooltip + never shows on a touch screen and a keyboard cannot reach it. The button is a + circle, the one round shape here: a help mark is an icon people recognise by + that shape, not a container. It is --hm-tap-min (24px, the WCAG 2.5.8 floor) + on both sides, and its minimum sizes are set too, because an app's own + button { min-height } would otherwise stretch the circle into an oval. At + 999px and narrower it keeps its 24px mark and grows a 44px hit area (the + ::after), not its layout. The border is --hm-border-control, 3:1 on every page + surface; hover, focus and the open state turn the border and the mark to the + brand colour, and a keyboard gets the focus ring. The bubble is a popover in + the top layer, so a scrolling table or a sheet cannot clip it; js/hint.js + places it under its button (above it near the bottom) and keeps it on screen, + and it stays hidden until placed so it never flashes in a corner. The hint + sits beside its term, often inside an all-caps, letter-spaced column head, so + the button and the bubble reset every inherited text style: the definition + must read as body text. Only for a word a person cannot act without knowing; + anything needed to act stays visible. */ +.hm-hint { + box-sizing: border-box; + display: inline-grid; + place-items: center; + inline-size: var(--hm-tap-min); + block-size: var(--hm-tap-min); + min-inline-size: var(--hm-tap-min); + min-block-size: var(--hm-tap-min); + margin-inline-start: var(--hm-space-1); + padding: 0; + border: 1px solid var(--hm-border-control); + border-radius: var(--hm-radius-pill); + background: transparent; + color: var(--hm-text-muted); + font-family: var(--hm-font-text); + font-size: var(--hm-text-xs); + font-style: normal; + font-weight: var(--hm-weight-semibold); + line-height: 1; + letter-spacing: normal; + text-transform: none; + vertical-align: middle; + cursor: help; +} + +.hm-hint:hover, +.hm-hint:focus-visible, +.hm-hint[aria-expanded="true"] { + border-color: var(--hm-brand); + color: var(--hm-brand); +} + +.hm-hint:focus-visible { + outline: 2px solid var(--hm-focus); + outline-offset: 2px; +} + +.hm-hint__bubble { + box-sizing: border-box; + inset: auto; + margin: 0; + max-inline-size: min(calc(var(--hm-measure) / 2), calc(100vw - 2 * var(--hm-space-4))); + padding: var(--hm-space-2) var(--hm-space-3); + border: 1px solid var(--hm-border-strong); + border-radius: var(--hm-radius-md); + background: var(--hm-surface-raised); + color: var(--hm-text); + font-family: var(--hm-font-text); + font-size: var(--hm-text-sm); + font-style: normal; + font-weight: var(--hm-weight-regular); + line-height: 1.45; + text-align: start; + text-indent: 0; + text-transform: none; + letter-spacing: normal; + word-spacing: normal; + white-space: normal; + box-shadow: var(--hm-shadow-md); +} + +.hm-hint__bubble:not([data-placed]) { + visibility: hidden; +} + +.hm-hint__bubble a { + color: var(--hm-link); + text-decoration: underline; + text-underline-offset: 0.15em; +} + +.hm-hint__bubble a:hover { + color: var(--hm-link-hover); +} + +@media (max-width: 999px) { + .hm-hint { + position: relative; + } + + .hm-hint::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: var(--hm-tap-comfort); + height: var(--hm-tap-comfort); + transform: translate(-50%, -50%); + } +} + +/* Table: a data table inside a scroller that a keyboard can reach. Mirrored + from the Humem Tower's tables (humemai/design-system#14); Stateloop's .stl-table + has no scroller, but its rule holds here too: a scroller has a name and a way + in from the keyboard. Wrap the table in .hm-table-scroll and give the wrapper + role="region", tabindex="0" and an aria-label naming the table; without them + a keyboard cannot scroll a wide table (the benchmark scroller on humem.ai + lacked all three). Only the wrapper scrolls, so the page never does. The + table is border-collapse: separate with no spacing, not collapse, because a + collapsed table drops the borders of a sticky header cell when it scrolls. + Text is --hm-text-sm (14px), the system's table size. Column widths are the + page's own: the Tower sets them by column role, the website by layout, and + neither belongs here. */ +.hm-table-scroll { + margin: 0 0 var(--hm-space-4); + overflow-x: auto; + background: var(--hm-surface); + border: 1px solid var(--hm-border); + border-radius: var(--hm-radius-md); +} + +.hm-table { + inline-size: 100%; + margin: 0; + border-collapse: separate; + border-spacing: 0; + font-size: var(--hm-text-sm); + line-height: 1.4; +} + +.hm-table th, +.hm-table td { + padding: var(--hm-space-1) var(--hm-space-2); + border-bottom: 1px solid var(--hm-border); + text-align: left; + vertical-align: top; + overflow-wrap: break-word; +} + +.hm-table th { + background: var(--hm-surface-sunken); + border-bottom: 2px solid var(--hm-border-strong); + font-weight: var(--hm-weight-semibold); +} + +.hm-table tbody tr:last-child td { + border-bottom: 0; +} + +/* Optional: the header row stays in view while the table scrolls down. It + sticks to the wrapper, so the wrapper needs a height of its own, for example + max-block-size: 70vh; a wrapper as tall as its table has nothing to scroll. */ +.hm-table--sticky-head thead th { + position: sticky; + inset-block-start: 0; + z-index: 1; +} + +/* A numeric column: against the right edge, so units and decimal points line + up, and in DM Mono. Schibsted Grotesk's default figures are proportional (the + Tower measured "1" at 5.8px and "0" at 10.4px at 17px), and its tabular + figures also widen the punctuation ("1 . 6 h"), so no tabular-nums here: the + cell takes the mono face. The header cell aligns right and keeps its own face. */ +.hm-table .hm-num { + text-align: right; + white-space: nowrap; +} + +.hm-table td.hm-num { + font-family: var(--hm-font-mono); +} + +/* Badge: a state or a label as a pill. The word is always there; the fill and a + symbol are a second cue, never colour alone. Three variants, because the brand + has one voice and one warning: brand (filled, the active or running thing), + danger (the failed or overdue thing), tint (the held, paused or stale thing). + With no variant it is a quiet outline. The pairings (text on its own fill, + border on the page) are in scripts/check.py. A symbol is the consumer's: put it + in a ::before with an empty alt, content: "\25B6\FE0E" / "", so a screen reader + reads the word, not the shape (the Tower maps its nine states this way). */ +.hm-badge { + display: inline-flex; + align-items: center; + gap: var(--hm-space-1); + padding: 0 var(--hm-space-2); + background: var(--hm-surface); + border: 1px solid var(--hm-border-control); + border-radius: var(--hm-radius-pill); + color: var(--hm-text-muted); + font-size: var(--hm-text-sm); + font-weight: var(--hm-weight-semibold); + line-height: 1.5; + white-space: nowrap; +} + +.hm-badge--brand { + background: var(--hm-brand); + border-color: var(--hm-brand); + color: var(--hm-on-brand); +} + +.hm-badge--danger { + border-color: var(--hm-danger); + color: var(--hm-danger); +} + +.hm-badge--tint { + background: var(--hm-tint); + border-color: var(--hm-tint-text); + color: var(--hm-tint-text); } diff --git a/bindings/python/docs/brand/css/tokens.css b/bindings/python/docs/brand/css/tokens.css index 12fa32c43bb..1b84fe63b44 100644 --- a/bindings/python/docs/brand/css/tokens.css +++ b/bindings/python/docs/brand/css/tokens.css @@ -93,6 +93,15 @@ --hm-space-20: 5rem; --hm-space-24: 6rem; + /* Tap targets. 24px is the WCAG 2.5.8 floor, for a mouse; at 999px and + narrower a control is 44px (the Behaviour section of the README). */ + --hm-tap-min: 1.5rem; /* 24 */ + --hm-tap-comfort: 2.75rem; /* 44 */ + /* The height of a button, input or select: --hm-tap-comfort (44) at 999px and + narrower, 36 from 1000px (the media block below), where a pointer is enough. + A control sets min-height: var(--hm-control-h) and pads the rest. */ + --hm-control-h: var(--hm-tap-comfort); + --hm-radius-sm: 0.375rem; --hm-radius-md: 0.625rem; --hm-radius-lg: 1rem; @@ -104,6 +113,7 @@ --hm-surface: var(--hm-neutral-0); --hm-surface-muted: var(--hm-neutral-50); --hm-surface-sunken: var(--hm-neutral-100); + --hm-surface-raised: var(--hm-neutral-0); /* what floats above the page: a hint bubble; the border and shadow lift it in light */ --hm-text: var(--hm-neutral-900); --hm-text-muted: var(--hm-neutral-600); --hm-text-subtle: var(--hm-neutral-500); /* on --hm-bg and --hm-surface only */ @@ -119,11 +129,13 @@ --hm-tint-text: var(--hm-oxblood-700); --hm-focus: var(--hm-oxblood-600); --hm-danger: var(--hm-danger-light); + --hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-900) 16%, transparent); } @media (min-width: 1000px) { :root { --hm-text-base: 1.0625rem; /* 17 */ + --hm-control-h: 2.25rem; /* 36 */ } } @@ -136,6 +148,7 @@ --hm-surface: var(--hm-neutral-900); --hm-surface-muted: var(--hm-neutral-900); --hm-surface-sunken: var(--hm-neutral-950); + --hm-surface-raised: var(--hm-neutral-800); --hm-text: var(--hm-neutral-50); --hm-text-muted: var(--hm-neutral-400); --hm-text-subtle: var(--hm-neutral-450); @@ -151,6 +164,7 @@ --hm-tint-text: var(--hm-oxblood-300); --hm-focus: var(--hm-oxblood-400); --hm-danger: var(--hm-danger-dark); + --hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-950) 70%, transparent); } } @@ -160,6 +174,7 @@ --hm-surface: var(--hm-neutral-900); --hm-surface-muted: var(--hm-neutral-900); --hm-surface-sunken: var(--hm-neutral-950); + --hm-surface-raised: var(--hm-neutral-800); --hm-text: var(--hm-neutral-50); --hm-text-muted: var(--hm-neutral-400); --hm-text-subtle: var(--hm-neutral-450); @@ -175,4 +190,5 @@ --hm-tint-text: var(--hm-oxblood-300); --hm-focus: var(--hm-oxblood-400); --hm-danger: var(--hm-danger-dark); + --hm-shadow-md: 0 3px 10px color-mix(in srgb, var(--hm-neutral-950) 70%, transparent); } diff --git a/bindings/python/docs/brand/js/hint.js b/bindings/python/docs/brand/js/hint.js new file mode 100644 index 00000000000..29b13b577bd --- /dev/null +++ b/bindings/python/docs/brand/js/hint.js @@ -0,0 +1,182 @@ +/** + * Explain a term in place: `.hm-hint` buttons open their `.hm-hint__bubble` + * popover (humemai/design-system #12; mirrored from Stateloop's hint.js). + * + * + * One sentence. + * + * The browser's Popover API does the opening and the dismissing: a click, tap + * or Enter shows it, and a tap outside or Esc closes it. This adds what it + * lacks: placing the bubble by its button and keeping it on screen, following + * it while the page or a panel scrolls, and hover with a mouse (it opens on + * enter and closes on leave, unless it was clicked, which pins it). The + * listeners sit on the document, so hints rendered later -- by an app that + * re-renders with innerHTML -- work with no per-render wiring. Call it once. + * + * @param {Document} [doc] + * @returns {() => void} cleanup + */ +const GAP = 6; +const EDGE = 8; +let bound = null; + +export function bindHints(doc = document) { + if (bound) return bound; + const win = doc.defaultView; + let open = null; + + const bubbleOf = (trigger) => doc.getElementById(trigger.getAttribute("popovertarget") || ""); + const triggerOf = (bubble) => + Array.from(doc.querySelectorAll(".hm-hint")).find((t) => t.getAttribute("popovertarget") === bubble.id); + const hintAt = (target) => (target && target.closest ? target.closest(".hm-hint") : null); + const isBubble = (el) => el && el.classList && el.classList.contains("hm-hint__bubble"); + const isOpen = (bubble) => bubble.matches(":popover-open"); + + function place(bubble) { + const trigger = triggerOf(bubble); + if (!trigger) return; + const t = trigger.getBoundingClientRect(); + const b = bubble.getBoundingClientRect(); + const vw = doc.documentElement.clientWidth; + const vh = win.innerHeight; + const left = Math.max(EDGE, Math.min(t.left + t.width / 2 - b.width / 2, vw - b.width - EDGE)); + let top = t.bottom + GAP; + if (top + b.height > vh - EDGE && t.top - GAP - b.height >= EDGE) top = t.top - GAP - b.height; + bubble.style.left = `${Math.round(left)}px`; + bubble.style.top = `${Math.round(top)}px`; + bubble.dataset.placed = ""; + } + + // `toggle` does not bubble, but a capturing listener on the document sees it. + function onToggle(e) { + const bubble = e.target; + if (!isBubble(bubble)) return; + const trigger = triggerOf(bubble); + if (e.newState === "open") { + open = bubble; + place(bubble); + if (trigger) trigger.setAttribute("aria-expanded", "true"); + } else { + if (open === bubble) open = null; + delete bubble.dataset.placed; + delete bubble.dataset.pinned; + if (trigger) trigger.setAttribute("aria-expanded", "false"); + } + } + + // A click pins it: leaving with the mouse no longer closes it. + function onClick(e) { + const trigger = hintAt(e.target); + const bubble = trigger && bubbleOf(trigger); + if (bubble) bubble.dataset.pinned = ""; + } + + // Hover is for a mouse only: a touch screen has no hover, and its tap is the click. + function onOver(e) { + if (e.pointerType !== "mouse") return; + const trigger = hintAt(e.target); + const bubble = trigger && bubbleOf(trigger); + if (bubble && !isOpen(bubble)) bubble.showPopover(); + } + + function onOut(e) { + if (e.pointerType !== "mouse") return; + const trigger = hintAt(e.target); + if (!trigger || trigger.contains(e.relatedTarget)) return; + const bubble = bubbleOf(trigger); + if (bubble && isOpen(bubble) && !("pinned" in bubble.dataset)) bubble.hidePopover(); + } + + function follow() { + if (open) place(open); + } + + doc.addEventListener("toggle", onToggle, true); + doc.addEventListener("click", onClick, true); + doc.addEventListener("pointerover", onOver, true); + doc.addEventListener("pointerout", onOut, true); + win.addEventListener("scroll", follow, true); + win.addEventListener("resize", follow); + + bound = () => { + doc.removeEventListener("toggle", onToggle, true); + doc.removeEventListener("click", onClick, true); + doc.removeEventListener("pointerover", onOver, true); + doc.removeEventListener("pointerout", onOut, true); + win.removeEventListener("scroll", follow, true); + win.removeEventListener("resize", follow); + bound = null; + }; + return bound; +} + +/** + * The "?" and its bubble for a glossary term, so no app keeps a private copy. + * + * const GLOSSARY = { fit: { term: "the fit score", text: "How well they match, 1 to 5.", learnMore: "https://example.com/fit" } }; + * header.innerHTML = `Fit ${termHint(GLOSSARY, "fit", "the fit score")}`; // an app that renders with innerHTML + * header.append("Fit ", termHintElement(GLOSSARY, "fit", "the fit score")); // an app with an h() helper + * + * Both give the markup in the header of this file: `button.hm-hint` (`aria-label` "What is ?") and `span.hm-hint__bubble`, + * with a fresh id (`hm-hint-1`, `hm-hint-2`, ...) from one counter in this module. A key the glossary lacks throws an Error that + * names it: a silent empty string hides a typo and drops the explanation. `learnMore` (an http(s) or site-relative URL) adds a last + * "Learn more" line to the bubble. `term` defaults to the entry's own `term`. + */ +let hintSeq = 0; + +function entryFor(glossary, key) { + const entry = glossary && Object.prototype.hasOwnProperty.call(glossary, key) ? glossary[key] : undefined; + if (!entry || typeof entry.text !== "string" || !entry.text) { + throw new Error(`termHint: no glossary entry for key "${key}"`); + } + if (entry.learnMore != null && !/^(https?:\/\/|\/(?!\/))/.test(entry.learnMore)) { + throw new Error(`termHint: learnMore of "${key}" must be an http(s) or site-relative URL`); + } + return entry; +} + +function esc(s) { + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); +} + +const MORE = "Learn more"; + +export function termHint(glossary, key, term) { + const entry = entryFor(glossary, key); + const id = `hm-hint-${++hintSeq}`; + const label = esc(`What is ${term ?? entry.term ?? key}?`); + const more = entry.learnMore ? `
${MORE}` : ""; + return `` + + `${esc(entry.text)}${more}`; +} + +export function termHintElement(glossary, key, term, doc = globalThis.document) { + const entry = entryFor(glossary, key); + const id = `hm-hint-${++hintSeq}`; + const button = doc.createElement("button"); + button.type = "button"; + button.className = "hm-hint"; + button.setAttribute("popovertarget", id); + button.setAttribute("popovertargetaction", "show"); + button.setAttribute("aria-label", `What is ${term ?? entry.term ?? key}?`); + button.setAttribute("aria-describedby", id); + button.textContent = "?"; + const bubble = doc.createElement("span"); + bubble.id = id; + bubble.className = "hm-hint__bubble"; + bubble.setAttribute("popover", ""); + bubble.setAttribute("role", "tooltip"); + bubble.append(entry.text); + if (entry.learnMore) { + const a = doc.createElement("a"); + a.href = entry.learnMore; + a.target = "_blank"; + a.rel = "noopener"; + a.textContent = MORE; + bubble.append(doc.createElement("br"), a); + } + const frag = doc.createDocumentFragment(); + frag.append(button, bubble); + return frag; +} diff --git a/bindings/python/docs/brand/verify.sh b/bindings/python/docs/brand/verify.sh index bffb1eb6323..9c56c4eec7a 100755 --- a/bindings/python/docs/brand/verify.sh +++ b/bindings/python/docs/brand/verify.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "$0")" claimed=$(sed -n 's/^Hash:[[:space:]]*//p' VERSION.md) -actual=$(find css logo assets export -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | awk '{print $1}') +actual=$(find css js logo assets export -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | awk '{print $1}') if [ "$claimed" != "$actual" ]; then echo "✗ brand files differ from VERSION.md ($claimed vs $actual)." echo " Change them in humemai/design-system and vendor again; don't edit the copy." diff --git a/bindings/python/docs/development/ci-setup.md b/bindings/python/docs/development/ci-setup.md index ab66d9cd16f..ec5533ed53e 100644 --- a/bindings/python/docs/development/ci-setup.md +++ b/bindings/python/docs/development/ci-setup.md @@ -88,7 +88,7 @@ A newer push to a pull request cancels the older run of this workflow; runs on ` ### `docs-build.yml` -Named `Docs Build`. Runs `mkdocs build --strict` (Python 3.12, pinned `mkdocs`, `mkdocs-material` and plugin versions, run with `uvx`; `deploy-python-docs.yml` runs `mike` with the same versions) on pull requests and pushes to `main` that change `bindings/python/docs/**`, `bindings/python/mkdocs.yml` or the workflow, and on manual dispatch. It does not run the code blocks in the pages. See [Documentation](documentation.md#build-documentation). +Named `Docs Build`. First checks that the vendored design system in `bindings/python/docs/brand/` still matches the hash in its `VERSION.md` (`brand/verify.sh`), so a hand edit of the copy fails. Then runs `mkdocs build --strict` (Python 3.12, pinned `mkdocs`, `mkdocs-material` and plugin versions, run with `uvx`; `deploy-python-docs.yml` runs `mike` with the same versions) on pull requests and pushes to `main` that change `bindings/python/docs/**`, `bindings/python/mkdocs.yml` or the workflow, and on manual dispatch. It does not run the code blocks in the pages. See [Documentation](documentation.md#build-documentation). ### `build-engine-jars.yml` diff --git a/bindings/python/docs/development/documentation.md b/bindings/python/docs/development/documentation.md index e20bbfd17d5..ffc2cc26eb1 100644 --- a/bindings/python/docs/development/documentation.md +++ b/bindings/python/docs/development/documentation.md @@ -322,7 +322,7 @@ The Material theme is mobile-responsive by default. Test by: ## Continuous Integration -The `Docs Build` workflow (`docs-build.yml`) runs `mkdocs build --strict` on every pull request +The `Docs Build` workflow (`docs-build.yml`) first runs `brand/verify.sh`, which fails when the vendored design system in `docs/brand/` was edited by hand, then runs `mkdocs build --strict` on every pull request and every push to `main` that changes `bindings/python/docs/**`, `bindings/python/mkdocs.yml` or the workflow. `deploy-python-docs.yml` runs only on a version tag or a manual dispatch, and it runs `mike deploy`, not a strict build. Run the strict build locally before you push a docs