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 {