Files
rpg-companion-sillytavern/src/systems/dashboard/dashboardIntegration.js
T
Lucas 'Paperboy' Rose-Winters d3c1f0a137 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)
2025-11-02 10:23:36 +11:00

570 lines
22 KiB
JavaScript

/**
* Dashboard Integration Module
*
* Handles initialization and integration of the v2 dashboard system
* with the main RPG Companion extension.
*/
import { extensionName } from '../../core/config.js';
import { extensionSettings } from '../../core/state.js';
import { saveSettings } from '../../core/persistence.js';
import { renderExtensionTemplateAsync } from '../../../../../../extensions.js';
import { DashboardManager } from './dashboardManager.js';
import { WidgetRegistry } from './widgetRegistry.js';
import { generateDefaultDashboard } from './defaultLayout.js';
import { TabScrollManager } from './tabScrollManager.js';
import { HeaderOverflowManager } from './headerOverflowManager.js';
import { showConfirmDialog } from './confirmDialog.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';
import { registerQuestsWidget } from './widgets/questsWidget.js';
// Global dashboard manager instance
let dashboardManager = null;
let tabScrollManager = null;
let headerOverflowManager = null;
/**
* Get the dashboard manager instance
*/
export function getDashboardManager() {
return dashboardManager;
}
/**
* Initialize the dashboard system
* @param {Object} dependencies - Dependencies from main extension
*/
export async function initializeDashboard(dependencies) {
console.log('[RPG Companion] Initializing Dashboard v2 System...');
try {
// Load dashboard template
const dashboardHtml = await loadDashboardTemplate();
// Find or create dashboard container in the panel
const panelContent = document.querySelector('#rpg-panel-content');
if (!panelContent) {
console.error('[RPG Companion] Panel content container not found');
return null;
}
// Insert dashboard HTML (replacing old content-box)
const contentBox = panelContent.querySelector('.rpg-content-box');
if (contentBox) {
// Replace old content-box with dashboard
contentBox.replaceWith(createDashboardContainer(dashboardHtml));
} else {
// If no content-box, insert dashboard after dice display
const diceDisplay = panelContent.querySelector('#rpg-dice-display');
if (diceDisplay) {
diceDisplay.insertAdjacentHTML('afterend', dashboardHtml);
} else {
panelContent.insertAdjacentHTML('afterbegin', dashboardHtml);
}
}
// Create widget registry
const registry = new WidgetRegistry();
// Register all widgets
registerAllWidgets(registry, dependencies);
// Initialize dashboard manager
const container = document.querySelector('#rpg-dashboard-container');
if (!container) {
console.error('[RPG Companion] Dashboard container not found after template load');
return null;
}
dashboardManager = new DashboardManager(container, {
registry,
autoSave: true,
onChange: (data) => {
// Handle dashboard changes
console.log('[RPG Companion] Dashboard changed:', data);
if (dependencies.onDashboardChange) {
dependencies.onDashboardChange(data);
}
}
});
// Initialize the dashboard
await dashboardManager.init();
// Set default layout (required for reset functionality)
const defaultLayout = generateDefaultDashboard();
dashboardManager.setDefaultLayout(defaultLayout);
console.log('[RPG Companion] Default layout set with', defaultLayout.tabs.length, 'tabs');
// Set up dashboard event listeners
setupDashboardEventListeners(dependencies);
// Initialize tab scroll manager
const tabsContainer = document.querySelector('#rpg-dashboard-tabs');
if (tabsContainer) {
tabScrollManager = new TabScrollManager(tabsContainer);
tabScrollManager.init();
}
// Initialize header overflow manager
const headerRight = document.querySelector('#rpg-dashboard-header-right');
if (headerRight) {
headerOverflowManager = new HeaderOverflowManager(headerRight);
headerOverflowManager.init();
}
console.log('[RPG Companion] Dashboard v2 initialized successfully');
return dashboardManager;
} catch (error) {
console.error('[RPG Companion] Failed to initialize dashboard:', error);
return null;
}
}
/**
* Load dashboard template HTML
*/
async function loadDashboardTemplate() {
try {
// Try to load from dashboardTemplate.html
const html = await renderExtensionTemplateAsync(extensionName, 'src/systems/dashboard/dashboardTemplate');
return html;
} catch (error) {
console.warn('[RPG Companion] Could not load dashboard template, using inline HTML');
// Fallback to inline template
return getInlineDashboardTemplate();
}
}
/**
* Create dashboard container div
*/
function createDashboardContainer(dashboardHtml) {
const wrapper = document.createElement('div');
wrapper.innerHTML = dashboardHtml;
return wrapper.firstElementChild;
}
/**
* Get inline dashboard template (fallback)
*/
function getInlineDashboardTemplate() {
return `
<div id="rpg-dashboard-container" class="rpg-dashboard-container">
<div class="rpg-dashboard-header">
<div class="rpg-dashboard-header-left">
<div id="rpg-dashboard-tabs" class="rpg-dashboard-tabs"></div>
</div>
<div class="rpg-dashboard-header-right">
<button id="rpg-dashboard-reset-layout" class="rpg-dashboard-btn rpg-reset-layout-btn" title="Reset to Default Layout">
<i class="fa-solid fa-rotate-left"></i>
</button>
<button id="rpg-dashboard-auto-layout" class="rpg-dashboard-btn rpg-auto-layout-btn" title="Auto-Arrange Widgets">
<i class="fa-solid fa-table-cells-large"></i>
</button>
<button id="rpg-dashboard-edit-mode" class="rpg-dashboard-btn rpg-edit-mode-btn" title="Toggle Edit Mode">
<i class="fa-solid fa-pen-to-square"></i>
</button>
<button id="rpg-dashboard-add-widget" class="rpg-dashboard-btn rpg-add-widget-btn" style="display: none;" title="Add Widget">
<i class="fa-solid fa-plus"></i>
</button>
<button id="rpg-dashboard-export-layout" class="rpg-dashboard-btn rpg-export-btn" style="display: none;" title="Export Layout">
<i class="fa-solid fa-download"></i>
</button>
<button id="rpg-dashboard-import-layout" class="rpg-dashboard-btn rpg-import-btn" style="display: none;" title="Import Layout">
<i class="fa-solid fa-upload"></i>
</button>
<input type="file" id="rpg-dashboard-import-file" accept=".json" style="display: none;" />
</div>
</div>
<div id="rpg-dashboard-grid" class="rpg-dashboard-grid" data-edit-mode="false"></div>
</div>
`;
}
/**
* Register all available widgets
*/
function registerAllWidgets(registry, dependencies) {
console.log('[RPG Companion] Registering widgets...');
// User modular widgets
registerUserInfoWidget(registry, dependencies);
registerUserStatsWidget(registry, dependencies);
registerUserMoodWidget(registry, dependencies);
registerUserAttributesWidget(registry, dependencies);
// 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);
// Quest widget
registerQuestsWidget(registry, dependencies);
console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`);
}
/**
* Set up dashboard event listeners
*/
function setupDashboardEventListeners(dependencies) {
// Reset layout button
const resetLayoutBtn = document.querySelector('#rpg-dashboard-reset-layout');
if (resetLayoutBtn) {
resetLayoutBtn.addEventListener('click', async () => {
if (dashboardManager) {
const confirmed = await showConfirmDialog({
title: 'Reset Layout?',
message: 'This will remove all widgets and reload the default layout. This action cannot be undone.',
variant: 'danger',
confirmText: 'Reset',
cancelText: 'Cancel'
});
if (confirmed) {
console.log('[RPG Companion] Reset layout button clicked');
dashboardManager.resetLayout();
}
}
});
}
// Auto-layout button
const autoLayoutBtn = document.querySelector('#rpg-dashboard-auto-layout');
if (autoLayoutBtn) {
autoLayoutBtn.addEventListener('click', async () => {
if (dashboardManager) {
const confirmed = await showConfirmDialog({
title: 'Auto-Arrange All Widgets?',
message: 'This will reorganize all widgets across all tabs and may change their positions. This action cannot be undone.',
variant: 'warning',
confirmText: 'Auto-Arrange',
cancelText: 'Cancel'
});
if (confirmed) {
console.log('[RPG Companion] Auto-layout button clicked');
dashboardManager.autoLayoutWidgets();
}
}
});
}
// Sort Tab button (layout current tab only)
const sortTabBtn = document.querySelector('#rpg-dashboard-sort-tab');
if (sortTabBtn) {
sortTabBtn.addEventListener('click', () => {
if (dashboardManager) {
console.log('[RPG Companion] Sort tab button clicked');
dashboardManager.autoLayoutCurrentTab();
}
});
}
// Edit mode toggle
const editModeBtn = document.querySelector('#rpg-dashboard-edit-mode');
if (editModeBtn) {
editModeBtn.addEventListener('click', () => {
if (dashboardManager && dashboardManager.editManager) {
console.log('[RPG Companion] Edit button clicked');
dashboardManager.editManager.toggleEditMode();
// Refresh header overflow menu to reflect edit mode button visibility changes
if (headerOverflowManager) {
setTimeout(() => headerOverflowManager.refresh(), 50);
}
}
});
}
// Lock/unlock widgets button
const lockWidgetsBtn = document.querySelector('#rpg-dashboard-lock-widgets');
if (lockWidgetsBtn) {
lockWidgetsBtn.addEventListener('click', () => {
if (dashboardManager && dashboardManager.editManager) {
console.log('[RPG Companion] Lock button clicked');
dashboardManager.editManager.toggleLock();
}
});
}
// 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) {
doneBtn.addEventListener('click', () => {
if (dashboardManager && dashboardManager.editManager) {
console.log('[RPG Companion] Done button clicked');
dashboardManager.editManager.exitEditMode(true); // Save changes
// Refresh header overflow menu to reflect edit mode button visibility changes
if (headerOverflowManager) {
setTimeout(() => headerOverflowManager.refresh(), 50);
}
}
});
}
// Add widget button - supports both desktop click and mobile touch
const addWidgetBtn = document.querySelector('#rpg-dashboard-add-widget');
if (addWidgetBtn) {
// Use pointerdown for universal desktop/mobile support
const openAddWidget = (e) => {
e.preventDefault();
e.stopPropagation();
if (dashboardManager) {
showAddWidgetDialog(dashboardManager);
}
};
// Listen to both click (desktop) and pointerdown (mobile) for maximum compatibility
addWidgetBtn.addEventListener('click', openAddWidget);
addWidgetBtn.addEventListener('pointerdown', openAddWidget, { once: true });
}
// Export layout button
const exportBtn = document.querySelector('#rpg-dashboard-export-layout');
if (exportBtn) {
exportBtn.addEventListener('click', () => {
if (dashboardManager) {
dashboardManager.exportLayout();
}
});
}
// Import layout button - trigger file input on click
const importBtn = document.querySelector('#rpg-dashboard-import-layout');
const importFile = document.querySelector('#rpg-dashboard-import-file');
if (importBtn && importFile) {
console.log('[RPG Companion] Import button and file input initialized');
// Trigger file picker on button click
importBtn.addEventListener('click', (e) => {
console.log('[RPG Companion] Import button clicked, triggering file picker');
console.log('[RPG Companion] File input element:', importFile);
console.log('[RPG Companion] File input visible:', importFile.offsetParent !== null);
try {
// Direct click works on desktop and mobile when input is properly positioned
importFile.click();
console.log('[RPG Companion] File input click() called successfully');
} catch (err) {
console.error('[RPG Companion] Error triggering file input:', err);
}
});
// Handle file selection
importFile.addEventListener('change', (e) => {
const file = e.target.files[0];
console.log('[RPG Companion] File input change event fired');
console.log('[RPG Companion] Selected file:', file);
if (file) {
if (dashboardManager) {
console.log('[RPG Companion] Importing layout from:', file.name);
dashboardManager.importLayout(file);
} else {
console.error('[RPG Companion] Dashboard manager not available');
}
importFile.value = ''; // Reset file input
} else {
console.warn('[RPG Companion] No file selected');
}
});
} else {
console.error('[RPG Companion] Import button or file input not found!', {
importBtn,
importFile
});
}
}
/**
* Show add widget dialog
*/
function showAddWidgetDialog(manager) {
// Get all available widgets
const registry = manager.registry;
const widgets = registry.getAll();
// Create widget cards HTML
// Note: registry.getAll() returns [{type, definition}, ...] not [[type, definition], ...]
const widgetCardsHtml = widgets.map(({type, definition}) => `
<div class="rpg-widget-card" data-widget-type="${type}">
<div class="rpg-widget-card-icon">${definition.icon}</div>
<div class="rpg-widget-card-name">${definition.name}</div>
<div class="rpg-widget-card-description">${definition.description}</div>
<button class="rpg-widget-card-add" data-widget-type="${type}">
<i class="fa-solid fa-plus"></i> Add
</button>
</div>
`).join('');
// Show modal
const modal = document.querySelector('#rpg-add-widget-modal');
if (!modal) {
console.warn('[RPG Companion] Add widget modal not found');
return;
}
// CRITICAL: Move modal to document.body on first use to escape panel constraints
// The panel has transform in its transition which creates a containing block,
// constraining position:fixed children to the panel instead of viewport
if (modal.parentElement?.id !== 'document-body-modals') {
// Create container for modals at body level (only once)
let bodyModalsContainer = document.getElementById('document-body-modals');
if (!bodyModalsContainer) {
bodyModalsContainer = document.createElement('div');
bodyModalsContainer.id = 'document-body-modals';
bodyModalsContainer.style.cssText = 'position: fixed; inset: 0; pointer-events: none; z-index: 10000; display: flex; align-items: center; justify-content: center;';
document.body.appendChild(bodyModalsContainer);
}
bodyModalsContainer.appendChild(modal);
console.log('[RPG Companion] Moved Add Widget modal to document.body for proper viewport positioning');
}
const widgetSelector = modal.querySelector('#rpg-widget-selector');
if (widgetSelector) {
widgetSelector.innerHTML = widgetCardsHtml;
// Attach add button handlers
widgetSelector.querySelectorAll('.rpg-widget-card-add').forEach(btn => {
btn.addEventListener('click', () => {
const widgetType = btn.dataset.widgetType;
// Use activeTabId property instead of getActiveTabId() method
const activeTab = manager.tabManager.activeTabId;
manager.addWidget(widgetType, activeTab);
hideModal('rpg-add-widget-modal');
});
});
}
// Show modal with proper pointer events (parent has pointer-events: none)
modal.style.display = 'flex';
modal.style.pointerEvents = 'auto';
// Set up modal close handlers
modal.querySelectorAll('[data-close="add-widget"]').forEach(btn => {
btn.onclick = () => hideModal('rpg-add-widget-modal');
});
// Close on backdrop click
modal.onclick = (e) => {
if (e.target === modal) {
hideModal('rpg-add-widget-modal');
}
};
}
/**
* Hide modal by ID
*/
function hideModal(modalId) {
const modal = document.querySelector(`#${modalId}`);
if (modal) {
modal.style.display = 'none';
}
}
/**
* Create default dashboard layout
*/
export function createDefaultLayout(manager) {
if (!manager) {
console.warn('[RPG Companion] Cannot create default layout - manager not initialized');
return;
}
console.log('[RPG Companion] Creating default dashboard layout with modular widgets...');
// Use activeTabId property instead of getActiveTabId() method
const mainTab = manager.tabManager.activeTabId;
// Add modular user widgets
// Row 0: User Info (avatar, name, level) - full width
manager.addWidget('userInfo', mainTab, { x: 0, y: 0, w: 2, h: 1 });
// Row 1-2: User Stats (health/energy bars) - full width
manager.addWidget('userStats', mainTab, { x: 0, y: 1, w: 2, h: 2 });
// Row 3-4: User Mood (left) + User Attributes (right)
manager.addWidget('userMood', mainTab, { x: 0, y: 3, w: 1, h: 1 });
manager.addWidget('userAttributes', mainTab, { x: 1, y: 3, w: 1, h: 2 });
// Row 5-6: Calendar (left) + Weather (right)
manager.addWidget('calendar', mainTab, { x: 0, y: 5, w: 1, h: 2 });
manager.addWidget('weather', mainTab, { x: 1, y: 5, w: 1, h: 2 });
// Row 7-8: Temperature (left) + Clock (right)
manager.addWidget('temperature', mainTab, { x: 0, y: 7, w: 1, h: 2 });
manager.addWidget('clock', mainTab, { x: 1, y: 7, w: 1, h: 2 });
// Row 9-10: Location (full width)
manager.addWidget('location', mainTab, { x: 0, y: 9, w: 2, h: 2 });
// Row 11-13: Present Characters (full width)
manager.addWidget('presentCharacters', mainTab, { x: 0, y: 11, w: 2, h: 3 });
console.log('[RPG Companion] Default layout created with modular widgets');
}
/**
* Refresh all widgets (called after data updates)
*/
export function refreshDashboard() {
if (dashboardManager && dashboardManager.widgets) {
// Re-render all active widgets by accessing the widgets Map directly
dashboardManager.widgets.forEach((widgetData, widgetId) => {
// Get the widget definition from registry
const definition = dashboardManager.registry.get(widgetData.widget.type);
if (definition && widgetData.element) {
// Re-render the widget content
dashboardManager.renderWidgetContent(widgetData.element, widgetData.widget, definition);
}
});
}
}
/**
* Destroy dashboard instance
*/
export function destroyDashboard() {
if (dashboardManager) {
console.log('[RPG Companion] Destroying dashboard...');
// Clean up would go here
dashboardManager = null;
}
}