Files
astrolabe-nvc/index.html

548 lines
22 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>
<!-- Resize Handle 1 -->
<div class="resize-handle" id="resize-handle-1"></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>
<!-- Resize Handle 2 -->
<div class="resize-handle" id="resize-handle-2"></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
// Panel resizing variables
let isResizing = false;
let currentHandle = null;
let startX = 0;
let startWidths = [];
// Panel memory for toggle functionality
let panelMemory = {
snippetWidth: '25%',
editorWidth: '50%',
previewWidth: '25%'
};
// 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"}
}
};
// Panel toggle and expansion functions
function updatePanelMemory() {
const snippetPanel = document.getElementById('snippet-panel');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
// Only update memory for visible panels
if (snippetPanel.style.display !== 'none') {
panelMemory.snippetWidth = snippetPanel.style.width || '25%';
}
if (editorPanel.style.display !== 'none') {
panelMemory.editorWidth = editorPanel.style.width || '50%';
}
if (previewPanel.style.display !== 'none') {
panelMemory.previewWidth = previewPanel.style.width || '25%';
}
}
function redistributePanelWidths() {
console.log('🔄 Redistributing panel widths...');
const snippetPanel = document.getElementById('snippet-panel');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
const panels = [
{ element: snippetPanel, id: 'snippet', memoryKey: 'snippetWidth' },
{ element: editorPanel, id: 'editor', memoryKey: 'editorWidth' },
{ element: previewPanel, id: 'preview', memoryKey: 'previewWidth' }
];
const visiblePanels = panels.filter(panel => panel.element.style.display !== 'none');
console.log('👁️ Visible panels:', visiblePanels.map(p => p.id));
if (visiblePanels.length === 0) return;
// Get total desired width from memory
let totalMemoryWidth = 0;
console.log('📊 Memory widths:');
visiblePanels.forEach(panel => {
const width = parseFloat(panelMemory[panel.memoryKey]);
console.log(` ${panel.id}: ${panelMemory[panel.memoryKey]}${width}`);
totalMemoryWidth += width;
});
console.log('📊 Total memory width:', totalMemoryWidth);
// Redistribute proportionally to fill 100%
console.log('🧮 Calculating new widths:');
visiblePanels.forEach(panel => {
const memoryWidth = parseFloat(panelMemory[panel.memoryKey]);
const newWidth = (memoryWidth / totalMemoryWidth) * 100;
console.log(` ${panel.id}: ${memoryWidth}/${totalMemoryWidth} * 100 = ${newWidth}%`);
panel.element.style.width = `${newWidth}%`;
});
}
function togglePanel(panelId) {
console.log('🔘 Toggle clicked for:', panelId);
// Fix ID mapping - buttons use plural, panels use singular
const panelIdMap = {
'snippets': 'snippet-panel',
'editor': 'editor-panel',
'preview': 'preview-panel'
};
const actualPanelId = panelIdMap[panelId];
const panel = document.getElementById(actualPanelId);
const button = document.getElementById('toggle-' + panelId);
console.log('🔍 Looking for panel:', actualPanelId, 'Found:', !!panel);
console.log('🔍 Looking for button:', 'toggle-' + panelId, 'Found:', !!button);
if (!panel || !button) {
console.error('❌ Panel or button not found!');
return;
}
console.log('📏 BEFORE toggle - Panel widths:');
logCurrentWidths();
if (panel.style.display === 'none') {
console.log('👁️ SHOWING panel:', panelId);
// Show panel
panel.style.display = 'flex';
button.classList.add('active');
// Restore from memory and redistribute
redistributePanelWidths();
} else {
console.log('🙈 HIDING panel:', panelId);
// Hide panel - DON'T update memory, just hide
panel.style.display = 'none';
button.classList.remove('active');
// Redistribute remaining panels
redistributePanelWidths();
}
console.log('📏 AFTER toggle - Panel widths:');
logCurrentWidths();
console.log('💾 Panel memory:', panelMemory);
saveLayoutToStorage();
}
function logCurrentWidths() {
const snippetPanel = document.getElementById('snippet-panel');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
console.log(' Snippets:', {
width: snippetPanel.style.width || 'default',
display: snippetPanel.style.display || 'default',
visible: snippetPanel.style.display !== 'none'
});
console.log(' Editor:', {
width: editorPanel.style.width || 'default',
display: editorPanel.style.display || 'default',
visible: editorPanel.style.display !== 'none'
});
console.log(' Preview:', {
width: previewPanel.style.width || 'default',
display: previewPanel.style.display || 'default',
visible: previewPanel.style.display !== 'none'
});
}
// Panel resizing functions
function saveLayoutToStorage() {
const snippetPanel = document.getElementById('snippet-panel');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
// DON'T update memory here - it's already updated during manual resize
const layout = {
snippetWidth: snippetPanel.style.width || '25%',
editorWidth: editorPanel.style.width || '50%',
previewWidth: previewPanel.style.width || '25%',
snippetVisible: snippetPanel.style.display !== 'none',
editorVisible: editorPanel.style.display !== 'none',
previewVisible: previewPanel.style.display !== 'none',
memory: panelMemory
};
localStorage.setItem('astrolabe-layout', JSON.stringify(layout));
}
function loadLayoutFromStorage() {
try {
const saved = localStorage.getItem('astrolabe-layout');
if (saved) {
const layout = JSON.parse(saved);
// Restore memory if available
if (layout.memory) {
panelMemory = layout.memory;
}
// Restore panel visibility
const snippetPanel = document.getElementById('snippet-panel');
const editorPanel = document.getElementById('editor-panel');
const previewPanel = document.getElementById('preview-panel');
snippetPanel.style.display = layout.snippetVisible !== false ? 'flex' : 'none';
editorPanel.style.display = layout.editorVisible !== false ? 'flex' : 'none';
previewPanel.style.display = layout.previewVisible !== false ? 'flex' : 'none';
// Update toggle button states
document.getElementById('toggle-snippets').classList.toggle('active', layout.snippetVisible !== false);
document.getElementById('toggle-editor').classList.toggle('active', layout.editorVisible !== false);
document.getElementById('toggle-preview').classList.toggle('active', layout.previewVisible !== false);
// Restore widths and redistribute
snippetPanel.style.width = layout.snippetWidth;
editorPanel.style.width = layout.editorWidth;
previewPanel.style.width = layout.previewWidth;
redistributePanelWidths();
}
} catch (error) {
// Ignore errors, use default layout
}
}
function initializeResize() {
const handles = document.querySelectorAll('.resize-handle');
const panels = [
document.getElementById('snippet-panel'),
document.getElementById('editor-panel'),
document.getElementById('preview-panel')
];
handles.forEach((handle, index) => {
handle.addEventListener('mousedown', (e) => {
isResizing = true;
currentHandle = index;
startX = e.clientX;
startWidths = panels.map(panel => panel.getBoundingClientRect().width);
handle.classList.add('dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
e.preventDefault();
});
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const containerWidth = document.querySelector('.main-panels').getBoundingClientRect().width;
if (currentHandle === 0) {
// Resizing between snippet and editor panels
const minWidth = 200;
const newSnippetWidth = Math.max(minWidth, startWidths[0] + deltaX);
const newEditorWidth = Math.max(minWidth, startWidths[1] - deltaX);
if (newSnippetWidth >= minWidth && newEditorWidth >= minWidth) {
panels[0].style.width = `${(newSnippetWidth / containerWidth) * 100}%`;
panels[1].style.width = `${(newEditorWidth / containerWidth) * 100}%`;
}
} else if (currentHandle === 1) {
// Resizing between editor and preview panels
const minWidth = 200;
const newEditorWidth = Math.max(minWidth, startWidths[1] + deltaX);
const newPreviewWidth = Math.max(minWidth, startWidths[2] - deltaX);
if (newEditorWidth >= minWidth && newPreviewWidth >= minWidth) {
panels[1].style.width = `${(newEditorWidth / containerWidth) * 100}%`;
panels[2].style.width = `${(newPreviewWidth / containerWidth) * 100}%`;
}
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
currentHandle = null;
document.querySelectorAll('.resize-handle').forEach(h => h.classList.remove('dragging'));
document.body.style.cursor = '';
document.body.style.userSelect = '';
// Update memory ONLY after manual resize
updatePanelMemory();
console.log('🎯 Manual resize completed - Updated memory:', panelMemory);
saveLayoutToStorage();
}
});
}
// 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 () {
// Load saved layout
loadLayoutFromStorage();
// Initialize resize functionality
initializeResize();
// 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();
});
// Enhanced toggle functionality with memory and expansion
const toggleButtons = document.querySelectorAll('.toggle-btn');
toggleButtons.forEach(button => {
button.addEventListener('click', function () {
const panelId = this.id.replace('toggle-', ''); // Remove 'toggle-' prefix
togglePanel(panelId);
});
});
// 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>