From 95f4ae184840c9e58e19882ecae3d57a40abe5bb Mon Sep 17 00:00:00 2001 From: Lucas 'Paperboy' Rose-Winters Date: Sun, 2 Nov 2025 16:21:56 +1100 Subject: [PATCH] feat(dashboard): add Recent Events widget for v2 system - Add registerRecentEventsWidget() in infoBoxWidgets.js - Implement notebook-style UI with rings, bullet points, and editable events - Support max 3 events with + placeholders for new entries - Parse 'Recent Events: event1, event2, event3' format from infoBox - Register widget in dashboardIntegration.js - Add to default layout Scene tab (row 4-5, below location) - Integrate with tracker system: - Add to WIDGET_TO_TAB_MAP (maps to tab-scene) - Add to shouldWidgetBeRemoved() rules - Add to detectConfigChanges() for re-addition support - Completes v2 widget migration - all tracker features now have widgets --- src/systems/dashboard/dashboardIntegration.js | 3 +- src/systems/dashboard/dashboardManager.js | 7 +- src/systems/dashboard/defaultLayout.js | 16 +- .../dashboard/widgets/infoBoxWidgets.js | 254 +++++++++++++++++- 4 files changed, 273 insertions(+), 7 deletions(-) diff --git a/src/systems/dashboard/dashboardIntegration.js b/src/systems/dashboard/dashboardIntegration.js index a1d932e..7878c08 100644 --- a/src/systems/dashboard/dashboardIntegration.js +++ b/src/systems/dashboard/dashboardIntegration.js @@ -21,7 +21,7 @@ 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 { registerCalendarWidget, registerWeatherWidget, registerTemperatureWidget, registerClockWidget, registerLocationWidget, registerRecentEventsWidget } from './widgets/infoBoxWidgets.js'; import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js'; import { registerInventoryWidget } from './widgets/inventoryWidget.js'; import { registerQuestsWidget } from './widgets/questsWidget.js'; @@ -209,6 +209,7 @@ function registerAllWidgets(registry, dependencies) { registerTemperatureWidget(registry, dependencies); registerClockWidget(registry, dependencies); registerLocationWidget(registry, dependencies); + registerRecentEventsWidget(registry, dependencies); // Social widgets registerPresentCharactersWidget(registry, dependencies); diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index 82d07fa..2358e99 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -1681,6 +1681,7 @@ export class DashboardManager { 'temperature': 'tab-scene', 'clock': 'tab-scene', 'location': 'tab-scene', + 'recentEvents': 'tab-scene', 'presentCharacters': 'tab-scene', 'userStats': 'tab-status', 'userInfo': 'tab-status', @@ -1705,13 +1706,14 @@ export class DashboardManager { const widgetsToAdd = []; - // Check infoBox widgets (calendar, weather, temperature, clock, location) + // Check infoBox widgets (calendar, weather, temperature, clock, location, recentEvents) const infoBoxWidgetMap = { 'date': 'calendar', 'weather': 'weather', 'temperature': 'temperature', 'time': 'clock', - 'location': 'location' + 'location': 'location', + 'recentEvents': 'recentEvents' }; Object.entries(infoBoxWidgetMap).forEach(([fieldKey, widgetType]) => { @@ -1961,6 +1963,7 @@ export class DashboardManager { 'temperature': () => config.infoBox?.widgets?.temperature?.enabled === false, 'clock': () => config.infoBox?.widgets?.time?.enabled === false, 'location': () => config.infoBox?.widgets?.location?.enabled === false, + 'recentEvents': () => config.infoBox?.widgets?.recentEvents?.enabled === false, 'userStats': () => { const customStats = config.userStats?.customStats || []; return customStats.filter(s => s.enabled).length === 0; diff --git a/src/systems/dashboard/defaultLayout.js b/src/systems/dashboard/defaultLayout.js index c7dae26..092d169 100644 --- a/src/systems/dashboard/defaultLayout.js +++ b/src/systems/dashboard/defaultLayout.js @@ -143,12 +143,24 @@ export function generateDefaultDashboard() { h: 2, config: {} }, - // Row 4-6: Present Characters (full width, will expand with auto-layout) + // Row 4-5: Recent Events (notebook style, full width) + { + id: 'widget-recentevents', + type: 'recentEvents', + x: 0, + y: 4, + w: 2, + h: 2, + config: { + maxEvents: 3 + } + }, + // Row 6-8: Present Characters (full width, will expand with auto-layout) { id: 'widget-presentchars', type: 'presentCharacters', x: 0, - y: 4, + y: 6, w: 2, h: 3, config: { diff --git a/src/systems/dashboard/widgets/infoBoxWidgets.js b/src/systems/dashboard/widgets/infoBoxWidgets.js index 6a517b8..1ca8e78 100644 --- a/src/systems/dashboard/widgets/infoBoxWidgets.js +++ b/src/systems/dashboard/widgets/infoBoxWidgets.js @@ -24,7 +24,8 @@ function parseInfoBoxData(infoBoxText) { weatherEmoji: '', weatherForecast: '', temperature: '', tempValue: 0, timeStart: '', timeEnd: '', - location: '' + location: '', + recentEvents: [] }; } @@ -34,7 +35,8 @@ function parseInfoBoxData(infoBoxText) { weatherEmoji: '', weatherForecast: '', temperature: '', tempValue: 0, timeStart: '', timeEnd: '', - location: '' + location: '', + recentEvents: [] }; for (const line of lines) { @@ -87,6 +89,13 @@ function parseInfoBoxData(infoBoxText) { } } } + // Recent Events parsing + else if (line.startsWith('Recent Events:')) { + const eventsString = line.replace('Recent Events:', '').trim(); + if (eventsString) { + data.recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e); + } + } } return data; @@ -476,3 +485,244 @@ function attachSimpleEditHandlers(container, dependencies) { }); }); } + +/** + * Register Recent Events Widget + * @param {WidgetRegistry} registry - Widget registry instance + * @param {Object} dependencies - External dependencies + * @param {Function} dependencies.getExtensionSettings - Get extension settings + * @param {Function} dependencies.saveSettings - Save settings + */ +export function registerRecentEventsWidget(registry, dependencies) { + const { getExtensionSettings, saveSettings } = dependencies; + + registry.register('recentEvents', { + name: 'Recent Events', + icon: '📝', + description: 'Recent events notebook', + category: 'scene', + minSize: { w: 2, 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 infoBoxData = settings.committedTrackerData?.infoBox || ''; + const data = parseInfoBoxData(infoBoxData); + + // Merge default config with user config + const finalConfig = { + maxEvents: 3, + ...config + }; + + // Get events array (filter out placeholders) + let validEvents = data.recentEvents.filter(e => + e && e.trim() && + e !== 'Event 1' && e !== 'Event 2' && e !== 'Event 3' && + e !== 'Click to add event' && e !== 'Add event...' + ); + + // If no valid events, show at least one placeholder + if (validEvents.length === 0) { + validEvents = ['Click to add event']; + } + + // Build events HTML + let eventsHtml = ''; + + // Render existing events (max maxEvents) + for (let i = 0; i < Math.min(validEvents.length, finalConfig.maxEvents); i++) { + eventsHtml += ` +
+ + ${validEvents[i]} +
+ `; + } + + // Add empty placeholders with + icon + for (let i = validEvents.length; i < finalConfig.maxEvents; i++) { + eventsHtml += ` +
+ + + Add event... +
+ `; + } + + // Render HTML + const html = ` +
+
+
+
+
+
+
Recent Events
+
+ ${eventsHtml} +
+
+ `; + + container.innerHTML = html; + + // Attach event handlers + attachRecentEventsHandlers(container, settings, saveSettings); + }, + + /** + * Get configuration options + * @returns {Object} Configuration schema + */ + getConfig() { + return { + maxEvents: { + type: 'number', + label: 'Max Events', + default: 3, + min: 1, + max: 5, + description: 'Maximum number of events to display' + } + }; + }, + + /** + * Handle configuration changes + * @param {HTMLElement} container - Widget container + * @param {Object} newConfig - New configuration + */ + onConfigChange(container, newConfig) { + this.render(container, newConfig); + } + }); +} + +/** + * Attach event handlers for Recent Events widget + * @private + */ +function attachRecentEventsHandlers(container, settings, saveSettings) { + const eventFields = container.querySelectorAll('.rpg-editable-event'); + + eventFields.forEach(field => { + const eventIndex = parseInt(field.dataset.eventIndex); + let originalValue = field.textContent.trim(); + + field.addEventListener('focus', () => { + originalValue = field.textContent.trim(); + // Clear placeholder text on focus + if (field.classList.contains('rpg-event-placeholder')) { + field.textContent = ''; + } + // Select all text + const range = document.createRange(); + range.selectNodeContents(field); + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + }); + + field.addEventListener('blur', () => { + const value = field.textContent.trim(); + + // Restore placeholder if empty + if (!value && field.classList.contains('rpg-event-placeholder')) { + field.textContent = 'Add event...'; + return; + } + + // Update if changed + if (value !== originalValue) { + updateRecentEvent(eventIndex, value, settings, saveSettings); + } + }); + + field.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + field.blur(); + } + if (e.key === 'Escape') { + e.preventDefault(); + field.textContent = originalValue; + field.blur(); + } + }); + + // Prevent paste with formatting + field.addEventListener('paste', (e) => { + e.preventDefault(); + const text = (e.clipboardData || window.clipboardData).getData('text/plain'); + document.execCommand('insertText', false, text); + }); + }); +} + +/** + * Update a specific recent event in infoBox data + * @private + */ +function updateRecentEvent(eventIndex, value, settings, saveSettings) { + // Parse current infoBox to get existing events + const infoBoxData = settings.committedTrackerData?.infoBox || ''; + const lines = infoBoxData.split('\n'); + let recentEvents = []; + + // Find existing Recent Events line + const recentEventsLine = lines.find(line => line.startsWith('Recent Events:')); + if (recentEventsLine) { + const eventsString = recentEventsLine.replace('Recent Events:', '').trim(); + if (eventsString) { + recentEvents = eventsString.split(',').map(e => e.trim()).filter(e => e); + } + } + + // Ensure array has enough slots + while (recentEvents.length <= eventIndex) { + recentEvents.push(''); + } + + // Update the specific event + recentEvents[eventIndex] = value; + + // Filter out empty events and rebuild the line + const validEvents = recentEvents.filter(e => e && e.trim()); + const newRecentEventsLine = validEvents.length > 0 + ? `Recent Events: ${validEvents.join(', ')}` + : ''; + + // Update infoBox with new Recent Events line + const updatedLines = lines.filter(line => !line.startsWith('Recent Events:')); + if (newRecentEventsLine) { + // Add Recent Events line at the end (before any empty lines) + let insertIndex = updatedLines.length; + for (let i = updatedLines.length - 1; i >= 0; i--) { + if (updatedLines[i].trim() !== '') { + insertIndex = i + 1; + break; + } + } + updatedLines.splice(insertIndex, 0, newRecentEventsLine); + } + + const updatedInfoBox = updatedLines.join('\n'); + + // Update committed and last generated data + settings.committedTrackerData.infoBox = updatedInfoBox; + if (settings.lastGeneratedData) { + settings.lastGeneratedData.infoBox = updatedInfoBox; + } + + // Save settings + saveSettings(); + + console.log(`[Recent Events Widget] Updated event ${eventIndex}: "${value}"`); +}