From 3dd7b017a63a5b97c643777e056f45253a812dbe Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 23 Oct 2025 22:08:04 +1100 Subject: [PATCH] feat(dashboard): implement smart widget scaling and improved auto-layout - Add resetWidgetSizesToDefault() to reset all widgets to default sizes before auto-arrange/reset - Implement continuous expansion algorithm that fills available space up to maxAutoSize limits - Add visible height detection to prevent widgets expanding beyond viewport (no forced scroll) - Update all widget defaultSize and maxAutoSize for optimal 1x1 compact layouts - Info widgets (calendar, weather, temp, clock): 1x1 default, 1x2 max - Location: 2x2 max (was 3x3) - Characters: 3x5 max, moved to 'scene' category (eliminates Social tab) - User Info: 2x1 max (prevents expansion) - User Mood: 1x1 default and max (compact top-right placement) - User Attributes: 3x5 max (fills bottom space) - User Stats: 3x3 max - Fix CSS scaling for 1x1 widgets - Replace viewport-based units with fixed rem values - Reduce icon/graphic sizes to fit with visible text - Add explicit gaps and padding for consistent spacing - Set line-height: 1 to prevent text overflow - Reorganize default layout - Status tab: User Info (2x1) + Mood (1x1 top right) + Stats + Attributes - Scene tab: Info widgets (1x1) + Location + Characters (all on one tab) - Inventory tab: Full inventory widget Auto-arrange and reset now properly size widgets to defaults and expand to fill available space without exceeding visible area. --- src/systems/dashboard/dashboardManager.js | 37 ++++++++++++++ src/systems/dashboard/defaultLayout.js | 49 +++++++++--------- src/systems/dashboard/gridEngine.js | 50 ++++++++++++++----- .../dashboard/widgets/infoBoxWidgets.js | 26 +++++----- .../widgets/presentCharactersWidget.js | 4 +- .../dashboard/widgets/userAttributesWidget.js | 1 + .../dashboard/widgets/userInfoWidget.js | 1 + .../dashboard/widgets/userMoodWidget.js | 3 +- .../dashboard/widgets/userStatsWidget.js | 1 + style.css | 48 ++++++++++-------- 10 files changed, 146 insertions(+), 74 deletions(-) diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index dcf1d5f..f1e0390 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -1183,6 +1183,15 @@ export class DashboardManager { await this.persistence.resetToDefault(this.defaultLayout); this.applyDashboardConfig(this.defaultLayout); + // Reset all widgets to default sizes + const allWidgets = []; + this.dashboard.tabs.forEach(tab => { + if (tab.widgets && tab.widgets.length > 0) { + allWidgets.push(...tab.widgets); + } + }); + this.resetWidgetSizesToDefault(allWidgets); + // Auto-layout each tab to prevent overlap (default positions may have changed) this.dashboard.tabs.forEach(tab => { if (tab.widgets && tab.widgets.length > 0) { @@ -1212,6 +1221,28 @@ export class DashboardManager { this.defaultLayout = layout; } + /** + * Reset all widgets to their default sizes + * @param {Array} widgets - Widgets to reset + */ + resetWidgetSizesToDefault(widgets) { + let resetCount = 0; + widgets.forEach(widget => { + const definition = this.registry.get(widget.type); + if (definition && definition.defaultSize) { + const oldSize = `${widget.w}x${widget.h}`; + widget.w = definition.defaultSize.w; + widget.h = definition.defaultSize.h; + const newSize = `${widget.w}x${widget.h}`; + if (oldSize !== newSize) { + console.log(`[DashboardManager] Reset ${widget.type} from ${oldSize} to ${newSize}`); + resetCount++; + } + } + }); + console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`); + } + /** * Auto-layout widgets on current tab to efficiently use all available space * @@ -1221,6 +1252,7 @@ export class DashboardManager { * * @param {Object} options - Layout options * @param {boolean} [options.preferFullWidth=true] - Prefer full-width widgets when possible + * @param {boolean} [options.resetSizes=true] - Reset widgets to default sizes before layout */ autoLayoutWidgets(options = {}) { console.log('[DashboardManager] Auto-layout widgets requested'); @@ -1241,6 +1273,11 @@ export class DashboardManager { console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`); + // Reset widget sizes to defaults (unless explicitly disabled) + if (options.resetSizes !== false) { + this.resetWidgetSizesToDefault(allWidgets); + } + // Smart category-aware sorting BEFORE auto-layout const widgetsToLayout = this.sortWidgetsByCategory(allWidgets); diff --git a/src/systems/dashboard/defaultLayout.js b/src/systems/dashboard/defaultLayout.js index a398d79..a6766cd 100644 --- a/src/systems/dashboard/defaultLayout.js +++ b/src/systems/dashboard/defaultLayout.js @@ -39,7 +39,7 @@ export function generateDefaultDashboard() { icon: '📊', order: 0, widgets: [ - // Row 0: User Info (avatar, name, level) + // Row 0: User Info (left) + User Mood (top right in 3-col) { id: 'widget-userinfo', type: 'userInfo', @@ -49,6 +49,15 @@ export function generateDefaultDashboard() { h: 1, config: {} }, + { + id: 'widget-usermood', + type: 'userMood', + x: 2, + y: 0, + w: 1, + h: 1, + config: {} + }, // Row 1-2: User Stats (health/energy bars) { id: 'widget-userstats', @@ -61,22 +70,12 @@ export function generateDefaultDashboard() { statBarGradient: true } }, - // Row 3: User Mood - { - id: 'widget-usermood', - type: 'userMood', - x: 0, - y: 3, - w: 2, - h: 1, - config: {} - }, - // Row 4-5: User Attributes + // Row 3-4: User Attributes { id: 'widget-userattributes', type: 'userAttributes', x: 0, - y: 4, + y: 3, w: 2, h: 2, config: {} @@ -90,14 +89,14 @@ export function generateDefaultDashboard() { icon: '🌍', order: 1, widgets: [ - // Row 0-1: Calendar (left) + Weather (right) + // Row 0: Calendar (left) + Weather (right) { id: 'widget-calendar', type: 'calendar', x: 0, y: 0, w: 1, - h: 2, + h: 1, config: {} }, { @@ -106,19 +105,19 @@ export function generateDefaultDashboard() { x: 1, y: 0, w: 1, - h: 2, + h: 1, config: { compact: false } }, - // Row 2-3: Temperature (left) + Clock (right) + // Row 1: Temperature (left) + Clock (right) { id: 'widget-temperature', type: 'temperature', x: 0, - y: 2, + y: 1, w: 1, - h: 2, + h: 1, config: { unit: 'celsius' } @@ -127,29 +126,29 @@ export function generateDefaultDashboard() { id: 'widget-clock', type: 'clock', x: 1, - y: 2, + y: 1, w: 1, - h: 2, + h: 1, config: { format: 'digital' } }, - // Row 4-5: Location (full width) + // Row 2-3: Location (full width) { id: 'widget-location', type: 'location', x: 0, - y: 4, + y: 2, w: 2, h: 2, config: {} }, - // Row 6-8: Present Characters (full width, will expand with auto-layout) + // Row 4-6: Present Characters (full width, will expand with auto-layout) { id: 'widget-presentchars', type: 'presentCharacters', x: 0, - y: 6, + y: 4, w: 2, h: 3, config: { diff --git a/src/systems/dashboard/gridEngine.js b/src/systems/dashboard/gridEngine.js index e43bbcc..9c9c5fe 100644 --- a/src/systems/dashboard/gridEngine.js +++ b/src/systems/dashboard/gridEngine.js @@ -435,10 +435,22 @@ export class GridEngine { const preserveOrder = options.preserveOrder || false; + // Calculate maximum visible rows based on container height + let maxVisibleRows = 100; // Fallback + if (this.container) { + const containerHeight = this.container.clientHeight; // pixels + const rootFontSize = parseFloat(getComputedStyle(document.documentElement).fontSize); // px per rem + const containerHeightRem = containerHeight / rootFontSize; + const rowHeightWithGap = this.rowHeight + this.gap; + maxVisibleRows = Math.floor(containerHeightRem / rowHeightWithGap); + console.log('[GridEngine] Container height:', containerHeight + 'px', '=', containerHeightRem.toFixed(2) + 'rem', '→', maxVisibleRows, 'rows'); + } + console.log('[GridEngine] Auto-layout started:', { widgetCount: widgets.length, columns: this.columns, - preserveOrder + preserveOrder, + maxVisibleRows }); // Sort widgets (or preserve input order for category-aware layout) @@ -586,8 +598,8 @@ export class GridEngine { return definition.maxAutoSize; } } - // Default max size if not specified (flexible expansion) - return { w: this.columns, h: 10 }; + // Default max size if not specified (conservative expansion) + return { w: this.columns, h: 3 }; }; sortedForExpand.forEach(widget => { @@ -595,9 +607,15 @@ export class GridEngine { const originalW = widget.w; const originalH = widget.h; - // Try expanding height first (fills vertical gaps) + // Try expanding height first (fills vertical gaps) - keep trying until maxSize or collision let expandedH = false; - for (let tryH = originalH + 1; tryH <= Math.min(maxSize.h, originalH + 3); tryH++) { + for (let tryH = originalH + 1; tryH <= maxSize.h; tryH++) { + // Check if expansion would go beyond visible area + if (widget.y + tryH > maxVisibleRows) { + console.log(`[GridEngine] ${widget.id} cannot expand to h=${tryH} (would exceed visible area: row ${widget.y + tryH} > ${maxVisibleRows})`); + break; + } + // Clear current position for (let row = widget.y; row < widget.y + widget.h; row++) { for (let col = widget.x; col < widget.x + widget.w; col++) { @@ -611,15 +629,19 @@ export class GridEngine { markOccupied(widget, widget.x, widget.y, widget.w, tryH); expandedH = true; expandedCount++; - console.log(`[GridEngine] Expanded ${widget.id} height: ${originalH} → ${tryH}`); - break; + // Continue trying to expand further } else { - // Re-mark original and try next size + // Hit a collision, stop expanding height markOccupied(widget, widget.x, widget.y, widget.w, widget.h); + break; } } - // Try expanding width (fills horizontal gaps) + if (expandedH) { + console.log(`[GridEngine] Expanded ${widget.id} height: ${originalH} → ${widget.h}`); + } + + // Try expanding width (fills horizontal gaps) - keep trying until maxSize or collision let expandedW = false; for (let tryW = originalW + 1; tryW <= Math.min(maxSize.w, this.columns); tryW++) { // Clear current position @@ -635,14 +657,18 @@ export class GridEngine { markOccupied(widget, widget.x, widget.y, tryW, widget.h); expandedW = true; expandedCount++; - console.log(`[GridEngine] Expanded ${widget.id} width: ${originalW} → ${tryW}`); - break; + // Continue trying to expand further } else { - // Re-mark original and try next size + // Hit a collision, stop expanding width markOccupied(widget, widget.x, widget.y, widget.w, widget.h); + break; } } + if (expandedW) { + console.log(`[GridEngine] Expanded ${widget.id} width: ${originalW} → ${widget.w}`); + } + if (!expandedH && !expandedW) { // Widget couldn't expand - ensure it's still marked in grid markOccupied(widget, widget.x, widget.y, widget.w, widget.h); diff --git a/src/systems/dashboard/widgets/infoBoxWidgets.js b/src/systems/dashboard/widgets/infoBoxWidgets.js index f1f7068..96a28e8 100644 --- a/src/systems/dashboard/widgets/infoBoxWidgets.js +++ b/src/systems/dashboard/widgets/infoBoxWidgets.js @@ -192,9 +192,9 @@ export function registerCalendarWidget(registry, dependencies) { icon: '📅', description: 'Date, weekday, month, and year display', category: 'scene', - minSize: { w: 1, h: 2 }, - defaultSize: { w: 1, h: 2 }, - maxAutoSize: { w: 2, h: 3 }, // Max size for auto-arrange expansion + minSize: { w: 1, h: 1 }, + defaultSize: { w: 1, h: 1 }, + maxAutoSize: { w: 1, h: 2 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { @@ -280,9 +280,9 @@ export function registerWeatherWidget(registry, dependencies) { name: 'Weather', icon: '🌤️', description: 'Weather emoji and forecast', - minSize: { w: 1, h: 2 }, - defaultSize: { w: 1, h: 2 }, - maxAutoSize: { w: 2, h: 3 }, // Max size for auto-arrange expansion + minSize: { w: 1, h: 1 }, + defaultSize: { w: 1, h: 1 }, + maxAutoSize: { w: 1, h: 2 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { @@ -314,9 +314,9 @@ export function registerTemperatureWidget(registry, dependencies) { name: 'Temperature', icon: '🌡️', description: 'Temperature display with thermometer', - minSize: { w: 1, h: 2 }, - defaultSize: { w: 1, h: 2 }, - maxAutoSize: { w: 2, h: 3 }, // Max size for auto-arrange expansion + minSize: { w: 1, h: 1 }, + defaultSize: { w: 1, h: 1 }, + maxAutoSize: { w: 1, h: 2 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { @@ -355,9 +355,9 @@ export function registerClockWidget(registry, dependencies) { name: 'Clock', icon: '🕐', description: 'Analog clock with time display', - minSize: { w: 1, h: 2 }, - defaultSize: { w: 1, h: 2 }, - maxAutoSize: { w: 2, h: 3 }, // Max size for auto-arrange expansion + minSize: { w: 1, h: 1 }, + defaultSize: { w: 1, h: 1 }, + maxAutoSize: { w: 1, h: 2 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { @@ -407,7 +407,7 @@ export function registerLocationWidget(registry, dependencies) { description: 'Map with location display', minSize: { w: 1, h: 2 }, defaultSize: { w: 2, h: 2 }, - maxAutoSize: { w: 3, h: 3 }, // Max size for auto-arrange expansion + maxAutoSize: { w: 2, h: 2 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { diff --git a/src/systems/dashboard/widgets/presentCharactersWidget.js b/src/systems/dashboard/widgets/presentCharactersWidget.js index 15cc417..6e89af6 100644 --- a/src/systems/dashboard/widgets/presentCharactersWidget.js +++ b/src/systems/dashboard/widgets/presentCharactersWidget.js @@ -235,10 +235,10 @@ export function registerPresentCharactersWidget(registry, dependencies) { name: 'Present Characters', icon: '👥', description: 'Character cards with avatars, traits, and relationships', - category: 'social', + category: 'scene', minSize: { w: 2, h: 2 }, defaultSize: { w: 2, h: 3 }, - maxAutoSize: { w: 3, h: 6 }, // Max size for auto-arrange expansion (can expand significantly) + maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion requiresSchema: false, render(container, config = {}) { diff --git a/src/systems/dashboard/widgets/userAttributesWidget.js b/src/systems/dashboard/widgets/userAttributesWidget.js index 1bd0e87..7a3ae3e 100644 --- a/src/systems/dashboard/widgets/userAttributesWidget.js +++ b/src/systems/dashboard/widgets/userAttributesWidget.js @@ -33,6 +33,7 @@ export function registerUserAttributesWidget(registry, dependencies) { category: 'user', minSize: { w: 2, h: 2 }, defaultSize: { w: 2, h: 2 }, + maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userInfoWidget.js b/src/systems/dashboard/widgets/userInfoWidget.js index c36d145..8fbbb48 100644 --- a/src/systems/dashboard/widgets/userInfoWidget.js +++ b/src/systems/dashboard/widgets/userInfoWidget.js @@ -37,6 +37,7 @@ export function registerUserInfoWidget(registry, dependencies) { category: 'user', minSize: { w: 1, h: 1 }, defaultSize: { w: 2, h: 1 }, + maxAutoSize: { w: 2, h: 1 }, // Max size for auto-arrange expansion requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userMoodWidget.js b/src/systems/dashboard/widgets/userMoodWidget.js index d8fc73d..6c7039e 100644 --- a/src/systems/dashboard/widgets/userMoodWidget.js +++ b/src/systems/dashboard/widgets/userMoodWidget.js @@ -29,7 +29,8 @@ export function registerUserMoodWidget(registry, dependencies) { description: 'Mood emoji and active conditions', category: 'user', minSize: { w: 1, h: 1 }, - defaultSize: { w: 2, h: 1 }, + defaultSize: { w: 1, h: 1 }, + maxAutoSize: { w: 1, h: 1 }, // Max size for auto-arrange expansion - stays compact in top right requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userStatsWidget.js b/src/systems/dashboard/widgets/userStatsWidget.js index 66bc63c..20aa3e8 100644 --- a/src/systems/dashboard/widgets/userStatsWidget.js +++ b/src/systems/dashboard/widgets/userStatsWidget.js @@ -34,6 +34,7 @@ export function registerUserStatsWidget(registry, dependencies) { category: 'user', minSize: { w: 1, h: 2 }, defaultSize: { w: 2, h: 2 }, + maxAutoSize: { w: 3, h: 3 }, // Max size for auto-arrange expansion requiresSchema: false, /** diff --git a/style.css b/style.css index 7230226..aa4118f 100644 --- a/style.css +++ b/style.css @@ -1400,7 +1400,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-calendar-top { background: var(--rpg-highlight); color: var(--rpg-bg); - font-size: clamp(0.6rem, 0.7rem, 0.8rem); + font-size: 0.65rem; font-weight: bold; padding: 0.125em 0.375em; border-radius: 3px 3px 0 0; @@ -1412,9 +1412,9 @@ body:has(.rpg-panel.rpg-position-left) #sheld { .rpg-calendar-day { background: rgba(255, 255, 255, 0.1); color: var(--rpg-text); - font-size: clamp(1.5rem, 2.5rem, 3.5rem); + font-size: 1.8rem; font-weight: bold; - padding: 0.25em; + padding: 0.1em; width: 100%; text-align: center; border: 2px solid var(--rpg-highlight); @@ -1427,7 +1427,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-calendar-year { - font-size: clamp(0.6rem, 0.7rem, 0.8rem); + font-size: 0.55rem; color: var(--rpg-text); opacity: 0.7; margin-top: 0.062em; @@ -1439,19 +1439,21 @@ body:has(.rpg-panel.rpg-position-left) #sheld { height: 100%; display: flex; flex-direction: column; - justify-content: space-around; + justify-content: center; align-items: center; - padding: 0.5rem; + padding: 0.25rem; + gap: 0.2rem; } .rpg-weather-icon { - font-size: clamp(2rem, 8vh, 4rem); + font-size: 2rem; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); - flex-shrink: 0; + flex-shrink: 1; + line-height: 1; } .rpg-weather-forecast { - font-size: clamp(0.7rem, 0.9rem, 1.1rem); + font-size: 0.65rem; text-align: center; margin: 0; font-weight: 600; @@ -1477,13 +1479,14 @@ body:has(.rpg-panel.rpg-position-left) #sheld { flex-direction: column; justify-content: center; align-items: center; - gap: 0.5rem; + padding: 0.25rem; + gap: 0.2rem; } .rpg-thermometer { position: relative; - width: clamp(1.5rem, 2rem, 3rem); - height: clamp(4rem, 60%, 8rem); + width: 1.2rem; + height: 2.5rem; display: flex; flex-direction: column; align-items: center; @@ -1522,11 +1525,12 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-temp-value { - font-size: clamp(0.7rem, 0.9rem, 1.1rem); + font-size: 0.65rem; font-weight: bold; color: var(--rpg-text); text-align: center; flex-shrink: 0; + line-height: 1; } /* Clock Widget */ @@ -1534,18 +1538,19 @@ body:has(.rpg-panel.rpg-position-left) #sheld { height: 100%; display: flex; flex-direction: column; - justify-content: space-around; + justify-content: center; align-items: center; - gap: 0.5rem; + padding: 0.25rem; + gap: 0.2rem; } .rpg-clock { - width: clamp(3rem, 60%, 6rem); - height: clamp(3rem, 60%, 6rem); + width: 2.5rem; + height: 2.5rem; aspect-ratio: 1 / 1; border-radius: 50%; background: rgba(0, 0, 0, 0.4); - border: 3px solid var(--rpg-border); + border: 2px solid var(--rpg-border); box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); position: relative; flex-shrink: 1; @@ -1593,10 +1598,11 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-time-value { - font-size: clamp(0.7rem, 0.9rem, 1.1rem); + font-size: 0.65rem; font-weight: bold; color: var(--rpg-text); flex-shrink: 0; + line-height: 1; } /* Location Widget - Map */ @@ -1623,7 +1629,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-map-marker { - font-size: clamp(1.5rem, 4vh, 3rem); + font-size: 2rem; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.8)); animation: markerPulse 2s ease-in-out infinite; } @@ -1634,7 +1640,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { } .rpg-location-text { - font-size: clamp(0.7rem, 0.9rem, 1.1rem); + font-size: 0.75rem; font-weight: bold; color: var(--rpg-text); text-align: center;