feat(internal/launchpad): add iframe viewer for prototypes and update links loader This commit introduces an iframe viewer in the launchpad to display prototypes directly within the application. It also updates the links loader to handle prototype links differently, opening them in the iframe instead of a new tab. The contributing guide has been updated to include a list of required development tools and recommended IDE setup, ensuring that contributors have the necessary tools to work on the project.
870 lines
40 KiB
HTML
870 lines
40 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Krow DevOps Launchpad</title>
|
|
<link rel="icon" type="image/x-icon" href="favicon.svg">
|
|
|
|
<!-- Tailwind CSS -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Mermaid -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.9.1/mermaid.min.js"></script>
|
|
|
|
<!-- Marked.js for Markdown parsing -->
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
|
|
<!-- Custom Tailwind Config -->
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
50: '#eff6ff',
|
|
100: '#dbeafe',
|
|
200: '#bfdbfe',
|
|
300: '#93c5fd',
|
|
400: '#60a5fa',
|
|
500: '#3b82f6',
|
|
600: '#2563eb',
|
|
700: '#1d4ed8',
|
|
800: '#1e40af',
|
|
900: '#1e3a8a',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
|
|
|
* {
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.gradient-bg {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
.card-hover {
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.card-hover:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
|
}
|
|
|
|
.nav-item {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.nav-item:hover {
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.badge-pulse {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: .7; }
|
|
}
|
|
|
|
.scrollbar-hide::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.scrollbar-hide {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
#diagram-container:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
/* Accordion styles */
|
|
.accordion-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
padding: 0.75rem 1rem;
|
|
font-size: 0.875rem;
|
|
color: #4b5563;
|
|
text-align: left;
|
|
background-color: #f9fafb;
|
|
border-radius: 0.5rem;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.accordion-button:hover {
|
|
background-color: #f3f4f6;
|
|
}
|
|
|
|
.accordion-button .chevron {
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.accordion-button[aria-expanded="true"] .chevron {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.accordion-panel {
|
|
overflow: hidden;
|
|
transition: max-height 0.3s ease-out;
|
|
max-height: 0;
|
|
}
|
|
|
|
/* Markdown styling */
|
|
.markdown-content { line-height: 1.7; color: #374151; }
|
|
.markdown-content h1 { font-size: 2em; font-weight: 700; margin-top: 1.5em; margin-bottom: 0.5em; padding-bottom: 0.3em; border-bottom: 2px solid #e5e7eb; color: #111827; }
|
|
.markdown-content h1:first-child { margin-top: 0; }
|
|
.markdown-content h2 { font-size: 1.5em; font-weight: 600; margin-top: 1.5em; margin-bottom: 0.5em; padding-bottom: 0.2em; border-bottom: 1px solid #e5e7eb; color: #111827; }
|
|
.markdown-content h3 { font-size: 1.25em; font-weight: 600; margin-top: 1.2em; margin-bottom: 0.5em; color: #111827; }
|
|
.markdown-content h4 { font-size: 1.1em; font-weight: 600; margin-top: 1em; margin-bottom: 0.5em; color: #111827; }
|
|
.markdown-content p { margin-bottom: 1em; }
|
|
.markdown-content ul, .markdown-content ol { margin-bottom: 1em; padding-left: 2em; }
|
|
.markdown-content ul { list-style-type: disc; }
|
|
.markdown-content ol { list-style-type: decimal; }
|
|
.markdown-content li { margin-bottom: 0.5em; }
|
|
.markdown-content code { background-color: #f3f4f6; padding: 0.2em 0.4em; border-radius: 0.25em; font-size: 0.9em; font-family: 'Courier New', monospace; color: #dc2626; }
|
|
.markdown-content pre { background-color: #1f2937; color: #f9fafb; padding: 1em; border-radius: 0.5em; overflow-x: auto; margin-bottom: 1em; }
|
|
.markdown-content pre code { background-color: transparent; padding: 0; color: inherit; }
|
|
.markdown-content blockquote { border-left: 4px solid #3b82f6; padding-left: 1em; margin: 1em 0; color: #6b7280; font-style: italic; }
|
|
.markdown-content a { color: #3b82f6; text-decoration: underline; }
|
|
.markdown-content a:hover { color: #2563eb; }
|
|
.markdown-content table { width: 100%; border-collapse: collapse; margin-bottom: 1em; }
|
|
.markdown-content th, .markdown-content td { border: 1px solid #e5e7eb; padding: 0.5em; text-align: left; }
|
|
.markdown-content th { background-color: #f9fafb; font-weight: 600; }
|
|
.markdown-content img { max-width: 100%; height: auto; border-radius: 0.5em; margin: 1em 0; }
|
|
.markdown-content hr { border: none; border-top: 2px solid #e5e7eb; margin: 2em 0; }
|
|
|
|
/* Loading Overlay */
|
|
#auth-loading {
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: white; z-index: 50;
|
|
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
}
|
|
#login-screen {
|
|
display: none;
|
|
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
|
z-index: 40;
|
|
align-items: center; justify-content: center;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-gradient-to-br from-gray-50 to-gray-100 min-h-screen">
|
|
|
|
<!-- Auth Loading State -->
|
|
<div id="auth-loading">
|
|
<div class="w-16 h-16 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin mb-4"></div>
|
|
<p class="text-gray-500 font-medium">Verifying access...</p>
|
|
</div>
|
|
|
|
<!-- Login Screen -->
|
|
<div id="login-screen" class="flex">
|
|
<div class="bg-white p-8 rounded-2xl shadow-xl max-w-md w-full mx-4 text-center">
|
|
<div class="flex justify-center mb-6">
|
|
<img src="logo.svg" alt="KROW Logo" class="h-16 w-16" onerror="this.src='https://via.placeholder.com/64?text=KROW'">
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-gray-900 mb-2">Internal DevOps Launchpad</h1>
|
|
<p class="text-gray-500 mb-8">Please sign in to access internal development resources.</p>
|
|
|
|
<button id="google-signin-btn" class="w-full flex items-center justify-center space-x-3 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-medium py-3 px-4 rounded-xl transition-all shadow-sm">
|
|
<svg class="w-5 h-5" viewBox="0 0 24 24">
|
|
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
|
|
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
|
|
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
|
|
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
|
|
</svg>
|
|
<span>Sign in with Google</span>
|
|
</button>
|
|
<p id="login-error" class="mt-4 text-red-500 text-sm hidden"></p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content (Protected) -->
|
|
<div id="app-content" class="hidden flex h-screen overflow-hidden">
|
|
|
|
<!-- Sidebar -->
|
|
<aside class="w-72 bg-white shadow-xl flex flex-col border-r border-gray-200">
|
|
<!-- Logo Section -->
|
|
<div class="p-6 border-b border-gray-200">
|
|
<div class="flex items-center justify-center space-x-3">
|
|
<div class="w-12 h-12 rounded-xl flex items-center justify-center">
|
|
<img src="logo.svg" alt="Krow Logo" style="height: 60px;" onerror="this.style.display='none'">
|
|
</div>
|
|
<div>
|
|
<h1 class="text-xl font-bold text-gray-900">KROW DevOps</h1>
|
|
<p class="text-xs text-gray-500">Launchpad Hub</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation -->
|
|
<nav class="flex-1 overflow-y-auto scrollbar-hide p-4" id="sidebar-nav">
|
|
<a href="#" id="nav-home" onclick="showView('home', this)"
|
|
class="nav-item flex items-center space-x-3 px-4 py-3 rounded-xl text-gray-700 bg-primary-50 border border-primary-200 font-medium mb-2">
|
|
<svg class="w-5 h-5 text-primary-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6">
|
|
</path>
|
|
</svg>
|
|
<span>Home</span>
|
|
</a>
|
|
|
|
<!-- Dynamic diagrams section - ALL diagrams loaded here -->
|
|
<div id="dynamic-diagrams-section"></div>
|
|
|
|
<!-- Documentation section -->
|
|
<div id="documentation-section"></div>
|
|
</nav>
|
|
|
|
<!-- Footer -->
|
|
<div class="p-4 border-t border-gray-200 bg-gray-50">
|
|
<div class="flex flex-col space-y-2">
|
|
<div class="flex items-center space-x-2 text-xs text-gray-500">
|
|
<div class="w-2 h-2 bg-green-500 rounded-full badge-pulse"></div>
|
|
<span>System Online</span>
|
|
</div>
|
|
<div class="flex items-center justify-between pt-2 border-t border-gray-100">
|
|
<span id="user-email" class="text-xs text-gray-400 truncate max-w-[150px]"></span>
|
|
<button id="logout-btn" class="text-xs text-red-500 hover:text-red-700 font-medium">Sign Out</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Main Content Area -->
|
|
<main class="flex-1 overflow-y-auto">
|
|
|
|
<!-- Home View -->
|
|
<div id="home-view" class="p-8">
|
|
<!-- Header -->
|
|
<div class="mb-8">
|
|
<h2 class="text-3xl font-bold text-gray-900 mb-2">Krow DevOps Launchpad</h2>
|
|
<p class="text-gray-600">Central hub for KROW development and operations infrastructure</p>
|
|
</div>
|
|
|
|
<div id="links-container" class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
<!-- Links will be loaded dynamically -->
|
|
<div class="col-span-full flex justify-center p-12">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Diagram Viewer -->
|
|
<div id="diagram-viewer" class="hidden h-full flex flex-col p-8">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<h3 id="diagram-title" class="text-2xl font-bold text-gray-900"></h3>
|
|
<div class="flex items-center space-x-2 bg-white rounded-xl shadow-lg p-2 border border-gray-200">
|
|
<button id="zoomInBtn" class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
title="Zoom In">
|
|
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7"></path>
|
|
</svg>
|
|
</button>
|
|
<button id="zoomOutBtn" class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
title="Zoom Out">
|
|
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7"></path>
|
|
</svg>
|
|
</button>
|
|
<button id="resetBtn" class="p-2 hover:bg-gray-100 rounded-lg transition-colors"
|
|
title="Reset View">
|
|
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15">
|
|
</path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div id="diagram-container" tabindex="-1"
|
|
class="flex-1 bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden cursor-grab flex items-center justify-center p-8">
|
|
<!-- SVG will be loaded here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Viewer -->
|
|
<div id="document-viewer" class="hidden h-full flex flex-col p-8">
|
|
<div class="mb-6">
|
|
<h3 id="document-title" class="text-2xl font-bold text-gray-900"></h3>
|
|
</div>
|
|
<div id="document-container"
|
|
class="flex-1 bg-white rounded-2xl shadow-xl border border-gray-200 overflow-y-auto p-8 markdown-content">
|
|
<!-- Document content will be loaded here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Iframe Viewer (Prototypes) -->
|
|
<div id="iframe-viewer" class="hidden h-full flex flex-col p-4">
|
|
<div class="flex items-center justify-between mb-4 px-2">
|
|
<h3 id="iframe-title" class="text-xl font-bold text-gray-900"></h3>
|
|
<div class="flex items-center space-x-2">
|
|
<a id="iframe-external-link" href="#" target="_blank" class="text-sm text-blue-600 hover:underline flex items-center">
|
|
Open in new tab
|
|
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"></path></svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 bg-white rounded-2xl shadow-xl border border-gray-200 overflow-hidden relative">
|
|
<iframe id="content-iframe" class="w-full h-full border-0" src=""></iframe>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
</div>
|
|
|
|
<!-- Links Loader -->
|
|
<script src="assets/js/links-loader.js"></script>
|
|
|
|
<!-- Panzoom -->
|
|
<script src="https://cdn.jsdelivr.net/npm/@panzoom/panzoom@4.5.1/dist/panzoom.min.js"></script>
|
|
|
|
<!-- Firebase Auth Logic -->
|
|
<script type="module">
|
|
import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.8.0/firebase-app.js';
|
|
import { getAuth, GoogleAuthProvider, signInWithPopup, onAuthStateChanged, signOut } from 'https://www.gstatic.com/firebasejs/10.8.0/firebase-auth.js';
|
|
|
|
// Elements
|
|
const authLoading = document.getElementById('auth-loading');
|
|
const loginScreen = document.getElementById('login-screen');
|
|
const appContent = document.getElementById('app-content');
|
|
const googleBtn = document.getElementById('google-signin-btn');
|
|
const logoutBtn = document.getElementById('logout-btn');
|
|
const loginError = document.getElementById('login-error');
|
|
const userEmailSpan = document.getElementById('user-email');
|
|
|
|
let pendingError = null;
|
|
|
|
// Initialize Firebase
|
|
// Note: fetch('/__/firebase/init.json') works only on deployed Firebase Hosting or firebase serve
|
|
async function initAuth() {
|
|
try {
|
|
let config;
|
|
// Fetch config from hosting environment
|
|
const res = await fetch('/__/firebase/init.json');
|
|
if (!res.ok) throw new Error('Failed to load Firebase config. Ensure you are running via "make launchpad-dev" or deployed on Hosting.');
|
|
config = await res.json();
|
|
console.log("Firebase Config Loaded:", config.projectId);
|
|
|
|
const app = initializeApp(config);
|
|
const auth = getAuth(app);
|
|
|
|
// Auth State Observer
|
|
onAuthStateChanged(auth, async (user) => {
|
|
if (user) {
|
|
// User is signed in, check if allowed
|
|
const allowed = await checkAccess(user.email);
|
|
if (allowed) {
|
|
showApp(user);
|
|
} else {
|
|
pendingError = `Access Denied: <b>${user.email}</b> is not authorized.<br>Please contact the Krow Internal Developers or an Administrator to request access.`;
|
|
signOut(auth);
|
|
}
|
|
} else {
|
|
// User is signed out
|
|
if (pendingError) {
|
|
showLogin(pendingError);
|
|
pendingError = null;
|
|
} else {
|
|
showLogin();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Login Handler
|
|
googleBtn.addEventListener('click', () => {
|
|
// Clear previous errors
|
|
loginError.classList.add('hidden');
|
|
const provider = new GoogleAuthProvider();
|
|
signInWithPopup(auth, provider).catch((error) => {
|
|
console.error("Login failed:", error);
|
|
showLogin("Login failed: " + error.message);
|
|
});
|
|
});
|
|
|
|
// Logout Handler
|
|
logoutBtn.addEventListener('click', () => {
|
|
signOut(auth);
|
|
});
|
|
|
|
} catch (e) {
|
|
console.error("Auth init error:", e);
|
|
// Fallback for local dev without firebase serving
|
|
if (window.location.hostname === 'localhost') {
|
|
alert("Firebase Auth failed to load. Ensure you are using 'firebase serve' or have configured local auth.");
|
|
}
|
|
}
|
|
}
|
|
|
|
async function checkAccess(email) {
|
|
try {
|
|
// 1. Fetch the secure list of hashes
|
|
const res = await fetch('allowed-hashes.json');
|
|
if (!res.ok) return false;
|
|
const allowedHashes = await res.json();
|
|
|
|
// 2. Hash the user's email (SHA-256)
|
|
const normalizedEmail = email.trim().toLowerCase();
|
|
const msgBuffer = new TextEncoder().encode(normalizedEmail);
|
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
|
|
// 3. Compare
|
|
return allowedHashes.includes(hashHex);
|
|
} catch (e) {
|
|
console.error("Failed to check access list:", e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function showApp(user) {
|
|
authLoading.style.display = 'none';
|
|
loginScreen.style.display = 'none';
|
|
appContent.classList.remove('hidden');
|
|
userEmailSpan.textContent = user.email;
|
|
|
|
// Trigger link loading if not already done
|
|
if (typeof loadLinks === 'function') loadLinks();
|
|
}
|
|
|
|
function showLogin(errorMsg) {
|
|
authLoading.style.display = 'none';
|
|
appContent.classList.add('hidden');
|
|
loginScreen.style.display = 'flex';
|
|
if (errorMsg) {
|
|
loginError.innerHTML = errorMsg;
|
|
loginError.classList.remove('hidden');
|
|
} else {
|
|
loginError.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
// Start
|
|
initAuth();
|
|
</script>
|
|
|
|
<script>
|
|
let allDiagrams = [];
|
|
let allDocuments = [];
|
|
const homeView = document.getElementById('home-view');
|
|
const diagramViewer = document.getElementById('diagram-viewer');
|
|
const diagramContainer = document.getElementById('diagram-container');
|
|
const diagramTitle = document.getElementById('diagram-title');
|
|
const documentViewer = document.getElementById('document-viewer');
|
|
const documentContainer = document.getElementById('document-container');
|
|
const documentTitle = document.getElementById('document-title');
|
|
|
|
// Iframe Elements
|
|
const iframeViewer = document.getElementById('iframe-viewer');
|
|
const iframeContent = document.getElementById('content-iframe');
|
|
const iframeTitle = document.getElementById('iframe-title');
|
|
const iframeExternalLink = document.getElementById('iframe-external-link');
|
|
|
|
const zoomInBtn = document.getElementById('zoomInBtn');
|
|
const zoomOutBtn = document.getElementById('zoomOutBtn');
|
|
const resetBtn = document.getElementById('resetBtn');
|
|
|
|
let panzoomInstance = null;
|
|
let currentScale = 1;
|
|
|
|
// Initialize Mermaid
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: 'default',
|
|
flowchart: {
|
|
useMaxWidth: false,
|
|
htmlLabels: true,
|
|
curve: 'basis'
|
|
}
|
|
});
|
|
|
|
// Build hierarchical structure from paths
|
|
function buildHierarchy(items, pathPrefix) {
|
|
const hierarchy = { _root: { _items: [], _children: {} } };
|
|
|
|
items.forEach(item => {
|
|
let relativePath = item.path;
|
|
if (relativePath.startsWith('./')) {
|
|
relativePath = relativePath.substring(2);
|
|
}
|
|
if (relativePath.startsWith(pathPrefix)) {
|
|
relativePath = relativePath.substring(pathPrefix.length);
|
|
}
|
|
|
|
const parts = relativePath.split('/');
|
|
const relevantParts = parts.slice(0, -1); // remove filename
|
|
|
|
let current = hierarchy._root;
|
|
relevantParts.forEach(part => {
|
|
if (!current._children[part]) {
|
|
current._children[part] = { _items: [], _children: {} };
|
|
}
|
|
current = current._children[part];
|
|
});
|
|
current._items.push(item);
|
|
});
|
|
return hierarchy;
|
|
}
|
|
|
|
// Generic function to create accordion navigation
|
|
function createAccordionNavigation(hierarchy, parentElement, createLinkFunction, sectionTitle) {
|
|
const createAccordion = (title, items, children) => {
|
|
const container = document.createElement('div');
|
|
container.className = 'mb-1';
|
|
|
|
const button = document.createElement('button');
|
|
button.className = 'accordion-button';
|
|
button.setAttribute('aria-expanded', 'false');
|
|
button.innerHTML = `
|
|
<span class="font-medium">${title.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}</span>
|
|
<svg class="chevron w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path></svg>
|
|
`;
|
|
|
|
const panel = document.createElement('div');
|
|
panel.className = 'accordion-panel pl-4 pt-1';
|
|
|
|
if (items) {
|
|
items.forEach(item => createLinkFunction(item, panel, 1));
|
|
}
|
|
|
|
if (children) {
|
|
Object.keys(children).forEach(childKey => {
|
|
const childSection = children[childKey];
|
|
const childHeading = document.createElement('div');
|
|
childHeading.className = 'px-4 pt-2 pb-1 text-xs font-semibold text-gray-400 uppercase tracking-wider';
|
|
childHeading.textContent = childKey.replace(/-/g, ' ');
|
|
panel.appendChild(childHeading);
|
|
childSection._items.forEach(item => createLinkFunction(item, panel, 2));
|
|
});
|
|
}
|
|
|
|
button.addEventListener('click', () => {
|
|
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
|
button.setAttribute('aria-expanded', !isExpanded);
|
|
if (!isExpanded) {
|
|
panel.style.maxHeight = panel.scrollHeight + 'px';
|
|
} else {
|
|
panel.style.maxHeight = '0px';
|
|
}
|
|
});
|
|
|
|
container.appendChild(button);
|
|
container.appendChild(panel);
|
|
return container;
|
|
};
|
|
|
|
const heading = document.createElement('div');
|
|
heading.className = 'px-4 text-xs font-semibold text-gray-500 uppercase tracking-wider mt-6 mb-3';
|
|
heading.textContent = sectionTitle;
|
|
parentElement.appendChild(heading);
|
|
|
|
// Process root items first
|
|
if (hierarchy._root && hierarchy._root._items.length > 0) {
|
|
hierarchy._root._items.forEach(item => createLinkFunction(item, parentElement, 0));
|
|
}
|
|
|
|
// Process categories as accordions
|
|
Object.keys(hierarchy._root._children).forEach(key => {
|
|
if (key.startsWith('_')) return;
|
|
const section = hierarchy._root._children[key];
|
|
const accordion = createAccordion(key, section._items, section._children);
|
|
parentElement.appendChild(accordion);
|
|
});
|
|
}
|
|
|
|
// Helper function to create a document link
|
|
function createDocumentLink(doc, parentElement, level) {
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
|
|
(level > 0 ? ' ' : '');
|
|
link.onclick = (e) => {
|
|
e.preventDefault();
|
|
showView('document', link, doc.path, doc.title);
|
|
};
|
|
|
|
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
|
</svg>`;
|
|
|
|
link.innerHTML = `${iconSvg}<span class="truncate">${doc.title}</span>`;
|
|
parentElement.appendChild(link);
|
|
}
|
|
|
|
// Helper function to create a diagram link
|
|
function createDiagramLink(diagram, parentElement, level) {
|
|
const link = document.createElement('a');
|
|
link.href = '#';
|
|
link.className = 'nav-item flex items-center space-x-3 px-4 py-2.5 rounded-lg text-gray-600 hover:bg-gray-100 text-sm mb-1' +
|
|
(level > 0 ? ' ' : '');
|
|
link.onclick = (e) => {
|
|
e.preventDefault();
|
|
showView('diagram', link, diagram.path, diagram.title, diagram.type);
|
|
};
|
|
|
|
const iconSvg = `<svg class="w-5 h-5 text-gray-400 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4l2 2h4a2 2 0 012 2v12a2 2 0 01-2 2h-4l-2-2H7z"></path>
|
|
</svg>`;
|
|
|
|
link.innerHTML = `${iconSvg}<span class="truncate">${diagram.title}</span>`;
|
|
parentElement.appendChild(link);
|
|
}
|
|
|
|
// Load all diagrams from config
|
|
async function loadAllDiagrams() {
|
|
const dynamicSection = document.getElementById('dynamic-diagrams-section');
|
|
|
|
try {
|
|
const response = await fetch('./assets/diagrams/diagrams-config.json');
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load diagrams config: ${response.status}`);
|
|
}
|
|
|
|
const text = await response.text();
|
|
allDiagrams = JSON.parse(text);
|
|
|
|
if (allDiagrams && allDiagrams.length > 0) {
|
|
const hierarchy = buildHierarchy(allDiagrams, 'assets/diagrams/');
|
|
createAccordionNavigation(hierarchy, dynamicSection, createDiagramLink, 'Diagrams');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading diagrams configuration:', error);
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
|
|
errorDiv.innerHTML = `
|
|
<div class="font-semibold mb-1">⚠️ Diagrams</div>
|
|
<div>Unable to load diagrams-config.json</div>
|
|
<div class="mt-1 text-amber-500">${error.message}</div>
|
|
`;
|
|
dynamicSection.appendChild(errorDiv);
|
|
}
|
|
}
|
|
|
|
// Load all documentation from config
|
|
async function loadAllDocuments() {
|
|
const documentationSection = document.getElementById('documentation-section');
|
|
|
|
try {
|
|
const response = await fetch('./assets/documents/documents-config.json');
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load documents config: ${response.status}`);
|
|
}
|
|
|
|
const text = await response.text();
|
|
allDocuments = JSON.parse(text);
|
|
|
|
if (allDocuments && allDocuments.length > 0) {
|
|
const hierarchy = buildHierarchy(allDocuments, 'assets/documents/');
|
|
createAccordionNavigation(hierarchy, documentationSection, createDocumentLink, 'Documentation');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading documents configuration:', error);
|
|
const errorDiv = document.createElement('div');
|
|
errorDiv.className = 'px-4 py-3 mx-2 mt-4 text-xs text-amber-600 bg-amber-50 rounded-lg border border-amber-200';
|
|
errorDiv.innerHTML = `
|
|
<div class="font-semibold mb-1">⚠️ Documentation</div>
|
|
<div>Unable to load documents-config.json</div>
|
|
<div class="mt-1 text-amber-500">${error.message}</div>
|
|
`;
|
|
documentationSection.appendChild(errorDiv);
|
|
}
|
|
}
|
|
|
|
function setActiveNav(activeLink) {
|
|
document.querySelectorAll('#sidebar-nav a').forEach(link => {
|
|
link.classList.remove('bg-primary-50', 'border', 'border-primary-200', 'text-primary-700');
|
|
link.classList.add('text-gray-700');
|
|
});
|
|
activeLink.classList.remove('text-gray-700');
|
|
activeLink.classList.add('bg-primary-50', 'border', 'border-primary-200', 'text-primary-700');
|
|
}
|
|
|
|
async function showView(viewName, navLink, filePath, title, type = 'svg') {
|
|
setActiveNav(navLink);
|
|
if (panzoomInstance) {
|
|
panzoomInstance.destroy();
|
|
panzoomInstance = null;
|
|
}
|
|
diagramContainer.innerHTML = '';
|
|
documentContainer.innerHTML = '';
|
|
// Reset iframe
|
|
iframeContent.src = 'about:blank';
|
|
currentScale = 1;
|
|
|
|
if (viewName === 'home') {
|
|
homeView.classList.remove('hidden');
|
|
diagramViewer.classList.add('hidden');
|
|
documentViewer.classList.add('hidden');
|
|
iframeViewer.classList.add('hidden');
|
|
} else if (viewName === 'iframe') {
|
|
// New Iframe View
|
|
homeView.classList.add('hidden');
|
|
diagramViewer.classList.add('hidden');
|
|
documentViewer.classList.add('hidden');
|
|
iframeViewer.classList.remove('hidden');
|
|
|
|
iframeTitle.textContent = title;
|
|
iframeExternalLink.href = filePath;
|
|
iframeContent.src = filePath;
|
|
} else if (viewName === 'diagram') {
|
|
homeView.classList.add('hidden');
|
|
diagramViewer.classList.remove('hidden');
|
|
documentViewer.classList.add('hidden');
|
|
iframeViewer.classList.add('hidden');
|
|
diagramTitle.textContent = title;
|
|
diagramContainer.innerHTML = `
|
|
<div class="flex flex-col items-center space-y-3">
|
|
<div class="w-12 h-12 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin"></div>
|
|
<p class="text-gray-600 font-medium">Loading diagram...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
if (type === 'svg') {
|
|
const response = await fetch(filePath);
|
|
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
|
const svgContent = await response.text();
|
|
|
|
const parser = new DOMParser();
|
|
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
|
|
const svgElement = svgDoc.querySelector('svg');
|
|
|
|
if (svgElement) {
|
|
diagramContainer.innerHTML = '';
|
|
diagramContainer.appendChild(svgElement);
|
|
panzoomInstance = Panzoom(svgElement, {
|
|
canvas: true,
|
|
maxScale: 10,
|
|
minScale: 0.3,
|
|
startScale: 1
|
|
});
|
|
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
|
|
diagramContainer.focus();
|
|
} else {
|
|
throw new Error('No SVG element found.');
|
|
}
|
|
} else if (type === 'mermaid') {
|
|
const response = await fetch(filePath);
|
|
if (!response.ok) throw new Error(`Network error: ${response.status}`);
|
|
const mermaidCode = await response.text();
|
|
|
|
const { svg } = await mermaid.render('mermaidDiagram_' + Date.now(), mermaidCode);
|
|
|
|
diagramContainer.innerHTML = svg;
|
|
const svgElement = diagramContainer.querySelector('svg');
|
|
|
|
if (svgElement) {
|
|
svgElement.style.maxWidth = 'none';
|
|
svgElement.style.height = 'auto';
|
|
|
|
panzoomInstance = Panzoom(svgElement, {
|
|
canvas: true,
|
|
maxScale: 10,
|
|
minScale: 0.3,
|
|
startScale: 1
|
|
});
|
|
diagramContainer.addEventListener('wheel', panzoomInstance.zoomWithWheel);
|
|
diagramContainer.focus();
|
|
} else {
|
|
throw new Error('Failed to render Mermaid diagram.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
diagramContainer.innerHTML = `
|
|
<div class="bg-red-50 border border-red-200 rounded-xl p-6 max-w-md">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<h4 class="font-bold text-red-900">Failed to load diagram</h4>
|
|
</div>
|
|
<p class="text-red-700 text-sm">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
} else if (viewName === 'document') {
|
|
homeView.classList.add('hidden');
|
|
diagramViewer.classList.add('hidden');
|
|
documentViewer.classList.remove('hidden');
|
|
iframeViewer.classList.add('hidden');
|
|
documentTitle.textContent = title;
|
|
documentContainer.innerHTML = `
|
|
<div class="flex flex-col items-center justify-center space-y-3 py-12">
|
|
<div class="w-12 h-12 border-4 border-primary-200 border-t-primary-600 rounded-full animate-spin"></div>
|
|
<p class="text-gray-600 font-medium">Loading document...</p>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const response = await fetch(filePath);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to load document: ${response.status}`);
|
|
}
|
|
|
|
const markdownText = await response.text();
|
|
const htmlContent = marked.parse(markdownText);
|
|
documentContainer.innerHTML = htmlContent;
|
|
} catch (error) {
|
|
console.error('Error loading document:', error);
|
|
documentContainer.innerHTML = `
|
|
<div class="bg-red-50 border border-red-200 rounded-xl p-6">
|
|
<div class="flex items-center space-x-3 mb-2">
|
|
<svg class="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
|
</svg>
|
|
<h4 class="font-bold text-red-900">Failed to load document</h4>
|
|
</div>
|
|
<p class="text-red-700 text-sm">${error.message}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
}
|
|
|
|
zoomInBtn.addEventListener('click', () => {
|
|
if (panzoomInstance) {
|
|
panzoomInstance.zoomIn();
|
|
diagramContainer.focus();
|
|
}
|
|
});
|
|
|
|
zoomOutBtn.addEventListener('click', () => {
|
|
if (panzoomInstance) {
|
|
panzoomInstance.zoomOut();
|
|
diagramContainer.focus();
|
|
}
|
|
});
|
|
|
|
resetBtn.addEventListener('click', () => {
|
|
if (panzoomInstance) {
|
|
panzoomInstance.reset();
|
|
diagramContainer.focus();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadAllDiagrams();
|
|
loadAllDocuments();
|
|
showView('home', document.getElementById('nav-home'));
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |