diff --git a/site/docusaurus.config.ts b/site/docusaurus.config.ts
index 8ceaf142..37c72234 100644
--- a/site/docusaurus.config.ts
+++ b/site/docusaurus.config.ts
@@ -149,7 +149,10 @@ export default {
],
],
- clientModules: [require.resolve('./src/clientModules/plausiblePageview.js')],
+ clientModules: [
+ require.resolve('./src/clientModules/plausiblePageview.js'),
+ require.resolve('./src/clientModules/kapa-sidebar.js'),
+ ],
stylesheets: [
{
diff --git a/site/src/clientModules/kapa-sidebar.js b/site/src/clientModules/kapa-sidebar.js
new file mode 100644
index 00000000..3e4210e4
--- /dev/null
+++ b/site/src/clientModules/kapa-sidebar.js
@@ -0,0 +1,101 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+
+// Injects the Kapa widget script with all configuration (including
+// data-color-scheme-selector which Docusaurus strips due to nested quotes),
+// then detects sidebar open/close and toggles .kapa-sidebar-open on .
+
+if (typeof window !== "undefined") {
+ // Inject the Kapa widget script tag with all attributes
+ const script = document.createElement("script");
+ script.src = "https://widget.kapa.ai/kapa-widget.bundle.js";
+ script.async = true;
+ const attrs = {
+ "data-website-id": "de266438-2cd4-4ed5-9633-a1c6b931dd7d",
+ "data-project-name": "Move Book",
+ "data-project-color": "#298DFF",
+ "data-button-hide": "true",
+ "data-view-mode": "sidebar",
+ "data-modal-title": "Ask Move AI",
+ "data-modal-ask-ai-input-placeholder": "Ask me anything about Move!",
+ "data-modal-example-questions": "How do I define a struct in Move?,What are abilities in Move?,How do I publish a Move package?,What are dynamic fields?",
+ "data-modal-overlay-hidden": "true",
+ "data-modal-lock-scroll": "false",
+ "data-modal-logo-hidden": "true",
+ "data-color-scheme-selector": "[data-theme='dark']",
+ };
+ for (const [key, value] of Object.entries(attrs)) {
+ script.setAttribute(key, value);
+ }
+ document.head.appendChild(script);
+
+ // Sidebar open/close detection
+ const OPEN_CLASS = "kapa-sidebar-open";
+ let kapaOpen = false;
+ let hookedRef = null;
+
+ function syncClass() {
+ document.documentElement.classList.toggle(OPEN_CLASS, kapaOpen);
+ }
+
+ function hookKapa() {
+ if (!window.Kapa || !window.Kapa.open || window.Kapa.open === hookedRef) return;
+
+ const origOpen = window.Kapa.open;
+ const origClose = window.Kapa.close;
+
+ window.Kapa.open = function (...args) {
+ kapaOpen = true;
+ syncClass();
+ return origOpen.apply(this, args);
+ };
+
+ window.Kapa.close = function (...args) {
+ kapaOpen = false;
+ syncClass();
+ return origClose.apply(this, args);
+ };
+
+ hookedRef = window.Kapa.open;
+ }
+
+ // Check if Kapa sidebar is covering the right side of the viewport
+ function isSidebarVisible() {
+ const x = window.innerWidth - 50;
+ const y = window.innerHeight / 2;
+ const el = document.elementFromPoint(x, y);
+ if (!el) return false;
+ const docRoot = document.getElementById("__docusaurus");
+ if (docRoot && docRoot.contains(el)) return false;
+ if (el === document.body || el === document.documentElement) return false;
+ return true;
+ }
+
+ // Navigation hooks for SPA
+ const origPush = history.pushState.bind(history);
+ const origReplace = history.replaceState.bind(history);
+ history.pushState = function (...args) {
+ const r = origPush(...args);
+ syncClass();
+ return r;
+ };
+ history.replaceState = function (...args) {
+ const r = origReplace(...args);
+ syncClass();
+ return r;
+ };
+ window.addEventListener("popstate", syncClass);
+
+ // Poll every 300ms
+ setInterval(() => {
+ hookKapa();
+
+ const visible = isSidebarVisible();
+ if (visible && !kapaOpen) {
+ kapaOpen = true;
+ } else if (!visible && kapaOpen) {
+ kapaOpen = false;
+ }
+ syncClass();
+ }, 300);
+}
diff --git a/site/src/css/custom.css b/site/src/css/custom.css
index a8c459ab..ada995ba 100644
--- a/site/src/css/custom.css
+++ b/site/src/css/custom.css
@@ -141,3 +141,90 @@ html {
padding: 0;
font-size: 2.25em;
}
+
+/* ---- Kapa sidebar content shift ---- */
+/* Kapa renders in Shadow DOM so CSS :has() can't detect it.
+ A client module (kapa-sidebar.js) toggles .kapa-sidebar-open on . */
+html.kapa-sidebar-open body {
+ width: calc(100vw - 500px) !important;
+ overflow-x: hidden;
+ transition: width 0.3s ease;
+}
+
+html.kapa-sidebar-open .navbar,
+html.kapa-sidebar-open .navbar--fixed-top {
+ right: 500px !important;
+ overflow: hidden;
+ transition: right 0.3s ease;
+}
+
+/* Hide the Ask AI and Search buttons in navbar when sidebar is open
+ since Kapa is already visible */
+html.kapa-sidebar-open .kapa-trigger-btn,
+html.kapa-sidebar-open .DocSearch-Button,
+html.kapa-sidebar-open [class*="searchBarContainer"] {
+ display: none !important;
+}
+
+.navbar,
+.navbar--fixed-top {
+ transition: right 0.3s ease;
+}
+
+/* Hide the desktop TOC column when sidebar is open to free space */
+html.kapa-sidebar-open .col.col--3:has(.theme-doc-toc-desktop) {
+ display: none;
+}
+
+/* Let the main content column expand to fill the freed space */
+html.kapa-sidebar-open .col[class*="docItemCol"] {
+ max-width: 100% !important;
+ flex: 1 !important;
+}
+
+body {
+ transition: width 0.3s ease;
+}
+
+@media (max-width: 768px) {
+ html.kapa-sidebar-open body {
+ width: 100vw !important;
+ }
+}
+
+/* Kapa trigger button styles */
+.kapa-trigger-btn {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ cursor: pointer;
+ background: none;
+ color: var(--ifm-font-color-base);
+ font-family: var(--ifm-font-family-base);
+ font-weight: var(--ifm-font-weight-bold);
+ font-size: var(--ifm-menu-link-font-size);
+ padding: 0.4rem 0.6rem;
+ border-radius: 4px;
+ border: none;
+ transition: color 0.15s, background-color 0.15s;
+}
+
+.kapa-trigger-btn:hover {
+ color: var(--ifm-menu-color-active);
+ background: var(--ifm-menu-color-background-hover);
+}
+
+[data-theme='dark'] .kapa-trigger-btn {
+ color: var(--ifm-font-color-base);
+}
+
+[data-theme='dark'] .kapa-trigger-btn:hover {
+ color: var(--ifm-menu-color-active);
+}
+
+/* Compact kapa button below full desktop width */
+@media screen and (max-width: 996px) {
+ .navbar .kapa-trigger-btn span {
+ display: none;
+ }
+}
diff --git a/site/src/theme/Navbar/Content/index.js b/site/src/theme/Navbar/Content/index.js
new file mode 100644
index 00000000..a3619b9e
--- /dev/null
+++ b/site/src/theme/Navbar/Content/index.js
@@ -0,0 +1,107 @@
+// Copyright (c) Mysten Labs, Inc.
+// SPDX-License-Identifier: Apache-2.0
+import * as React from "react";
+import { useThemeConfig, ErrorCauseBoundary } from "@docusaurus/theme-common";
+import {
+ splitNavbarItems,
+ useNavbarMobileSidebar,
+} from "@docusaurus/theme-common/internal";
+import NavbarItem from "@theme/NavbarItem";
+import NavbarMobileSidebarToggle from "@theme/Navbar/MobileSidebar/Toggle";
+import NavbarLogo from "@theme/Navbar/Logo";
+import NavbarSearch from "@theme/Navbar/Search";
+import NavbarColorModeToggle from "@theme/Navbar/ColorModeToggle";
+import SearchBar from "@theme/SearchBar";
+
+function useNavbarItems() {
+ return useThemeConfig().navbar.items;
+}
+
+function useMobileSidebarSafe() {
+ try {
+ return useNavbarMobileSidebar();
+ } catch {
+ return { disabled: true, toggle: () => {} };
+ }
+}
+
+function NavbarItems({ items }) {
+ return (
+ <>
+ {items.map((item, i) => {
+ // Skip the search item — we render SearchBar separately
+ if (item.type === "search") return null;
+ return (
+