724281b6bb
Integrates the User Attributes Widget with upstream's new RPG attributes
customization system, enabling full attribute customization (add/remove/rename)
with bi-directional sync between widget and Tracker Editor.
Changes to userAttributesWidget.js:
1. render() method (lines 44-106):
- Read from trackerConfig.userStats.rpgAttributes (not hardcoded)
- Filter to enabled attributes only
- Use custom attr.name for labels (e.g., "STRENGTH" vs "STR")
- Support widget-level visibleAttrs filtering
- Support legacy visibleStats config for backward compat
- Fallback to default 6 attributes if no config
2. getConfig() method (lines 112-143):
- Dynamically generate options from enabled attributes
- Changed visibleStats → visibleAttrs (with legacy support)
- Set default to null (show all enabled attributes)
- Add hint: "To add/remove/rename attributes globally, use Tracker Settings"
3. getOptimalSize() method (lines 179-199):
- Calculate height based on enabled attribute count (not hardcoded 6)
- Respect widget-level visibleAttrs override if specified
- Support legacy visibleStats parameter
4. Widget description updated:
- Header docs: Added customization features, bi-directional sync
- Registry description: "Customizable RPG attributes" instead of "Classic RPG stats"
Changes to dashboardManager.js:
1. shouldWidgetBeRemoved() (lines 1976-1984):
- Add 'userAttributes' removal rule
- Remove if showRPGAttributes === false
- Remove if all attributes disabled
2. detectConfigChanges() (lines 1743-1752):
- Detect when RPG Attributes section re-enabled
- Detect when attributes re-enabled
- Auto-add widget when conditions met
Integration Benefits:
✅ Custom attribute names (e.g., "STRENGTH", "AGILITY", "LUCK")
✅ Add custom attributes (e.g., "LCK", "PER", "APP")
✅ Remove unwanted default attributes
✅ Widget auto-updates when tracker config changes
✅ Widget auto-removed when section/attrs disabled
✅ Widget auto-added when section/attrs re-enabled
✅ Widget-level filtering (show subset of enabled attrs)
✅ Backward compatible with existing dashboards
Testing Required:
- Widget renders with default attributes
- Widget respects custom attribute names
- Widget supports custom attributes (e.g., adding "LCK")
- Widget removed when section disabled
- Widget re-added when section re-enabled
- +/- buttons work with custom attributes
- AI prompts use custom attribute names
Follows pattern from: userStatsWidget.js (lines 51-78)
Related: commit a02be34 (upstream merge)
247 lines
9.5 KiB
JavaScript
247 lines
9.5 KiB
JavaScript
/**
|
||
* User Attributes Widget
|
||
*
|
||
* Displays customizable RPG attribute scores with +/- adjustment buttons.
|
||
* Integrates with Tracker Settings for full attribute customization.
|
||
*
|
||
* Features:
|
||
* - Fully customizable attributes (add/remove/rename via Tracker Settings)
|
||
* - Custom attribute names (e.g., "STRENGTH" instead of "STR", or add "LCK")
|
||
* - Widget-level filtering (show subset of globally enabled attributes)
|
||
* - +/- buttons for quick adjustments (1-20 range)
|
||
* - Responsive 2-column grid layout
|
||
* - Smart sizing: auto-adjusts height based on attribute count
|
||
* - Bi-directional sync with Tracker Editor
|
||
*/
|
||
|
||
import { parseNumber } from '../widgetBase.js';
|
||
|
||
/**
|
||
* Register User Attributes Widget
|
||
* @param {WidgetRegistry} registry - Widget registry instance
|
||
* @param {Object} dependencies - External dependencies
|
||
* @param {Function} dependencies.getExtensionSettings - Get extension settings
|
||
* @param {Function} dependencies.onStatsChange - Callback when stats change
|
||
*/
|
||
export function registerUserAttributesWidget(registry, dependencies) {
|
||
const {
|
||
getExtensionSettings,
|
||
onStatsChange
|
||
} = dependencies;
|
||
|
||
registry.register('userAttributes', {
|
||
name: 'User Attributes',
|
||
icon: '⚔️',
|
||
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
|
||
requiresSchema: false,
|
||
|
||
/**
|
||
* Render widget content
|
||
* @param {HTMLElement} container - Widget container
|
||
* @param {Object} config - Widget configuration
|
||
*/
|
||
render(container, config = {}) {
|
||
const settings = getExtensionSettings();
|
||
const classicStats = settings.classicStats;
|
||
const trackerConfig = settings.trackerConfig?.userStats;
|
||
|
||
// Get globally enabled attributes from trackerConfig
|
||
const globallyEnabledAttrs = trackerConfig?.rpgAttributes
|
||
?.filter(attr => attr.enabled)
|
||
.map(attr => ({ id: attr.id, name: attr.name })) || [];
|
||
|
||
// If no globally enabled attrs, fall back to defaults
|
||
const availableAttrs = globallyEnabledAttrs.length > 0
|
||
? globallyEnabledAttrs
|
||
: [
|
||
{ id: 'str', name: 'STR' },
|
||
{ id: 'dex', name: 'DEX' },
|
||
{ id: 'con', name: 'CON' },
|
||
{ id: 'int', name: 'INT' },
|
||
{ id: 'wis', name: 'WIS' },
|
||
{ id: 'cha', name: 'CHA' }
|
||
];
|
||
|
||
// Apply widget-level filter if specified (support both visibleAttrs and legacy visibleStats)
|
||
let visibleAttrs = availableAttrs;
|
||
const filterList = config.visibleAttrs || config.visibleStats;
|
||
if (filterList && filterList.length > 0) {
|
||
visibleAttrs = availableAttrs.filter(attr =>
|
||
filterList.includes(attr.id)
|
||
);
|
||
}
|
||
|
||
// Merge default config
|
||
const finalConfig = {
|
||
showLabels: true,
|
||
...config
|
||
};
|
||
|
||
// Build stats HTML using custom names from trackerConfig
|
||
const statsHtml = visibleAttrs.map(attr => `
|
||
<div class="rpg-classic-stat" data-stat="${attr.id}">
|
||
${finalConfig.showLabels ? `<span class="rpg-classic-stat-label">${attr.name}</span>` : ''}
|
||
<div class="rpg-classic-stat-buttons">
|
||
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${attr.id}">−</button>
|
||
<span class="rpg-classic-stat-value">${classicStats[attr.id] || 10}</span>
|
||
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${attr.id}">+</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Render HTML
|
||
const html = `
|
||
<div class="rpg-classic-stats">
|
||
<div class="rpg-classic-stats-grid">
|
||
${statsHtml}
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
container.innerHTML = html;
|
||
|
||
// Attach event handlers
|
||
attachEventHandlers(container, settings, onStatsChange);
|
||
},
|
||
|
||
/**
|
||
* Get configuration options
|
||
* @returns {Object} Configuration schema
|
||
*/
|
||
getConfig() {
|
||
const settings = getExtensionSettings();
|
||
const trackerConfig = settings.trackerConfig?.userStats;
|
||
|
||
// Get enabled attributes from trackerConfig for options
|
||
const enabledAttrs = trackerConfig?.rpgAttributes
|
||
?.filter(attr => attr.enabled)
|
||
.map(attr => ({ value: attr.id, label: attr.name })) || [
|
||
{ value: 'str', label: 'STR' },
|
||
{ value: 'dex', label: 'DEX' },
|
||
{ value: 'con', label: 'CON' },
|
||
{ value: 'int', label: 'INT' },
|
||
{ value: 'wis', label: 'WIS' },
|
||
{ value: 'cha', label: 'CHA' }
|
||
];
|
||
|
||
return {
|
||
visibleAttrs: {
|
||
type: 'multiselect',
|
||
label: 'Visible Attributes',
|
||
default: null, // null means "show all enabled attributes"
|
||
options: enabledAttrs,
|
||
description: 'Select which attributes to show in this widget (leave empty to show all enabled attributes)',
|
||
hint: 'To add/remove/rename attributes globally, use Tracker Settings'
|
||
},
|
||
showLabels: {
|
||
type: 'boolean',
|
||
label: 'Show Stat Labels',
|
||
default: true
|
||
}
|
||
};
|
||
},
|
||
|
||
/**
|
||
* Handle configuration changes
|
||
* @param {HTMLElement} container - Widget container
|
||
* @param {Object} newConfig - New configuration
|
||
*/
|
||
onConfigChange(container, newConfig) {
|
||
this.render(container, newConfig);
|
||
},
|
||
|
||
/**
|
||
* Handle widget resize
|
||
* @param {HTMLElement} container - Widget container
|
||
* @param {number} newW - New width
|
||
* @param {number} newH - New height
|
||
*/
|
||
onResize(container, newW, newH) {
|
||
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)';
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Calculate optimal size based on content
|
||
* Used by smart auto-layout to determine ideal widget dimensions
|
||
* @param {Object} config - Widget configuration
|
||
* @returns {Object} Optimal size { w, h }
|
||
*/
|
||
getOptimalSize(config = {}) {
|
||
const settings = getExtensionSettings();
|
||
const trackerConfig = settings.trackerConfig?.userStats;
|
||
|
||
// Count globally enabled attributes
|
||
const globallyEnabledCount = trackerConfig?.rpgAttributes
|
||
?.filter(attr => attr.enabled).length || 6;
|
||
|
||
// If widget has visibleAttrs override, use that count (support legacy visibleStats too)
|
||
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);
|
||
|
||
return {
|
||
w: 2, // Prefer 2-column grid layout
|
||
h: Math.max(this.minSize.h, optimalHeight)
|
||
};
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Attach event handlers to widget
|
||
* @private
|
||
*/
|
||
function attachEventHandlers(container, settings, onStatsChange) {
|
||
// Handle classic stat +/- buttons
|
||
const increaseButtons = container.querySelectorAll('.rpg-stat-increase');
|
||
const decreaseButtons = container.querySelectorAll('.rpg-stat-decrease');
|
||
|
||
increaseButtons.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const statName = btn.dataset.stat;
|
||
const valueSpan = btn.parentElement.querySelector('.rpg-classic-stat-value');
|
||
const currentValue = parseNumber(valueSpan.textContent, 10, 1, 20);
|
||
const newValue = Math.min(20, currentValue + 1);
|
||
|
||
valueSpan.textContent = newValue;
|
||
settings.classicStats[statName] = newValue;
|
||
|
||
if (onStatsChange) {
|
||
onStatsChange('classicStats', statName, newValue);
|
||
}
|
||
});
|
||
});
|
||
|
||
decreaseButtons.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const statName = btn.dataset.stat;
|
||
const valueSpan = btn.parentElement.querySelector('.rpg-classic-stat-value');
|
||
const currentValue = parseNumber(valueSpan.textContent, 10, 1, 20);
|
||
const newValue = Math.max(1, currentValue - 1);
|
||
|
||
valueSpan.textContent = newValue;
|
||
settings.classicStats[statName] = newValue;
|
||
|
||
if (onStatsChange) {
|
||
onStatsChange('classicStats', statName, newValue);
|
||
}
|
||
});
|
||
});
|
||
}
|