feat(dashboard): implement responsive header with tab scrolling and overflow menus

Add comprehensive responsive header system with Google-quality UX:

Tab Navigation:
- Add TabScrollManager for horizontal scrolling tabs
- Left/Right navigation arrows appear when scrollable
- Edge fade indicators show more content exists
- Smooth scroll behavior with momentum
- Progressive sizing: full → icon+name → icon-only
- Automatic scroll position tracking

Button Overflow System:
- Add HeaderOverflowManager with ResizeObserver
- Three responsive modes based on container width:
  * Full mode (>900px): all buttons visible
  * Overflow mode (500-900px): priority + "More" (⋮) menu
  * Compact mode (<500px): priority + hamburger (☰) menu
- Priority buttons (Lock + Edit) always visible
- Smooth transitions between modes

Dropdown Menu:
- Professional slide-down animation
- Full keyboard navigation (arrows, Home, End, Escape)
- Click-outside-to-close behavior
- ARIA attributes for accessibility
- Focus management and trap
- Auto-refresh on edit mode changes
- High z-index (10003) ensures visibility above all UI

Cross-Tab Widget Dragging:
- Add collision detection for widget placement
- Implement moveWidgetToTab() with collision avoidance
- Find available positions in target tab automatically
- Update dragDrop.js to detect tab hover
- Visual feedback with tab highlight on hover
- Proper widget positioning after cross-tab move

Additional Features:
- Sort Tab button for current-tab-only auto-layout
- Mobile optimizations with compact buttons
- Responsive breakpoints at 768px and 480px
- Hardware-accelerated animations
- Touch-friendly 44px minimum targets

Files changed:
- New: tabScrollManager.js, headerOverflowManager.js
- Modified: dashboardTemplate.html, dashboardIntegration.js
- Modified: dashboardManager.js, dragDrop.js, style.css
- Modified: editModeManager.js (lock state default)
This commit is contained in:
Lucas 'Paperboy' Rose-Winters
2025-10-27 14:48:38 +11:00
parent 45c5853dcb
commit f566ad1d93
8 changed files with 1474 additions and 41 deletions
+187 -11
View File
@@ -166,11 +166,12 @@ export class DashboardManager {
onWidgetSettings: (widgetId) => this.openWidgetSettings(widgetId)
});
// Initialize Drag & Drop (with editManager reference)
// Initialize Drag & Drop (with editManager and dashboardManager references)
this.dragHandler = new DragDropHandler(this.gridEngine, {
showGrid: true,
enableSnap: true,
editManager: this.editManager
editManager: this.editManager,
dashboardManager: this
});
// Initialize Resize Handler (with editManager reference)
@@ -489,6 +490,106 @@ export class DashboardManager {
this.notifyChange('widgetRemoved', { widgetId });
}
/**
* Move a widget from one tab to another
* @param {string} widgetId - Widget ID to move
* @param {string} targetTabId - Target tab ID
*/
moveWidgetToTab(widgetId, targetTabId) {
console.log(`[DashboardManager] Moving widget ${widgetId} to tab ${targetTabId}`);
// Find which tab currently contains the widget
let sourceTab = null;
let widgetData = null;
for (const tab of this.dashboard.tabs) {
if (tab.widgets) {
const index = tab.widgets.findIndex(w => w.id === widgetId);
if (index !== -1) {
sourceTab = tab;
widgetData = tab.widgets[index];
break;
}
}
}
if (!sourceTab || !widgetData) {
console.warn(`[DashboardManager] Widget ${widgetId} not found in any tab`);
return;
}
// Get target tab
const targetTab = this.tabManager.getTab(targetTabId);
if (!targetTab) {
console.warn(`[DashboardManager] Target tab ${targetTabId} not found`);
return;
}
// Don't move if already in target tab
if (sourceTab.id === targetTabId) {
console.log(`[DashboardManager] Widget ${widgetId} already in tab ${targetTabId}`);
return;
}
// Remove from source tab
const index = sourceTab.widgets.findIndex(w => w.id === widgetId);
sourceTab.widgets.splice(index, 1);
// Find available position in target tab (collision detection)
if (!targetTab.widgets) {
targetTab.widgets = [];
}
// Find available position explicitly checking against target tab widgets
const availablePosition = this.findAvailablePositionInWidgets(
{ w: widgetData.w, h: widgetData.h },
targetTab.widgets
);
widgetData.x = availablePosition.x;
widgetData.y = availablePosition.y;
console.log(`[DashboardManager] Found available position in target tab: (${availablePosition.x}, ${availablePosition.y})`);
// Add to target tab
targetTab.widgets.push(widgetData);
// Update runtime widget data if it exists
const runtimeData = this.widgets.get(widgetId);
if (runtimeData) {
runtimeData.tabId = targetTabId;
}
// Update DOM if source or target is current tab
if (sourceTab.id === this.currentTabId || targetTabId === this.currentTabId) {
// If widget is being moved from current tab, remove its element
if (sourceTab.id === this.currentTabId && runtimeData) {
const definition = this.registry.get(widgetData.type);
if (definition && definition.onRemove) {
definition.onRemove(runtimeData.element, widgetData.config);
}
this.dragHandler.destroyWidget(runtimeData.element);
this.resizeHandler.destroyWidget(runtimeData.element);
runtimeData.element.remove();
this.widgets.delete(widgetId);
}
// If widget is being moved to current tab, render it
if (targetTabId === this.currentTabId) {
const definition = this.registry.get(widgetData.type);
if (definition) {
this.renderWidget(widgetData, definition);
}
}
}
// Trigger auto-save
this.triggerAutoSave();
console.log(`[DashboardManager] Moved widget ${widgetId} from ${sourceTab.id} to ${targetTabId} at position (${widgetData.x}, ${widgetData.y})`);
this.notifyChange('widgetMoved', { widgetId, sourceTabId: sourceTab.id, targetTabId });
}
/**
* Update a widget's configuration
* @param {string} widgetId - Widget ID
@@ -851,25 +952,44 @@ export class DashboardManager {
// Simple algorithm: try to place at top-left, move right, then down
const tab = this.tabManager.getTab(this.currentTabId);
const widgets = tab?.widgets || [];
return this.findAvailablePositionInWidgets(size, widgets);
}
/**
* Find available position for widget in a specific widgets array
* @param {Object} size - Widget size { w, h }
* @param {Array<Object>} widgets - Array of existing widgets to check against
* @returns {Object} Position { x, y }
*/
findAvailablePositionInWidgets(size, widgets) {
console.log(`[DashboardManager] Finding available position for ${size.w}x${size.h} widget among ${widgets.length} existing widgets`);
// Try to place at top-left, move right, then down
for (let y = 0; y < 20; y++) {
for (let x = 0; x <= this.config.columns - size.w; x++) {
const position = { x, y };
const testWidget = { ...position, w: size.w, h: size.h };
for (let x = 0; x <= this.gridEngine.columns - size.w; x++) {
const testWidget = { x, y, w: size.w, h: size.h };
// Check if position is free
const hasCollision = widgets.some(w =>
this.gridEngine.detectCollision(testWidget, [w])
);
// Check if position overlaps with any existing widget
const hasCollision = widgets.some(existingWidget => {
const overlapsX = testWidget.x < existingWidget.x + existingWidget.w &&
testWidget.x + testWidget.w > existingWidget.x;
const overlapsY = testWidget.y < existingWidget.y + existingWidget.h &&
testWidget.y + testWidget.h > existingWidget.y;
return overlapsX && overlapsY;
});
if (!hasCollision) {
return position;
console.log(`[DashboardManager] Found available position: (${x}, ${y})`);
return { x, y };
}
}
}
// Fallback: place at bottom
const maxY = Math.max(...widgets.map(w => w.y + w.h), 0);
const maxY = widgets.length > 0
? Math.max(...widgets.map(w => w.y + w.h))
: 0;
console.log(`[DashboardManager] No free space found, placing at bottom: (0, ${maxY})`);
return { x: 0, y: maxY };
}
@@ -1254,6 +1374,62 @@ export class DashboardManager {
console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`);
}
/**
* Auto-layout widgets on current tab only
* Sorts and arranges widgets on the current tab to maximize space usage
*
* @param {Object} options - Layout options
* @param {boolean} [options.preserveOrder=true] - Maintain widget order during layout
* @param {boolean} [options.resetSizes=true] - Reset widgets to default sizes before layout
*/
autoLayoutCurrentTab(options = {}) {
console.log('[DashboardManager] Auto-layout current tab requested');
// Get current tab
const currentTab = this.tabManager.getTab(this.currentTabId);
if (!currentTab) {
console.warn('[DashboardManager] No current tab found');
return;
}
if (!currentTab.widgets || currentTab.widgets.length === 0) {
console.warn('[DashboardManager] Current tab has no widgets to layout');
return;
}
console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`);
// Reset widget sizes to defaults (unless explicitly disabled)
if (options.resetSizes !== false) {
this.resetWidgetSizesToDefault(currentTab.widgets);
}
// Sort widgets by category for better organization
const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets);
// Update tab's widgets array with sorted order
currentTab.widgets = sortedWidgets;
// Auto-layout widgets on the current tab
this.gridEngine.autoLayout(currentTab.widgets, {
preserveOrder: options.preserveOrder !== false
});
// Re-render all widgets with new positions
this.clearGrid();
currentTab.widgets.forEach(widget => {
const definition = this.registry.get(widget.type);
if (definition) {
this.renderWidget(widget, definition);
}
});
// Save layout
this.triggerAutoSave();
console.log('[DashboardManager] Current tab layout complete');
}
/**
* Auto-layout widgets on current tab to efficiently use all available space
*