feat(dashboard): split user
Stats widget into 4 modular widgets - Create userInfoWidget (avatar, name, level) - Refactor userStatsWidget (stats bars only with smart sizing) - Create userMoodWidget (mood emoji, conditions) - Create userAttributesWidget (STR/DEX/CON/INT/WIS/CHA) - Add category field to widgets for auto-layout grouping - Register all new modular widgets in dashboardIntegration.js All widgets include getOptimalSize() for smart content-aware auto-layout. Part of Phase 1 & 3.1 of dashboard modularization plan.
This commit is contained in:
@@ -13,7 +13,10 @@ import { DashboardManager } from './dashboardManager.js';
|
|||||||
import { WidgetRegistry } from './widgetRegistry.js';
|
import { WidgetRegistry } from './widgetRegistry.js';
|
||||||
|
|
||||||
// Widget imports
|
// Widget imports
|
||||||
|
import { registerUserInfoWidget } from './widgets/userInfoWidget.js';
|
||||||
import { registerUserStatsWidget } from './widgets/userStatsWidget.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 { registerCalendarWidget, registerWeatherWidget, registerTemperatureWidget, registerClockWidget, registerLocationWidget } from './widgets/infoBoxWidgets.js';
|
||||||
import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js';
|
import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js';
|
||||||
import { registerInventoryWidget } from './widgets/inventoryWidget.js';
|
import { registerInventoryWidget } from './widgets/inventoryWidget.js';
|
||||||
@@ -165,18 +168,25 @@ function getInlineDashboardTemplate() {
|
|||||||
function registerAllWidgets(registry, dependencies) {
|
function registerAllWidgets(registry, dependencies) {
|
||||||
console.log('[RPG Companion] Registering widgets...');
|
console.log('[RPG Companion] Registering widgets...');
|
||||||
|
|
||||||
// Core widgets
|
// User modular widgets
|
||||||
|
registerUserInfoWidget(registry, dependencies);
|
||||||
registerUserStatsWidget(registry, dependencies);
|
registerUserStatsWidget(registry, dependencies);
|
||||||
registerPresentCharactersWidget(registry, dependencies);
|
registerUserMoodWidget(registry, dependencies);
|
||||||
registerInventoryWidget(registry, dependencies);
|
registerUserAttributesWidget(registry, dependencies);
|
||||||
|
|
||||||
// Info Box modular widgets
|
// Scene info widgets
|
||||||
registerCalendarWidget(registry, dependencies);
|
registerCalendarWidget(registry, dependencies);
|
||||||
registerWeatherWidget(registry, dependencies);
|
registerWeatherWidget(registry, dependencies);
|
||||||
registerTemperatureWidget(registry, dependencies);
|
registerTemperatureWidget(registry, dependencies);
|
||||||
registerClockWidget(registry, dependencies);
|
registerClockWidget(registry, dependencies);
|
||||||
registerLocationWidget(registry, dependencies);
|
registerLocationWidget(registry, dependencies);
|
||||||
|
|
||||||
|
// Social widgets
|
||||||
|
registerPresentCharactersWidget(registry, dependencies);
|
||||||
|
|
||||||
|
// Inventory widget
|
||||||
|
registerInventoryWidget(registry, dependencies);
|
||||||
|
|
||||||
console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`);
|
console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,34 @@ export class DashboardManager {
|
|||||||
container: this.gridContainer,
|
container: this.gridContainer,
|
||||||
onColumnsChange: (newCols, oldCols) => {
|
onColumnsChange: (newCols, oldCols) => {
|
||||||
console.log('[DashboardManager] Grid columns changed:', oldCols, '→', newCols);
|
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();
|
this.renderAllWidgets();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -182,9 +209,6 @@ export class DashboardManager {
|
|||||||
// Measure container width and set up responsive sizing
|
// Measure container width and set up responsive sizing
|
||||||
this.setupContainerSizing();
|
this.setupContainerSizing();
|
||||||
|
|
||||||
// Migrate old 12-column layouts to new responsive grid
|
|
||||||
this.migrateOldLayouts();
|
|
||||||
|
|
||||||
// Render tab navigation
|
// Render tab navigation
|
||||||
this.renderTabs();
|
this.renderTabs();
|
||||||
|
|
||||||
@@ -515,8 +539,11 @@ export class DashboardManager {
|
|||||||
element.dataset.widgetId = widget.id;
|
element.dataset.widgetId = widget.id;
|
||||||
element.dataset.widgetType = widget.type;
|
element.dataset.widgetType = widget.type;
|
||||||
|
|
||||||
// Position widget using grid engine (responsive units for scaling)
|
// Validate widget dimensions (defensive check - shouldn't be needed if onColumnsChange works)
|
||||||
const pos = this.gridEngine.getWidgetPosition(widget);
|
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.position = 'absolute';
|
||||||
element.style.left = pos.left; // % of container (e.g., "5.23%")
|
element.style.left = pos.left; // % of container (e.g., "5.23%")
|
||||||
element.style.top = pos.top; // vh units (e.g., "10.45vh")
|
element.style.top = pos.top; // vh units (e.g., "10.45vh")
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function registerInventoryWidget(registry, dependencies) {
|
|||||||
name: 'Inventory',
|
name: 'Inventory',
|
||||||
icon: '🎒',
|
icon: '🎒',
|
||||||
description: 'Full inventory system with On Person, Stored, and Assets',
|
description: 'Full inventory system with On Person, Stored, and Assets',
|
||||||
|
category: 'inventory',
|
||||||
minSize: { w: 2, h: 4 },
|
minSize: { w: 2, h: 4 },
|
||||||
defaultSize: { w: 2, h: 6 },
|
defaultSize: { w: 2, h: 6 },
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ export function registerPresentCharactersWidget(registry, dependencies) {
|
|||||||
name: 'Present Characters',
|
name: 'Present Characters',
|
||||||
icon: '👥',
|
icon: '👥',
|
||||||
description: 'Character cards with avatars, traits, and relationships',
|
description: 'Character cards with avatars, traits, and relationships',
|
||||||
|
category: 'social',
|
||||||
minSize: { w: 2, h: 2 },
|
minSize: { w: 2, h: 2 },
|
||||||
defaultSize: { w: 2, h: 3 },
|
defaultSize: { w: 2, h: 3 },
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|||||||
@@ -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 => `
|
||||||
|
<div class="rpg-classic-stat" data-stat="${stat}">
|
||||||
|
${finalConfig.showLabels ? `<span class="rpg-classic-stat-label">${stat.toUpperCase()}</span>` : ''}
|
||||||
|
<div class="rpg-classic-stat-buttons">
|
||||||
|
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${stat}">−</button>
|
||||||
|
<span class="rpg-classic-stat-value">${classicStats[stat]}</span>
|
||||||
|
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${stat}">+</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() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 = `
|
||||||
|
<div class="rpg-user-info-row">
|
||||||
|
${finalConfig.showAvatar ? `<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />` : ''}
|
||||||
|
${finalConfig.showName ? `<span class="rpg-user-name">${userName}</span>` : ''}
|
||||||
|
${finalConfig.showLevel ? `
|
||||||
|
<span style="opacity: 0.5;">|</span>
|
||||||
|
<span class="rpg-level-label">LVL</span>
|
||||||
|
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${settings.level}</span>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 = `
|
||||||
|
<div class="rpg-mood">
|
||||||
|
${finalConfig.showMoodEmoji ? `<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>` : ''}
|
||||||
|
${finalConfig.showConditions ? `<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${stats.conditions}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,14 @@
|
|||||||
/**
|
/**
|
||||||
* User Stats Widget
|
* User Stats Widget (Refactored - Modular)
|
||||||
*
|
*
|
||||||
* Displays user health/satiety/energy/hygiene/arousal bars,
|
* Displays user vital statistics as progress bars:
|
||||||
* mood/conditions, and classic D&D stats (STR/DEX/CON/INT/WIS/CHA).
|
* - Health, Satiety, Energy, Hygiene, Arousal
|
||||||
*
|
*
|
||||||
* Features:
|
* Features:
|
||||||
* - Editable stat values with live update
|
* - Editable stat values with live update
|
||||||
* - Progress bars with customizable colors
|
* - Progress bars with customizable colors
|
||||||
* - User portrait and level display
|
* - Configurable visible stats
|
||||||
* - Classic stats with +/- buttons
|
* - Smart content-aware sizing (more bars = needs more height)
|
||||||
* - Mobile-responsive layout
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createProgressBar, attachEditableHandlers, parseNumber } from '../widgetBase.js';
|
import { createProgressBar, attachEditableHandlers, parseNumber } from '../widgetBase.js';
|
||||||
@@ -19,14 +18,11 @@ import { createProgressBar, attachEditableHandlers, parseNumber } from '../widge
|
|||||||
* @param {WidgetRegistry} registry - Widget registry instance
|
* @param {WidgetRegistry} registry - Widget registry instance
|
||||||
* @param {Object} dependencies - External dependencies
|
* @param {Object} dependencies - External dependencies
|
||||||
* @param {Function} dependencies.getContext - Get SillyTavern context
|
* @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.getExtensionSettings - Get extension settings
|
||||||
* @param {Function} dependencies.onStatsChange - Callback when stats change
|
* @param {Function} dependencies.onStatsChange - Callback when stats change
|
||||||
*/
|
*/
|
||||||
export function registerUserStatsWidget(registry, dependencies) {
|
export function registerUserStatsWidget(registry, dependencies) {
|
||||||
const {
|
const {
|
||||||
getContext,
|
|
||||||
getUserAvatar,
|
|
||||||
getExtensionSettings,
|
getExtensionSettings,
|
||||||
onStatsChange
|
onStatsChange
|
||||||
} = dependencies;
|
} = dependencies;
|
||||||
@@ -34,9 +30,10 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
registry.register('userStats', {
|
registry.register('userStats', {
|
||||||
name: 'User Stats',
|
name: 'User Stats',
|
||||||
icon: '❤️',
|
icon: '❤️',
|
||||||
description: 'Health, energy, satiety bars and classic RPG stats',
|
description: 'Health, energy, satiety bars',
|
||||||
|
category: 'user',
|
||||||
minSize: { w: 1, h: 2 },
|
minSize: { w: 1, h: 2 },
|
||||||
defaultSize: { w: 2, h: 3 },
|
defaultSize: { w: 2, h: 2 },
|
||||||
requiresSchema: false,
|
requiresSchema: false,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,16 +44,9 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
render(container, config = {}) {
|
render(container, config = {}) {
|
||||||
const settings = getExtensionSettings();
|
const settings = getExtensionSettings();
|
||||||
const stats = settings.userStats;
|
const stats = settings.userStats;
|
||||||
const classicStats = settings.classicStats;
|
|
||||||
const context = getContext();
|
|
||||||
const userName = context.name1;
|
|
||||||
const userPortrait = getUserAvatar();
|
|
||||||
|
|
||||||
// Merge default config with user config
|
// Merge default config with user config
|
||||||
const finalConfig = {
|
const finalConfig = {
|
||||||
showClassicStats: true,
|
|
||||||
showMood: true,
|
|
||||||
showPortrait: true,
|
|
||||||
statBarGradient: true,
|
statBarGradient: true,
|
||||||
visibleStats: ['health', 'satiety', 'energy', 'hygiene', 'arousal'],
|
visibleStats: ['health', 'satiety', 'energy', 'hygiene', 'arousal'],
|
||||||
...config
|
...config
|
||||||
@@ -79,56 +69,12 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
});
|
});
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
// Build classic stats HTML
|
// Render HTML
|
||||||
const classicStatsHtml = finalConfig.showClassicStats ? `
|
|
||||||
<div class="rpg-stats-right">
|
|
||||||
<div class="rpg-classic-stats">
|
|
||||||
<div class="rpg-classic-stats-grid">
|
|
||||||
${['str', 'dex', 'con', 'int', 'wis', 'cha'].map(stat => `
|
|
||||||
<div class="rpg-classic-stat" data-stat="${stat}">
|
|
||||||
<span class="rpg-classic-stat-label">${stat.toUpperCase()}</span>
|
|
||||||
<div class="rpg-classic-stat-buttons">
|
|
||||||
<button class="rpg-classic-stat-btn rpg-stat-decrease" data-stat="${stat}">−</button>
|
|
||||||
<span class="rpg-classic-stat-value">${classicStats[stat]}</span>
|
|
||||||
<button class="rpg-classic-stat-btn rpg-stat-increase" data-stat="${stat}">+</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`).join('')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
// Build mood section HTML
|
|
||||||
const moodHtml = finalConfig.showMood ? `
|
|
||||||
<div class="rpg-mood">
|
|
||||||
<div class="rpg-mood-emoji rpg-editable" contenteditable="true" data-field="mood" title="Click to edit emoji">${stats.mood}</div>
|
|
||||||
<div class="rpg-mood-conditions rpg-editable" contenteditable="true" data-field="conditions" title="Click to edit conditions">${stats.conditions}</div>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
// Build portrait section HTML
|
|
||||||
const portraitHtml = finalConfig.showPortrait ? `
|
|
||||||
<div class="rpg-user-info-row">
|
|
||||||
<img src="${userPortrait}" alt="${userName}" class="rpg-user-portrait" onerror="this.style.opacity='0.5';this.onerror=null;" />
|
|
||||||
<span class="rpg-user-name">${userName}</span>
|
|
||||||
<span style="opacity: 0.5;">|</span>
|
|
||||||
<span class="rpg-level-label">LVL</span>
|
|
||||||
<span class="rpg-level-value rpg-editable" contenteditable="true" data-field="level" title="Click to edit level">${settings.level}</span>
|
|
||||||
</div>
|
|
||||||
` : '';
|
|
||||||
|
|
||||||
// Render complete HTML
|
|
||||||
const html = `
|
const html = `
|
||||||
<div class="rpg-stats-content">
|
<div class="rpg-stats-content rpg-stats-modular">
|
||||||
<div class="rpg-stats-left">
|
|
||||||
${portraitHtml}
|
|
||||||
<div class="rpg-stats-grid">
|
<div class="rpg-stats-grid">
|
||||||
${progressBarsHtml}
|
${progressBarsHtml}
|
||||||
</div>
|
</div>
|
||||||
${moodHtml}
|
|
||||||
</div>
|
|
||||||
${classicStatsHtml}
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -144,21 +90,6 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
*/
|
*/
|
||||||
getConfig() {
|
getConfig() {
|
||||||
return {
|
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: {
|
statBarGradient: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Use Gradient for Stat Bars',
|
label: 'Use Gradient for Stat Bars',
|
||||||
@@ -196,16 +127,26 @@ export function registerUserStatsWidget(registry, dependencies) {
|
|||||||
* @param {number} newH - New height
|
* @param {number} newH - New height
|
||||||
*/
|
*/
|
||||||
onResize(container, newW, newH) {
|
onResize(container, newW, newH) {
|
||||||
// Adjust layout based on size
|
// Layout adjustments if needed (currently none)
|
||||||
const statsContent = container.querySelector('.rpg-stats-content');
|
},
|
||||||
if (!statsContent) return;
|
|
||||||
|
|
||||||
// Stack vertically on narrow widgets
|
/**
|
||||||
if (newW < 5) {
|
* Calculate optimal size based on content
|
||||||
statsContent.style.flexDirection = 'column';
|
* Used by smart auto-layout to determine ideal widget dimensions
|
||||||
} else {
|
* @param {Object} config - Widget configuration
|
||||||
statsContent.style.flexDirection = 'row';
|
* @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);
|
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user