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:
@@ -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 ? `
|
||||
<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
|
||||
// Render HTML
|
||||
const html = `
|
||||
<div class="rpg-stats-content">
|
||||
<div class="rpg-stats-left">
|
||||
${portraitHtml}
|
||||
<div class="rpg-stats-grid">
|
||||
${progressBarsHtml}
|
||||
</div>
|
||||
${moodHtml}
|
||||
<div class="rpg-stats-content rpg-stats-modular">
|
||||
<div class="rpg-stats-grid">
|
||||
${progressBarsHtml}
|
||||
</div>
|
||||
${classicStatsHtml}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user