/** * Interactive Architecture Diagram Component * Tractatus Framework - Phase 3: Interactive Architecture Diagram * * Handles click/hover interactions on the hexagonal service diagram * Shows service details in a side panel */ class InteractiveDiagram { constructor() { this.activeService = null; this.loadTranslations(); this.init(); } loadTranslations() { // Get translations from i18n system const i18n = window.i18nTranslations || {}; const diagram = i18n.diagram_services || {}; // Define static properties (icons and colors) const staticProps = { overview: { color: '#0ea5e9', icon: '⚙️' }, boundary: { color: '#10b981', icon: '🔒' }, instruction: { color: '#6366f1', icon: '📋' }, validator: { color: '#8b5cf6', icon: '✓' }, pressure: { color: '#f59e0b', icon: '⚡' }, metacognitive: { color: '#ec4899', icon: '💡' }, deliberation: { color: '#14b8a6', icon: '👥' } }; // Build serviceData from translations this.serviceData = {}; Object.keys(staticProps).forEach(serviceId => { const trans = diagram[serviceId] || {}; const details = []; // Collect detail1-detail6 (some services have 4, overview has 6) for (let i = 1; i <= 6; i++) { if (trans[`detail${i}`]) { details.push(trans[`detail${i}`]); } } this.serviceData[serviceId] = { name: trans.name || serviceId, shortName: trans.shortName || serviceId, color: staticProps[serviceId].color, icon: staticProps[serviceId].icon, description: trans.description || '', details: details, promise: trans.promise || '' }; }); console.log('[InteractiveDiagram] Loaded translations for', Object.keys(this.serviceData).length, 'services'); } handleLanguageChange() { console.log('[InteractiveDiagram] Language changed, reloading translations'); this.loadTranslations(); // Re-render current service if one is active if (this.activeService) { const service = this.serviceData[this.activeService]; if (service) { this.renderServicePanel(service); } } } init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.setup()); } else { this.setup(); } console.log('[InteractiveDiagram] Initialized'); } setup() { // SVG is loaded via tag, need to access its contentDocument const objectElement = document.getElementById('interactive-svg-object'); if (!objectElement) { console.warn('[InteractiveDiagram] SVG object element not found'); return; } // Wait for object to load with retry mechanism const initializeSVG = () => { const svgDoc = objectElement.contentDocument; if (!svgDoc) { console.warn('[InteractiveDiagram] Could not access SVG contentDocument, retrying...'); setTimeout(initializeSVG, 100); return; } // The SVG is the document element itself, or we can query for it let svg = svgDoc.getElementById('interactive-arch-diagram'); if (!svg) { // Try getting the root SVG element svg = svgDoc.documentElement; console.log('[InteractiveDiagram] Using documentElement as SVG'); } if (!svg) { console.warn('[InteractiveDiagram] SVG diagram not found in contentDocument'); return; } // Verify it's actually an SVG element (case-insensitive check) const tagName = svg.tagName ? svg.tagName.toLowerCase() : ''; if (tagName !== 'svg') { console.warn('[InteractiveDiagram] Element found but not SVG, tagName:', tagName, '- retrying...'); // This is the race condition - contentDocument is HTML, not SVG yet setTimeout(initializeSVG, 100); return; } // Store reference to SVG document for later use this.svgDoc = svgDoc; this.svg = svg; const nodes = svg.querySelectorAll('.service-node'); console.log(`[InteractiveDiagram] Found ${nodes.length} service nodes`); if (nodes.length === 0) { console.warn('[InteractiveDiagram] No service nodes found in SVG'); return; } nodes.forEach(node => { const serviceId = node.getAttribute('data-service'); // Click handler node.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.showServiceDetails(serviceId); }, true); // Touch support for mobile devices node.addEventListener('touchstart', (e) => { e.preventDefault(); const serviceId = node.getAttribute('data-service'); this.showServiceDetails(serviceId); }, { passive: false }); // Hover effects node.addEventListener('mouseenter', () => { this.highlightService(serviceId); }); node.addEventListener('mouseleave', () => { this.unhighlightService(serviceId); }); // Add pointer cursor via JavaScript (CSP-compliant) node.style.cursor = 'pointer'; }); this.addKeyboardNavigation(nodes); // Show initial state (overview) this.showServiceDetails('overview'); console.log('[InteractiveDiagram] Setup complete, showing overview'); }; // FIXED: Better load detection with SVG verification const checkAndInit = () => { const svgDoc = objectElement.contentDocument; // Check if contentDocument exists and is actually SVG if (svgDoc && svgDoc.documentElement) { const rootTagName = svgDoc.documentElement.tagName ? svgDoc.documentElement.tagName.toLowerCase() : ''; if (rootTagName === 'svg') { // It's an SVG - safe to initialize console.log('[InteractiveDiagram] SVG detected in contentDocument, initializing'); initializeSVG(); return true; } else { console.log('[InteractiveDiagram] contentDocument exists but root is:', rootTagName, '- not ready yet'); return false; } } return false; }; // Try immediate initialization if already loaded if (!checkAndInit()) { // Not ready yet - wait for load event console.log('[InteractiveDiagram] Waiting for object to load...'); objectElement.addEventListener('load', () => { console.log('[InteractiveDiagram] Object load event fired'); // Small delay to ensure contentDocument is fully parsed setTimeout(() => { if (!checkAndInit()) { // Still not ready - start retry mechanism initializeSVG(); } }, 50); }); // Also try periodic checks as fallback let retryCount = 0; const maxRetries = 20; const retryInterval = setInterval(() => { retryCount++; if (checkAndInit() || retryCount >= maxRetries) { clearInterval(retryInterval); if (retryCount >= maxRetries) { console.error('[InteractiveDiagram] Failed to load SVG after', maxRetries, 'retries'); } } }, 100); } } highlightService(serviceId) { if (!this.svg) return; const connectionLine = this.svg.querySelector(`#conn-${serviceId}`); if (connectionLine) { connectionLine.classList.add('active'); } const node = this.svg.querySelector(`#node-${serviceId}`); if (node) { node.classList.add('hover'); } } unhighlightService(serviceId) { if (!this.svg) return; if (this.activeService === serviceId) return; const connectionLine = this.svg.querySelector(`#conn-${serviceId}`); if (connectionLine) { connectionLine.classList.remove('active'); } const node = this.svg.querySelector(`#node-${serviceId}`); if (node) { node.classList.remove('hover'); } } showServiceDetails(serviceId) { const service = this.serviceData[serviceId]; if (!service) { console.error('[InteractiveDiagram] Service not found:', serviceId); return; } this.activeService = serviceId; if (this.svg) { this.svg.querySelectorAll('.service-node').forEach(n => n.classList.remove('active')); this.svg.querySelectorAll('.connection-line').forEach(l => l.classList.remove('active')); const node = this.svg.querySelector(`#node-${serviceId}`); if (node) { node.classList.add('active'); } const connectionLine = this.svg.querySelector(`#conn-${serviceId}`); if (connectionLine) { connectionLine.classList.add('active'); } } this.renderServicePanel(service); console.log('[InteractiveDiagram] Showing details for:', service.name); } renderServicePanel(service) { const panel = document.getElementById('service-detail-panel'); if (!panel) { console.error('[InteractiveDiagram] Service detail panel not found in DOM'); return; } // Update border color to match selected service panel.style.borderColor = service.color; panel.style.borderWidth = '2px'; const html = `
${service.icon}

${service.name}

${service.shortName}

${service.description}

Key Features

Early Promise: ${service.promise}
`; panel.innerHTML = html; // Apply styles via JavaScript (CSP-compliant) const iconBox = panel.querySelector('.service-icon-box'); if (iconBox) { const color = iconBox.getAttribute('data-color'); iconBox.style.background = `linear-gradient(135deg, ${color} 0%, ${color}dd 100%)`; } // Style all check icons const checkIcons = panel.querySelectorAll('.service-check-icon'); checkIcons.forEach(icon => { const color = icon.getAttribute('data-color'); icon.style.color = color; }); // Style promise badge const promiseBadge = panel.querySelector('.service-promise-badge'); if (promiseBadge) { const color = promiseBadge.getAttribute('data-color'); promiseBadge.style.backgroundColor = color; } // Style promise text const promiseText = panel.querySelector('.service-promise-text'); if (promiseText) { const color = promiseText.getAttribute('data-color'); promiseText.style.color = color; } } addKeyboardNavigation(nodes) { nodes.forEach((node, index) => { node.setAttribute('tabindex', '0'); node.setAttribute('role', 'button'); node.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const serviceId = node.getAttribute('data-service'); this.showServiceDetails(serviceId); } }); }); } } if (typeof window !== 'undefined') { window.interactiveDiagram = new InteractiveDiagram(); // Listen for i18n initialization window.addEventListener('i18nInitialized', () => { if (window.interactiveDiagram) { window.interactiveDiagram.handleLanguageChange(); } }); // Listen for language changes window.addEventListener('languageChanged', () => { if (window.interactiveDiagram) { window.interactiveDiagram.handleLanguageChange(); } }); } if (typeof module !== 'undefined' && module.exports) { module.exports = InteractiveDiagram; }