diff --git a/.github/workflows/docs-theme.yml b/.github/workflows/docs-theme.yml
new file mode 100644
index 0000000..711ae9e
--- /dev/null
+++ b/.github/workflows/docs-theme.yml
@@ -0,0 +1,119 @@
+name: Documentation theme
+
+on:
+ push:
+ branches: [main]
+ paths: ['index.html', 'docs-theme/**', 'tests/**', '.github/workflows/docs-theme.yml']
+ pull_request:
+ paths: ['index.html', 'docs-theme/**', 'tests/**', '.github/workflows/docs-theme.yml']
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: docs-theme-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ compatibility:
+ name: ${{ matrix.project }} (${{ matrix.sha }})
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+ strategy:
+ fail-fast: false
+ max-parallel: 2
+ matrix:
+ include:
+ - project: pypto
+ sha: 812628941a4ad118321e097ced984e9ec383203e
+ python: '3.11'
+ page: api/tile/index.html
+ - project: simpler
+ sha: 405b5bbda30f6d0f6167e4b2f283df2d42ca3443
+ python: '3.10'
+ page: user/reference/api/worker/index.html
+ - project: pypto-lib
+ sha: 216b497e4b7d8bb7b47e304e79fd59e01de2a2e9
+ python: '3.10'
+ page: get-started/first-kernel/index.html
+ - project: pypto-serving
+ sha: 967c839c22402cb3f23c7430129ce539a0accbdf
+ python: '3.11'
+ page: user-guide/online-serving/index.html
+ - project: pypto-tools
+ sha: 955cfcfc01c7028f0e9bef01b3fdabc04e90de9e
+ python: '3.11'
+ page: runtime/chip-swimlane/index.html
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ path: theme
+ - name: Check out fixed downstream snapshot
+ uses: actions/checkout@v4
+ with:
+ repository: hw-native-sys/${{ matrix.project }}
+ ref: ${{ matrix.sha }}
+ path: downstream
+ persist-credentials: false
+ - uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python }}
+ - name: Install shared renderer
+ run: python -m pip install -r theme/docs-theme/constraints.txt
+ - name: Prepare candidate theme configuration
+ run: >-
+ python theme/tests/docs_theme_compat.py prepare
+ --repo downstream --project '${{ matrix.project }}'
+ --expected-sha '${{ matrix.sha }}'
+ - name: Install project documentation plugins
+ # Toolkit's temporary requirements test the 9.7.7 candidate explicitly;
+ # no upstream dependency file or source config is changed.
+ run: >-
+ python -m pip install -c theme/docs-theme/constraints.txt
+ -r downstream/requirements-theme-compat.txt
+ - name: Record tested source and toolchain
+ run: |
+ git -C downstream rev-parse HEAD
+ python --version
+ python -m pip freeze
+ python -m pip check
+ - name: Run existing documentation checks and strict build
+ working-directory: downstream
+ env:
+ DOCS_REF: ${{ matrix.sha }}
+ run: |
+ if [ -f .claude/skills/testing/load-env.sh ]; then
+ source .claude/skills/testing/load-env.sh
+ fi
+ for check in check_docs_nav check_public_docs check_docs_en_zh_parity check_docs_symbol_coverage check_op_docstring_parity; do
+ if [ -f "tests/lint/$check.py" ]; then
+ python "tests/lint/$check.py"
+ fi
+ done
+ if [ -f tests/docs/test_repo_links.py ]; then
+ python -m unittest discover -s tests/docs -v
+ fi
+ mkdocs build --strict -f mkdocs.theme-compat.yml --site-dir site-theme
+ - name: Validate rendered branding, links, and assets
+ run: >-
+ python theme/tests/docs_theme_compat.py check
+ --site downstream/site-theme --project '${{ matrix.project }}'
+ --page '${{ matrix.page }}'
+ - name: Upload review preview
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: docs-theme-${{ matrix.project }}-${{ matrix.sha }}
+ path: downstream/site-theme
+ retention-days: 7
+ - name: Install browser checks
+ if: matrix.project == 'pypto'
+ run: |
+ python -m pip install -r theme/tests/requirements.txt
+ python -m playwright install --with-deps chromium
+ - name: Check homepage and bilingual PyPTO in Chromium
+ if: matrix.project == 'pypto'
+ env:
+ PYPTO_DOCS_SITE: ${{ github.workspace }}/downstream/site-theme
+ run: python -m pytest theme/tests/test_browser_theme.py -q
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6c56ff1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+__pycache__/
+.pytest_cache/
diff --git a/docs-theme/base.yml b/docs-theme/base.yml
new file mode 100644
index 0000000..9283f3a
--- /dev/null
+++ b/docs-theme/base.yml
@@ -0,0 +1,51 @@
+# Paths to the overrides directory belong in the consuming mkdocs.yml.
+theme:
+ name: material
+ font: false
+ logo: assets/images/pypto.svg
+ favicon: assets/images/pypto.svg
+ features:
+ - navigation.tabs
+ - navigation.sections
+ - navigation.indexes
+ - navigation.top
+ - navigation.footer
+ - content.code.copy
+ - content.action.edit
+ - search.suggest
+ - search.highlight
+ - toc.follow
+ # A mapping avoids Material's per-project palette selector. The shared
+ # selector supports system/light/dark with one preference across projects.
+ palette:
+ scheme: default
+ primary: custom
+ accent: custom
+
+extra_css:
+ - assets/stylesheets/brand.css
+ - assets/stylesheets/docs.css
+
+extra:
+ homepage: https://www.pypto.ai/
+ pypto_projects:
+ - id: pypto
+ name: PyPTO
+ url: https://www.pypto.ai/pypto/
+ languages: [en, zh]
+ - id: simpler
+ name: Simpler
+ url: https://www.pypto.ai/simpler/
+ languages: [en]
+ - id: pypto-lib
+ name: PyPTO Lib
+ url: https://www.pypto.ai/pypto-lib/
+ languages: [en]
+ - id: pypto-serving
+ name: PyPTO Serving
+ url: https://www.pypto.ai/pypto-serving/
+ languages: [en]
+ - id: pypto-tools
+ name: PyPTO Toolkit
+ url: https://www.pypto.ai/pypto-tools/
+ languages: [en, zh]
diff --git a/docs-theme/constraints.txt b/docs-theme/constraints.txt
new file mode 100644
index 0000000..86bb35b
--- /dev/null
+++ b/docs-theme/constraints.txt
@@ -0,0 +1,3 @@
+# Shared renderer versions. Project-specific plugins remain in each repository.
+mkdocs==1.6.1
+mkdocs-material==9.7.7
diff --git a/docs-theme/overrides/assets/images/pypto.svg b/docs-theme/overrides/assets/images/pypto.svg
new file mode 100644
index 0000000..fb1d53c
--- /dev/null
+++ b/docs-theme/overrides/assets/images/pypto.svg
@@ -0,0 +1,9 @@
+
diff --git a/docs-theme/overrides/assets/javascripts/preferences.js b/docs-theme/overrides/assets/javascripts/preferences.js
new file mode 100644
index 0000000..fee9187
--- /dev/null
+++ b/docs-theme/overrides/assets/javascripts/preferences.js
@@ -0,0 +1,96 @@
+/* One preference for the homepage, project roots, and translated pages. */
+(() => {
+ "use strict";
+ if (window.PyPTOTheme) return;
+
+ const key = "pypto:color-scheme";
+ const modes = new Set(["system", "light", "dark"]);
+ const system = window.matchMedia("(prefers-color-scheme: dark)");
+ function readMode(fallback = "system") {
+ try {
+ const saved = window.localStorage.getItem(key);
+ return modes.has(saved) ? saved : "system";
+ } catch {
+ // Storage may be unavailable in private or embedded browser contexts.
+ return fallback;
+ }
+ }
+ let mode = readMode();
+
+ function apply() {
+ const dark = mode === "dark" || (mode === "system" && system.matches);
+ document.documentElement.dataset.pyptoTheme = dark ? "dark" : "light";
+ document.documentElement.classList.remove("no-js");
+ if (document.body) {
+ document.body.dataset.mdColorScheme = dark ? "slate" : "default";
+ }
+ document.querySelectorAll("[data-pypto-theme-select]").forEach(select => {
+ select.value = mode;
+ });
+ }
+
+ function closeProjects(restoreFocus = false) {
+ document.querySelectorAll(".pypto-project-switcher[open]").forEach(menu => {
+ menu.open = false;
+ if (restoreFocus) menu.querySelector("summary").focus();
+ });
+ }
+
+ function updatePage() {
+ apply();
+ closeProjects();
+ const search = document.querySelector("[data-md-component='search-query']");
+ const project = document.querySelector("[data-pypto-project-name]");
+ if (search && project) {
+ const name = project.dataset.pyptoProjectName;
+ const label = document.documentElement.lang.startsWith("zh") ? `搜索 ${name}` : `Search ${name}`;
+ search.placeholder = label;
+ search.setAttribute("aria-label", label);
+ }
+ }
+
+ let connected = false;
+ window.PyPTOTheme = {
+ apply,
+ connectMaterial() {
+ if (!connected && window.document$) {
+ connected = true;
+ window.document$.subscribe(updatePage);
+ }
+ updatePage();
+ }
+ };
+
+ apply();
+ document.addEventListener("DOMContentLoaded", updatePage);
+ document.addEventListener("change", event => {
+ if (!event.target.matches("[data-pypto-theme-select]")) return;
+ const selected = event.target.value;
+ if (!modes.has(selected)) return;
+ mode = selected;
+ try {
+ window.localStorage.setItem(key, mode);
+ } catch {
+ // Keep the selection for this page even when it cannot be persisted.
+ }
+ apply();
+ });
+ document.addEventListener("click", event => {
+ if (!event.target.closest(".pypto-project-switcher")) closeProjects();
+ });
+ document.addEventListener("keydown", event => {
+ if (event.key === "Escape") closeProjects(true);
+ });
+ window.addEventListener("storage", event => {
+ if (event.key !== key && event.key !== null) return;
+ mode = modes.has(event.newValue) ? event.newValue : "system";
+ apply();
+ });
+ window.addEventListener("pageshow", () => {
+ mode = readMode(mode);
+ apply();
+ });
+ system.addEventListener("change", () => {
+ if (mode === "system") apply();
+ });
+})();
diff --git a/docs-theme/overrides/assets/stylesheets/brand.css b/docs-theme/overrides/assets/stylesheets/brand.css
new file mode 100644
index 0000000..9318f0d
--- /dev/null
+++ b/docs-theme/overrides/assets/stylesheets/brand.css
@@ -0,0 +1,78 @@
+/* Shared by the organization homepage and every documentation build. */
+:root {
+ color-scheme: light;
+ --pypto-background: #f7f8fc;
+ --pypto-surface: #ffffff;
+ --pypto-text: #192039;
+ --pypto-muted: #59627a;
+ --pypto-line: #dfe3ee;
+ --pypto-accent: #4352c7;
+ --pypto-featured: #f0f2ff;
+ --pypto-shadow: 0 12px 32px rgb(25 32 57 / 6%);
+ --pypto-font: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ --pypto-code-font: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", monospace;
+}
+
+:root[data-pypto-theme="dark"] {
+ color-scheme: dark;
+ --pypto-background: #10121b;
+ --pypto-surface: #191d2b;
+ --pypto-text: #edf0ff;
+ --pypto-muted: #aab3cc;
+ --pypto-line: #343b53;
+ --pypto-accent: #a8b2ff;
+ --pypto-featured: #20263e;
+ --pypto-shadow: 0 12px 32px rgb(0 0 0 / 18%);
+}
+
+@media (prefers-color-scheme: dark) {
+ :root:not([data-pypto-theme]) {
+ color-scheme: dark;
+ --pypto-background: #10121b;
+ --pypto-surface: #191d2b;
+ --pypto-text: #edf0ff;
+ --pypto-muted: #aab3cc;
+ --pypto-line: #343b53;
+ --pypto-accent: #a8b2ff;
+ --pypto-featured: #20263e;
+ --pypto-shadow: 0 12px 32px rgb(0 0 0 / 18%);
+ }
+}
+
+.pypto-theme-control {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ color: var(--pypto-muted);
+ font: 14px var(--pypto-font);
+}
+
+.pypto-theme-control select {
+ min-height: 40px;
+ max-width: 100%;
+ padding: 6px 8px;
+ color: var(--pypto-text);
+ background: var(--pypto-surface);
+ border: 1px solid var(--pypto-line);
+ border-radius: 8px;
+ font: inherit;
+ cursor: pointer;
+}
+
+.pypto-theme-control select:focus-visible {
+ outline: 3px solid var(--pypto-accent);
+ outline-offset: 3px;
+}
+
+.pypto-visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ border: 0;
+}
+
+.no-js .pypto-theme-control { display: none; }
diff --git a/docs-theme/overrides/assets/stylesheets/docs.css b/docs-theme/overrides/assets/stylesheets/docs.css
new file mode 100644
index 0000000..6c96bff
--- /dev/null
+++ b/docs-theme/overrides/assets/stylesheets/docs.css
@@ -0,0 +1,114 @@
+/* Material keeps its layout and behavior; these rules supply PyPTO's visuals. */
+:root {
+ --md-text-font: var(--pypto-font);
+ --md-code-font: var(--pypto-code-font);
+}
+
+body[data-md-color-scheme] {
+ --md-default-bg-color: var(--pypto-background);
+ --md-default-fg-color: var(--pypto-text);
+ --md-default-fg-color--light: var(--pypto-muted);
+ --md-default-fg-color--lighter: var(--pypto-line);
+ --md-primary-fg-color: var(--pypto-accent);
+ --md-primary-bg-color: var(--pypto-background);
+ --md-primary-bg-color--light: var(--pypto-muted);
+ --md-accent-fg-color: var(--pypto-accent);
+ --md-accent-fg-color--transparent: var(--pypto-featured);
+ --md-typeset-a-color: var(--pypto-accent);
+ --md-typeset-color: var(--pypto-text);
+ --md-admonition-fg-color: var(--pypto-text);
+ --md-code-bg-color: var(--pypto-surface);
+ --md-code-fg-color: var(--pypto-text);
+ --md-footer-bg-color: var(--pypto-surface);
+ --md-footer-bg-color--dark: var(--pypto-surface);
+ --md-footer-fg-color: var(--pypto-text);
+ --md-footer-fg-color--light: var(--pypto-muted);
+ --md-footer-fg-color--lighter: var(--pypto-muted);
+ --md-typeset-table-color: var(--pypto-line);
+ --md-typeset-table-color--light: var(--pypto-featured);
+ background: var(--pypto-background);
+}
+
+.md-grid { max-width: 72rem; }
+.md-header { color: var(--pypto-text); background: var(--pypto-surface); border-bottom: 1px solid var(--pypto-line); box-shadow: none; }
+.pypto-header .md-header__inner { min-height: 3.6rem; gap: .6rem; padding: .55rem .8rem; }
+.pypto-brand { display: inline-flex; flex-shrink: 0; align-items: center; gap: .5rem; font-size: 1.05rem; font-weight: 750; letter-spacing: -.04em; }
+.pypto-brand img { width: 1.8rem; height: 1.8rem; }
+.pypto-header-spacer { flex: 1; }
+.pypto-header .md-header__button { margin: 0; }
+.pypto-header .md-select__inner { inset-inline: auto 0; }
+.pypto-source { display: inline-flex; align-items: center; justify-content: center; min-width: 2rem; min-height: 2rem; }
+.pypto-source svg { width: 1rem; height: 1rem; }
+.pypto-project-switcher { position: relative; min-width: 0; color: var(--pypto-text); }
+.pypto-project-switcher summary { display: flex; align-items: center; gap: .5rem; min-height: 2rem; padding: .35rem .6rem; border: 1px solid var(--pypto-line); border-radius: .4rem; cursor: pointer; list-style: none; font-size: .75rem; font-weight: 600; }
+.pypto-project-switcher summary::-webkit-details-marker { display: none; }
+.pypto-project-switcher summary > span:first-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.pypto-project-switcher summary > svg { flex-shrink: 0; }
+.pypto-project-menu { position: absolute; top: calc(100% + .55rem); left: 0; z-index: 10; width: 15rem; max-width: calc(100vw - 2rem); padding: .5rem; background: var(--pypto-surface); border: 1px solid var(--pypto-line); border-radius: .6rem; box-shadow: var(--pypto-shadow); }
+.pypto-project-menu p { margin: .3rem .5rem .5rem; color: var(--pypto-muted); font-size: .65rem; }
+.pypto-project-menu a { display: flex; justify-content: space-between; align-items: center; gap: .6rem; min-height: 2.2rem; padding: .45rem .55rem; border-radius: .3rem; font-size: .75rem; }
+.pypto-project-menu a:hover, .pypto-project-menu a[aria-current] { color: var(--pypto-accent); background: var(--pypto-featured); }
+.pypto-project-menu small { color: var(--pypto-muted); font-size: .6rem; }
+.pypto-project-menu .pypto-project-home { margin-top: .4rem; border-top: 1px solid var(--pypto-line); border-radius: 0; }
+.md-tabs { background: var(--pypto-surface); border-bottom: 1px solid var(--pypto-line); }
+.md-tabs__link { font-size: .75rem; opacity: 1; color: var(--pypto-muted); }
+.md-tabs__link--active, .md-tabs__item--active .md-tabs__link { color: var(--pypto-accent); font-weight: 650; }
+.md-tabs__link:hover { color: var(--pypto-accent); }
+.md-tabs__item--active { border-bottom: 2px solid var(--pypto-accent); }
+.md-main__inner { margin-top: 1.6rem; }
+.md-content { min-width: 0; }
+.md-content__inner { margin-inline: 1.6rem; padding-bottom: 2rem; }
+.md-typeset { font-size: .8rem; line-height: 1.75; }
+.md-typeset h1 { color: var(--pypto-text); font-size: 1.9rem; font-weight: 700; letter-spacing: -.045em; line-height: 1.25; }
+.md-typeset h2 { font-weight: 650; letter-spacing: -.025em; }
+.md-typeset h3 { font-weight: 650; }
+.md-typeset a:hover { text-decoration: underline; text-underline-offset: .15em; }
+.md-typeset .md-button { border-radius: .4rem; }
+.md-typeset .md-button--primary { color: var(--pypto-background); background: var(--pypto-accent); }
+.md-typeset code { border-radius: .2rem; }
+.md-typeset pre > code { padding: 1rem; border: 1px solid var(--pypto-line); border-radius: .5rem; }
+.md-typeset pre { max-width: 100%; }
+.md-typeset .highlight { max-width: 100%; }
+.md-typeset table:not([class]) { border-radius: .4rem; background: var(--pypto-surface); }
+.md-typeset table:not([class]) th { background: var(--pypto-featured); font-weight: 650; }
+.md-typeset .admonition, .md-typeset details { border-radius: .4rem; font-size: .75rem; }
+.md-nav { font-size: .7rem; line-height: 1.5; }
+.md-nav__link { padding-block: .15rem; }
+.md-nav__link--active { color: var(--pypto-accent); font-weight: 650; }
+.md-nav__title { color: var(--pypto-text); background: transparent; box-shadow: none; }
+.md-sidebar--secondary .md-nav { border-left: 1px solid var(--pypto-line); padding-left: .7rem; }
+.md-search__form { background: var(--pypto-background); border: 1px solid var(--pypto-line); border-radius: .4rem; }
+.md-search__input { color: var(--pypto-text); }
+.md-search__input::placeholder { color: var(--pypto-muted); }
+.md-search__icon { color: var(--pypto-muted); }
+.md-footer { border-top: 1px solid var(--pypto-line); }
+html .md-typeset a:focus-visible, .pypto-header a:focus-visible, .pypto-header summary:focus-visible { outline: 3px solid var(--pypto-accent); outline-offset: 3px; }
+
+@media (max-width: 76.234375em) {
+ .md-nav--primary .md-nav__title { background: var(--pypto-surface); color: var(--pypto-text); }
+ .md-content__inner { margin-inline: 1.2rem; }
+}
+
+@media (max-width: 59.984375em) {
+ .pypto-header .md-header__inner { min-height: 3.2rem; gap: .4rem; }
+ .pypto-header-spacer { display: none; }
+ .pypto-project-switcher { flex: 1; }
+ .pypto-project-switcher summary { justify-content: space-between; }
+ .pypto-project-menu { left: auto; right: 0; }
+}
+
+@media (max-width: 600px) {
+ .pypto-brand span, .pypto-source { display: none; }
+ .pypto-header .md-header__inner { flex-wrap: wrap; gap: .25rem; padding-inline: .4rem; }
+ .pypto-brand img { width: 1.5rem; height: 1.5rem; }
+ .pypto-project-switcher { flex: 1 1 3rem; }
+ .pypto-project-switcher summary { max-width: 11rem; }
+ .pypto-project-menu { position: fixed; top: auto; left: .5rem; right: .5rem; width: auto; max-width: none; margin-top: .5rem; }
+ .pypto-theme-control select { padding-inline: 3px; font-size: 14px; }
+ .md-content__inner { margin-inline: .8rem; }
+ .md-typeset h1 { font-size: 1.6rem; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .pypto-header * { scroll-behavior: auto; transition: none; }
+}
diff --git a/docs-theme/overrides/main.html b/docs-theme/overrides/main.html
new file mode 100644
index 0000000..8e2347b
--- /dev/null
+++ b/docs-theme/overrides/main.html
@@ -0,0 +1,17 @@
+{% extends "base.html" %}
+
+{% block extrahead %}
+ {{ super() }}
+
+
+{% endblock %}
+
+{% block header %}
+
+ {% include "partials/pypto-header.html" %}
+{% endblock %}
+
+{% block scripts %}
+ {{ super() }}
+
+{% endblock %}
diff --git a/docs-theme/overrides/partials/project-switcher.html b/docs-theme/overrides/partials/project-switcher.html
new file mode 100644
index 0000000..f4a6d63
--- /dev/null
+++ b/docs-theme/overrides/partials/project-switcher.html
@@ -0,0 +1,20 @@
+{% set chinese = lang.t('language').startswith('zh') %}
+
+ {{ config.site_name }}
+ {{ '切换项目' if chinese else 'Switch project' }}
+
+
+
+