mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
Implement Phase 2: Complete resizable panels with enhanced toggle functionality and memory persistence
This commit is contained in:
@@ -72,17 +72,33 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu
|
||||
|
||||
---
|
||||
|
||||
### **Phase 2: Resizable Panels**
|
||||
### **Phase 2: Resizable Panels** ✅ **COMPLETE**
|
||||
**Goal**: Make panels draggable to resize
|
||||
|
||||
- [ ] Add resize handles/dividers between panels
|
||||
- [ ] Implement vanilla JS drag handlers for horizontal resizing
|
||||
- [ ] Store panel widths in localStorage (restore on load)
|
||||
- [ ] Implement toggle button logic to show/hide each panel
|
||||
- [ ] Handle edge cases (minimum widths, hiding panels)
|
||||
- [x] Add resize handles/dividers between panels
|
||||
- [x] Implement vanilla JS drag handlers for horizontal resizing
|
||||
- [x] Store panel widths in localStorage (restore on load)
|
||||
- [x] Implement toggle button logic to show/hide each panel
|
||||
- [x] Handle edge cases (minimum widths, hiding panels)
|
||||
|
||||
**Deliverable**: Fully interactive layout with resizable and toggleable panels
|
||||
|
||||
**Key Achievements**:
|
||||
- Added 4px retro-styled resize handles between panels with hover/drag feedback
|
||||
- Implemented smooth mouse-based dragging with real-time width updates
|
||||
- Added 200px minimum width constraints to prevent unusable panels
|
||||
- Created intelligent toggle system with proportional space redistribution
|
||||
- Implemented panel memory system that preserves preferred sizes across hide/show cycles
|
||||
- Added comprehensive localStorage persistence for panel sizes, visibility, and memory
|
||||
- Fixed CSS flex properties to allow dynamic width changes (flex: 0 1 auto)
|
||||
- Enhanced toggle buttons with proper state management and visual feedback
|
||||
|
||||
**Advanced Features**:
|
||||
- **Smart Memory**: Panels remember their preferred sizes even when hidden
|
||||
- **Proportional Expansion**: Remaining visible panels expand proportionally when others are hidden
|
||||
- **Stable Proportions**: Toggle hide/show cycles maintain original size relationships
|
||||
- **Manual Resize Memory**: Dragging resize handles updates the memory for future toggle operations
|
||||
|
||||
---
|
||||
|
||||
### **Phase 3: Monaco Editor Integration** ✅ **COMPLETE**
|
||||
@@ -285,4 +301,6 @@ Astrolabe is a focused tool for managing, editing, and previewing Vega-Lite visu
|
||||
---
|
||||
|
||||
**Current Phase**: Phase 5 - Data Model + LocalStorage
|
||||
**Status**: Ready to begin implementation
|
||||
**Status**: Ready to begin implementation
|
||||
|
||||
**Note**: Phase 2 (Resizable Panels) was completed after Phase 4 to fill the gap
|
||||
301
index.html
301
index.html
@@ -67,6 +67,9 @@
|
||||
</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">
|
||||
@@ -77,6 +80,9 @@
|
||||
</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">
|
||||
@@ -93,6 +99,19 @@
|
||||
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",
|
||||
@@ -116,6 +135,262 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 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');
|
||||
@@ -185,6 +460,12 @@
|
||||
}
|
||||
|
||||
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 () {
|
||||
@@ -235,26 +516,12 @@
|
||||
renderVisualization();
|
||||
});
|
||||
|
||||
// Basic toggle functionality
|
||||
// Enhanced toggle functionality with memory and expansion
|
||||
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');
|
||||
}
|
||||
const panelId = this.id.replace('toggle-', ''); // Remove 'toggle-' prefix
|
||||
togglePanel(panelId);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -89,6 +89,24 @@ body {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
width: 4px;
|
||||
background: #808080;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
border-left: 1px solid #a0a0a0;
|
||||
border-right: 1px solid #606060;
|
||||
}
|
||||
|
||||
.resize-handle:hover {
|
||||
background: #606060;
|
||||
}
|
||||
|
||||
.resize-handle.dragging {
|
||||
background: #316ac5;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
padding: 8px 12px;
|
||||
background: #c0c0c0;
|
||||
@@ -108,15 +126,18 @@ body {
|
||||
|
||||
/* Panel sizing */
|
||||
.snippet-panel {
|
||||
flex: 0 0 20%;
|
||||
width: 25%;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.editor-panel {
|
||||
flex: 0 0 30%;
|
||||
width: 50%;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
.preview-panel {
|
||||
flex: 0 0 50%;
|
||||
width: 25%;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
/* Toggle buttons */
|
||||
|
||||
Reference in New Issue
Block a user