feat(dashboard): integrate tracker editor with widget system
Implemented hierarchical customization where trackerConfig controls content (fields, names, AI instructions) and dashboard controls layout (positioning, tabs, widget instances). Both systems now work together instead of conflicting. **Widget Integration:** - userStatsWidget: Respects trackerConfig for stat names and enable/disable - userStatsWidget: Supports per-widget stat filtering via config.visibleStats - userStatsWidget: Dynamically generates config options from trackerConfig - infoBoxWidgets: All widgets (calendar, weather, temperature, clock, location) check trackerConfig.infoBox.widgets.*.enabled before rendering - Widgets show "disabled" state with link to Tracker Settings when field disabled **Dashboard UI:** - Added Tracker Settings button to dashboard header (sliders icon) - Button opens tracker editor modal for global field configuration - Button positioned next to Edit Layout for clear separation of concerns **Tracker Editor:** - Added help text explaining relationship with dashboard system - Help text clarifies: Tracker Settings = content, Edit Layout = positioning - Styled with info banner at top of modal **Migration:** - Enhanced migrateV1ToV2Dashboard() to respect trackerConfig - Removes userStats widget if all stats disabled in trackerConfig - Removes presentCharacters widget if thoughts disabled in trackerConfig - Ensures smooth upgrade path from v1.x **CSS:** - Added .rpg-editor-help styling for tracker editor help banner - Added .rpg-widget-empty-state for disabled widget messaging - Info-style banner with icon and clear typography **Result:** Two-level customization system: 1. Tracker Settings (global): What fields exist, their names, AI instructions 2. Edit Layout (local): Where widgets appear, per-widget overrides Files modified: - src/systems/dashboard/widgets/userStatsWidget.js (+75 lines) - src/systems/dashboard/widgets/infoBoxWidgets.js (+67 lines) - src/systems/dashboard/dashboardIntegration.js (+15 lines) - src/systems/dashboard/dashboardTemplate.html (+4 lines) - src/systems/dashboard/defaultLayout.js (+22 lines) - template.html (+6 lines) - style.css (+58 lines)
This commit is contained in:
@@ -305,6 +305,21 @@ function setupDashboardEventListeners(dependencies) {
|
||||
});
|
||||
}
|
||||
|
||||
// Tracker Settings button (open tracker editor modal)
|
||||
const trackerSettingsBtn = document.querySelector('#rpg-dashboard-tracker-settings');
|
||||
if (trackerSettingsBtn) {
|
||||
trackerSettingsBtn.addEventListener('click', () => {
|
||||
console.log('[RPG Companion] Tracker Settings button clicked');
|
||||
// Trigger the tracker editor button from main UI
|
||||
const trackerEditorBtn = document.getElementById('rpg-open-tracker-editor');
|
||||
if (trackerEditorBtn) {
|
||||
trackerEditorBtn.click();
|
||||
} else {
|
||||
console.warn('[RPG Companion] Tracker editor button not found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Done button (exit edit mode)
|
||||
const doneBtn = document.querySelector('#rpg-dashboard-done-edit');
|
||||
if (doneBtn) {
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
<i class="fa-solid fa-pen-to-square"></i>
|
||||
</button>
|
||||
|
||||
<button id="rpg-dashboard-tracker-settings" class="rpg-dashboard-btn rpg-tracker-settings-btn rpg-priority-btn" title="Tracker Settings - Customize fields, names, and AI instructions" aria-label="Tracker settings">
|
||||
<i class="fa-solid fa-sliders"></i>
|
||||
</button>
|
||||
|
||||
<!-- Full mode buttons (hidden on overflow) -->
|
||||
<button id="rpg-dashboard-reset-layout" class="rpg-dashboard-btn rpg-reset-layout-btn rpg-overflow-btn" title="Reset to Default Layout" aria-label="Reset layout">
|
||||
<i class="fa-solid fa-rotate-left"></i>
|
||||
|
||||
@@ -225,20 +225,32 @@ export function migrateV1ToV2Dashboard(oldSettings) {
|
||||
// Respect user's visibility preferences from v1.x
|
||||
const statusTab = dashboard.tabs[0];
|
||||
|
||||
// Remove widgets that were hidden in v1.x
|
||||
if (!oldSettings.showUserStats) {
|
||||
// Check trackerConfig for field-level disabling
|
||||
const trackerConfig = oldSettings.trackerConfig;
|
||||
|
||||
// Remove userStats widget if hidden in v1.x OR all stats disabled in trackerConfig
|
||||
const allStatsDisabled = trackerConfig?.userStats?.customStats
|
||||
?.every(stat => !stat.enabled) ?? false;
|
||||
|
||||
if (!oldSettings.showUserStats || allStatsDisabled) {
|
||||
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'userStats');
|
||||
console.log('[DefaultLayout] Removed userStats widget (was hidden in v1.x)');
|
||||
console.log('[DefaultLayout] Removed userStats widget', allStatsDisabled ? '(all stats disabled in trackerConfig)' : '(was hidden in v1.x)');
|
||||
}
|
||||
|
||||
// Remove infoBox widget if hidden in v1.x
|
||||
// Note: We keep individual info widgets (calendar, weather, etc.) even if fields are disabled
|
||||
// because widgets will show disabled state with link to Tracker Settings
|
||||
if (!oldSettings.showInfoBox) {
|
||||
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'infoBox');
|
||||
console.log('[DefaultLayout] Removed infoBox widget (was hidden in v1.x)');
|
||||
}
|
||||
|
||||
if (!oldSettings.showCharacterThoughts) {
|
||||
// Remove presentCharacters widget if hidden in v1.x OR thoughts disabled in trackerConfig
|
||||
const thoughtsDisabled = trackerConfig?.presentCharacters?.thoughts?.enabled === false;
|
||||
|
||||
if (!oldSettings.showCharacterThoughts || thoughtsDisabled) {
|
||||
statusTab.widgets = statusTab.widgets.filter(w => w.type !== 'presentCharacters');
|
||||
console.log('[DefaultLayout] Removed presentCharacters widget (was hidden in v1.x)');
|
||||
console.log('[DefaultLayout] Removed presentCharacters widget', thoughtsDisabled ? '(thoughts disabled in trackerConfig)' : '(was hidden in v1.x)');
|
||||
}
|
||||
|
||||
// Remove inventory tab if it was hidden in v1.x
|
||||
|
||||
@@ -12,6 +12,43 @@
|
||||
* Users can arrange them independently or group them together.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if a field is enabled in trackerConfig and render disabled state if not
|
||||
* @param {HTMLElement} container - Widget container
|
||||
* @param {string} fieldName - Field name in trackerConfig (e.g., 'date', 'weather', 'temperature')
|
||||
* @param {string} displayName - Display name for the field (e.g., 'Date', 'Weather')
|
||||
* @param {Object} dependencies - Dependencies object
|
||||
* @returns {boolean} True if enabled, false if disabled
|
||||
*/
|
||||
function checkFieldEnabled(container, fieldName, displayName, dependencies) {
|
||||
const { getExtensionSettings } = dependencies;
|
||||
const settings = getExtensionSettings();
|
||||
const trackerConfig = settings.trackerConfig?.infoBox;
|
||||
const fieldEnabled = trackerConfig?.widgets?.[fieldName]?.enabled !== false;
|
||||
|
||||
if (!fieldEnabled) {
|
||||
container.innerHTML = `
|
||||
<div class="rpg-widget-empty-state">
|
||||
<p>⚠️ ${displayName} disabled</p>
|
||||
<p style="font-size: 0.85em; opacity: 0.7;">
|
||||
Enable in <a href="#" class="rpg-open-tracker-settings">Tracker Settings</a>
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Handle tracker settings link
|
||||
const link = container.querySelector('.rpg-open-tracker-settings');
|
||||
if (link) {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('rpg-open-tracker-editor')?.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return fieldEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Info Box data from shared data source
|
||||
* @param {string} infoBoxText - Raw info box text
|
||||
@@ -199,6 +236,12 @@ export function registerCalendarWidget(registry, dependencies) {
|
||||
|
||||
render(container, config = {}) {
|
||||
const { getInfoBoxData } = dependencies;
|
||||
|
||||
// Check if date field is enabled in trackerConfig
|
||||
if (!checkFieldEnabled(container, 'date', 'Date', dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseInfoBoxData(getInfoBoxData());
|
||||
|
||||
const monthShort = data.month ? data.month.substring(0, 3).toUpperCase() : 'MON';
|
||||
@@ -287,6 +330,12 @@ export function registerWeatherWidget(registry, dependencies) {
|
||||
|
||||
render(container, config = {}) {
|
||||
const { getInfoBoxData } = dependencies;
|
||||
|
||||
// Check if weather field is enabled in trackerConfig
|
||||
if (!checkFieldEnabled(container, 'weather', 'Weather', dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseInfoBoxData(getInfoBoxData());
|
||||
|
||||
const weatherEmoji = data.weatherEmoji || '🌤️';
|
||||
@@ -319,6 +368,12 @@ export function registerTemperatureWidget(registry, dependencies) {
|
||||
|
||||
render(container, config = {}) {
|
||||
const { getInfoBoxData } = dependencies;
|
||||
|
||||
// Check if temperature field is enabled in trackerConfig
|
||||
if (!checkFieldEnabled(container, 'temperature', 'Temperature', dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseInfoBoxData(getInfoBoxData());
|
||||
|
||||
const tempDisplay = data.temperature || '20°C';
|
||||
@@ -360,6 +415,12 @@ export function registerClockWidget(registry, dependencies) {
|
||||
|
||||
render(container, config = {}) {
|
||||
const { getInfoBoxData } = dependencies;
|
||||
|
||||
// Check if time field is enabled in trackerConfig
|
||||
if (!checkFieldEnabled(container, 'time', 'Time', dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseInfoBoxData(getInfoBoxData());
|
||||
|
||||
const timeDisplay = data.timeEnd || data.timeStart || '12:00';
|
||||
@@ -410,6 +471,12 @@ export function registerLocationWidget(registry, dependencies) {
|
||||
|
||||
render(container, config = {}) {
|
||||
const { getInfoBoxData } = dependencies;
|
||||
|
||||
// Check if location field is enabled in trackerConfig
|
||||
if (!checkFieldEnabled(container, 'location', 'Location', dependencies)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = parseInfoBoxData(getInfoBoxData());
|
||||
|
||||
const locationDisplay = data.location || 'Location';
|
||||
|
||||
@@ -51,11 +51,35 @@ export function registerUserStatsWidget(registry, dependencies) {
|
||||
render(container, config = {}) {
|
||||
const settings = getExtensionSettings();
|
||||
const stats = settings.userStats;
|
||||
const trackerConfig = settings.trackerConfig?.userStats;
|
||||
|
||||
// Get globally enabled stats from trackerConfig
|
||||
const globallyEnabledStats = trackerConfig?.customStats
|
||||
?.filter(stat => stat.enabled)
|
||||
.map(stat => ({ id: stat.id, name: stat.name })) || [];
|
||||
|
||||
// If no globally enabled stats, fall back to defaults
|
||||
const availableStats = globallyEnabledStats.length > 0
|
||||
? globallyEnabledStats
|
||||
: [
|
||||
{ id: 'health', name: 'Health' },
|
||||
{ id: 'satiety', name: 'Satiety' },
|
||||
{ id: 'energy', name: 'Energy' },
|
||||
{ id: 'hygiene', name: 'Hygiene' },
|
||||
{ id: 'arousal', name: 'Arousal' }
|
||||
];
|
||||
|
||||
// Apply widget-level filter if specified (config.visibleStats overrides)
|
||||
let visibleStats = availableStats;
|
||||
if (config.visibleStats && config.visibleStats.length > 0) {
|
||||
visibleStats = availableStats.filter(stat =>
|
||||
config.visibleStats.includes(stat.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Merge default config with user config
|
||||
const finalConfig = {
|
||||
statBarGradient: true,
|
||||
visibleStats: ['health', 'satiety', 'energy', 'hygiene', 'arousal'],
|
||||
...config
|
||||
};
|
||||
|
||||
@@ -64,23 +88,32 @@ export function registerUserStatsWidget(registry, dependencies) {
|
||||
? `linear-gradient(to right, ${settings.statBarColorLow}, ${settings.statBarColorHigh})`
|
||||
: settings.statBarColorHigh;
|
||||
|
||||
// Build progress bars HTML
|
||||
const progressBarsHtml = finalConfig.visibleStats.map(statName => {
|
||||
const label = statName.charAt(0).toUpperCase() + statName.slice(1);
|
||||
// Build progress bars HTML using trackerConfig names
|
||||
const progressBarsHtml = visibleStats.map(stat => {
|
||||
return createProgressBar({
|
||||
label,
|
||||
value: stats[statName],
|
||||
label: stat.name,
|
||||
value: stats[stat.id] || 0,
|
||||
gradient,
|
||||
editable: true,
|
||||
field: statName
|
||||
field: stat.id
|
||||
});
|
||||
}).join('');
|
||||
|
||||
// Show message if no stats are enabled
|
||||
const content = visibleStats.length > 0
|
||||
? progressBarsHtml
|
||||
: `<div class="rpg-widget-empty-state">
|
||||
<p>⚠️ No stats enabled</p>
|
||||
<p style="font-size: 0.85em; opacity: 0.7;">
|
||||
Enable stats in <a href="#" class="rpg-open-tracker-settings">Tracker Settings</a>
|
||||
</p>
|
||||
</div>`;
|
||||
|
||||
// Render HTML
|
||||
const html = `
|
||||
<div class="rpg-stats-content rpg-stats-modular">
|
||||
<div class="rpg-stats-grid">
|
||||
${progressBarsHtml}
|
||||
${content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -89,6 +122,15 @@ export function registerUserStatsWidget(registry, dependencies) {
|
||||
|
||||
// Attach event handlers
|
||||
attachEventHandlers(container, settings, onStatsChange);
|
||||
|
||||
// Handle "Tracker Settings" link click
|
||||
const trackerSettingsLink = container.querySelector('.rpg-open-tracker-settings');
|
||||
if (trackerSettingsLink) {
|
||||
trackerSettingsLink.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById('rpg-open-tracker-editor')?.click();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -96,23 +138,34 @@ export function registerUserStatsWidget(registry, dependencies) {
|
||||
* @returns {Object} Configuration schema
|
||||
*/
|
||||
getConfig() {
|
||||
return {
|
||||
statBarGradient: {
|
||||
type: 'boolean',
|
||||
label: 'Use Gradient for Stat Bars',
|
||||
default: true
|
||||
},
|
||||
visibleStats: {
|
||||
type: 'multiselect',
|
||||
label: 'Visible Stats',
|
||||
default: ['health', 'satiety', 'energy', 'hygiene', 'arousal'],
|
||||
options: [
|
||||
const settings = getExtensionSettings();
|
||||
const trackerConfig = settings.trackerConfig?.userStats;
|
||||
|
||||
// Get enabled stats from trackerConfig for options
|
||||
const enabledStats = trackerConfig?.customStats
|
||||
?.filter(stat => stat.enabled)
|
||||
.map(stat => ({ value: stat.id, label: stat.name })) || [
|
||||
{ value: 'health', label: 'Health' },
|
||||
{ value: 'satiety', label: 'Satiety' },
|
||||
{ value: 'energy', label: 'Energy' },
|
||||
{ value: 'hygiene', label: 'Hygiene' },
|
||||
{ value: 'arousal', label: 'Arousal' }
|
||||
]
|
||||
];
|
||||
|
||||
return {
|
||||
statBarGradient: {
|
||||
type: 'boolean',
|
||||
label: 'Use Gradient for Stat Bars',
|
||||
default: true,
|
||||
description: 'Show progress bars with color gradient from low to high'
|
||||
},
|
||||
visibleStats: {
|
||||
type: 'multiselect',
|
||||
label: 'Visible Stats',
|
||||
default: null, // null means "show all enabled stats"
|
||||
options: enabledStats,
|
||||
description: 'Select which stats to show in this widget (leave empty to show all enabled stats)',
|
||||
hint: 'To add/remove/rename stats globally, use Tracker Settings'
|
||||
}
|
||||
};
|
||||
},
|
||||
@@ -144,7 +197,15 @@ export function registerUserStatsWidget(registry, dependencies) {
|
||||
* @returns {Object} Optimal size { w, h }
|
||||
*/
|
||||
getOptimalSize(config = {}) {
|
||||
const visibleStatCount = config.visibleStats?.length || 5;
|
||||
const settings = getExtensionSettings();
|
||||
const trackerConfig = settings.trackerConfig?.userStats;
|
||||
|
||||
// Count globally enabled stats
|
||||
const globallyEnabledCount = trackerConfig?.customStats
|
||||
?.filter(stat => stat.enabled).length || 5;
|
||||
|
||||
// If widget has visibleStats override, use that count
|
||||
const visibleStatCount = config.visibleStats?.length || globallyEnabledCount;
|
||||
|
||||
// Each stat bar needs ~0.4 rows of height
|
||||
// Add 0.5 row for padding/margins
|
||||
|
||||
@@ -7684,3 +7684,61 @@ body:has(.rpg-panel.rpg-position-left) #sheld {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* ========================================
|
||||
Tracker Settings & Widget Integration
|
||||
======================================== */
|
||||
|
||||
/* Tracker Editor Help Text */
|
||||
.rpg-editor-help {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75em;
|
||||
padding: 0.75em 1em;
|
||||
margin: 0 0 1em 0;
|
||||
background: rgba(100, 149, 237, 0.1);
|
||||
border: 1px solid rgba(100, 149, 237, 0.3);
|
||||
border-radius: 0.375em;
|
||||
color: var(--rpg-text);
|
||||
font-size: 0.875em;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.rpg-editor-help i {
|
||||
color: #6495ed;
|
||||
font-size: 1.1em;
|
||||
margin-top: 0.15em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Empty Widget State */
|
||||
.rpg-widget-empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2em 1em;
|
||||
text-align: center;
|
||||
color: var(--rpg-text);
|
||||
opacity: 0.7;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.rpg-widget-empty-state p {
|
||||
margin: 0.5em 0;
|
||||
}
|
||||
|
||||
.rpg-widget-empty-state p:first-child {
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.rpg-widget-empty-state a {
|
||||
color: var(--rpg-highlight);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.rpg-widget-empty-state a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@@ -342,6 +342,12 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div class="rpg-editor-help">
|
||||
<i class="fa-solid fa-circle-info"></i>
|
||||
<span><strong>Tracker Settings</strong> control available fields, names, and AI instructions. To arrange widgets on your dashboard, use <strong>Edit Layout</strong> mode.</span>
|
||||
</div>
|
||||
|
||||
<div class="rpg-settings-popup-body">
|
||||
<!-- Tab contents will be rendered by JavaScript -->
|
||||
<div id="rpg-editor-tab-userStats" class="rpg-editor-tab-content"></div>
|
||||
|
||||
Reference in New Issue
Block a user