tractatus/public/js/components/interactive-diagram.js
TheFlow 25fccdde4b feat(i18n): add interactive diagram modal translations for EN/DE/FR
- Added diagram_services section to all three language JSON files
- Modified interactive-diagram.js to load translations from i18n system
- Added language change event listeners to update modals dynamically
- Removed hardcoded English serviceData from JavaScript
- Modals now fully translate when language is switched
2025-10-26 13:15:50 +13:00

332 lines
10 KiB
JavaScript

/**
* 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');
}
// 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 = `
<div class="flex items-start mb-4">
<div class="flex items-center space-x-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center text-2xl service-icon-box" data-color="${service.color}">
${service.icon}
</div>
<div>
<h3 class="text-xl font-bold text-gray-900">${service.name}</h3>
<span class="text-xs font-medium text-gray-600 uppercase">${service.shortName}</span>
</div>
</div>
</div>
<p class="text-gray-700 mb-4 leading-relaxed">${service.description}</p>
<div class="mb-4">
<h4 class="text-sm font-semibold text-gray-900 mb-2 uppercase">Key Features</h4>
<ul class="space-y-2" id="service-features-list">
${service.details.map(detail => `
<li class="flex items-start text-sm text-gray-700">
<svg class="w-4 h-4 mr-2 mt-0.5 flex-shrink-0 service-check-icon" data-color="${service.color}" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"/>
</svg>
<span>${detail}</span>
</li>
`).join('')}
</ul>
</div>
<div class="text-xs rounded px-3 py-2 bg-opacity-20 service-promise-badge" data-color="${service.color}">
<strong class="service-promise-text" data-color="${service.color}">Early Promise:</strong>
<span class="text-gray-800">${service.promise}</span>
</div>
`;
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();
}
if (typeof module !== 'undefined' && module.exports) {
module.exports = InteractiveDiagram;
}
// Listen for language changes and reload translations
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);
}
}
}
}
// Initialize and listen for language changes
if (typeof window !== 'undefined') {
window.interactiveDiagram = new InteractiveDiagram();
// Listen for i18n initialization and language changes
window.addEventListener('i18nInitialized', () => {
if (window.interactiveDiagram) {
window.interactiveDiagram.handleLanguageChange();
// Listen for language changes and reload translations
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);
}
}
}
}
// Initialize and listen for language changes
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;
}