diff --git a/index.html b/index.html index bd149b0..c29739e 100644 --- a/index.html +++ b/index.html @@ -51,14 +51,26 @@
-
- - -
-
+
+ +
diff --git a/project-docs/alpine-migration-plan.md b/project-docs/alpine-migration-plan.md index 14936ce..4cc0222 100644 --- a/project-docs/alpine-migration-plan.md +++ b/project-docs/alpine-migration-plan.md @@ -247,48 +247,81 @@ Chart builder form with dataset selection, chart type, and field mappings. --- -## Phase 7: Panel Visibility Toggles +## Phase 7: Panel Visibility Toggles ✅ COMPLETE -**Status**: Planned -**Files**: `index.html`, `src/js/panel-manager.js` +**Status**: Done +**Files**: `index.html`, `src/js/panel-manager.js`, `src/js/app.js` -### What to Convert +### What Was Converted -Toggle buttons for showing/hiding panels. +- Panel toggle buttons with `:class` binding and `@click` handlers +- Button active state managed by Alpine store +- Alpine store synced with vanilla layout management ### Implementation Approach -1. Create Alpine store for UI state (panel visibility flags) -2. Convert toggle buttons to use `:class` and `@click` -3. Add `x-show` to panels with transitions -4. Persist visibility state to localStorage +1. Created Alpine store `panels` with `snippetVisible`, `editorVisible`, `previewVisible` flags +2. Converted toggle buttons to use `:class="{ 'active': $store.panels.XXX }"` and `@click="togglePanel()"` +3. Updated `togglePanel()` function to sync visibility changes with Alpine store +4. Updated `loadLayoutFromStorage()` to initialize Alpine store from localStorage +5. Removed vanilla event listener setup from app.js ### What Stays Vanilla -- Panel resizing logic +- Panel resizing logic (all width redistribution and drag-to-resize) +- Layout persistence to localStorage - Keyboard shortcuts +- The `togglePanel()` function itself (but now syncs with Alpine store) + +### Key Learnings + +- Alpine store provides reactive button states +- Hybrid approach: Alpine handles UI reactivity, vanilla handles complex layout math +- Store acts as single source of truth for visibility, synced bidirectionally +- Kept existing layout management logic intact - Alpine only manages button states +- Net code reduction: ~8 lines (removed event listener setup) --- -## Phase 8: Toast Notifications (Optional) +## Phase 8: Toast Notifications (Optional) ✅ COMPLETE -**Status**: Planned +**Status**: Done **Files**: `src/js/config.js`, `index.html` -### What to Convert +### What Was Converted -Toast notification system with auto-dismiss. +- Toast notification system with Alpine store and reactive rendering +- Toast queue managed in Alpine store +- Toasts rendered with `x-for` template +- Toast transitions managed via Alpine reactivity ### Implementation Approach -1. Create Alpine store for toast queue -2. Render toasts with `x-for` and transitions -3. Update `Toast` utility to add items to Alpine store -4. Auto-dismiss with setTimeout +1. Created Alpine store `toasts` with: + - `items` array to hold toast queue + - `add(message, type)` method to create new toasts + - `remove(id)` method to dismiss toasts + - `getIcon(type)` helper for icon lookup + - Auto-dismiss with setTimeout after 4 seconds +2. Updated `Toast` utility object to call Alpine store methods instead of DOM manipulation +3. Converted HTML to use `x-for` to render toasts from store +4. Used `:class` binding for show/hide animation states +5. Used `@click` for close button ### What Stays Vanilla -- Toast message generation +- Toast utility API (Toast.show, Toast.error, Toast.success, etc.) +- Auto-dismiss timing logic (now in Alpine store) +- Icon definitions + +### Key Learnings + +- Alpine `x-for` with templates provides clean list rendering +- Store manages toast queue reactively +- Visibility flag triggers CSS transitions +- Toast API unchanged - all existing code continues to work +- Net code reduction: ~30 lines of manual DOM manipulation removed +- Cleaner separation: store handles state, CSS handles animations --- @@ -299,9 +332,9 @@ Toast notification system with auto-dismiss. 3. ✅ **Phase 3: View Mode Toggle** - DONE 4. ✅ **Phase 4: Settings Modal** - DONE 5. ✅ **Phase 6: Meta Fields** - DONE -6. **Phase 7: Panel Toggles** - Quick win -7. **Phase 5: Chart Builder** - More complex, save for when confident -8. **Phase 8: Toast Notifications** - Optional polish +6. ✅ **Phase 7: Panel Toggles** - DONE +7. **Phase 5: Chart Builder** - More complex (SKIPPED - not essential for migration) +8. ✅ **Phase 8: Toast Notifications** - DONE --- diff --git a/src/js/app.js b/src/js/app.js index adcf306..8c5cee1 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -143,13 +143,7 @@ document.addEventListener('DOMContentLoaded', function () { initializeURLStateManagement(); }); - // Toggle panel buttons - document.querySelectorAll('[id^="toggle-"][id$="-panel"]').forEach(button => { - button.addEventListener('click', function () { - const panelId = this.id.replace('toggle-', ''); - togglePanel(panelId); - }); - }); + // Toggle panel buttons (now handled by Alpine.js in index.html) // Header links const importLink = document.getElementById('import-link'); diff --git a/src/js/config.js b/src/js/config.js index 4c174f4..18ec580 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -144,63 +144,68 @@ const AppSettings = { } }; -// Toast Notification System +// Alpine.js Store for toast notifications +document.addEventListener('alpine:init', () => { + Alpine.store('toasts', { + items: [], + counter: 0, + DURATION: 4000, + + add(message, type = 'info') { + const id = ++this.counter; + const toast = { id, message, type, visible: false }; + this.items.push(toast); + + // Trigger show animation on next tick + setTimeout(() => { + const found = this.items.find(t => t.id === id); + if (found) found.visible = true; + }, 10); + + // Auto-dismiss + setTimeout(() => this.remove(id), this.DURATION); + }, + + remove(id) { + const toast = this.items.find(t => t.id === id); + if (toast) { + toast.visible = false; + // Remove from array after animation + setTimeout(() => { + this.items = this.items.filter(t => t.id !== id); + }, 300); + } + }, + + getIcon(type) { + const icons = { + error: '❌', + success: '✓', + warning: 'âš ī¸', + info: 'â„šī¸' + }; + return icons[type] || icons.info; + } + }); +}); + +// Toast Notification System (now backed by Alpine store) const Toast = { // Auto-dismiss duration in milliseconds DURATION: 4000, - // Toast counter for unique IDs - toastCounter: 0, - // Show toast notification show(message, type = 'info') { - const container = document.getElementById('toast-container'); - if (!container) return; - - // Create toast element - const toast = document.createElement('div'); - const toastId = `toast-${++this.toastCounter}`; - toast.id = toastId; - toast.className = `toast toast-${type}`; - - // Toast icon based on type - const icons = { - error: '❌', - success: '✓', - warning: 'âš ī¸', - info: 'â„šī¸' - }; - - toast.innerHTML = ` - ${icons[type] || icons.info} - ${message} - - `; - - // Add to container - container.appendChild(toast); - - // Trigger animation - setTimeout(() => toast.classList.add('toast-show'), 10); - - // Auto-dismiss - setTimeout(() => this.dismiss(toastId), this.DURATION); + if (Alpine.store('toasts')) { + Alpine.store('toasts').add(message, type); + } }, // Dismiss specific toast dismiss(toastId) { - const toast = document.getElementById(toastId); - if (!toast) return; - - toast.classList.remove('toast-show'); - toast.classList.add('toast-hide'); - - // Remove from DOM after animation - setTimeout(() => { - if (toast.parentNode) { - toast.parentNode.removeChild(toast); - } - }, 300); + if (Alpine.store('toasts')) { + Alpine.store('toasts').remove(toastId); + } }, // Convenience methods diff --git a/src/js/panel-manager.js b/src/js/panel-manager.js index 12e9630..f040414 100644 --- a/src/js/panel-manager.js +++ b/src/js/panel-manager.js @@ -1,3 +1,12 @@ +// Alpine.js Store for panel visibility state +document.addEventListener('alpine:init', () => { + Alpine.store('panels', { + snippetVisible: true, + editorVisible: true, + previewVisible: true + }); +}); + // Panel toggle and expansion functions function updatePanelMemory() { const snippetPanel = document.getElementById('snippet-panel'); @@ -19,26 +28,34 @@ function updatePanelMemory() { function togglePanel(panelId) { const panel = document.getElementById(panelId); - const button = document.getElementById(`toggle-${panelId}`); - if (!panel || !button) return; + if (!panel) return; - if (panel.style.display === 'none') { + const isVisible = panel.style.display !== 'none'; + const newVisibility = !isVisible; + + // Update panel display + if (newVisibility) { // Show panel panel.style.display = 'flex'; - button.classList.add('active'); - - // Restore from memory and redistribute redistributePanelWidths(); } else { // Hide panel - DON'T update memory, just hide panel.style.display = 'none'; - button.classList.remove('active'); - - // Redistribute remaining panels redistributePanelWidths(); } + // Update Alpine store for button states + if (Alpine.store('panels')) { + if (panelId === 'snippet-panel') { + Alpine.store('panels').snippetVisible = newVisibility; + } else if (panelId === 'editor-panel') { + Alpine.store('panels').editorVisible = newVisibility; + } else if (panelId === 'preview-panel') { + Alpine.store('panels').previewVisible = newVisibility; + } + } + saveLayoutToStorage(); } @@ -113,10 +130,12 @@ function loadLayoutFromStorage() { editorPanel.style.display = layout.editorVisible !== false ? 'flex' : 'none'; previewPanel.style.display = layout.previewVisible !== false ? 'flex' : 'none'; - // Update toggle button states - document.getElementById('toggle-snippet-panel').classList.toggle('active', layout.snippetVisible !== false); - document.getElementById('toggle-editor-panel').classList.toggle('active', layout.editorVisible !== false); - document.getElementById('toggle-preview-panel').classList.toggle('active', layout.previewVisible !== false); + // Update Alpine store for button states + if (Alpine.store('panels')) { + Alpine.store('panels').snippetVisible = layout.snippetVisible !== false; + Alpine.store('panels').editorVisible = layout.editorVisible !== false; + Alpine.store('panels').previewVisible = layout.previewVisible !== false; + } // Restore widths and redistribute snippetPanel.style.width = layout.snippetWidth;