feat: implement panel visibility toggles and toast notifications using Alpine.js

This commit is contained in:
2025-11-24 23:02:00 +02:00
parent 93299b1c79
commit ba89c3bd3a
5 changed files with 165 additions and 94 deletions

View File

@@ -51,14 +51,26 @@
<div class="app-container">
<!-- Toggle Button Strip -->
<div class="toggle-strip">
<button class="btn btn-icon xlarge active" id="toggle-snippet-panel" title="Toggle Snippets Panel">
<div class="toggle-strip" x-data>
<button class="btn btn-icon xlarge"
id="toggle-snippet-panel"
:class="{ 'active': $store.panels.snippetVisible }"
@click="togglePanel('snippet-panel')"
title="Toggle Snippets Panel">
📄
</button>
<button class="btn btn-icon xlarge active" id="toggle-editor-panel" title="Toggle Editor Panel">
<button class="btn btn-icon xlarge"
id="toggle-editor-panel"
:class="{ 'active': $store.panels.editorVisible }"
@click="togglePanel('editor-panel')"
title="Toggle Editor Panel">
✏️
</button>
<button class="btn btn-icon xlarge active" id="toggle-preview-panel" title="Toggle Preview Panel">
<button class="btn btn-icon xlarge"
id="toggle-preview-panel"
:class="{ 'active': $store.panels.previewVisible }"
@click="togglePanel('preview-panel')"
title="Toggle Preview Panel">
👁️
</button>
<button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets">
@@ -975,7 +987,15 @@
</div>
<!-- Toast Notification Container -->
<div id="toast-container"></div>
<div id="toast-container" x-data>
<template x-for="toast in $store.toasts.items" :key="toast.id">
<div :class="'toast toast-' + toast.type + (toast.visible ? ' toast-show' : '')">
<span class="toast-icon" x-text="$store.toasts.getIcon(toast.type)"></span>
<span class="toast-message" x-text="toast.message"></span>
<button class="toast-close" @click="$store.toasts.remove(toast.id)">×</button>
</div>
</template>
</div>
<script src="src/js/user-settings.js"></script>
<script src="src/js/config.js"></script>

View File

@@ -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
---

View File

@@ -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');

View File

@@ -144,63 +144,68 @@ const AppSettings = {
}
};
// Toast Notification System
const Toast = {
// Auto-dismiss duration in milliseconds
// Alpine.js Store for toast notifications
document.addEventListener('alpine:init', () => {
Alpine.store('toasts', {
items: [],
counter: 0,
DURATION: 4000,
// Toast counter for unique IDs
toastCounter: 0,
add(message, type = 'info') {
const id = ++this.counter;
const toast = { id, message, type, visible: false };
this.items.push(toast);
// Show toast notification
show(message, type = 'info') {
const container = document.getElementById('toast-container');
if (!container) return;
// Trigger show animation on next tick
setTimeout(() => {
const found = this.items.find(t => t.id === id);
if (found) found.visible = true;
}, 10);
// Create toast element
const toast = document.createElement('div');
const toastId = `toast-${++this.toastCounter}`;
toast.id = toastId;
toast.className = `toast toast-${type}`;
// Auto-dismiss
setTimeout(() => this.remove(id), this.DURATION);
},
// Toast icon based on type
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.innerHTML = `
<span class="toast-icon">${icons[type] || icons.info}</span>
<span class="toast-message">${message}</span>
<button class="toast-close" onclick="Toast.dismiss('${toastId}')">×</button>
`;
// Toast Notification System (now backed by Alpine store)
const Toast = {
// Auto-dismiss duration in milliseconds
DURATION: 4000,
// Add to container
container.appendChild(toast);
// Trigger animation
setTimeout(() => toast.classList.add('toast-show'), 10);
// Auto-dismiss
setTimeout(() => this.dismiss(toastId), this.DURATION);
// Show toast notification
show(message, type = 'info') {
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);
if (Alpine.store('toasts')) {
Alpine.store('toasts').remove(toastId);
}
}, 300);
},
// Convenience methods

View File

@@ -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;