diff --git a/mdx-components.js b/mdx-components.js index 7b4c98a4..a0e986c2 100644 --- a/mdx-components.js +++ b/mdx-components.js @@ -1,12 +1,25 @@ -import { useMDXComponents as getThemeComponents } from "nextra-theme-docs"; // nextra-theme-blog or your custom theme +import { DocsLayout } from './src/components/docs/DocsLayout'; +import { buildPageMap } from './src/app/docs/page-map'; -// Get the default MDX components -const themeComponents = getThemeComponents(); - -// Merge components +// Custom MDX components without nextra-theme-docs export function useMDXComponents(components) { return { - ...themeComponents, + // Wrapper component that wraps the entire MDX content with DocsLayout + wrapper: ({ children, toc, metadata, sourceCode, ...props }) => { + const { pageMap } = buildPageMap(); + + return ( + + {children} + + ); + }, + // You can add custom component mappings here + // Example: + // h1: (props) =>

, + // h2: (props) =>

, + // a: (props) => , ...components, }; } + diff --git a/package.json b/package.json index 69910105..b62948fc 100644 --- a/package.json +++ b/package.json @@ -22,14 +22,15 @@ "dependencies": { "@react-three/drei": "^10.7.6", "@react-three/fiber": "^9.4.0", + "@theguild/remark-mermaid": "^0.3.0", "dotenv": "^17.2.3", "framer-motion": "^12.23.22", "lucide-react": "^0.545.0", "mermaid": "^11.12.1", "next": "^15.5.9", "next-intl": "^4.3.12", - "nextra": "^4.6.0", - "nextra-theme-docs": "^4.6.0", + "next-themes": "^0.4.6", + "nextra": "^4.6.1", "postmark": "^4.0.5", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/src/app/docs/layout.tsx b/src/app/docs/layout.tsx index 374d7e81..d1db4a4b 100644 --- a/src/app/docs/layout.tsx +++ b/src/app/docs/layout.tsx @@ -1,6 +1,5 @@ -import { Layout } from 'nextra-theme-docs' -import 'nextra-theme-docs/style.css' import { DocsNavbar, DocsFooter, DocsBanner } from '@/components/docs/index' +import { DocsProvider } from '@/components/docs/DocsProvider' import { Inter, JetBrains_Mono } from "next/font/google" import { Suspense } from 'react' import { ThemeProvider } from "next-themes" @@ -23,44 +22,30 @@ export const metadata = { description: 'Official documentation for KubeStellar - Multi-cluster orchestration platform', } -const banner = -const navbar = ( - }> - - -) -const footer = - type Props = { children: React.ReactNode } export default async function DocsLayout({ children }: Props) { // Build page map from local docs - const { pageMap } = buildPageMap() + buildPageMap(); return ( - - {children} - + +
+ + }> + + +
+ {children} +
+ +
+
diff --git a/src/app/globals.css b/src/app/globals.css index b2d881d3..aae62256 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -59,6 +59,33 @@ html:not(.dark) .nextra-toc { border-right-color: rgba(0, 0, 0, 0.1); } +/* Light mode: Improve MDX content readability */ +html:not(.dark) .nextra-content, html:not(.dark) .prose { + color: #222; + background: transparent; +} +html:not(.dark) .nextra-content h1, html:not(.dark) .prose h1, +html:not(.dark) .nextra-content h2, html:not(.dark) .prose h2, +html:not(.dark) .nextra-content h3, html:not(.dark) .prose h3, +html:not(.dark) .nextra-content h4, html:not(.dark) .prose h4 { + color: #1a202c; +} +html:not(.dark) .nextra-content p, html:not(.dark) .prose p { + color: #222; +} +html:not(.dark) .nextra-content table, html:not(.dark) .prose table { + color: #222; + background: #fff; +} +html:not(.dark) .nextra-content th, html:not(.dark) .prose th { + color: #1a202c; + background: #f3f4f6; +} +html:not(.dark) .nextra-content td, html:not(.dark) .prose td { + color: #222; + background: #fff; +} + /* Mobile responsive - remove separators */ @media (max-width: 1024px) { .nextra-toc { @@ -86,7 +113,7 @@ html:not(.dark) .nextra-toc { } body { - background: var(--background); + background: var(--color-bg-primary); color: var(--foreground); font-family: var(--font-inter); transition: @@ -2109,3 +2136,282 @@ html:not(.dark) .contributor-card:hover { z-index: 44 !important; } } + +/* Custom Documentation Styling */ +.prose { + color: #1f2937; + max-width: none !important; +} + +html.dark .prose { + color: #e5e7eb; +} + +.prose h1 { + font-size: 2.25rem; + font-weight: 700; + margin-bottom: 1.5rem; + margin-top: 2rem; + color: #111827; + border-bottom: 1px solid #e5e7eb; + padding-bottom: 0.5rem; +} + +html.dark .prose h1 { + color: #f3f4f6; + border-bottom-color: #1f2937; +} + +.prose h2 { + font-size: 1.875rem; + font-weight: 700; + margin-top: 2.5rem; + margin-bottom: 1rem; + color: #111827; + border-bottom: 1px solid #e5e7eb; + padding-bottom: 0.5rem; + scroll-margin-top: 5rem; +} + +html.dark .prose h2 { + color: #f3f4f6; + border-bottom-color: #1f2937; +} + +.prose h3 { + font-size: 1.5rem; + font-weight: 700; + margin-top: 2rem; + margin-bottom: 0.75rem; + color: #111827; + scroll-margin-top: 5rem; +} + +html.dark .prose h3 { + color: #f3f4f6; +} + +.prose h4 { + font-size: 1.25rem; + font-weight: 700; + margin-top: 1.5rem; + margin-bottom: 0.5rem; + color: #111827; + scroll-margin-top: 5rem; +} + +html.dark .prose h4 { + color: #f3f4f6; +} + +.prose h5 { + font-size: 1.125rem; + font-weight: 700; + margin-top: 1rem; + margin-bottom: 0.5rem; + color: #111827; +} + +html.dark .prose h5 { + color: #f3f4f6; +} + +.prose h6 { + font-size: 1rem; + font-weight: 700; + margin-top: 0.75rem; + margin-bottom: 0.5rem; + color: #111827; +} + +html.dark .prose h6 { + color: #f3f4f6; +} + +.prose p { + margin-bottom: 1rem; + line-height: 1.75; +} + +.prose a { + color: #2563eb; + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 2px; +} + +.prose a:hover { + text-decoration: underline; +} + +html.dark .prose a { + color: #60a5fa; +} + +.prose code { + background: #f3f4f6; + color: #1a202c; + padding: 0.125rem 0.375rem; + border-radius: 0.375rem; +} + +html.dark .prose code { + background: #1f2937; + color: #f3f4f6; +} + +.prose pre { + background: #f9fafb; + border-radius: 0.5rem; + padding: 1rem; + overflow-x: auto; + margin-top: 1.5rem; + margin-bottom: 1.5rem; + border: 1px solid #e5e7eb; +} + +html.dark .prose pre { + background: #111827; + border-color: #374151; +} + +.prose pre code { + background: transparent !important; + padding: 0; + border-radius: 0; + color: #1f2937; +} + +html.dark .prose pre code { + background: transparent !important; + color: #f3f4f6; +} + +.prose ul, .prose ol { + margin-top: 1rem; + margin-bottom: 1rem; + padding-left: 1.5rem; +} + +.prose ul { + list-style-type: disc; +} + +.prose ol { + list-style-type: decimal; +} + +.prose ul ul, .prose ol ul { + list-style-type: circle; +} + +.prose ul ul ul, .prose ol ul ul, .prose ol ol ul { + list-style-type: square; +} + +.prose li { + margin-bottom: 0.5rem; + padding-left: 0.25rem; +} + +.prose blockquote { + border-left: 4px solid #3b82f6; + padding-left: 1rem; + font-style: italic; + margin-top: 1.5rem; + margin-bottom: 1.5rem; + color: #374151; +} + +html.dark .prose blockquote { + color: #d1d5db; +} + +.prose table { + width: 100%; + margin-top: 1.5rem; + margin-bottom: 1.5rem; + border-collapse: collapse; +} + +.prose thead { + background: #f3f4f6; +} + +html.dark .prose thead { + background: #1f2937; +} + +.prose th { + border: 1px solid #d1d5db; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + text-align: left; + font-weight: 600; +} + +html.dark .prose th { + border-color: #374151; +} + +.prose td { + border: 1px solid #d1d5db; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +html.dark .prose td { + border-color: #374151; +} + +.prose img { + border-radius: 0.5rem; + margin-top: 1.5rem; + margin-bottom: 1.5rem; + margin-left: auto; + margin-right: auto; +} + +.prose hr { + margin-top: 2rem; + margin-bottom: 2rem; + border-color: #d1d5db; +} + +html.dark .prose hr { + border-color: #374151; +} + +/* Sidebar Scrollbar Styling */ +aside::-webkit-scrollbar { + width: 6px; +} + +aside::-webkit-scrollbar-track { + background: #f3f4f6; +} + +html.dark aside::-webkit-scrollbar-track { + background: #111827; +} + +aside::-webkit-scrollbar-thumb { + background: #d1d5db; + border-radius: 9999px; +} + +html.dark aside::-webkit-scrollbar-thumb { + background: #374151; +} + +aside::-webkit-scrollbar-thumb:hover { + background: #9ca3af; +} + +html.dark aside::-webkit-scrollbar-thumb:hover { + background: #4b5563; +} diff --git a/src/components/docs/DocsBanner.tsx b/src/components/docs/DocsBanner.tsx index 57af8560..77cd066e 100644 --- a/src/components/docs/DocsBanner.tsx +++ b/src/components/docs/DocsBanner.tsx @@ -1,42 +1,60 @@ 'use client' -import { Banner } from 'nextra/components' import Link from 'next/link' import { useTheme } from 'next-themes' import { useState, useEffect } from 'react' import { getLocalizedUrl } from '@/lib/url' +import { useDocsMenu } from './DocsProvider' export function DocsBanner() { const { resolvedTheme } = useTheme() const [mounted, setMounted] = useState(false) + const { bannerDismissed, dismissBanner } = useDocsMenu() useEffect(() => { setMounted(true) }, []) - if (!mounted) { + if (!mounted || bannerDismissed) { return null } const isDark = resolvedTheme === 'dark' return ( - - - 🚀 🚀 🚀 ATTENTION: KubeStellar needs your help - please take our 2-minute survey{' '} - - {getLocalizedUrl("https://kubestellar.io/survey")} - - {' '}🚀 🚀 🚀 - - +
+
+
+
+ + 🚀 🚀 🚀 ATTENTION: KubeStellar needs your help - please take our 2-minute survey{' '} + + {getLocalizedUrl("https://kubestellar.io/survey")} + + {' '}🚀 🚀 🚀 + +
+ +
+
+
) } diff --git a/src/components/docs/DocsLayout.tsx b/src/components/docs/DocsLayout.tsx new file mode 100644 index 00000000..38537369 --- /dev/null +++ b/src/components/docs/DocsLayout.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { ReactNode } from 'react'; +import { DocsSidebar } from './DocsSidebar'; +import { TableOfContents } from './TableOfContents'; +import { MobileTOC } from './MobileTOC'; +import { MobileHeader } from './MobileSidebarToggle'; +import { useDocsMenu } from './DocsProvider'; + +interface TOCItem { + id: string; + value: string; + depth: number; +} + +interface PageMapItem { + name: string; + route?: string; + title?: string; + children?: PageMapItem[]; + frontMatter?: Record; + kind?: string; +} + +interface Metadata { + title?: string; + description?: string; + [key: string]: unknown; +} + +interface DocsLayoutProps { + children: ReactNode; + pageMap: PageMapItem[]; + toc?: TOCItem[]; + metadata?: Metadata; +} + +export function DocsLayout({ children, pageMap, toc, metadata }: DocsLayoutProps) { + const { menuOpen, toggleMenu } = useDocsMenu(); + + return ( +
+ {/* Sidebar - Self-contained with all logic */} + + + {/* Mobile overlay */} + {menuOpen && ( +
+ )} + + {/* Main content area */} +
+
+ {/* Mobile Header with Sidebar Toggle - Only visible on mobile/tablet */} + + + {/* Mobile TOC Accordion - Only visible on mobile/tablet */} + + + {/* Article content */} +
+ {children} +
+
+
+ + {/* Table of Contents - Right sidebar on desktop */} + +
+ ); +} diff --git a/src/components/docs/DocsNavbar.tsx b/src/components/docs/DocsNavbar.tsx index 1ac5c627..eba0c7a8 100644 --- a/src/components/docs/DocsNavbar.tsx +++ b/src/components/docs/DocsNavbar.tsx @@ -7,14 +7,12 @@ import { useTheme } from "next-themes"; // import { useSearchParams, usePathname, useRouter } from 'next/navigation' import { VERSIONS } from '@/config/versions' import { getLocalizedUrl } from "@/lib/url"; -import { useMenu, setMenu } from 'nextra-theme-docs' type DropdownType = "contribute" | "community" | "language" | "github" | null; export default function DocsNavbar() { const [isMenuOpen, setIsMenuOpen] = useState(false); const [openDropdown, setOpenDropdown] = useState(null); - const menuOpen = useMenu(); const [searchQuery, setSearchQuery] = useState(""); const [isSearchOpen, setIsSearchOpen] = useState(false); const [searchResults, setSearchResults] = useState +
- {/* Sidebar toggle button for mobile - integrates with Nextra */} - + ) : ( + // Page - clickable link with icon + + + {displayTitle} + + )} +
+ + {/* Render children */} + {hasChildren && ( +
+ {/* Vertical line connecting children */} +
+ {item.children!.map(child => renderMenuItem(child, depth + 1, itemKey))} +
+ )} +
+ ); + }; + + // Render full sidebar (expanded state) + const renderFullSidebar = () => ( + <> + {/* Scrollable navigation area */} +
+ +
+ + {/* Footer at bottom */} + + + ); + + // Render slim sidebar (collapsed state) - Desktop only + const renderSlimSidebar = () => ( +
+ {/* Spacer */} +
+ + {/* Footer with icon buttons */} + +
+ ); + + const isDark = mounted && resolvedTheme === 'dark'; + + return ( + + ); +} diff --git a/src/components/docs/MobileSidebarToggle.tsx b/src/components/docs/MobileSidebarToggle.tsx new file mode 100644 index 00000000..277c8f2d --- /dev/null +++ b/src/components/docs/MobileSidebarToggle.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import { useDocsMenu } from './DocsProvider'; + +interface MobileHeaderProps { + onToggleSidebar: () => void; + pageTitle?: string; +} + +export function MobileHeader({ onToggleSidebar, pageTitle }: MobileHeaderProps) { + const { resolvedTheme } = useTheme(); + const { dismissBanner } = useDocsMenu(); + const [mounted, setMounted] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + const handleToggle = () => { + // Dismiss the banner when opening the sidebar on mobile + dismissBanner(); + onToggleSidebar(); + }; + + // Prevent hydration mismatch by not applying theme-specific styles until mounted + const isDark = mounted ? resolvedTheme === 'dark' : false; + + return ( +
+ +
+ ); +} diff --git a/src/components/docs/MobileTOC.tsx b/src/components/docs/MobileTOC.tsx new file mode 100644 index 00000000..0bc9e8ce --- /dev/null +++ b/src/components/docs/MobileTOC.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import Link from 'next/link'; + +interface TOCItem { + id: string; + value: string; + depth: number; +} + +interface MobileTOCProps { + toc?: TOCItem[]; +} + +function TOCLink({ item, isDark, onClose }: { item: TOCItem; isDark: boolean; onClose: () => void }) { + const [isHovered, setIsHovered] = useState(false); + const indent = (item.depth - 2) * 16; + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + const element = document.getElementById(item.id); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // Update URL without jumping + window.history.pushState(null, '', `#${item.id}`); + } + onClose(); + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + suppressHydrationWarning + > + {item.value} + + ); +} + +export function MobileTOC({ toc }: MobileTOCProps) { + const [isOpen, setIsOpen] = useState(false); + const { resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + const [headerHovered, setHeaderHovered] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + if (!toc || toc.length === 0) { + return null; + } + + // Prevent hydration mismatch by using default light theme on server + const isDark = mounted && resolvedTheme === 'dark'; + + return ( +
+ {/* Accordion Header */} + + + {/* Accordion Content */} +
+ +
+
+ ); +} diff --git a/src/components/docs/SidebarFooter.tsx b/src/components/docs/SidebarFooter.tsx new file mode 100644 index 00000000..edf338df --- /dev/null +++ b/src/components/docs/SidebarFooter.tsx @@ -0,0 +1,151 @@ +"use client"; + +import { useState, useEffect } from 'react'; +import { useTheme } from 'next-themes'; +import { Moon, Sun, PanelRightOpenIcon, PanelLeftOpen } from 'lucide-react'; + +interface SidebarFooterProps { + onCollapse: () => void; + variant?: 'full' | 'slim'; + isMobile?: boolean; +} + +export function SidebarFooter({ onCollapse, variant = 'full', isMobile = false }: SidebarFooterProps) { + const [mounted, setMounted] = useState(false); + const { resolvedTheme, setTheme } = useTheme(); + + useEffect(() => { + setMounted(true); + }, []); + + const isDark = mounted && resolvedTheme === 'dark'; + + // Slim variant - icon-only vertical layout + if (variant === 'slim') { + if (!mounted) { + return ( +
+
+
+
+ ); + } + + return ( +
+ {/* Theme Toggle Icon */} + + + {/* Expand Sidebar Icon */} + +
+ ); + } + + // Full variant - horizontal layout with text + if (!mounted) { + return ( +
+
+
+ ); + } + + return ( +
+ {/* Theme Toggle Button */} + + + {/* Collapse Sidebar Button - Hidden on mobile */} + {!isMobile && ( + + )} +
+ ); +} diff --git a/src/components/docs/TableOfContents.tsx b/src/components/docs/TableOfContents.tsx new file mode 100644 index 00000000..597ded60 --- /dev/null +++ b/src/components/docs/TableOfContents.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { useEffect, useState } from 'react'; +import { useTheme } from 'next-themes'; +import Link from 'next/link'; + +interface TOCItem { + id: string; + value: string; + depth: number; +} + +interface TableOfContentsProps { + toc?: TOCItem[]; +} + +function TOCLink({ item, isActive, isDark }: { item: TOCItem; isActive: boolean; isDark: boolean }) { + const [isHovered, setIsHovered] = useState(false); + const indent = (item.depth - 2) * 12; + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + const element = document.getElementById(item.id); + if (element) { + element.scrollIntoView({ behavior: 'smooth', block: 'start' }); + // Update URL without jumping + window.history.pushState(null, '', `#${item.id}`); + } + }; + + return ( + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + suppressHydrationWarning + > + {item.value} + + ); +} + +export function TableOfContents({ toc }: TableOfContentsProps) { + const [activeId, setActiveId] = useState(''); + const { resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + useEffect(() => { + if (!toc || toc.length === 0) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveId(entry.target.id); + } + }); + }, + { + rootMargin: '0px 0px -80% 0px', + threshold: 1.0, + } + ); + + // Observe all heading elements + toc.forEach(({ id }) => { + const element = document.getElementById(id); + if (element) { + observer.observe(element); + } + }); + + return () => { + observer.disconnect(); + }; + }, [toc]); + + if (!toc || toc.length === 0) { + return null; + } + + const isDark = mounted && resolvedTheme === 'dark'; + + return ( + + ); +} diff --git a/src/components/docs/ThemeToggle.tsx b/src/components/docs/ThemeToggle.tsx new file mode 100644 index 00000000..82045867 --- /dev/null +++ b/src/components/docs/ThemeToggle.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useTheme } from "next-themes"; +import { useEffect, useState } from "react"; +import { Moon, Sun } from "lucide-react"; + +interface ThemeToggleProps { + variant?: 'fixed' | 'icon'; +} + +export function ThemeToggle({ variant = 'fixed' }: ThemeToggleProps) { + const { resolvedTheme, setTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + // Avoid hydration mismatch + useEffect(() => { + setMounted(true); + }, []); + + const isDark = resolvedTheme === "dark"; + + // Icon-only variant for collapsed sidebar + if (variant === 'icon') { + if (!mounted) { + return ( + + ); + } + + return ( + + ); + } + + // Fixed variant (original behavior) + if (!mounted) { + return ( + + ); + } + + return ( + + ); +} diff --git a/src/components/docs/index.tsx b/src/components/docs/index.tsx index 8e2bba51..df85d0b3 100644 --- a/src/components/docs/index.tsx +++ b/src/components/docs/index.tsx @@ -2,3 +2,9 @@ export { default as DocsNavbar } from './DocsNavbar'; export { default as DocsFooter } from './DocsFooter'; export { DocsBanner } from './DocsBanner'; export { default as EditViewSourceButtons } from './EditViewSourceButtons'; +export { DocsProvider, useDocsMenu } from './DocsProvider'; +export { DocsLayout } from './DocsLayout'; +export { DocsSidebar } from './DocsSidebar'; +export { TableOfContents } from './TableOfContents'; +export { ThemeToggle } from './ThemeToggle'; +export { SidebarFooter } from './SidebarFooter';