- 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
332 lines
10 KiB
JavaScript
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;
|
|
}
|