Refactor Documentation Page for Improved Readability and Consistency

- Standardized quotation marks in the `index.vue` file for consistency.
- Enhanced code readability by formatting and organizing the structure of the document.
- Improved the styling of various elements to ensure a cleaner layout and better user experience.
- Adjusted event handling for document links and scroll events to enhance functionality and maintainability.
This commit is contained in:
Md Afiq Iskandar 2025-07-29 13:23:38 +08:00
parent e4b1c7e444
commit 9cf10a4596

View File

@ -39,8 +39,8 @@ renderer.heading = function (text, level, raw) {
};
// 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();
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>`;
}
@ -71,9 +71,9 @@ onMounted(async () => {
.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, "")
@ -140,8 +140,11 @@ onUnmounted(() => {
// 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;
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();
@ -153,30 +156,34 @@ function handleScrollSpy() {
}
onMounted(() => {
document.querySelector('.main-content')?.addEventListener('scroll', handleScrollSpy);
window.addEventListener('scroll', handleScrollSpy);
document
.querySelector(".main-content")
?.addEventListener("scroll", handleScrollSpy);
window.addEventListener("scroll", handleScrollSpy);
});
onUnmounted(() => {
document.querySelector('.main-content')?.removeEventListener('scroll', handleScrollSpy);
window.removeEventListener('scroll', handleScrollSpy);
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');
const target = e.target.closest(".doc-link");
if (target && target.dataset.doc) {
e.preventDefault();
e.stopPropagation();
selectDoc(target.dataset.doc);
}
};
document.addEventListener('click', handler);
document.addEventListener("click", handler);
window.__docLinkHandler = handler;
});
onUnmounted(() => {
if (window.__docLinkHandler) {
document.removeEventListener('click', window.__docLinkHandler);
document.removeEventListener("click", window.__docLinkHandler);
delete window.__docLinkHandler;
}
});
@ -367,7 +374,9 @@ watch(selected, () => {
<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">
<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
@ -391,8 +400,13 @@ watch(selected, () => {
<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
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"
@ -401,7 +415,7 @@ watch(selected, () => {
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"
@ -421,7 +435,9 @@ watch(selected, () => {
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">
<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 -->
@ -436,12 +452,16 @@ watch(selected, () => {
:key="category"
class="mb-6"
>
<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wide mb-3">
<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)"
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"
@ -459,8 +479,12 @@ watch(selected, () => {
>
<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>
<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"
@ -476,7 +500,9 @@ watch(selected, () => {
<!-- Search Results -->
<div v-else class="space-y-1">
<div
v-if="filteredDocs.filter((doc) => doc.order !== 999).length === 0"
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 }}"
@ -494,7 +520,9 @@ watch(selected, () => {
>
<div class="flex items-center justify-between">
<span>{{ doc.title }}</span>
<span class="text-xs text-gray-400">{{ doc.readingTime }} min</span>
<span class="text-xs text-gray-400"
>{{ doc.readingTime }} min</span
>
</div>
</button>
</div>
@ -506,17 +534,27 @@ watch(selected, () => {
<div class="flex gap-6">
<!-- Document Content -->
<div class="flex-1">
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<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" />
<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>
<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>
@ -526,7 +564,9 @@ watch(selected, () => {
<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">
<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>
@ -542,7 +582,10 @@ watch(selected, () => {
</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" />
<Icon
name="material-symbols:schedule"
class="w-4 h-4"
/>
<span>{{ selectedDoc.readingTime }} min read</span>
</div>
</div>
@ -550,11 +593,26 @@ watch(selected, () => {
</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="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
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">
@ -572,14 +630,19 @@ watch(selected, () => {
</div>
<!-- Navigation Footer -->
<div class="px-8 py-6 bg-gray-50 border-t border-gray-200 flex justify-between items-center">
<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" />
<Icon
name="material-symbols:chevron-left"
class="mr-1"
/>
{{ previousDoc.title }}
</RsButton>
<div v-else></div>
@ -591,7 +654,10 @@ watch(selected, () => {
size="sm"
>
{{ nextDoc.title }}
<Icon name="material-symbols:chevron-right" class="ml-1" />
<Icon
name="material-symbols:chevron-right"
class="ml-1"
/>
</RsButton>
<div v-else></div>
</div>
@ -599,9 +665,17 @@ watch(selected, () => {
<!-- 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>
<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>
@ -611,8 +685,12 @@ watch(selected, () => {
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">
<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">
@ -622,7 +700,12 @@ watch(selected, () => {
@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' : '']"
:class="[
{ 'ml-4': item.level === 3 },
activeSection === item.id
? 'border-blue-500 text-blue-700 font-semibold bg-blue-50'
: '',
]"
>
{{ item.title }}
</a>
@ -670,8 +753,9 @@ watch(selected, () => {
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;
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;
@ -787,6 +871,7 @@ watch(selected, () => {
border: 1px solid #334155;
position: relative;
overflow-x: auto;
margin-bottom: 5px !important;
}
.prose :where(pre):not(:where([class~="not-prose"] *))::before {
@ -799,6 +884,12 @@ watch(selected, () => {
background: #3b82f6;
}
.prose :where(code[class^="language-"], code[class*=" language-"]) {
background: none !important;
color: inherit !important;
border: none !important;
}
.prose :where(code):not(:where([class~="not-prose"] *)) {
background: #f1f5f9;
color: #3b82f6;
@ -810,12 +901,18 @@ watch(selected, () => {
.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));
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);
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;
@ -848,7 +945,9 @@ watch(selected, () => {
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);
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;
}
@ -880,7 +979,11 @@ watch(selected, () => {
}
.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));
background: linear-gradient(
135deg,
rgba(59, 130, 246, 0.02),
rgba(99, 102, 241, 0.02)
);
}
/* Add some visual enhancements */
@ -895,7 +998,12 @@ watch(selected, () => {
left: -2rem;
width: 2px;
height: 100%;
background: linear-gradient(to bottom, transparent, rgba(59, 130, 246, 0.3), transparent);
background: linear-gradient(
to bottom,
transparent,
rgba(59, 130, 246, 0.3),
transparent
);
border-radius: 1px;
}
@ -939,7 +1047,8 @@ html {
/* Transitions */
.transition-colors {
transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
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;
}
@ -955,13 +1064,13 @@ html {
.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;
@ -973,13 +1082,13 @@ html {
display: inline-block;
margin-right: 0.5em;
}
.prose blockquote p strong:contains('Note:') {
.prose blockquote p strong:contains("Note:") {
color: #2563eb;
}
.prose blockquote p strong:contains('Warning:') {
.prose blockquote p strong:contains("Warning:") {
color: #f59e42;
}
.prose blockquote p strong:contains('Tip:') {
.prose blockquote p strong:contains("Tip:") {
color: #10b981;
}
.prose blockquote {
@ -988,7 +1097,7 @@ html {
padding: 1.5rem 2rem;
margin: 2rem 0;
border-radius: 0.75rem;
box-shadow: 0 2px 8px 0 rgba(59,130,246,0.04);
box-shadow: 0 2px 8px 0 rgba(59, 130, 246, 0.04);
}
/* Responsive tables */
.prose table {
@ -997,7 +1106,8 @@ html {
overflow-x: auto;
border-radius: 0.5rem;
}
.prose th, .prose td {
.prose th,
.prose td {
white-space: nowrap;
}
/* Code block improvements */
@ -1021,7 +1131,9 @@ html {
border: 1px solid #e2e8f0;
}
/* Spacing tweaks for readability */
.prose p, .prose ul, .prose ol {
.prose p,
.prose ul,
.prose ol {
margin-bottom: 1.5em;
}
.prose h2 {
@ -1054,26 +1166,26 @@ hr {
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);
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 */
.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 */
.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 */
.prose blockquote p strong:contains("Tip:")::before {
content: "\1F4A1"; /* lightbulb icon */
color: #10b981;
font-size: 1.2em;
margin-right: 0.5em;
@ -1084,7 +1196,7 @@ hr {
}
/* Custom bullets for lists */
.prose ul > li::before {
content: '\2022';
content: "\2022";
color: #3b82f6;
font-size: 1.2em;
margin-right: 0.75em;
@ -1105,8 +1217,10 @@ hr {
}
/* 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);
box-shadow:
0 8px 32px 0 rgba(30, 64, 175, 0.1),
0 1.5px 4px 0 rgba(59, 130, 246, 0.04);
border-radius: 1.25rem;
background: linear-gradient(135deg, #f8fafc 80%, #f0fdfa 100%);
}
</style>
</style>