mirror of
https://github.com/olehomelchenko/astrolabe-nvc.git
synced 2025-12-21 21:22:23 +00:00
feat: implement panel visibility toggles and toast notifications using Alpine.js
This commit is contained in:
30
index.html
30
index.html
@@ -51,14 +51,26 @@
|
|||||||
|
|
||||||
<div class="app-container">
|
<div class="app-container">
|
||||||
<!-- Toggle Button Strip -->
|
<!-- Toggle Button Strip -->
|
||||||
<div class="toggle-strip">
|
<div class="toggle-strip" x-data>
|
||||||
<button class="btn btn-icon xlarge active" id="toggle-snippet-panel" title="Toggle Snippets Panel">
|
<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>
|
||||||
<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>
|
||||||
<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>
|
||||||
<button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets">
|
<button class="btn btn-icon xlarge" id="toggle-datasets" title="Datasets">
|
||||||
@@ -975,7 +987,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Notification Container -->
|
<!-- 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/user-settings.js"></script>
|
||||||
<script src="src/js/config.js"></script>
|
<script src="src/js/config.js"></script>
|
||||||
|
|||||||
@@ -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
|
**Status**: Done
|
||||||
**Files**: `index.html`, `src/js/panel-manager.js`
|
**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
|
### Implementation Approach
|
||||||
|
|
||||||
1. Create Alpine store for UI state (panel visibility flags)
|
1. Created Alpine store `panels` with `snippetVisible`, `editorVisible`, `previewVisible` flags
|
||||||
2. Convert toggle buttons to use `:class` and `@click`
|
2. Converted toggle buttons to use `:class="{ 'active': $store.panels.XXX }"` and `@click="togglePanel()"`
|
||||||
3. Add `x-show` to panels with transitions
|
3. Updated `togglePanel()` function to sync visibility changes with Alpine store
|
||||||
4. Persist visibility state to localStorage
|
4. Updated `loadLayoutFromStorage()` to initialize Alpine store from localStorage
|
||||||
|
5. Removed vanilla event listener setup from app.js
|
||||||
|
|
||||||
### What Stays Vanilla
|
### What Stays Vanilla
|
||||||
|
|
||||||
- Panel resizing logic
|
- Panel resizing logic (all width redistribution and drag-to-resize)
|
||||||
|
- Layout persistence to localStorage
|
||||||
- Keyboard shortcuts
|
- 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`
|
**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
|
### Implementation Approach
|
||||||
|
|
||||||
1. Create Alpine store for toast queue
|
1. Created Alpine store `toasts` with:
|
||||||
2. Render toasts with `x-for` and transitions
|
- `items` array to hold toast queue
|
||||||
3. Update `Toast` utility to add items to Alpine store
|
- `add(message, type)` method to create new toasts
|
||||||
4. Auto-dismiss with setTimeout
|
- `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
|
### 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
|
3. ✅ **Phase 3: View Mode Toggle** - DONE
|
||||||
4. ✅ **Phase 4: Settings Modal** - DONE
|
4. ✅ **Phase 4: Settings Modal** - DONE
|
||||||
5. ✅ **Phase 6: Meta Fields** - DONE
|
5. ✅ **Phase 6: Meta Fields** - DONE
|
||||||
6. **Phase 7: Panel Toggles** - Quick win
|
6. ✅ **Phase 7: Panel Toggles** - DONE
|
||||||
7. **Phase 5: Chart Builder** - More complex, save for when confident
|
7. **Phase 5: Chart Builder** - More complex (SKIPPED - not essential for migration)
|
||||||
8. **Phase 8: Toast Notifications** - Optional polish
|
8. ✅ **Phase 8: Toast Notifications** - DONE
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -143,13 +143,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
initializeURLStateManagement();
|
initializeURLStateManagement();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Toggle panel buttons
|
// Toggle panel buttons (now handled by Alpine.js in index.html)
|
||||||
document.querySelectorAll('[id^="toggle-"][id$="-panel"]').forEach(button => {
|
|
||||||
button.addEventListener('click', function () {
|
|
||||||
const panelId = this.id.replace('toggle-', '');
|
|
||||||
togglePanel(panelId);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Header links
|
// Header links
|
||||||
const importLink = document.getElementById('import-link');
|
const importLink = document.getElementById('import-link');
|
||||||
|
|||||||
@@ -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 = {
|
const Toast = {
|
||||||
// Auto-dismiss duration in milliseconds
|
// Auto-dismiss duration in milliseconds
|
||||||
DURATION: 4000,
|
DURATION: 4000,
|
||||||
|
|
||||||
// Toast counter for unique IDs
|
|
||||||
toastCounter: 0,
|
|
||||||
|
|
||||||
// Show toast notification
|
// Show toast notification
|
||||||
show(message, type = 'info') {
|
show(message, type = 'info') {
|
||||||
const container = document.getElementById('toast-container');
|
if (Alpine.store('toasts')) {
|
||||||
if (!container) return;
|
Alpine.store('toasts').add(message, type);
|
||||||
|
}
|
||||||
// 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 = `
|
|
||||||
<span class="toast-icon">${icons[type] || icons.info}</span>
|
|
||||||
<span class="toast-message">${message}</span>
|
|
||||||
<button class="toast-close" onclick="Toast.dismiss('${toastId}')">×</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add to container
|
|
||||||
container.appendChild(toast);
|
|
||||||
|
|
||||||
// Trigger animation
|
|
||||||
setTimeout(() => toast.classList.add('toast-show'), 10);
|
|
||||||
|
|
||||||
// Auto-dismiss
|
|
||||||
setTimeout(() => this.dismiss(toastId), this.DURATION);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Dismiss specific toast
|
// Dismiss specific toast
|
||||||
dismiss(toastId) {
|
dismiss(toastId) {
|
||||||
const toast = document.getElementById(toastId);
|
if (Alpine.store('toasts')) {
|
||||||
if (!toast) return;
|
Alpine.store('toasts').remove(toastId);
|
||||||
|
}
|
||||||
toast.classList.remove('toast-show');
|
|
||||||
toast.classList.add('toast-hide');
|
|
||||||
|
|
||||||
// Remove from DOM after animation
|
|
||||||
setTimeout(() => {
|
|
||||||
if (toast.parentNode) {
|
|
||||||
toast.parentNode.removeChild(toast);
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Convenience methods
|
// Convenience methods
|
||||||
|
|||||||
@@ -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
|
// Panel toggle and expansion functions
|
||||||
function updatePanelMemory() {
|
function updatePanelMemory() {
|
||||||
const snippetPanel = document.getElementById('snippet-panel');
|
const snippetPanel = document.getElementById('snippet-panel');
|
||||||
@@ -19,26 +28,34 @@ function updatePanelMemory() {
|
|||||||
|
|
||||||
function togglePanel(panelId) {
|
function togglePanel(panelId) {
|
||||||
const panel = document.getElementById(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
|
// Show panel
|
||||||
panel.style.display = 'flex';
|
panel.style.display = 'flex';
|
||||||
button.classList.add('active');
|
|
||||||
|
|
||||||
// Restore from memory and redistribute
|
|
||||||
redistributePanelWidths();
|
redistributePanelWidths();
|
||||||
} else {
|
} else {
|
||||||
// Hide panel - DON'T update memory, just hide
|
// Hide panel - DON'T update memory, just hide
|
||||||
panel.style.display = 'none';
|
panel.style.display = 'none';
|
||||||
button.classList.remove('active');
|
|
||||||
|
|
||||||
// Redistribute remaining panels
|
|
||||||
redistributePanelWidths();
|
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();
|
saveLayoutToStorage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +130,12 @@ function loadLayoutFromStorage() {
|
|||||||
editorPanel.style.display = layout.editorVisible !== false ? 'flex' : 'none';
|
editorPanel.style.display = layout.editorVisible !== false ? 'flex' : 'none';
|
||||||
previewPanel.style.display = layout.previewVisible !== false ? 'flex' : 'none';
|
previewPanel.style.display = layout.previewVisible !== false ? 'flex' : 'none';
|
||||||
|
|
||||||
// Update toggle button states
|
// Update Alpine store for button states
|
||||||
document.getElementById('toggle-snippet-panel').classList.toggle('active', layout.snippetVisible !== false);
|
if (Alpine.store('panels')) {
|
||||||
document.getElementById('toggle-editor-panel').classList.toggle('active', layout.editorVisible !== false);
|
Alpine.store('panels').snippetVisible = layout.snippetVisible !== false;
|
||||||
document.getElementById('toggle-preview-panel').classList.toggle('active', layout.previewVisible !== false);
|
Alpine.store('panels').editorVisible = layout.editorVisible !== false;
|
||||||
|
Alpine.store('panels').previewVisible = layout.previewVisible !== false;
|
||||||
|
}
|
||||||
|
|
||||||
// Restore widths and redistribute
|
// Restore widths and redistribute
|
||||||
snippetPanel.style.width = layout.snippetWidth;
|
snippetPanel.style.width = layout.snippetWidth;
|
||||||
|
|||||||
Reference in New Issue
Block a user