mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
281 lines
11 KiB
HTML
281 lines
11 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Astrolabe - Vega-Lite Snippet Manager</title>
|
|
<link rel="stylesheet" href="src/styles.css">
|
|
|
|
<!-- Monaco Editor -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs/loader.js"></script>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<div class="header-left">
|
|
<span class="header-icon">🔭</span>
|
|
<span class="header-title">Astrolabe</span>
|
|
</div>
|
|
<div class="header-links">
|
|
<span class="header-link">New</span>
|
|
<span class="header-link">Import</span>
|
|
<span class="header-link">Export</span>
|
|
<span class="header-link">Help</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="app-container">
|
|
<!-- Toggle Button Strip -->
|
|
<div class="toggle-strip">
|
|
<button class="toggle-btn active" id="toggle-snippets" title="Toggle Snippets Panel">
|
|
📄
|
|
</button>
|
|
<button class="toggle-btn active" id="toggle-editor" title="Toggle Editor Panel">
|
|
✏️
|
|
</button>
|
|
<button class="toggle-btn active" id="toggle-preview" title="Toggle Preview Panel">
|
|
👁️
|
|
</button>
|
|
</div>
|
|
|
|
<div class="main-panels">
|
|
<!-- Snippet Library Panel -->
|
|
<div class="panel snippet-panel" id="snippet-panel">
|
|
<div class="panel-header">
|
|
Snippets
|
|
</div>
|
|
<div class="panel-content">
|
|
<ul class="snippet-list">
|
|
<li class="snippet-item">
|
|
<div class="snippet-name">2025-10-13_14-23-45</div>
|
|
<div class="snippet-date">Oct 13, 2:23 PM</div>
|
|
</li>
|
|
<li class="snippet-item">
|
|
<div class="snippet-name">2025-10-12_09-15-30</div>
|
|
<div class="snippet-date">Oct 12, 9:15 AM</div>
|
|
</li>
|
|
<li class="snippet-item">
|
|
<div class="snippet-name">2025-10-11_16-42-18</div>
|
|
<div class="snippet-date">Oct 11, 4:42 PM</div>
|
|
</li>
|
|
</ul>
|
|
<div class="placeholder">
|
|
Click to select a snippet
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor Panel -->
|
|
<div class="panel editor-panel" id="editor-panel">
|
|
<div class="panel-header">
|
|
Editor
|
|
</div>
|
|
<div class="panel-content">
|
|
<div id="monaco-editor" style="height: 100%; width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Panel -->
|
|
<div class="panel preview-panel" id="preview-panel">
|
|
<div class="panel-header">
|
|
Preview
|
|
</div>
|
|
<div class="panel-content">
|
|
<div id="vega-preview" style="height: 100%; width: 100%; overflow: auto;"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let editor; // Global editor instance
|
|
let renderTimeout; // For debouncing
|
|
|
|
// Sample Vega-Lite specification
|
|
const sampleSpec = {
|
|
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
|
"description": "A simple bar chart with embedded data.",
|
|
"data": {
|
|
"values": [
|
|
{"category": "A", "value": 28},
|
|
{"category": "B", "value": 55},
|
|
{"category": "C", "value": 43},
|
|
{"category": "D", "value": 91},
|
|
{"category": "E", "value": 81},
|
|
{"category": "F", "value": 53},
|
|
{"category": "G", "value": 19},
|
|
{"category": "H", "value": 87}
|
|
]
|
|
},
|
|
"mark": "bar",
|
|
"encoding": {
|
|
"x": {"field": "category", "type": "nominal", "axis": {"labelAngle": 0}},
|
|
"y": {"field": "value", "type": "quantitative"}
|
|
}
|
|
};
|
|
|
|
// Render function that takes spec from editor
|
|
async function renderVisualization() {
|
|
const previewContainer = document.getElementById('vega-preview');
|
|
|
|
try {
|
|
// Get current content from editor
|
|
const specText = editor.getValue();
|
|
const spec = JSON.parse(specText);
|
|
|
|
// Render with Vega-Embed (use global variable)
|
|
await window.vegaEmbed('#vega-preview', spec, {
|
|
actions: false, // Hide action menu for cleaner look
|
|
renderer: 'svg' // Use SVG for better quality
|
|
});
|
|
|
|
} catch (error) {
|
|
// Handle rendering errors gracefully
|
|
previewContainer.innerHTML = `
|
|
<div style="padding: 20px; color: #d32f2f; font-size: 12px; font-family: monospace;">
|
|
<strong>Rendering Error:</strong><br>
|
|
${error.message}
|
|
<br><br>
|
|
<em>Check your JSON syntax and Vega-Lite specification.</em>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Debounced render function
|
|
function debouncedRender() {
|
|
clearTimeout(renderTimeout);
|
|
renderTimeout = setTimeout(renderVisualization, 1500); // 500ms delay
|
|
}
|
|
|
|
// Load Vega libraries dynamically with UMD builds
|
|
function loadVegaLibraries() {
|
|
return new Promise((resolve, reject) => {
|
|
// Temporarily disable AMD define to avoid conflicts
|
|
const originalDefine = window.define;
|
|
window.define = undefined;
|
|
|
|
// Load Vega
|
|
const vegaScript = document.createElement('script');
|
|
vegaScript.src = 'https://unpkg.com/vega@5/build/vega.min.js';
|
|
vegaScript.onload = () => {
|
|
// Load Vega-Lite
|
|
const vegaLiteScript = document.createElement('script');
|
|
vegaLiteScript.src = 'https://unpkg.com/vega-lite@5/build/vega-lite.min.js';
|
|
vegaLiteScript.onload = () => {
|
|
// Load Vega-Embed
|
|
const vegaEmbedScript = document.createElement('script');
|
|
vegaEmbedScript.src = 'https://unpkg.com/vega-embed@6/build/vega-embed.min.js';
|
|
vegaEmbedScript.onload = () => {
|
|
// Restore AMD define
|
|
window.define = originalDefine;
|
|
resolve();
|
|
};
|
|
vegaEmbedScript.onerror = reject;
|
|
document.head.appendChild(vegaEmbedScript);
|
|
};
|
|
vegaLiteScript.onerror = reject;
|
|
document.head.appendChild(vegaLiteScript);
|
|
};
|
|
vegaScript.onerror = reject;
|
|
document.head.appendChild(vegaScript);
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Initialize Monaco Editor
|
|
require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.47.0/min/vs' } });
|
|
require(['vs/editor/editor.main'], async function () {
|
|
// Fetch actual Vega-Lite schema JSON for better validation
|
|
let vegaLiteSchema;
|
|
try {
|
|
const response = await fetch('https://vega.github.io/schema/vega-lite/v5.json');
|
|
vegaLiteSchema = await response.json();
|
|
} catch (error) {
|
|
vegaLiteSchema = null;
|
|
}
|
|
|
|
// Configure JSON language with actual schema
|
|
if (vegaLiteSchema) {
|
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
|
validate: true,
|
|
schemas: [{
|
|
uri: "https://vega.github.io/schema/vega-lite/v5.json",
|
|
fileMatch: ["*"], // Associate with all files
|
|
schema: vegaLiteSchema
|
|
}]
|
|
});
|
|
}
|
|
|
|
// Load Vega libraries before creating editor
|
|
await loadVegaLibraries();
|
|
|
|
// Create the editor with improved configuration
|
|
editor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
|
value: JSON.stringify(sampleSpec, null, 2),
|
|
language: 'json',
|
|
theme: 'vs-light',
|
|
fontSize: 12,
|
|
minimap: { enabled: false },
|
|
scrollBeyondLastLine: false,
|
|
automaticLayout: true,
|
|
wordWrap: 'on',
|
|
formatOnPaste: true,
|
|
formatOnType: true
|
|
});
|
|
|
|
// Add debounced auto-render on editor change
|
|
editor.onDidChangeModelContent(() => {
|
|
debouncedRender();
|
|
});
|
|
|
|
// Initial render
|
|
renderVisualization();
|
|
});
|
|
|
|
// Basic toggle functionality
|
|
const toggleButtons = document.querySelectorAll('.toggle-btn');
|
|
const panels = {
|
|
'toggle-snippets': document.getElementById('snippet-panel'),
|
|
'toggle-editor': document.getElementById('editor-panel'),
|
|
'toggle-preview': document.getElementById('preview-panel')
|
|
};
|
|
|
|
toggleButtons.forEach(button => {
|
|
button.addEventListener('click', function () {
|
|
const panelId = this.id;
|
|
const panel = panels[panelId];
|
|
|
|
if (panel.style.display === 'none') {
|
|
panel.style.display = 'flex';
|
|
this.classList.add('active');
|
|
} else {
|
|
panel.style.display = 'none';
|
|
this.classList.remove('active');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Basic snippet selection
|
|
const snippetItems = document.querySelectorAll('.snippet-item');
|
|
snippetItems.forEach(item => {
|
|
item.addEventListener('click', function () {
|
|
snippetItems.forEach(i => i.classList.remove('selected'));
|
|
this.classList.add('selected');
|
|
});
|
|
});
|
|
|
|
// Header link handlers (placeholder)
|
|
const headerLinks = document.querySelectorAll('.header-link');
|
|
headerLinks.forEach(link => {
|
|
link.addEventListener('click', function () {
|
|
// TODO: Implement actual functionality in future phases
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
|
|
</html> |