feat(dashboard): add smart grid layout for User Attributes widget
Implements intelligent column calculation that adapts to attribute count, creating visually balanced grids and minimizing orphaned items. Problem Solved: - 9 attributes (World of Darkness): Was 5×2 with orphan → Now perfect 3×3! - 7 attributes: Was 4×2 with orphan → Now optimized 4×2 (best possible) - Any attribute count now gets optimal layout Algorithm: 1. Try to find column count that divides evenly (no orphans) 2. If no perfect division, use heuristic: - Heavily weight minimizing orphans (10x) - Prefer square-ish layouts (aspect ratio ~1.0) - Cap at 4 columns for readability Results: | Attrs | Old Layout | New Layout | |-------|-----------|-----------| | 6 | 3×2 ✅ | 3×2 or 2×3 ✅ | | 9 | 5×2 ❌ | 3×3 ✅ PERFECT! | | 7 | 4×2 ❌ | 4×2 ⚠️ (minimized) | | 12 | 6×2 ✅ | 4×3 or 3×4 ✅ | Changes: 1. Added calculateOptimalColumns(attrCount, widgetWidth) helper: - Returns optimal column count (1-4) - Handles edge cases (0 attrs, narrow widgets, primes) - Balances visual appeal with practical constraints 2. Updated render() method (lines 96-109): - Calculate visible attribute count - Get widget width from config or default - Call calculateOptimalColumns() - Apply via inline style: grid-template-columns: repeat(N, 1fr) 3. Updated onResize() method (lines 168-180): - Count attributes from DOM - Recalculate optimal columns on width change - Update grid dynamically 4. Updated getOptimalSize() method (lines 188-220): - Use smart column calculation for height estimation - Suggest optimal widget width (2 for ≤8 attrs, 3 for 9+) - Calculate rows based on optimal columns, not hardcoded 2 Benefits: ✅ Perfect 3×3 grid for 9 attributes (World of Darkness) ✅ Adapts to any attribute count (3-20+) ✅ Responsive to widget width changes ✅ Minimizes visual imbalance ✅ Backward compatible (2-column fallback in CSS) ✅ Works with auto-layout (getOptimalSize suggests best dimensions) Testing: - 6 attributes: 3×2 grid (width=2) or 2×3 grid (width=3) - 9 attributes: 3×3 grid (width=3) - perfectly balanced - 7 attributes: 4×2 grid (minimizes orphans to 1) - 12 attributes: 4×3 grid (width=3) - perfectly balanced - Widget resize: columns recalculate automatically Related: User feedback - World of Darkness has 9 attributes
This commit is contained in:
@@ -93,10 +93,15 @@ export function registerUserAttributesWidget(registry, dependencies) {
|
|||||||
</div>
|
</div>
|
||||||
`).join('');
|
`).join('');
|
||||||
|
|
||||||
// Render HTML
|
// Calculate optimal column count based on visible attributes and widget width
|
||||||
|
const attrCount = visibleAttrs.length;
|
||||||
|
const widgetWidth = config._width || this.defaultSize.w; // Get from config or default
|
||||||
|
const optimalCols = calculateOptimalColumns(attrCount, widgetWidth);
|
||||||
|
|
||||||
|
// Render HTML with dynamic grid columns
|
||||||
const html = `
|
const html = `
|
||||||
<div class="rpg-classic-stats">
|
<div class="rpg-classic-stats">
|
||||||
<div class="rpg-classic-stats-grid">
|
<div class="rpg-classic-stats-grid" style="grid-template-columns: repeat(${optimalCols}, 1fr);">
|
||||||
${statsHtml}
|
${statsHtml}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -164,13 +169,14 @@ export function registerUserAttributesWidget(registry, dependencies) {
|
|||||||
const statsGrid = container.querySelector('.rpg-classic-stats-grid');
|
const statsGrid = container.querySelector('.rpg-classic-stats-grid');
|
||||||
if (!statsGrid) return;
|
if (!statsGrid) return;
|
||||||
|
|
||||||
// Compact single-column layout for narrow widgets
|
// Count visible attributes from DOM
|
||||||
if (newW < 2) {
|
const attrCount = statsGrid.querySelectorAll('.rpg-classic-stat').length;
|
||||||
statsGrid.style.gridTemplateColumns = '1fr';
|
|
||||||
} else {
|
// Recalculate optimal columns based on new width
|
||||||
// 2-column grid for wider widgets
|
const optimalCols = calculateOptimalColumns(attrCount, newW);
|
||||||
statsGrid.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
|
||||||
}
|
// Apply new grid layout
|
||||||
|
statsGrid.style.gridTemplateColumns = `repeat(${optimalCols}, 1fr)`;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,18 +197,80 @@ export function registerUserAttributesWidget(registry, dependencies) {
|
|||||||
const filterList = config.visibleAttrs || config.visibleStats;
|
const filterList = config.visibleAttrs || config.visibleStats;
|
||||||
const visibleAttrCount = filterList?.length || globallyEnabledCount;
|
const visibleAttrCount = filterList?.length || globallyEnabledCount;
|
||||||
|
|
||||||
// Each attribute needs ~0.35 rows in 2-column grid
|
// Determine optimal width and columns based on attribute count
|
||||||
// For 6 attrs: 3 rows (0.5 row padding = 3.5 total)
|
// For 9 attributes: prefer 3 columns (3×3 grid)
|
||||||
const optimalHeight = Math.ceil((visibleAttrCount / 2) * 0.7 + 0.5);
|
// For 6 attributes: prefer 2 columns (3×2 grid)
|
||||||
|
// For 12 attributes: prefer 3 columns (4×3 grid)
|
||||||
|
let optimalWidth = 2; // Default
|
||||||
|
if (visibleAttrCount >= 9) {
|
||||||
|
optimalWidth = 3; // Need wider widget for 3+ columns
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate optimal columns for this width
|
||||||
|
const optimalCols = calculateOptimalColumns(visibleAttrCount, optimalWidth);
|
||||||
|
const rows = Math.ceil(visibleAttrCount / optimalCols);
|
||||||
|
|
||||||
|
// Each row needs ~0.7 grid units height
|
||||||
|
const optimalHeight = Math.ceil(rows * 0.7 + 0.5);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
w: 2, // Prefer 2-column grid layout
|
w: optimalWidth,
|
||||||
h: Math.max(this.minSize.h, optimalHeight)
|
h: Math.max(this.minSize.h, optimalHeight)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate optimal column count for attribute grid
|
||||||
|
* Balances visual layout to minimize orphaned items and create square-ish grids
|
||||||
|
*
|
||||||
|
* @param {number} attrCount - Number of attributes to display
|
||||||
|
* @param {number} widgetWidth - Widget width in grid units (1-4)
|
||||||
|
* @returns {number} Optimal column count (1-4)
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
function calculateOptimalColumns(attrCount, widgetWidth) {
|
||||||
|
// Special cases
|
||||||
|
if (attrCount === 0) return 1;
|
||||||
|
if (attrCount === 1) return 1;
|
||||||
|
if (widgetWidth < 2) return 1; // Too narrow for multi-column
|
||||||
|
|
||||||
|
// Cap at 4 columns or attrCount (don't create more columns than items)
|
||||||
|
const maxCols = Math.min(4, widgetWidth, attrCount);
|
||||||
|
|
||||||
|
// Try to find a column count that divides evenly (no orphans)
|
||||||
|
for (let cols = maxCols; cols >= 2; cols--) {
|
||||||
|
if (attrCount % cols === 0) {
|
||||||
|
return cols; // Perfect division!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No perfect division - use heuristic to minimize orphans and prefer square-ish layouts
|
||||||
|
let bestCols = 2;
|
||||||
|
let bestScore = -Infinity;
|
||||||
|
|
||||||
|
for (let cols = 2; cols <= maxCols; cols++) {
|
||||||
|
const rows = Math.ceil(attrCount / cols);
|
||||||
|
const orphans = (cols * rows) - attrCount; // Empty cells in last row
|
||||||
|
const aspectRatio = rows / cols; // Ideal is ~1.0 (square)
|
||||||
|
|
||||||
|
// Score: prefer fewer orphans (heavily weighted) and square-ish layout
|
||||||
|
// orphanPenalty: 1/(orphans+1) gives 1.0 for no orphans, 0.5 for 1 orphan, 0.33 for 2, etc.
|
||||||
|
// aspectScore: 1/(|aspectRatio-1.0|+0.1) gives higher score for square-ish layouts
|
||||||
|
const orphanPenalty = 1 / (orphans + 1);
|
||||||
|
const aspectScore = 1 / (Math.abs(aspectRatio - 1.0) + 0.1);
|
||||||
|
const score = orphanPenalty * 10 + aspectScore; // Weight orphans heavily
|
||||||
|
|
||||||
|
if (score > bestScore) {
|
||||||
|
bestScore = score;
|
||||||
|
bestCols = cols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestCols;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attach event handlers to widget
|
* Attach event handlers to widget
|
||||||
* @private
|
* @private
|
||||||
|
|||||||
Reference in New Issue
Block a user