diff --git a/public/docs-viewer.html b/public/docs-viewer.html index 2b9ce0c3..fe33935d 100644 --- a/public/docs-viewer.html +++ b/public/docs-viewer.html @@ -51,8 +51,17 @@ -
-
+
+
+
+
+ + +
@@ -60,6 +69,8 @@ + + diff --git a/public/js/components/code-copy-button.js b/public/js/components/code-copy-button.js new file mode 100644 index 00000000..07bf5f2c --- /dev/null +++ b/public/js/components/code-copy-button.js @@ -0,0 +1,179 @@ +/** + * Code Copy Button Component + * Tractatus Framework - Phase 3: Interactive Documentation + * + * Adds "Copy" buttons to all code blocks for easy copying + * Shows success feedback on copy + */ + +class CodeCopyButtons { + constructor() { + this.buttonClass = 'code-copy-btn'; + this.successClass = 'code-copy-success'; + this.init(); + } + + init() { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.addCopyButtons()); + } else { + this.addCopyButtons(); + } + + console.log('[CodeCopyButtons] Initialized'); + } + + addCopyButtons() { + // Find all code blocks (pre elements) + const codeBlocks = document.querySelectorAll('pre'); + console.log(`[CodeCopyButtons] Found ${codeBlocks.length} code blocks`); + + codeBlocks.forEach((pre, index) => { + // Skip if already has a copy button + if (pre.querySelector(`.${this.buttonClass}`)) { + return; + } + + // Make pre relative positioned for absolute button + pre.style.position = 'relative'; + + // Create copy button + const button = this.createCopyButton(pre, index); + + // Add button to pre element + pre.appendChild(button); + }); + } + + createCopyButton(pre, index) { + const button = document.createElement('button'); + button.className = `${this.buttonClass} absolute top-2 right-2 px-3 py-1 text-xs font-medium rounded transition-all duration-200`; + button.style.cssText = ` + background: rgba(255, 255, 255, 0.1); + color: #e5e7eb; + border: 1px solid rgba(255, 255, 255, 0.2); + `; + button.textContent = 'Copy'; + button.setAttribute('aria-label', 'Copy code to clipboard'); + button.setAttribute('data-code-index', index); + + // Add hover styles via class + button.addEventListener('mouseenter', () => { + button.style.background = 'rgba(255, 255, 255, 0.2)'; + }); + + button.addEventListener('mouseleave', () => { + if (!button.classList.contains(this.successClass)) { + button.style.background = 'rgba(255, 255, 255, 0.1)'; + } + }); + + // Add click handler + button.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + this.copyCode(pre, button); + }); + + return button; + } + + async copyCode(pre, button) { + // Get code content (find code element inside pre) + const codeElement = pre.querySelector('code'); + const code = codeElement ? codeElement.textContent : pre.textContent; + + try { + // Use Clipboard API + await navigator.clipboard.writeText(code); + + // Show success feedback + this.showSuccess(button); + + console.log('[CodeCopyButtons] Code copied to clipboard'); + } catch (err) { + console.error('[CodeCopyButtons] Failed to copy code:', err); + + // Fallback: try using execCommand + this.fallbackCopy(code, button); + } + } + + fallbackCopy(text, button) { + // Create temporary textarea + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + + try { + textarea.select(); + const successful = document.execCommand('copy'); + + if (successful) { + this.showSuccess(button); + console.log('[CodeCopyButtons] Code copied using fallback method'); + } else { + this.showError(button); + } + } catch (err) { + console.error('[CodeCopyButtons] Fallback copy failed:', err); + this.showError(button); + } finally { + document.body.removeChild(textarea); + } + } + + showSuccess(button) { + button.classList.add(this.successClass); + button.textContent = '✓ Copied!'; + button.style.background = 'rgba(16, 185, 129, 0.3)'; // Green + button.style.borderColor = 'rgba(16, 185, 129, 0.5)'; + button.style.color = '#d1fae5'; + + // Reset after 2 seconds + setTimeout(() => { + button.classList.remove(this.successClass); + button.textContent = 'Copy'; + button.style.background = 'rgba(255, 255, 255, 0.1)'; + button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; + button.style.color = '#e5e7eb'; + }, 2000); + } + + showError(button) { + button.textContent = '✗ Failed'; + button.style.background = 'rgba(239, 68, 68, 0.3)'; // Red + button.style.borderColor = 'rgba(239, 68, 68, 0.5)'; + + // Reset after 2 seconds + setTimeout(() => { + button.textContent = 'Copy'; + button.style.background = 'rgba(255, 255, 255, 0.1)'; + button.style.borderColor = 'rgba(255, 255, 255, 0.2)'; + }, 2000); + } + + // Public method to refresh buttons (useful for dynamically loaded content) + refresh() { + this.addCopyButtons(); + } +} + +// Auto-initialize when script loads +if (typeof window !== 'undefined') { + window.codeCopyButtons = new CodeCopyButtons(); + + // Listen for custom event from document viewer for dynamic content + document.addEventListener('documentLoaded', () => { + console.log('[CodeCopyButtons] Document loaded, refreshing buttons'); + window.codeCopyButtons.refresh(); + }); +} + +// Export for module usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = CodeCopyButtons; +} diff --git a/public/js/components/toc.js b/public/js/components/toc.js new file mode 100644 index 00000000..0fa1dd2d --- /dev/null +++ b/public/js/components/toc.js @@ -0,0 +1,268 @@ +/** + * Table of Contents Component + * Tractatus Framework - Phase 3: Interactive Documentation + * + * Creates sticky TOC sidebar on desktop, collapsible on mobile + * Highlights current section on scroll + * Smooth scroll to sections + */ + +class TableOfContents { + constructor(options = {}) { + this.contentSelector = options.contentSelector || '#document-viewer'; + this.tocSelector = options.tocSelector || '#table-of-contents'; + this.headingSelector = options.headingSelector || 'h1, h2, h3'; + this.activeClass = 'toc-active'; + this.collapsedClass = 'toc-collapsed'; + + this.headings = []; + this.tocLinks = []; + this.currentActiveIndex = -1; + + this.init(); + } + + init() { + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => this.build()); + } else { + this.build(); + } + + console.log('[TOC] Initialized'); + } + + build() { + const tocContainer = document.querySelector(this.tocSelector); + if (!tocContainer) { + console.warn('[TOC] TOC container not found:', this.tocSelector); + return; + } + + const content = document.querySelector(this.contentSelector); + if (!content) { + console.warn('[TOC] Content container not found:', this.contentSelector); + return; + } + + // Find all headings in content + this.headings = Array.from(content.querySelectorAll(this.headingSelector)); + + if (this.headings.length === 0) { + console.log('[TOC] No headings found, hiding TOC'); + tocContainer.style.display = 'none'; + return; + } + + console.log(`[TOC] Found ${this.headings.length} headings`); + + // Generate IDs for headings if they don't have them + this.headings.forEach((heading, index) => { + if (!heading.id) { + heading.id = `toc-heading-${index}`; + } + }); + + // Build TOC HTML + const tocHTML = this.buildTOCHTML(); + tocContainer.innerHTML = tocHTML; + + // Store TOC links + this.tocLinks = Array.from(tocContainer.querySelectorAll('a')); + + // Add scroll spy + this.initScrollSpy(); + + // Add smooth scroll + this.initSmoothScroll(); + + // Add mobile toggle functionality + this.initMobileToggle(); + } + + buildTOCHTML() { + let html = ''; + + return html; + } + + initScrollSpy() { + // Use Intersection Observer for better performance + const observerOptions = { + rootMargin: '-20% 0px -35% 0px', + threshold: 0 + }; + + const observer = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + const id = entry.target.id; + this.setActiveLink(id); + } + }); + }, observerOptions); + + // Observe all headings + this.headings.forEach(heading => { + observer.observe(heading); + }); + + console.log('[TOC] Scroll spy initialized'); + } + + setActiveLink(targetId) { + // Remove active class from all links + this.tocLinks.forEach(link => { + link.classList.remove(this.activeClass); + link.classList.remove('border-blue-600', 'text-blue-600', 'font-semibold'); + link.classList.add('border-transparent', 'text-gray-700'); + }); + + // Add active class to current link + const activeLink = this.tocLinks.find(link => link.dataset.target === targetId); + if (activeLink) { + activeLink.classList.add(this.activeClass); + activeLink.classList.remove('border-transparent', 'text-gray-700'); + activeLink.classList.add('border-blue-600', 'text-blue-600', 'font-semibold'); + } + } + + initSmoothScroll() { + this.tocLinks.forEach(link => { + link.addEventListener('click', (e) => { + e.preventDefault(); + const targetId = link.dataset.target; + const targetElement = document.getElementById(targetId); + + if (targetElement) { + // Smooth scroll to target + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + + // Update URL hash without jumping + if (history.pushState) { + history.pushState(null, null, `#${targetId}`); + } else { + window.location.hash = targetId; + } + + // On mobile, collapse TOC after clicking + if (window.innerWidth < 768) { + this.collapseTOC(); + } + } + }); + }); + + console.log('[TOC] Smooth scroll initialized'); + } + + initMobileToggle() { + const toggleButton = document.getElementById('toc-toggle'); + const tocList = document.getElementById('toc-list'); + + if (!toggleButton || !tocList) return; + + toggleButton.addEventListener('click', () => { + const isCollapsed = tocList.classList.contains('hidden'); + + if (isCollapsed) { + this.expandTOC(); + } else { + this.collapseTOC(); + } + }); + + // Start collapsed on mobile + if (window.innerWidth < 768) { + this.collapseTOC(); + } + + console.log('[TOC] Mobile toggle initialized'); + } + + collapseTOC() { + const tocList = document.getElementById('toc-list'); + const toggleButton = document.getElementById('toc-toggle'); + + if (tocList) { + tocList.classList.add('hidden'); + } + + if (toggleButton) { + const svg = toggleButton.querySelector('svg'); + if (svg) { + svg.style.transform = 'rotate(0deg)'; + } + } + } + + expandTOC() { + const tocList = document.getElementById('toc-list'); + const toggleButton = document.getElementById('toc-toggle'); + + if (tocList) { + tocList.classList.remove('hidden'); + } + + if (toggleButton) { + const svg = toggleButton.querySelector('svg'); + if (svg) { + svg.style.transform = 'rotate(180deg)'; + } + } + } + + // Public method to rebuild TOC (useful for dynamically loaded content) + rebuild() { + this.build(); + } +} + +// Auto-initialize when script loads (if TOC container exists) +if (typeof window !== 'undefined') { + window.tocInstance = new TableOfContents(); + + // Listen for custom event from document viewer for dynamic content + document.addEventListener('documentLoaded', () => { + console.log('[TOC] Document loaded, rebuilding TOC'); + window.tocInstance.rebuild(); + }); +} + +// Export for module usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = TableOfContents; +}