diff --git a/RESIZE_HANDLES_INVESTIGATION.md b/RESIZE_HANDLES_INVESTIGATION.md
new file mode 100644
index 0000000..43425d6
--- /dev/null
+++ b/RESIZE_HANDLES_INVESTIGATION.md
@@ -0,0 +1,321 @@
+# Resize Handle Overlay Issue - Investigation Report
+
+## Problem Summary
+
+The resize handles in edit mode are being rendered **INSIDE the widget container DOM**, causing:
+- Widgets to stretch and overflow their grid bounds
+- Scrollbars to appear unexpectedly
+- Edit/delete buttons to be hidden or inconsistently visible
+- Layout overflow issues
+
+The handles use negative positioning (`top: -6px`, `left: -3px`) to extend outside widget bounds, but being children of the widget element causes them to contribute to the widget's `offsetHeight` and `offsetWidth`, which creates unwanted scrollbars and overflow.
+
+---
+
+## Investigation Findings
+
+### 1. Where Resize Handles Are Created and Appended
+
+**File:** `src/systems/dashboard/resizeHandler.js`
+
+**Key Code (Lines 172-215):**
+```javascript
+createResizeHandles() {
+ const container = document.createElement('div');
+ container.className = 'resize-handles';
+ container.style.position = 'absolute';
+ container.style.inset = '0';
+ container.style.pointerEvents = 'none';
+
+ // Create 8 handles (4 corners + 4 edges)
+ Object.entries(this.handleTypes).forEach(([handleType, cursor]) => {
+ const handle = document.createElement('div');
+ handle.className = `resize-handle resize-handle-${handleType}`;
+ // ... positioning ...
+ handle.style.top = '-6px'; // Negative positioning
+ handle.style.left = '-3px'; // Negative positioning
+ handle.style.zIndex = '100';
+ container.appendChild(handle);
+ });
+
+ return container;
+}
+```
+
+**Appended At (Line 77):**
+```javascript
+initWidget(element, widget, onResizeEnd, constraints = {}) {
+ const handles = this.createResizeHandles();
+ element.appendChild(handles); // <-- APPENDED INSIDE WIDGET
+ // ...
+}
+```
+
+**Problem:** The handles container is appended directly to the widget element (`element.appendChild(handles)`), making it a child of `.rpg-widget`.
+
+---
+
+### 2. Where Edit/Delete Buttons Are Created
+
+**File:** `src/systems/dashboard/editModeManager.js`
+
+**Key Code (Lines 325-373):**
+```javascript
+addWidgetControls(element, widgetId) {
+ const controls = document.createElement('div');
+ controls.className = 'widget-edit-controls';
+ controls.style.position = 'absolute';
+ controls.style.top = '4px';
+ controls.style.right = '4px';
+ controls.style.display = 'flex';
+ controls.style.gap = '4px';
+ controls.style.zIndex = '100';
+ controls.style.opacity = '0';
+ controls.style.transition = 'opacity 0.2s';
+
+ // Create settings and delete buttons
+ const settingsBtn = this.createControlButton('⚙', 'Settings');
+ const deleteBtn = this.createControlButton('×', 'Delete');
+
+ controls.appendChild(settingsBtn);
+ controls.appendChild(deleteBtn);
+
+ element.appendChild(controls); // <-- APPENDED INSIDE WIDGET
+ // ...
+}
+```
+
+**Problem:** Like the resize handles, the edit controls are appended inside the widget element as a child.
+
+---
+
+### 3. Current DOM Structure
+
+```
+
+
+
...
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+**Why This Causes Issues:**
+- Even though handles have `position: absolute`, they're still part of the DOM flow calculation
+- Negative positioning extends them outside the widget visually, but the browser still includes them in overflow calculations
+- This causes scrollbars when the widget container has `overflow: auto` or `overflow: scroll`
+- The controls at `top: 4px; right: 4px` with `z-index: 100` can be covered or hidden by other elements
+
+---
+
+### 4. CSS Widget Styling
+
+**File:** `style.css`
+
+**Key Widget CSS:**
+```css
+.rpg-widget {
+ box-sizing: border-box;
+ overflow: visible; /* Allow resize handles to extend beyond widget bounds */
+ display: flex;
+ flex-direction: column;
+ max-height: 100%; /* Prevent content from overflowing grid cell */
+ /* ... other styles ... */
+}
+
+/* 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;
+}
+
+/* Hide resize handles when widgets are locked */
+.widgets-locked .resize-handles {
+ opacity: 0 !important;
+ pointer-events: none !important;
+}
+```
+
+**Current State:**
+- Widget has `overflow: visible` - correct for allowing handles to show
+- But the negative positioning of handles inside the widget still causes layout issues
+- The `max-height: 100%` on flex column can cause scrollbars if child heights exceed parent
+
+---
+
+### 5. Why Buttons Are Inconsistently Visible
+
+The edit/delete buttons are positioned inside the widget at `top: 4px; right: 4px;` with `z-index: 100`. Issues arise:
+
+1. **Scrollbars Overlap:** If the widget develops a scrollbar, the buttons are positioned relative to the widget's content box, not the visible area, so they can be hidden by the scrollbar.
+
+2. **Parent Stacking Context:** The widget element's positioning and z-index hierarchy may cause the buttons to be layered differently depending on scroll state.
+
+3. **Hover State Lost:** When scrollbars appear, the widget's visual bounds change, and hover detection may fail to show/hide buttons consistently.
+
+4. **Absolute Positioning Within Scrollable Parent:** Buttons positioned absolutely within a widget that can scroll create unpredictable rendering.
+
+---
+
+## Recommended Approach: Make Handles & Buttons True Overlays
+
+### Strategy
+
+**Move resize handles and edit controls outside the widget DOM to a shared overlay container at the dashboard/grid level.**
+
+**Current (Problematic) Structure:**
+```
+
+
+
+
+
+```
+
+**Target (Fixed) Structure:**
+```
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Benefits
+
+1. **No DOM Overflow:** Handles and controls are outside widgets, don't contribute to widget dimensions
+2. **Clean Widget DOM:** Widgets only contain their actual content
+3. **Consistent Visibility:** Overlays are positioned relative to grid container, not affected by widget scrolling
+4. **Proper Z-stacking:** True layers with proper z-index control
+5. **Easier Positioning:** Overlay containers can be precisely positioned relative to grid, and handles/controls positioned relative to overlay
+6. **No Scrollbar Interference:** Buttons and handles won't be hidden by scrollbars
+
+### Implementation Plan
+
+1. **Create overlay container management in DashboardManager:**
+ - Create and maintain `#rpg-resize-handles-overlay` container
+ - Create and maintain `#rpg-edit-controls-overlay` container
+ - Both positioned absolutely, covering entire grid, `pointer-events: none` by default
+
+2. **Modify ResizeHandler:**
+ - Change `initWidget()` to NOT append handles to widget element
+ - Instead, create handles and append to `#rpg-resize-handles-overlay`
+ - Position handles using absolute positioning relative to overlay container
+ - Calculate positions based on widget's grid position + negative offsets
+
+3. **Modify EditModeManager:**
+ - Change `addWidgetControls()` to NOT append controls to widget element
+ - Instead, create controls and append to `#rpg-edit-controls-overlay`
+ - Position controls using absolute positioning relative to overlay container
+ - Calculate positions based on widget's grid position
+
+4. **Update repositioning logic:**
+ - When widgets are repositioned (drag/resize), update overlay child positions
+ - On tab switch, show/hide overlay child elements for that tab's widgets
+ - On widget removal, remove corresponding overlay children
+
+5. **CSS updates:**
+ - Add styles for overlay containers
+ - Add positioning rules for handles and controls within overlays
+
+---
+
+## Key Files Needing Changes
+
+| File | Change | Impact |
+|------|--------|--------|
+| `src/systems/dashboard/resizeHandler.js` | Don't append handles to widget; append to overlay instead | Prevents widget overflow |
+| `src/systems/dashboard/editModeManager.js` | Don't append controls to widget; append to overlay instead | Fixes button visibility |
+| `src/systems/dashboard/dashboardManager.js` | Create/maintain overlay containers; manage overlay children on reposition | Coordinates layout |
+| `style.css` | Add overlay container styles; update handle/control positioning | Visual presentation |
+
+---
+
+## CSS Overflow Issue Analysis
+
+**Current `.rpg-widget` CSS:**
+```css
+.rpg-widget {
+ overflow: visible; /* Good - allows content overflow */
+ max-height: 100%; /* Can cause scrollbars if flex children exceed 100% */
+ display: flex;
+ flex-direction: column;
+}
+```
+
+**Why Scrollbars Appear:**
+1. Widget has `display: flex; flex-direction: column`
+2. Widget has `max-height: 100%`
+3. If flex children (content + handles + controls) exceed max-height, scrollbars appear
+4. The `overflow: visible` doesn't prevent scrollbars - `max-height` triggers them
+
+**Solution:**
+- Moving handles/controls outside widget DOM solves the flex child height problem
+- Keep `overflow: visible` for clean content overflow
+- Remove or adjust `max-height` constraint
+
+---
+
+## Event Handler Interaction Points
+
+### Resize Handler
+- **Source:** `resizeHandler.js`, line 77 in `initWidget()`
+- **Current:** Appends handles to widget element
+- **Change:** Accept overlay container reference, append there instead
+
+### Drag/Drop Handler
+- **Source:** `dragDrop.js`, line 76 checks for `.resize-handle` with `closest()`
+- **Impact:** Still works (CSS class-based detection)
+- **Change:** None needed - will still detect overlaid handles
+
+### Edit Mode Manager
+- **Source:** `editModeManager.js`, lines 325-373 in `addWidgetControls()`
+- **Current:** Appends controls to widget element
+- **Change:** Accept overlay container reference, append there instead
+
+### Dashboard Manager
+- **Source:** `dashboardManager.js`, lines 631-703 in `renderWidget()`
+- **Current:** Calls `resizeHandler.initWidget()` and `editManager.addWidgetControls()` with widget element
+- **Change:** Pass overlay containers, handle repositioning on layout changes
+
+---
+
+## Summary
+
+The resize handles are being rendered **inside the widget container**, causing them to:
+1. Contribute to widget dimensions via negative positioning tricks
+2. Trigger scrollbars when combined with flex layout and `max-height`
+3. Cause edit/delete buttons to be hidden or inaccessible
+4. Create inconsistent UI behavior
+
+**Solution:** Create true overlay containers at the grid level, position handles and controls outside the widget DOM, and coordinate their positioning through the DashboardManager lifecycle.
+
+This approach is used in many professional UI frameworks and provides:
+- Clean separation of concerns
+- Better visual control
+- Elimination of overflow/scrollbar issues
+- Consistent button visibility
+- Proper z-index layering
diff --git a/src/systems/dashboard/dashboardIntegration.js b/src/systems/dashboard/dashboardIntegration.js
index d066d06..eaf198a 100644
--- a/src/systems/dashboard/dashboardIntegration.js
+++ b/src/systems/dashboard/dashboardIntegration.js
@@ -306,14 +306,21 @@ function setupDashboardEventListeners(dependencies) {
});
}
- // Add widget button
+ // Add widget button - supports both desktop click and mobile touch
const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
if (addWidgetBtn) {
- addWidgetBtn.addEventListener('click', () => {
+ // Use pointerdown for universal desktop/mobile support
+ const openAddWidget = (e) => {
+ e.preventDefault();
+ e.stopPropagation();
if (dashboardManager) {
showAddWidgetDialog(dashboardManager);
}
- });
+ };
+
+ // Listen to both click (desktop) and pointerdown (mobile) for maximum compatibility
+ addWidgetBtn.addEventListener('click', openAddWidget);
+ addWidgetBtn.addEventListener('pointerdown', openAddWidget, { once: true });
}
// Export layout button
@@ -326,22 +333,51 @@ function setupDashboardEventListeners(dependencies) {
});
}
- // Import layout button
+ // Import layout button - trigger file input on click
const importBtn = document.querySelector('#rpg-dashboard-import-layout');
const importFile = document.querySelector('#rpg-dashboard-import-file');
if (importBtn && importFile) {
- importBtn.addEventListener('click', () => {
- importFile.click();
+ console.log('[RPG Companion] Import button and file input initialized');
+
+ // Trigger file picker on button click
+ importBtn.addEventListener('click', (e) => {
+ console.log('[RPG Companion] Import button clicked, triggering file picker');
+ console.log('[RPG Companion] File input element:', importFile);
+ console.log('[RPG Companion] File input visible:', importFile.offsetParent !== null);
+
+ try {
+ // Direct click works on desktop and mobile when input is properly positioned
+ importFile.click();
+ console.log('[RPG Companion] File input click() called successfully');
+ } catch (err) {
+ console.error('[RPG Companion] Error triggering file input:', err);
+ }
});
+ // Handle file selection
importFile.addEventListener('change', (e) => {
const file = e.target.files[0];
- if (file && dashboardManager) {
- dashboardManager.importLayout(file);
+ console.log('[RPG Companion] File input change event fired');
+ console.log('[RPG Companion] Selected file:', file);
+
+ if (file) {
+ if (dashboardManager) {
+ console.log('[RPG Companion] Importing layout from:', file.name);
+ dashboardManager.importLayout(file);
+ } else {
+ console.error('[RPG Companion] Dashboard manager not available');
+ }
importFile.value = ''; // Reset file input
+ } else {
+ console.warn('[RPG Companion] No file selected');
}
});
+ } else {
+ console.error('[RPG Companion] Import button or file input not found!', {
+ importBtn,
+ importFile
+ });
}
}
@@ -354,7 +390,8 @@ function showAddWidgetDialog(manager) {
const widgets = registry.getAll();
// Create widget cards HTML
- const widgetCardsHtml = widgets.map(([type, definition]) => `
+ // Note: registry.getAll() returns [{type, definition}, ...] not [[type, definition], ...]
+ const widgetCardsHtml = widgets.map(({type, definition}) => `
${definition.icon}
${definition.name}
@@ -372,6 +409,22 @@ function showAddWidgetDialog(manager) {
return;
}
+ // CRITICAL: Move modal to document.body on first use to escape panel constraints
+ // The panel has transform in its transition which creates a containing block,
+ // constraining position:fixed children to the panel instead of viewport
+ if (modal.parentElement?.id !== 'document-body-modals') {
+ // Create container for modals at body level (only once)
+ let bodyModalsContainer = document.getElementById('document-body-modals');
+ if (!bodyModalsContainer) {
+ bodyModalsContainer = document.createElement('div');
+ bodyModalsContainer.id = 'document-body-modals';
+ bodyModalsContainer.style.cssText = 'position: fixed; inset: 0; pointer-events: none; z-index: 10000; display: flex; align-items: center; justify-content: center;';
+ document.body.appendChild(bodyModalsContainer);
+ }
+ bodyModalsContainer.appendChild(modal);
+ console.log('[RPG Companion] Moved Add Widget modal to document.body for proper viewport positioning');
+ }
+
const widgetSelector = modal.querySelector('#rpg-widget-selector');
if (widgetSelector) {
widgetSelector.innerHTML = widgetCardsHtml;
@@ -380,7 +433,8 @@ function showAddWidgetDialog(manager) {
widgetSelector.querySelectorAll('.rpg-widget-card-add').forEach(btn => {
btn.addEventListener('click', () => {
const widgetType = btn.dataset.widgetType;
- const activeTab = manager.tabManager.getActiveTabId();
+ // Use activeTabId property instead of getActiveTabId() method
+ const activeTab = manager.tabManager.activeTabId;
manager.addWidget(widgetType, activeTab);
hideModal('rpg-add-widget-modal');
@@ -388,7 +442,9 @@ function showAddWidgetDialog(manager) {
});
}
+ // Show modal with proper pointer events (parent has pointer-events: none)
modal.style.display = 'flex';
+ modal.style.pointerEvents = 'auto';
// Set up modal close handlers
modal.querySelectorAll('[data-close="add-widget"]').forEach(btn => {
@@ -424,7 +480,8 @@ export function createDefaultLayout(manager) {
console.log('[RPG Companion] Creating default dashboard layout with modular widgets...');
- const mainTab = manager.tabManager.getActiveTabId();
+ // Use activeTabId property instead of getActiveTabId() method
+ const mainTab = manager.tabManager.activeTabId;
// Add modular user widgets
// Row 0: User Info (avatar, name, level) - full width
diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js
index 72b5441..296137f 100644
--- a/src/systems/dashboard/dashboardManager.js
+++ b/src/systems/dashboard/dashboardManager.js
@@ -20,6 +20,7 @@ import { DragDropHandler } from './dragDrop.js';
import { ResizeHandler } from './resizeHandler.js';
import { EditModeManager } from './editModeManager.js';
import { LayoutPersistence } from './layoutPersistence.js';
+import { generateDefaultDashboard } from './defaultLayout.js';
/**
* @typedef {Object} DashboardConfig
@@ -86,6 +87,8 @@ export class DashboardManager {
// Container elements
this.gridContainer = null;
this.tabContainer = null;
+ this.resizeHandlesOverlay = null;
+ this.editControlsOverlay = null;
this.changeListeners = new Set();
@@ -159,6 +162,7 @@ export class DashboardManager {
// Initialize Edit Mode Manager first (needed by drag/resize handlers)
this.editManager = new EditModeManager({
container: this.container,
+ editControlsOverlay: this.editControlsOverlay,
onSave: () => this.handleEditSave(),
onCancel: (originalLayout) => this.handleEditCancel(originalLayout),
onWidgetAdd: (type) => this.addWidget(type),
@@ -174,13 +178,14 @@ export class DashboardManager {
dashboardManager: this
});
- // Initialize Resize Handler (with editManager reference)
+ // Initialize Resize Handler (with editManager and overlay references)
this.resizeHandler = new ResizeHandler(this.gridEngine, {
minWidth: 1,
minHeight: 2,
maxWidth: 4, // Max 4 columns (will be clamped to actual column count)
maxHeight: 10,
- editManager: this.editManager
+ editManager: this.editManager,
+ resizeHandlesOverlay: this.resizeHandlesOverlay
});
// Initialize Layout Persistence
@@ -240,7 +245,21 @@ export class DashboardManager {
this.container.appendChild(this.gridContainer);
}
- console.log('[DashboardManager] Container structure ready');
+ // Create overlay containers for resize handles and edit controls
+ // These are positioned outside the widget DOM to prevent overflow/scrollbar issues
+ this.resizeHandlesOverlay = document.createElement('div');
+ this.resizeHandlesOverlay.id = 'rpg-resize-handles-overlay';
+ this.resizeHandlesOverlay.className = 'rpg-overlay-container';
+ this.resizeHandlesOverlay.style.cssText = 'position: absolute; inset: 0; pointer-events: none; z-index: 9999;';
+ this.gridContainer.appendChild(this.resizeHandlesOverlay);
+
+ this.editControlsOverlay = document.createElement('div');
+ this.editControlsOverlay.id = 'rpg-edit-controls-overlay';
+ this.editControlsOverlay.className = 'rpg-overlay-container';
+ this.editControlsOverlay.style.cssText = 'position: absolute; inset: 0; pointer-events: none; z-index: 10000;';
+ this.gridContainer.appendChild(this.editControlsOverlay);
+
+ console.log('[DashboardManager] Container structure ready (including overlays)');
}
/**
@@ -735,8 +754,8 @@ export class DashboardManager {
el.contentEditable = 'false';
});
- // Also disable input fields
- const inputElements = element.querySelectorAll('input, textarea');
+ // Also disable input fields (except file inputs which should remain functional)
+ const inputElements = element.querySelectorAll('input:not([type="file"]), textarea');
inputElements.forEach(el => {
el.dataset.wasEnabled = el.disabled ? 'false' : 'true';
el.disabled = true;
@@ -755,6 +774,32 @@ export class DashboardManager {
element.style.top = pos.top;
element.style.width = pos.width;
element.style.height = pos.height;
+
+ // Update overlay positions (resize handles and edit controls) to match new widget position
+ this.syncOverlaysForWidget(element, widget.id);
+ }
+
+ /**
+ * Sync overlay elements (handles and controls) for a specific widget
+ * @param {HTMLElement} element - Widget element
+ * @param {string} widgetId - Widget ID
+ */
+ syncOverlaysForWidget(element, widgetId) {
+ // Update resize handles position
+ if (this.resizeHandler) {
+ const handlerData = this.resizeHandler.resizeHandlers.get(element);
+ if (handlerData && handlerData.handles) {
+ this.resizeHandler.updateHandlePosition(handlerData.handles, element);
+ }
+ }
+
+ // Update edit controls position
+ if (this.editManager && this.editManager.isEditMode) {
+ const controlData = this.editManager.widgetControlsMap.get(widgetId);
+ if (controlData && controlData.controls) {
+ this.editManager.updateControlPosition(controlData.controls, element);
+ }
+ }
}
/**
@@ -1098,6 +1143,11 @@ export class DashboardManager {
* Clear all widgets from grid
*/
clearGrid() {
+ // Clean up edit controls overlay first
+ if (this.editManager) {
+ this.editManager.removeAllControls();
+ }
+
// Destroy all widgets
this.widgets.forEach((widgetData, widgetId) => {
const definition = this.registry.get(widgetData.widget.type);
@@ -1351,8 +1401,13 @@ export class DashboardManager {
* Reset to default layout
*/
async resetLayout() {
+ // Regenerate fresh default layout to ensure all original widgets are restored
+ // This ensures deleted widgets come back on reset
+ console.log('[DashboardManager] Regenerating fresh default layout...');
+ this.defaultLayout = generateDefaultDashboard();
+
if (!this.defaultLayout) {
- console.warn('[DashboardManager] No default layout defined');
+ console.warn('[DashboardManager] Failed to generate default layout');
return;
}
diff --git a/src/systems/dashboard/dashboardTemplate.html b/src/systems/dashboard/dashboardTemplate.html
index 9361cb5..a3d420b 100644
--- a/src/systems/dashboard/dashboardTemplate.html
+++ b/src/systems/dashboard/dashboardTemplate.html
@@ -64,7 +64,9 @@
-
+
+
+
diff --git a/src/systems/dashboard/editModeManager.js b/src/systems/dashboard/editModeManager.js
index 7206650..6d58490 100644
--- a/src/systems/dashboard/editModeManager.js
+++ b/src/systems/dashboard/editModeManager.js
@@ -23,6 +23,7 @@ export class EditModeManager {
*/
constructor(config) {
this.container = config.container;
+ this.editControlsOverlay = config.editControlsOverlay || null; // Overlay container for edit controls
this.onSave = config.onSave;
this.onCancel = config.onCancel;
this.onWidgetAdd = config.onWidgetAdd;
@@ -69,6 +70,9 @@ export class EditModeManager {
// Add edit class to container
this.container.classList.add('edit-mode');
+ // Add controls to all currently rendered widgets
+ this.syncAllControls();
+
this.notifyChange('editModeEntered');
console.log('[EditModeManager] Entered edit mode');
}
@@ -180,8 +184,8 @@ export class EditModeManager {
element.contentEditable = 'false';
});
- // Also disable input fields
- const inputElements = this.container.querySelectorAll('input, textarea');
+ // Also disable input fields (except file inputs which should remain functional)
+ const inputElements = this.container.querySelectorAll('input:not([type="file"]), textarea');
inputElements.forEach(element => {
element.dataset.wasEnabled = element.disabled ? 'false' : 'true';
element.disabled = true;
@@ -356,20 +360,86 @@ export class EditModeManager {
controls.appendChild(settingsBtn);
controls.appendChild(deleteBtn);
- element.appendChild(controls);
+ // Store reference to widget element for positioning
+ controls.dataset.widgetId = widgetId;
- // Show controls on hover
+ // Append to overlay instead of widget to prevent overflow/scrollbar issues
+ if (this.editControlsOverlay) {
+ this.editControlsOverlay.appendChild(controls);
+ // Position controls to match widget bounds
+ this.updateControlPosition(controls, element);
+ } else {
+ // Fallback to old behavior if overlay not available
+ element.appendChild(controls);
+ }
+
+ // Show controls on hover - keep visible when hovering controls themselves
+ let isHoveringWidget = false;
+ let isHoveringControls = false;
+ let hideTimeout = null;
+
+ const checkAndHideControls = () => {
+ // Clear any existing timeout
+ if (hideTimeout) {
+ clearTimeout(hideTimeout);
+ }
+
+ // Add small delay to allow mouse to move between widget and controls
+ hideTimeout = setTimeout(() => {
+ if (!isHoveringWidget && !isHoveringControls) {
+ controls.style.opacity = '0';
+ }
+ }, 100);
+ };
+
+ // Widget hover
element.addEventListener('mouseenter', () => {
+ isHoveringWidget = true;
if (this.isEditMode) {
controls.style.opacity = '1';
}
});
element.addEventListener('mouseleave', () => {
- controls.style.opacity = '0';
+ isHoveringWidget = false;
+ checkAndHideControls();
});
- this.widgetControlsMap.set(widgetId, controls);
+ // Controls hover - keep visible when hovering the buttons
+ controls.addEventListener('mouseenter', () => {
+ isHoveringControls = true;
+ controls.style.opacity = '1';
+ });
+
+ controls.addEventListener('mouseleave', () => {
+ isHoveringControls = false;
+ checkAndHideControls();
+ });
+
+ this.widgetControlsMap.set(widgetId, { controls, element });
+ }
+
+ /**
+ * Update control position to match widget bounds
+ * @param {HTMLElement} controls - Edit controls container
+ * @param {HTMLElement} element - Widget element
+ */
+ updateControlPosition(controls, element) {
+ if (!controls || !element) return;
+
+ const overlay = this.editControlsOverlay;
+ if (!overlay) return;
+
+ // Use offset properties for parent-relative positioning
+ // Both widget and overlay are children of the same grid container
+ const widgetLeft = element.offsetLeft;
+ const widgetTop = element.offsetTop;
+ const widgetWidth = element.offsetWidth;
+
+ // Position controls at top-right of widget (4px from top, 4px from right)
+ controls.style.left = `${widgetLeft + widgetWidth - 60}px`; // 60px approximate width of controls
+ controls.style.top = `${widgetTop + 4}px`;
+ controls.style.pointerEvents = 'auto'; // Ensure controls are clickable
}
/**
@@ -377,13 +447,58 @@ export class EditModeManager {
* @param {string} widgetId - Widget ID
*/
removeWidgetControls(widgetId) {
- const controls = this.widgetControlsMap.get(widgetId);
- if (controls) {
- controls.remove();
+ const data = this.widgetControlsMap.get(widgetId);
+ if (data) {
+ if (data.controls) {
+ data.controls.remove();
+ }
this.widgetControlsMap.delete(widgetId);
}
}
+ /**
+ * Sync controls for all currently rendered widgets
+ * Adds controls to widgets that don't have them yet
+ */
+ syncAllControls() {
+ // Find all widget elements in the grid
+ const gridContainer = this.container.querySelector('#rpg-dashboard-grid');
+ if (!gridContainer) return;
+
+ const widgets = gridContainer.querySelectorAll('.rpg-widget');
+ widgets.forEach(widgetElement => {
+ const widgetId = widgetElement.dataset.widgetId;
+ if (!widgetId) return;
+
+ // Add controls if they don't exist yet
+ if (!this.widgetControlsMap.has(widgetId)) {
+ this.addWidgetControls(widgetElement, widgetId);
+ } else {
+ // Update position if controls already exist
+ const data = this.widgetControlsMap.get(widgetId);
+ if (data && data.controls) {
+ this.updateControlPosition(data.controls, widgetElement);
+ }
+ }
+ });
+
+ console.log('[EditModeManager] Synced controls for', widgets.length, 'widgets');
+ }
+
+ /**
+ * Remove all widget controls
+ * Called when clearing the grid or switching tabs
+ */
+ removeAllControls() {
+ this.widgetControlsMap.forEach((data, widgetId) => {
+ if (data.controls) {
+ data.controls.remove();
+ }
+ });
+ this.widgetControlsMap.clear();
+ console.log('[EditModeManager] Removed all widget controls');
+ }
+
/**
* Create a control button
* @param {string} icon - Button icon/text
diff --git a/src/systems/dashboard/resizeHandler.js b/src/systems/dashboard/resizeHandler.js
index bbd7133..e112c6c 100644
--- a/src/systems/dashboard/resizeHandler.js
+++ b/src/systems/dashboard/resizeHandler.js
@@ -28,6 +28,7 @@ export class ResizeHandler {
constructor(gridEngine, options = {}) {
this.gridEngine = gridEngine;
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
+ this.resizeHandlesOverlay = options.resizeHandlesOverlay || null; // Overlay container for handles
this.options = {
showDimensions: true,
showGrid: true,
@@ -74,7 +75,19 @@ export class ResizeHandler {
initWidget(element, widget, onResizeEnd, constraints = {}) {
// Create resize handles
const handles = this.createResizeHandles();
- element.appendChild(handles);
+
+ // Store reference to widget element for positioning
+ handles.dataset.widgetId = element.id;
+
+ // Append to overlay instead of widget to prevent overflow/scrollbar issues
+ if (this.resizeHandlesOverlay) {
+ this.resizeHandlesOverlay.appendChild(handles);
+ // Position handles to match widget bounds
+ this.updateHandlePosition(handles, element);
+ } else {
+ // Fallback to old behavior if overlay not available
+ element.appendChild(handles);
+ }
// Store constraints
const widgetConstraints = {
@@ -215,6 +228,25 @@ export class ResizeHandler {
return container;
}
+ /**
+ * Update handle container position to match widget bounds
+ * @param {HTMLElement} handles - Resize handles container
+ * @param {HTMLElement} element - Widget element
+ */
+ updateHandlePosition(handles, element) {
+ if (!handles || !element) return;
+
+ const overlay = this.resizeHandlesOverlay;
+ if (!overlay) return;
+
+ // Use offset properties for parent-relative positioning
+ // Both widget and overlay are children of the same grid container
+ handles.style.left = `${element.offsetLeft}px`;
+ handles.style.top = `${element.offsetTop}px`;
+ handles.style.width = `${element.offsetWidth}px`;
+ handles.style.height = `${element.offsetHeight}px`;
+ }
+
/**
* Start resize operation
* @param {MouseEvent|Touch} e - Pointer event
@@ -416,6 +448,12 @@ export class ResizeHandler {
onResizeEnd(widget, widget.w, widget.h, widget.x, widget.y);
}
+ // Update handle positions to match new widget size
+ const handlerData = this.resizeHandlers.get(element);
+ if (handlerData && handlerData.handles) {
+ this.updateHandlePosition(handlerData.handles, element);
+ }
+
this.cleanup();
console.log('[ResizeHandler] Resize completed:', widget.id, `${widget.w}×${widget.h} at (${widget.x}, ${widget.y})`);
}
@@ -445,6 +483,12 @@ export class ResizeHandler {
// Remove resizing class
element.classList.remove('resizing');
+ // Update handle positions to match restored widget size
+ const handlerData = this.resizeHandlers.get(element);
+ if (handlerData && handlerData.handles) {
+ this.updateHandlePosition(handlerData.handles, element);
+ }
+
this.cleanup();
console.log('[ResizeHandler] Resize cancelled');
}
diff --git a/style.css b/style.css
index d7a5b3c..55d149d 100644
--- a/style.css
+++ b/style.css
@@ -1748,6 +1748,35 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
padding: 0.75rem 1rem;
font-size: 1.05rem; /* Touch-friendly size for mobile readability */
}
+
+ /* Add Widget dialog mobile optimizations */
+ .rpg-widget-grid {
+ grid-template-columns: 1fr; /* Single column on mobile for better readability */
+ gap: 0.75rem; /* Slightly tighter spacing */
+ }
+
+ .rpg-widget-card {
+ padding: 0.875rem; /* Slightly less padding on mobile */
+ }
+
+ .rpg-widget-card-icon {
+ font-size: 2rem; /* Scale down icon for mobile */
+ }
+
+ .rpg-widget-card-name {
+ font-size: 0.9rem; /* Slightly smaller name */
+ }
+
+ .rpg-widget-card-description {
+ font-size: 0.7rem; /* Compact description */
+ line-height: 1.25;
+ }
+
+ .rpg-widget-card-add {
+ min-height: 44px; /* Touch-friendly button size */
+ padding: 0.75rem 1rem;
+ font-size: 0.95rem;
+ }
}
.rpg-dashboard-grid {