fix(dashboard): fix resize, drag-drop, overflow, and add auto-migration

Multiple critical fixes for dashboard v2:

**1. ResizeHandler error - updateContainerWidth is not a function**
- resizeHandler.js:288 was calling non-existent method
- Removed call - containerWidth tracked by ResizeObserver
- Resizing now functional

**2. DragDrop bug - widgets can't be released**
- endDrag() destructured widgets, originalX, originalY from dragState
- These fields were never added in startDrag()
- Added widgets parameter to initWidget() and startDrag()
- Store originalX, originalY, widgets in dragState
- dashboardManager now passes current tab widgets
- Widgets can now be dropped properly

**3. Widget content overflow**
- Added base .rpg-widget CSS: overflow: hidden, box-sizing: border-box
- Prevents content extending beyond widget bounds
- max-width: 100% on children

**4. Automatic layout migration**
- Old 12-column layouts (w: 8, w: 12) cause 500%+ widths in 2-4 column grid
- Added migrateOldLayouts() method
- Detects widgets with w > current column count
- Runs auto-layout to reposition for responsive grid
- Clears and re-renders current tab with new positions
- Saves migrated layout automatically

**5. Tab rendering**
- Implemented renderTabs() method
- Displays tab buttons with icons and names
- Active state highlighting
- Click handlers to switch tabs

**6. Collision prevention**
- Modified dragDrop endDrag() to check collisions
- Same-size widgets: swap positions
- Different sizes: revert to original
- Prevents overlapping widgets

**7. Edit mode fixes**
- Fixed edit button to call toggleEditMode()
- Added CSS to hide resize handles when not in edit mode
- Handles only visible in edit mode

**8. Icon-only header buttons**
- Auto-Arrange and Edit buttons now icon-only
- Saves horizontal space in header

All issues from user testing resolved.
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-23 14:49:33 +11:00
parent 29afefb76e
commit 264ea2fc4c
6 changed files with 245 additions and 23 deletions
@@ -138,15 +138,12 @@ function getInlineDashboardTemplate() {
<div class="rpg-dashboard-header-right"> <div class="rpg-dashboard-header-right">
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets"> <button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
<i class="fa-solid fa-table-cells-large"></i> <i class="fa-solid fa-table-cells-large"></i>
<span>Auto-Arrange</span>
</button> </button>
<button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode"> <button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
<span>Edit</span>
</button> </button>
<button id="rpg-dashboard-add-widget" class="rpg-dashboard-btn rpg-add-widget-btn" style="display: none;" title="Add Widget"> <button id="rpg-dashboard-add-widget" class="rpg-dashboard-btn rpg-add-widget-btn" style="display: none;" title="Add Widget">
<i class="fa-solid fa-plus"></i> <i class="fa-solid fa-plus"></i>
<span>Add Widget</span>
</button> </button>
<button id="rpg-dashboard-export-layout" class="rpg-dashboard-btn rpg-export-btn" style="display: none;" title="Export Layout"> <button id="rpg-dashboard-export-layout" class="rpg-dashboard-btn rpg-export-btn" style="display: none;" title="Export Layout">
<i class="fa-solid fa-download"></i> <i class="fa-solid fa-download"></i>
@@ -202,9 +199,9 @@ function setupDashboardEventListeners(dependencies) {
const editModeBtn = document.querySelector('#rpg-dashboard-edit-mode'); const editModeBtn = document.querySelector('#rpg-dashboard-edit-mode');
if (editModeBtn) { if (editModeBtn) {
editModeBtn.addEventListener('click', () => { editModeBtn.addEventListener('click', () => {
if (dashboardManager) { if (dashboardManager && dashboardManager.editManager) {
const isEditMode = dashboardManager.editModeManager.isEditMode(); console.log('[RPG Companion] Edit button clicked');
dashboardManager.editModeManager.setEditMode(!isEditMode); dashboardManager.editManager.toggleEditMode();
} }
}); });
} }
+112 -1
View File
@@ -182,6 +182,12 @@ export class DashboardManager {
// Measure container width and set up responsive sizing // Measure container width and set up responsive sizing
this.setupContainerSizing(); this.setupContainerSizing();
// Migrate old 12-column layouts to new responsive grid
this.migrateOldLayouts();
// Render tab navigation
this.renderTabs();
console.log('[DashboardManager] All systems initialized'); console.log('[DashboardManager] All systems initialized');
this.notifyChange('initialized'); this.notifyChange('initialized');
} }
@@ -256,6 +262,104 @@ export class DashboardManager {
console.log('[DashboardManager] Viewport resize listener added'); console.log('[DashboardManager] Viewport resize listener added');
} }
/**
* Migrate old 12-column layouts to new responsive grid
* Detects if any widgets have widths exceeding current column count
* and automatically runs auto-layout to fix them
*/
migrateOldLayouts() {
console.log('[DashboardManager] Checking for old layouts to migrate...');
let needsMigration = false;
// Check all tabs
this.dashboard.tabs.forEach(tab => {
if (!tab.widgets || tab.widgets.length === 0) return;
// Check if any widget has width exceeding current column count
tab.widgets.forEach(widget => {
if (widget.w > this.gridEngine.columns) {
console.warn(`[DashboardManager] Widget ${widget.id} has width ${widget.w} exceeding column count ${this.gridEngine.columns}`);
needsMigration = true;
}
});
if (needsMigration) {
console.log(`[DashboardManager] Migrating tab ${tab.id} to new responsive grid...`);
// Run auto-layout on this tab's widgets
this.gridEngine.autoLayout(tab.widgets, { preferFullWidth: true });
console.log(`[DashboardManager] Tab ${tab.id} migrated successfully`);
}
});
if (needsMigration) {
// Save migrated layout
this.triggerAutoSave();
// Re-render current tab with new positions
this.clearGrid();
const currentTab = this.tabManager.getTab(this.currentTabId);
if (currentTab && currentTab.widgets) {
currentTab.widgets.forEach(widget => {
const definition = this.registry.get(widget.type);
if (definition) {
this.renderWidget(widget, definition);
}
});
}
console.log('[DashboardManager] Old layouts migrated, saved, and re-rendered');
} else {
console.log('[DashboardManager] No migration needed');
}
}
/**
* Render tab navigation UI
*/
renderTabs() {
if (!this.tabContainer) {
console.warn('[DashboardManager] Tab container not found');
return;
}
// Clear existing tabs
this.tabContainer.innerHTML = '';
// Get all tabs sorted by order
const tabs = this.tabManager.getTabs();
if (tabs.length === 0) {
console.warn('[DashboardManager] No tabs to render');
return;
}
// Create tab buttons
tabs.forEach(tab => {
const button = document.createElement('button');
button.className = 'rpg-dashboard-tab';
button.dataset.tabId = tab.id;
button.innerHTML = `
<span class="rpg-tab-icon">${tab.icon}</span>
<span class="rpg-tab-name">${tab.name}</span>
`;
// Mark active tab
if (tab.id === this.currentTabId) {
button.classList.add('active');
}
// Tab click handler
button.addEventListener('click', () => {
this.switchTab(tab.id);
});
this.tabContainer.appendChild(button);
});
console.log(`[DashboardManager] Rendered ${tabs.length} tabs`);
}
/** /**
* Add a new widget to the dashboard * Add a new widget to the dashboard
* @param {string} type - Widget type (must be registered) * @param {string} type - Widget type (must be registered)
@@ -425,13 +529,17 @@ export class DashboardManager {
// Render widget content // Render widget content
this.renderWidgetContent(element, widget, definition); this.renderWidgetContent(element, widget, definition);
// Get current tab's widgets for collision detection
const currentTab = this.tabManager.getTab(this.currentTabId);
const allWidgets = currentTab ? currentTab.widgets : [];
// Initialize drag & drop // Initialize drag & drop
this.dragHandler.initWidget(element, widget, (updated, newX, newY) => { this.dragHandler.initWidget(element, widget, (updated, newX, newY) => {
widget.x = newX; widget.x = newX;
widget.y = newY; widget.y = newY;
this.repositionWidget(element, widget); this.repositionWidget(element, widget);
this.triggerAutoSave(); this.triggerAutoSave();
}); }, allWidgets);
// Initialize resize // Initialize resize
this.resizeHandler.initWidget(element, widget, (updated, newW, newH, newX, newY) => { this.resizeHandler.initWidget(element, widget, (updated, newW, newH, newX, newY) => {
@@ -573,6 +681,9 @@ export class DashboardManager {
console.log(`[DashboardManager] Switching to tab: ${tabId}`); console.log(`[DashboardManager] Switching to tab: ${tabId}`);
this.currentTabId = tabId; this.currentTabId = tabId;
// Re-render tabs to update active state
this.renderTabs();
// Clear grid // Clear grid
this.clearGrid(); this.clearGrid();
@@ -11,13 +11,11 @@
<!-- Auto-Layout Button --> <!-- Auto-Layout Button -->
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets"> <button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
<i class="fa-solid fa-table-cells-large"></i> <i class="fa-solid fa-table-cells-large"></i>
<span>Auto-Arrange</span>
</button> </button>
<!-- Edit Mode Toggle --> <!-- Edit Mode Toggle -->
<button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode"> <button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode">
<i class="fa-solid fa-pen-to-square"></i> <i class="fa-solid fa-pen-to-square"></i>
<span>Edit</span>
</button> </button>
<!-- Add Widget Button (shown in edit mode) --> <!-- Add Widget Button (shown in edit mode) -->
+56 -9
View File
@@ -51,8 +51,9 @@ export class DragDropHandler {
* @param {HTMLElement} element - Widget DOM element * @param {HTMLElement} element - Widget DOM element
* @param {Object} widget - Widget data object * @param {Object} widget - Widget data object
* @param {Function} onDragEnd - Callback when drag completes (widget, newX, newY) * @param {Function} onDragEnd - Callback when drag completes (widget, newX, newY)
* @param {Array<Object>} widgets - All widgets (for collision detection)
*/ */
initWidget(element, widget, onDragEnd) { initWidget(element, widget, onDragEnd, widgets = []) {
// Store handler reference for cleanup // Store handler reference for cleanup
const dragHandle = element.querySelector('.drag-handle') || element; const dragHandle = element.querySelector('.drag-handle') || element;
@@ -65,7 +66,7 @@ export class DragDropHandler {
} }
e.preventDefault(); e.preventDefault();
this.startDrag(e, element, widget, onDragEnd); this.startDrag(e, element, widget, onDragEnd, widgets);
}; };
const touchStartHandler = (e) => { const touchStartHandler = (e) => {
@@ -77,7 +78,7 @@ export class DragDropHandler {
// Delay touch drag to allow scrolling // Delay touch drag to allow scrolling
this.touchTimer = setTimeout(() => { this.touchTimer = setTimeout(() => {
e.preventDefault(); e.preventDefault();
this.startDrag(e.touches[0], element, widget, onDragEnd); this.startDrag(e.touches[0], element, widget, onDragEnd, widgets);
}, this.options.touchDelay); }, this.options.touchDelay);
}; };
@@ -129,8 +130,9 @@ export class DragDropHandler {
* @param {HTMLElement} element - Element being dragged * @param {HTMLElement} element - Element being dragged
* @param {Object} widget - Widget data * @param {Object} widget - Widget data
* @param {Function} onDragEnd - Callback when drag completes * @param {Function} onDragEnd - Callback when drag completes
* @param {Array<Object>} widgets - All widgets (for collision detection)
*/ */
startDrag(e, element, widget, onDragEnd) { startDrag(e, element, widget, onDragEnd, widgets = []) {
// Calculate pointer offset from element top-left // Calculate pointer offset from element top-left
const rect = element.getBoundingClientRect(); const rect = element.getBoundingClientRect();
const offsetX = e.clientX - rect.left; const offsetX = e.clientX - rect.left;
@@ -148,7 +150,10 @@ export class DragDropHandler {
offsetY, offsetY,
ghost, ghost,
isDragging: true, isDragging: true,
onDragEnd onDragEnd,
widgets,
originalX: widget.x,
originalY: widget.y
}; };
// Change cursor // Change cursor
@@ -263,7 +268,7 @@ export class DragDropHandler {
endDrag() { endDrag() {
if (!this.dragState) return; if (!this.dragState) return;
const { element, widget, onDragEnd } = this.dragState; const { element, widget, onDragEnd, widgets, originalX, originalY } = this.dragState;
// Restore original element // Restore original element
element.style.opacity = '1'; element.style.opacity = '1';
@@ -272,9 +277,51 @@ export class DragDropHandler {
const dragHandle = element.querySelector('.drag-handle') || element; const dragHandle = element.querySelector('.drag-handle') || element;
dragHandle.style.cursor = 'grab'; dragHandle.style.cursor = 'grab';
// Call callback with new position // Check for collision before committing
if (onDragEnd) { const otherWidgets = widgets.filter(w => w.id !== widget.id);
onDragEnd(widget, widget.x, widget.y); const collision = this.gridEngine.detectCollision(widget, otherWidgets);
if (collision) {
// Find which widget we collided with
const collidedWidget = otherWidgets.find(other => {
return !(
widget.x + widget.w <= other.x ||
widget.x >= other.x + other.w ||
widget.y + widget.h <= other.y ||
widget.y >= other.y + other.h
);
});
// If same size, swap positions
if (collidedWidget && widget.w === collidedWidget.w && widget.h === collidedWidget.h) {
console.log('[DragDrop] Swapping positions with:', collidedWidget.id);
const tempX = collidedWidget.x;
const tempY = collidedWidget.y;
collidedWidget.x = widget.x;
collidedWidget.y = widget.y;
widget.x = tempX;
widget.y = tempY;
// Call callback with swapped position
if (onDragEnd) {
onDragEnd(widget, widget.x, widget.y);
}
} else {
// Different sizes or multiple collisions - revert to original
console.warn('[DragDrop] Collision detected, reverting to original position');
widget.x = originalX;
widget.y = originalY;
// Call callback with original position (no change)
if (onDragEnd) {
onDragEnd(widget, widget.x, widget.y);
}
}
} else {
// No collision, commit new position
if (onDragEnd) {
onDragEnd(widget, widget.x, widget.y);
}
} }
this.cleanup(); this.cleanup();
+1 -2
View File
@@ -284,8 +284,7 @@ export class ResizeHandler {
const deltaX = clientX - startX; const deltaX = clientX - startX;
const deltaY = clientY - startY; const deltaY = clientY - startY;
// Get column/row size in pixels // Get column/row size in pixels (containerWidth already set by ResizeObserver in DashboardManager)
this.gridEngine.updateContainerWidth();
const totalGaps = this.gridEngine.gap * (this.gridEngine.columns + 1); const totalGaps = this.gridEngine.gap * (this.gridEngine.columns + 1);
const colWidth = (this.gridEngine.containerWidth - totalGaps) / this.gridEngine.columns; const colWidth = (this.gridEngine.containerWidth - totalGaps) / this.gridEngine.columns;
const rowHeight = this.gridEngine.rowHeight; const rowHeight = this.gridEngine.rowHeight;
+73 -3
View File
@@ -1073,12 +1073,56 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
flex-wrap: wrap; flex-wrap: wrap;
} }
.rpg-dashboard-btn { .rpg-dashboard-tabs {
display: flex;
align-items: center;
gap: 0.25rem;
flex-wrap: wrap;
}
.rpg-dashboard-tab {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.3rem; gap: 0.3rem;
padding: 0.4rem 0.6rem; padding: 0.4rem 0.7rem;
font-size: 0.75rem; font-size: 0.75rem;
border: 1px solid transparent;
background: transparent;
color: var(--SmartThemeBodyColor);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
opacity: 0.6;
}
.rpg-dashboard-tab:hover {
background: var(--SmartThemeBlurTintColor);
opacity: 0.9;
}
.rpg-dashboard-tab.active {
background: var(--SmartThemeBlurTintColor);
border-color: var(--SmartThemeBorderColor);
opacity: 1;
font-weight: 600;
}
.rpg-tab-icon {
font-size: 0.9rem;
}
.rpg-tab-name {
font-size: 0.75rem;
}
.rpg-dashboard-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
width: 2rem;
height: 2rem;
font-size: 0.9rem;
border: 1px solid var(--SmartThemeBorderColor); border: 1px solid var(--SmartThemeBorderColor);
background: var(--SmartThemeBlurTintColor); background: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor); color: var(--SmartThemeBodyColor);
@@ -1093,7 +1137,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
} }
.rpg-dashboard-btn i { .rpg-dashboard-btn i {
font-size: 0.85rem; font-size: 0.9rem;
} }
.rpg-dashboard-grid { .rpg-dashboard-grid {
@@ -1102,6 +1146,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
min-height: 200px; min-height: 200px;
} }
/* Hide resize handles by default */
.resize-handles {
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
/* Show resize handles in edit mode */
.edit-mode .resize-handles {
opacity: 1;
pointer-events: auto;
}
/* ======================================== /* ========================================
DASHBOARD V2 WIDGET STYLES DASHBOARD V2 WIDGET STYLES
======================================== ========================================
@@ -1116,6 +1173,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
- %: Container-relative sizing - %: Container-relative sizing
======================================== */ ======================================== */
/* Base widget container - ensures content stays within bounds */
.rpg-widget {
box-sizing: border-box;
overflow: hidden;
display: flex;
flex-direction: column;
}
.rpg-widget > * {
box-sizing: border-box;
max-width: 100%;
}
.rpg-widget .rpg-stats-content { .rpg-widget .rpg-stats-content {
display: flex; display: flex;
flex-direction: column; /* Stack vertically */ flex-direction: column; /* Stack vertically */