refactor(dashboard): move controls to overlays and fix mobile/positioning issues
BREAKING CHANGES: - Resize handles and edit controls now render in separate overlay containers - This prevents widget overflow and scrollbar issues Major Changes: Overlay System Refactor: - Create overlay containers for resize handles and edit controls - Append handles/controls to overlays instead of widget DOM - Fix position calculations using offsetLeft/Top instead of getBoundingClientRect - Add position sync after resize/drag/reposition operations - Add cleanup methods when switching tabs Mobile Fixes: - Add pointer/touch event support for Add Widget button - Fix file upload trigger for iOS/Android compatibility - Move modals to document.body with flex centering - Fix modal button font-sizes (rem-based instead of vw) - Add mobile-responsive styling for widget dialog Bug Fixes: - Fix getActiveTabId() calls (use activeTabId property instead) - Fix file input disabled state (exclude type="file" from edit mode disable) - Fix Add Widget modal registry.getAll() destructuring (objects not arrays) - Fix edit/delete button hover (keep visible when hovering buttons) - Fix reset layout to restore deleted widgets (regenerate default layout) UI Improvements: - No more scrollbars from resize handles - Consistent button visibility in edit mode - Touch-friendly button sizes on mobile (44px min-height) - Single-column widget grid on mobile - Proper z-index stacking for overlays Files Changed: - dashboardManager.js: Overlay container management, sync methods - resizeHandler.js: Append to overlay, update positioning - editModeManager.js: Append to overlay, hover behavior, sync/cleanup - dashboardIntegration.js: Mobile touch events, file upload, modal fixes - dashboardTemplate.html: File input accessibility - style.css: Modal button font-sizes, mobile optimizations - RESIZE_HANDLES_INVESTIGATION.md: Technical investigation documentation
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
```
|
||||||
|
<div class="rpg-widget" id="widget-widget-usermood">
|
||||||
|
<!-- Widget content (rendered by widget definition) -->
|
||||||
|
<div class="widget-content">...</div>
|
||||||
|
|
||||||
|
<!-- Resize handles INSIDE widget (PROBLEM) -->
|
||||||
|
<div class="resize-handles" style="position: absolute; inset: 0; pointer-events: none;">
|
||||||
|
<div class="resize-handle resize-handle-nw" style="top: -6px; left: -3px; ..."></div>
|
||||||
|
<!-- 7 more handles... -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit controls INSIDE widget (PROBLEM) -->
|
||||||
|
<div class="widget-edit-controls" style="position: absolute; top: 4px; right: 4px; ...">
|
||||||
|
<button>⚙</button>
|
||||||
|
<button>×</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**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:**
|
||||||
|
```
|
||||||
|
<div class="rpg-widget">
|
||||||
|
<widget-content/>
|
||||||
|
<resize-handles/>
|
||||||
|
<edit-controls/>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Target (Fixed) Structure:**
|
||||||
|
```
|
||||||
|
<div id="rpg-dashboard-grid">
|
||||||
|
<div class="rpg-widget">
|
||||||
|
<widget-content/>
|
||||||
|
</div>
|
||||||
|
<div id="rpg-widget-overlays">
|
||||||
|
<resize-handles for="widget-1"/>
|
||||||
|
<resize-handles for="widget-2"/>
|
||||||
|
<edit-controls for="widget-1"/>
|
||||||
|
<edit-controls for="widget-2"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
@@ -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');
|
const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
|
||||||
if (addWidgetBtn) {
|
if (addWidgetBtn) {
|
||||||
addWidgetBtn.addEventListener('click', () => {
|
// Use pointerdown for universal desktop/mobile support
|
||||||
|
const openAddWidget = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
if (dashboardManager) {
|
if (dashboardManager) {
|
||||||
showAddWidgetDialog(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
|
// 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 importBtn = document.querySelector('#rpg-dashboard-import-layout');
|
||||||
const importFile = document.querySelector('#rpg-dashboard-import-file');
|
const importFile = document.querySelector('#rpg-dashboard-import-file');
|
||||||
|
|
||||||
if (importBtn && importFile) {
|
if (importBtn && importFile) {
|
||||||
importBtn.addEventListener('click', () => {
|
console.log('[RPG Companion] Import button and file input initialized');
|
||||||
importFile.click();
|
|
||||||
|
// 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) => {
|
importFile.addEventListener('change', (e) => {
|
||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (file && dashboardManager) {
|
console.log('[RPG Companion] File input change event fired');
|
||||||
dashboardManager.importLayout(file);
|
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
|
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();
|
const widgets = registry.getAll();
|
||||||
|
|
||||||
// Create widget cards HTML
|
// 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}) => `
|
||||||
<div class="rpg-widget-card" data-widget-type="${type}">
|
<div class="rpg-widget-card" data-widget-type="${type}">
|
||||||
<div class="rpg-widget-card-icon">${definition.icon}</div>
|
<div class="rpg-widget-card-icon">${definition.icon}</div>
|
||||||
<div class="rpg-widget-card-name">${definition.name}</div>
|
<div class="rpg-widget-card-name">${definition.name}</div>
|
||||||
@@ -372,6 +409,22 @@ function showAddWidgetDialog(manager) {
|
|||||||
return;
|
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');
|
const widgetSelector = modal.querySelector('#rpg-widget-selector');
|
||||||
if (widgetSelector) {
|
if (widgetSelector) {
|
||||||
widgetSelector.innerHTML = widgetCardsHtml;
|
widgetSelector.innerHTML = widgetCardsHtml;
|
||||||
@@ -380,7 +433,8 @@ function showAddWidgetDialog(manager) {
|
|||||||
widgetSelector.querySelectorAll('.rpg-widget-card-add').forEach(btn => {
|
widgetSelector.querySelectorAll('.rpg-widget-card-add').forEach(btn => {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
const widgetType = btn.dataset.widgetType;
|
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);
|
manager.addWidget(widgetType, activeTab);
|
||||||
hideModal('rpg-add-widget-modal');
|
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.display = 'flex';
|
||||||
|
modal.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
// Set up modal close handlers
|
// Set up modal close handlers
|
||||||
modal.querySelectorAll('[data-close="add-widget"]').forEach(btn => {
|
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...');
|
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
|
// Add modular user widgets
|
||||||
// Row 0: User Info (avatar, name, level) - full width
|
// Row 0: User Info (avatar, name, level) - full width
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { DragDropHandler } from './dragDrop.js';
|
|||||||
import { ResizeHandler } from './resizeHandler.js';
|
import { ResizeHandler } from './resizeHandler.js';
|
||||||
import { EditModeManager } from './editModeManager.js';
|
import { EditModeManager } from './editModeManager.js';
|
||||||
import { LayoutPersistence } from './layoutPersistence.js';
|
import { LayoutPersistence } from './layoutPersistence.js';
|
||||||
|
import { generateDefaultDashboard } from './defaultLayout.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} DashboardConfig
|
* @typedef {Object} DashboardConfig
|
||||||
@@ -86,6 +87,8 @@ export class DashboardManager {
|
|||||||
// Container elements
|
// Container elements
|
||||||
this.gridContainer = null;
|
this.gridContainer = null;
|
||||||
this.tabContainer = null;
|
this.tabContainer = null;
|
||||||
|
this.resizeHandlesOverlay = null;
|
||||||
|
this.editControlsOverlay = null;
|
||||||
|
|
||||||
this.changeListeners = new Set();
|
this.changeListeners = new Set();
|
||||||
|
|
||||||
@@ -159,6 +162,7 @@ export class DashboardManager {
|
|||||||
// Initialize Edit Mode Manager first (needed by drag/resize handlers)
|
// Initialize Edit Mode Manager first (needed by drag/resize handlers)
|
||||||
this.editManager = new EditModeManager({
|
this.editManager = new EditModeManager({
|
||||||
container: this.container,
|
container: this.container,
|
||||||
|
editControlsOverlay: this.editControlsOverlay,
|
||||||
onSave: () => this.handleEditSave(),
|
onSave: () => this.handleEditSave(),
|
||||||
onCancel: (originalLayout) => this.handleEditCancel(originalLayout),
|
onCancel: (originalLayout) => this.handleEditCancel(originalLayout),
|
||||||
onWidgetAdd: (type) => this.addWidget(type),
|
onWidgetAdd: (type) => this.addWidget(type),
|
||||||
@@ -174,13 +178,14 @@ export class DashboardManager {
|
|||||||
dashboardManager: this
|
dashboardManager: this
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Resize Handler (with editManager reference)
|
// Initialize Resize Handler (with editManager and overlay references)
|
||||||
this.resizeHandler = new ResizeHandler(this.gridEngine, {
|
this.resizeHandler = new ResizeHandler(this.gridEngine, {
|
||||||
minWidth: 1,
|
minWidth: 1,
|
||||||
minHeight: 2,
|
minHeight: 2,
|
||||||
maxWidth: 4, // Max 4 columns (will be clamped to actual column count)
|
maxWidth: 4, // Max 4 columns (will be clamped to actual column count)
|
||||||
maxHeight: 10,
|
maxHeight: 10,
|
||||||
editManager: this.editManager
|
editManager: this.editManager,
|
||||||
|
resizeHandlesOverlay: this.resizeHandlesOverlay
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize Layout Persistence
|
// Initialize Layout Persistence
|
||||||
@@ -240,7 +245,21 @@ export class DashboardManager {
|
|||||||
this.container.appendChild(this.gridContainer);
|
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';
|
el.contentEditable = 'false';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also disable input fields
|
// Also disable input fields (except file inputs which should remain functional)
|
||||||
const inputElements = element.querySelectorAll('input, textarea');
|
const inputElements = element.querySelectorAll('input:not([type="file"]), textarea');
|
||||||
inputElements.forEach(el => {
|
inputElements.forEach(el => {
|
||||||
el.dataset.wasEnabled = el.disabled ? 'false' : 'true';
|
el.dataset.wasEnabled = el.disabled ? 'false' : 'true';
|
||||||
el.disabled = true;
|
el.disabled = true;
|
||||||
@@ -755,6 +774,32 @@ export class DashboardManager {
|
|||||||
element.style.top = pos.top;
|
element.style.top = pos.top;
|
||||||
element.style.width = pos.width;
|
element.style.width = pos.width;
|
||||||
element.style.height = pos.height;
|
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
|
* Clear all widgets from grid
|
||||||
*/
|
*/
|
||||||
clearGrid() {
|
clearGrid() {
|
||||||
|
// Clean up edit controls overlay first
|
||||||
|
if (this.editManager) {
|
||||||
|
this.editManager.removeAllControls();
|
||||||
|
}
|
||||||
|
|
||||||
// Destroy all widgets
|
// Destroy all widgets
|
||||||
this.widgets.forEach((widgetData, widgetId) => {
|
this.widgets.forEach((widgetData, widgetId) => {
|
||||||
const definition = this.registry.get(widgetData.widget.type);
|
const definition = this.registry.get(widgetData.widget.type);
|
||||||
@@ -1351,8 +1401,13 @@ export class DashboardManager {
|
|||||||
* Reset to default layout
|
* Reset to default layout
|
||||||
*/
|
*/
|
||||||
async resetLayout() {
|
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) {
|
if (!this.defaultLayout) {
|
||||||
console.warn('[DashboardManager] No default layout defined');
|
console.warn('[DashboardManager] Failed to generate default layout');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,9 @@
|
|||||||
<!-- Menu items added dynamically -->
|
<!-- Menu items added dynamically -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input type="file" id="rpg-dashboard-import-file" accept=".json" style="display: none;" />
|
<!-- File input: visually hidden but accessible for mobile compatibility -->
|
||||||
|
<!-- Use 1px size for better browser compatibility while keeping hidden -->
|
||||||
|
<input type="file" id="rpg-dashboard-import-file" accept=".json" style="position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; z-index: -1; pointer-events: auto;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export class EditModeManager {
|
|||||||
*/
|
*/
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
this.container = config.container;
|
this.container = config.container;
|
||||||
|
this.editControlsOverlay = config.editControlsOverlay || null; // Overlay container for edit controls
|
||||||
this.onSave = config.onSave;
|
this.onSave = config.onSave;
|
||||||
this.onCancel = config.onCancel;
|
this.onCancel = config.onCancel;
|
||||||
this.onWidgetAdd = config.onWidgetAdd;
|
this.onWidgetAdd = config.onWidgetAdd;
|
||||||
@@ -69,6 +70,9 @@ export class EditModeManager {
|
|||||||
// Add edit class to container
|
// Add edit class to container
|
||||||
this.container.classList.add('edit-mode');
|
this.container.classList.add('edit-mode');
|
||||||
|
|
||||||
|
// Add controls to all currently rendered widgets
|
||||||
|
this.syncAllControls();
|
||||||
|
|
||||||
this.notifyChange('editModeEntered');
|
this.notifyChange('editModeEntered');
|
||||||
console.log('[EditModeManager] Entered edit mode');
|
console.log('[EditModeManager] Entered edit mode');
|
||||||
}
|
}
|
||||||
@@ -180,8 +184,8 @@ export class EditModeManager {
|
|||||||
element.contentEditable = 'false';
|
element.contentEditable = 'false';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Also disable input fields
|
// Also disable input fields (except file inputs which should remain functional)
|
||||||
const inputElements = this.container.querySelectorAll('input, textarea');
|
const inputElements = this.container.querySelectorAll('input:not([type="file"]), textarea');
|
||||||
inputElements.forEach(element => {
|
inputElements.forEach(element => {
|
||||||
element.dataset.wasEnabled = element.disabled ? 'false' : 'true';
|
element.dataset.wasEnabled = element.disabled ? 'false' : 'true';
|
||||||
element.disabled = true;
|
element.disabled = true;
|
||||||
@@ -356,20 +360,86 @@ export class EditModeManager {
|
|||||||
controls.appendChild(settingsBtn);
|
controls.appendChild(settingsBtn);
|
||||||
controls.appendChild(deleteBtn);
|
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', () => {
|
element.addEventListener('mouseenter', () => {
|
||||||
|
isHoveringWidget = true;
|
||||||
if (this.isEditMode) {
|
if (this.isEditMode) {
|
||||||
controls.style.opacity = '1';
|
controls.style.opacity = '1';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
element.addEventListener('mouseleave', () => {
|
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
|
* @param {string} widgetId - Widget ID
|
||||||
*/
|
*/
|
||||||
removeWidgetControls(widgetId) {
|
removeWidgetControls(widgetId) {
|
||||||
const controls = this.widgetControlsMap.get(widgetId);
|
const data = this.widgetControlsMap.get(widgetId);
|
||||||
if (controls) {
|
if (data) {
|
||||||
controls.remove();
|
if (data.controls) {
|
||||||
|
data.controls.remove();
|
||||||
|
}
|
||||||
this.widgetControlsMap.delete(widgetId);
|
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
|
* Create a control button
|
||||||
* @param {string} icon - Button icon/text
|
* @param {string} icon - Button icon/text
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export class ResizeHandler {
|
|||||||
constructor(gridEngine, options = {}) {
|
constructor(gridEngine, options = {}) {
|
||||||
this.gridEngine = gridEngine;
|
this.gridEngine = gridEngine;
|
||||||
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
|
this.editManager = options.editManager || null; // Reference to EditModeManager for lock state
|
||||||
|
this.resizeHandlesOverlay = options.resizeHandlesOverlay || null; // Overlay container for handles
|
||||||
this.options = {
|
this.options = {
|
||||||
showDimensions: true,
|
showDimensions: true,
|
||||||
showGrid: true,
|
showGrid: true,
|
||||||
@@ -74,7 +75,19 @@ export class ResizeHandler {
|
|||||||
initWidget(element, widget, onResizeEnd, constraints = {}) {
|
initWidget(element, widget, onResizeEnd, constraints = {}) {
|
||||||
// Create resize handles
|
// Create resize handles
|
||||||
const handles = this.createResizeHandles();
|
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
|
// Store constraints
|
||||||
const widgetConstraints = {
|
const widgetConstraints = {
|
||||||
@@ -215,6 +228,25 @@ export class ResizeHandler {
|
|||||||
return container;
|
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
|
* Start resize operation
|
||||||
* @param {MouseEvent|Touch} e - Pointer event
|
* @param {MouseEvent|Touch} e - Pointer event
|
||||||
@@ -416,6 +448,12 @@ export class ResizeHandler {
|
|||||||
onResizeEnd(widget, widget.w, widget.h, widget.x, widget.y);
|
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();
|
this.cleanup();
|
||||||
console.log('[ResizeHandler] Resize completed:', widget.id, `${widget.w}×${widget.h} at (${widget.x}, ${widget.y})`);
|
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
|
// Remove resizing class
|
||||||
element.classList.remove('resizing');
|
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();
|
this.cleanup();
|
||||||
console.log('[ResizeHandler] Resize cancelled');
|
console.log('[ResizeHandler] Resize cancelled');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1748,6 +1748,35 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
font-size: 1.05rem; /* Touch-friendly size for mobile readability */
|
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 {
|
.rpg-dashboard-grid {
|
||||||
|
|||||||
Reference in New Issue
Block a user