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>
|
||||
`).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 = `
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
@@ -164,13 +169,14 @@ export function registerUserAttributesWidget(registry, dependencies) {
|
||||
const statsGrid = container.querySelector('.rpg-classic-stats-grid');
|
||||
if (!statsGrid) return;
|
||||
|
||||
// Compact single-column layout for narrow widgets
|
||||
if (newW < 2) {
|
||||
statsGrid.style.gridTemplateColumns = '1fr';
|
||||
} else {
|
||||
// 2-column grid for wider widgets
|
||||
statsGrid.style.gridTemplateColumns = 'repeat(2, 1fr)';
|
||||
}
|
||||
// Count visible attributes from DOM
|
||||
const attrCount = statsGrid.querySelectorAll('.rpg-classic-stat').length;
|
||||
|
||||
// Recalculate optimal columns based on new width
|
||||
const optimalCols = calculateOptimalColumns(attrCount, newW);
|
||||
|
||||
// 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 visibleAttrCount = filterList?.length || globallyEnabledCount;
|
||||
|
||||
// Each attribute needs ~0.35 rows in 2-column grid
|
||||
// For 6 attrs: 3 rows (0.5 row padding = 3.5 total)
|
||||
const optimalHeight = Math.ceil((visibleAttrCount / 2) * 0.7 + 0.5);
|
||||
// Determine optimal width and columns based on attribute count
|
||||
// For 9 attributes: prefer 3 columns (3×3 grid)
|
||||
// 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 {
|
||||
w: 2, // Prefer 2-column grid layout
|
||||
w: optimalWidth,
|
||||
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
|
||||
* @private
|
||||
|
||||
Reference in New Issue
Block a user