UNPKG

rp-markdown-docs

Version:

A modern, beautiful documentation generator that converts markdown files into interactive HTML documentation sites

337 lines (289 loc) 12.5 kB
// Modern Documentation App class DocumentationApp { constructor() { this.config = window.__DOCS_CONFIG__ || {}; this.currentPage = null; this.searchIndex = []; this.darkMode = localStorage.getItem('darkMode') === 'true'; this.init(); } async init() { this.setupDOM(); this.setupEventListeners(); this.setupTheme(); await this.loadSearchIndex(); this.handleRouting(); // Remove loading screen const loading = document.querySelector('.loading'); if (loading) { loading.style.opacity = '0'; setTimeout(() => loading.remove(), 300); } } setupDOM() { const root = document.getElementById('root'); root.innerHTML = ` <div class="docs-container"> <header class="docs-header"> <div style="display: flex; align-items: center; gap: 1rem;"> <button class="btn btn-ghost mobile-menu-btn" style="display: none;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="12" x2="21" y2="12"></line> <line x1="3" y1="18" x2="21" y2="18"></line> </svg> </button> <div style="display: flex; align-items: center; gap: 0.75rem;"> <div style="width: 32px; height: 32px; background: linear-gradient(135deg, var(--primary-500), var(--accent-500)); border-radius: 8px; display: flex; align-items: center; justify-content: center;"> <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"> <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path> </svg> </div> <div> <h1 style="margin: 0; font-size: 1.25rem; font-weight: 700;">${this.config.title || 'Documentation'}</h1> <p style="margin: 0; font-size: 0.75rem; color: var(--neutral-500);">Technical Reference</p> </div> </div> </div> <div style="display: flex; align-items: center; gap: 1rem;"> <button class="btn btn-ghost theme-toggle"> <svg class="sun-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="5"></circle> <line x1="12" y1="1" x2="12" y2="3"></line> <line x1="12" y1="21" x2="12" y2="23"></line> <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line> <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line> <line x1="1" y1="12" x2="3" y2="12"></line> <line x1="21" y1="12" x2="23" y2="12"></line> <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line> <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line> </svg> <svg class="moon-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display: none;"> <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path> </svg> </button> </div> </header> <div style="display: flex; flex: 1; overflow: hidden;"> <aside class="docs-sidebar"> <div style="padding: 1.5rem;"> <div style="margin-bottom: 1.5rem;"> <input type="text" class="search-input" placeholder="Search documentation..." /> </div> <nav class="docs-nav"></nav> </div> </aside> <main class="docs-content"> <div class="content-area"></div> </main> </div> </div> <div class="mobile-overlay" style="display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 30;"></div> `; } setupEventListeners() { // Theme toggle document.querySelector('.theme-toggle').addEventListener('click', () => { this.toggleTheme(); }); // Mobile menu const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); const sidebar = document.querySelector('.docs-sidebar'); const overlay = document.querySelector('.mobile-overlay'); mobileMenuBtn.addEventListener('click', () => { sidebar.classList.toggle('open'); overlay.style.display = sidebar.classList.contains('open') ? 'block' : 'none'; }); overlay.addEventListener('click', () => { sidebar.classList.remove('open'); overlay.style.display = 'none'; }); // Search const searchInput = document.querySelector('.search-input'); searchInput.addEventListener('input', (e) => { this.handleSearch(e.target.value); }); // Handle responsive this.handleResize(); window.addEventListener('resize', () => this.handleResize()); // Handle navigation window.addEventListener('popstate', () => this.handleRouting()); } setupTheme() { document.documentElement.classList.toggle('dark', this.darkMode); this.updateThemeIcon(); } toggleTheme() { this.darkMode = !this.darkMode; localStorage.setItem('darkMode', this.darkMode.toString()); document.documentElement.classList.toggle('dark', this.darkMode); this.updateThemeIcon(); } updateThemeIcon() { const sunIcon = document.querySelector('.sun-icon'); const moonIcon = document.querySelector('.moon-icon'); if (this.darkMode) { sunIcon.style.display = 'none'; moonIcon.style.display = 'block'; } else { sunIcon.style.display = 'block'; moonIcon.style.display = 'none'; } } handleResize() { const mobileMenuBtn = document.querySelector('.mobile-menu-btn'); const isMobile = window.innerWidth < 1024; mobileMenuBtn.style.display = isMobile ? 'block' : 'none'; if (!isMobile) { document.querySelector('.docs-sidebar').classList.remove('open'); document.querySelector('.mobile-overlay').style.display = 'none'; } } async loadSearchIndex() { try { const response = await fetch('./search-index.json'); this.searchIndex = await response.json(); } catch (error) { console.warn('Search index not found'); this.searchIndex = this.config.pages || []; } } handleSearch(query) { const nav = document.querySelector('.docs-nav'); if (!query.trim()) { this.renderNavigation(); return; } const results = this.searchIndex.filter(page => page.title.toLowerCase().includes(query.toLowerCase()) || page.content.toLowerCase().includes(query.toLowerCase()) || (page.tags && page.tags.some(tag => tag.toLowerCase().includes(query.toLowerCase()))) ); nav.innerHTML = ` <div style="margin-bottom: 1rem;"> <h3 style="margin: 0; font-size: 0.875rem; font-weight: 600; color: var(--neutral-700);"> Search Results (${results.length}) </h3> </div> ${results.map(page => ` <a href="${page.path}" class="nav-item" data-page="${page.id}"> <div style="font-weight: 500;">${this.highlightText(page.title, query)}</div> ${page.content ? `<div style="font-size: 0.75rem; color: var(--neutral-500); margin-top: 0.25rem;">${this.highlightText(page.content.substring(0, 100), query)}...</div>` : ''} </a> `).join('')} `; this.attachNavListeners(); } highlightText(text, query) { if (!query) return text; const regex = new RegExp(`(${query})`, 'gi'); return text.replace(regex, '<mark style="background: var(--primary-200); color: var(--primary-800);">$1</mark>'); } renderNavigation() { const nav = document.querySelector('.docs-nav'); nav.innerHTML = this.renderNavItems(this.config.navigation || []); this.attachNavListeners(); } renderNavItems(items, depth = 0) { return items.map(item => { const hasChildren = item.children && item.children.length > 0; const isActive = this.currentPage === item.id; return ` <div> <a href="${item.path || '#'}" class="nav-item ${isActive ? 'active' : ''}" data-page="${item.id}" style="padding-left: ${depth * 1 + 1}rem;"> <div style="display: flex; align-items: center; gap: 0.5rem;"> ${hasChildren ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9,18 15,12 9,6"></polyline></svg>' : '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="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></svg>' } <span>${item.title}</span> </div> </a> ${hasChildren ? `<div style="margin-left: 1rem;">${this.renderNavItems(item.children, depth + 1)}</div>` : ''} </div> `; }).join(''); } attachNavListeners() { document.querySelectorAll('.nav-item').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const pageId = link.dataset.page; this.navigateToPage(pageId); // Close mobile menu if (window.innerWidth < 1024) { document.querySelector('.docs-sidebar').classList.remove('open'); document.querySelector('.mobile-overlay').style.display = 'none'; } }); }); } navigateToPage(pageId) { const page = this.findPage(pageId); if (!page) return; this.currentPage = pageId; history.pushState({ pageId }, page.title, page.path); this.renderPage(page); this.updateActiveNav(); } findPage(pageId) { return this.config.pages?.find(page => page.id === pageId); } renderPage(page) { const contentArea = document.querySelector('.content-area'); if (!page) { contentArea.innerHTML = ` <div style="text-align: center; padding: 4rem 2rem;"> <svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" style="color: var(--neutral-400); margin-bottom: 1.5rem;"> <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path> <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path> </svg> <h2 style="color: var(--neutral-700); margin-bottom: 0.5rem;">Welcome to Documentation</h2> <p style="color: var(--neutral-500);">Select a topic from the sidebar to get started.</p> </div> `; return; } contentArea.innerHTML = ` <article class="prose fade-in"> ${page.metadata?.description ? ` <div style="background: var(--primary-50); border: 1px solid var(--primary-200); border-radius: 0.75rem; padding: 1.5rem; margin-bottom: 2rem;"> <p style="margin: 0; color: var(--primary-800);">${page.metadata.description}</p> </div> ` : ''} ${page.content} ${page.metadata?.lastModified ? ` <div style="margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--neutral-200); font-size: 0.875rem; color: var(--neutral-500);"> Last updated: ${new Date(page.metadata.lastModified).toLocaleDateString()} </div> ` : ''} </article> `; // Scroll to top contentArea.scrollTop = 0; // Update document title document.title = `${page.title} - ${this.config.title}`; } updateActiveNav() { document.querySelectorAll('.nav-item').forEach(item => { item.classList.toggle('active', item.dataset.page === this.currentPage); }); } handleRouting() { const path = window.location.pathname; const page = this.config.pages?.find(p => p.path === path) || this.config.pages?.[0]; if (page) { this.currentPage = page.id; this.renderPage(page); } this.renderNavigation(); this.updateActiveNav(); } } // Initialize app when DOM is loaded if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new DocumentationApp()); } else { new DocumentationApp(); }