From 8dc07a938a685113411d1a0e0dfb810cc274d4b7 Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Thu, 6 Nov 2025 20:42:57 +1100 Subject: [PATCH] feat: implement responsive dashboard layout with column-aware widget sizing **Status Tab Layout Changes:** - User Info widget: 1x2 vertical (left column) instead of 2x1 horizontal - User Stats widget: scales from 1x3 (narrow) to 2x3 (wide) - User Mood widget: 1x1 positioned below User Info - User Attributes widget: scales from 2x4 (narrow) to 3x4 (wide), full width **Technical Changes:** - Update widget definitions to use column-aware defaultSize() functions - userInfoWidget: Returns 1x2 for desktop, 1x1 for mobile - userStatsWidget: Returns 1x3 for 2 cols, 2x3 for 3+ cols - userAttributesWidget: Returns 2x4 for 2 cols, 3x4 for 3+ cols - Remove autoLayout from resetLayout() to preserve default positions - Add resetWidgetSizesToDefault() to apply column-aware sizes - Update CSS for 1x1 compact avatar (round) and 1x2 wide avatar layouts **User Info Widget Improvements:** - 1x2 layout: Horizontal split with name left, level right over avatar - 1x1 layout: Round avatar with bottom nameplate (flush positioning) - Transparent glass-style backgrounds for better avatar visibility - Proper aspect-ratio for circular avatar in compact mode **Result:** - Widgets scale intelligently based on panel width (2-4 columns) - Desktop users get larger, more spacious layouts - Mobile/narrow screens get efficient vertical stacking - Reset Layout respects custom positions while applying responsive sizes - Window resize triggers autoLayout via ResizeObserver for reflow --- src/systems/dashboard/dashboardManager.js | 14 +- src/systems/dashboard/defaultLayout.js | 37 ++--- .../dashboard/widgets/userAttributesWidget.js | 16 +- .../dashboard/widgets/userInfoWidget.js | 41 +++-- .../dashboard/widgets/userStatsWidget.js | 14 +- style.css | 151 +++++++++++++++--- 6 files changed, 207 insertions(+), 66 deletions(-) diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index 2527823..9b0720e 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -1562,7 +1562,8 @@ export class DashboardManager { // Skip initial switch in applyDashboardConfig since we'll switch after layout calculations this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true }); - // Reset all widgets to default sizes + // Apply column-aware widget sizes from widget definitions + // This makes widgets scale properly based on screen width (2-4 columns) const allWidgets = []; this.dashboard.tabs.forEach(tab => { if (tab.widgets && tab.widgets.length > 0) { @@ -1571,13 +1572,10 @@ export class DashboardManager { }); 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) { - console.log(`[DashboardManager] Auto-laying out tab "${tab.name}" (${tab.widgets.length} widgets)`); - this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true }); - } - }); + // Don't call autoLayout - preserve positions from defaultLayout.js + // Widget definitions now have column-aware sizes (defaultSize returns correct size for column count) + // ResizeObserver will handle column changes and trigger autoLayout when screen resizes + console.log('[DashboardManager] Using column-aware sizes from widget definitions, preserving positions from defaultLayout.js'); // Force re-render tabs this.renderTabs(); diff --git a/src/systems/dashboard/defaultLayout.js b/src/systems/dashboard/defaultLayout.js index a0f42ae..b53b277 100644 --- a/src/systems/dashboard/defaultLayout.js +++ b/src/systems/dashboard/defaultLayout.js @@ -39,45 +39,46 @@ export function generateDefaultDashboard() { icon: 'fa-solid fa-user', order: 0, widgets: [ - // Row 0: User Info (left) + User Mood (top right in 3-col) + // Row 0-1: User Info (left column, vertical) { id: 'widget-userinfo', type: 'userInfo', x: 0, y: 0, - w: 2, - h: 1, - config: {} - }, - { - id: 'widget-usermood', - type: 'userMood', - x: 2, - y: 0, w: 1, - h: 1, + h: 2, config: {} }, - // Row 1-2: User Stats (health/energy bars) + // Row 0-2: User Stats (right side, tall, 2 cols wide) { id: 'widget-userstats', type: 'userStats', - x: 0, - y: 1, + x: 1, + y: 0, w: 2, - h: 2, + h: 3, config: { statBarGradient: true } }, - // Row 3-4: User Attributes + // Row 2: User Mood (below user info, left column) + { + id: 'widget-usermood', + type: 'userMood', + x: 0, + y: 2, + w: 1, + h: 1, + config: {} + }, + // Row 3-6: User Attributes (full width below everything, 3 cols wide) { id: 'widget-userattributes', type: 'userAttributes', x: 0, y: 3, - w: 2, - h: 2, + w: 3, + h: 4, config: {} } ] diff --git a/src/systems/dashboard/widgets/userAttributesWidget.js b/src/systems/dashboard/widgets/userAttributesWidget.js index a1babd4..a94ad6b 100644 --- a/src/systems/dashboard/widgets/userAttributesWidget.js +++ b/src/systems/dashboard/widgets/userAttributesWidget.js @@ -35,8 +35,20 @@ export function registerUserAttributesWidget(registry, dependencies) { description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)', category: 'user', minSize: { w: 2, h: 2 }, - defaultSize: { w: 2, h: 2 }, - maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion + // Column-aware sizing: full width at each column count + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; // Mobile: 2 cols wide (full), 4 rows tall + } + return { w: 3, h: 4 }; // Desktop: 3 cols wide (full), 4 rows tall + }, + // Column-aware max size: same as default + maxAutoSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; + } + return { w: 3, h: 4 }; + }, requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userInfoWidget.js b/src/systems/dashboard/widgets/userInfoWidget.js index d93dbf4..08de019 100644 --- a/src/systems/dashboard/widgets/userInfoWidget.js +++ b/src/systems/dashboard/widgets/userInfoWidget.js @@ -38,19 +38,19 @@ export function registerUserInfoWidget(registry, dependencies) { description: 'User avatar, name, and level display', category: 'user', minSize: { w: 1, h: 1 }, - // Column-aware default size: start at 2x1 in desktop so mood doesn't block expansion + // Column-aware default size: vertical 1x2 with mood below defaultSize: (columns) => { if (columns <= 2) { - return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout + return { w: 1, h: 1 }; // Mobile: 1x1, compact } - return { w: 2, h: 1 }; // Desktop: 2x1 from the start + return { w: 1, h: 2 }; // Desktop: 1x2 vertical, mood sits below }, - // Column-aware max size: same as defaultSize to prevent further expansion + // Column-aware max size: same as defaultSize to prevent expansion maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout + return { w: 1, h: 1 }; // Mobile: 1x1, compact } - return { w: 2, h: 1 }; // Desktop: 2x1, mood sits in top-right + return { w: 1, h: 2 }; // Desktop: 1x2 vertical, mood below at y:2 }, requiresSchema: false, @@ -89,15 +89,22 @@ export function registerUserInfoWidget(registry, dependencies) { const html = `
- + ` : ''}
`; @@ -155,11 +162,15 @@ export function registerUserInfoWidget(registry, dependencies) { const infoContainer = container.querySelector('.rpg-user-info-container'); if (!infoContainer) return; - // Apply compact mode class at narrow widths for smaller text - if (newW < 3) { - infoContainer.classList.add('rpg-user-info-compact'); - } else { + // Apply layout classes based on widget width + if (newW >= 2) { + // Wide layout (2x1+): Horizontal split with name left, level right + infoContainer.classList.add('rpg-user-info-wide'); infoContainer.classList.remove('rpg-user-info-compact'); + } else { + // Compact layout (1x1): Round avatar with flush text overlays + infoContainer.classList.add('rpg-user-info-compact'); + infoContainer.classList.remove('rpg-user-info-wide'); } } }); diff --git a/src/systems/dashboard/widgets/userStatsWidget.js b/src/systems/dashboard/widgets/userStatsWidget.js index f668596..78b1ac1 100644 --- a/src/systems/dashboard/widgets/userStatsWidget.js +++ b/src/systems/dashboard/widgets/userStatsWidget.js @@ -33,13 +33,19 @@ export function registerUserStatsWidget(registry, dependencies) { description: 'Health, energy, satiety bars', category: 'user', minSize: { w: 1, h: 2 }, - defaultSize: { w: 2, h: 2 }, - // Column-aware max size: full width in 3-4 col for horizontal spread + // Column-aware sizing: narrow and tall at 2 cols, wider at 3+ cols + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 1, h: 3 }; // Mobile: 1 col wide, 3 rows tall + } + return { w: 2, h: 3 }; // Desktop: 2 cols wide, 3 rows tall + }, + // Column-aware max size: same as default to prevent expansion maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 2 }; // Mobile: use full 2-col width + return { w: 1, h: 3 }; // Mobile: 1x3 } - return { w: 3, h: 3 }; // Desktop: span 3 columns horizontally + return { w: 2, h: 3 }; // Desktop: 2x3 }, requiresSchema: false, diff --git a/style.css b/style.css index 36f7bc6..e3bd1c8 100644 --- a/style.css +++ b/style.css @@ -1938,7 +1938,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* User info widget - avatar background with text overlay */ .rpg-user-info-container { display: flex; - align-items: center; + align-items: flex-end; justify-content: center; height: 100%; width: 100%; @@ -1963,20 +1963,71 @@ body:has(.rpg-panel.rpg-position-left) #sheld { z-index: 1; } -/* Text container with backdrop */ -.rpg-user-info-text { - display: flex; - flex-direction: column; - gap: 0.2rem; - align-items: center; - text-align: center; +/* Round avatar image (used in 1x1 compact mode, hidden by default) */ +.rpg-user-avatar-img { + display: none; + position: absolute; + width: 75%; + height: 75%; + object-fit: cover; + border-radius: 50%; + z-index: 0; +} + +/* Name and level containers - base styles */ +.rpg-user-name-container, +.rpg-user-level-container { position: relative; z-index: 2; - padding: 0.5rem 0.75rem; - background: rgba(0, 0, 0, 0.5); + padding: 0.3rem 0.6rem; + background: rgba(0, 0, 0, 0.15); backdrop-filter: blur(4px); border-radius: 0.375rem; border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +/* WIDE LAYOUT (2x1+): Horizontal split over avatar background */ +.rpg-user-info-wide .rpg-user-avatar-img { + display: none; +} + +.rpg-user-info-wide .rpg-user-info-container::before { + display: block; +} + +.rpg-user-info-wide .rpg-user-name-container { + position: absolute; + left: 0.5rem; + top: 50%; + transform: translateY(-50%); + max-width: 45%; + padding: 0.2rem 0.4rem; +} + +.rpg-user-info-wide .rpg-user-level-container { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + max-width: 35%; + padding: 0.2rem 0.4rem; +} + +/* Smaller text for wide layout to prevent overlap */ +.rpg-user-info-wide .rpg-user-name { + font-size: 0.7rem; +} + +.rpg-user-info-wide .rpg-level-label { + font-size: 0.6rem; +} + +.rpg-user-info-wide .rpg-level-value { + font-size: 0.65rem; + padding: 0.1rem 0.3rem; } /* User name */ @@ -2029,27 +2080,89 @@ body:has(.rpg-panel.rpg-position-left) #sheld { background: var(--rpg-bg); } -/* Compact mode for narrow widths (< 3 grid units) */ +/* COMPACT LAYOUT (1x1): Round avatar with bottom nameplate */ .rpg-user-info-compact { - padding: 0.25rem !important; + align-items: center; + justify-content: center; + padding: 0 !important; } -.rpg-user-info-compact .rpg-user-info-text { - gap: 0.15rem !important; - padding: 0.35rem 0.5rem !important; +/* Hide background image and overlay in 1x1 mode */ +.rpg-user-info-compact { + background-image: none !important; +} + +.rpg-user-info-compact::before { + display: none; +} + +/* Show round avatar image - proper circle */ +.rpg-user-info-compact .rpg-user-avatar-img { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + object-position: center; + border-radius: 50%; +} + +/* Name container at bottom - flush, no top/bottom padding on widget */ +.rpg-user-info-compact .rpg-user-name-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 0.2rem 0.3rem; + border-radius: 0; } .rpg-user-info-compact .rpg-user-name { - font-size: 0.75rem !important; + font-size: 0.65rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Level container also at bottom, positioned next to name */ +.rpg-user-info-compact .rpg-user-level-container { + position: absolute; + bottom: 0; + right: 0; + padding: 0.2rem 0.3rem; + border-radius: 0; + background: transparent; + border: none; + backdrop-filter: none; +} + +.rpg-user-info-compact .rpg-user-level { + display: flex; + align-items: center; + gap: 0.2rem; } .rpg-user-info-compact .rpg-level-label { - font-size: 0.65rem !important; + font-size: 0.55rem; + font-weight: 600; + color: var(--rpg-text); + opacity: 0.7; } .rpg-user-info-compact .rpg-level-value { - font-size: 0.75rem !important; - padding: 0.1rem 0.3rem !important; + font-size: 0.65rem; + font-weight: 700; + color: var(--rpg-highlight); + padding: 0.1rem 0.3rem; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.25rem; + min-width: 1.2rem; + text-align: center; } /* Stat bars - rem for text, vh for bar height */