feat(dashboard): implement smart widget collision and category-aware layout
Complete dashboard v2 improvements for better UX and visual consistency: **1. Push-Aside Drag/Drop (dragDrop.js)** - Replace swap/revert logic with intelligent reflow algorithm - When widgets collide on drag, automatically push overlapping widgets down - All affected widgets repositioned after reflow completes - Eliminates widget overlap issues **2. Unified Widget Styling (style.css)** - Add consistent .rpg-widget container styling for all widgets - Background: rgba(0,0,0,0.2) for visual separation - Border-left: 3px highlight for category identification - Box-shadow and border-radius for depth and polish - Maintain individual widget decorative styles **3. Logical Default Layout (defaultLayout.js)** - Reorganize widgets into semantic clusters with clear comments: - USER CLUSTER (top): userInfo → userStats → userMood + userAttributes - SCENE CLUSTER (middle): calendar + weather → temp + clock → location - SOCIAL CLUSTER (bottom): presentCharacters - userInfo widget now at top (y=0) as expected - All positions use rem units for responsive scaling **4. Category-Aware Auto-Layout (dashboardManager.js)** - Implement sortWidgetsByCategory() with priority ordering: user → scene → social → inventory - Within user category, specific ordering: userInfo → userStats → userMood → userAttributes - Add preserveOrder option to gridEngine.autoLayout() - Auto-arrange now uses logical grouping instead of random bin-packing **5. Multi-Tab Auto-Distribution (dashboardManager.js)** - Add estimateLayoutHeight() to detect when content exceeds threshold - Implement distributeWidgetsByCategory() for automatic tab creation: - "Status" tab: user + scene widgets - "Social" tab: social widgets (if any) - "Inventory" tab: inventory widgets (if any) - Each tab gets category-aware auto-layout - 80rem height threshold for single-tab limit **6. Widget Category Metadata (widgets/)** - Add category field to all widget definitions: - userInfo, userStats, userMood, userAttributes: 'user' - calendar, weather, temperature, clock, location: 'scene' - presentCharacters: 'social' - inventory: 'inventory' **7. Integration Improvements (dashboardIntegration.js)** - Set default layout on initialization for reset functionality - Add reset layout button to dashboard header - Wire up reset button event handler **8. Core State Management (index.js)** - Add getInfoBoxData() and setInfoBoxData() to state API - Ensure info box data persists across sessions **Technical Details:** - Rem units throughout for 1080p→4K→mobile responsive scaling - Reflow algorithm leverages existing gridEngine collision detection - Category-aware sorting preserves logical relationships - Multi-tab distribution prevents single-page scroll fatigue - All changes maintain backwards compatibility with existing layouts Fixes dashboard issues after rem unit conversion introduced massive positioning bugs. Users reported widgets overlapping on drag, visual inconsistency, and random auto-arrange behavior. Related: Epic 2 (Dashboard v2), Phase 3.2
This commit is contained in:
@@ -518,12 +518,17 @@ async function initUI() {
|
|||||||
getFallbackAvatar: () => FALLBACK_AVATAR_DATA_URI,
|
getFallbackAvatar: () => FALLBACK_AVATAR_DATA_URI,
|
||||||
getAvatarUrl: (type, avatar) => getThumbnailUrl(type, avatar),
|
getAvatarUrl: (type, avatar) => getThumbnailUrl(type, avatar),
|
||||||
getCharacterThoughts: () => extensionSettings.characterThoughts || '',
|
getCharacterThoughts: () => extensionSettings.characterThoughts || '',
|
||||||
|
getInfoBoxData: () => extensionSettings.infoBoxData || 'Info Box\n---\n',
|
||||||
|
|
||||||
// Data setters
|
// Data setters
|
||||||
setCharacterThoughts: (value) => {
|
setCharacterThoughts: (value) => {
|
||||||
extensionSettings.characterThoughts = value;
|
extensionSettings.characterThoughts = value;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
},
|
},
|
||||||
|
setInfoBoxData: (value) => {
|
||||||
|
extensionSettings.infoBoxData = value;
|
||||||
|
saveSettings();
|
||||||
|
},
|
||||||
|
|
||||||
// Event callbacks
|
// Event callbacks
|
||||||
onDataChange: (dataType, field, value, extra) => {
|
onDataChange: (dataType, field, value, extra) => {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { saveSettings } from '../../core/persistence.js';
|
|||||||
import { renderExtensionTemplateAsync } from '../../../../../../extensions.js';
|
import { renderExtensionTemplateAsync } from '../../../../../../extensions.js';
|
||||||
import { DashboardManager } from './dashboardManager.js';
|
import { DashboardManager } from './dashboardManager.js';
|
||||||
import { WidgetRegistry } from './widgetRegistry.js';
|
import { WidgetRegistry } from './widgetRegistry.js';
|
||||||
|
import { generateDefaultDashboard } from './defaultLayout.js';
|
||||||
|
|
||||||
// Widget imports
|
// Widget imports
|
||||||
import { registerUserInfoWidget } from './widgets/userInfoWidget.js';
|
import { registerUserInfoWidget } from './widgets/userInfoWidget.js';
|
||||||
@@ -92,6 +93,11 @@ export async function initializeDashboard(dependencies) {
|
|||||||
// Initialize the dashboard
|
// Initialize the dashboard
|
||||||
await dashboardManager.init();
|
await dashboardManager.init();
|
||||||
|
|
||||||
|
// Set default layout (required for reset functionality)
|
||||||
|
const defaultLayout = generateDefaultDashboard();
|
||||||
|
dashboardManager.setDefaultLayout(defaultLayout);
|
||||||
|
console.log('[RPG Companion] Default layout set with', defaultLayout.tabs.length, 'tabs');
|
||||||
|
|
||||||
// Set up dashboard event listeners
|
// Set up dashboard event listeners
|
||||||
setupDashboardEventListeners(dependencies);
|
setupDashboardEventListeners(dependencies);
|
||||||
|
|
||||||
@@ -139,6 +145,9 @@ function getInlineDashboardTemplate() {
|
|||||||
<div id="rpg-dashboard-tabs" class="rpg-dashboard-tabs"></div>
|
<div id="rpg-dashboard-tabs" class="rpg-dashboard-tabs"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="rpg-dashboard-header-right">
|
<div class="rpg-dashboard-header-right">
|
||||||
|
<button id="rpg-dashboard-reset-layout" class="rpg-dashboard-btn rpg-reset-layout-btn" title="Reset to Default Layout">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i>
|
||||||
|
</button>
|
||||||
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
|
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
|
||||||
<i class="fa-solid fa-table-cells-large"></i>
|
<i class="fa-solid fa-table-cells-large"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -194,13 +203,26 @@ function registerAllWidgets(registry, dependencies) {
|
|||||||
* Set up dashboard event listeners
|
* Set up dashboard event listeners
|
||||||
*/
|
*/
|
||||||
function setupDashboardEventListeners(dependencies) {
|
function setupDashboardEventListeners(dependencies) {
|
||||||
|
// Reset layout button
|
||||||
|
const resetLayoutBtn = document.querySelector('#rpg-dashboard-reset-layout');
|
||||||
|
if (resetLayoutBtn) {
|
||||||
|
resetLayoutBtn.addEventListener('click', () => {
|
||||||
|
if (dashboardManager) {
|
||||||
|
if (confirm('Reset dashboard to default layout? This will remove all widgets and reload the defaults.')) {
|
||||||
|
console.log('[RPG Companion] Reset layout button clicked');
|
||||||
|
dashboardManager.resetLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Auto-layout button
|
// Auto-layout button
|
||||||
const autoLayoutBtn = document.querySelector('#rpg-dashboard-auto-layout');
|
const autoLayoutBtn = document.querySelector('#rpg-dashboard-auto-layout');
|
||||||
if (autoLayoutBtn) {
|
if (autoLayoutBtn) {
|
||||||
autoLayoutBtn.addEventListener('click', () => {
|
autoLayoutBtn.addEventListener('click', () => {
|
||||||
if (dashboardManager) {
|
if (dashboardManager) {
|
||||||
console.log('[RPG Companion] Auto-layout button clicked');
|
console.log('[RPG Companion] Auto-layout button clicked');
|
||||||
dashboardManager.autoLayoutWidgets({ preferFullWidth: true });
|
dashboardManager.autoLayoutWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -332,32 +354,36 @@ export function createDefaultLayout(manager) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[RPG Companion] Creating default dashboard layout (2-column optimized)...');
|
console.log('[RPG Companion] Creating default dashboard layout with modular widgets...');
|
||||||
|
|
||||||
const mainTab = manager.tabManager.getActiveTabId();
|
const mainTab = manager.tabManager.getActiveTabId();
|
||||||
|
|
||||||
// Add widgets with 2-column layout positions
|
// Add modular user widgets
|
||||||
// Row 1-2: User Stats (full width)
|
// Row 0: User Info (avatar, name, level) - full width
|
||||||
manager.addWidget('userStats', mainTab, { x: 0, y: 0, w: 2, h: 3 });
|
manager.addWidget('userInfo', mainTab, { x: 0, y: 0, w: 2, h: 1 });
|
||||||
|
|
||||||
// Row 3: Calendar (left) + Weather (right)
|
// Row 1-2: User Stats (health/energy bars) - full width
|
||||||
manager.addWidget('calendar', mainTab, { x: 0, y: 3, w: 1, h: 2 });
|
manager.addWidget('userStats', mainTab, { x: 0, y: 1, w: 2, h: 2 });
|
||||||
manager.addWidget('weather', mainTab, { x: 1, y: 3, w: 1, h: 2 });
|
|
||||||
|
|
||||||
// Row 4: Temperature (left) + Clock (right)
|
// Row 3-4: User Mood (left) + User Attributes (right)
|
||||||
manager.addWidget('temperature', mainTab, { x: 0, y: 5, w: 1, h: 2 });
|
manager.addWidget('userMood', mainTab, { x: 0, y: 3, w: 1, h: 1 });
|
||||||
manager.addWidget('clock', mainTab, { x: 1, y: 5, w: 1, h: 2 });
|
manager.addWidget('userAttributes', mainTab, { x: 1, y: 3, w: 1, h: 2 });
|
||||||
|
|
||||||
// Row 5: Location (full width)
|
// Row 5-6: Calendar (left) + Weather (right)
|
||||||
manager.addWidget('location', mainTab, { x: 0, y: 7, w: 2, h: 2 });
|
manager.addWidget('calendar', mainTab, { x: 0, y: 5, w: 1, h: 2 });
|
||||||
|
manager.addWidget('weather', mainTab, { x: 1, y: 5, w: 1, h: 2 });
|
||||||
|
|
||||||
// Row 6-7: Present Characters (full width)
|
// Row 7-8: Temperature (left) + Clock (right)
|
||||||
manager.addWidget('presentCharacters', mainTab, { x: 0, y: 9, w: 2, h: 3 });
|
manager.addWidget('temperature', mainTab, { x: 0, y: 7, w: 1, h: 2 });
|
||||||
|
manager.addWidget('clock', mainTab, { x: 1, y: 7, w: 1, h: 2 });
|
||||||
|
|
||||||
// Row 8-13: Inventory (full width)
|
// Row 9-10: Location (full width)
|
||||||
manager.addWidget('inventory', mainTab, { x: 0, y: 12, w: 2, h: 6 });
|
manager.addWidget('location', mainTab, { x: 0, y: 9, w: 2, h: 2 });
|
||||||
|
|
||||||
console.log('[RPG Companion] Default layout created (2-column optimized)');
|
// Row 11-13: Present Characters (full width)
|
||||||
|
manager.addWidget('presentCharacters', mainTab, { x: 0, y: 11, w: 2, h: 3 });
|
||||||
|
|
||||||
|
console.log('[RPG Companion] Default layout created with modular widgets');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ export class DashboardManager {
|
|||||||
this.container = container;
|
this.container = container;
|
||||||
this.config = {
|
this.config = {
|
||||||
columns: config.columns || 12,
|
columns: config.columns || 12,
|
||||||
rowHeight: config.rowHeight || 80,
|
rowHeight: config.rowHeight || 5, // rem units for responsive scaling
|
||||||
gap: config.gap || 12,
|
gap: config.gap || 0.75, // rem units for responsive scaling
|
||||||
debounceMs: config.debounceMs || 500,
|
debounceMs: config.debounceMs || 500,
|
||||||
onSave: config.onSave,
|
onSave: config.onSave,
|
||||||
onLoad: config.onLoad,
|
onLoad: config.onLoad,
|
||||||
@@ -564,7 +564,11 @@ export class DashboardManager {
|
|||||||
this.dragHandler.initWidget(element, widget, (updated, newX, newY) => {
|
this.dragHandler.initWidget(element, widget, (updated, newX, newY) => {
|
||||||
widget.x = newX;
|
widget.x = newX;
|
||||||
widget.y = newY;
|
widget.y = newY;
|
||||||
this.repositionWidget(element, widget);
|
|
||||||
|
// After drag (which may have triggered reflow), reposition ALL widgets
|
||||||
|
// because reflow may have moved other widgets
|
||||||
|
this.repositionAllWidgetsInCurrentTab();
|
||||||
|
|
||||||
this.triggerAutoSave();
|
this.triggerAutoSave();
|
||||||
}, allWidgets);
|
}, allWidgets);
|
||||||
|
|
||||||
@@ -650,6 +654,185 @@ export class DashboardManager {
|
|||||||
console.log('[DashboardManager] Repositioned all widgets');
|
console.log('[DashboardManager] Repositioned all widgets');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reposition all widgets in the current tab
|
||||||
|
* Used after drag/drop reflow to update positions of all affected widgets
|
||||||
|
*/
|
||||||
|
repositionAllWidgetsInCurrentTab() {
|
||||||
|
const currentTab = this.tabManager.getTab(this.currentTabId);
|
||||||
|
if (!currentTab) return;
|
||||||
|
|
||||||
|
// Reposition each widget in the current tab
|
||||||
|
currentTab.widgets.forEach((widget) => {
|
||||||
|
const widgetData = this.widgets.get(widget.id);
|
||||||
|
if (widgetData && widgetData.element) {
|
||||||
|
this.repositionWidget(widgetData.element, widget);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Repositioned all widgets in current tab after reflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate total height needed for widgets if laid out
|
||||||
|
* Simple estimation: sum all widget heights + gaps
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} widgets - Widgets to estimate
|
||||||
|
* @returns {number} Estimated height in rem
|
||||||
|
*/
|
||||||
|
estimateLayoutHeight(widgets) {
|
||||||
|
if (widgets.length === 0) return 0;
|
||||||
|
|
||||||
|
// Sum all heights (widgets are already in rem units)
|
||||||
|
const totalHeight = widgets.reduce((sum, w) => sum + w.h, 0);
|
||||||
|
|
||||||
|
// Add gaps (rowHeight + gap between each widget)
|
||||||
|
const gaps = (widgets.length - 1) * this.gridEngine.gap;
|
||||||
|
|
||||||
|
return totalHeight * this.gridEngine.rowHeight + gaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distribute widgets across multiple tabs by category
|
||||||
|
* Creates category-based tabs: Status, Social, Inventory
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} widgets - All widgets to distribute
|
||||||
|
*/
|
||||||
|
distributeWidgetsByCategory(widgets) {
|
||||||
|
console.log('[DashboardManager] Distributing widgets across multiple tabs');
|
||||||
|
|
||||||
|
// Group widgets by category
|
||||||
|
const groups = {
|
||||||
|
user: [],
|
||||||
|
scene: [],
|
||||||
|
social: [],
|
||||||
|
inventory: []
|
||||||
|
};
|
||||||
|
|
||||||
|
widgets.forEach(widget => {
|
||||||
|
const def = this.registry.get(widget.type);
|
||||||
|
const category = def?.category || 'user';
|
||||||
|
if (groups[category]) {
|
||||||
|
groups[category].push(widget);
|
||||||
|
} else {
|
||||||
|
groups.user.push(widget); // Fallback to user
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear existing tabs
|
||||||
|
this.dashboard.tabs = [];
|
||||||
|
|
||||||
|
// Create Status tab (user + scene)
|
||||||
|
const statusWidgets = [...groups.user, ...groups.scene];
|
||||||
|
if (statusWidgets.length > 0) {
|
||||||
|
this.dashboard.tabs.push({
|
||||||
|
id: 'tab-status',
|
||||||
|
name: 'Status',
|
||||||
|
icon: '📊',
|
||||||
|
order: 0,
|
||||||
|
widgets: statusWidgets
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-layout status widgets
|
||||||
|
this.gridEngine.autoLayout(statusWidgets, { preserveOrder: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Social tab if there are social widgets
|
||||||
|
if (groups.social.length > 0) {
|
||||||
|
this.dashboard.tabs.push({
|
||||||
|
id: 'tab-social',
|
||||||
|
name: 'Social',
|
||||||
|
icon: '👥',
|
||||||
|
order: 1,
|
||||||
|
widgets: groups.social
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-layout social widgets
|
||||||
|
this.gridEngine.autoLayout(groups.social, { preserveOrder: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Inventory tab if there are inventory widgets
|
||||||
|
if (groups.inventory.length > 0) {
|
||||||
|
this.dashboard.tabs.push({
|
||||||
|
id: 'tab-inventory',
|
||||||
|
name: 'Inventory',
|
||||||
|
icon: '🎒',
|
||||||
|
order: 2,
|
||||||
|
widgets: groups.inventory
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-layout inventory widgets
|
||||||
|
this.gridEngine.autoLayout(groups.inventory, { preserveOrder: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Created', this.dashboard.tabs.length, 'tabs');
|
||||||
|
|
||||||
|
// Re-render tabs and switch to first tab
|
||||||
|
this.renderTabs();
|
||||||
|
if (this.dashboard.tabs.length > 0) {
|
||||||
|
this.switchTab(this.dashboard.tabs[0].id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save layout
|
||||||
|
this.triggerAutoSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort widgets by category for logical auto-layout
|
||||||
|
* Groups: user → scene → social → inventory
|
||||||
|
* Within groups, maintains smart ordering (e.g., userInfo before userStats)
|
||||||
|
*
|
||||||
|
* @param {Array<Object>} widgets - Widgets to sort
|
||||||
|
* @returns {Array<Object>} Sorted widgets
|
||||||
|
*/
|
||||||
|
sortWidgetsByCategory(widgets) {
|
||||||
|
// Category priority order
|
||||||
|
const categoryOrder = {
|
||||||
|
'user': 1,
|
||||||
|
'scene': 2,
|
||||||
|
'social': 3,
|
||||||
|
'inventory': 4,
|
||||||
|
'other': 5
|
||||||
|
};
|
||||||
|
|
||||||
|
// Specific widget type ordering within user category
|
||||||
|
const userWidgetOrder = {
|
||||||
|
'userInfo': 1, // Name/level at top
|
||||||
|
'userStats': 2, // Health/energy bars
|
||||||
|
'userMood': 3, // Mood
|
||||||
|
'userAttributes': 4 // STR/DEX/etc
|
||||||
|
};
|
||||||
|
|
||||||
|
return [...widgets].sort((a, b) => {
|
||||||
|
// Get widget definitions from registry
|
||||||
|
const defA = this.registry.get(a.type);
|
||||||
|
const defB = this.registry.get(b.type);
|
||||||
|
|
||||||
|
const catA = defA?.category || 'other';
|
||||||
|
const catB = defB?.category || 'other';
|
||||||
|
|
||||||
|
// Sort by category first
|
||||||
|
const catOrderA = categoryOrder[catA] || 999;
|
||||||
|
const catOrderB = categoryOrder[catB] || 999;
|
||||||
|
|
||||||
|
if (catOrderA !== catOrderB) {
|
||||||
|
return catOrderA - catOrderB;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within user category, use specific ordering
|
||||||
|
if (catA === 'user' && catB === 'user') {
|
||||||
|
const orderA = userWidgetOrder[a.type] || 999;
|
||||||
|
const orderB = userWidgetOrder[b.type] || 999;
|
||||||
|
if (orderA !== orderB) {
|
||||||
|
return orderA - orderB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise maintain original order
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find available position for new widget
|
* Find available position for new widget
|
||||||
* @param {Object} size - Widget size { w, h }
|
* @param {Object} size - Widget size { w, h }
|
||||||
@@ -862,6 +1045,22 @@ export class DashboardManager {
|
|||||||
applyDashboardConfig(config) {
|
applyDashboardConfig(config) {
|
||||||
console.log('[DashboardManager] Applying dashboard config');
|
console.log('[DashboardManager] Applying dashboard config');
|
||||||
|
|
||||||
|
// Update grid config from dashboard config
|
||||||
|
if (config.gridConfig) {
|
||||||
|
this.config.rowHeight = config.gridConfig.rowHeight || this.config.rowHeight;
|
||||||
|
this.config.gap = config.gridConfig.gap || this.config.gap;
|
||||||
|
|
||||||
|
// Update gridEngine with new config
|
||||||
|
if (this.gridEngine) {
|
||||||
|
this.gridEngine.rowHeight = this.config.rowHeight;
|
||||||
|
this.gridEngine.gap = this.config.gap;
|
||||||
|
console.log('[DashboardManager] Updated grid config:', {
|
||||||
|
rowHeight: this.config.rowHeight + 'rem',
|
||||||
|
gap: this.config.gap + 'rem'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Clear existing
|
// Clear existing
|
||||||
this.clearGrid();
|
this.clearGrid();
|
||||||
|
|
||||||
@@ -951,8 +1150,19 @@ export class DashboardManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Resetting to default layout...');
|
||||||
|
console.log('[DashboardManager] Default layout has:', this.defaultLayout.tabs.length, 'tabs');
|
||||||
|
this.defaultLayout.tabs.forEach(tab => {
|
||||||
|
console.log(`[DashboardManager] Tab "${tab.name}" (${tab.id}):`, tab.widgets.length, 'widgets');
|
||||||
|
});
|
||||||
|
|
||||||
await this.persistence.resetToDefault(this.defaultLayout);
|
await this.persistence.resetToDefault(this.defaultLayout);
|
||||||
this.applyDashboardConfig(this.defaultLayout);
|
this.applyDashboardConfig(this.defaultLayout);
|
||||||
|
|
||||||
|
// Force re-render tabs
|
||||||
|
this.renderTabs();
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Reset complete');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -983,12 +1193,31 @@ export class DashboardManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run auto-layout algorithm on widgets
|
// Smart category-aware sorting BEFORE auto-layout
|
||||||
const widgetsToLayout = [...currentTab.widgets];
|
const widgetsToLayout = this.sortWidgetsByCategory(currentTab.widgets);
|
||||||
this.gridEngine.autoLayout(widgetsToLayout, options);
|
|
||||||
|
|
||||||
// Update tab widgets with new positions
|
// Calculate estimated height to determine if multi-tab distribution is needed
|
||||||
currentTab.widgets = widgetsToLayout;
|
const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout);
|
||||||
|
const heightThreshold = 80; // rem - reasonable max height for single tab
|
||||||
|
|
||||||
|
console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem');
|
||||||
|
|
||||||
|
// If widgets fit comfortably, use single-tab auto-layout
|
||||||
|
if (estimatedHeight <= heightThreshold) {
|
||||||
|
console.log('[DashboardManager] Using single-tab auto-layout');
|
||||||
|
|
||||||
|
// Run auto-layout algorithm on pre-sorted widgets
|
||||||
|
// (gridEngine will preserve this logical order instead of sorting by area)
|
||||||
|
this.gridEngine.autoLayout(widgetsToLayout, { preserveOrder: true });
|
||||||
|
|
||||||
|
// Update tab widgets with new positions
|
||||||
|
currentTab.widgets = widgetsToLayout;
|
||||||
|
} else {
|
||||||
|
// Too many widgets - distribute across multiple tabs by category
|
||||||
|
console.log('[DashboardManager] Height exceeds threshold, using multi-tab distribution');
|
||||||
|
this.distributeWidgetsByCategory(widgetsToLayout);
|
||||||
|
return; // distributeWidgetsByCategory handles rendering
|
||||||
|
}
|
||||||
|
|
||||||
// Re-render all widgets with new positions
|
// Re-render all widgets with new positions
|
||||||
this.clearGrid();
|
this.clearGrid();
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rpg-dashboard-header-right">
|
<div class="rpg-dashboard-header-right">
|
||||||
|
<!-- Reset Layout Button -->
|
||||||
|
<button id="rpg-dashboard-reset-layout" class="rpg-dashboard-btn rpg-reset-layout-btn" title="Reset to Default Layout">
|
||||||
|
<i class="fa-solid fa-rotate-left"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Auto-Layout Button -->
|
<!-- Auto-Layout Button -->
|
||||||
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
|
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
|
||||||
<i class="fa-solid fa-table-cells-large"></i>
|
<i class="fa-solid fa-table-cells-large"></i>
|
||||||
|
|||||||
@@ -24,8 +24,9 @@ export function generateDefaultDashboard() {
|
|||||||
gridConfig: {
|
gridConfig: {
|
||||||
// Columns calculated dynamically by GridEngine (2-4 based on panel width)
|
// Columns calculated dynamically by GridEngine (2-4 based on panel width)
|
||||||
// Mobile: always 2, Desktop: 2-4 based on width
|
// Mobile: always 2, Desktop: 2-4 based on width
|
||||||
rowHeight: 80,
|
columns: 2, // Default to 2 columns (will be recalculated on init)
|
||||||
gap: 12,
|
rowHeight: 5, // rem units for responsive scaling (1080p → 4K → mobile)
|
||||||
|
gap: 0.75, // rem units (scales with screen DPI)
|
||||||
snapToGrid: true,
|
snapToGrid: true,
|
||||||
showGrid: true
|
showGrid: true
|
||||||
},
|
},
|
||||||
@@ -37,25 +38,56 @@ export function generateDefaultDashboard() {
|
|||||||
icon: '📊',
|
icon: '📊',
|
||||||
order: 0,
|
order: 0,
|
||||||
widgets: [
|
widgets: [
|
||||||
// Row 1: User Stats (full width)
|
// === USER CLUSTER (Top) ===
|
||||||
|
// Row 0: User Info (avatar, name, level) - AT TOP
|
||||||
|
{
|
||||||
|
id: 'widget-userinfo',
|
||||||
|
type: 'userInfo',
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
w: 2,
|
||||||
|
h: 1,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
// Row 1-2: User Stats (health/energy bars)
|
||||||
{
|
{
|
||||||
id: 'widget-userstats',
|
id: 'widget-userstats',
|
||||||
type: 'userStats',
|
type: 'userStats',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 1,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 3,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
showClassicStats: true,
|
statBarGradient: true
|
||||||
statBarStyle: 'gradient'
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Row 2: Calendar (left) + Weather (right)
|
// Row 3-4: User Mood (left) + User Attributes (right)
|
||||||
|
{
|
||||||
|
id: 'widget-usermood',
|
||||||
|
type: 'userMood',
|
||||||
|
x: 0,
|
||||||
|
y: 3,
|
||||||
|
w: 1,
|
||||||
|
h: 1,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'widget-userattributes',
|
||||||
|
type: 'userAttributes',
|
||||||
|
x: 1,
|
||||||
|
y: 3,
|
||||||
|
w: 1,
|
||||||
|
h: 2,
|
||||||
|
config: {}
|
||||||
|
},
|
||||||
|
|
||||||
|
// === SCENE CLUSTER (Middle) ===
|
||||||
|
// Row 5-6: Calendar (left) + Weather (right)
|
||||||
{
|
{
|
||||||
id: 'widget-calendar',
|
id: 'widget-calendar',
|
||||||
type: 'calendar',
|
type: 'calendar',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 3,
|
y: 5,
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {}
|
config: {}
|
||||||
@@ -64,19 +96,19 @@ export function generateDefaultDashboard() {
|
|||||||
id: 'widget-weather',
|
id: 'widget-weather',
|
||||||
type: 'weather',
|
type: 'weather',
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 3,
|
y: 5,
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
compact: false
|
compact: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Row 3: Temperature (left) + Clock (right)
|
// Row 7-8: Temperature (left) + Clock (right)
|
||||||
{
|
{
|
||||||
id: 'widget-temperature',
|
id: 'widget-temperature',
|
||||||
type: 'temperature',
|
type: 'temperature',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 5,
|
y: 7,
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
@@ -87,29 +119,31 @@ export function generateDefaultDashboard() {
|
|||||||
id: 'widget-clock',
|
id: 'widget-clock',
|
||||||
type: 'clock',
|
type: 'clock',
|
||||||
x: 1,
|
x: 1,
|
||||||
y: 5,
|
y: 7,
|
||||||
w: 1,
|
w: 1,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {
|
config: {
|
||||||
format: 'digital'
|
format: 'digital'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Row 4: Location (full width)
|
// Row 9-10: Location (full width)
|
||||||
{
|
{
|
||||||
id: 'widget-location',
|
id: 'widget-location',
|
||||||
type: 'location',
|
type: 'location',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 7,
|
y: 9,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 2,
|
h: 2,
|
||||||
config: {}
|
config: {}
|
||||||
},
|
},
|
||||||
// Row 5-6: Present Characters (full width)
|
|
||||||
|
// === SOCIAL CLUSTER (Bottom) ===
|
||||||
|
// Row 11-13: Present Characters (full width)
|
||||||
{
|
{
|
||||||
id: 'widget-presentchars',
|
id: 'widget-presentchars',
|
||||||
type: 'presentCharacters',
|
type: 'presentCharacters',
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 9,
|
y: 11,
|
||||||
w: 2,
|
w: 2,
|
||||||
h: 3,
|
h: 3,
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -282,46 +282,19 @@ export class DragDropHandler {
|
|||||||
const collision = this.gridEngine.detectCollision(widget, otherWidgets);
|
const collision = this.gridEngine.detectCollision(widget, otherWidgets);
|
||||||
|
|
||||||
if (collision) {
|
if (collision) {
|
||||||
// Find which widget we collided with
|
console.log('[DragDrop] Collision detected, pushing widgets aside and reflowing');
|
||||||
const collidedWidget = otherWidgets.find(other => {
|
|
||||||
return !(
|
|
||||||
widget.x + widget.w <= other.x ||
|
|
||||||
widget.x >= other.x + other.w ||
|
|
||||||
widget.y + widget.h <= other.y ||
|
|
||||||
widget.y >= other.y + other.h
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// If same size, swap positions
|
// Instead of reverting, reflow all widgets to push collisions aside
|
||||||
if (collidedWidget && widget.w === collidedWidget.w && widget.h === collidedWidget.h) {
|
// The reflow algorithm will automatically push overlapping widgets down
|
||||||
console.log('[DragDrop] Swapping positions with:', collidedWidget.id);
|
const allWidgets = [widget, ...otherWidgets];
|
||||||
const tempX = collidedWidget.x;
|
this.gridEngine.reflow(allWidgets);
|
||||||
const tempY = collidedWidget.y;
|
|
||||||
collidedWidget.x = widget.x;
|
|
||||||
collidedWidget.y = widget.y;
|
|
||||||
widget.x = tempX;
|
|
||||||
widget.y = tempY;
|
|
||||||
|
|
||||||
// Call callback with swapped position
|
console.log('[DragDrop] Reflow complete, widget at:', widget.x, widget.y);
|
||||||
if (onDragEnd) {
|
}
|
||||||
onDragEnd(widget, widget.x, widget.y);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Different sizes or multiple collisions - revert to original
|
|
||||||
console.warn('[DragDrop] Collision detected, reverting to original position');
|
|
||||||
widget.x = originalX;
|
|
||||||
widget.y = originalY;
|
|
||||||
|
|
||||||
// Call callback with original position (no change)
|
// Always commit the position (either the dropped position or reflowed position)
|
||||||
if (onDragEnd) {
|
if (onDragEnd) {
|
||||||
onDragEnd(widget, widget.x, widget.y);
|
onDragEnd(widget, widget.x, widget.y);
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No collision, commit new position
|
|
||||||
if (onDragEnd) {
|
|
||||||
onDragEnd(widget, widget.x, widget.y);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ export class GridEngine {
|
|||||||
* Initialize grid engine with configuration
|
* Initialize grid engine with configuration
|
||||||
*
|
*
|
||||||
* @param {Object} config - Grid configuration
|
* @param {Object} config - Grid configuration
|
||||||
* @param {number} [config.rowHeight=80] - Height of each row in pixels
|
* @param {number} [config.rowHeight=5] - Height of each row in rem units
|
||||||
* @param {number} [config.gap=12] - Gap between widgets in pixels
|
* @param {number} [config.gap=0.75] - Gap between widgets in rem units
|
||||||
* @param {boolean} [config.snapToGrid=true] - Enable auto-snapping to grid
|
* @param {boolean} [config.snapToGrid=true] - Enable auto-snapping to grid
|
||||||
* @param {HTMLElement} [config.container=null] - Container element
|
* @param {HTMLElement} [config.container=null] - Container element
|
||||||
*/
|
*/
|
||||||
constructor(config = {}) {
|
constructor(config = {}) {
|
||||||
// Start with 2 columns (safest default for side panel)
|
// Start with 2 columns (safest default for side panel)
|
||||||
this.columns = 2;
|
this.columns = 2;
|
||||||
this.rowHeight = config.rowHeight || 80;
|
// Use rem for responsive sizing across all resolutions (1080p, 4K, mobile)
|
||||||
this.gap = config.gap || 12;
|
this.rowHeight = config.rowHeight || 5; // rem (was 80px)
|
||||||
|
this.gap = config.gap || 0.75; // rem (was 12px)
|
||||||
this.snapToGrid = config.snapToGrid !== false;
|
this.snapToGrid = config.snapToGrid !== false;
|
||||||
this.container = config.container || null;
|
this.container = config.container || null;
|
||||||
|
|
||||||
@@ -33,12 +34,32 @@ export class GridEngine {
|
|||||||
|
|
||||||
console.log('[GridEngine] Initialized:', {
|
console.log('[GridEngine] Initialized:', {
|
||||||
columns: this.columns,
|
columns: this.columns,
|
||||||
rowHeight: this.rowHeight,
|
rowHeight: this.rowHeight + 'rem',
|
||||||
gap: this.gap,
|
gap: this.gap + 'rem',
|
||||||
snapToGrid: this.snapToGrid
|
snapToGrid: this.snapToGrid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert rem to pixels using current browser font size
|
||||||
|
* @param {number} rem - Value in rem units
|
||||||
|
* @returns {number} Value in pixels
|
||||||
|
*/
|
||||||
|
remToPixels(rem) {
|
||||||
|
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
return rem * fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert pixels to rem using current browser font size
|
||||||
|
* @param {number} pixels - Value in pixels
|
||||||
|
* @returns {number} Value in rem
|
||||||
|
*/
|
||||||
|
pixelsToRem(pixels) {
|
||||||
|
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||||
|
return pixels / fontSize;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if we're on a mobile device
|
* Check if we're on a mobile device
|
||||||
* Mobile is defined as screen width ≤ 1000px
|
* Mobile is defined as screen width ≤ 1000px
|
||||||
@@ -101,6 +122,7 @@ export class GridEngine {
|
|||||||
*
|
*
|
||||||
* Converts grid-based widget position (x, y, w, h) to actual pixel values
|
* Converts grid-based widget position (x, y, w, h) to actual pixel values
|
||||||
* (left, top, width, height) for CSS positioning.
|
* (left, top, width, height) for CSS positioning.
|
||||||
|
* Note: rowHeight and gap are stored in rem, converted to pixels here.
|
||||||
*
|
*
|
||||||
* @param {Object} widget - Widget with grid coordinates
|
* @param {Object} widget - Widget with grid coordinates
|
||||||
* @param {number} widget.x - Grid column position (0-based)
|
* @param {number} widget.x - Grid column position (0-based)
|
||||||
@@ -121,24 +143,28 @@ export class GridEngine {
|
|||||||
this.columns = this.calculateColumns(350); // Recalculate columns for fallback
|
this.columns = this.calculateColumns(350); // Recalculate columns for fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert rem to pixels for calculations
|
||||||
|
const gapPx = this.remToPixels(this.gap);
|
||||||
|
const rowHeightPx = this.remToPixels(this.rowHeight);
|
||||||
|
|
||||||
// Calculate column width
|
// Calculate column width
|
||||||
// Formula: (containerWidth - gaps) / columns
|
// Formula: (containerWidth - gaps) / columns
|
||||||
// Gaps: (columns + 1) gaps total (one before each column + one after last)
|
// Gaps: (columns + 1) gaps total (one before each column + one after last)
|
||||||
const totalGaps = this.gap * (this.columns + 1);
|
const totalGaps = gapPx * (this.columns + 1);
|
||||||
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
||||||
|
|
||||||
// Calculate positions
|
// Calculate positions
|
||||||
// Left: x columns * (colWidth + gap) + initial gap
|
// Left: x columns * (colWidth + gap) + initial gap
|
||||||
const left = widget.x * (colWidth + this.gap) + this.gap;
|
const left = widget.x * (colWidth + gapPx) + gapPx;
|
||||||
|
|
||||||
// Top: y rows * (rowHeight + gap) + initial gap
|
// Top: y rows * (rowHeight + gap) + initial gap
|
||||||
const top = widget.y * (this.rowHeight + this.gap) + this.gap;
|
const top = widget.y * (rowHeightPx + gapPx) + gapPx;
|
||||||
|
|
||||||
// Width: w columns * colWidth + (w - 1) inner gaps
|
// Width: w columns * colWidth + (w - 1) inner gaps
|
||||||
const width = widget.w * colWidth + (widget.w - 1) * this.gap;
|
const width = widget.w * colWidth + (widget.w - 1) * gapPx;
|
||||||
|
|
||||||
// Height: h rows * rowHeight + (h - 1) inner gaps
|
// Height: h rows * rowHeight + (h - 1) inner gaps
|
||||||
const height = widget.h * this.rowHeight + (widget.h - 1) * this.gap;
|
const height = widget.h * rowHeightPx + (widget.h - 1) * gapPx;
|
||||||
|
|
||||||
return { left, top, width, height };
|
return { left, top, width, height };
|
||||||
}
|
}
|
||||||
@@ -187,11 +213,6 @@ export class GridEngine {
|
|||||||
colWidthPercent: colWidthPercent.toFixed(2) + '%'
|
colWidthPercent: colWidthPercent.toFixed(2) + '%'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate row height in vh for vertical scaling
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
const rowHeightVh = (this.rowHeight / viewportHeight) * 100;
|
|
||||||
const gapVh = (this.gap / viewportHeight) * 100;
|
|
||||||
|
|
||||||
// Calculate positions
|
// Calculate positions
|
||||||
// Horizontal: % of container (since widgets are absolutely positioned within container)
|
// Horizontal: % of container (since widgets are absolutely positioned within container)
|
||||||
const left = widget.x * (colWidthPercent + gapPercent) + gapPercent;
|
const left = widget.x * (colWidthPercent + gapPercent) + gapPercent;
|
||||||
@@ -203,15 +224,16 @@ export class GridEngine {
|
|||||||
formula: `${widget.w} * ${colWidthPercent.toFixed(2)}% + ${widget.w - 1} * ${gapPercent.toFixed(2)}%`
|
formula: `${widget.w} * ${colWidthPercent.toFixed(2)}% + ${widget.w - 1} * ${gapPercent.toFixed(2)}%`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Vertical: vh units (scales with viewport height)
|
// Vertical: rem units (scales across all resolutions - 1080p, 4K, mobile)
|
||||||
const top = widget.y * (rowHeightVh + gapVh) + gapVh;
|
// rem scales with browser font size, which adapts to screen DPI
|
||||||
const height = widget.h * rowHeightVh + (widget.h - 1) * gapVh;
|
const top = widget.y * (this.rowHeight + this.gap) + this.gap;
|
||||||
|
const height = widget.h * this.rowHeight + (widget.h - 1) * this.gap;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
left: `${left.toFixed(2)}%`,
|
left: `${left.toFixed(2)}%`,
|
||||||
top: `${top.toFixed(2)}vh`,
|
top: `${top.toFixed(2)}rem`,
|
||||||
width: `${width.toFixed(2)}%`,
|
width: `${width.toFixed(2)}%`,
|
||||||
height: `${height.toFixed(2)}vh`
|
height: `${height.toFixed(2)}rem`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,15 +272,19 @@ export class GridEngine {
|
|||||||
this.columns = this.calculateColumns(350); // Recalculate columns for fallback
|
this.columns = this.calculateColumns(350); // Recalculate columns for fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert rem to pixels for calculations
|
||||||
|
const gapPx = this.remToPixels(this.gap);
|
||||||
|
const rowHeightPx = this.remToPixels(this.rowHeight);
|
||||||
|
|
||||||
// Calculate column width
|
// Calculate column width
|
||||||
const totalGaps = this.gap * (this.columns + 1);
|
const totalGaps = gapPx * (this.columns + 1);
|
||||||
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
const colWidth = (this.containerWidth - totalGaps) / this.columns;
|
||||||
|
|
||||||
// Convert pixel to grid coordinates
|
// Convert pixel to grid coordinates
|
||||||
// Reverse of getPixelPosition formula
|
// Reverse of getPixelPosition formula
|
||||||
// x = (pixelX - gap) / (colWidth + gap)
|
// x = (pixelX - gap) / (colWidth + gap)
|
||||||
const x = Math.round((pixelX - this.gap) / (colWidth + this.gap));
|
const x = Math.round((pixelX - gapPx) / (colWidth + gapPx));
|
||||||
const y = Math.round((pixelY - this.gap) / (this.rowHeight + this.gap));
|
const y = Math.round((pixelY - gapPx) / (rowHeightPx + gapPx));
|
||||||
|
|
||||||
// Clamp to valid grid bounds
|
// Clamp to valid grid bounds
|
||||||
return {
|
return {
|
||||||
@@ -370,7 +396,7 @@ export class GridEngine {
|
|||||||
* Calculate total grid height needed for all widgets
|
* Calculate total grid height needed for all widgets
|
||||||
*
|
*
|
||||||
* @param {Array<Object>} widgets - Array of widgets
|
* @param {Array<Object>} widgets - Array of widgets
|
||||||
* @returns {number} Total height in pixels
|
* @returns {number} Total height in rem units
|
||||||
*/
|
*/
|
||||||
calculateGridHeight(widgets) {
|
calculateGridHeight(widgets) {
|
||||||
if (widgets.length === 0) return 0;
|
if (widgets.length === 0) return 0;
|
||||||
@@ -378,7 +404,7 @@ export class GridEngine {
|
|||||||
// Find the bottom-most widget
|
// Find the bottom-most widget
|
||||||
const maxY = Math.max(...widgets.map(w => w.y + w.h));
|
const maxY = Math.max(...widgets.map(w => w.y + w.h));
|
||||||
|
|
||||||
// Calculate total height including gaps
|
// Calculate total height including gaps (in rem)
|
||||||
return maxY * (this.rowHeight + this.gap) + this.gap;
|
return maxY * (this.rowHeight + this.gap) + this.gap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,37 +412,34 @@ export class GridEngine {
|
|||||||
* Auto-layout widgets to efficiently use all available space
|
* Auto-layout widgets to efficiently use all available space
|
||||||
*
|
*
|
||||||
* Packs widgets in reading order (left to right, top to bottom) with no gaps.
|
* Packs widgets in reading order (left to right, top to bottom) with no gaps.
|
||||||
|
* Respects each widget's defined size - only repositions, doesn't resize.
|
||||||
* Respects current column count (responsive to panel width).
|
* Respects current column count (responsive to panel width).
|
||||||
* Scales widgets to maximize space usage while respecting minimum sizes.
|
|
||||||
*
|
*
|
||||||
* Strategy:
|
* Strategy:
|
||||||
* 1. Sort widgets by area (largest first) for better packing
|
* 1. Sort widgets (by area or preserve order if requested)
|
||||||
* 2. For each widget, try to fit full width (all columns)
|
* 2. For each widget, keep its defined size (w, h)
|
||||||
* 3. If widget prefers smaller size, use that
|
* 3. Find first available position from top-left
|
||||||
* 4. Find first available position from top-left
|
* 4. Ensure no overlaps
|
||||||
* 5. Ensure no overlaps
|
* 5. If widget doesn't fit at preferred size, try narrower widths
|
||||||
*
|
*
|
||||||
* @param {Array<Object>} widgets - Array of widgets to auto-layout
|
* @param {Array<Object>} widgets - Array of widgets to auto-layout
|
||||||
* @param {Object} options - Layout options
|
* @param {Object} options - Layout options
|
||||||
* @param {boolean} [options.preferFullWidth=true] - Prefer full-width widgets when possible
|
* @param {boolean} [options.preserveOrder=false] - Keep input order instead of sorting by area
|
||||||
* @param {Object} [options.minSize={w:1, h:2}] - Minimum widget size
|
|
||||||
* @returns {Array<Object>} Re-positioned widgets (same array, modified in place)
|
* @returns {Array<Object>} Re-positioned widgets (same array, modified in place)
|
||||||
*/
|
*/
|
||||||
autoLayout(widgets, options = {}) {
|
autoLayout(widgets, options = {}) {
|
||||||
if (widgets.length === 0) return widgets;
|
if (widgets.length === 0) return widgets;
|
||||||
|
|
||||||
const preferFullWidth = options.preferFullWidth !== false;
|
const preserveOrder = options.preserveOrder || false;
|
||||||
const minSize = options.minSize || { w: 1, h: 2 };
|
|
||||||
|
|
||||||
console.log('[GridEngine] Auto-layout started:', {
|
console.log('[GridEngine] Auto-layout started:', {
|
||||||
widgetCount: widgets.length,
|
widgetCount: widgets.length,
|
||||||
columns: this.columns,
|
columns: this.columns,
|
||||||
preferFullWidth,
|
preserveOrder
|
||||||
minSize
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort widgets by area (largest first) for better packing efficiency
|
// Sort widgets (or preserve input order for category-aware layout)
|
||||||
const sorted = [...widgets].sort((a, b) => {
|
const sorted = preserveOrder ? [...widgets] : [...widgets].sort((a, b) => {
|
||||||
const areaA = a.w * a.h;
|
const areaA = a.w * a.h;
|
||||||
const areaB = b.w * b.h;
|
const areaB = b.w * b.h;
|
||||||
if (areaB !== areaA) return areaB - areaA;
|
if (areaB !== areaA) return areaB - areaA;
|
||||||
@@ -470,30 +493,19 @@ export class GridEngine {
|
|||||||
|
|
||||||
// Process each widget
|
// Process each widget
|
||||||
sorted.forEach(widget => {
|
sorted.forEach(widget => {
|
||||||
// Determine optimal size for this widget
|
// Respect widget's defined size - only clamp to grid bounds
|
||||||
let targetW, targetH;
|
// Don't force sizes - widgets define their own optimal dimensions
|
||||||
|
let targetW = Math.min(widget.w, this.columns); // Clamp to column count
|
||||||
if (preferFullWidth) {
|
let targetH = widget.h; // Respect widget's height
|
||||||
// Try to use full width when possible
|
|
||||||
targetW = this.columns;
|
|
||||||
targetH = widget.h;
|
|
||||||
} else {
|
|
||||||
// Keep original size or clamp to current column count
|
|
||||||
targetW = Math.min(widget.w, this.columns);
|
|
||||||
targetH = widget.h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure minimum size
|
|
||||||
targetW = Math.max(minSize.w, Math.min(targetW, this.columns));
|
|
||||||
targetH = Math.max(minSize.h, targetH);
|
|
||||||
|
|
||||||
// Try to find position for preferred size
|
// Try to find position for preferred size
|
||||||
let pos = findPosition(targetW, targetH);
|
let pos = findPosition(targetW, targetH);
|
||||||
|
|
||||||
// If preferred size doesn't fit well, try smaller widths
|
// If preferred size doesn't fit well, try smaller widths
|
||||||
if (pos.y > 100 && targetW > minSize.w) {
|
// (but never go below 1 column)
|
||||||
|
if (pos.y > 100 && targetW > 1) {
|
||||||
// Widget would be placed very far down, try narrower width
|
// Widget would be placed very far down, try narrower width
|
||||||
for (let tryW = targetW - 1; tryW >= minSize.w; tryW--) {
|
for (let tryW = targetW - 1; tryW >= 1; tryW--) {
|
||||||
const tryPos = findPosition(tryW, targetH);
|
const tryPos = findPosition(tryW, targetH);
|
||||||
if (tryPos.y < pos.y) {
|
if (tryPos.y < pos.y) {
|
||||||
// Found better position with narrower width
|
// Found better position with narrower width
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export function registerCalendarWidget(registry, dependencies) {
|
|||||||
name: 'Calendar',
|
name: 'Calendar',
|
||||||
icon: '📅',
|
icon: '📅',
|
||||||
description: 'Date, weekday, month, and year display',
|
description: 'Date, weekday, month, and year display',
|
||||||
|
category: 'scene',
|
||||||
minSize: { w: 1, h: 2 },
|
minSize: { w: 1, h: 2 },
|
||||||
defaultSize: { w: 1, h: 2 },
|
defaultSize: { w: 1, h: 2 },
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
@@ -274,6 +275,7 @@ function attachCalendarHandlers(container, dependencies) {
|
|||||||
*/
|
*/
|
||||||
export function registerWeatherWidget(registry, dependencies) {
|
export function registerWeatherWidget(registry, dependencies) {
|
||||||
registry.register('weather', {
|
registry.register('weather', {
|
||||||
|
category: 'scene',
|
||||||
name: 'Weather',
|
name: 'Weather',
|
||||||
icon: '🌤️',
|
icon: '🌤️',
|
||||||
description: 'Weather emoji and forecast',
|
description: 'Weather emoji and forecast',
|
||||||
@@ -306,6 +308,7 @@ export function registerWeatherWidget(registry, dependencies) {
|
|||||||
*/
|
*/
|
||||||
export function registerTemperatureWidget(registry, dependencies) {
|
export function registerTemperatureWidget(registry, dependencies) {
|
||||||
registry.register('temperature', {
|
registry.register('temperature', {
|
||||||
|
category: 'scene',
|
||||||
name: 'Temperature',
|
name: 'Temperature',
|
||||||
icon: '🌡️',
|
icon: '🌡️',
|
||||||
description: 'Temperature display with thermometer',
|
description: 'Temperature display with thermometer',
|
||||||
@@ -345,6 +348,7 @@ export function registerTemperatureWidget(registry, dependencies) {
|
|||||||
*/
|
*/
|
||||||
export function registerClockWidget(registry, dependencies) {
|
export function registerClockWidget(registry, dependencies) {
|
||||||
registry.register('clock', {
|
registry.register('clock', {
|
||||||
|
category: 'scene',
|
||||||
name: 'Clock',
|
name: 'Clock',
|
||||||
icon: '🕐',
|
icon: '🕐',
|
||||||
description: 'Analog clock with time display',
|
description: 'Analog clock with time display',
|
||||||
@@ -393,6 +397,7 @@ export function registerClockWidget(registry, dependencies) {
|
|||||||
*/
|
*/
|
||||||
export function registerLocationWidget(registry, dependencies) {
|
export function registerLocationWidget(registry, dependencies) {
|
||||||
registry.register('location', {
|
registry.register('location', {
|
||||||
|
category: 'scene',
|
||||||
name: 'Location',
|
name: 'Location',
|
||||||
icon: '📍',
|
icon: '📍',
|
||||||
description: 'Map with location display',
|
description: 'Map with location display',
|
||||||
|
|||||||
@@ -1179,6 +1179,13 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
/* Unified widget styling - consistent look for all widgets */
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-left: 3px solid var(--rpg-highlight);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rpg-widget > * {
|
.rpg-widget > * {
|
||||||
|
|||||||
Reference in New Issue
Block a user