diff --git a/src/systems/dashboard/dashboardIntegration.js b/src/systems/dashboard/dashboardIntegration.js index 16d23f3..ecaba5d 100644 --- a/src/systems/dashboard/dashboardIntegration.js +++ b/src/systems/dashboard/dashboardIntegration.js @@ -13,7 +13,10 @@ import { DashboardManager } from './dashboardManager.js'; import { WidgetRegistry } from './widgetRegistry.js'; // Widget imports +import { registerUserInfoWidget } from './widgets/userInfoWidget.js'; import { registerUserStatsWidget } from './widgets/userStatsWidget.js'; +import { registerUserMoodWidget } from './widgets/userMoodWidget.js'; +import { registerUserAttributesWidget } from './widgets/userAttributesWidget.js'; import { registerCalendarWidget, registerWeatherWidget, registerTemperatureWidget, registerClockWidget, registerLocationWidget } from './widgets/infoBoxWidgets.js'; import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js'; import { registerInventoryWidget } from './widgets/inventoryWidget.js'; @@ -165,18 +168,25 @@ function getInlineDashboardTemplate() { function registerAllWidgets(registry, dependencies) { console.log('[RPG Companion] Registering widgets...'); - // Core widgets + // User modular widgets + registerUserInfoWidget(registry, dependencies); registerUserStatsWidget(registry, dependencies); - registerPresentCharactersWidget(registry, dependencies); - registerInventoryWidget(registry, dependencies); + registerUserMoodWidget(registry, dependencies); + registerUserAttributesWidget(registry, dependencies); - // Info Box modular widgets + // Scene info widgets registerCalendarWidget(registry, dependencies); registerWeatherWidget(registry, dependencies); registerTemperatureWidget(registry, dependencies); registerClockWidget(registry, dependencies); registerLocationWidget(registry, dependencies); + // Social widgets + registerPresentCharactersWidget(registry, dependencies); + + // Inventory widget + registerInventoryWidget(registry, dependencies); + console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`); } diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index 694705a..f9bc60e 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -102,7 +102,34 @@ export class DashboardManager { container: this.gridContainer, onColumnsChange: (newCols, oldCols) => { console.log('[DashboardManager] Grid columns changed:', oldCols, '→', newCols); - // Re-render all widgets when column count changes + + // Fix widget dimensions when column count changes + // This prevents widgets from shrinking when grid switches between 2/3/4 columns + const currentTab = this.tabManager.getTab(this.currentTabId); + if (currentTab) { + currentTab.widgets.forEach(widget => { + // If widget was full-width in old grid, make it full-width in new grid + if (widget.w === oldCols) { + console.log(`[DashboardManager] Adjusting full-width widget ${widget.id}: w=${widget.w} → ${newCols}`); + widget.w = newCols; + } + // If widget is wider than new grid, clamp it + else if (widget.w > newCols) { + console.log(`[DashboardManager] Clamping oversized widget ${widget.id}: w=${widget.w} → ${newCols}`); + widget.w = newCols; + } + // If widget x position is out of bounds, reset to 0 + if (widget.x >= newCols) { + console.log(`[DashboardManager] Resetting out-of-bounds widget ${widget.id}: x=${widget.x} → 0`); + widget.x = 0; + } + }); + + // Save changes + this.triggerAutoSave(); + } + + // Re-render all widgets with adjusted dimensions this.renderAllWidgets(); } }); @@ -182,9 +209,6 @@ export class DashboardManager { // Measure container width and set up responsive sizing this.setupContainerSizing(); - // Migrate old 12-column layouts to new responsive grid - this.migrateOldLayouts(); - // Render tab navigation this.renderTabs(); @@ -515,8 +539,11 @@ export class DashboardManager { element.dataset.widgetId = widget.id; element.dataset.widgetType = widget.type; - // Position widget using grid engine (responsive units for scaling) - const pos = this.gridEngine.getWidgetPosition(widget); + // Validate widget dimensions (defensive check - shouldn't be needed if onColumnsChange works) + const validated = this.gridEngine.validateWidget(widget, definition.minSize || { w: 1, h: 1 }); + + // Position widget using validated dimensions + const pos = this.gridEngine.getWidgetPosition(validated); element.style.position = 'absolute'; element.style.left = pos.left; // % of container (e.g., "5.23%") element.style.top = pos.top; // vh units (e.g., "10.45vh") diff --git a/src/systems/dashboard/widgets/inventoryWidget.js b/src/systems/dashboard/widgets/inventoryWidget.js index e63435f..1b66acc 100644 --- a/src/systems/dashboard/widgets/inventoryWidget.js +++ b/src/systems/dashboard/widgets/inventoryWidget.js @@ -62,6 +62,7 @@ export function registerInventoryWidget(registry, dependencies) { name: 'Inventory', icon: '🎒', description: 'Full inventory system with On Person, Stored, and Assets', + category: 'inventory', minSize: { w: 2, h: 4 }, defaultSize: { w: 2, h: 6 }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/presentCharactersWidget.js b/src/systems/dashboard/widgets/presentCharactersWidget.js index 9e90161..95bf2c8 100644 --- a/src/systems/dashboard/widgets/presentCharactersWidget.js +++ b/src/systems/dashboard/widgets/presentCharactersWidget.js @@ -235,6 +235,7 @@ export function registerPresentCharactersWidget(registry, dependencies) { name: 'Present Characters', icon: '👥', description: 'Character cards with avatars, traits, and relationships', + category: 'social', minSize: { w: 2, h: 2 }, defaultSize: { w: 2, h: 3 }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/userAttributesWidget.js b/src/systems/dashboard/widgets/userAttributesWidget.js new file mode 100644 index 0000000..7e4bf46 --- /dev/null +++ b/src/systems/dashboard/widgets/userAttributesWidget.js @@ -0,0 +1,197 @@ +/** + * 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); + } + }); + }); +} diff --git a/src/systems/dashboard/widgets/userInfoWidget.js b/src/systems/dashboard/widgets/userInfoWidget.js new file mode 100644 index 0000000..c36d145 --- /dev/null +++ b/src/systems/dashboard/widgets/userInfoWidget.js @@ -0,0 +1,187 @@ +/** + * User Info Widget + * + * Displays user avatar, name, and level. + * Compact widget showing basic user identity with editable level. + * + * Features: + * - User portrait/avatar display + * - User name from SillyTavern context + * - Editable level field (1-100) + * - Compact horizontal layout + */ + +import { parseNumber } from '../widgetBase.js'; + +/** + * Register User Info Widget + * @param {WidgetRegistry} registry - Widget registry instance + * @param {Object} dependencies - External dependencies + * @param {Function} dependencies.getContext - Get SillyTavern context + * @param {Function} dependencies.getUserAvatar - Get user avatar URL + * @param {Function} dependencies.getExtensionSettings - Get extension settings + * @param {Function} dependencies.onStatsChange - Callback when stats change + */ +export function registerUserInfoWidget(registry, dependencies) { + const { + getContext, + getUserAvatar, + getExtensionSettings, + onStatsChange + } = dependencies; + + registry.register('userInfo', { + name: 'User Info', + icon: '👤', + description: 'User avatar, name, and level display', + category: 'user', + minSize: { w: 1, h: 1 }, + defaultSize: { w: 2, h: 1 }, + requiresSchema: false, + + /** + * Render widget content + * @param {HTMLElement} container - Widget container + * @param {Object} config - Widget configuration + */ + render(container, config = {}) { + const settings = getExtensionSettings(); + const context = getContext(); + const userName = context.name1; + const userPortrait = getUserAvatar(); + + // Merge default config + const finalConfig = { + showAvatar: true, + showName: true, + showLevel: true, + ...config + }; + + // Build HTML + const html = ` +
+ ${finalConfig.showAvatar ? `${userName}` : ''} + ${finalConfig.showName ? `${userName}` : ''} + ${finalConfig.showLevel ? ` + | + LVL + ${settings.level} + ` : ''} +
+ `; + + container.innerHTML = html; + + // Attach event handlers + attachEventHandlers(container, settings, onStatsChange); + }, + + /** + * Get configuration options + * @returns {Object} Configuration schema + */ + getConfig() { + return { + showAvatar: { + type: 'boolean', + label: 'Show Avatar', + default: true + }, + showName: { + type: 'boolean', + label: 'Show User Name', + default: true + }, + showLevel: { + type: 'boolean', + label: 'Show Level', + 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) { + // Responsive adjustments if needed + const infoRow = container.querySelector('.rpg-user-info-row'); + if (!infoRow) return; + + // Stack vertically on very narrow widgets + if (newW < 2) { + infoRow.style.flexDirection = 'column'; + infoRow.style.alignItems = 'center'; + } else { + infoRow.style.flexDirection = 'row'; + infoRow.style.alignItems = 'center'; + } + } + }); +} + +/** + * Attach event handlers to widget + * @private + */ +function attachEventHandlers(container, settings, onStatsChange) { + // Handle level editing + const levelValue = container.querySelector('.rpg-level-value.rpg-editable'); + if (!levelValue) return; + + let originalLevel = parseNumber(levelValue.textContent.trim(), 1, 1, 100); + + levelValue.addEventListener('focus', () => { + originalLevel = parseNumber(levelValue.textContent.trim(), 1, 1, 100); + // Select all text + const range = document.createRange(); + range.selectNodeContents(levelValue); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + + levelValue.addEventListener('blur', () => { + const value = parseNumber(levelValue.textContent.trim(), originalLevel, 1, 100); + levelValue.textContent = value; + + if (value !== originalLevel) { + settings.level = value; + if (onStatsChange) { + onStatsChange('level', null, value); + } + } + }); + + levelValue.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + levelValue.blur(); + } + if (e.key === 'Escape') { + e.preventDefault(); + levelValue.textContent = originalLevel; + levelValue.blur(); + } + }); + + // Prevent paste with formatting + levelValue.addEventListener('paste', (e) => { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData('text/plain'); + document.execCommand('insertText', false, text); + }); +} diff --git a/src/systems/dashboard/widgets/userMoodWidget.js b/src/systems/dashboard/widgets/userMoodWidget.js new file mode 100644 index 0000000..d8fc73d --- /dev/null +++ b/src/systems/dashboard/widgets/userMoodWidget.js @@ -0,0 +1,210 @@ +/** + * User Mood Widget + * + * Displays user's current mood emoji and active conditions. + * Compact widget showing emotional state and status effects. + * + * Features: + * - Large mood emoji (editable) + * - Conditions/status effects text (editable) + * - Responsive layout + */ + +/** + * Register User Mood 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 registerUserMoodWidget(registry, dependencies) { + const { + getExtensionSettings, + onStatsChange + } = dependencies; + + registry.register('userMood', { + name: 'User Mood', + icon: '😊', + description: 'Mood emoji and active conditions', + category: 'user', + minSize: { w: 1, h: 1 }, + defaultSize: { w: 2, h: 1 }, + requiresSchema: false, + + /** + * Render widget content + * @param {HTMLElement} container - Widget container + * @param {Object} config - Widget configuration + */ + render(container, config = {}) { + const settings = getExtensionSettings(); + const stats = settings.userStats; + + // Merge default config + const finalConfig = { + showMoodEmoji: true, + showConditions: true, + ...config + }; + + // Build HTML + const html = ` +
+ ${finalConfig.showMoodEmoji ? `
${stats.mood}
` : ''} + ${finalConfig.showConditions ? `
${stats.conditions}
` : ''} +
+ `; + + container.innerHTML = html; + + // Attach event handlers + attachEventHandlers(container, settings, onStatsChange); + }, + + /** + * Get configuration options + * @returns {Object} Configuration schema + */ + getConfig() { + return { + showMoodEmoji: { + type: 'boolean', + label: 'Show Mood Emoji', + default: true + }, + showConditions: { + type: 'boolean', + label: 'Show Conditions', + 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) { + // Responsive adjustments if needed + const mood = container.querySelector('.rpg-mood'); + if (!mood) return; + + // Adjust layout for narrow widgets + if (newW < 2) { + mood.style.flexDirection = 'column'; + } else { + mood.style.flexDirection = 'row'; + } + } + }); +} + +/** + * Attach event handlers to widget + * @private + */ +function attachEventHandlers(container, settings, onStatsChange) { + // Handle mood emoji editing + const moodEmoji = container.querySelector('.rpg-mood-emoji.rpg-editable'); + if (moodEmoji) { + let originalMood = moodEmoji.textContent.trim(); + + moodEmoji.addEventListener('focus', () => { + originalMood = moodEmoji.textContent.trim(); + const range = document.createRange(); + range.selectNodeContents(moodEmoji); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + + moodEmoji.addEventListener('blur', () => { + const value = moodEmoji.textContent.trim() || '😐'; + moodEmoji.textContent = value; + + if (value !== originalMood) { + settings.userStats.mood = value; + if (onStatsChange) { + onStatsChange('userStats', 'mood', value); + } + } + }); + + moodEmoji.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + moodEmoji.blur(); + } + if (e.key === 'Escape') { + e.preventDefault(); + moodEmoji.textContent = originalMood; + moodEmoji.blur(); + } + }); + + // Prevent paste with formatting + moodEmoji.addEventListener('paste', (e) => { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData('text/plain'); + document.execCommand('insertText', false, text); + }); + } + + // Handle conditions editing + const moodConditions = container.querySelector('.rpg-mood-conditions.rpg-editable'); + if (moodConditions) { + let originalConditions = moodConditions.textContent.trim(); + + moodConditions.addEventListener('focus', () => { + originalConditions = moodConditions.textContent.trim(); + const range = document.createRange(); + range.selectNodeContents(moodConditions); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + + moodConditions.addEventListener('blur', () => { + const value = moodConditions.textContent.trim() || 'None'; + moodConditions.textContent = value; + + if (value !== originalConditions) { + settings.userStats.conditions = value; + if (onStatsChange) { + onStatsChange('userStats', 'conditions', value); + } + } + }); + + moodConditions.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + moodConditions.blur(); + } + if (e.key === 'Escape') { + e.preventDefault(); + moodConditions.textContent = originalConditions; + moodConditions.blur(); + } + }); + + // Prevent paste with formatting + moodConditions.addEventListener('paste', (e) => { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData('text/plain'); + document.execCommand('insertText', false, text); + }); + } +} diff --git a/src/systems/dashboard/widgets/userStatsWidget.js b/src/systems/dashboard/widgets/userStatsWidget.js index 37be315..66bc63c 100644 --- a/src/systems/dashboard/widgets/userStatsWidget.js +++ b/src/systems/dashboard/widgets/userStatsWidget.js @@ -1,15 +1,14 @@ /** - * User Stats Widget + * User Stats Widget (Refactored - Modular) * - * Displays user health/satiety/energy/hygiene/arousal bars, - * mood/conditions, and classic D&D stats (STR/DEX/CON/INT/WIS/CHA). + * Displays user vital statistics as progress bars: + * - Health, Satiety, Energy, Hygiene, Arousal * * Features: * - Editable stat values with live update * - Progress bars with customizable colors - * - User portrait and level display - * - Classic stats with +/- buttons - * - Mobile-responsive layout + * - Configurable visible stats + * - Smart content-aware sizing (more bars = needs more height) */ import { createProgressBar, attachEditableHandlers, parseNumber } from '../widgetBase.js'; @@ -19,14 +18,11 @@ import { createProgressBar, attachEditableHandlers, parseNumber } from '../widge * @param {WidgetRegistry} registry - Widget registry instance * @param {Object} dependencies - External dependencies * @param {Function} dependencies.getContext - Get SillyTavern context - * @param {Function} dependencies.getUserAvatar - Get user avatar URL * @param {Function} dependencies.getExtensionSettings - Get extension settings * @param {Function} dependencies.onStatsChange - Callback when stats change */ export function registerUserStatsWidget(registry, dependencies) { const { - getContext, - getUserAvatar, getExtensionSettings, onStatsChange } = dependencies; @@ -34,9 +30,10 @@ export function registerUserStatsWidget(registry, dependencies) { registry.register('userStats', { name: 'User Stats', icon: '❤️', - description: 'Health, energy, satiety bars and classic RPG stats', + description: 'Health, energy, satiety bars', + category: 'user', minSize: { w: 1, h: 2 }, - defaultSize: { w: 2, h: 3 }, + defaultSize: { w: 2, h: 2 }, requiresSchema: false, /** @@ -47,16 +44,9 @@ export function registerUserStatsWidget(registry, dependencies) { render(container, config = {}) { const settings = getExtensionSettings(); const stats = settings.userStats; - const classicStats = settings.classicStats; - const context = getContext(); - const userName = context.name1; - const userPortrait = getUserAvatar(); // Merge default config with user config const finalConfig = { - showClassicStats: true, - showMood: true, - showPortrait: true, statBarGradient: true, visibleStats: ['health', 'satiety', 'energy', 'hygiene', 'arousal'], ...config @@ -79,56 +69,12 @@ export function registerUserStatsWidget(registry, dependencies) { }); }).join(''); - // Build classic stats HTML - const classicStatsHtml = finalConfig.showClassicStats ? ` -
-
-
- ${['str', 'dex', 'con', 'int', 'wis', 'cha'].map(stat => ` -
- ${stat.toUpperCase()} -
- - ${classicStats[stat]} - -
-
- `).join('')} -
-
-
- ` : ''; - - // Build mood section HTML - const moodHtml = finalConfig.showMood ? ` -
-
${stats.mood}
-
${stats.conditions}
-
- ` : ''; - - // Build portrait section HTML - const portraitHtml = finalConfig.showPortrait ? ` -
- ${userName} - ${userName} - | - LVL - ${settings.level} -
- ` : ''; - - // Render complete HTML + // Render HTML const html = ` -
-
- ${portraitHtml} -
- ${progressBarsHtml} -
- ${moodHtml} +
+
+ ${progressBarsHtml}
- ${classicStatsHtml}
`; @@ -144,21 +90,6 @@ export function registerUserStatsWidget(registry, dependencies) { */ getConfig() { return { - showClassicStats: { - type: 'boolean', - label: 'Show Classic Stats (STR/DEX/etc)', - default: true - }, - showMood: { - type: 'boolean', - label: 'Show Mood & Conditions', - default: true - }, - showPortrait: { - type: 'boolean', - label: 'Show User Portrait', - default: true - }, statBarGradient: { type: 'boolean', label: 'Use Gradient for Stat Bars', @@ -196,16 +127,26 @@ export function registerUserStatsWidget(registry, dependencies) { * @param {number} newH - New height */ onResize(container, newW, newH) { - // Adjust layout based on size - const statsContent = container.querySelector('.rpg-stats-content'); - if (!statsContent) return; + // Layout adjustments if needed (currently none) + }, - // Stack vertically on narrow widgets - if (newW < 5) { - statsContent.style.flexDirection = 'column'; - } else { - statsContent.style.flexDirection = 'row'; - } + /** + * 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 || 5; + + // Each stat bar needs ~0.4 rows of height + // Add 0.5 row for padding/margins + const optimalHeight = Math.ceil(visibleStatCount * 0.4 + 0.5); + + return { + w: 2, // Prefer full width for readability + h: Math.max(this.minSize.h, optimalHeight) + }; } }); } @@ -274,157 +215,4 @@ function attachEventHandlers(container, settings, onStatsChange) { document.execCommand('insertText', false, text); }); }); - - // Handle mood emoji editing - const moodEmoji = container.querySelector('.rpg-mood-emoji.rpg-editable'); - if (moodEmoji) { - let originalMood = moodEmoji.textContent.trim(); - - moodEmoji.addEventListener('focus', () => { - originalMood = moodEmoji.textContent.trim(); - const range = document.createRange(); - range.selectNodeContents(moodEmoji); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - }); - - moodEmoji.addEventListener('blur', () => { - const value = moodEmoji.textContent.trim() || '😐'; - moodEmoji.textContent = value; - - if (value !== originalMood) { - settings.userStats.mood = value; - if (onStatsChange) { - onStatsChange('userStats', 'mood', value); - } - } - }); - - moodEmoji.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - moodEmoji.blur(); - } - if (e.key === 'Escape') { - e.preventDefault(); - moodEmoji.textContent = originalMood; - moodEmoji.blur(); - } - }); - } - - // Handle conditions editing - const moodConditions = container.querySelector('.rpg-mood-conditions.rpg-editable'); - if (moodConditions) { - let originalConditions = moodConditions.textContent.trim(); - - moodConditions.addEventListener('focus', () => { - originalConditions = moodConditions.textContent.trim(); - const range = document.createRange(); - range.selectNodeContents(moodConditions); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - }); - - moodConditions.addEventListener('blur', () => { - const value = moodConditions.textContent.trim() || 'None'; - moodConditions.textContent = value; - - if (value !== originalConditions) { - settings.userStats.conditions = value; - if (onStatsChange) { - onStatsChange('userStats', 'conditions', value); - } - } - }); - - moodConditions.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - moodConditions.blur(); - } - if (e.key === 'Escape') { - e.preventDefault(); - moodConditions.textContent = originalConditions; - moodConditions.blur(); - } - }); - } - - // Handle level editing - const levelValue = container.querySelector('.rpg-level-value.rpg-editable'); - if (levelValue) { - let originalLevel = parseNumber(levelValue.textContent.trim(), 1, 1, 100); - - levelValue.addEventListener('focus', () => { - originalLevel = parseNumber(levelValue.textContent.trim(), 1, 1, 100); - const range = document.createRange(); - range.selectNodeContents(levelValue); - const selection = window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - }); - - levelValue.addEventListener('blur', () => { - const value = parseNumber(levelValue.textContent.trim(), originalLevel, 1, 100); - levelValue.textContent = value; - - if (value !== originalLevel) { - settings.level = value; - if (onStatsChange) { - onStatsChange('level', null, value); - } - } - }); - - levelValue.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - levelValue.blur(); - } - if (e.key === 'Escape') { - e.preventDefault(); - levelValue.textContent = originalLevel; - levelValue.blur(); - } - }); - } - - // 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); - } - }); - }); }