Features implemented: - Visual node/edge editing with property panels - Drag and drop node positioning - Color customization for nodes and edges - Text editing with real-time updates - Bidirectional Mermaid code synchronization - Subgraph/grouping support for layout control - Export functionality to JSON with position data - Layout lock system (experimental) - Edge label editing and styling - Comprehensive Mermaid syntax parser 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
454 lines
No EOL
18 KiB
PHP
454 lines
No EOL
18 KiB
PHP
<?php
|
|
require_once 'config.php';
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title><?php echo SITE_TITLE; ?></title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--primary-btn-color: #333;
|
|
--primary-btn-hover-color: #555;
|
|
--background-color: #fff;
|
|
--text-color: #333;
|
|
--border-color: #ccc;
|
|
--header-footer-bg: #333;
|
|
--header-footer-text: #fff;
|
|
--textarea-bg: #fff;
|
|
--textarea-text: #333;
|
|
--output-bg: #fff;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Montserrat', sans-serif;
|
|
line-height: 1.6;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 100vh;
|
|
background-color: var(--background-color);
|
|
color: var(--text-color);
|
|
transition: background-color 0.3s ease, color 0.3s ease;
|
|
}
|
|
header {
|
|
background-color: var(--header-footer-bg);
|
|
color: var(--header-footer-text);
|
|
text-align: center;
|
|
padding: 0.5rem;
|
|
}
|
|
main {
|
|
flex-grow: 1;
|
|
padding: 2rem;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.container {
|
|
display: flex;
|
|
gap: 2rem;
|
|
flex-grow: 1;
|
|
}
|
|
.input-area, .output-area {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
textarea {
|
|
width: 100%;
|
|
height: 300px;
|
|
margin-bottom: 1rem;
|
|
font-family: 'Montserrat', sans-serif;
|
|
background-color: var(--textarea-bg);
|
|
color: var(--textarea-text);
|
|
border: 1px solid var(--border-color);
|
|
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
|
|
}
|
|
#mermaidOutput {
|
|
border: 1px solid var(--border-color);
|
|
padding: 1rem;
|
|
flex-grow: 1;
|
|
overflow: auto;
|
|
background-color: #fff; /* Always keep diagram background white */
|
|
color: #333; /* Always keep diagram text dark */
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
.button-container {
|
|
margin-top: 1rem;
|
|
}
|
|
button {
|
|
padding: 0.5rem 1rem;
|
|
background-color: var(--primary-btn-color);
|
|
color: #fff;
|
|
border: none;
|
|
cursor: pointer;
|
|
font-family: 'Montserrat', sans-serif;
|
|
margin-right: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
transition: background-color 0.3s ease;
|
|
}
|
|
button:hover {
|
|
background-color: var(--primary-btn-hover-color);
|
|
}
|
|
footer {
|
|
background-color: var(--header-footer-bg);
|
|
color: var(--header-footer-text);
|
|
text-align: center;
|
|
padding: 1rem;
|
|
}
|
|
|
|
/* Dark Mode Toggle Button */
|
|
.dark-mode-toggle {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
z-index: 1000;
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background-color: var(--primary-btn-color);
|
|
border: none;
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
|
color: #fff;
|
|
}
|
|
|
|
/* Dark Mode Styles */
|
|
.dark-mode {
|
|
--background-color: #1e1e1e;
|
|
--text-color: #f5f5f5;
|
|
--border-color: #444;
|
|
--header-footer-bg: #000;
|
|
--header-footer-text: #f5f5f5;
|
|
--textarea-bg: #333;
|
|
--textarea-text: #f5f5f5;
|
|
--output-bg: #333;
|
|
--primary-btn-color: #555;
|
|
--primary-btn-hover-color: #777;
|
|
}
|
|
|
|
/* Special styling for the output area heading in dark mode */
|
|
.dark-mode .output-area h2 {
|
|
background-color: #333;
|
|
padding: 8px;
|
|
border-radius: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
/* Auth styles */
|
|
#protected-content {
|
|
display: block;
|
|
}
|
|
|
|
/* Auth buttons positioning */
|
|
.auth-buttons {
|
|
text-align: left;
|
|
margin-bottom: 1rem;
|
|
}
|
|
</style>
|
|
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
|
|
<!-- Auth: MSAL library -->
|
|
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<h1><?php echo SITE_TITLE; ?></h1>
|
|
</header>
|
|
|
|
<!-- Dark Mode Toggle Button -->
|
|
<button id="darkModeToggle" class="dark-mode-toggle" title="Toggle Dark Mode">
|
|
<span id="lightModeIcon">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
|
<!-- Sun icon path -->
|
|
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
|
|
</svg>
|
|
</span>
|
|
<span id="darkModeIcon" style="display: none;">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
|
<!-- Moon icon path -->
|
|
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
|
|
</svg>
|
|
</span>
|
|
</button>
|
|
|
|
<main>
|
|
<!-- Auth buttons -->
|
|
<div class="auth-buttons">
|
|
<button id="logout-button" onclick="signOut()" style="display:none;">Log Out</button>
|
|
<button id="login-button" onclick="signIn()" style="display:none;">Log In</button>
|
|
</div>
|
|
|
|
<div class="container">
|
|
<div class="input-area">
|
|
<h2>Input Mermaid Diagram Code</h2>
|
|
<textarea id="mermaidInput">graph TD
|
|
A[Christmas] -->|Get money| B(Go shopping)
|
|
B --> C{Let me think}
|
|
C -->|One| D[Android]
|
|
C -->|Two| E[Nuka Cola]
|
|
C -->|Three| F[Cold fusion generator]</textarea>
|
|
<div class="button-container">
|
|
<button id="renderBtn">Render Diagram</button>
|
|
<button id="exportMmdBtn">Download as .mmd</button>
|
|
<button id="importMmdBtn">Import .mmd</button>
|
|
<input type="file" id="fileInput" accept=".mmd" style="display: none;">
|
|
</div>
|
|
</div>
|
|
<div class="output-area" id="protected-content">
|
|
<h2>Rendered Diagram</h2>
|
|
<div id="mermaidOutput"></div>
|
|
<div class="button-container">
|
|
<button id="exportBtn">Download as PNG</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<footer>
|
|
<p>© <?php echo date('Y'); ?> <?php echo SITE_TITLE; ?></p>
|
|
</footer>
|
|
<script>
|
|
// Initial configuration will be set based on dark mode preference
|
|
let initialConfig = {
|
|
startOnLoad: false,
|
|
theme: 'base',
|
|
themeVariables: {
|
|
primaryColor: '#ffc406',
|
|
primaryTextColor: '#000000',
|
|
primaryBorderColor: '#000000',
|
|
lineColor: '#000000',
|
|
secondaryColor: '#B8B9B9',
|
|
tertiaryColor: '#B8B9B9'
|
|
},
|
|
flowchart: {
|
|
useMaxWidth: false,
|
|
htmlLabels: true,
|
|
curve: 'basis'
|
|
},
|
|
securityLevel: 'loose',
|
|
fontFamily: 'Montserrat, sans-serif',
|
|
fontSize: 14
|
|
};
|
|
|
|
// Always keep the Mermaid diagram in light mode for consistency
|
|
// Dark mode only applies to the UI, not the diagram itself
|
|
|
|
// Initialize with the appropriate theme
|
|
mermaid.initialize(initialConfig);
|
|
|
|
function renderMermaidDiagram() {
|
|
const input = document.getElementById('mermaidInput').value;
|
|
const output = document.getElementById('mermaidOutput');
|
|
|
|
output.innerHTML = ''; // Clear previous content
|
|
|
|
mermaid.render('mermaid-svg', input).then(result => {
|
|
output.innerHTML = result.svg;
|
|
}).catch(error => {
|
|
output.innerHTML = `<p style="color: red;">Error rendering diagram: ${error.message}</p>`;
|
|
});
|
|
}
|
|
|
|
// Initial render
|
|
document.addEventListener('DOMContentLoaded', renderMermaidDiagram);
|
|
|
|
document.getElementById('renderBtn').addEventListener('click', renderMermaidDiagram);
|
|
|
|
document.getElementById('exportBtn').addEventListener('click', function() {
|
|
const svg = document.querySelector('#mermaidOutput svg');
|
|
if (!svg) {
|
|
alert('No diagram to export. Please render a diagram first.');
|
|
return;
|
|
}
|
|
const svgData = new XMLSerializer().serializeToString(svg);
|
|
|
|
// Create a high-resolution canvas
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
const scale = 4; // Increase this for higher resolution
|
|
canvas.width = svg.viewBox.baseVal.width * scale;
|
|
canvas.height = svg.viewBox.baseVal.height * scale;
|
|
|
|
const img = new Image();
|
|
img.onload = function() {
|
|
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
const pngFile = canvas.toDataURL('image/png');
|
|
const downloadLink = document.createElement('a');
|
|
downloadLink.download = 'mermaid_diagram_high_res.png';
|
|
downloadLink.href = pngFile;
|
|
downloadLink.click();
|
|
};
|
|
img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
|
|
});
|
|
|
|
// Export as .mmd file
|
|
document.getElementById('exportMmdBtn').addEventListener('click', function() {
|
|
const mermaidCode = document.getElementById('mermaidInput').value;
|
|
if (!mermaidCode.trim()) {
|
|
alert('No diagram code to export.');
|
|
return;
|
|
}
|
|
|
|
// Create a blob with the mermaid code
|
|
const blob = new Blob([mermaidCode], {type: 'text/plain'});
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
// Create download link
|
|
const downloadLink = document.createElement('a');
|
|
downloadLink.download = 'mermaid_diagram.mmd';
|
|
downloadLink.href = url;
|
|
downloadLink.click();
|
|
|
|
// Clean up
|
|
URL.revokeObjectURL(url);
|
|
});
|
|
|
|
// Import .mmd file
|
|
document.getElementById('importMmdBtn').addEventListener('click', function() {
|
|
document.getElementById('fileInput').click();
|
|
});
|
|
|
|
document.getElementById('fileInput').addEventListener('change', function(event) {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const contents = e.target.result;
|
|
document.getElementById('mermaidInput').value = contents;
|
|
renderMermaidDiagram();
|
|
};
|
|
reader.readAsText(file);
|
|
|
|
// Reset file input
|
|
event.target.value = '';
|
|
});
|
|
|
|
// Dark mode toggle functionality
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Check for saved dark mode preference
|
|
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
|
|
|
|
if (darkModeEnabled) {
|
|
document.body.classList.add('dark-mode');
|
|
document.getElementById('lightModeIcon').style.display = 'none';
|
|
document.getElementById('darkModeIcon').style.display = 'block';
|
|
|
|
// Update Mermaid theme for dark mode
|
|
updateMermaidTheme(true);
|
|
}
|
|
|
|
// Toggle dark mode when button is clicked
|
|
document.getElementById('darkModeToggle').addEventListener('click', function() {
|
|
document.body.classList.toggle('dark-mode');
|
|
const isDarkMode = document.body.classList.contains('dark-mode');
|
|
|
|
// Save preference and toggle icons
|
|
if (isDarkMode) {
|
|
localStorage.setItem('darkMode', 'enabled');
|
|
document.getElementById('lightModeIcon').style.display = 'none';
|
|
document.getElementById('darkModeIcon').style.display = 'block';
|
|
} else {
|
|
localStorage.setItem('darkMode', 'disabled');
|
|
document.getElementById('lightModeIcon').style.display = 'block';
|
|
document.getElementById('darkModeIcon').style.display = 'none';
|
|
}
|
|
|
|
// Update Mermaid theme based on dark mode
|
|
updateMermaidTheme(isDarkMode);
|
|
|
|
// Re-render the diagram with new theme
|
|
renderMermaidDiagram();
|
|
});
|
|
});
|
|
|
|
// Function to update Mermaid theme based on dark mode
|
|
function updateMermaidTheme(isDarkMode) {
|
|
// Always use light theme for diagrams regardless of site theme
|
|
// This ensures exported diagrams look good and consistent
|
|
mermaid.initialize({
|
|
startOnLoad: false,
|
|
theme: 'base',
|
|
themeVariables: {
|
|
primaryColor: '#ffc406',
|
|
primaryTextColor: '#000000',
|
|
primaryBorderColor: '#000000',
|
|
lineColor: '#000000',
|
|
secondaryColor: '#B8B9B9',
|
|
tertiaryColor: '#B8B9B9'
|
|
},
|
|
flowchart: {
|
|
useMaxWidth: false,
|
|
htmlLabels: true,
|
|
curve: 'basis'
|
|
},
|
|
securityLevel: 'loose',
|
|
fontFamily: 'Montserrat, sans-serif',
|
|
fontSize: 14
|
|
});
|
|
}
|
|
|
|
// Auth Configuration
|
|
const msalConfig = {
|
|
auth: {
|
|
clientId: "9079054c-9620-4757-a256-23413042f1ef",
|
|
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
|
redirectUri: "https://ai-sandbox.oliver.solutions/mermaid"
|
|
},
|
|
cache: {
|
|
cacheLocation: "sessionStorage",
|
|
storeAuthStateInCookie: true,
|
|
}
|
|
};
|
|
|
|
const loginRequest = {
|
|
scopes: ["user.read"]
|
|
};
|
|
|
|
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
|
|
|
signIn();
|
|
|
|
function signIn() {
|
|
myMSALObj.loginPopup(loginRequest)
|
|
.then(loginResponse => {
|
|
console.log("User logged in:", loginResponse.account.username);
|
|
thisUser = loginResponse.account.username;
|
|
sessionStorage.setItem('accessToken', loginResponse.accessToken);
|
|
showProtectedContent(); // Show protected content after successful login
|
|
//onAuthenticated(); // Special for this app
|
|
}).catch(error => {
|
|
console.error("Error during login:", error);
|
|
});
|
|
}
|
|
|
|
function signOut() {
|
|
// Clear the session storage and (does not) sign out from Microsoft Identity
|
|
sessionStorage.removeItem('accessToken');
|
|
//myMSALObj.logoutPopup();
|
|
console.log("User logged out.");
|
|
document.getElementById('protected-content').style.display = 'none'; // Hide protected content
|
|
document.getElementById('logout-button').style.display = 'none'; // Hide logout button
|
|
document.getElementById('login-button').style.display = 'flex'; // Show login button
|
|
}
|
|
|
|
|
|
function showProtectedContent() {
|
|
// Verify that the access token exists before showing protected content
|
|
const accessToken = sessionStorage.getItem('accessToken');
|
|
if (accessToken) {
|
|
document.getElementById('protected-content').style.display = 'block';
|
|
document.getElementById('logout-button').style.display = 'block'; // Show logout button
|
|
document.getElementById('login-button').style.display = 'none'; // Hide login button
|
|
}
|
|
}
|
|
|
|
// Check if the user is already logged in when the page loads
|
|
window.addEventListener('load', showProtectedContent);
|
|
</script>
|
|
</body>
|
|
</html>
|