/** * User Attributes Widget * * Displays classic D&D-style attribute scores with +/- adjustment buttons. * Shows STR, DEX, CON, INT, WIS, CHA stats. * * Features: * - 6 classic RPG attributes * - +/- buttons for quick adjustments (1-20 range) * - Responsive grid layout * - Smart sizing: compact for narrow, grid for wide */ 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: 'Classic RPG stats (STR, DEX, CON, INT, WIS, CHA)', category: 'user', minSize: { w: 1, h: 2 }, defaultSize: { w: 2, h: 2 }, 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; // Merge default config const finalConfig = { visibleStats: ['str', 'dex', 'con', 'int', 'wis', 'cha'], showLabels: true, ...config }; // Build stats HTML const statsHtml = finalConfig.visibleStats.map(stat => `
${finalConfig.showLabels ? `${stat.toUpperCase()}` : ''}
${classicStats[stat]}
`).join(''); // Render HTML const html = `
${statsHtml}
`; container.innerHTML = html; // Attach event handlers attachEventHandlers(container, settings, onStatsChange); }, /** * Get configuration options * @returns {Object} Configuration schema */ getConfig() { return { visibleStats: { type: 'multiselect', label: 'Visible Attributes', default: ['str', 'dex', 'con', 'int', 'wis', 'cha'], options: [ { value: 'str', label: 'Strength (STR)' }, { value: 'dex', label: 'Dexterity (DEX)' }, { value: 'con', label: 'Constitution (CON)' }, { value: 'int', label: 'Intelligence (INT)' }, { value: 'wis', label: 'Wisdom (WIS)' }, { value: 'cha', label: 'Charisma (CHA)' } ] }, 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 visibleStatCount = config.visibleStats?.length || 6; // Each stat needs ~0.35 rows in 2-column grid // For 6 stats: 3 rows (0.5 row padding = 3.5 total) const optimalHeight = Math.ceil((visibleStatCount / 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); } }); }); }