feat: Add intelligent auto-router and enhanced integrations

- Add intelligent-router.sh hook for automatic agent routing
- Add AUTO-TRIGGER-SUMMARY.md documentation
- Add FINAL-INTEGRATION-SUMMARY.md documentation
- Complete Prometheus integration (6 commands + 4 tools)
- Complete Dexto integration (12 commands + 5 tools)
- Enhanced Ralph with access to all agents
- Fix /clawd command (removed disable-model-invocation)
- Update hooks.json to v5 with intelligent routing
- 291 total skills now available
- All 21 commands with automatic routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
admin
2026-01-28 00:27:56 +04:00
Unverified
parent 3b128ba3bd
commit b52318eeae
1724 changed files with 351216 additions and 0 deletions

View File

@@ -0,0 +1,428 @@
import React, { useState, useCallback, useRef, useEffect } from 'react';
interface CopyMarkdownProps {
className?: string;
}
export default function CopyMarkdown({ className }: CopyMarkdownProps) {
const [copied, setCopied] = useState(false);
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const timeoutRef = useRef<number | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const extractMarkdownFromPage = useCallback((): string => {
// Get the main content area
const article = document.querySelector('article[role="main"]') ||
document.querySelector('main.docMainContainer') ||
document.querySelector('.markdown');
if (!article) {
return 'Unable to extract content';
}
// Clone the article to avoid modifying the original
const clone = article.cloneNode(true) as HTMLElement;
// Remove elements that shouldn't be in markdown
const elementsToRemove = [
'.theme-edit-this-page',
'.theme-last-updated',
'.pagination-nav',
'.theme-doc-breadcrumbs',
'.copy-markdown-button',
'nav',
'.theme-doc-toc-mobile',
'.theme-doc-toc-desktop'
];
elementsToRemove.forEach(selector => {
const elements = clone.querySelectorAll(selector);
elements.forEach(el => el.remove());
});
return convertHtmlToMarkdown(clone);
}, []);
const convertHtmlToMarkdown = (element: HTMLElement): string => {
let markdown = '';
const processNode = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || '';
}
if (node.nodeType !== Node.ELEMENT_NODE) {
return '';
}
const el = node as HTMLElement;
const tagName = el.tagName.toLowerCase();
const children = Array.from(el.childNodes).map(processNode).join('');
switch (tagName) {
case 'h1':
return `# ${children}\n\n`;
case 'h2':
return `## ${children}\n\n`;
case 'h3':
return `### ${children}\n\n`;
case 'h4':
return `#### ${children}\n\n`;
case 'h5':
return `##### ${children}\n\n`;
case 'h6':
return `###### ${children}\n\n`;
case 'p':
return `${children}\n\n`;
case 'strong':
case 'b':
return `**${children}**`;
case 'em':
case 'i':
return `*${children}*`;
case 'code':
// Check if it's inline code (not in a pre block)
if (el.parentElement?.tagName.toLowerCase() !== 'pre') {
return `\`${children}\``;
}
return children;
case 'pre': {
// Try to get the language from class
const codeEl = el.querySelector('code');
const className = codeEl?.className || '';
const languageMatch = className.match(/language-(\w+)/);
const language = languageMatch ? languageMatch[1] : '';
return `\`\`\`${language}\n${children}\n\`\`\`\n\n`;
}
case 'a': {
const href = el.getAttribute('href') || '';
return `[${children}](${href})`;
}
case 'ul':
return `${children}\n`;
case 'ol':
return `${children}\n`;
case 'li': {
// Check if parent is ol or ul
const parent = el.parentElement;
if (parent?.tagName.toLowerCase() === 'ol') {
// For ordered lists, we'll use 1. for simplicity
return `1. ${children}\n`;
} else {
return `- ${children}\n`;
}
}
case 'blockquote':
return `> ${children}\n\n`;
case 'hr':
return `---\n\n`;
case 'br':
return '\n';
case 'img': {
const src = el.getAttribute('src') || '';
const alt = el.getAttribute('alt') || '';
return `![${alt}](${src})`;
}
case 'table':
return convertTable(el) + '\n\n';
case 'div':
// Handle admonitions and other special divs
if (el.className.includes('admonition')) {
return handleAdmonition(el);
}
return children;
case 'details': {
const summary = el.querySelector('summary');
const summaryText = summary ? summary.textContent : 'Details';
return `<details>\n<summary>${summaryText}</summary>\n\n${children}\n</details>\n\n`;
}
case 'summary':
return ''; // Handled in details
default:
return children;
}
};
return processNode(element).trim();
};
const convertTable = (table: HTMLElement): string => {
const rows = table.querySelectorAll('tr');
if (rows.length === 0) return '';
let markdown = '';
rows.forEach((row, index) => {
const cells = row.querySelectorAll('td, th');
const rowData = Array.from(cells).map(cell => cell.textContent?.trim() || '').join(' | ');
markdown += `| ${rowData} |\n`;
// Add header separator after first row if it contains th elements
if (index === 0 && row.querySelector('th')) {
const separator = Array.from(cells).map(() => '---').join(' | ');
markdown += `| ${separator} |\n`;
}
});
return markdown;
};
const handleAdmonition = (el: HTMLElement): string => {
const type = el.className.match(/admonition-(\w+)/)?.[1] || 'note';
const title = el.querySelector('.admonition-heading')?.textContent || type;
const content = el.querySelector('.admonition-content')?.textContent || '';
return `:::${type}[${title}]\n${content}\n:::\n\n`;
};
const handleCopy = async () => {
try {
const markdown = extractMarkdownFromPage();
await navigator.clipboard.writeText(markdown);
setCopied(true);
timeoutRef.current = window.setTimeout(() => setCopied(false), 2000);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error in handleCopy (clipboard API): ${message}`);
// Fallback for older browsers
const textarea = document.createElement('textarea');
textarea.value = extractMarkdownFromPage();
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
setCopied(true);
timeoutRef.current = window.setTimeout(() => setCopied(false), 2000);
} catch (fallbackErr) {
const fbMessage = fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr);
console.error(`Error in handleCopy (fallback copy): ${fbMessage}`);
}
document.body.removeChild(textarea);
}
};
useEffect(() => {
return () => {
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
};
}, []);
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsDropdownOpen(false);
}
};
if (isDropdownOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isDropdownOpen]);
const handleViewMarkdown = () => {
const currentUrl = window.location.pathname;
const markdownUrl = currentUrl + '.md';
const win = window.open(markdownUrl, '_blank', 'noopener,noreferrer');
if (win) win.opener = null;
setIsDropdownOpen(false);
};
const handleCopyMarkdown = async () => {
await handleCopy();
setIsDropdownOpen(false);
};
return (
<div className={`copy-markdown-container ${className || ''}`} ref={dropdownRef} style={{ position: 'relative', display: 'inline-block' }}>
<div style={{ display: 'flex', alignItems: 'center' }}>
{/* Main Copy Button */}
<button
onClick={handleCopyMarkdown}
className="copy-markdown-button"
title="Copy page as Markdown"
style={{
background: 'var(--ifm-background-surface-color)',
border: '1px solid var(--ifm-color-emphasis-500)',
borderRadius: '6px 0 0 6px',
padding: '6px 10px',
fontSize: '12px',
fontWeight: 500,
color: 'var(--ifm-color-content-secondary)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '6px',
transition: 'all 0.2s ease',
minWidth: '80px',
justifyContent: 'center',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
opacity: 0.8,
borderRight: 'none',
height: '32px' // Smaller height
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--ifm-color-emphasis-200)';
e.currentTarget.style.borderColor = 'var(--ifm-color-emphasis-400)';
e.currentTarget.style.opacity = '1';
e.currentTarget.style.color = 'var(--ifm-color-content)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--ifm-background-surface-color)';
e.currentTarget.style.borderColor = 'var(--ifm-color-emphasis-500)';
e.currentTarget.style.opacity = '0.8';
e.currentTarget.style.color = 'var(--ifm-color-content-secondary)';
}}
>
{copied ? (
<>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
Copied!
</>
) : (
<>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5,15 L5,5 A2,2 0 0,1 7,3 L17,3"></path>
</svg>
Copy page
</>
)}
</button>
{/* Dropdown Toggle Button */}
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="copy-markdown-dropdown-toggle"
title="More options"
style={{
background: 'var(--ifm-background-surface-color)',
border: '1px solid var(--ifm-color-emphasis-500)',
borderRadius: '0 6px 6px 0',
padding: '6px 8px',
fontSize: '12px',
fontWeight: 500,
color: 'var(--ifm-color-content-secondary)',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transition: 'all 0.2s ease',
boxShadow: '0 1px 3px rgba(0, 0, 0, 0.1)',
opacity: 0.8,
borderLeft: 'none',
height: '32px', // Match the main button height
minWidth: '28px'
}}
onMouseEnter={(e) => {
e.currentTarget.style.background = 'var(--ifm-color-emphasis-200)';
e.currentTarget.style.borderColor = 'var(--ifm-color-emphasis-400)';
e.currentTarget.style.opacity = '1';
e.currentTarget.style.color = 'var(--ifm-color-content)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--ifm-background-surface-color)';
e.currentTarget.style.borderColor = 'var(--ifm-color-emphasis-500)';
e.currentTarget.style.opacity = '0.8';
e.currentTarget.style.color = 'var(--ifm-color-content-secondary)';
}}
>
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
</div>
{/* Dropdown Menu */}
{isDropdownOpen && (
<div
style={{
position: 'absolute',
top: '100%',
right: 0,
marginTop: '4px',
background: 'var(--ifm-background-surface-color)',
border: '1px solid var(--ifm-color-emphasis-500)',
borderRadius: '8px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
zIndex: 1000,
minWidth: '200px',
overflow: 'hidden'
}}
>
<button
onClick={handleCopyMarkdown}
style={{
width: '100%',
padding: '12px 16px',
border: 'none',
background: 'transparent',
color: 'var(--ifm-color-content-secondary)',
fontSize: '13px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'background-color 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--ifm-color-emphasis-100)';
e.currentTarget.style.color = 'var(--ifm-color-content)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--ifm-color-content-secondary)';
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5,15 L5,5 A2,2 0 0,1 7,3 L17,3"></path>
</svg>
Copy as Markdown
</button>
<button
onClick={handleViewMarkdown}
style={{
width: '100%',
padding: '12px 16px',
border: 'none',
background: 'transparent',
color: 'var(--ifm-color-content-secondary)',
fontSize: '13px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
transition: 'background-color 0.2s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = 'var(--ifm-color-emphasis-100)';
e.currentTarget.style.color = 'var(--ifm-color-content)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent';
e.currentTarget.style.color = 'var(--ifm-color-content-secondary)';
}}
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14,2 14,8 20,8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10,9 9,9 8,9"></polyline>
</svg>
View as Markdown
</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,196 @@
/* Container for the thumbnail */
.image-thumbnail-container {
margin: 1.5rem 0;
display: flex;
justify-content: center;
}
/* Thumbnail view - clickable image */
.image-thumbnail {
position: relative;
display: inline-block;
border: 1px solid var(--ifm-color-gray-200);
border-radius: var(--ifm-card-border-radius);
background: var(--ifm-background-surface-color);
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
}
.image-thumbnail:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: var(--ifm-color-primary);
}
[data-theme='dark'] .image-thumbnail {
border-color: var(--ifm-color-gray-700);
}
.image-thumbnail img {
display: block;
max-width: 100%;
height: auto;
}
/* Overlay with expand hint */
.image-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.image-thumbnail:hover .image-overlay {
opacity: 1;
}
.expand-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: white;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.expand-hint svg {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
/* Full-screen modal */
.image-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 2rem;
animation: fadeIn 0.2s ease;
/* Ensure modal centers relative to viewport, not page */
height: 100vh;
width: 100vw;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.image-modal-content {
background: var(--ifm-background-surface-color);
border-radius: var(--ifm-card-border-radius);
width: 92vw;
height: 92vh;
max-width: 95vw;
max-height: 95vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.image-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--ifm-color-gray-200);
background: var(--ifm-color-gray-50);
}
[data-theme='dark'] .image-modal-header {
border-bottom-color: var(--ifm-color-gray-700);
background: var(--ifm-color-gray-800);
}
.image-modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--ifm-color-content);
}
.image-close-btn {
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
border-radius: 0.25rem;
color: var(--ifm-color-content-secondary);
transition: all 0.2s ease;
}
.image-close-btn:hover {
background: var(--ifm-color-gray-200);
color: var(--ifm-color-content);
}
[data-theme='dark'] .image-close-btn:hover {
background: var(--ifm-color-gray-700);
}
.image-modal-body {
padding: 1.5rem;
overflow: auto;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.image-modal-body img {
width: 100%;
height: auto;
max-width: none;
max-height: 85vh;
object-fit: contain;
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.image-modal-backdrop {
padding: 1rem;
}
.image-modal-header {
padding: 0.75rem 1rem;
}
.image-modal-header h3 {
font-size: 1rem;
}
.image-modal-body {
padding: 1rem;
}
.expand-hint span {
font-size: 0.875rem;
}
}

View File

@@ -0,0 +1,161 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import './ExpandableImage.css';
interface ExpandableImageProps {
src: string;
alt: string;
title?: string;
width?: string | number;
videoSrc?: string; // Optional: MP4 video source for better performance
}
const ExpandableImage: React.FC<ExpandableImageProps> = ({
src,
alt,
title = alt,
width = 600,
videoSrc
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isInView, setIsInView] = useState(false);
const thumbnailRef = useRef<HTMLDivElement>(null);
const openModal = () => {
setIsModalOpen(true);
document.body.style.overflow = 'hidden';
};
const closeModal = useCallback(() => {
setIsModalOpen(false);
document.body.style.overflow = 'unset';
}, [setIsModalOpen]);
// Close modal on Escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
if (isModalOpen) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [isModalOpen, closeModal]);
// Cleanup body overflow on unmount
useEffect(() => {
return () => {
document.body.style.overflow = 'unset';
};
}, []);
// Intersection Observer for lazy loading with margin
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsInView(true);
observer.disconnect();
}
});
},
{
rootMargin: '50px', // Start loading 50px before entering viewport
}
);
if (thumbnailRef.current) {
observer.observe(thumbnailRef.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
<>
{/* Thumbnail image/video with click-to-expand */}
<div className="image-thumbnail-container" ref={thumbnailRef}>
<div className="image-thumbnail" onClick={openModal}>
<div className="image-overlay">
<div className="expand-hint">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white" stroke="white" strokeWidth="2">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
<span>Click to expand</span>
</div>
</div>
{isInView && (
<>
{videoSrc ? (
<video
src={videoSrc}
width={width}
autoPlay
loop
muted
playsInline
preload="metadata"
style={{ display: 'block' }}
/>
) : (
<img src={src} alt={alt} width={width} loading="lazy" decoding="async" />
)}
</>
)}
{!isInView && (
<div style={{ width, height: '400px', backgroundColor: 'var(--ifm-color-emphasis-200)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<span style={{ color: 'var(--ifm-color-emphasis-600)' }}>Loading...</span>
</div>
)}
</div>
</div>
{/* Full-screen modal */}
{isModalOpen && (
<div
className="image-modal-backdrop"
onClick={closeModal}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="image-modal-content" onClick={(e) => e.stopPropagation()}>
<div className="image-modal-header">
<h3 id="modal-title">{title}</h3>
<button className="image-close-btn" onClick={closeModal} aria-label="Close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2"/>
</svg>
</button>
</div>
<div className="image-modal-body">
{videoSrc ? (
<video
src={videoSrc}
autoPlay
loop
muted
playsInline
controls
style={{ display: 'block', maxWidth: '100%', maxHeight: '85vh' }}
/>
) : (
<img src={src} alt={alt} loading="lazy" decoding="async" />
)}
</div>
</div>
</div>
)}
</>
);
};
export default ExpandableImage;

View File

@@ -0,0 +1,226 @@
/* Thumbnail view - clickable diagram */
.mermaid-thumbnail {
position: relative;
border: 1px solid var(--ifm-color-gray-200);
border-radius: var(--ifm-card-border-radius);
margin: 1.5rem 0;
background: var(--ifm-background-surface-color);
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
}
.mermaid-thumbnail:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
border-color: var(--ifm-color-primary);
}
[data-theme='dark'] .mermaid-thumbnail {
border-color: var(--ifm-color-gray-700);
}
/* Overlay with expand hint */
.mermaid-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s ease;
z-index: 2;
}
.mermaid-thumbnail:hover .mermaid-overlay {
opacity: 1;
}
.expand-hint {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: white;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.expand-hint svg {
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5));
}
/* Thumbnail diagram styling */
.mermaid-thumbnail .mermaid {
min-height: 350px !important;
padding: 1rem;
margin: 0;
border: none;
background: transparent;
}
.mermaid-thumbnail .mermaid svg {
min-height: 300px;
/* Use full scale to improve readability in thumbnails */
transform: scale(1);
transform-origin: center center;
}
/* Full-screen modal */
.mermaid-modal-backdrop {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
padding: 2rem;
animation: fadeIn 0.2s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.mermaid-modal-content {
background: var(--ifm-background-surface-color);
border-radius: var(--ifm-card-border-radius);
/* Fill most of the viewport so diagrams get plenty of space */
width: 92vw;
height: 92vh;
max-width: 95vw;
max-height: 95vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: scale(0.9) translateY(-20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.mermaid-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--ifm-color-gray-200);
background: var(--ifm-color-gray-50);
}
[data-theme='dark'] .mermaid-modal-header {
border-bottom-color: var(--ifm-color-gray-700);
background: var(--ifm-color-gray-800);
}
.mermaid-modal-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--ifm-color-content);
}
.mermaid-close-btn {
background: none;
border: none;
padding: 0.5rem;
cursor: pointer;
border-radius: 0.25rem;
color: var(--ifm-color-content-secondary);
transition: all 0.2s ease;
}
.mermaid-close-btn:hover {
background: var(--ifm-color-gray-200);
color: var(--ifm-color-content);
}
[data-theme='dark'] .mermaid-close-btn:hover {
background: var(--ifm-color-gray-700);
}
.mermaid-modal-body {
padding: 1.5rem;
overflow: auto; /* let users scroll if diagram exceeds viewport */
flex: 1;
}
/* Large diagram in modal - make it much bigger */
.mermaid-modal-body .mermaid {
width: 100% !important;
min-height: 80vh !important;
padding: 2rem !important;
margin: 0 !important;
border: none !important;
background: transparent !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
}
.mermaid-modal-body .mermaid svg {
/* Let the SVG take the entire width while preserving aspect ratio */
width: 100% !important;
height: auto !important;
max-height: 100% !important;
min-height: 65vh !important;
}
/* Extra large screens - even bigger */
@media (min-width: 1400px) {
.mermaid-modal-body .mermaid svg {
transform: scale(2.5) !important;
min-height: 70vh !important;
}
}
/* Mobile responsiveness */
@media (max-width: 768px) {
.mermaid-modal-backdrop {
padding: 1rem;
}
.mermaid-modal-header {
padding: 0.75rem 1rem;
}
.mermaid-modal-header h3 {
font-size: 1rem;
}
.mermaid-modal-body {
padding: 1rem;
}
.mermaid-modal-body .mermaid {
min-height: 50vh !important;
padding: 1rem;
}
.mermaid-modal-body .mermaid svg {
min-height: 50vh !important;
transform: scale(1.8) !important;
}
.expand-hint span {
font-size: 0.875rem;
}
}

View File

@@ -0,0 +1,89 @@
import React, { useState, useEffect } from 'react';
import './ExpandableMermaid.css';
interface ExpandableMermaidProps {
children: React.ReactNode;
title?: string;
}
const ExpandableMermaid: React.FC<ExpandableMermaidProps> = ({ children, title = "Diagram" }) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const openModal = () => {
setIsModalOpen(true);
document.body.style.overflow = 'hidden'; // Prevent background scrolling
};
const closeModal = () => {
setIsModalOpen(false);
document.body.style.overflow = 'unset';
};
// Close modal on Escape key
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
closeModal();
}
};
if (isModalOpen) {
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('keydown', handleEscape);
};
}, [isModalOpen]);
// Cleanup body overflow on unmount
useEffect(() => {
return () => {
document.body.style.overflow = 'unset';
};
}, []);
return (
<>
{/* Regular diagram with click-to-expand */}
<div className="mermaid-thumbnail" onClick={openModal}>
<div className="mermaid-overlay">
<div className="expand-hint">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white" stroke="white" strokeWidth="2">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
<span>Click to expand</span>
</div>
</div>
{children}
</div>
{/* Full-screen modal */}
{isModalOpen && (
<div
className="mermaid-modal-backdrop"
onClick={closeModal}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="mermaid-modal-content" onClick={(e) => e.stopPropagation()}>
<div className="mermaid-modal-header">
<h3 id="modal-title">{title}</h3>
<button className="mermaid-close-btn" onClick={closeModal} aria-label="Close">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" strokeWidth="2"/>
</svg>
</button>
</div>
<div className="mermaid-modal-body">
{children}
</div>
</div>
</div>
)}
</>
);
};
export default ExpandableMermaid;

View File

@@ -0,0 +1,71 @@
import type {ReactNode} from 'react';
import clsx from 'clsx';
import Heading from '@theme/Heading';
import styles from './styles.module.css';
type FeatureItem = {
title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: ReactNode;
};
const FeatureList: FeatureItem[] = [
{
title: 'Easy to Use',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Docusaurus was designed from the ground up to be easily installed and
used to get your website up and running quickly.
</>
),
},
{
title: 'Focus on What Matters',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
Docusaurus lets you focus on your docs, and we&apos;ll do the chores. Go
ahead and move your docs into the <code>docs</code> directory.
</>
),
},
{
title: 'Powered by React',
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend or customize your website layout by reusing React. Docusaurus can
be extended while reusing the same header and footer.
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>
<div className="text--center padding-horiz--md">
<Heading as="h3">{title}</Heading>
<p>{description}</p>
</div>
</div>
);
}
export default function HomepageFeatures(): ReactNode {
return (
<section className={styles.features}>
<div className="container">
<div className="row">
{FeatureList.map((props, idx) => (
<Feature key={idx} {...props} />
))}
</div>
</div>
</section>
);
}

View File

@@ -0,0 +1,11 @@
.features {
display: flex;
align-items: center;
padding: 2rem 0;
width: 100%;
}
.featureSvg {
height: 200px;
width: 200px;
}

View File

@@ -0,0 +1,87 @@
/* Brand Theme Variables (Infima overrides) */
:root {
color-scheme: light dark;
/* ==============================================
Primary Brand Colors - Turquoise / Teal
============================================== */
--ifm-color-primary: #14b8a6; /* Teal-500 */
--ifm-color-primary-dark: #0d9488; /* Teal-600 */
--ifm-color-primary-darker: #0f766e; /* Teal-700 */
--ifm-color-primary-darkest: #115e59; /* Teal-800 */
--ifm-color-primary-light: #2dd4bf; /* Teal-400 */
--ifm-color-primary-lighter: #5eead4; /* Teal-300 */
--ifm-color-primary-lightest: #99f6e4; /* Teal-200 */
/* ==============================================
Typography - Geist
============================================== */
--ifm-font-family-base: 'Geist', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--ifm-font-family-monospace: 'Geist Mono', 'Monaco', 'Menlo', monospace;
--ifm-font-size-base: 15px; /* Slightly smaller for density */
--ifm-line-height-base: 1.6;
--ifm-heading-font-weight: 600;
/* ==============================================
Layout & Spacing
============================================== */
--ifm-container-width-xl: 1400px;
--ifm-navbar-height: 3.5rem; /* Compact navbar */
/* Modern Rounded Corners */
--ifm-card-border-radius: 10px;
--ifm-button-border-radius: 6px;
--ifm-alert-border-radius: 8px;
--ifm-code-border-radius: 8px;
--ifm-menu-link-border-radius: 6px;
/* ==============================================
Light Mode Defaults
============================================== */
--ifm-background-color: #ffffff;
--ifm-background-surface-color: #ffffff;
--ifm-border-color: #e5e7eb;
/* Transitions */
--ifm-transition-fast: 200ms ease;
--ifm-transition-slow: 400ms ease;
}
/* ==============================================
Dark Mode Overrides - "Rich Dark" Theme
============================================== */
[data-theme='dark'] {
/* Primary Colors */
--ifm-color-primary: #2dd4bf; /* Teal-400 */
--ifm-color-primary-dark: #14b8a6;
--ifm-color-primary-darker: #0d9488;
--ifm-color-primary-darkest: #0f766e;
--ifm-color-primary-light: #5eead4;
--ifm-color-primary-lighter: #99f6e4;
--ifm-color-primary-lightest: #ccfbf1;
/* Backgrounds - Rich Dark (Not Pure Black) */
--ifm-background-color: #0a0a0a !important; /* Deepest Grey - Readable */
--ifm-background-surface-color: #0a0a0a !important; /* Match bg for clean look */
/* Borders - Subtle Separation */
--ifm-border-color: #262626; /* Neutral Dark Grey */
--sidebar-border-color: #262626;
/* Text - Softened White */
--ifm-color-content: #ededed; /* Not harsh pure white */
--ifm-color-content-secondary: #a3a3a3; /* Neutral Grey */
--ifm-menu-color: #a3a3a3;
/* Navbar */
--ifm-navbar-background-color: rgba(10, 10, 10, 0.8);
/* Code Blocks */
--ifm-code-background: #171717; /* Slightly lighter than bg */
--docusaurus-highlighted-code-line-bg: rgba(45, 212, 191, 0.1);
/* Scrollbars */
--ifm-scrollbar-color: #262626;
--ifm-scrollbar-hover-color: #404040;
}

View File

@@ -0,0 +1,855 @@
/**
* Dexto Documentation - Futuristic Dark Theme
* Aesthetic: Clean, Dark, Modern, Sleek, Minimal
*/
/* ==========================================
Variables & Theme Tokens
========================================== */
:root {
--dexto-glass-bg: rgba(255, 255, 255, 0.95);
--dexto-glass-border: rgba(15, 23, 42, 0.08);
--dexto-panel-bg: var(--ifm-background-surface-color);
--dexto-panel-border: var(--ifm-border-color);
}
html[data-theme='dark'] {
--dexto-glass-bg: rgba(8, 8, 8, 0.82);
--dexto-glass-border: rgba(255, 255, 255, 0.06);
}
/* Fallback for dark mode if data-theme attribute isn't set */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--dexto-glass-bg: rgba(8, 8, 8, 0.82);
--dexto-glass-border: rgba(255, 255, 255, 0.06);
}
}
/* ==========================================
Global Reset & Base
========================================== */
html {
font-size: 17px;
/* Scale factor */
font-feature-settings: 'cv02', 'cv03', 'cv04', 'cv11';
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
scroll-behavior: smooth;
}
body {
background-color: var(--ifm-background-color);
}
[data-theme='light'] body {
color: var(--ifm-font-color-base);
background-image:
radial-gradient(circle at 25% 10%, rgba(20, 184, 166, 0.12) 0%, transparent 35%),
radial-gradient(circle at 75% 0%, rgba(59, 130, 246, 0.08) 0%, transparent 30%);
background-attachment: fixed;
}
[data-theme='dark'] body {
background-image:
radial-gradient(circle at 50% 0%, rgba(45, 212, 191, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 10%, rgba(124, 58, 237, 0.03) 0%, transparent 30%);
background-attachment: fixed;
}
/* ==========================================
Navbar - Glassmorphism
========================================== */
.navbar {
background: var(--dexto-glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--dexto-glass-border);
transition: all 0.3s ease;
}
.navbar__brand {
font-weight: 700;
letter-spacing: -0.02em;
}
.navbar__item {
font-size: 0.9rem;
font-weight: 500;
color: var(--ifm-color-content-secondary);
border-radius: 6px;
padding: 0.5rem 0.75rem;
transition: all 0.2s ease;
}
.navbar__link:hover,
.navbar__link--active {
color: var(--ifm-color-content);
background: rgba(255, 255, 255, 0.03);
}
.navbar__link--active {
color: var(--ifm-color-primary);
}
.navbar__link:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
/* ==========================================
Sidebar - Minimal & Clean
========================================== */
.theme-doc-sidebar-container {
background: transparent !important;
border-right: 1px solid var(--ifm-border-color);
}
/* Ensure mobile navbar sidebar overlays main content */
@media (max-width: 996px) {
#__docusaurus > nav > div.theme-layout-navbar-sidebar.navbar-sidebar {
position: fixed;
inset: 0;
z-index: 11000;
width: 100vw;
height: 100vh;
overflow-y: auto;
background: var(--ifm-background-surface-color);
}
#__docusaurus > nav > div.theme-layout-navbar-sidebar.navbar-sidebar .navbar-sidebar__backdrop {
z-index: 10999;
}
}
.menu {
padding: 2rem 1.25rem 2.5rem 1.25rem;
background: transparent;
}
.menu__list {
padding-right: 0.5rem;
}
/* Sidebar Toggle Button */
[class*="collapseSidebarButton"] {
display: inline-flex !important;
align-items: center;
justify-content: center;
width: 2.25rem;
height: 2.25rem;
position: sticky;
top: calc(var(--ifm-navbar-height) + 0.75rem);
margin-left: auto;
margin-right: 0.35rem;
border-radius: 9999px;
border: 1px solid var(--ifm-border-color);
background: var(--dexto-glass-bg);
box-shadow: 0 10px 26px rgba(0, 0, 0, 0.08);
color: var(--ifm-color-content);
transition: transform 0.2s ease, border-color 0.2s ease, background 0.2s ease, color 0.2s ease;
z-index: 3;
}
@media (max-width: 996px) {
[class*="collapseSidebarButton"] {
display: none !important;
}
}
/* Mobile sidebar overlay */
[class*="collapseSidebarButton"]::before {
content: "";
display: block;
width: 1rem;
height: 1rem;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='15 18 9 12 15 6'%3E%3C/polyline%3E%3C/svg%3E") center / contain no-repeat;
background-color: currentColor;
}
[class*="collapseSidebarButton"] svg,
[class*="collapseSidebarButton"] svg * {
display: none !important;
}
[class*="collapseSidebarButton"]:hover {
transform: translateX(-2px);
border-color: var(--ifm-color-primary);
background: rgba(45, 212, 191, 0.12);
color: var(--ifm-color-primary);
}
/* Sidebar Icons & Alignment */
.menu__list-item-collapsible .menu__link {
display: flex;
align-items: center;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ifm-color-content);
margin: 0;
padding: 0.4rem 0.65rem;
border-radius: 6px;
}
.menu__list .menu__link {
padding: 0.4rem 0.65rem !important;
margin: 0.12rem 0;
border-radius: 6px;
}
.menu__link.menu__link--active {
color: var(--ifm-color-primary) !important;
}
.menu__link:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
}
.menu__list-item-collapsible .menu__link::before,
.menu__list .menu__link::before {
content: none;
}
/* Icons for Sidebar Categories */
.sidebar-icon-getting-started>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-guides>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-mcp>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-tutorials>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-examples>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-concepts>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-community>.menu__list-item-collapsible>.menu__link::before,
.sidebar-icon-architecture>.menu__list-item-collapsible>.menu__link::before {
content: "";
display: inline-block;
width: 1rem;
height: 1rem;
margin-right: 0.5rem;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
opacity: 0.9;
}
/* Icon SVGs (Teal #2dd4bf) */
.sidebar-icon-getting-started>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 16.5c-1.5 1.25-2 5-2 5s3.75-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z'%3E%3C/path%3E%3Cpath d='m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z'%3E%3C/path%3E%3Cpath d='M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0'%3E%3C/path%3E%3C/svg%3E");
}
.sidebar-icon-guides>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m16 6 4 14'%3E%3C/path%3E%3Cpath d='M12 6v14'%3E%3C/path%3E%3Cpath d='M8 8v12'%3E%3C/path%3E%3Cpath d='M4 4v16'%3E%3C/path%3E%3C/svg%3E");
}
.sidebar-icon-mcp>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='18' cy='5' r='3'%3E%3C/circle%3E%3Ccircle cx='6' cy='12' r='3'%3E%3C/circle%3E%3Ccircle cx='18' cy='19' r='3'%3E%3C/circle%3E%3Cline x1='8.59' x2='15.42' y1='13.51' y2='17.49'%3E%3C/line%3E%3Cline x1='15.41' x2='8.59' y1='6.51' y2='10.49'%3E%3C/line%3E%3C/svg%3E");
}
.sidebar-icon-tutorials>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21.42 10.922a1 1 0 0 0-.019-1.838L12.83 5.18a2 2 0 0 0-1.66 0L2.6 9.08a1 1 0 0 0 0 1.832l8.57 3.908a2 2 0 0 0 1.66 0z'%3E%3C/path%3E%3Cpath d='M22 10v6'%3E%3C/path%3E%3Cpath d='M6 12.5V16a6 3 0 0 0 12 0v-3.5'%3E%3C/path%3E%3C/svg%3E");
}
.sidebar-icon-examples>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='16 18 22 12 16 6'%3E%3C/polyline%3E%3Cpolyline points='8 6 2 12 8 18'%3E%3C/polyline%3E%3C/svg%3E");
}
.sidebar-icon-concepts>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5'%3E%3C/path%3E%3Cpath d='M9 18h6'%3E%3C/path%3E%3Cpath d='M10 22h4'%3E%3C/path%3E%3C/svg%3E");
}
.sidebar-icon-community>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2'%3E%3C/path%3E%3Ccircle cx='9' cy='7' r='4'%3E%3C/circle%3E%3Cpath d='M22 21v-2a4 4 0 0 0-3-3.87'%3E%3C/path%3E%3Cpath d='M16 3.13a4 4 0 0 1 0 7.75'%3E%3C/path%3E%3C/svg%3E");
}
.sidebar-icon-architecture>.menu__list-item-collapsible>.menu__link::before {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%232dd4bf' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z'%3E%3C/path%3E%3Cpath d='m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65'%3E%3C/path%3E%3Cpath d='m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65'%3E%3C/path%3E%3C/svg%3E");
}
.menu__link {
font-size: 0.9rem;
color: var(--ifm-menu-color);
border-radius: 6px;
padding: 0.4rem 0.65rem;
transition: all 0.15s ease;
border-left: 1px solid transparent;
line-height: 1.35;
}
.menu__link:hover {
color: var(--ifm-color-content);
background: rgba(255, 255, 255, 0.03);
}
.menu__link--active {
color: var(--ifm-color-primary);
background: rgba(45, 212, 191, 0.05);
font-weight: 500;
}
/* Sub-items indentation & Continuous Border */
.menu__list .menu__list {
border-left: 1px solid var(--ifm-border-color);
margin-left: 1.5rem;
padding-left: 0.5rem;
}
.menu__list .menu__list .menu__link {
padding-left: 1rem;
border-left: none;
border-radius: 4px;
}
.menu__list .menu__list .menu__link:hover {
border-left-color: transparent;
}
.menu__list .menu__list .menu__link--active {
border-left-color: transparent;
padding-left: 1rem;
/* Keep consistent */
}
/* ==========================================
Layout Consistency - Docs
========================================== */
.docMainContainer .container,
.theme-doc-index-page .container {
max-width: 1200px !important;
padding: 2.5rem 3rem;
}
@media (max-width: 996px) {
.docMainContainer .container,
.theme-doc-index-page .container {
padding: 2rem 1.5rem;
}
}
@media (max-width: 768px) {
.docMainContainer .container,
.theme-doc-index-page .container {
padding: 1.5rem 1.25rem;
}
}
.theme-doc-content,
.theme-doc-index-page {
background: transparent;
}
.theme-doc-index-page .row {
row-gap: 1.5rem;
}
.theme-doc-index-page .card {
background: var(--ifm-background-surface-color);
border: 1px solid var(--ifm-border-color);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.08);
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
}
.theme-doc-index-page .card:hover {
transform: translateY(-4px);
border-color: var(--ifm-color-primary);
box-shadow: 0 20px 45px rgba(13, 148, 136, 0.15);
}
.theme-doc-index-page .card__header .card__title,
.theme-doc-index-page .card__body {
color: var(--ifm-color-content);
}
/* Redoc minimal rounding */
.redocusaurus .sc-buTqWO,
.redocusaurus .api-content button {
border-radius: 12px !important;
}
.redocusaurus .http-verb,
.redocusaurus .operation-type {
border-radius: 999px !important;
}
.redocusaurus select {
border-radius: 10px !important;
}
.redocusaurus .sc-xKhEK {
border-radius: 12px !important;
}
.redocusaurus .sc-hjsuWn {
background: var(--ifm-background-surface-color) !important;
border: none !important;
border-radius: 12px !important;
}
.redocusaurus .redoc-wrap {
background: var(--ifm-background-color) !important;
color: var(--ifm-color-content) !important;
}
.redocusaurus .api-content,
.redocusaurus .menu-content {
background: var(--ifm-background-surface-color) !important;
border: 1px solid var(--ifm-border-color) !important;
border-radius: 0 !important;
}
.redocusaurus .api-content .sc-dTvVRJ:not(:first-of-type) {
position: relative;
padding: 2.25rem 4rem 1.25rem 4rem;
margin-top: 1.25rem;
}
.redocusaurus .api-content .sc-dTvVRJ:not(:first-of-type)::before {
content: '';
position: absolute;
top: 0;
left: 5.75rem;
right: 5rem;
border-top: 1px solid var(--ifm-border-color);
}
.redocusaurus .api-content .response,
.redocusaurus .api-content .code-samples,
.redocusaurus .api-content .try-out,
.redocusaurus .api-content .tab-panel,
.redocusaurus .api-content .example-panel {
background: #0e1116 !important;
border: 1px solid #1a1f26 !important;
border-radius: 12px !important;
}
.redocusaurus .react-tabs__tab-panel,
.redocusaurus .sc-bSFBcf,
.redocusaurus .sc-eVqvcJ {
border-radius: 12px !important;
}
.menu__list-item-collapsible {
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
gap: 0.25rem;
margin: 0.12rem 0;
padding: 0.12rem 0.25rem;
border-radius: 8px;
background: transparent;
}
.menu__list-item-collapsible>.menu__link {
padding-right: 0.35rem;
}
.theme-doc-sidebar-container {
background: linear-gradient(145deg, rgba(45, 212, 191, 0.06), rgba(37, 99, 235, 0.04));
border-right: 1px solid var(--ifm-border-color);
backdrop-filter: blur(6px);
}
.menu__caret {
position: relative;
height: 1.4rem;
width: 1.4rem;
display: grid;
place-items: center;
margin-left: 0.15rem;
border-radius: 6px;
color: var(--ifm-color-content-secondary);
transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease;
border: none !important;
background: none !important;
box-shadow: none !important;
overflow: visible;
}
.menu__caret::after {
content: "" !important;
display: none !important;
}
.menu__caret::before {
content: "";
width: 0.8rem;
height: 0.8rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
background-size: contain;
color: currentColor;
mask: none;
display: block;
transform: rotate(-90deg);
transition: transform 0.15s ease;
}
.menu__list-item-collapsible:hover .menu__caret {
color: var(--ifm-color-primary);
background: rgba(255, 255, 255, 0.04);
}
.menu__caret[aria-expanded="true"]::before {
transform: rotate(0deg);
}
.menu__list-item-collapsible:hover,
.menu__list-item-collapsible--active {
background: rgba(255, 255, 255, 0.02);
}
.menu__list-item-collapsible--active .menu__caret {
color: var(--ifm-color-primary);
}
/* ==========================================
Content & Typography
========================================== */
.main-wrapper {
max-width: 100%;
width: 100%;
background: var(--ifm-background-color);
}
.container {
max-width: 1200px !important;
margin: 0 auto;
padding: 0 2rem;
background: transparent;
}
/* Clean content area with proper spacing */
.theme-doc-markdown {
max-width: 100%;
padding: 2rem 3rem;
}
/* Responsive padding for smaller screens */
@media (max-width: 996px) {
.theme-doc-markdown {
padding: 1.5rem 2rem;
}
}
@media (max-width: 768px) {
.theme-doc-markdown {
padding: 1rem 1.5rem;
}
}
.markdown h1 {
font-size: 2.5rem;
letter-spacing: -0.03em;
margin-bottom: 1.5rem;
color: var(--ifm-color-primary);
text-shadow: none;
}
.title_kItE {
color: var(--ifm-color-primary);
}
.markdown h2 {
font-size: 1.75rem;
letter-spacing: -0.01em;
margin-top: 3rem;
border-bottom: none;
color: var(--ifm-color-content);
text-shadow: none;
}
.markdown h3 {
font-size: 1.25rem;
margin-top: 2rem;
color: var(--ifm-color-content-secondary);
letter-spacing: -0.005em;
text-shadow: none;
}
.markdown p {
color: var(--ifm-font-color-base);
line-height: 1.75;
margin-bottom: 1.5rem;
}
.markdown a {
text-decoration: none;
border-bottom: 1px solid rgba(45, 212, 191, 0.3);
transition: border-color 0.2s;
}
.markdown a:hover {
border-bottom-color: var(--ifm-color-primary);
color: var(--ifm-color-primary-light);
}
.markdown a:focus-visible {
outline: 2px solid var(--ifm-color-primary);
outline-offset: 2px;
border-bottom-color: var(--ifm-color-primary);
color: var(--ifm-color-primary-light);
}
/* Responsive Copy Markdown Button Layout */
.mdx-content-wrapper {
position: relative;
}
.copy-markdown-header {
display: flex;
justify-content: flex-end;
align-items: flex-start;
margin-bottom: 1rem;
min-height: 40px;
position: relative;
z-index: 10;
}
/* On larger screens, position button absolutely to overlay nicely */
@media (min-width: 1200px) {
.copy-markdown-header {
position: absolute;
top: 1rem;
right: 1rem;
margin-bottom: 0;
width: auto;
}
/* Add right margin to first heading to prevent overlap */
.mdx-content-wrapper h1:first-of-type {
margin-right: 200px;
}
}
/* On medium screens, position button absolutely like large screens */
@media (max-width: 1199px) and (min-width: 769px) {
.copy-markdown-header {
position: absolute;
top: 1rem;
right: 1rem;
margin-bottom: 0;
width: auto;
}
/* Reduce margin for medium screens */
.mdx-content-wrapper h1:first-of-type {
margin-right: 180px;
}
}
/* On small screens, hide copy button */
@media (max-width: 768px) {
.copy-markdown-header {
display: none;
}
/* Remove margin on small screens */
.mdx-content-wrapper h1:first-of-type {
margin-right: 0;
}
}
/* Handle very long titles that would still cause overlap */
@container (max-width: 800px) {
.mdx-content-wrapper h1:first-of-type {
margin-right: 0;
}
.copy-markdown-header {
position: static;
justify-content: flex-end;
margin-bottom: 1rem;
}
}
/* ==========================================
Code Blocks & Pre
========================================== */
.prism-code {
border: 1px solid var(--ifm-border-color);
border-radius: 8px;
}
[data-theme='light'] .prism-code {
background-color: #fafafa !important;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
}
[data-theme='dark'] .prism-code {
background-color: #0f0f0f !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
}
code {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 4px;
padding: 0.1rem 0.3rem;
font-size: 0.85em;
color: var(--ifm-color-primary-light);
}
[data-theme='light'] code {
background: rgba(15, 23, 42, 0.06);
border-color: rgba(15, 23, 42, 0.1);
color: #0f172a;
}
[data-theme='dark'] code {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(255, 255, 255, 0.05);
color: var(--ifm-color-primary-light);
}
/* ==========================================
Admonitions (Alerts)
========================================== */
.alert {
background: transparent;
border: 1px solid;
border-left-width: 4px;
border-radius: 6px;
padding: 1rem 1.25rem;
}
.alert--info {
border-color: rgba(59, 130, 246, 0.2);
border-left-color: #3b82f6;
background: linear-gradient(90deg, rgba(59, 130, 246, 0.05) 0%, transparent 100%);
}
.alert--success {
border-color: rgba(16, 185, 129, 0.2);
border-left-color: #10b981;
background: linear-gradient(90deg, rgba(16, 185, 129, 0.05) 0%, transparent 100%);
}
.alert--warning {
border-color: rgba(245, 158, 11, 0.2);
border-left-color: #f59e0b;
background: linear-gradient(90deg, rgba(245, 158, 11, 0.05) 0%, transparent 100%);
}
.alert--danger {
border-color: rgba(239, 68, 68, 0.2);
border-left-color: #ef4444;
background: linear-gradient(90deg, rgba(239, 68, 68, 0.05) 0%, transparent 100%);
}
/* ==========================================
Cards & UI Components
========================================== */
.card {
background: var(--ifm-background-surface-color);
border: 1px solid var(--ifm-border-color);
border-radius: 12px;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.card:hover {
border-color: var(--ifm-color-primary);
transform: translateY(-4px);
box-shadow: 0 10px 30px -10px rgba(45, 212, 191, 0.15);
}
/* ==========================================
API Reference
========================================== */
.api-endpoint-header {
background: rgba(255, 255, 255, 0.02);
border: 1px solid var(--ifm-border-color);
border-radius: 8px;
padding: 0.75rem 1rem;
font-family: var(--ifm-font-family-monospace);
}
.api-method {
padding: 0.25rem 0.6rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
}
/* Enhanced Pagination */
.pagination-nav {
margin-top: 3rem;
padding-top: 2rem;
border-top: 1px solid var(--ifm-border-color);
}
.pagination-nav__link {
backdrop-filter: blur(10px);
border-radius: 0.75rem;
padding: 1.5rem;
transition: all var(--ifm-transition-fast);
}
[data-theme='light'] .pagination-nav__link {
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(15, 23, 42, 0.12);
}
[data-theme='dark'] .pagination-nav__link {
background: rgba(37, 37, 38, 0.4);
border: 1px solid rgba(45, 212, 191, 0.2);
}
.pagination-nav__link:hover {
border-color: var(--ifm-color-primary);
transform: translateY(-2px);
}
[data-theme='light'] .pagination-nav__link:hover {
box-shadow: 0 8px 25px rgba(20, 184, 166, 0.15);
background: rgba(255, 255, 255, 1);
}
[data-theme='dark'] .pagination-nav__link:hover {
box-shadow: 0 8px 25px rgba(45, 212, 191, 0.2);
}
.pagination-nav__label {
color: var(--ifm-color-primary);
font-weight: 600;
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.pagination-nav__sublabel {
color: var(--ifm-color-content);
font-weight: 500;
font-size: 1rem;
margin-top: 0.5rem;
}
/* ==========================================
Footer
========================================== */
.footer {
border-top: 1px solid var(--ifm-border-color);
}
[data-theme='light'] .footer {
background: #f8f9fa;
}
[data-theme='dark'] .footer {
background: #000000;
}
.footer__title {
color: var(--ifm-color-content);
font-weight: 600;
}
.footer__link-item {
color: var(--ifm-color-content-secondary);
font-size: 0.9rem;
}
.footer__link-item:hover {
color: var(--ifm-color-primary);
}

View File

@@ -0,0 +1,23 @@
/**
* CSS files with the .module.css suffix will be treated as CSS modules
* and scoped locally.
*/
.heroBanner {
padding: 4rem 0;
text-align: center;
position: relative;
overflow: hidden;
}
@media screen and (max-width: 996px) {
.heroBanner {
padding: 2rem;
}
}
.buttons {
display: flex;
align-items: center;
justify-content: center;
}

View File

@@ -0,0 +1,6 @@
import React from 'react';
import {Redirect} from '@docusaurus/router';
export default function Home(): React.ReactElement {
return <Redirect to="docs/category/getting-started" />;
}

View File

@@ -0,0 +1,183 @@
import * as path from 'path';
import * as fs from 'fs';
import type { Plugin, LoadContext } from '@docusaurus/types';
interface MarkdownRoutePluginOptions {
enabled?: boolean;
}
export default function markdownRoutePlugin(
context: LoadContext,
_options: MarkdownRoutePluginOptions = {}
): Plugin {
// Consolidate context destructuring without stray diff markers
const { siteDir, baseUrl = '/' } = context;
const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
const DOCS_PREFIX = `${normalizedBase}/docs/`;
const API_PREFIX = `${normalizedBase}/api/`;
// Helper function to find markdown file (try .md then .mdx)
function findMarkdownFile(basePath: string): string | null {
const candidates = [
`${basePath}.md`,
`${basePath}.mdx`,
path.join(basePath, 'index.md'),
path.join(basePath, 'index.mdx'),
path.join(basePath, 'README.md'),
path.join(basePath, 'README.mdx'),
];
return candidates.find((p) => fs.existsSync(p)) ?? null;
}
// Helper function to copy markdown files to build folder for production
function copyMarkdownFiles(buildDir: string): void {
// Copy docs markdown files
const docsDir = path.join(siteDir, 'docs');
const buildDocsDir = path.join(buildDir, 'docs');
if (fs.existsSync(docsDir)) {
copyDirectoryMarkdown(docsDir, buildDocsDir, 'docs');
}
// Copy api markdown files
const apiDir = path.join(siteDir, 'api');
const buildApiDir = path.join(buildDir, 'api');
if (fs.existsSync(apiDir)) {
copyDirectoryMarkdown(apiDir, buildApiDir, 'api');
}
}
function copyDirectoryMarkdown(
sourceDir: string,
targetDir: string,
prefix: string = ''
): void {
if (!fs.existsSync(sourceDir)) return;
// Create target directory
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
const items = fs.readdirSync(sourceDir);
for (const item of items) {
const sourcePath = path.join(sourceDir, item);
const targetPath = path.join(targetDir, item);
const stat = fs.statSync(sourcePath);
if (stat.isDirectory()) {
// Recursively copy subdirectories
copyDirectoryMarkdown(sourcePath, targetPath, prefix);
} else if (item.endsWith('.md') || item.endsWith('.mdx')) {
// Copy markdown files
try {
fs.copyFileSync(sourcePath, targetPath);
console.log(
`✅ Copied ${prefix}/${path.relative(path.join(siteDir, prefix), sourcePath)} to static folder`
);
} catch (error) {
console.error(`❌ Error copying ${sourcePath}:`, error);
}
}
}
}
return {
name: 'markdown-route-plugin',
// Copy markdown files during build for production
async postBuild({ outDir }) {
console.log('📄 Copying markdown files to build folder for production...');
copyMarkdownFiles(outDir);
},
configureWebpack(_config: any, isServer: boolean): any {
// Only add devServer middleware for client-side development builds
if (isServer || process.env.NODE_ENV === 'production') {
return {};
}
return {
devServer: {
setupMiddlewares: (middlewares: any, devServer: any) => {
if (!devServer) {
throw new Error('webpack-dev-server is not defined');
}
console.log('🔧 Setting up markdown route middleware for development');
// Add middleware at the beginning to intercept before other routes
middlewares.unshift({
name: 'markdown-route-middleware',
middleware: (req: any, res: any, next: any) => {
// Only handle .md requests
if (!req.path.endsWith('.md')) {
return next();
}
const requestPath = req.path;
console.log(`📄 Markdown request: ${requestPath}`);
// Remove .md extension to get the original route
const originalPath = requestPath.replace(/\.md$/, '');
// Map the route to the actual markdown file
let filePath = null;
// Replace hard-coded docs/api blocks with unified, safe resolution
const docsRoot = path.join(siteDir, 'docs');
const apiRoot = path.join(siteDir, 'api');
const matchPrefix = (p: string, prefix: string) =>
p.startsWith(prefix) ? p.slice(prefix.length) : null;
let relativePath = matchPrefix(originalPath, DOCS_PREFIX);
let root = docsRoot;
if (relativePath == null) {
relativePath = matchPrefix(originalPath, API_PREFIX);
root = apiRoot;
}
if (relativePath != null) {
// Normalize and ensure the resolved path stays within root
const resolvedBase = path.resolve(root, relativePath);
const rootResolved = path.resolve(root);
if (
resolvedBase !== rootResolved &&
!resolvedBase.startsWith(rootResolved + path.sep)
) {
res.status(400).send('Invalid path');
return;
}
filePath = findMarkdownFile(resolvedBase);
}
if (filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.send(content);
console.log(
`✅ Served markdown: ${path.relative(siteDir, filePath)}`
);
} catch (error) {
console.error(
`❌ Error reading markdown file ${filePath}:`,
error
);
res.status(500).send('Error reading markdown file');
}
} else {
console.log(`❌ Markdown file not found for: ${originalPath}`);
res.status(404).send('Markdown file not found');
}
},
});
return middlewares;
},
},
};
},
};
}

View File

@@ -0,0 +1,20 @@
import React from 'react';
import MDXContent from '@theme-original/MDXContent';
import CopyMarkdown from '../../components/CopyMarkdown';
import { useLocation } from '@docusaurus/router';
export default function MDXContentWrapper(props: React.ComponentProps<typeof MDXContent>) {
const location = useLocation();
const isBlogPost = location.pathname.startsWith('/blog/');
return (
<div className="mdx-content-wrapper">
{!isBlogPost && (
<div className="copy-markdown-header">
<CopyMarkdown />
</div>
)}
<MDXContent {...props} />
</div>
);
}

View File

@@ -0,0 +1,27 @@
import React, {type ReactNode} from 'react';
import {useColorMode, useThemeConfig} from '@docusaurus/theme-common';
import ColorModeToggle from '@theme/ColorModeToggle';
import type {Props} from '@theme/Navbar/ColorModeToggle';
import styles from './styles.module.css';
export default function NavbarColorModeToggle({className}: Props): ReactNode {
const navbarStyle = useThemeConfig().navbar.style;
const {disableSwitch, respectPrefersColorScheme} = useThemeConfig().colorMode;
const {colorModeChoice, setColorMode} = useColorMode();
if (disableSwitch) {
return null;
}
return (
<ColorModeToggle
className={className}
buttonClassName={
navbarStyle === 'dark' ? styles.darkNavbarColorModeToggle : undefined
}
respectPrefersColorScheme={respectPrefersColorScheme}
value={colorModeChoice}
onChange={setColorMode}
/>
);
}

View File

@@ -0,0 +1,4 @@
.darkNavbarColorModeToggle:hover,
.darkNavbarColorModeToggle:focus-visible {
background: var(--ifm-color-gray-800);
}

View File

@@ -0,0 +1,107 @@
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import {
useThemeConfig,
ErrorCauseBoundary,
ThemeClassNames,
} from '@docusaurus/theme-common';
import {
splitNavbarItems,
useNavbarMobileSidebar,
} from '@docusaurus/theme-common/internal';
import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem';
import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle';
import SearchBar from '@theme/SearchBar';
import NavbarMobileSidebarToggle from '@theme/Navbar/MobileSidebar/Toggle';
import NavbarLogo from '@theme/Navbar/Logo';
import NavbarSearch from '@theme/Navbar/Search';
import styles from './styles.module.css';
function useNavbarItems() {
// TODO temporary casting until ThemeConfig type is improved
return useThemeConfig().navbar.items as NavbarItemConfig[];
}
function NavbarItems({items}: {items: NavbarItemConfig[]}): ReactNode {
return (
<>
{items.map((item, i) => (
<ErrorCauseBoundary
key={i}
onError={(error) =>
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},
)
}>
<NavbarItem {...item} />
</ErrorCauseBoundary>
))}
</>
);
}
function NavbarContentLayout({
left,
right,
}: {
left: ReactNode;
right: ReactNode;
}) {
return (
<div className="navbar__inner">
<div
className={clsx(
ThemeClassNames.layout.navbar.containerLeft,
'navbar__items',
)}>
{left}
</div>
<div
className={clsx(
ThemeClassNames.layout.navbar.containerRight,
'navbar__items navbar__items--right',
)}>
{right}
</div>
</div>
);
}
export default function NavbarContent(): ReactNode {
const mobileSidebar = useNavbarMobileSidebar();
const items = useNavbarItems();
const [leftItems, rightItems] = splitNavbarItems(items);
const searchBarItem = items.find((item) => item.type === 'search');
return (
<NavbarContentLayout
left={
// TODO stop hardcoding items?
<>
{!mobileSidebar.disabled && <NavbarMobileSidebarToggle />}
<NavbarLogo />
<NavbarItems items={leftItems} />
</>
}
right={
// TODO stop hardcoding items?
// Ask the user to add the respective navbar items => more flexible
<>
<NavbarItems items={rightItems} />
<NavbarColorModeToggle className={styles.colorModeToggle} />
{!searchBarItem && (
<NavbarSearch>
<SearchBar />
</NavbarSearch>
)}
</>
}
/>
);
}

View File

@@ -0,0 +1,16 @@
/*
Hide color mode toggle in small viewports
*/
@media (max-width: 996px) {
.colorModeToggle {
display: none;
}
}
/*
Restore some Infima style that broke with CSS Cascade Layers
See https://github.com/facebook/docusaurus/pull/11142
*/
:global(.navbar__items--right) > :last-child {
padding-right: 0;
}

View File

@@ -0,0 +1,57 @@
import React, {type ComponentProps, type ReactNode} from 'react';
import clsx from 'clsx';
import {ThemeClassNames, useThemeConfig} from '@docusaurus/theme-common';
import {
useHideableNavbar,
useNavbarMobileSidebar,
} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import NavbarMobileSidebar from '@theme/Navbar/MobileSidebar';
import type {Props} from '@theme/Navbar/Layout';
import styles from './styles.module.css';
function NavbarBackdrop(props: ComponentProps<'div'>) {
return (
<div
role="presentation"
{...props}
className={clsx('navbar-sidebar__backdrop', props.className)}
/>
);
}
export default function NavbarLayout({children}: Props): ReactNode {
const {
navbar: {hideOnScroll, style},
} = useThemeConfig();
const mobileSidebar = useNavbarMobileSidebar();
const {navbarRef, isNavbarVisible} = useHideableNavbar(hideOnScroll);
return (
<nav
ref={navbarRef}
aria-label={translate({
id: 'theme.NavBar.navAriaLabel',
message: 'Main',
description: 'The ARIA label for the main navigation',
})}
className={clsx(
ThemeClassNames.layout.navbar.container,
'navbar',
'navbar--fixed-top',
hideOnScroll && [
styles.navbarHideable,
!isNavbarVisible && styles.navbarHidden,
],
{
'navbar--dark': style === 'dark',
'navbar--primary': style === 'primary',
'navbar-sidebar--show': mobileSidebar.shown,
},
)}>
{children}
<NavbarBackdrop onClick={mobileSidebar.toggle} />
<NavbarMobileSidebar />
</nav>
);
}

View File

@@ -0,0 +1,7 @@
.navbarHideable {
transition: transform var(--ifm-transition-fast);
}
.navbarHidden {
transform: translate3d(0, calc(-100% - 2px), 0);
}

View File

@@ -0,0 +1,12 @@
import React, {type ReactNode} from 'react';
import Logo from '@theme/Logo';
export default function NavbarLogo(): ReactNode {
return (
<Logo
className="navbar__brand"
imageClassName="navbar__logo"
titleClassName="navbar__title text--truncate"
/>
);
}

View File

@@ -0,0 +1,33 @@
import React, {type ReactNode} from 'react';
import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import NavbarColorModeToggle from '@theme/Navbar/ColorModeToggle';
import IconClose from '@theme/Icon/Close';
import NavbarLogo from '@theme/Navbar/Logo';
function CloseButton() {
const mobileSidebar = useNavbarMobileSidebar();
return (
<button
type="button"
aria-label={translate({
id: 'theme.docs.sidebar.closeSidebarButtonAriaLabel',
message: 'Close navigation bar',
description: 'The ARIA label for close button of mobile sidebar',
})}
className="clean-btn navbar-sidebar__close"
onClick={() => mobileSidebar.toggle()}>
<IconClose color="var(--ifm-color-emphasis-600)" />
</button>
);
}
export default function NavbarMobileSidebarHeader(): ReactNode {
return (
<div className="navbar-sidebar__brand">
<NavbarLogo />
<NavbarColorModeToggle className="margin-right--md" />
<CloseButton />
</div>
);
}

View File

@@ -0,0 +1,94 @@
import React, {version, useEffect, useRef, type ReactNode, type HTMLAttributes} from 'react';
import clsx from 'clsx';
import {useNavbarSecondaryMenu} from '@docusaurus/theme-common/internal';
import {ThemeClassNames} from '@docusaurus/theme-common';
import type {Props} from '@theme/Navbar/MobileSidebar/Layout';
// TODO Docusaurus v4: remove temporary inert workaround
// See https://github.com/facebook/react/issues/17157
// See https://github.com/radix-ui/themes/pull/509
function inertProps(inert: boolean): HTMLAttributes<HTMLDivElement> {
const majorVersion = (() => {
if (typeof version !== 'string') return undefined;
const first = version.split('.')[0];
const parsed = Number.parseInt(first ?? '', 10);
return Number.isNaN(parsed) ? undefined : parsed;
})();
const isBeforeReact19 = majorVersion !== undefined && majorVersion < 19;
if (isBeforeReact19) {
// For React <19, do not set the prop here; we'll set/remove the attribute via ref effect
return {};
}
return inert ? { inert } : {};
}
function NavbarMobileSidebarPanel({
children,
inert,
}: {
children: ReactNode;
inert: boolean;
}) {
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const majorVersion = (() => {
if (typeof version !== 'string') return undefined;
const first = version.split('.')[0];
const parsed = Number.parseInt(first ?? '', 10);
return Number.isNaN(parsed) ? undefined : parsed;
})();
const isBeforeReact19 = majorVersion !== undefined && majorVersion < 19;
if (!isBeforeReact19) {
return;
}
const node = panelRef.current;
if (!node) {
return;
}
if (inert) {
node.setAttribute('inert', '');
} else {
node.removeAttribute('inert');
}
}, [inert]);
return (
<div
ref={panelRef}
className={clsx(
ThemeClassNames.layout.navbar.mobileSidebar.panel,
'navbar-sidebar__item menu',
)}
{...inertProps(inert)}>
{children}
</div>
);
}
export default function NavbarMobileSidebarLayout({
header,
primaryMenu,
secondaryMenu,
}: Props): ReactNode {
const {shown: secondaryMenuShown} = useNavbarSecondaryMenu();
return (
<div
className={clsx(
ThemeClassNames.layout.navbar.mobileSidebar.container,
'navbar-sidebar',
)}>
{header}
<div
className={clsx('navbar-sidebar__items', {
'navbar-sidebar__items--show-secondary': secondaryMenuShown,
})}>
<NavbarMobileSidebarPanel inert={secondaryMenuShown}>
{primaryMenu}
</NavbarMobileSidebarPanel>
<NavbarMobileSidebarPanel inert={!secondaryMenuShown}>
{secondaryMenu}
</NavbarMobileSidebarPanel>
</div>
</div>
);
}

View File

@@ -0,0 +1,31 @@
import React, {type ReactNode} from 'react';
import {useThemeConfig} from '@docusaurus/theme-common';
import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal';
import NavbarItem, {type Props as NavbarItemConfig} from '@theme/NavbarItem';
function useNavbarItems() {
// TODO temporary casting until ThemeConfig type is improved
return useThemeConfig().navbar.items as NavbarItemConfig[];
}
// The primary menu displays the navbar items
export default function NavbarMobilePrimaryMenu(): ReactNode {
const mobileSidebar = useNavbarMobileSidebar();
// TODO how can the order be defined for mobile?
// Should we allow providing a different list of items?
const items = useNavbarItems();
return (
<ul className="menu__list">
{items.map((item, i) => (
<NavbarItem
mobile
{...item}
onClick={() => mobileSidebar.toggle()}
key={i}
/>
))}
</ul>
);
}

View File

@@ -0,0 +1,32 @@
import React, {type ComponentProps, type ReactNode} from 'react';
import {useThemeConfig} from '@docusaurus/theme-common';
import {useNavbarSecondaryMenu} from '@docusaurus/theme-common/internal';
import Translate from '@docusaurus/Translate';
function SecondaryMenuBackButton(props: ComponentProps<'button'>) {
return (
<button {...props} type="button" className="clean-btn navbar-sidebar__back">
<Translate
id="theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel"
description="The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)">
Back to main menu
</Translate>
</button>
);
}
// The secondary menu slides from the right and shows contextual information
// such as the docs sidebar
export default function NavbarMobileSidebarSecondaryMenu(): ReactNode {
const isPrimaryMenuEmpty = useThemeConfig().navbar.items.length === 0;
const secondaryMenu = useNavbarSecondaryMenu();
return (
<>
{/* edge-case: prevent returning to the primaryMenu when it's empty */}
{!isPrimaryMenuEmpty && (
<SecondaryMenuBackButton onClick={() => secondaryMenu.hide()} />
)}
{secondaryMenu.content}
</>
);
}

View File

@@ -0,0 +1,23 @@
import React, {type ReactNode} from 'react';
import {useNavbarMobileSidebar} from '@docusaurus/theme-common/internal';
import {translate} from '@docusaurus/Translate';
import IconMenu from '@theme/Icon/Menu';
export default function MobileSidebarToggle(): ReactNode {
const {toggle, shown} = useNavbarMobileSidebar();
return (
<button
onClick={toggle}
aria-label={translate({
id: 'theme.docs.sidebar.toggleSidebarButtonAriaLabel',
message: 'Toggle navigation bar',
description:
'The ARIA label for hamburger menu button of mobile navigation',
})}
aria-expanded={shown}
className="navbar__toggle clean-btn"
type="button">
<IconMenu />
</button>
);
}

View File

@@ -0,0 +1,26 @@
import React, {type ReactNode} from 'react';
import {
useLockBodyScroll,
useNavbarMobileSidebar,
} from '@docusaurus/theme-common/internal';
import NavbarMobileSidebarLayout from '@theme/Navbar/MobileSidebar/Layout';
import NavbarMobileSidebarHeader from '@theme/Navbar/MobileSidebar/Header';
import NavbarMobileSidebarPrimaryMenu from '@theme/Navbar/MobileSidebar/PrimaryMenu';
import NavbarMobileSidebarSecondaryMenu from '@theme/Navbar/MobileSidebar/SecondaryMenu';
export default function NavbarMobileSidebar(): ReactNode {
const mobileSidebar = useNavbarMobileSidebar();
useLockBodyScroll(mobileSidebar.shown);
if (!mobileSidebar.shouldRender) {
return null;
}
return (
<NavbarMobileSidebarLayout
header={<NavbarMobileSidebarHeader />}
primaryMenu={<NavbarMobileSidebarPrimaryMenu />}
secondaryMenu={<NavbarMobileSidebarSecondaryMenu />}
/>
);
}

View File

@@ -0,0 +1,13 @@
import React, {type ReactNode} from 'react';
import clsx from 'clsx';
import type {Props} from '@theme/Navbar/Search';
import styles from './styles.module.css';
export default function NavbarSearch({children, className}: Props): ReactNode {
return (
<div className={clsx(className, styles.navbarSearchContainer)}>
{children}
</div>
);
}

View File

@@ -0,0 +1,21 @@
/*
Workaround to avoid rendering empty search container
See https://github.com/facebook/docusaurus/pull/9385
*/
.navbarSearchContainer:empty {
display: none;
}
@media (max-width: 996px) {
.navbarSearchContainer {
position: absolute;
right: var(--ifm-navbar-padding-horizontal);
}
}
@media (min-width: 997px) {
.navbarSearchContainer {
padding: var(--ifm-navbar-item-padding-vertical)
var(--ifm-navbar-item-padding-horizontal);
}
}

View File

@@ -0,0 +1,11 @@
import React, {type ReactNode} from 'react';
import NavbarLayout from '@theme/Navbar/Layout';
import NavbarContent from '@theme/Navbar/Content';
export default function Navbar(): ReactNode {
return (
<NavbarLayout>
<NavbarContent />
</NavbarLayout>
);
}