- Added comprehensive documentation covering key features, user guides, and best practices for the Corrad ProcessMaker platform. - Introduced new API endpoints for serving documentation files dynamically. - Enhanced the navigation structure to include a dedicated documentation section for improved accessibility. - Updated the Nuxt configuration to optimize the development environment and ensure proper handling of dependencies. - Included new dependencies in package.json to support documentation rendering and processing. - Improved the user interface for the documentation page, enhancing the overall user experience.
1112 lines
35 KiB
Vue
1112 lines
35 KiB
Vue
<script setup>
|
|
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from "vue";
|
|
import { marked } from "marked";
|
|
|
|
const docs = ref([]);
|
|
const loading = ref(true);
|
|
const error = ref(null);
|
|
const selected = ref(null);
|
|
const searchQuery = ref("");
|
|
const showMobileSidebar = ref(false);
|
|
const scrollProgress = ref(0);
|
|
const currentToc = ref([]);
|
|
const showToc = ref(false);
|
|
const activeSection = ref(null);
|
|
|
|
// Initialize marked with custom renderer for better HTML
|
|
marked.setOptions({
|
|
breaks: true,
|
|
gfm: true,
|
|
headerIds: true,
|
|
});
|
|
|
|
definePageMeta({
|
|
title: "Documentation",
|
|
layout: "empty",
|
|
});
|
|
|
|
// Configure custom renderer for TOC extraction
|
|
const renderer = new marked.Renderer();
|
|
const originalHeading = renderer.heading;
|
|
const originalLink = renderer.link;
|
|
renderer.heading = function (text, level, raw) {
|
|
const safeRaw = String(raw || text || "");
|
|
const id = safeRaw
|
|
.toLowerCase()
|
|
.replace(/[^\w\s-]/g, "")
|
|
.replace(/[\s]+/g, "-");
|
|
return `<h${level} id="${id}" class="scroll-mt-20">${text}</h${level}>`;
|
|
};
|
|
// Custom link renderer for internal .md links
|
|
renderer.link = function (href, title, text) {
|
|
if (href && String(href).endsWith('.md')) {
|
|
const file = String(href).split('/').pop();
|
|
// Use a special class for delegation
|
|
return `<a href="#" class="doc-link" data-doc="${file}">${text}</a>`;
|
|
}
|
|
return originalLink.call(this, href, title, text);
|
|
};
|
|
marked.setOptions({ renderer });
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const files = await $fetch("/api/documentation");
|
|
const docPromises = files.map(async (file) => {
|
|
try {
|
|
const response = await $fetch(`/api/documentation/${file}`);
|
|
let content = response?.content || "";
|
|
|
|
if (!content) {
|
|
console.warn(`Empty content for file: ${file}`);
|
|
return null;
|
|
}
|
|
|
|
// Enhanced content cleaning to remove any object serialization issues
|
|
content = String(content)
|
|
.replace(/\[object Object\]/g, "")
|
|
.replace(/\[object\s+Object\]/gi, "")
|
|
.replace(/\bobject Object\b/gi, "")
|
|
.replace(/\[Object\]/gi, "")
|
|
.replace(/Object\s*\{[^}]*\}/gi, "")
|
|
.replace(/\{\s*\[native code\]\s*\}/gi, "")
|
|
.replace(/^\s*\[object.*?\]\s*$/gim, "")
|
|
.trim();
|
|
|
|
let html = marked.parse(content);
|
|
|
|
// Post-process HTML to remove any remaining object references
|
|
html = html
|
|
.replace(/\[object Object\]/g, "")
|
|
.replace(/\[object\s+Object\]/gi, "")
|
|
.replace(/\bobject Object\b/gi, "")
|
|
.replace(/<p>\s*\[object.*?\]\s*<\/p>/gi, "")
|
|
.replace(/<[^>]*>\s*\[object.*?\]\s*<\/[^>]*>/gi, "");
|
|
|
|
// Extract metadata from filename
|
|
const match = file.match(/^(\d+)-(.+)\.md$/);
|
|
const order = match ? parseInt(match[1]) : 999;
|
|
const cleanTitle = (match ? match[2] : file.replace(/\.md$/, ""))
|
|
.replace(/[-_]/g, " ")
|
|
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
|
|
// Extract table of contents
|
|
const tocMatch = content.match(
|
|
/## Table of Contents([\s\S]*?)(?=\n---|\n##[^#]|$)/
|
|
);
|
|
const toc = tocMatch ? extractTocFromContent(tocMatch[1]) : [];
|
|
|
|
// Estimate reading time (250 words per minute)
|
|
const wordCount = content
|
|
.split(/\s+/)
|
|
.filter((word) => word.length > 0).length;
|
|
const readingTime = Math.max(1, Math.ceil(wordCount / 250));
|
|
|
|
return {
|
|
file,
|
|
title: cleanTitle,
|
|
html,
|
|
content,
|
|
order,
|
|
toc,
|
|
readingTime,
|
|
category: getCategoryFromOrder(order),
|
|
};
|
|
} catch (error) {
|
|
console.error(`Error processing file ${file}:`, error);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
docs.value = (await Promise.all(docPromises))
|
|
.filter((doc) => doc !== null)
|
|
.sort((a, b) => a.order - b.order);
|
|
|
|
selected.value = docs.value[0]?.file || null;
|
|
updateToc();
|
|
|
|
// Add keyboard listeners
|
|
document.addEventListener("keydown", handleKeyNavigation);
|
|
} catch (e) {
|
|
error.value = "Failed to load documentation.";
|
|
console.error("Documentation loading error:", e);
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
document.removeEventListener("keydown", handleKeyNavigation);
|
|
});
|
|
|
|
// Scrollspy for TOC
|
|
function handleScrollSpy() {
|
|
const headings = Array.from(document.querySelectorAll('.prose h2, .prose h3'));
|
|
const scrollY = window.scrollY || document.querySelector('.main-content')?.scrollTop || 0;
|
|
let current = null;
|
|
for (const heading of headings) {
|
|
const rect = heading.getBoundingClientRect();
|
|
if (rect.top + window.scrollY - 120 <= scrollY) {
|
|
current = heading.id;
|
|
}
|
|
}
|
|
activeSection.value = current;
|
|
}
|
|
|
|
onMounted(() => {
|
|
document.querySelector('.main-content')?.addEventListener('scroll', handleScrollSpy);
|
|
window.addEventListener('scroll', handleScrollSpy);
|
|
});
|
|
onUnmounted(() => {
|
|
document.querySelector('.main-content')?.removeEventListener('scroll', handleScrollSpy);
|
|
window.removeEventListener('scroll', handleScrollSpy);
|
|
});
|
|
|
|
// Event delegation for .doc-link clicks
|
|
onMounted(() => {
|
|
const handler = (e) => {
|
|
const target = e.target.closest('.doc-link');
|
|
if (target && target.dataset.doc) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
selectDoc(target.dataset.doc);
|
|
}
|
|
};
|
|
document.addEventListener('click', handler);
|
|
window.__docLinkHandler = handler;
|
|
});
|
|
onUnmounted(() => {
|
|
if (window.__docLinkHandler) {
|
|
document.removeEventListener('click', window.__docLinkHandler);
|
|
delete window.__docLinkHandler;
|
|
}
|
|
});
|
|
|
|
// Extract TOC from markdown content
|
|
function extractTocFromContent(tocContent) {
|
|
if (!tocContent || typeof tocContent !== "string") {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const lines = tocContent.split("\n").filter((line) => line.trim());
|
|
return lines
|
|
.map((line) => {
|
|
const match = line.match(/^\s*\d+\.\s*\[([^\]]+)\]\(#([^)]+)\)/);
|
|
if (match) {
|
|
return { title: match[1], id: match[2], level: 1 };
|
|
}
|
|
return null;
|
|
})
|
|
.filter(Boolean);
|
|
} catch (error) {
|
|
console.error("Error extracting TOC:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// Get category based on chapter order
|
|
function getCategoryFromOrder(order) {
|
|
if (order <= 1) return "Getting Started";
|
|
if (order <= 3) return "Form Management";
|
|
if (order <= 5) return "Process Management";
|
|
return "Advanced Topics";
|
|
}
|
|
|
|
// Computed properties
|
|
const filteredDocs = computed(() => {
|
|
if (!searchQuery.value.trim()) return docs.value;
|
|
|
|
const query = searchQuery.value.toLowerCase();
|
|
return docs.value.filter((doc) => {
|
|
if (!doc || !doc.title || !doc.content) return false;
|
|
return (
|
|
doc.title.toLowerCase().includes(query) ||
|
|
doc.content.toLowerCase().includes(query)
|
|
);
|
|
});
|
|
});
|
|
|
|
const selectedDoc = computed(() => {
|
|
return docs.value.find((doc) => doc.file === selected.value);
|
|
});
|
|
|
|
const currentDocIndex = computed(() => {
|
|
return docs.value.findIndex((doc) => doc.file === selected.value);
|
|
});
|
|
|
|
const previousDoc = computed(() => {
|
|
const index = currentDocIndex.value;
|
|
return index > 0 ? docs.value[index - 1] : null;
|
|
});
|
|
|
|
const nextDoc = computed(() => {
|
|
const index = currentDocIndex.value;
|
|
return index < docs.value.length - 1 ? docs.value[index + 1] : null;
|
|
});
|
|
|
|
// Functions
|
|
const selectDoc = (file) => {
|
|
selected.value = file;
|
|
showMobileSidebar.value = false;
|
|
scrollProgress.value = 0;
|
|
searchQuery.value = "";
|
|
updateToc();
|
|
scrollToTop();
|
|
};
|
|
|
|
// Keyboard navigation
|
|
const handleKeyNavigation = (event) => {
|
|
if (event.ctrlKey || event.metaKey) {
|
|
switch (event.key) {
|
|
case "k":
|
|
event.preventDefault();
|
|
document.querySelector('input[type="text"]')?.focus();
|
|
break;
|
|
case "ArrowLeft":
|
|
if (previousDoc.value) {
|
|
event.preventDefault();
|
|
selectDoc(previousDoc.value.file);
|
|
}
|
|
break;
|
|
case "ArrowRight":
|
|
if (nextDoc.value) {
|
|
event.preventDefault();
|
|
selectDoc(nextDoc.value.file);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (event.key === "Escape") {
|
|
searchQuery.value = "";
|
|
showMobileSidebar.value = false;
|
|
}
|
|
};
|
|
|
|
const updateToc = () => {
|
|
nextTick(() => {
|
|
const headings = document.querySelectorAll(".prose h2, .prose h3");
|
|
currentToc.value = Array.from(headings).map((heading) => ({
|
|
id: heading.id,
|
|
title: heading.textContent,
|
|
level: parseInt(heading.tagName.charAt(1)),
|
|
}));
|
|
showToc.value = currentToc.value.length > 3;
|
|
});
|
|
};
|
|
|
|
const scrollToTop = () => {
|
|
const mainContent = document.querySelector(".main-content");
|
|
if (mainContent) {
|
|
mainContent.scrollTo({ top: 0, behavior: "smooth" });
|
|
}
|
|
};
|
|
|
|
const scrollToSection = (id) => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
}
|
|
};
|
|
|
|
// Add copy buttons to code blocks after content is loaded
|
|
const addCopyButtons = () => {
|
|
nextTick(() => {
|
|
const codeBlocks = document.querySelectorAll(".prose pre");
|
|
codeBlocks.forEach((block) => {
|
|
// Skip if button already exists
|
|
if (block.querySelector(".copy-button")) return;
|
|
|
|
const button = document.createElement("button");
|
|
button.className =
|
|
"copy-button absolute top-2 right-2 px-2 py-1 text-xs bg-gray-700 hover:bg-gray-600 text-white rounded transition-colors";
|
|
button.textContent = "Copy";
|
|
button.onclick = () => {
|
|
const code = block.querySelector("code");
|
|
if (code) {
|
|
navigator.clipboard.writeText(code.textContent).then(() => {
|
|
button.textContent = "Copied!";
|
|
button.classList.add("bg-green-500");
|
|
setTimeout(() => {
|
|
button.textContent = "Copy";
|
|
button.classList.remove("bg-green-500");
|
|
}, 2000);
|
|
});
|
|
}
|
|
};
|
|
|
|
block.style.position = "relative";
|
|
block.appendChild(button);
|
|
});
|
|
});
|
|
};
|
|
|
|
// Watch for scroll progress and add copy buttons
|
|
watch(selected, () => {
|
|
nextTick(() => {
|
|
const mainContent = document.querySelector(".main-content");
|
|
if (mainContent) {
|
|
const updateProgress = () => {
|
|
const scrollTop = mainContent.scrollTop;
|
|
const scrollHeight =
|
|
mainContent.scrollHeight - mainContent.clientHeight;
|
|
scrollProgress.value =
|
|
scrollHeight > 0 ? (scrollTop / scrollHeight) * 100 : 0;
|
|
};
|
|
|
|
mainContent.addEventListener("scroll", updateProgress);
|
|
updateProgress();
|
|
}
|
|
|
|
// Add copy buttons to code blocks
|
|
addCopyButtons();
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col h-screen bg-gray-50">
|
|
<!-- Header Bar ---->
|
|
<header class="bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between shadow-sm">
|
|
<div class="flex items-center gap-4">
|
|
<NuxtLink to="/" class="cursor-pointer">
|
|
<Icon
|
|
name="ph:arrow-circle-left-duotone"
|
|
class="w-6 h-6 hover:text-gray-600 text-gray-500"
|
|
/>
|
|
</NuxtLink>
|
|
<div class="flex items-center gap-3">
|
|
<img
|
|
src="@/assets/img/logo/logo-word-black.svg"
|
|
alt="Corrad Logo"
|
|
class="h-8"
|
|
/>
|
|
<div class="border-l border-gray-300 pl-3">
|
|
<h1 class="text-xl font-semibold text-gray-900">Documentation</h1>
|
|
<p class="text-sm text-gray-500">System guides and references</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
<!-- Search Bar -->
|
|
<div class="relative">
|
|
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
|
<Icon name="material-symbols:search" class="h-5 w-5 text-gray-400" />
|
|
</div>
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Search documentation... (Ctrl+K)"
|
|
class="pl-10 pr-4 py-2 w-80 border border-gray-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Mobile menu button -->
|
|
<button
|
|
@click="showMobileSidebar = !showMobileSidebar"
|
|
class="lg:hidden p-2 rounded-md text-gray-600 hover:text-gray-900 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
>
|
|
<Icon name="material-symbols:menu" class="h-6 w-6" />
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Main Content Area -->
|
|
<div class="flex-1 overflow-auto">
|
|
<div class="p-6">
|
|
<div class="flex gap-6">
|
|
<!-- Sidebar -->
|
|
<aside
|
|
class="w-80 flex-shrink-0 sticky top-6 h-fit"
|
|
:class="showMobileSidebar ? 'block' : 'hidden lg:block'"
|
|
>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<h2 class="text-lg font-semibold text-gray-900 mb-6">Contents</h2>
|
|
|
|
<!-- Category Filter -->
|
|
<div class="space-y-1" v-if="!searchQuery">
|
|
<div
|
|
v-for="category in [
|
|
'Getting Started',
|
|
'Form Management',
|
|
'Process Management',
|
|
'Advanced Topics',
|
|
]"
|
|
:key="category"
|
|
class="mb-6"
|
|
>
|
|
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
|
|
{{ category }}
|
|
</h3>
|
|
<div class="space-y-1">
|
|
<button
|
|
v-for="doc in docs.filter((d) => d.category === category && d.order !== 999)"
|
|
:key="doc.file"
|
|
@click="selectDoc(doc.file)"
|
|
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors group"
|
|
:class="
|
|
selected === doc.file
|
|
? 'bg-blue-50 text-blue-700 font-medium border border-blue-200'
|
|
: 'text-gray-700 hover:bg-gray-50 hover:text-gray-900'
|
|
"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center space-x-3">
|
|
<span
|
|
class="flex-shrink-0 w-6 h-6 rounded-full bg-blue-600 text-white text-xs font-bold flex items-center justify-center"
|
|
>{{ doc.order }}</span
|
|
>
|
|
<span>{{ doc.title }}</span>
|
|
</div>
|
|
<div class="flex items-center space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
<span class="text-xs text-gray-400">{{ doc.readingTime }} min</span>
|
|
<Icon
|
|
v-if="selected === doc.file"
|
|
name="material-symbols:check-circle"
|
|
class="w-4 h-4 text-blue-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Results -->
|
|
<div v-else class="space-y-1">
|
|
<div
|
|
v-if="filteredDocs.filter((doc) => doc.order !== 999).length === 0"
|
|
class="text-sm text-gray-500 text-center py-4"
|
|
>
|
|
No results found for "{{ searchQuery }}"
|
|
</div>
|
|
<button
|
|
v-for="doc in filteredDocs.filter((doc) => doc.order !== 999)"
|
|
:key="doc.file"
|
|
@click="selectDoc(doc.file)"
|
|
class="w-full text-left px-3 py-2 rounded-lg text-sm transition-colors"
|
|
:class="
|
|
selected === doc.file
|
|
? 'bg-blue-50 text-blue-700 font-medium'
|
|
: 'text-gray-700 hover:bg-gray-50'
|
|
"
|
|
>
|
|
<div class="flex items-center justify-between">
|
|
<span>{{ doc.title }}</span>
|
|
<span class="text-xs text-gray-400">{{ doc.readingTime }} min</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content -->
|
|
<main class="flex-1 min-w-0">
|
|
<div class="flex gap-6">
|
|
<!-- Document Content -->
|
|
<div class="flex-1">
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<!-- Loading State -->
|
|
<div v-if="loading" class="p-12 text-center">
|
|
<Icon name="material-symbols:progress-activity" class="w-8 h-8 animate-spin text-blue-500 mx-auto mb-4" />
|
|
<p class="text-gray-500">Loading documentation...</p>
|
|
</div>
|
|
|
|
<!-- Error State -->
|
|
<div v-else-if="error" class="p-12 text-center">
|
|
<Icon name="material-symbols:error-outline" class="w-16 h-16 text-red-300 mx-auto mb-4" />
|
|
<div class="text-red-500 text-lg font-medium mb-2">{{ error }}</div>
|
|
<p class="text-gray-500">Please try refreshing the page</p>
|
|
</div>
|
|
|
|
<!-- Document Content -->
|
|
<div v-else-if="selectedDoc">
|
|
<!-- Document Header -->
|
|
<div class="px-8 py-4 border-b border-gray-200 bg-gray-50">
|
|
<div class="flex items-center justify-between my-4">
|
|
<div class="flex items-center space-x-4">
|
|
<span class="w-10 h-10 rounded-full bg-blue-600 text-white text-lg font-bold flex items-center justify-center">
|
|
{{ selectedDoc.order }}
|
|
</span>
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<RsBadge variant="secondary" size="sm">
|
|
{{ selectedDoc.category }}
|
|
</RsBadge>
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-gray-900">
|
|
{{ selectedDoc.title }}
|
|
</h1>
|
|
</div>
|
|
</div>
|
|
<div class="text-right text-sm text-gray-500">
|
|
<div class="flex items-center gap-1">
|
|
<Icon name="material-symbols:schedule" class="w-4 h-4" />
|
|
<span>{{ selectedDoc.readingTime }} min read</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Body -->
|
|
<div class="main-content bg-gradient-to-br from-gray-50 to-white shadow-xl rounded-2xl" style="overflow-y: auto; position:relative;">
|
|
<div class="relative">
|
|
<!-- Subtle background pattern -->
|
|
<div class="absolute inset-0 opacity-10 pointer-events-none">
|
|
<div class="absolute inset-0" style="background-image: radial-gradient(circle at 1px 1px, #3b82f6 1px, transparent 0); background-size: 32px 32px;"></div>
|
|
</div>
|
|
<div class="relative px-8 py-12">
|
|
<div class="max-w-4xl mx-auto">
|
|
<!-- Divider after TOC and before first heading -->
|
|
<div v-if="currentToc.length > 0" class="mb-8">
|
|
<hr class="border-t-2 border-blue-100 my-6" />
|
|
</div>
|
|
<div
|
|
class="prose prose-lg max-w-none prose-headings:font-semibold prose-headings:tracking-tight prose-headings:scroll-mt-20 prose-h1:text-3xl prose-h1:mt-0 prose-h1:mb-8 prose-h1:text-gray-900 prose-h2:text-xl prose-h2:mt-10 prose-h2:mb-6 prose-h2:text-gray-800 prose-h3:text-lg prose-h3:mt-8 prose-h3:mb-4 prose-h3:text-gray-700 prose-p:mb-6 prose-p:leading-relaxed prose-p:text-gray-600 prose-p:text-base prose-code:bg-blue-50 prose-code:text-blue-700 prose-code:px-2 prose-code:py-1 prose-code:rounded-md prose-code:text-sm prose-code:font-medium prose-code:border prose-code:border-blue-200 prose-pre:bg-slate-900 prose-pre:text-slate-100 prose-pre:rounded-xl prose-pre:p-6 prose-pre:overflow-x-auto prose-pre:my-8 prose-pre:shadow-lg prose-table:rounded-xl prose-table:overflow-hidden prose-table:border-0 prose-table:border-gray-200 prose-table:my-8 prose-table:shadow-sm prose-th:bg-gray-100 prose-th:font-semibold prose-th:p-4 prose-th:text-gray-800 prose-td:p-4 prose-td:border-b prose-td:border-gray-100 prose-a:text-blue-600 prose-a:no-underline prose-a:font-medium hover:prose-a:underline hover:prose-a:text-blue-700 prose-blockquote:border-l-4 prose-blockquote:border-blue-500 prose-blockquote:bg-gradient-to-r prose-blockquote:from-blue-50 prose-blockquote:to-indigo-50 prose-blockquote:p-6 prose-blockquote:rounded-r-xl prose-blockquote:my-8 prose-blockquote:shadow-sm prose-ul:my-6 prose-ul:pl-0 prose-ol:my-6 prose-ol:pl-0 prose-li:my-2 prose-li:leading-relaxed prose-li:text-gray-600 prose-strong:font-semibold prose-strong:text-gray-900"
|
|
v-html="selectedDoc.html"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Footer -->
|
|
<div class="px-8 py-6 bg-gray-50 border-t border-gray-200 flex justify-between items-center">
|
|
<RsButton
|
|
v-if="previousDoc"
|
|
@click="selectDoc(previousDoc.file)"
|
|
variant="secondary"
|
|
size="sm"
|
|
>
|
|
<Icon name="material-symbols:chevron-left" class="mr-1" />
|
|
{{ previousDoc.title }}
|
|
</RsButton>
|
|
<div v-else></div>
|
|
|
|
<RsButton
|
|
v-if="nextDoc"
|
|
@click="selectDoc(nextDoc.file)"
|
|
variant="primary"
|
|
size="sm"
|
|
>
|
|
{{ nextDoc.title }}
|
|
<Icon name="material-symbols:chevron-right" class="ml-1" />
|
|
</RsButton>
|
|
<div v-else></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty State -->
|
|
<div v-else class="p-12 text-center">
|
|
<Icon name="material-symbols:description-outline" class="w-16 h-16 text-gray-300 mx-auto mb-4" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">Select a document</h3>
|
|
<p class="text-gray-600">Choose a documentation topic from the sidebar to get started</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table of Contents -->
|
|
<aside
|
|
v-if="showToc && currentToc.length > 0"
|
|
class="hidden xl:block w-64 flex-shrink-0 sticky top-24 h-fit"
|
|
>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4">
|
|
On This Page
|
|
</h3>
|
|
<nav class="space-y-2">
|
|
<a
|
|
v-for="item in currentToc"
|
|
:key="item.id"
|
|
@click.prevent="scrollToSection(item.id)"
|
|
href="#"
|
|
class="block text-sm text-gray-600 hover:text-blue-600 transition-colors py-1 border-l-2 border-transparent hover:border-blue-500 pl-3"
|
|
:class="[{'ml-4': item.level === 3}, activeSection === item.id ? 'border-blue-500 text-blue-700 font-semibold bg-blue-50' : '']"
|
|
>
|
|
{{ item.title }}
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile Sidebar Overlay -->
|
|
<div
|
|
v-if="showMobileSidebar"
|
|
@click="showMobileSidebar = false"
|
|
class="fixed inset-0 bg-black bg-opacity-50 z-40 lg:hidden"
|
|
></div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Enhanced prose styling with visual richness */
|
|
.prose :where(h1):not(:where([class~="not-prose"] *)) {
|
|
font-size: 2.5rem;
|
|
margin-top: 0;
|
|
margin-bottom: 2.5rem;
|
|
color: #1f2937;
|
|
font-weight: 800;
|
|
line-height: 1.2;
|
|
letter-spacing: -0.025em;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.prose :where(h2):not(:where([class~="not-prose"] *)) {
|
|
font-size: 1.75rem;
|
|
margin-top: 3.5rem;
|
|
margin-bottom: 1.5rem;
|
|
color: #1f2937;
|
|
position: relative;
|
|
font-weight: 700;
|
|
line-height: 1.3;
|
|
padding-left: 1.5rem;
|
|
border-left: 4px solid transparent;
|
|
background: linear-gradient(white, white) padding-box,
|
|
linear-gradient(135deg, #3b82f6, #6366f1) border-box;
|
|
border-left: 4px solid;
|
|
background-clip: padding-box, border-box;
|
|
padding: 0.75rem 0 0.75rem 1.5rem;
|
|
margin-left: -1.5rem;
|
|
border-radius: 0 8px 8px 0;
|
|
background-color: rgba(59, 130, 246, 0.03);
|
|
}
|
|
|
|
.prose :where(h2):not(:where([class~="not-prose"] *))::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: -1.5rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 8px;
|
|
height: 8px;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
border-radius: 50%;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.prose :where(h3):not(:where([class~="not-prose"] *)) {
|
|
font-size: 1.375rem;
|
|
margin-top: 2.5rem;
|
|
margin-bottom: 1.25rem;
|
|
color: #374151;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
position: relative;
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
.prose :where(h3):not(:where([class~="not-prose"] *))::before {
|
|
content: "▶";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
color: #3b82f6;
|
|
font-size: 0.75rem;
|
|
transform: rotate(90deg);
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.prose :where(p):not(:where([class~="not-prose"] *)) {
|
|
margin-bottom: 1.75rem;
|
|
line-height: 1.8;
|
|
color: #4b5563;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.prose :where(p):not(:where([class~="not-prose"] *)):first-child {
|
|
margin-top: 0;
|
|
font-size: 1.125rem;
|
|
color: #374151;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.prose :where(ul):not(:where([class~="not-prose"] *)),
|
|
.prose :where(ol):not(:where([class~="not-prose"] *)) {
|
|
margin: 2rem 0;
|
|
padding-left: 0;
|
|
}
|
|
|
|
.prose :where(li):not(:where([class~="not-prose"] *)) {
|
|
margin: 0.75rem 0;
|
|
line-height: 1.7;
|
|
color: #4b5563;
|
|
position: relative;
|
|
padding-left: 1.5rem;
|
|
}
|
|
|
|
.prose :where(ul li):not(:where([class~="not-prose"] *))::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0.75rem;
|
|
width: 6px;
|
|
height: 6px;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
border-radius: 50%;
|
|
transform: translateY(-50%);
|
|
}
|
|
|
|
.prose :where(strong):not(:where([class~="not-prose"] *)) {
|
|
font-weight: 700;
|
|
color: #1f2937;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
/* Enhanced visual elements */
|
|
.prose :where(em):not(:where([class~="not-prose"] *)) {
|
|
font-style: italic;
|
|
color: #6366f1;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.prose :where(hr):not(:where([class~="not-prose"] *)) {
|
|
border: none;
|
|
height: 2px;
|
|
background: linear-gradient(to right, transparent, #3b82f6, transparent);
|
|
margin: 3rem 0;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.prose :where(pre):not(:where([class~="not-prose"] *)) {
|
|
background: #0f172a;
|
|
color: #f1f5f9;
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid #334155;
|
|
position: relative;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.prose :where(pre):not(:where([class~="not-prose"] *))::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: #3b82f6;
|
|
}
|
|
|
|
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
|
background: #f1f5f9;
|
|
color: #3b82f6;
|
|
border-radius: 0.375rem;
|
|
padding: 0.2em 0.5em;
|
|
font-weight: 500;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.prose :where(blockquote):not(:where([class~="not-prose"] *)) {
|
|
border: none;
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.08), rgba(99, 102, 241, 0.08));
|
|
margin: 2.5rem 0;
|
|
padding: 2rem;
|
|
border-radius: 1rem;
|
|
position: relative;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
border-left: 4px solid transparent;
|
|
background-clip: padding-box;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.prose :where(blockquote):not(:where([class~="not-prose"] *))::before {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 4px;
|
|
background: linear-gradient(135deg, #3b82f6, #6366f1);
|
|
}
|
|
|
|
.prose :where(blockquote):not(:where([class~="not-prose"] *))::after {
|
|
content: "\201C";
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
right: 1rem;
|
|
font-size: 4rem;
|
|
color: rgba(59, 130, 246, 0.15);
|
|
font-family: Georgia, serif;
|
|
line-height: 1;
|
|
}
|
|
|
|
.prose :where(table):not(:where([class~="not-prose"] *)) {
|
|
border-collapse: separate;
|
|
border-spacing: 0;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 1rem;
|
|
overflow: hidden;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
background: white;
|
|
margin: 2.5rem 0;
|
|
}
|
|
|
|
.prose :where(th):not(:where([class~="not-prose"] *)) {
|
|
background: linear-gradient(135deg, #f8fafc, #f1f5f9);
|
|
font-weight: 700;
|
|
color: #1f2937;
|
|
text-align: left;
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
position: relative;
|
|
}
|
|
|
|
.prose :where(th):not(:where([class~="not-prose"] *))::after {
|
|
content: "";
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background: linear-gradient(to right, #3b82f6, #6366f1);
|
|
}
|
|
|
|
.prose :where(td):not(:where([class~="not-prose"] *)) {
|
|
padding: 1rem 1.5rem;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.prose :where(tr):not(:where([class~="not-prose"] *)):hover {
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.02), rgba(99, 102, 241, 0.02));
|
|
}
|
|
|
|
/* Add some visual enhancements */
|
|
.prose {
|
|
position: relative;
|
|
}
|
|
|
|
.prose::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: -2rem;
|
|
width: 2px;
|
|
height: 100%;
|
|
background: linear-gradient(to bottom, transparent, rgba(59, 130, 246, 0.3), transparent);
|
|
border-radius: 1px;
|
|
}
|
|
|
|
/* Smooth scrolling */
|
|
html {
|
|
scroll-behavior: smooth;
|
|
}
|
|
|
|
/* Custom scrollbar */
|
|
.main-content::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.main-content::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.main-content::-webkit-scrollbar-thumb {
|
|
background: #3b82f6;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.main-content::-webkit-scrollbar-thumb:hover {
|
|
background: #2563eb;
|
|
}
|
|
|
|
/* Animation for sidebar toggle */
|
|
@media (max-width: 1024px) {
|
|
aside {
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
}
|
|
|
|
/* Focus styles */
|
|
.prose :where(a):not(:where([class~="not-prose"] *)):focus {
|
|
outline: 2px solid #3b82f6;
|
|
outline-offset: 2px;
|
|
border-radius: 0.25rem;
|
|
}
|
|
|
|
/* Transitions */
|
|
.transition-colors {
|
|
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 150ms;
|
|
}
|
|
|
|
.transition-opacity {
|
|
transition-property: opacity;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 150ms;
|
|
}
|
|
|
|
/* Print styles */
|
|
@media print {
|
|
.prose {
|
|
max-width: none;
|
|
}
|
|
|
|
.prose :where(pre):not(:where([class~="not-prose"] *)) {
|
|
background: #f8f9fa !important;
|
|
color: #212529 !important;
|
|
border: 1px solid #dee2e6 !important;
|
|
}
|
|
|
|
.prose :where(code):not(:where([class~="not-prose"] *)) {
|
|
background: #f8f9fa !important;
|
|
color: #212529 !important;
|
|
}
|
|
}
|
|
|
|
/* Callout styles for markdown notes/warnings/tips */
|
|
.prose blockquote p strong {
|
|
display: inline-block;
|
|
margin-right: 0.5em;
|
|
}
|
|
.prose blockquote p strong:contains('Note:') {
|
|
color: #2563eb;
|
|
}
|
|
.prose blockquote p strong:contains('Warning:') {
|
|
color: #f59e42;
|
|
}
|
|
.prose blockquote p strong:contains('Tip:') {
|
|
color: #10b981;
|
|
}
|
|
.prose blockquote {
|
|
border-left: 6px solid #3b82f6;
|
|
background: linear-gradient(135deg, #eff6ff 80%, #f0fdfa 100%);
|
|
padding: 1.5rem 2rem;
|
|
margin: 2rem 0;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.04);
|
|
}
|
|
/* Responsive tables */
|
|
.prose table {
|
|
display: block;
|
|
width: 100%;
|
|
overflow-x: auto;
|
|
border-radius: 0.5rem;
|
|
}
|
|
.prose th, .prose td {
|
|
white-space: nowrap;
|
|
}
|
|
/* Code block improvements */
|
|
.prose pre {
|
|
font-size: 1rem;
|
|
line-height: 1.6;
|
|
background: #0f172a;
|
|
color: #f1f5f9;
|
|
border-radius: 0.75rem;
|
|
padding: 1.5rem;
|
|
border: 1px solid #334155;
|
|
overflow-x: auto;
|
|
}
|
|
.prose code {
|
|
font-size: 1em;
|
|
background: #f1f5f9;
|
|
color: #3b82f6;
|
|
border-radius: 0.375rem;
|
|
padding: 0.2em 0.5em;
|
|
font-weight: 500;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
/* Spacing tweaks for readability */
|
|
.prose p, .prose ul, .prose ol {
|
|
margin-bottom: 1.5em;
|
|
}
|
|
.prose h2 {
|
|
margin-top: 2.5em;
|
|
margin-bottom: 1.2em;
|
|
}
|
|
.prose h3 {
|
|
margin-top: 2em;
|
|
margin-bottom: 1em;
|
|
}
|
|
/* Make first paragraph after h1 larger and more readable */
|
|
.prose h1 + p {
|
|
font-size: 1.25rem;
|
|
color: #3730a3;
|
|
font-weight: 500;
|
|
margin-bottom: 2.5rem;
|
|
margin-top: 0.5rem;
|
|
}
|
|
/* Divider after TOC and before first heading */
|
|
hr {
|
|
border: none;
|
|
border-top: 2px solid #dbeafe;
|
|
margin: 2.5rem 0 2rem 0;
|
|
}
|
|
/* Callout styles with icons */
|
|
.prose blockquote {
|
|
position: relative;
|
|
padding-left: 3.5rem;
|
|
border-left: 6px solid #3b82f6;
|
|
background: linear-gradient(135deg, #eff6ff 80%, #f0fdfa 100%);
|
|
margin: 2.5rem 0;
|
|
border-radius: 0.75rem;
|
|
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.04);
|
|
}
|
|
.prose blockquote p strong {
|
|
display: inline-block;
|
|
margin-right: 0.5em;
|
|
}
|
|
.prose blockquote p strong:contains('Note:')::before {
|
|
content: '\2139'; /* info icon */
|
|
color: #2563eb;
|
|
font-size: 1.2em;
|
|
margin-right: 0.5em;
|
|
}
|
|
.prose blockquote p strong:contains('Warning:')::before {
|
|
content: '\26A0'; /* warning icon */
|
|
color: #f59e42;
|
|
font-size: 1.2em;
|
|
margin-right: 0.5em;
|
|
}
|
|
.prose blockquote p strong:contains('Tip:')::before {
|
|
content: '\1F4A1'; /* lightbulb icon */
|
|
color: #10b981;
|
|
font-size: 1.2em;
|
|
margin-right: 0.5em;
|
|
}
|
|
/* Zebra striping for tables */
|
|
.prose tr:nth-child(even) td {
|
|
background: #f3f4f6;
|
|
}
|
|
/* Custom bullets for lists */
|
|
.prose ul > li::before {
|
|
content: '\2022';
|
|
color: #3b82f6;
|
|
font-size: 1.2em;
|
|
margin-right: 0.75em;
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0.5em;
|
|
}
|
|
.prose ul > li {
|
|
position: relative;
|
|
padding-left: 2em;
|
|
}
|
|
/* Enhance active TOC item */
|
|
.toc a.border-blue-500 {
|
|
background: linear-gradient(90deg, #dbeafe 60%, transparent);
|
|
border-left: 4px solid #3b82f6 !important;
|
|
color: #1d4ed8 !important;
|
|
font-weight: 600;
|
|
}
|
|
/* Card shadow for main content */
|
|
.main-content {
|
|
box-shadow: 0 8px 32px 0 rgba(30, 64, 175, 0.10), 0 1.5px 4px 0 rgba(59,130,246,0.04);
|
|
border-radius: 1.25rem;
|
|
background: linear-gradient(135deg, #f8fafc 80%, #f0fdfa 100%);
|
|
}
|
|
</style> |