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:
428
dexto/docs/src/components/CopyMarkdown.tsx
Normal file
428
dexto/docs/src/components/CopyMarkdown.tsx
Normal 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 ``;
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
196
dexto/docs/src/components/ExpandableImage.css
Normal file
196
dexto/docs/src/components/ExpandableImage.css
Normal 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;
|
||||
}
|
||||
}
|
||||
161
dexto/docs/src/components/ExpandableImage.tsx
Normal file
161
dexto/docs/src/components/ExpandableImage.tsx
Normal 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;
|
||||
226
dexto/docs/src/components/ExpandableMermaid.css
Normal file
226
dexto/docs/src/components/ExpandableMermaid.css
Normal 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;
|
||||
}
|
||||
}
|
||||
89
dexto/docs/src/components/ExpandableMermaid.tsx
Normal file
89
dexto/docs/src/components/ExpandableMermaid.tsx
Normal 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;
|
||||
71
dexto/docs/src/components/HomepageFeatures/index.tsx
Normal file
71
dexto/docs/src/components/HomepageFeatures/index.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
11
dexto/docs/src/components/HomepageFeatures/styles.module.css
Normal file
11
dexto/docs/src/components/HomepageFeatures/styles.module.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.features {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 2rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.featureSvg {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
}
|
||||
87
dexto/docs/src/css/brand.css
Normal file
87
dexto/docs/src/css/brand.css
Normal 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;
|
||||
}
|
||||
855
dexto/docs/src/css/custom.css
Normal file
855
dexto/docs/src/css/custom.css
Normal 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);
|
||||
}
|
||||
23
dexto/docs/src/pages/index.module.css
Normal file
23
dexto/docs/src/pages/index.module.css
Normal 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;
|
||||
}
|
||||
6
dexto/docs/src/pages/index.tsx
Normal file
6
dexto/docs/src/pages/index.tsx
Normal 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" />;
|
||||
}
|
||||
183
dexto/docs/src/plugins/markdown-route-plugin.ts
Normal file
183
dexto/docs/src/plugins/markdown-route-plugin.ts
Normal 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;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
20
dexto/docs/src/theme/MDXContent/index.tsx
Normal file
20
dexto/docs/src/theme/MDXContent/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
dexto/docs/src/theme/Navbar/ColorModeToggle/index.tsx
Normal file
27
dexto/docs/src/theme/Navbar/ColorModeToggle/index.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.darkNavbarColorModeToggle:hover,
|
||||
.darkNavbarColorModeToggle:focus-visible {
|
||||
background: var(--ifm-color-gray-800);
|
||||
}
|
||||
107
dexto/docs/src/theme/Navbar/Content/index.tsx
Normal file
107
dexto/docs/src/theme/Navbar/Content/index.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
dexto/docs/src/theme/Navbar/Content/styles.module.css
Normal file
16
dexto/docs/src/theme/Navbar/Content/styles.module.css
Normal 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;
|
||||
}
|
||||
57
dexto/docs/src/theme/Navbar/Layout/index.tsx
Normal file
57
dexto/docs/src/theme/Navbar/Layout/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
7
dexto/docs/src/theme/Navbar/Layout/styles.module.css
Normal file
7
dexto/docs/src/theme/Navbar/Layout/styles.module.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.navbarHideable {
|
||||
transition: transform var(--ifm-transition-fast);
|
||||
}
|
||||
|
||||
.navbarHidden {
|
||||
transform: translate3d(0, calc(-100% - 2px), 0);
|
||||
}
|
||||
12
dexto/docs/src/theme/Navbar/Logo/index.tsx
Normal file
12
dexto/docs/src/theme/Navbar/Logo/index.tsx
Normal 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"
|
||||
/>
|
||||
);
|
||||
}
|
||||
33
dexto/docs/src/theme/Navbar/MobileSidebar/Header/index.tsx
Normal file
33
dexto/docs/src/theme/Navbar/MobileSidebar/Header/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
94
dexto/docs/src/theme/Navbar/MobileSidebar/Layout/index.tsx
Normal file
94
dexto/docs/src/theme/Navbar/MobileSidebar/Layout/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
</>
|
||||
);
|
||||
}
|
||||
23
dexto/docs/src/theme/Navbar/MobileSidebar/Toggle/index.tsx
Normal file
23
dexto/docs/src/theme/Navbar/MobileSidebar/Toggle/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
dexto/docs/src/theme/Navbar/MobileSidebar/index.tsx
Normal file
26
dexto/docs/src/theme/Navbar/MobileSidebar/index.tsx
Normal 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 />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
13
dexto/docs/src/theme/Navbar/Search/index.tsx
Normal file
13
dexto/docs/src/theme/Navbar/Search/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
21
dexto/docs/src/theme/Navbar/Search/styles.module.css
Normal file
21
dexto/docs/src/theme/Navbar/Search/styles.module.css
Normal 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);
|
||||
}
|
||||
}
|
||||
11
dexto/docs/src/theme/Navbar/index.tsx
Normal file
11
dexto/docs/src/theme/Navbar/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user