From ea3711a69229c3c80ffa7a7d73f86693216d2688 Mon Sep 17 00:00:00 2001 From: Oleh Omelchenko Date: Mon, 13 Oct 2025 01:50:17 +0300 Subject: [PATCH] Implement Phase 2: Complete resizable panels with enhanced toggle functionality and memory persistence --- docs/dev-plan.md | 32 +++-- index.html | 301 ++++++++++++++++++++++++++++++++++++++++++++--- src/styles.css | 27 ++++- 3 files changed, 333 insertions(+), 27 deletions(-) diff --git a/docs/dev-plan.md b/docs/dev-plan.md index 050e6df..11cc4a6 100644 --- a/docs/dev-plan.md +++ b/docs/dev-plan.md @@ -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 \ No newline at end of file +**Status**: Ready to begin implementation + +**Note**: Phase 2 (Resizable Panels) was completed after Phase 4 to fill the gap \ No newline at end of file diff --git a/index.html b/index.html index a194268..48977bf 100644 --- a/index.html +++ b/index.html @@ -67,6 +67,9 @@ + +
+
@@ -77,6 +80,9 @@
+ +
+
@@ -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); }); }); diff --git a/src/styles.css b/src/styles.css index c5b0f3e..ddcc296 100644 --- a/src/styles.css +++ b/src/styles.css @@ -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 */