Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions packages/docs/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,86 @@
@import 'fumadocs-ui/css/preset.css';

@source '../node_modules/fumadocs-ui/dist/**/*.js';

/* ---- Kapa sidebar content shift ---- */
html.kapa-sidebar-open body {
width: calc(100vw - 500px) !important;
overflow-x: hidden;
transition: width 0.3s ease;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded 500px shift isn't tied to Kapa's actual sidebar width. calc(100vw - 500px) (and the nav right: 500px) assume the widget is exactly 500px — if Kapa's sidebar width differs now or later, content overlaps or leaves a gap.


html.kapa-sidebar-open nav {
right: 500px !important;
overflow: hidden;
transition: right 0.3s ease;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global nav selector is over-broad — these rules target every <nav> (top navbar, left sidebar, TOC), not just the intended one, so any positioned nav shifts 500px when the class is active. Scope this to the specific nav.

}

html.kapa-sidebar-open .kapa-trigger-btn {
display: none !important;
}

nav {
transition: right 0.3s ease;
}

body {
transition: width 0.3s ease;
}

@media (max-width: 768px) {
html.kapa-sidebar-open body {
width: 100vw !important;
}
}

/* Kapa trigger button */
.kapa-trigger-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
background: var(--fd-background);
color: var(--fd-foreground);
font-weight: 500;
font-size: 0.875rem;
padding: 0.5rem 1rem;
border-radius: 9999px;
border: 1px solid var(--fd-border);
transition: background-color 0.15s;
}

.kapa-trigger-btn:hover {
background: var(--fd-muted);
}

/* Floating Kapa button (bottom-right) */
.kapa-floating-btn {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
z-index: 999;
display: inline-flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
background: #298dff;
color: white;
font-weight: 600;
font-size: 0.875rem;
padding: 0.75rem 1.25rem;
border-radius: 9999px;
border: none;
box-shadow: 0 4px 12px rgba(41, 141, 255, 0.4);
transition:
background-color 0.15s,
box-shadow 0.15s;
}

.kapa-floating-btn:hover {
background: #1b6fd1;
box-shadow: 0 6px 16px rgba(41, 141, 255, 0.5);
}

html.kapa-sidebar-open .kapa-floating-btn {
display: none !important;
}
22 changes: 22 additions & 0 deletions packages/docs/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Inter } from 'next/font/google';
import Link from 'next/link';
import Script from 'next/script';
import CloudFlareAnalytics from '@/components/CloudFlareAnalytics';
import { KapaSidebar, KapaButton } from '@/components/KapaWidget';

export const metadata: Metadata = {
title: {
Expand Down Expand Up @@ -72,6 +73,27 @@ export default function Layout({ children }: { children: React.ReactNode }) {
</Banner>
<RootProvider>{children}</RootProvider>
<CloudFlareAnalytics />
<KapaButton />
<KapaSidebar />
<Script
src="https://widget.kapa.ai/kapa-widget.bundle.js"
data-website-id="f40e82ec-5fe9-4776-a287-f889da70eaaa"
data-project-name="Sui SDK Knowledge"
data-project-color="#298DFF"
data-button-hide="true"
data-launcher-button-image="/logo.png"
data-launcher-button-image-height="24"
data-launcher-button-image-width="24"
data-view-mode="sidebar"
data-modal-image="/logo.png"
data-modal-title="Ask Mysten Labs SDK AI"
data-modal-ask-ai-input-placeholder="Ask me anything about the Sui SDKs!"
data-modal-example-questions="How do I create a transaction in TypeScript?,How do I connect a wallet with dApp Kit?,How do I query objects with the Sui SDK?,How do I use BCS encoding?"
data-color-scheme-selector=".dark"
data-modal-overlay-hidden="true"
data-modal-lock-scroll="false"
strategy="afterInteractive"
/>
</body>
</html>
);
Expand Down
99 changes: 99 additions & 0 deletions packages/docs/components/KapaWidget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

'use client';

import { useEffect, useState } from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused useState import — oxlint's no-unused-vars flags it (warning only, so CI still passes). Remove it.


declare global {
interface Window {
Kapa?: {
open: (query?: string) => void;
close: () => void;
};
}
}

const OPEN_CLASS = 'kapa-sidebar-open';

export function KapaSidebar() {
useEffect(() => {
let kapaOpen = false;
let hookedRef: ((query?: string) => void) | null = null;

function syncClass() {
document.documentElement.classList.toggle(OPEN_CLASS, kapaOpen);
}

function hookKapa() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Monkeypatch is never restored; remount double-wraps. Cleanup only clears the interval — window.Kapa.open/close stay wrapped. React StrictMode (dev) or any remount gets a fresh hookedRef=null and re-wraps the already-wrapped functions. Harmless in today's single-mount layout, latent leak otherwise.

if (!window.Kapa || !window.Kapa.open || window.Kapa.open === hookedRef) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this how they recommend building this out, this feels like a pretty hacky way to manage a sidebar


const origOpen = window.Kapa.open;
const origClose = window.Kapa.close;

window.Kapa.open = function (...args: Parameters<typeof origOpen>) {
kapaOpen = true;
syncClass();
return origOpen.apply(this, args);
};

window.Kapa.close = function (...args: Parameters<typeof origClose>) {
kapaOpen = false;
syncClass();
return origClose.apply(this, args);
};

hookedRef = window.Kapa.open;
}

function isSidebarVisible() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This detection heuristic is likely unnecessary and fragile. The <Script> already sets data-view-mode="sidebar" (layout.tsx), so Kapa natively renders a sidebar — this custom elementFromPoint polling reinvents that. Also, getElementById('nd-docs-layout') returns null (that id doesn't exist anywhere in the repo and Fumadocs doesn't emit it), so containment falls back solely to querySelector('main'). Any fixed/sticky element at (innerWidth-50, innerHeight/2) outside <main> (e.g. Fumadocs' right-hand TOC <aside>) yields a permanent false positive, leaving the body shifted 500px with the button hidden. Prefer Kapa's official view-mode / state callbacks over DOM-probing.

const x = window.innerWidth - 50;
const y = window.innerHeight / 2;
const el = document.elementFromPoint(x, y);
if (!el) return false;
if (el === document.body || el === document.documentElement) return false;
const root = document.getElementById('nd-docs-layout') || document.querySelector('main');
if (root && root.contains(el)) return false;
return true;
}

const interval = setInterval(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Polling overwrites the monkeypatch state, causing flicker. Every 300ms this interval recomputes kapaOpen purely from the elementFromPoint guess, discarding what the hooked open()/close() just set. On click, open() sets kapaOpen=true + adds the class, but Kapa's sidebar animates in over a few hundred ms — the next tick probes the right-middle point before the sidebar occupies it, reads visible=false, flips the class off (body snaps back, floating button reappears), then re-adds it next tick. Result is a flicker on every open/close.

hookKapa();
const visible = isSidebarVisible();
if (visible && !kapaOpen) {
kapaOpen = true;
} else if (!visible && kapaOpen) {
kapaOpen = false;
}
syncClass();
}, 300);

return () => clearInterval(interval);
}, []);

return null;
}

export function KapaButton() {
return (
<button type="button" onClick={() => window.Kapa?.open()} className="kapa-floating-btn">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Button is a silent no-op before the async script loads: window.Kapa?.open() does nothing if clicked before afterInteractive defines window.Kapa. Consider a disabled/loading state.

<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M8 15h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2z" />
<path d="M9 18h6" />
<path d="M10 22h4" />
<path d="M10 18v4" />
<path d="M14 18v4" />
</svg>
<span>Ask SDK AI</span>
</button>
);
}
Binary file added packages/docs/public/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading