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 ( + + new Error( + `A theme navbar item failed to render. +Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config: +${JSON.stringify(item, null, 2)}`, + { cause: error }, + ) + } + > + + + ); + })} + + ); +} + +function NavbarContentLayout({ left, right }) { + return ( +
+
{left}
+
{right}
+
+ ); +} + +function KapaButton() { + const handleClick = () => { + if (typeof window !== "undefined" && window.Kapa) { + window.Kapa.open(); + } + }; + + return ( + + ); +} + +export default function NavbarContent() { + const mobileSidebar = useMobileSidebarSafe(); + const items = useNavbarItems(); + const [leftItems, rightItems] = splitNavbarItems(items); + + return ( + + {!mobileSidebar.disabled && } + + + + } + right={ + <> + + + + + + + + } + /> + ); +}