@@ -183,7 +198,7 @@ function HomeNavbar() {
{/* Compact Pill Navbar */}
-
+
{/* Compact Logo */}
@@ -238,7 +253,7 @@ function HomeNavbar() {
{/* Compact AI Chat Toggle */}
-
+
@@ -283,11 +298,14 @@ export function HomeLayout({ children }: { children: ReactNode }) {
}, []);
return (
-
-
-
- {children}
-
-
+
+
+
);
}
diff --git a/docs/src/components/layouts/page.tsx b/docs/src/components/layouts/page.tsx
index 16bed9bca..b6e2c2f13 100644
--- a/docs/src/components/layouts/page.tsx
+++ b/docs/src/components/layouts/page.tsx
@@ -24,7 +24,7 @@ import {
import { BackToTop } from '../ui/back-to-top';
import { buttonVariants } from '../ui/button';
import { slot } from './shared';
-import { useTOC } from './toc-context';
+import { useSidebar } from './sidebar-context';
const ClerkTOCItems = lazy(() => import('@/components/layout/toc-clerk'));
@@ -118,7 +118,7 @@ export function DocsPage({
} = {},
...props
}: DocsPageProps) {
- const { setIsFullPage } = useTOC();
+ const { setIsFullPage } = useSidebar();
// Update the full page state in the context
useEffect(() => {
diff --git a/docs/src/components/layouts/shared-header.tsx b/docs/src/components/layouts/shared-header.tsx
index 3c2b5f245..b25d4b4be 100644
--- a/docs/src/components/layouts/shared-header.tsx
+++ b/docs/src/components/layouts/shared-header.tsx
@@ -4,13 +4,13 @@ import { SearchInputToggle } from '@/components/layout/custom-search-toggle';
import Waves from '@/components/layouts/api/waves';
import { isInApiSection, isInComponentsSection, isInSdkSection } from '@/components/layouts/shared/section-utils';
import { type NavLink } from '@/lib/navigation-utils';
-import { List, Menu, Sparkles, X } from 'lucide-react';
+import { Key, Menu, Sparkles, TableOfContents, X } from 'lucide-react';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { ReactNode } from 'react';
import { useEffect, useState } from 'react';
-import { useChatContext } from '../chat/ai-chat';
-import { useTOC } from './toc-context';
+import { cn } from '../../lib/cn';
+import { useSidebar } from './sidebar-context';
type SharedHeaderProps = {
/** Navigation links to display */
@@ -73,19 +73,41 @@ function isNavLinkActive(pathname: string, navLink: NavLink): boolean {
* AI Chat Toggle Button
*/
function AIChatToggleButton() {
- const { isOpen, toggleChat } = useChatContext();
+ const sidebarContext = useSidebar();
+ const [animationVariant, setAnimationVariant] = useState('');
+
+ // Return null if context is not available
+ if (!sidebarContext) {
+ return null;
+ }
+
+ const { isChatOpen, toggleChat } = sidebarContext;
+
+ // Generate random variant when chat is opened
+ const handleToggle = () => {
+ if (!isChatOpen) {
+ // Generate random variant (2-4, keeping 1 as default)
+ const variants = ['variant-2', 'variant-3', 'variant-4'];
+ const randomVariant = variants[Math.floor(Math.random() * variants.length)];
+ setAnimationVariant(randomVariant);
+ } else {
+ setAnimationVariant('');
+ }
+ toggleChat();
+ };
return (
);
}
@@ -94,40 +116,35 @@ function AIChatToggleButton() {
* Inner TOC Toggle Button that uses the context
*/
function TOCToggleButtonInner() {
- const { isTocOpen, toggleToc } = useTOC();
- const { isOpen: isChatOpen } = useChatContext();
+ const sidebarContext = useSidebar();
- // TOC is effectively visible only if it's open AND chat is not open
- const isTocEffectivelyVisible = isTocOpen && !isChatOpen;
+ // Return null if context is not available
+ if (!sidebarContext) {
+ return null;
+ }
- return (
-
- );
-}
-
-/**
- * TOC Toggle Button Wrapper that safely checks full page state
- */
-function TOCToggleButtonWrapper() {
- const { isFullPage } = useTOC();
+ const { isTocOpen, toggleToc, isChatOpen, isFullPage } = sidebarContext;
// Hide TOC button on full pages
if (isFullPage) return null;
- return
;
+ // When chat is open, TOC is effectively not visible
+ const isTocEffectivelyVisible = isTocOpen && !isChatOpen;
+
+ return (
+
+ );
}
/**
@@ -141,12 +158,36 @@ function TOCToggleButton() {
if (!isDocsPage) return null;
- try {
- return
;
- } catch {
- // TOC context not available
+ return
;
+}
+
+/**
+ * Auth Toggle Button - Shows on all pages like AI Chat button
+ */
+function AuthToggleButton() {
+ const sidebarContext = useSidebar();
+
+ // Return null if context is not available
+ if (!sidebarContext) {
return null;
}
+
+ const { isAuthOpen, toggleAuth } = sidebarContext;
+
+ return (
+
+ );
}
/**
@@ -280,6 +321,11 @@ export function SharedHeader({
+ {/* Auth Toggle Button - Shows on all pages like AI Chat button */}
+
diff --git a/docs/src/components/layouts/sidebar-context.tsx b/docs/src/components/layouts/sidebar-context.tsx
new file mode 100644
index 000000000..74735049e
--- /dev/null
+++ b/docs/src/components/layouts/sidebar-context.tsx
@@ -0,0 +1,156 @@
+'use client';
+
+import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
+
+type SidebarType = 'toc' | 'chat' | 'auth' | null;
+
+type SidebarContextType = {
+ // Current active sidebar
+ activeSidebar: SidebarType,
+
+ // TOC state
+ isTocOpen: boolean,
+ setTocOpen: (open: boolean) => void,
+ toggleToc: () => void,
+
+ // Chat state
+ isChatOpen: boolean,
+ setChatOpen: (open: boolean) => void,
+ toggleChat: () => void,
+
+ // Chat expansion
+ isChatExpanded: boolean,
+ setChatExpanded: (expanded: boolean) => void,
+
+ // Auth state
+ isAuthOpen: boolean,
+ setAuthOpen: (open: boolean) => void,
+ toggleAuth: () => void,
+
+ // Full page state
+ isFullPage: boolean,
+ setIsFullPage: (fullPage: boolean) => void,
+
+ // Unified controls
+ openSidebar: (type: SidebarType) => void,
+ closeSidebar: () => void,
+}
+
+const SidebarContext = createContext
(null);
+
+export function useSidebar() {
+ const context = useContext(SidebarContext);
+ return context;
+}
+
+export function SidebarProvider({ children }: { children: ReactNode }) {
+ const [activeSidebar, setActiveSidebar] = useState(null);
+ const [isChatExpanded, setIsChatExpanded] = useState(false);
+ const [isFullPage, setIsFullPage] = useState(false);
+
+ // Load state from localStorage on mount
+ useEffect(() => {
+ const savedChat = localStorage.getItem('ai-chat-open');
+ const savedExpanded = localStorage.getItem('ai-chat-expanded');
+
+ if (savedChat === 'true') {
+ setActiveSidebar('chat');
+ }
+ if (savedExpanded === 'true') {
+ setIsChatExpanded(true);
+ }
+ }, []);
+
+ // Manage body classes based on sidebar state
+ useEffect(() => {
+ // Remove all classes first
+ document.body.classList.remove('chat-open', 'toc-open', 'auth-open');
+
+ // Add appropriate class based on active sidebar
+ if (activeSidebar === 'chat') {
+ document.body.classList.add('chat-open');
+ } else if (activeSidebar === 'toc') {
+ document.body.classList.add('toc-open');
+ } else if (activeSidebar === 'auth') {
+ document.body.classList.add('auth-open');
+ }
+
+ return () => {
+ document.body.classList.remove('chat-open', 'toc-open', 'auth-open');
+ };
+ }, [activeSidebar]);
+
+ // Derived state
+ const isTocOpen = activeSidebar === 'toc';
+ const isChatOpen = activeSidebar === 'chat';
+ const isAuthOpen = activeSidebar === 'auth';
+
+ // Individual controls
+ const setTocOpen = (open: boolean) => {
+ setActiveSidebar(open ? 'toc' : null);
+ };
+
+ const setChatOpen = (open: boolean) => {
+ if (open) {
+ setActiveSidebar('chat');
+ localStorage.setItem('ai-chat-open', 'true');
+ } else {
+ setActiveSidebar(null);
+ localStorage.setItem('ai-chat-open', 'false');
+ setIsChatExpanded(false);
+ }
+ };
+
+ const setAuthOpen = (open: boolean) => {
+ setActiveSidebar(open ? 'auth' : null);
+ };
+
+ const toggleToc = () => setTocOpen(!isTocOpen);
+ const toggleChat = () => setChatOpen(!isChatOpen);
+ const toggleAuth = () => setAuthOpen(!isAuthOpen);
+
+ const setChatExpanded = (expanded: boolean) => {
+ setIsChatExpanded(expanded);
+ localStorage.setItem('ai-chat-expanded', expanded.toString());
+ };
+
+ // Unified controls
+ const openSidebar = (type: SidebarType) => {
+ setActiveSidebar(type);
+
+ if (type === 'chat') {
+ localStorage.setItem('ai-chat-open', 'true');
+ } else {
+ localStorage.setItem('ai-chat-open', 'false');
+ }
+ };
+
+ const closeSidebar = () => {
+ setActiveSidebar(null);
+ localStorage.setItem('ai-chat-open', 'false');
+ setIsChatExpanded(false);
+ };
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/docs/src/components/layouts/toc-context.tsx b/docs/src/components/layouts/toc-context.tsx
deleted file mode 100644
index 1d2a1ada4..000000000
--- a/docs/src/components/layouts/toc-context.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-'use client';
-
-import { createContext, useContext, useState, type ReactNode } from 'react';
-
-type TOCContextType = {
- isTocOpen: boolean,
- setIsTocOpen: (open: boolean) => void,
- toggleToc: () => void,
- isFullPage: boolean,
- setIsFullPage: (fullPage: boolean) => void,
-}
-
-const TOCContext = createContext(null);
-
-export function useTOC() {
- const context = useContext(TOCContext);
- if (!context) {
- throw new Error('useTOC must be used within TOCProvider');
- }
- return context;
-}
-
-export function TOCProvider({ children }: { children: ReactNode }) {
- const [isTocOpen, setIsTocOpen] = useState(false); // Default closed
- const [isFullPage, setIsFullPage] = useState(false); // Default not full page
-
- const toggleToc = () => setIsTocOpen(!isTocOpen);
-
- return (
-
- {children}
-
- );
-}