Md Afiq Iskandar e4548647b5 Update Documentation and Configuration for Corrad ProcessMaker
- 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.
2025-07-24 17:17:11 +08:00

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>