SUMMARY: Implemented Phase 3 Tasks 3.6.1 and 3.6.2: Enhanced documentation with interactive code copy buttons and collapsible table of contents. CHANGES: 1. Created code-copy-button.js (new): - Auto-detects all <pre> code blocks - Adds "Copy" button to top-right of each block - Clipboard API with fallback for older browsers - Visual feedback: "✓ Copied!" on success - Styled to work on dark code backgrounds - Listens for 'documentLoaded' event for dynamic content - Accessible with aria-label 2. Created toc.js (new): - Automatically builds TOC from h1, h2, h3 headings - Sticky sidebar on desktop (lg:block) - Collapsible on mobile with toggle button - Scroll spy with Intersection Observer - Highlights current section - Smooth scroll to sections - Updates URL hash on navigation - Auto-collapses on mobile after clicking link 3. Updated docs-viewer.html: - Added TOC sidebar (sticky, desktop-only) - Improved layout with flex containers - Added both components to script imports - Maintained existing document viewer functionality FEATURES: Code Copy Buttons: - Button text: "Copy" → "✓ Copied!" → "Copy" - 2-second success/error feedback - Works on all <pre><code> blocks - Respects code indentation Table of Contents: - Auto-generated from headings with IDs - 3-level hierarchy (h1, h2, h3) - Visual active indicator (blue border + bold) - Mobile toggle with chevron icon - Sticky positioning on desktop - Smooth scroll behavior ACCESSIBILITY: ✓ Zero CSP violations maintained ✓ Keyboard navigation supported ✓ ARIA labels on interactive elements ✓ Semantic HTML (nav, aside) ✓ Focus indicators PERFORMANCE: - Intersection Observer for scroll spy (better than scroll listeners) - Minimal DOM manipulation - CSS transitions for smooth UX - Lazy initialization UI_TRANSFORMATION_PROJECT_PLAN.md: ✓ Phase 3 Task 3.6.1: Code snippet copy buttons ✓ Phase 3 Task 3.6.2: Collapsible table of contents NEXT STEPS: - Deploy to production for testing - Phase 3 Task 3.2: Interactive architecture diagram (complex, deferred) 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
5.1 KiB
JavaScript
179 lines
5.1 KiB
JavaScript
/**
|
|
* 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;
|
|
}
|