/** * Document Cards Component * Renders document sections as interactive cards */ class DocumentCards { constructor(containerId) { this.container = document.getElementById(containerId); this.currentDocument = null; this.modalViewer = new ModalViewer(); this.legendVisible = false; } /** * Get translations for color legend */ getLegendTranslations() { const lang = (window.I18n && window.I18n.currentLang) || 'en'; const translations = { en: { title: 'Colour Guide', critical: 'Critical sections', conceptual: 'Conceptual explanations', practical: 'Practical guides', technical: 'Technical details', reference: 'Reference documentation' }, de: { title: 'Farbcode', critical: 'Kritische Abschnitte', conceptual: 'Konzeptionelle Erklärungen', practical: 'Praktische Anleitungen', technical: 'Technische Details', reference: 'Referenzdokumentation' }, fr: { title: 'Guide des couleurs', critical: 'Sections critiques', conceptual: 'Explications conceptuelles', practical: 'Guides pratiques', technical: 'Détails techniques', reference: 'Documentation de référence' } }; return translations[lang] || translations.en; } /** * Render document as card grid */ render(document) { if (!document || !document.sections || document.sections.length === 0) { this.renderTraditionalView(document); return; } this.currentDocument = document; // Create document header const headerHtml = this.renderHeader(document); // Render cards in original markdown order (no grouping) const cardsHtml = this.renderCardGrid(document.sections); this.container.innerHTML = ` ${headerHtml} ${cardsHtml} `; // Add event listeners after a brief delay to ensure DOM is ready setTimeout(() => { this.attachEventListeners(); }, 0); } /** * Render document header */ renderHeader(document) { const version = document.metadata?.version || ''; const dateUpdated = document.metadata?.date_updated ? new Date(document.metadata.date_updated).toLocaleDateString('en-US', { year: 'numeric', month: 'short' }) : ''; const versionText = version ? `v${version}` : ''; const metaText = [versionText, dateUpdated ? `Updated ${dateUpdated}` : ''] .filter(Boolean) .join(' | '); const hasToC = document.toc && document.toc.length > 0; const t = this.getLegendTranslations(); return `

${document.title}

${metaText ? `

${metaText}

` : ''} ${document.sections ? `

${document.sections.length} sections

` : ''}
${document.sections && document.sections.length > 0 ? ` ` : ''} ${hasToC ? ` ` : ''}
`; } /** * Group sections by category */ groupByCategory(sections) { const groups = { conceptual: [], practical: [], technical: [], reference: [], critical: [] }; sections.forEach(section => { const category = section.category || 'conceptual'; if (groups[category]) { groups[category].push(section); } else { groups.conceptual.push(section); } }); return groups; } /** * Render card grid */ renderCardGrid(sections) { const categoryConfig = { critical: { color: 'red' }, conceptual: { color: 'blue' }, practical: { color: 'green' }, technical: { color: 'purple' }, reference: { color: 'gray' } }; // Render all cards in original markdown order (no grouping) const html = `
${sections.map(section => { const category = section.category || 'conceptual'; const color = categoryConfig[category]?.color || 'blue'; return this.renderCard(section, color); }).join('')}
`; return html; } /** * Insert soft hyphens in CamelCase words for better wrapping */ insertSoftHyphens(text) { // Insert soft hyphens (­) before capital letters in CamelCase words // e.g., "InstructionPersistenceClassifier" → "Instruction­Persistence­Classifier" return text.replace(/([a-z])([A-Z])/g, '$1­$2'); } /** * Render individual card */ renderCard(section, color) { const levelIcons = { basic: '○', intermediate: '◐', advanced: '●' }; const technicalLevel = section.technicalLevel || 'basic'; const levelIcon = levelIcons[technicalLevel] || '○'; const levelLabel = technicalLevel.charAt(0).toUpperCase() + technicalLevel.slice(1); // Add soft hyphens to long titles for better wrapping const titleWithHyphens = this.insertSoftHyphens(section.title); const borderColor = { red: 'border-red-500', blue: 'border-blue-400', green: 'border-green-400', purple: 'border-purple-400', gray: 'border-gray-400' }[color] || 'border-blue-400'; const hoverColor = { red: 'hover:border-red-700 hover:shadow-red-100', blue: 'hover:border-blue-600 hover:shadow-blue-100', green: 'hover:border-green-600 hover:shadow-green-100', purple: 'hover:border-purple-600 hover:shadow-purple-100', gray: 'hover:border-gray-600 hover:shadow-gray-100' }[color] || 'hover:border-blue-600'; const bgColor = { red: 'bg-red-50', blue: 'bg-blue-50', green: 'bg-green-50', purple: 'bg-purple-50', gray: 'bg-gray-50' }[color] || 'bg-blue-50'; return `

${titleWithHyphens}

${section.excerpt}

${section.readingTime} min read ${levelIcon} ${levelLabel}
`; } /** * Fallback: render traditional view for documents without sections */ renderTraditionalView(document) { if (!document) return; this.container.innerHTML = `
${document.content_html}
`; } /** * Attach event listeners to cards */ attachEventListeners() { const cards = this.container.querySelectorAll('.doc-card'); cards.forEach(card => { card.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); const sectionSlug = card.dataset.sectionSlug; const section = this.currentDocument.sections.find(s => s.slug === sectionSlug); if (section) { this.modalViewer.show(section, this.currentDocument.sections); } }); }); // Attach ToC button listener const tocButton = document.getElementById('toc-button'); if (tocButton) { tocButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (typeof openToCModal === 'function') { openToCModal(); } }); } // Attach color legend toggle listener const legendButton = document.getElementById('color-legend-button'); const legendPopup = document.getElementById('color-legend-popup'); if (legendButton && legendPopup) { legendButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); legendPopup.classList.toggle('hidden'); this.legendVisible = !this.legendVisible; }); // Close legend when clicking outside document.addEventListener('click', (e) => { if (this.legendVisible && !legendButton.contains(e.target) && !legendPopup.contains(e.target)) { legendPopup.classList.add('hidden'); this.legendVisible = false; } }); } } } /** * Modal Viewer Component * Displays section content in a modal */ class ModalViewer { constructor() { this.modal = null; this.currentSection = null; this.allSections = []; this.currentIndex = 0; this.createModal(); } /** * Create modal structure */ createModal() { const modalHtml = `
`; document.body.insertAdjacentHTML('beforeend', modalHtml); this.modal = document.getElementById('section-modal'); this.attachModalListeners(); } /** * Show modal with section content */ show(section, allSections) { this.currentSection = section; this.allSections = allSections; this.currentIndex = allSections.findIndex(s => s.slug === section.slug); // Update content const titleEl = document.getElementById('modal-title'); const contentEl = document.getElementById('modal-content'); if (!titleEl || !contentEl) { return; } titleEl.textContent = section.title; // Remove duplicate title (H1 or H2) from content (it's already in modal header) let contentHtml = section.content_html; // Try removing h1 first, then h2 const firstH1Match = contentHtml.match(/]*>.*?<\/h1>/); if (firstH1Match) { contentHtml = contentHtml.replace(firstH1Match[0], ''); } else { const firstH2Match = contentHtml.match(/]*>.*?<\/h2>/); if (firstH2Match) { contentHtml = contentHtml.replace(firstH2Match[0], ''); } } contentEl.innerHTML = contentHtml; // Update navigation this.updateNavigation(); // Show modal this.modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; // Scroll to top of content contentEl.scrollTop = 0; } /** * Hide modal */ hide() { this.modal.style.display = 'none'; document.body.style.overflow = ''; } /** * Update navigation buttons */ updateNavigation() { const prevBtn = document.getElementById('modal-prev'); const nextBtn = document.getElementById('modal-next'); const progress = document.getElementById('modal-progress'); prevBtn.disabled = this.currentIndex === 0; nextBtn.disabled = this.currentIndex === this.allSections.length - 1; progress.textContent = `${this.currentIndex + 1} of ${this.allSections.length}`; } /** * Navigate to previous section */ showPrevious() { if (this.currentIndex > 0) { this.show(this.allSections[this.currentIndex - 1], this.allSections); } } /** * Navigate to next section */ showNext() { if (this.currentIndex < this.allSections.length - 1) { this.show(this.allSections[this.currentIndex + 1], this.allSections); } } /** * Attach modal event listeners */ attachModalListeners() { // Close button document.getElementById('modal-close').addEventListener('click', () => this.hide()); // Navigation buttons document.getElementById('modal-prev').addEventListener('click', () => this.showPrevious()); document.getElementById('modal-next').addEventListener('click', () => this.showNext()); // Close on background click this.modal.addEventListener('click', (e) => { if (e.target === this.modal) { this.hide(); } }); // Keyboard navigation document.addEventListener('keydown', (e) => { // Check if modal is visible using display style instead of hidden class if (this.modal.style.display === 'flex') { if (e.key === 'Escape') { this.hide(); } else if (e.key === 'ArrowLeft') { this.showPrevious(); } else if (e.key === 'ArrowRight') { this.showNext(); } } }); } }