diff --git a/src/systems/dashboard/dashboardIntegration.js b/src/systems/dashboard/dashboardIntegration.js index ba99a57..0da6eda 100644 --- a/src/systems/dashboard/dashboardIntegration.js +++ b/src/systems/dashboard/dashboardIntegration.js @@ -27,6 +27,7 @@ import { registerSceneInfoWidget } from './widgets/sceneInfoWidget.js'; import { registerPresentCharactersWidget } from './widgets/presentCharactersWidget.js'; import { registerInventoryWidget } from './widgets/inventoryWidget.js'; import { registerQuestsWidget } from './widgets/questsWidget.js'; +import { registerUserSkillsWidget } from './widgets/userSkillsWidget.js'; // Global dashboard manager instance let dashboardManager = null; @@ -254,6 +255,9 @@ function registerAllWidgets(registry, dependencies) { // Quest widget registerQuestsWidget(registry, dependencies); + // Skills widget + registerUserSkillsWidget(registry, dependencies); + console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`); } @@ -335,24 +339,16 @@ function setupDashboardEventListeners(dependencies) { if (dashboardManager && dashboardManager.editManager) { console.log('[RPG Companion] Lock button clicked'); dashboardManager.editManager.toggleLock(); + // Refresh header overflow menu to reflect lock button state change + if (headerOverflowManager) { + setTimeout(() => headerOverflowManager.refresh(), 50); + } } }); } - // 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'); - } - }); - } + // Tracker Settings button now uses ID 'rpg-open-tracker-editor' + // Event handler is in trackerEditor.js using jQuery delegation // Done button (exit edit mode) const doneBtn = document.querySelector('#rpg-dashboard-done-edit'); diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index 2527823..289b63e 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -185,18 +185,8 @@ export class DashboardManager { }); // Initialize Tab Manager with dashboard data structure - // Create default tab if no tabs exist - if (this.dashboard.tabs.length === 0) { - this.dashboard.tabs.push({ - id: 'main', - name: 'Main', - icon: 'fa-solid fa-house', - order: 0, - widgets: [] - }); - this.dashboard.defaultTab = 'main'; - } - + // Note: Tabs will be populated by loadLayout() which runs after init() + // Default layout is set via setDefaultLayout() before init() is called this.tabManager = new TabManager(this.dashboard); // Set current tab to active tab from TabManager @@ -959,7 +949,8 @@ export class DashboardManager { scene: [], social: [], inventory: [], - quests: [] + quests: [], + skills: [] }; widgets.forEach(widget => { @@ -1041,6 +1032,19 @@ export class DashboardManager { this.gridEngine.autoLayout(groups.quests, { preserveOrder: true }); } + // Create Skills tab if there are skills widgets + if (groups.skills.length > 0) { + this.dashboard.tabs.push({ + id: 'tab-skills', + name: 'Skills', + icon: 'fa-solid fa-book', + order: 5, + widgets: groups.skills + }); + + this.gridEngine.autoLayout(groups.skills, { preserveOrder: true }); + } + console.log('[DashboardManager] Created', this.dashboard.tabs.length, 'tabs'); // Re-render tabs and switch to first tab @@ -1080,7 +1084,8 @@ export class DashboardManager { 'social': 3, 'inventory': 4, 'quests': 5, - 'other': 6 + 'skills': 6, + 'other': 7 }; // Specific widget type ordering within user category @@ -1485,32 +1490,29 @@ export class DashboardManager { /** * Load saved layout + * + * For first-run users (no saved layout), calls resetLayout() for comprehensive + * initialization. This ensures consistent behavior between first-run and manual + * reset, using a single code path for default layout setup. */ async loadLayout() { try { const saved = await this.persistence.loadLayout(); if (saved) { + console.log('[DashboardManager] Loading saved layout'); this.applyDashboardConfig(saved); - } else if (this.defaultLayout) { - console.log('[DashboardManager] No saved layout, using default with auto-layout'); - this.applyDashboardConfig(this.defaultLayout); - - // Auto-layout each tab to prevent overlap (default positions may not fit screen) - this.dashboard.tabs.forEach(tab => { - if (tab.widgets && tab.widgets.length > 0) { - console.log(`[DashboardManager] Auto-laying out default tab "${tab.name}" (${tab.widgets.length} widgets)`); - this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true }); - } - }); - - // Save the auto-laid-out default as the initial saved layout - await this.saveLayout(true); + } else { + // First run - use resetLayout() for comprehensive initialization + // This provides: fresh layout generation, state reset, validation, + // column-aware sizing, and proper UI rendering + console.log('[DashboardManager] No saved layout found, calling resetLayout() for first-run initialization'); + await this.resetLayout(); } } catch (error) { console.error('[DashboardManager] Failed to load layout:', error); - if (this.defaultLayout) { - this.applyDashboardConfig(this.defaultLayout); - } + // Fallback: use resetLayout() for clean state recovery + console.log('[DashboardManager] Recovering with resetLayout()'); + await this.resetLayout(); } } @@ -1562,7 +1564,8 @@ export class DashboardManager { // Skip initial switch in applyDashboardConfig since we'll switch after layout calculations this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true }); - // Reset all widgets to default sizes + // Apply column-aware widget sizes from widget definitions + // This makes widgets scale properly based on screen width (2-4 columns) const allWidgets = []; this.dashboard.tabs.forEach(tab => { if (tab.widgets && tab.widgets.length > 0) { @@ -1571,13 +1574,10 @@ export class DashboardManager { }); this.resetWidgetSizesToDefault(allWidgets); - // Auto-layout each tab to prevent overlap (default positions may have changed) - this.dashboard.tabs.forEach(tab => { - if (tab.widgets && tab.widgets.length > 0) { - console.log(`[DashboardManager] Auto-laying out tab "${tab.name}" (${tab.widgets.length} widgets)`); - this.gridEngine.autoLayout(tab.widgets, { preserveOrder: true }); - } - }); + // Don't call autoLayout - preserve positions from defaultLayout.js + // Widget definitions now have column-aware sizes (defaultSize returns correct size for column count) + // ResizeObserver will handle column changes and trigger autoLayout when screen resizes + console.log('[DashboardManager] Using column-aware sizes from widget definitions, preserving positions from defaultLayout.js'); // Force re-render tabs this.renderTabs(); @@ -1631,6 +1631,146 @@ export class DashboardManager { console.log(`[DashboardManager] Reset ${resetCount} widgets to default sizes`); } + /** + * Try to apply default layout positions to current tab + * + * Checks if the current tab's widgets match the default layout and applies + * the default positions if they do. This ensures "Sort Current Page" produces + * the same layout as "Reset Layout" for default widgets. + * + * @param {Object} tab - Tab to apply default layout to + * @param {Object} options - Layout options + * @returns {boolean} True if default layout was applied, false otherwise + */ + tryApplyDefaultLayoutToTab(tab, options = {}) { + if (!this.defaultLayout || !this.defaultLayout.tabs) { + console.log('[DashboardManager] No default layout available'); + return false; + } + + // Find matching default tab by ID + const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id); + if (!defaultTab) { + console.log(`[DashboardManager] No default layout for tab "${tab.name}" (${tab.id})`); + return false; + } + + // Check if widgets match (same types, possibly different IDs) + const currentTypes = tab.widgets.map(w => w.type).sort(); + const defaultTypes = defaultTab.widgets.map(w => w.type).sort(); + + if (currentTypes.length !== defaultTypes.length || + !currentTypes.every((type, i) => type === defaultTypes[i])) { + console.log('[DashboardManager] Tab widgets do not match default layout (custom widgets present)'); + return false; + } + + console.log('[DashboardManager] Applying default layout positions to current tab'); + + // Reset widget sizes to defaults (unless explicitly disabled) + if (options.resetSizes !== false) { + this.resetWidgetSizesToDefault(tab.widgets); + } + + // Apply default positions to each widget + tab.widgets.forEach(widget => { + const defaultWidget = defaultTab.widgets.find(w => w.type === widget.type); + if (defaultWidget) { + widget.x = defaultWidget.x; + widget.y = defaultWidget.y; + // Size is already set by resetWidgetSizesToDefault + console.log(`[DashboardManager] Set ${widget.type} to default position (${widget.x}, ${widget.y})`); + } + }); + + return true; + } + + /** + * Try to apply default layout to all tabs + * + * Checks if the current dashboard widgets match the default layout and applies + * the default positions if they do. This ensures "Auto Arrange" produces + * the same layout as "Reset Layout" for default widgets. + * + * @param {Object} options - Layout options + * @returns {boolean} True if default layout was applied, false otherwise + */ + tryApplyDefaultLayout(options = {}) { + if (!this.defaultLayout || !this.defaultLayout.tabs) { + console.log('[DashboardManager] No default layout available'); + return false; + } + + // Check if tabs match default layout + if (this.dashboard.tabs.length !== this.defaultLayout.tabs.length) { + console.log('[DashboardManager] Tab count does not match default layout'); + return false; + } + + // Check if all tabs and widgets match + for (let i = 0; i < this.dashboard.tabs.length; i++) { + const tab = this.dashboard.tabs[i]; + const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id); + + if (!defaultTab) { + console.log(`[DashboardManager] No default tab found for "${tab.name}" (${tab.id})`); + return false; + } + + const currentTypes = tab.widgets.map(w => w.type).sort(); + const defaultTypes = defaultTab.widgets.map(w => w.type).sort(); + + if (currentTypes.length !== defaultTypes.length || + !currentTypes.every((type, j) => type === defaultTypes[j])) { + console.log(`[DashboardManager] Tab "${tab.name}" widgets do not match default layout`); + return false; + } + } + + console.log('[DashboardManager] Applying default layout positions to all tabs'); + + // Gather all widgets from all tabs + const allWidgets = []; + this.dashboard.tabs.forEach(tab => { + if (tab.widgets && tab.widgets.length > 0) { + allWidgets.push(...tab.widgets); + } + }); + + // Reset widget sizes to defaults (unless explicitly disabled) + if (options.resetSizes !== false) { + this.resetWidgetSizesToDefault(allWidgets); + } + + // Apply default positions to each tab + this.dashboard.tabs.forEach(tab => { + const defaultTab = this.defaultLayout.tabs.find(t => t.id === tab.id); + if (defaultTab) { + tab.widgets.forEach(widget => { + const defaultWidget = defaultTab.widgets.find(w => w.type === widget.type); + if (defaultWidget) { + widget.x = defaultWidget.x; + widget.y = defaultWidget.y; + console.log(`[DashboardManager] Set ${widget.type} to default position (${widget.x}, ${widget.y})`); + } + }); + } + }); + + // Re-render tabs and switch to first tab + this.renderTabs(); + if (this.dashboard.tabs.length > 0) { + this.switchTab(this.dashboard.tabs[0].id); + } + + // Save layout + this.triggerAutoSave(); + + console.log('[DashboardManager] Default layout applied successfully'); + return true; + } + /** * Auto-layout widgets on current tab only * Sorts and arranges widgets on the current tab to maximize space usage @@ -1656,40 +1796,48 @@ export class DashboardManager { console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`); - // Reset widget sizes to defaults (unless explicitly disabled) - if (options.resetSizes !== false) { - this.resetWidgetSizesToDefault(currentTab.widgets); - } + // Check if we can use default layout positions + const useDefaultLayout = this.tryApplyDefaultLayoutToTab(currentTab, options); - // Sort widgets by category for better organization - const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets); + if (!useDefaultLayout) { + // Fallback to traditional auto-layout + console.log('[DashboardManager] Using gridEngine.autoLayout (custom widgets or no default layout)'); - // Update tab's widgets array with sorted order - currentTab.widgets = sortedWidgets; - - // Store current widget dimensions before auto-layout - const dimensionsBefore = new Map(); - currentTab.widgets.forEach(widget => { - dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h }); - }); - - // Auto-layout widgets on the current tab - this.gridEngine.autoLayout(currentTab.widgets, { - preserveOrder: options.preserveOrder !== false - }); - - // Call onResize handlers for widgets whose dimensions changed - // This allows widgets to update internal layouts (e.g., User Attributes grid columns) - currentTab.widgets.forEach(widget => { - const before = dimensionsBefore.get(widget.id); - if (before && (before.w !== widget.w || before.h !== widget.h)) { - const widgetData = this.widgets.get(widget.id); - if (widgetData?.definition?.onResize && widgetData.element) { - console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`); - widgetData.definition.onResize(widgetData.element, widget.w, widget.h); - } + // Reset widget sizes to defaults (unless explicitly disabled) + if (options.resetSizes !== false) { + this.resetWidgetSizesToDefault(currentTab.widgets); } - }); + + // Sort widgets by category for better organization + const sortedWidgets = this.sortWidgetsByCategory(currentTab.widgets); + + // Update tab's widgets array with sorted order + currentTab.widgets = sortedWidgets; + + // Store current widget dimensions before auto-layout + const dimensionsBefore = new Map(); + currentTab.widgets.forEach(widget => { + dimensionsBefore.set(widget.id, { w: widget.w, h: widget.h }); + }); + + // Auto-layout widgets on the current tab + this.gridEngine.autoLayout(currentTab.widgets, { + preserveOrder: options.preserveOrder !== false + }); + + // Call onResize handlers for widgets whose dimensions changed + // This allows widgets to update internal layouts (e.g., User Attributes grid columns) + currentTab.widgets.forEach(widget => { + const before = dimensionsBefore.get(widget.id); + if (before && (before.w !== widget.w || before.h !== widget.h)) { + const widgetData = this.widgets.get(widget.id); + if (widgetData?.definition?.onResize && widgetData.element) { + console.log(`[DashboardManager] Calling onResize for ${widget.type} (${before.w}x${before.h} → ${widget.w}x${widget.h})`); + widgetData.definition.onResize(widgetData.element, widget.w, widget.h); + } + } + }); + } // Re-render all widgets with new positions this.clearGrid(); @@ -1721,42 +1869,50 @@ export class DashboardManager { console.log('[DashboardManager] ===== AUTO-LAYOUT WIDGETS CALLED ====='); console.log('[DashboardManager] Auto-layout widgets requested'); - // Gather ALL widgets from ALL tabs (don't lose inventory, social, etc.) - const allWidgets = []; - this.dashboard.tabs.forEach(tab => { - if (tab.widgets && tab.widgets.length > 0) { - console.log(`[DashboardManager] Gathering ${tab.widgets.length} widgets from tab "${tab.name}"`); - allWidgets.push(...tab.widgets); + // Check if we can use default layout + const useDefaultLayout = this.tryApplyDefaultLayout(options); + + if (!useDefaultLayout) { + // Fallback to traditional auto-layout + console.log('[DashboardManager] Using traditional auto-layout (custom widgets or no default layout)'); + + // Gather ALL widgets from ALL tabs (don't lose inventory, social, etc.) + const allWidgets = []; + this.dashboard.tabs.forEach(tab => { + if (tab.widgets && tab.widgets.length > 0) { + console.log(`[DashboardManager] Gathering ${tab.widgets.length} widgets from tab "${tab.name}"`); + allWidgets.push(...tab.widgets); + } + }); + + if (allWidgets.length === 0) { + console.warn('[DashboardManager] No widgets to auto-layout'); + return; } - }); - if (allWidgets.length === 0) { - console.warn('[DashboardManager] No widgets to auto-layout'); - return; + console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`); + + // Reset widget sizes to defaults (unless explicitly disabled) + if (options.resetSizes !== false) { + this.resetWidgetSizesToDefault(allWidgets); + } + + // Smart category-aware sorting BEFORE auto-layout + const widgetsToLayout = this.sortWidgetsByCategory(allWidgets); + + // Calculate estimated height to determine if multi-tab distribution is needed + const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout); + const heightThreshold = 80; // rem - reasonable max height for single tab + + console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem'); + + // Always use multi-tab distribution when we have many widgets + // This preserves all widgets (inventory, social, etc.) + console.log('[DashboardManager] Using multi-tab distribution to preserve all widgets'); + this.distributeWidgetsByCategory(widgetsToLayout); + + // distributeWidgetsByCategory handles rendering and tab switching } - - console.log(`[DashboardManager] Total widgets to layout: ${allWidgets.length}`); - - // Reset widget sizes to defaults (unless explicitly disabled) - if (options.resetSizes !== false) { - this.resetWidgetSizesToDefault(allWidgets); - } - - // Smart category-aware sorting BEFORE auto-layout - const widgetsToLayout = this.sortWidgetsByCategory(allWidgets); - - // Calculate estimated height to determine if multi-tab distribution is needed - const estimatedHeight = this.estimateLayoutHeight(widgetsToLayout); - const heightThreshold = 80; // rem - reasonable max height for single tab - - console.log('[DashboardManager] Estimated height:', estimatedHeight + 'rem', 'Threshold:', heightThreshold + 'rem'); - - // Always use multi-tab distribution when we have many widgets - // This preserves all widgets (inventory, social, etc.) - console.log('[DashboardManager] Using multi-tab distribution to preserve all widgets'); - this.distributeWidgetsByCategory(widgetsToLayout); - - // distributeWidgetsByCategory handles rendering and tab switching } /** diff --git a/src/systems/dashboard/dashboardTemplate.html b/src/systems/dashboard/dashboardTemplate.html index 79e61dd..587a730 100644 --- a/src/systems/dashboard/dashboardTemplate.html +++ b/src/systems/dashboard/dashboardTemplate.html @@ -24,7 +24,7 @@ - diff --git a/src/systems/dashboard/defaultLayout.js b/src/systems/dashboard/defaultLayout.js index a0f42ae..31232b9 100644 --- a/src/systems/dashboard/defaultLayout.js +++ b/src/systems/dashboard/defaultLayout.js @@ -39,45 +39,46 @@ export function generateDefaultDashboard() { icon: 'fa-solid fa-user', order: 0, widgets: [ - // Row 0: User Info (left) + User Mood (top right in 3-col) + // Row 0-1: User Info (left column, vertical) { id: 'widget-userinfo', type: 'userInfo', x: 0, y: 0, - w: 2, - h: 1, - config: {} - }, - { - id: 'widget-usermood', - type: 'userMood', - x: 2, - y: 0, w: 1, - h: 1, + h: 2, config: {} }, - // Row 1-2: User Stats (health/energy bars) + // Row 0-2: User Stats (right side, tall, 2 cols wide) { id: 'widget-userstats', type: 'userStats', - x: 0, - y: 1, + x: 1, + y: 0, w: 2, - h: 2, + h: 3, config: { statBarGradient: true } }, - // Row 3-4: User Attributes + // Row 2: User Mood (below user info, left column) + { + id: 'widget-usermood', + type: 'userMood', + x: 0, + y: 2, + w: 1, + h: 1, + config: {} + }, + // Row 3-6: User Attributes (full width below everything, 3 cols wide) { id: 'widget-userattributes', type: 'userAttributes', x: 0, y: 3, - w: 2, - h: 2, + w: 3, + h: 4, config: {} } ] @@ -89,36 +90,36 @@ export function generateDefaultDashboard() { icon: 'fa-solid fa-map', order: 1, widgets: [ - // Row 0-1: Scene Info (combined: calendar, weather, temp, clock, location) + // Row 0-2: Scene Info (combined: calendar, weather, temp, clock, location) { id: 'widget-sceneinfo', type: 'sceneInfo', x: 0, y: 0, - w: 2, - h: 2, + w: 3, + h: 3, config: {} }, - // Row 2-3: Recent Events (notebook style, full width) + // Row 3-4: Recent Events (notebook style, full width) { id: 'widget-recentevents', type: 'recentEvents', x: 0, - y: 2, - w: 2, + y: 3, + w: 3, h: 2, config: { maxEvents: 3 } }, - // Row 4-7: Present Characters (full width, will expand with auto-layout) + // Row 5-6: Present Characters (full width, fits 1080p screen) { id: 'widget-presentchars', type: 'presentCharacters', x: 0, - y: 4, - w: 2, - h: 4, + y: 5, + w: 3, + h: 2, config: { cardLayout: 'grid', showThoughtBubbles: true @@ -166,6 +167,29 @@ export function generateDefaultDashboard() { } } ] + }, + // Tab 5: Skills (Full tab for skills system) + { + id: 'tab-skills', + name: 'Skills', + icon: 'fa-solid fa-book', + order: 4, + widgets: [ + { + id: 'widget-userskills', + type: 'userSkills', + x: 0, + y: 0, + w: 3, + h: 7, + config: { + defaultSubTab: 'all', + showXP: true, + showCategories: true, + maxLevel: 10 + } + } + ] } ], diff --git a/src/systems/dashboard/editModeManager.js b/src/systems/dashboard/editModeManager.js index 745098d..61522b6 100644 --- a/src/systems/dashboard/editModeManager.js +++ b/src/systems/dashboard/editModeManager.js @@ -336,15 +336,6 @@ export class EditModeManager { controls.style.opacity = '0'; controls.style.transition = 'opacity 0.2s'; - // Settings button - const settingsBtn = this.createControlButton('⚙', 'Settings'); - settingsBtn.onclick = (e) => { - e.stopPropagation(); - if (this.onWidgetSettings) { - this.onWidgetSettings(widgetId); - } - }; - // Delete button const deleteBtn = this.createControlButton('×', 'Delete'); deleteBtn.onclick = (e) => { @@ -353,7 +344,6 @@ export class EditModeManager { }; deleteBtn.style.background = '#e94560'; - controls.appendChild(settingsBtn); controls.appendChild(deleteBtn); // Store reference to widget element for positioning diff --git a/src/systems/dashboard/widgets/infoBoxWidgets.js b/src/systems/dashboard/widgets/infoBoxWidgets.js index a9626d6..7ba58fd 100644 --- a/src/systems/dashboard/widgets/infoBoxWidgets.js +++ b/src/systems/dashboard/widgets/infoBoxWidgets.js @@ -529,7 +529,19 @@ export function registerRecentEventsWidget(registry, dependencies) { description: 'Recent events notebook', category: 'scene', minSize: { w: 2, h: 2 }, - defaultSize: { w: 2, h: 2 }, + // Column-aware sizing: full width at all sizes + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 2 }; // Mobile: 2 cols wide (full), 2 rows + } + return { w: 3, h: 2 }; // Desktop: 3 cols wide (full), 2 rows + }, + maxAutoSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 3 }; + } + return { w: 3, h: 3 }; + }, requiresSchema: false, /** @@ -538,7 +550,40 @@ export function registerRecentEventsWidget(registry, dependencies) { * @param {Object} config - Widget configuration */ render(container, config = {}) { - const { getInfoBoxData } = dependencies; + const { getInfoBoxData, getExtensionSettings } = dependencies; + + // Check if Recent Events is enabled in tracker config + const settings = getExtensionSettings(); + const trackerConfig = settings.trackerConfig; + const isEnabled = trackerConfig?.infoBox?.widgets?.recentEvents?.enabled !== false; + + // If disabled, show helpful message + if (!isEnabled) { + const html = ` +
+
+
+
+
+
+
+
Recent Events
+
+ +

Recent Events tracking is currently disabled.

+ +
+
+
+ `; + container.innerHTML = html; + attachDisabledStateHandlers(container); + return; + } + const data = parseInfoBoxData(getInfoBoxData()); // Merge default config with user config @@ -755,3 +800,31 @@ function updateRecentEvent(eventIndex, value, dependencies) { console.log(`[Recent Events Widget] Updated event ${eventIndex}: "${value}"`); } + +/** + * Attach handlers for disabled widget state + * Opens Tracker Settings when "Enable" button is clicked + * @private + */ +function attachDisabledStateHandlers(container) { + const enableBtn = container.querySelector('.rpg-widget-enable-btn'); + if (enableBtn) { + enableBtn.addEventListener('click', () => { + // Open Tracker Settings modal + const trackerSettingsBtn = document.querySelector('#rpg-open-tracker-editor'); + if (trackerSettingsBtn) { + trackerSettingsBtn.click(); + + // After modal opens, switch to Info Box tab + setTimeout(() => { + const infoBoxTab = document.querySelector('.rpg-editor-tab[data-tab="infoBox"]'); + if (infoBoxTab) { + infoBoxTab.click(); + } + }, 100); + } else { + console.warn('[Recent Events Widget] Tracker Settings button not found'); + } + }); + } +} diff --git a/src/systems/dashboard/widgets/inventoryWidget.js b/src/systems/dashboard/widgets/inventoryWidget.js index d913994..273e02d 100644 --- a/src/systems/dashboard/widgets/inventoryWidget.js +++ b/src/systems/dashboard/widgets/inventoryWidget.js @@ -65,18 +65,18 @@ export function registerInventoryWidget(registry, dependencies) { description: 'Full inventory system with On Person, Stored, and Assets', category: 'inventory', minSize: { w: 2, h: 4 }, - // Column-aware sizing: compact on mobile, spacious on desktop + // Column-aware sizing: compact on mobile, full width on desktop defaultSize: (columns) => { if (columns <= 2) { return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact) } - return { w: 2, h: 6 }; // Desktop: 2×6 (default) + return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p) }, maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 8 }; // Mobile: 2×8 max (increased for expansion headroom) + return { w: 2, h: 8 }; // Mobile: 2×8 max } - return { w: 3, h: 8 }; // Desktop: 3×8 max (can expand) + return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand) }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/presentCharactersWidget.js b/src/systems/dashboard/widgets/presentCharactersWidget.js index ae60e74..eab55a9 100644 --- a/src/systems/dashboard/widgets/presentCharactersWidget.js +++ b/src/systems/dashboard/widgets/presentCharactersWidget.js @@ -276,8 +276,20 @@ export function registerPresentCharactersWidget(registry, dependencies) { description: 'Character cards with avatars, traits, and relationships', category: 'scene', minSize: { w: 2, h: 2 }, - defaultSize: { w: 2, h: 2 }, // Compact size fits both mobile and desktop viewports - maxAutoSize: { w: 4, h: 5 }, // Max size for auto-arrange expansion (supports up to 4-col on large displays) + // Column-aware sizing: narrow and tall on mobile, wide and short on desktop + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; // Mobile: 2 cols wide (full), 4 rows tall + } + return { w: 3, h: 2 }; // Desktop: 3 cols wide (full), 2 rows tall (fits 1080p) + }, + // Column-aware max size: same as default to prevent expansion + maxAutoSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; // Mobile: stay at 4 rows + } + return { w: 3, h: 2 }; // Desktop: stay at 2 rows (fits 1080p without scrolling) + }, requiresSchema: false, render(container, config = {}) { diff --git a/src/systems/dashboard/widgets/questsWidget.js b/src/systems/dashboard/widgets/questsWidget.js index 5bd8c1f..fe08cd2 100644 --- a/src/systems/dashboard/widgets/questsWidget.js +++ b/src/systems/dashboard/widgets/questsWidget.js @@ -395,18 +395,18 @@ export function registerQuestsWidget(registry, dependencies) { description: 'Quest tracking with main and optional quests', category: 'quests', minSize: { w: 2, h: 4 }, - // Column-aware sizing: compact on mobile, spacious on desktop + // Column-aware sizing: compact on mobile, full width on desktop defaultSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 4 }; // Mobile: 2×4 (full width, compact) + return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact) } - return { w: 2, h: 5 }; // Desktop: 2×5 (default) + return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p) }, maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 7 }; // Mobile: 2×7 max (increased for expansion headroom) + return { w: 2, h: 8 }; // Mobile: 2×8 max } - return { w: 3, h: 7 }; // Desktop: 3×7 max (can expand) + return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand) }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/userAttributesWidget.js b/src/systems/dashboard/widgets/userAttributesWidget.js index a1babd4..a94ad6b 100644 --- a/src/systems/dashboard/widgets/userAttributesWidget.js +++ b/src/systems/dashboard/widgets/userAttributesWidget.js @@ -35,8 +35,20 @@ export function registerUserAttributesWidget(registry, dependencies) { description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)', category: 'user', minSize: { w: 2, h: 2 }, - defaultSize: { w: 2, h: 2 }, - maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion + // Column-aware sizing: full width at each column count + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; // Mobile: 2 cols wide (full), 4 rows tall + } + return { w: 3, h: 4 }; // Desktop: 3 cols wide (full), 4 rows tall + }, + // Column-aware max size: same as default + maxAutoSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 4 }; + } + return { w: 3, h: 4 }; + }, requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userInfoWidget.js b/src/systems/dashboard/widgets/userInfoWidget.js index d93dbf4..53479bf 100644 --- a/src/systems/dashboard/widgets/userInfoWidget.js +++ b/src/systems/dashboard/widgets/userInfoWidget.js @@ -38,19 +38,22 @@ export function registerUserInfoWidget(registry, dependencies) { description: 'User avatar, name, and level display', category: 'user', minSize: { w: 1, h: 1 }, - // Column-aware default size: start at 2x1 in desktop so mood doesn't block expansion + // Column-aware default size: vertical 1x2 with mood below defaultSize: (columns) => { - if (columns <= 2) { - return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout + // Mobile detection: screen width ≤ 1000px uses compact 1x1 + const isMobile = window.innerWidth <= 1000; + if (isMobile) { + return { w: 1, h: 1 }; // Mobile: 1x1, compact (round avatar) } - return { w: 2, h: 1 }; // Desktop: 2x1 from the start + return { w: 1, h: 2 }; // Desktop (all widths): 1x2 vertical, mood sits below }, - // Column-aware max size: same as defaultSize to prevent further expansion + // Column-aware max size: same as defaultSize to prevent expansion maxAutoSize: (columns) => { - if (columns <= 2) { - return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout + const isMobile = window.innerWidth <= 1000; + if (isMobile) { + return { w: 1, h: 1 }; // Mobile: 1x1, compact } - return { w: 2, h: 1 }; // Desktop: 2x1, mood sits in top-right + return { w: 1, h: 2 }; // Desktop: 1x2 vertical, mood below at y:2 }, requiresSchema: false, @@ -89,15 +92,22 @@ export function registerUserInfoWidget(registry, dependencies) { const html = `
-
- ${finalConfig.showName ? `
${userName}
` : ''} - ${finalConfig.showLevel ? ` + ${finalConfig.showAvatar ? `User Avatar` : ''} + + ${finalConfig.showName ? ` +
+
${userName}
+
+ ` : ''} + + ${finalConfig.showLevel ? ` +
LVL ${settings.level}
- ` : ''} -
+
+ ` : ''}
`; @@ -155,11 +165,15 @@ export function registerUserInfoWidget(registry, dependencies) { const infoContainer = container.querySelector('.rpg-user-info-container'); if (!infoContainer) return; - // Apply compact mode class at narrow widths for smaller text - if (newW < 3) { - infoContainer.classList.add('rpg-user-info-compact'); - } else { + // Apply layout classes based on widget width + if (newW >= 2) { + // Wide layout (2x1+): Horizontal split with name left, level right + infoContainer.classList.add('rpg-user-info-wide'); infoContainer.classList.remove('rpg-user-info-compact'); + } else { + // Compact layout (1x1): Round avatar with flush text overlays + infoContainer.classList.add('rpg-user-info-compact'); + infoContainer.classList.remove('rpg-user-info-wide'); } } }); diff --git a/src/systems/dashboard/widgets/userSkillsWidget.js b/src/systems/dashboard/widgets/userSkillsWidget.js new file mode 100644 index 0000000..ea6608a --- /dev/null +++ b/src/systems/dashboard/widgets/userSkillsWidget.js @@ -0,0 +1,1079 @@ +/** + * User Skills Widget + * + * Comprehensive skills tracking system with categories, levels, and XP progress. + * Features three sub-tabs, multiple view modes, and full CRUD operations. + * + * Data Model: + * skills: { + * version: 1, + * categories: { + * 'Combat': [{ name: 'Swordsmanship', level: 5, xp: 75, maxXP: 100 }, ...], + * 'Magic': [...] + * }, + * uncategorized: [...] + * } + */ + +import { parseItems, serializeItems } from '../../../utils/itemParser.js'; + +// Per-widget state storage (Map: widgetId => state) +const widgetStates = new Map(); + +/** + * Get or initialize widget state + */ +function getWidgetState(widgetId) { + if (!widgetStates.has(widgetId)) { + widgetStates.set(widgetId, { + activeSubTab: 'all', + viewModes: { + all: 'list', + categories: 'list', + quick: 'grid' + }, + collapsedCategories: [], + sortBy: 'level', // 'level', 'name', 'xp' + filterText: '' + }); + } + return widgetStates.get(widgetId); +} + +/** + * Migrate old string format to structured format + */ +function migrateSkillsData(oldSkills) { + // Already in new format + if (oldSkills && typeof oldSkills === 'object' && oldSkills.version) { + return oldSkills; + } + + // Old string format: "Swordsmanship, Lockpicking, Alchemy" + if (typeof oldSkills === 'string' && oldSkills.trim()) { + const skillNames = parseItems(oldSkills); + return { + version: 1, + categories: {}, + uncategorized: skillNames.map(name => ({ + name, + level: 1, + xp: 0, + maxXP: 100 + })) + }; + } + + // Empty or null + return { + version: 1, + categories: {}, + uncategorized: [] + }; +} + +/** + * Get all skills as flat array + */ +function getAllSkills(skillsData) { + const skills = []; + + // Add skills from categories + for (const [category, categorySkills] of Object.entries(skillsData.categories || {})) { + categorySkills.forEach(skill => { + skills.push({ ...skill, category }); + }); + } + + // Add uncategorized skills + (skillsData.uncategorized || []).forEach(skill => { + skills.push({ ...skill, category: null }); + }); + + return skills; +} + +/** + * Sort skills + */ +function sortSkills(skills, sortBy) { + const sorted = [...skills]; + + switch (sortBy) { + case 'level': + sorted.sort((a, b) => b.level - a.level || a.name.localeCompare(b.name)); + break; + case 'name': + sorted.sort((a, b) => a.name.localeCompare(b.name)); + break; + case 'xp': + sorted.sort((a, b) => { + const progressA = a.xp / a.maxXP; + const progressB = b.xp / b.maxXP; + return progressB - progressA || b.level - a.level; + }); + break; + } + + return sorted; +} + +/** + * Filter skills by search text + */ +function filterSkills(skills, filterText) { + if (!filterText.trim()) return skills; + + const search = filterText.toLowerCase(); + return skills.filter(skill => + skill.name.toLowerCase().includes(search) || + (skill.category && skill.category.toLowerCase().includes(search)) + ); +} + +/** + * Sanitize skill name + */ +function sanitizeSkillName(name) { + return name.trim().replace(/[<>]/g, '').slice(0, 100); +} + +/** + * Sanitize category name + */ +function sanitizeCategoryName(name) { + return name.trim().replace(/[<>]/g, '').slice(0, 50); +} + +/** + * Register User Skills Widget + */ +export function registerUserSkillsWidget(registry, dependencies) { + const { getExtensionSettings, onDataChange } = dependencies; + + registry.register('userSkills', { + name: 'User Skills', + icon: '⚔️', + description: 'Character skills with categories, levels, and XP tracking', + category: 'skills', + minSize: { w: 2, h: 4 }, + // Large widget like Inventory/Quests + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 6 }; // Mobile: 2 cols (full), 6 rows + } + return { w: 3, h: 7 }; // Desktop: 3 cols (full), 7 rows + }, + maxAutoSize: (columns) => { + if (columns <= 2) { + return { w: 2, h: 8 }; + } + return { w: 3, h: 10 }; + }, + requiresSchema: false, + + /** + * Render widget content + */ + render(container, config = {}) { + const settings = getExtensionSettings(); + const skillsConfig = settings.trackerConfig?.userStats?.skillsSection; + + // Check if skills tracking is enabled + if (!skillsConfig?.enabled) { + container.innerHTML = ` +
+ +

Skills tracking is disabled

+ Enable in Tracker Settings +
+ `; + return; + } + + // Migrate and get skills data + let skillsData = settings.userStats?.skills; + skillsData = migrateSkillsData(skillsData); + + // Save migrated data + if (!settings.userStats) settings.userStats = {}; + settings.userStats.skills = skillsData; + + // Get widget ID from container + const widgetId = container.closest('[data-widget-id]')?.dataset.widgetId || 'default'; + const state = getWidgetState(widgetId); + + // Build UI based on active sub-tab + const html = renderSkillsUI(skillsData, state, config, widgetId); + container.innerHTML = html; + + // Attach event handlers + attachSkillsHandlers(container, widgetId, dependencies, config); + }, + + /** + * Get widget configuration schema + */ + getConfig() { + return { + showXP: { + type: 'boolean', + label: 'Show XP Progress Bars', + default: true, + description: 'Display XP progress bars for each skill' + }, + showCategories: { + type: 'boolean', + label: 'Show Category Tags', + default: true, + description: 'Show category labels on skill cards' + }, + defaultSort: { + type: 'select', + label: 'Default Sort Order', + options: [ + { value: 'level', label: 'By Level (High to Low)' }, + { value: 'name', label: 'By Name (A-Z)' }, + { value: 'xp', label: 'By XP Progress' } + ], + default: 'level', + description: 'How to sort skills in All Skills view' + }, + maxLevel: { + type: 'number', + label: 'Maximum Skill Level', + default: 10, + min: 1, + max: 100, + description: 'Highest level a skill can reach' + } + }; + }, + + /** + * Handle widget resize + */ + onResize(container, newW, newH) { + // Add compact class for narrow widths + if (newW <= 2) { + container.classList.add('rpg-skills-compact'); + container.classList.remove('rpg-skills-wide'); + } else { + container.classList.add('rpg-skills-wide'); + container.classList.remove('rpg-skills-compact'); + } + } + }); +} + +/** + * Render skills UI + */ +function renderSkillsUI(skillsData, state, config, widgetId) { + const allSkills = getAllSkills(skillsData); + const hasSkills = allSkills.length > 0; + + let html = '
'; + + // Sub-tab navigation + html += renderSubTabs(state.activeSubTab); + + // Scrollable content area + html += '
'; + + // Content based on active tab + switch (state.activeSubTab) { + case 'all': + html += renderAllSkillsTab(skillsData, state, config); + break; + case 'categories': + html += renderCategoriesTab(skillsData, state, config); + break; + case 'quick': + html += renderQuickViewTab(skillsData, state, config); + break; + } + + html += '
'; // Close rpg-skills-views + html += '
'; // Close rpg-skills-widget + return html; +} + +/** + * Render sub-tab navigation + */ +function renderSubTabs(activeTab) { + const tabs = [ + { id: 'all', label: 'All', icon: 'fa-list' }, + { id: 'categories', label: 'By Category', icon: 'fa-folder-tree' }, + { id: 'quick', label: 'Quick', icon: 'fa-bolt' } + ]; + + let html = '
'; + tabs.forEach(tab => { + const active = tab.id === activeTab ? 'active' : ''; + html += ` + + `; + }); + html += '
'; + + return html; +} + +/** + * Render All Skills tab + */ +function renderAllSkillsTab(skillsData, state, config) { + const allSkills = getAllSkills(skillsData); + + let html = '
'; + + // Header with controls + html += ` +
+
+ + All Skills +
+
+ +
+ + +
+ +
+
+ `; + + // Search/filter + html += ` +
+ + +
+ `; + + // Add skill form (hidden by default) + html += renderAddSkillForm(skillsData); + + // Skills list/grid + if (allSkills.length === 0) { + html += ` +
+ +

No skills yet

+ Click the + button to add your first skill +
+ `; + } else { + let filtered = filterSkills(allSkills, state.filterText); + let sorted = sortSkills(filtered, state.sortBy); + + const viewMode = state.viewModes.all; + html += `
`; + sorted.forEach(skill => { + html += renderSkillCard(skill, config, viewMode); + }); + html += '
'; + + if (filtered.length === 0 && allSkills.length > 0) { + html += ` +
+ +

No skills match your search

+
+ `; + } + } + + html += '
'; + return html; +} + +/** + * Render By Category tab + */ +function renderCategoriesTab(skillsData, state, config) { + let html = '
'; + + // Header + html += ` +
+
+ + Skills by Category +
+
+
+ + +
+ +
+
+ `; + + // Add category form (hidden) + html += renderAddCategoryForm(); + + const viewMode = state.viewModes.categories; + const categories = Object.keys(skillsData.categories || {}).sort(); + const uncategorized = skillsData.uncategorized || []; + + if (categories.length === 0 && uncategorized.length === 0) { + html += ` +
+ +

No categories yet

+ Click the folder+ button to create a category +
+ `; + } else { + // Render categories + categories.forEach(category => { + html += renderCategory(category, skillsData.categories[category], state, config, viewMode); + }); + + // Render uncategorized + if (uncategorized.length > 0) { + html += renderCategory('Uncategorized', uncategorized, state, config, viewMode, true); + } + } + + html += '
'; + return html; +} + +/** + * Render Quick View tab + */ +function renderQuickViewTab(skillsData, state, config) { + const allSkills = getAllSkills(skillsData); + const topSkills = allSkills.sort((a, b) => b.level - a.level).slice(0, 12); + + let html = '
'; + + html += ` +
+
+ + Quick View +
+
+ `; + + html += `
+ + Showing your top skills for quick reference +
`; + + if (topSkills.length === 0) { + html += ` +
+ +

No skills to display

+ Add skills in the "All Skills" tab +
+ `; + } else { + html += '
'; + topSkills.forEach(skill => { + html += renderSkillCard(skill, config, 'quick'); + }); + html += '
'; + } + + html += '
'; + return html; +} + +/** + * Render category section + */ +function renderCategory(categoryName, skills, state, config, viewMode, isUncategorized = false) { + const isCollapsed = state.collapsedCategories.includes(categoryName); + + let html = '
'; + + // Category header + html += ` +
+ +
${categoryName}
+
${skills.length}
+ ${!isUncategorized ? ` + + + ` : ''} + +
+ `; + + // Category content + if (!isCollapsed) { + html += renderAddSkillForm(null, categoryName, true); + html += `
`; + skills.forEach(skill => { + html += renderSkillCard({ ...skill, category: isUncategorized ? null : categoryName }, config, viewMode); + }); + html += '
'; + } + + html += '
'; + return html; +} + +/** + * Render skill card + */ +function renderSkillCard(skill, config, viewMode) { + const xpPercent = (skill.xp / skill.maxXP) * 100; + const showXP = config.showXP !== false; + const showCategory = config.showCategories !== false && skill.category; + const isQuickView = viewMode === 'quick'; + + let html = `
`; + + // Skill info wrapper (name, level, XP bar) + html += '
'; + + // Header row with name and level + html += '
'; + html += `
${skill.name}
`; + html += `
Lv ${skill.level}
`; + html += '
'; + + // XP bar (if not quick view) + if (showXP && !isQuickView) { + html += ` +
+
+
${skill.xp}/${skill.maxXP} XP
+
+ `; + } + + html += '
'; // Close rpg-skill-info + + // Actions + html += '
'; + if (!isQuickView) { + html += ` + + + + `; + } else { + html += ` + + `; + } + html += '
'; + + html += '
'; + return html; +} + +/** + * Render add skill form + */ +function renderAddSkillForm(skillsData, targetCategory = null, isInCategory = false) { + const categories = skillsData ? Object.keys(skillsData.categories || {}).sort() : []; + + let html = `'; + return html; +} + +/** + * Render add category form + */ +function renderAddCategoryForm() { + let html = ''; + return html; +} + +/** + * Attach event handlers + */ +function attachSkillsHandlers(container, widgetId, dependencies, config) { + const { getExtensionSettings, onDataChange } = dependencies; + + // Check if handlers are already attached to prevent duplicate listeners + if (container.dataset.handlersAttached === 'true') { + return; + } + container.dataset.handlersAttached = 'true'; + + // Event delegation + container.addEventListener('click', (e) => { + const target = e.target.closest('[data-action]'); + if (!target) return; + + const action = target.dataset.action; + handleAction(action, target, container, widgetId, dependencies, config); + }); + + // Filter input + const filterInput = container.querySelector('.rpg-filter-input'); + if (filterInput) { + filterInput.addEventListener('input', (e) => { + const state = getWidgetState(widgetId); + state.filterText = e.target.value; + rerender(container, widgetId, dependencies, config); + }); + } + + // Sort select + const sortSelect = container.querySelector('.rpg-sort-select'); + if (sortSelect) { + sortSelect.addEventListener('change', (e) => { + const state = getWidgetState(widgetId); + state.sortBy = e.target.value; + rerender(container, widgetId, dependencies, config); + }); + } + + // Skill name editing + container.addEventListener('blur', (e) => { + if (e.target.hasAttribute('contenteditable') && e.target.dataset.action === 'edit-skill-name') { + const skillName = e.target.dataset.original; + const newName = sanitizeSkillName(e.target.textContent); + + if (newName && newName !== skillName) { + updateSkillName(skillName, e.target.closest('.rpg-skill-card').dataset.category, newName, dependencies); + rerender(container, widgetId, dependencies, config); + } else { + e.target.textContent = skillName; + } + } + }, true); + + // Keyboard shortcuts + container.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const target = e.target; + if (target.classList.contains('rpg-inline-input')) { + e.preventDefault(); + const saveBtn = target.closest('.rpg-inline-form').querySelector('.rpg-inline-save'); + if (saveBtn) saveBtn.click(); + } else if (target.hasAttribute('contenteditable')) { + e.preventDefault(); + target.blur(); + } + } else if (e.key === 'Escape') { + const target = e.target; + if (target.classList.contains('rpg-inline-input')) { + const cancelBtn = target.closest('.rpg-inline-form').querySelector('.rpg-inline-cancel'); + if (cancelBtn) cancelBtn.click(); + } else if (target.hasAttribute('contenteditable')) { + const original = target.dataset.original; + target.textContent = original; + target.blur(); + } + } + }); +} + +/** + * Handle actions + */ +function handleAction(action, target, container, widgetId, dependencies, config) { + const settings = dependencies.getExtensionSettings(); + const state = getWidgetState(widgetId); + + switch (action) { + case 'switch-tab': + state.activeSubTab = target.dataset.tab; + rerender(container, widgetId, dependencies, config); + break; + + case 'change-view': + state.viewModes[target.dataset.tab] = target.dataset.view; + rerender(container, widgetId, dependencies, config); + break; + + case 'show-add-skill': + showAddSkillForm(container); + break; + + case 'show-add-skill-to-category': + showAddSkillForm(container, target.dataset.category); + break; + + case 'cancel-add-skill': + hideAddSkillForm(container, target); + break; + + case 'save-add-skill': + saveNewSkill(container, target, dependencies); + rerender(container, widgetId, dependencies, config); + break; + + case 'show-add-category': + showAddCategoryForm(container); + break; + + case 'cancel-add-category': + hideAddCategoryForm(container); + break; + + case 'save-add-category': + saveNewCategory(container, dependencies); + rerender(container, widgetId, dependencies, config); + break; + + case 'toggle-category': + toggleCategory(target.dataset.category, state); + rerender(container, widgetId, dependencies, config); + break; + + case 'level-up': + levelUpSkill(target, dependencies); + rerender(container, widgetId, dependencies, config); + break; + + case 'level-down': + levelDownSkill(target, dependencies); + rerender(container, widgetId, dependencies, config); + break; + + case 'delete-skill': + deleteSkill(target, dependencies); + rerender(container, widgetId, dependencies, config); + break; + + case 'delete-category': + deleteCategory(target.dataset.category, dependencies); + rerender(container, widgetId, dependencies, config); + break; + } +} + +/** + * Show add skill form + */ +function showAddSkillForm(container, targetCategory = null) { + const form = targetCategory + ? container.querySelector(`.rpg-add-skill-form[data-target-category="${targetCategory}"]`) + : container.querySelector('.rpg-add-skill-form:not([data-target-category])'); + + if (form) { + form.style.display = 'block'; + const input = form.querySelector('input[data-field="name"]'); + if (input) input.focus(); + } +} + +/** + * Hide add skill form + */ +function hideAddSkillForm(container, cancelBtn) { + const form = cancelBtn.closest('.rpg-add-skill-form'); + if (form) { + form.style.display = 'none'; + form.querySelectorAll('input').forEach(input => input.value = ''); + } +} + +/** + * Save new skill + */ +function saveNewSkill(container, saveBtn, dependencies) { + const form = saveBtn.closest('.rpg-add-skill-form'); + const nameInput = form.querySelector('[data-field="name"]'); + const levelInput = form.querySelector('[data-field="level"]'); + const categorySelect = form.querySelector('[data-field="category"]'); + + const name = sanitizeSkillName(nameInput.value); + const level = parseInt(levelInput.value) || 1; + const category = form.dataset.targetCategory || (categorySelect ? categorySelect.value : null); + + if (!name) return; + + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + const newSkill = { + name, + level: Math.max(1, Math.min(100, level)), + xp: 0, + maxXP: 100 + }; + + if (category && category !== '' && category !== 'Uncategorized') { + if (!skillsData.categories[category]) { + skillsData.categories[category] = []; + } + skillsData.categories[category].push(newSkill); + } else { + skillsData.uncategorized.push(newSkill); + } + + saveSkillsData(settings, skillsData, dependencies); + + form.style.display = 'none'; + form.querySelectorAll('input').forEach(input => input.value = ''); +} + +/** + * Show add category form + */ +function showAddCategoryForm(container) { + const form = container.querySelector('.rpg-add-category-form'); + if (form) { + form.style.display = 'block'; + const input = form.querySelector('input'); + if (input) input.focus(); + } +} + +/** + * Hide add category form + */ +function hideAddCategoryForm(container) { + const form = container.querySelector('.rpg-add-category-form'); + if (form) { + form.style.display = 'none'; + form.querySelector('input').value = ''; + } +} + +/** + * Save new category + */ +function saveNewCategory(container, dependencies) { + const form = container.querySelector('.rpg-add-category-form'); + const input = form.querySelector('input'); + const name = sanitizeCategoryName(input.value); + + if (!name) return; + + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + if (!skillsData.categories[name]) { + skillsData.categories[name] = []; + saveSkillsData(settings, skillsData, dependencies); + } + + form.style.display = 'none'; + input.value = ''; +} + +/** + * Toggle category collapsed state + */ +function toggleCategory(categoryName, state) { + const index = state.collapsedCategories.indexOf(categoryName); + if (index >= 0) { + state.collapsedCategories.splice(index, 1); + } else { + state.collapsedCategories.push(categoryName); + } +} + +/** + * Level up skill + */ +function levelUpSkill(target, dependencies) { + const card = target.closest('.rpg-skill-card'); + const skillName = card.dataset.skill; + const category = card.dataset.category === 'Uncategorized' ? null : card.dataset.category; + + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + const skill = findSkill(skillsData, skillName, category); + if (skill) { + skill.level++; + skill.xp = 0; // Reset XP on level up + saveSkillsData(settings, skillsData, dependencies); + } +} + +/** + * Level down skill + */ +function levelDownSkill(target, dependencies) { + const card = target.closest('.rpg-skill-card'); + const skillName = card.dataset.skill; + const category = card.dataset.category === 'Uncategorized' ? null : card.dataset.category; + + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + const skill = findSkill(skillsData, skillName, category); + if (skill && skill.level > 1) { + skill.level--; + skill.xp = 0; // Reset XP on level change + saveSkillsData(settings, skillsData, dependencies); + } +} + +/** + * Delete skill + */ +function deleteSkill(target, dependencies) { + const card = target.closest('.rpg-skill-card'); + const skillName = card.dataset.skill; + const category = card.dataset.category === 'Uncategorized' ? null : card.dataset.category; + + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + removeSkill(skillsData, skillName, category); + saveSkillsData(settings, skillsData, dependencies); +} + +/** + * Delete category + */ +function deleteCategory(categoryName, dependencies) { + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + if (skillsData.categories[categoryName]) { + // Move skills to uncategorized + const skills = skillsData.categories[categoryName]; + skillsData.uncategorized.push(...skills); + delete skillsData.categories[categoryName]; + saveSkillsData(settings, skillsData, dependencies); + } +} + +/** + * Update skill name + */ +function updateSkillName(oldName, category, newName, dependencies) { + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + + const skill = findSkill(skillsData, oldName, category === 'Uncategorized' ? null : category); + if (skill) { + skill.name = newName; + saveSkillsData(settings, skillsData, dependencies); + } +} + +/** + * Find skill in data + */ +function findSkill(skillsData, name, category) { + if (category) { + const categorySkills = skillsData.categories[category]; + return categorySkills ? categorySkills.find(s => s.name === name) : null; + } else { + return skillsData.uncategorized.find(s => s.name === name); + } +} + +/** + * Remove skill from data + */ +function removeSkill(skillsData, name, category) { + if (category) { + const categorySkills = skillsData.categories[category]; + if (categorySkills) { + const index = categorySkills.findIndex(s => s.name === name); + if (index >= 0) categorySkills.splice(index, 1); + } + } else { + const index = skillsData.uncategorized.findIndex(s => s.name === name); + if (index >= 0) skillsData.uncategorized.splice(index, 1); + } +} + +/** + * Save skills data + */ +function saveSkillsData(settings, skillsData, dependencies) { + settings.userStats.skills = skillsData; + + if (dependencies.onDataChange) { + dependencies.onDataChange('userStats', 'skills', skillsData); + } +} + +/** + * Re-render widget + */ +function rerender(container, widgetId, dependencies, config) { + const settings = dependencies.getExtensionSettings(); + const skillsData = settings.userStats.skills; + const state = getWidgetState(widgetId); + + const html = renderSkillsUI(skillsData, state, config, widgetId); + container.innerHTML = html; + + attachSkillsHandlers(container, widgetId, dependencies, config); +} diff --git a/src/systems/dashboard/widgets/userStatsWidget.js b/src/systems/dashboard/widgets/userStatsWidget.js index f668596..78b1ac1 100644 --- a/src/systems/dashboard/widgets/userStatsWidget.js +++ b/src/systems/dashboard/widgets/userStatsWidget.js @@ -33,13 +33,19 @@ export function registerUserStatsWidget(registry, dependencies) { description: 'Health, energy, satiety bars', category: 'user', minSize: { w: 1, h: 2 }, - defaultSize: { w: 2, h: 2 }, - // Column-aware max size: full width in 3-4 col for horizontal spread + // Column-aware sizing: narrow and tall at 2 cols, wider at 3+ cols + defaultSize: (columns) => { + if (columns <= 2) { + return { w: 1, h: 3 }; // Mobile: 1 col wide, 3 rows tall + } + return { w: 2, h: 3 }; // Desktop: 2 cols wide, 3 rows tall + }, + // Column-aware max size: same as default to prevent expansion maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 2 }; // Mobile: use full 2-col width + return { w: 1, h: 3 }; // Mobile: 1x3 } - return { w: 3, h: 3 }; // Desktop: span 3 columns horizontally + return { w: 2, h: 3 }; // Desktop: 2x3 }, requiresSchema: false, diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 3a6d020..7f01ee5 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -56,6 +56,10 @@ function separateEmojiFromText(str) { function stripBrackets(text) { if (!text) return text; + const originalLength = text.length; + debugLog('[RPG Parser] stripBrackets: Input length:', originalLength); + debugLog('[RPG Parser] stripBrackets: Contains "Skills:":', text.includes('Skills:')); + // Remove leading and trailing whitespace first text = text.trim(); @@ -67,6 +71,7 @@ function stripBrackets(text) { (text.startsWith('(') && text.endsWith(')')) ) { text = text.substring(1, text.length - 1).trim(); + debugLog('[RPG Parser] stripBrackets: Removed wrapping brackets, new length:', text.length); } // Remove placeholder text patterns like [Location], [Mood Emoji], [Name], etc. @@ -102,23 +107,103 @@ function stripBrackets(text) { }; // Replace placeholders with empty string, keep real content + let removedPlaceholders = []; text = text.replace(placeholderPattern, (match, content) => { if (isPlaceholder(match, content)) { + removedPlaceholders.push(match); return ''; // Remove placeholder } return match; // Keep real bracketed content }); + if (removedPlaceholders.length > 0) { + debugLog('[RPG Parser] stripBrackets: Removed placeholders:', removedPlaceholders.join(', ')); + } // Clean up any resulting empty labels (e.g., "Status: " with nothing after) - text = text.replace(/^([A-Za-z\s]+):\s*$/gm, ''); // Remove lines that are just "Label: " with nothing + // BUT: Don't remove structural section headers that have content on following lines + const beforeCleanup = text.length; + + // Known section headers that should NEVER be removed (structural markers) + const structuralHeaders = ['Skills', 'Status', 'Inventory', 'On Person', 'Stored', 'Assets', 'Main Quest', 'Main Quests', 'Optional Quest', 'Optional Quests']; + + // Split into lines to intelligently remove only truly empty labels + const lines = text.split('\n'); + const filteredLines = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Check if this is a label line (ends with colon, no other content) + const labelMatch = trimmedLine.match(/^([A-Za-z\s]+):\s*$/); + + if (labelMatch) { + const labelName = labelMatch[1]; + + // Never remove structural section headers + if (structuralHeaders.includes(labelName)) { + debugLog('[RPG Parser] stripBrackets: Keeping structural header:', trimmedLine); + filteredLines.push(line); + continue; + } + + // Check if there's ANY content in the next few lines (look ahead up to 3 lines) + let hasContentBelow = false; + for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) { + const futureLine = lines[j].trim(); + if (futureLine === '') continue; // Skip empty lines + + // If we find a line with content (not just another label), this label has content below + if (futureLine && !/^([A-Za-z\s]+):\s*$/.test(futureLine)) { + hasContentBelow = true; + break; + } + } + + if (hasContentBelow) { + // This label has content below (even if through other labels), keep it + debugLog('[RPG Parser] stripBrackets: Keeping section header:', trimmedLine); + filteredLines.push(line); + } else { + // This is a truly empty label with no content anywhere below, remove it + debugLog('[RPG Parser] stripBrackets: Removing empty label:', trimmedLine); + } + } else { + // Not a label line, keep it + filteredLines.push(line); + } + } + + text = filteredLines.join('\n'); + + if (text.length !== beforeCleanup) { + debugLog('[RPG Parser] stripBrackets: Removed empty labels, chars removed:', beforeCleanup - text.length); + } + text = text.replace(/^([A-Za-z\s]+):\s*,/gm, '$1:'); // Fix "Label: ," patterns text = text.replace(/:\s*\|/g, ':'); // Fix ": |" patterns text = text.replace(/\|\s*\|/g, '|'); // Fix "| |" patterns (double pipes from removed content) text = text.replace(/\|\s*$/gm, ''); // Remove trailing pipes at end of lines // Clean up multiple spaces and empty lines + const beforeSpaceCleanup = text.length; text = text.replace(/\s{2,}/g, ' '); // Multiple spaces to single space text = text.replace(/^\s*\n/gm, ''); // Remove empty lines + if (text.length !== beforeSpaceCleanup) { + debugLog('[RPG Parser] stripBrackets: Cleaned up spaces/newlines, chars removed:', beforeSpaceCleanup - text.length); + } + + const finalLength = text.trim().length; + debugLog('[RPG Parser] stripBrackets: Output length:', finalLength); + debugLog('[RPG Parser] stripBrackets: Total chars removed:', originalLength - finalLength); + debugLog('[RPG Parser] stripBrackets: Contains "Skills:" after processing:', text.includes('Skills:')); + + if (text.includes('Skills:')) { + const skillsIndex = text.indexOf('Skills:'); + debugLog('[RPG Parser] stripBrackets: Text around Skills (index ' + skillsIndex + '):', text.substring(skillsIndex, skillsIndex + 200)); + } else if (originalLength !== finalLength) { + debugLog('[RPG Parser] stripBrackets: WARNING - Skills section was removed! Last 200 chars:', text.substring(text.length - 200)); + } return text.trim(); } @@ -133,6 +218,164 @@ function debugLog(message, data = null) { } } +/** + * Extract structured skills data from stats text + * Parses format: + * Skills: + * CategoryName: + * - SkillName (Lv X) + * - SkillName (Lv X) + * Uncategorized: + * - SkillName (Lv X) + * + * @param {string} statsText - Stats section text containing skills + * @returns {Object|null} Structured skills data or null if not found + */ +function extractSkills(statsText) { + if (!statsText) { + debugLog('[RPG Parser] extractSkills: No stats text provided'); + return null; + } + + debugLog('[RPG Parser] extractSkills: Searching for Skills section in text length:', statsText.length); + debugLog('[RPG Parser] extractSkills: Text contains "Skills:":', statsText.includes('Skills:')); + + // Find the Skills section + const skillsMatch = statsText.match(/Skills:([\s\S]*?)(?=\n\n|On Person:|Stored|Assets:|Main Quest|Optional Quest|$)/i); + if (!skillsMatch) { + debugLog('[RPG Parser] extractSkills: Main regex did not match'); + debugLog('[RPG Parser] extractSkills: Checking if "On Person:" exists:', statsText.includes('On Person:')); + debugLog('[RPG Parser] extractSkills: Text around Skills:', statsText.substring(statsText.indexOf('Skills:'), statsText.indexOf('Skills:') + 200)); + + // Fallback: try simple format "Skills: skill1, skill2" + const simpleMatch = statsText.match(/Skills:\s*(.+)/i); + if (simpleMatch) { + const skillsText = simpleMatch[1].trim(); + debugLog('[RPG Parser] extractSkills: Simple format matched:', skillsText); + if (skillsText && skillsText !== 'None') { + // Return as string for backward compatibility + return skillsText; + } + } + debugLog('[RPG Parser] extractSkills: No Skills section found'); + return null; + } + + debugLog('[RPG Parser] extractSkills: Main regex matched, captured length:', skillsMatch[1].length); + + const skillsSection = skillsMatch[1]; + const skillsData = { + version: 1, + categories: {}, + uncategorized: [] + }; + + // Split into lines and process + const lines = skillsSection.split('\n').map(line => line.trim()).filter(line => line); + + debugLog('[RPG Parser] Skills section lines:', lines); + + let currentCategory = null; + + for (const line of lines) { + // Check if this is a category header (ends with colon, no dash) + if (line.endsWith(':') && !line.startsWith('-')) { + currentCategory = line.slice(0, -1).trim(); + debugLog(`[RPG Parser] Found category header: "${currentCategory}"`); + if (currentCategory !== 'Uncategorized' && !skillsData.categories[currentCategory]) { + skillsData.categories[currentCategory] = []; + debugLog(`[RPG Parser] Created category array for: "${currentCategory}"`); + } + continue; + } + + // Check if this is a skill line (starts with -, has level info) + // Try numeric format first: "- Skill Name (Lv 5)" + let skillMatch = line.match(/^-\s*(.+?)\s*\(Lv\s*(\d+)\)/i); + if (skillMatch) { + const skillName = skillMatch[1].trim(); + const level = parseInt(skillMatch[2], 10) || 1; + + const skill = { + name: skillName, + level: level, + xp: 0, + maxXP: 100 + }; + + if (currentCategory === 'Uncategorized' || currentCategory === null) { + debugLog(`[RPG Parser] Adding "${skillName}" to uncategorized (currentCategory="${currentCategory}")`); + skillsData.uncategorized.push(skill); + } else if (currentCategory && skillsData.categories[currentCategory]) { + debugLog(`[RPG Parser] Adding "${skillName}" to category "${currentCategory}"`); + skillsData.categories[currentCategory].push(skill); + } else { + debugLog(`[RPG Parser] ERROR: Could not add "${skillName}" - currentCategory="${currentCategory}", categoryExists=${!!skillsData.categories[currentCategory]}`); + // Fallback to uncategorized if category doesn't exist + skillsData.uncategorized.push(skill); + } + } else { + // Fallback: Try text-based proficiency format: "- Skill Name (Proficient)" + const textMatch = line.match(/^-\s*(.+?)\s*\((.+?)\)/i); + if (textMatch) { + const skillName = textMatch[1].trim(); + const proficiencyText = textMatch[2].trim().toLowerCase(); + + // Map text proficiency to numeric level + const proficiencyMap = { + 'initiated': 1, + 'novice': 1, + 'basic': 2, + 'beginner': 2, + 'intermediate': 4, + 'proficient': 5, + 'competent': 6, + 'advanced': 7, + 'expert': 8, + 'mastered': 9, + 'master': 9, + 'grandmaster': 10, + 'legendary': 10 + }; + + const level = proficiencyMap[proficiencyText] || 5; // Default to 5 if unknown + + const skill = { + name: skillName, + level: level, + xp: 0, + maxXP: 100 + }; + + if (currentCategory === 'Uncategorized' || currentCategory === null) { + debugLog(`[RPG Parser] Adding "${skillName}" to uncategorized (currentCategory="${currentCategory}")`); + skillsData.uncategorized.push(skill); + } else if (currentCategory && skillsData.categories[currentCategory]) { + debugLog(`[RPG Parser] Adding "${skillName}" to category "${currentCategory}"`); + skillsData.categories[currentCategory].push(skill); + } else { + debugLog(`[RPG Parser] ERROR: Could not add "${skillName}" - currentCategory="${currentCategory}", categoryExists=${!!skillsData.categories[currentCategory]}`); + // Fallback to uncategorized if category doesn't exist + skillsData.uncategorized.push(skill); + } + } + } + } + + // Return null if no skills were found + if (Object.keys(skillsData.categories).length === 0 && skillsData.uncategorized.length === 0) { + return null; + } + + debugLog('[RPG Parser] Final skills data:', { + categories: Object.keys(skillsData.categories), + categoryCounts: Object.entries(skillsData.categories).map(([cat, skills]) => `${cat}: ${skills.length}`), + uncategorizedCount: skillsData.uncategorized.length + }); + + return skillsData; +} + /** * Parses the model response to extract the different data sections. * Extracts tracker data from markdown code blocks in the AI response. @@ -170,7 +413,13 @@ export function parseResponse(responseText) { const content = match[1].trim(); debugLog(`[RPG Parser] --- Code Block ${i + 1} ---`); + debugLog('[RPG Parser] Content length:', content.length); debugLog('[RPG Parser] First 300 chars:', content.substring(0, 300)); + debugLog('[RPG Parser] Contains "Skills:":', content.includes('Skills:')); + if (content.includes('Skills:')) { + const skillsIndex = content.indexOf('Skills:'); + debugLog('[RPG Parser] Text around Skills (index ' + skillsIndex + '):', content.substring(skillsIndex, skillsIndex + 200)); + } // Check if this is a combined code block with multiple sections const hasMultipleSections = ( @@ -351,10 +600,12 @@ export function parseUserStats(statsText) { // Parse skills section if enabled const skillsConfig = trackerConfig?.userStats?.skillsSection; if (skillsConfig?.enabled) { - const skillsMatch = statsText.match(/Skills:\s*(.+)/i); - if (skillsMatch) { - extensionSettings.userStats.skills = skillsMatch[1].trim(); - debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim()); + const skillsData = extractSkills(statsText); + if (skillsData) { + extensionSettings.userStats.skills = skillsData; + debugLog('[RPG Parser] Skills extracted:', skillsData); + } else { + debugLog('[RPG Parser] Skills extraction failed or none found'); } } diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 62be66c..17f1547 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -10,6 +10,53 @@ import { extensionSettings, committedTrackerData, FEATURE_FLAGS } from '../../co // Type imports /** @typedef {import('../../types/inventory.js').InventoryV2} InventoryV2 */ +/** + * Builds a formatted skills summary for AI context injection. + * Converts structured skills data to multi-line plaintext format organized by category. + * + * @param {Object|string} skills - Current skills (structured or legacy string) + * @returns {string} Formatted skills summary for prompt injection + * @example + * // Structured input: { version: 1, categories: { Combat: [{name: 'Swordsmanship', level: 5}] }, uncategorized: [] } + * // Returns: "Skills:\nCombat:\n- Swordsmanship (Lv 5)" + */ +export function buildSkillsSummary(skills) { + // Handle legacy string format + if (typeof skills === 'string') { + return `Skills: ${skills}`; + } + + // Handle structured format + if (skills && typeof skills === 'object' && skills.version) { + let summary = 'Skills:'; + const categories = skills.categories || {}; + const uncategorized = skills.uncategorized || []; + + // Add categorized skills + for (const [categoryName, skillsList] of Object.entries(categories)) { + if (skillsList && skillsList.length > 0) { + summary += `\n${categoryName}:`; + for (const skill of skillsList) { + summary += `\n- ${skill.name} (Lv ${skill.level})`; + } + } + } + + // Add uncategorized skills + if (uncategorized.length > 0) { + summary += '\nUncategorized:'; + for (const skill of uncategorized) { + summary += `\n- ${skill.name} (Lv ${skill.level})`; + } + } + + return summary; + } + + // Empty or invalid + return 'Skills: None'; +} + /** * Builds a formatted inventory summary for AI context injection. * Converts v2 inventory structure to multi-line plaintext format. @@ -166,9 +213,13 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon // Add skills section if enabled if (userStatsConfig?.skillsSection?.enabled) { - const skillFields = userStatsConfig.skillsSection.customFields || []; - const skillFieldsText = skillFields.map(f => `[${f}]`).join(', '); - instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`; + instructions += `Skills:\n`; + instructions += `[Category Name]:\n`; + instructions += `- [Skill Name] (Lv [1-100])\n`; + instructions += `- [Another Skill] (Lv [1-100])\n`; + instructions += `Uncategorized:\n`; + instructions += `- [Uncategorized Skill] (Lv [1-100])\n`; + instructions += `(Organize skills by logical categories like Combat, Magic, Social, Crafting, etc. IMPORTANT: Use numeric levels only - write "Lv 5" not "Proficient", "Lv 7" not "Advanced". Use integers 1-100 where 1=novice, 5=intermediate, 10=expert. Skills without a clear category go in Uncategorized.)\n`; } // Add inventory format based on feature flag diff --git a/style.css b/style.css index ee88288..bca3d5d 100644 --- a/style.css +++ b/style.css @@ -1938,7 +1938,7 @@ body:has(.rpg-panel.rpg-position-left) #sheld { /* User info widget - avatar background with text overlay */ .rpg-user-info-container { display: flex; - align-items: center; + align-items: flex-end; justify-content: center; height: 100%; width: 100%; @@ -1963,20 +1963,71 @@ body:has(.rpg-panel.rpg-position-left) #sheld { z-index: 1; } -/* Text container with backdrop */ -.rpg-user-info-text { - display: flex; - flex-direction: column; - gap: 0.2rem; - align-items: center; - text-align: center; +/* Round avatar image (used in 1x1 compact mode, hidden by default) */ +.rpg-user-avatar-img { + display: none; + position: absolute; + width: 75%; + height: 75%; + object-fit: cover; + border-radius: 50%; + z-index: 0; +} + +/* Name and level containers - base styles */ +.rpg-user-name-container, +.rpg-user-level-container { position: relative; z-index: 2; - padding: 0.5rem 0.75rem; - background: rgba(0, 0, 0, 0.5); + padding: 0.3rem 0.6rem; + background: rgba(0, 0, 0, 0.15); backdrop-filter: blur(4px); border-radius: 0.375rem; border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + align-items: center; + justify-content: center; +} + +/* WIDE LAYOUT (2x1+): Horizontal split over avatar background */ +.rpg-user-info-wide .rpg-user-avatar-img { + display: none; +} + +.rpg-user-info-wide .rpg-user-info-container::before { + display: block; +} + +.rpg-user-info-wide .rpg-user-name-container { + position: absolute; + left: 0.5rem; + top: 50%; + transform: translateY(-50%); + max-width: 45%; + padding: 0.2rem 0.4rem; +} + +.rpg-user-info-wide .rpg-user-level-container { + position: absolute; + right: 0.5rem; + top: 50%; + transform: translateY(-50%); + max-width: 35%; + padding: 0.2rem 0.4rem; +} + +/* Smaller text for wide layout to prevent overlap */ +.rpg-user-info-wide .rpg-user-name { + font-size: 0.7rem; +} + +.rpg-user-info-wide .rpg-level-label { + font-size: 0.6rem; +} + +.rpg-user-info-wide .rpg-level-value { + font-size: 0.65rem; + padding: 0.1rem 0.3rem; } /* User name */ @@ -2029,27 +2080,89 @@ body:has(.rpg-panel.rpg-position-left) #sheld { background: var(--rpg-bg); } -/* Compact mode for narrow widths (< 3 grid units) */ +/* COMPACT LAYOUT (1x1): Round avatar with bottom nameplate */ .rpg-user-info-compact { - padding: 0.25rem !important; + align-items: center; + justify-content: center; + padding: 0 !important; } -.rpg-user-info-compact .rpg-user-info-text { - gap: 0.15rem !important; - padding: 0.35rem 0.5rem !important; +/* Hide background image and overlay in 1x1 mode */ +.rpg-user-info-compact { + background-image: none !important; +} + +.rpg-user-info-compact::before { + display: none; +} + +/* Show round avatar image - proper circle */ +.rpg-user-info-compact .rpg-user-avatar-img { + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 100%; + height: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + object-position: center; + border-radius: 50%; +} + +/* Name container at bottom - flush, no top/bottom padding on widget */ +.rpg-user-info-compact .rpg-user-name-container { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 0.2rem 0.3rem; + border-radius: 0; } .rpg-user-info-compact .rpg-user-name { - font-size: 0.75rem !important; + font-size: 0.65rem; + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Level container at top-right - flush with container edge */ +.rpg-user-info-compact .rpg-user-level-container { + position: absolute; + top: 0; + right: 0; + padding: 0.2rem 0.3rem; + border-radius: 0; + background: transparent; + border: none; + backdrop-filter: none; +} + +.rpg-user-info-compact .rpg-user-level { + display: flex; + align-items: center; + gap: 0.2rem; } .rpg-user-info-compact .rpg-level-label { - font-size: 0.65rem !important; + font-size: 0.55rem; + font-weight: 600; + color: var(--rpg-text); + opacity: 0.7; } .rpg-user-info-compact .rpg-level-value { - font-size: 0.75rem !important; - padding: 0.1rem 0.3rem !important; + font-size: 0.65rem; + font-weight: 700; + color: var(--rpg-highlight); + padding: 0.1rem 0.3rem; + background: rgba(0, 0, 0, 0.3); + border-radius: 0.25rem; + min-width: 1.2rem; + text-align: center; } /* Stat bars - rem for text, vh for bar height */ @@ -2663,6 +2776,72 @@ body:has(.rpg-panel.rpg-position-left) #sheld { opacity: 1; } +/* Widget Disabled State */ +.rpg-widget-disabled { + position: relative; + opacity: 0.6; + pointer-events: none; +} + +.rpg-widget-disabled-message { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + padding: 1rem; + pointer-events: all; + z-index: 10; + width: 80%; +} + +.rpg-widget-disabled-message i.fa-circle-info { + font-size: 2rem; + color: var(--rpg-highlight); + margin-bottom: 0.5rem; + display: block; + opacity: 0.8; +} + +.rpg-widget-disabled-message p { + color: var(--rpg-text); + font-size: 0.875rem; + margin: 0.5rem 0; + opacity: 0.9; + line-height: 1.4; +} + +.rpg-widget-enable-btn { + background: var(--rpg-accent); + color: white; + border: none; + border-radius: 0.375rem; + padding: 0.5rem 1rem; + font-size: 0.8125rem; + cursor: pointer; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin-top: 0.5rem; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +.rpg-widget-enable-btn:hover { + background: var(--rpg-highlight); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); +} + +.rpg-widget-enable-btn:active { + transform: translateY(0); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.rpg-widget-enable-btn i { + font-size: 1rem; +} + /* ============================================================================ Scene Info Grid Widget Compact information-dense layout showing all scene data at once @@ -4257,6 +4436,82 @@ body:has(.rpg-panel.rpg-position-left) #sheld { color: var(--rpg-highlight); } +/* Apply theme colors to skills subtabs */ +.rpg-panel[data-theme="sci-fi"] .rpg-skills-subtabs, +.rpg-panel[data-theme="fantasy"] .rpg-skills-subtabs, +.rpg-panel[data-theme="cyberpunk"] .rpg-skills-subtabs { + border-bottom-color: var(--rpg-border); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-skills-subtab, +.rpg-panel[data-theme="fantasy"] .rpg-skills-subtab, +.rpg-panel[data-theme="cyberpunk"] .rpg-skills-subtab { + border-color: var(--rpg-border); + color: var(--rpg-text); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-skills-subtab:hover, +.rpg-panel[data-theme="fantasy"] .rpg-skills-subtab:hover, +.rpg-panel[data-theme="cyberpunk"] .rpg-skills-subtab:hover { + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-skills-subtab.active, +.rpg-panel[data-theme="fantasy"] .rpg-skills-subtab.active, +.rpg-panel[data-theme="cyberpunk"] .rpg-skills-subtab.active { + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + +/* Apply theme colors to skill cards */ +.rpg-panel[data-theme="sci-fi"] .rpg-skill-card, +.rpg-panel[data-theme="fantasy"] .rpg-skill-card, +.rpg-panel[data-theme="cyberpunk"] .rpg-skill-card { + border-color: var(--rpg-border); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-skill-card:hover, +.rpg-panel[data-theme="fantasy"] .rpg-skill-card:hover, +.rpg-panel[data-theme="cyberpunk"] .rpg-skill-card:hover { + border-color: var(--rpg-highlight); +} + +/* Apply theme colors to category headers */ +.rpg-panel[data-theme="sci-fi"] .rpg-category-header, +.rpg-panel[data-theme="fantasy"] .rpg-category-header, +.rpg-panel[data-theme="cyberpunk"] .rpg-category-header { + background: var(--rpg-highlight); + border-color: var(--rpg-border); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-category-name, +.rpg-panel[data-theme="fantasy"] .rpg-category-name, +.rpg-panel[data-theme="cyberpunk"] .rpg-category-name { + color: var(--rpg-text); +} + +/* Apply theme colors to XP bars */ +.rpg-panel[data-theme="sci-fi"] .rpg-xp-bar, +.rpg-panel[data-theme="fantasy"] .rpg-xp-bar, +.rpg-panel[data-theme="cyberpunk"] .rpg-xp-bar { + border-color: var(--rpg-border); +} + +.rpg-panel[data-theme="sci-fi"] .rpg-xp-fill, +.rpg-panel[data-theme="fantasy"] .rpg-xp-fill, +.rpg-panel[data-theme="cyberpunk"] .rpg-xp-fill { + background: linear-gradient(90deg, var(--rpg-highlight), var(--rpg-accent)); +} + +/* Apply theme colors to skills add button */ +.rpg-panel[data-theme="sci-fi"] .rpg-skills-add-btn, +.rpg-panel[data-theme="fantasy"] .rpg-skills-add-btn, +.rpg-panel[data-theme="cyberpunk"] .rpg-skills-add-btn { + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + /* Apply theme colors to storage locations */ .rpg-panel[data-theme="sci-fi"] .rpg-storage-location, .rpg-panel[data-theme="fantasy"] .rpg-storage-location, @@ -5005,25 +5260,35 @@ body:has(.rpg-panel.rpg-position-left) #sheld { align-items: center; gap: 0.5em; padding: 0.5em; - background: var(--rpg-accent); - border: 1px solid var(--rpg-border); + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); border-radius: 0.375em; } -.rpg-stat-toggle { +.rpg-stat-toggle, +.rpg-attr-toggle { flex-shrink: 0; } -.rpg-stat-name { +.rpg-stat-name, +.rpg-attr-name { flex: 1; padding: 0.375em 0.5em; - background: var(--rpg-bg); - border: 1px solid var(--rpg-border); + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); border-radius: 0.25em; - color: var(--rpg-text); + color: var(--SmartThemeBodyColor); font-size: 0.95em; } +.rpg-stat-name:focus, +.rpg-attr-name:focus { + outline: none; + border-color: var(--rpg-highlight); +} + +.rpg-stat-remove, +.rpg-attr-remove { .rpg-stat-remove, .rpg-attr-remove, .rpg-remove-relationship { @@ -7614,6 +7879,648 @@ body:has(.rpg-panel.rpg-position-left) #sheld { color: var(--rpg-highlight); } +/* ============================================ + SKILLS WIDGET STYLES + ============================================ */ + +/* Skills Widget - Flex container for proper scrolling */ +.rpg-skills-widget { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +/* Skills Views - Scrollable content area */ +.rpg-skills-views { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +/* Skills Sub-tabs Navigation */ +.rpg-skills-subtabs { + display: flex; + gap: 0.5rem; + border-bottom: 2px solid var(--SmartThemeBorderColor); + padding-bottom: 0.5rem; + overflow-x: auto; + overflow-y: hidden; + flex-wrap: nowrap; + scrollbar-width: thin; + scrollbar-color: var(--SmartThemeBorderColor) transparent; +} + +.rpg-skills-subtabs::-webkit-scrollbar { + height: 6px; +} + +.rpg-skills-subtabs::-webkit-scrollbar-track { + background: transparent; +} + +.rpg-skills-subtabs::-webkit-scrollbar-thumb { + background: var(--SmartThemeBorderColor); + border-radius: 3px; +} + +.rpg-skills-subtabs::-webkit-scrollbar-thumb:hover { + background: var(--rpg-accent); +} + +.rpg-skills-subtab { + flex: 1; + min-width: fit-content; + white-space: nowrap; + padding: 0.5rem 1rem; + background: transparent; + border: 2px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + color: var(--SmartThemeBodyColor); + cursor: pointer; + transition: all 0.2s ease; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.rpg-skills-subtab i { + font-size: 1rem; +} + +.rpg-skills-subtab:hover { + background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1); + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + +.rpg-skills-subtab.active { + background: transparent; + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); + font-weight: 600; +} + +/* Skills Sections */ +.rpg-skills-section { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.rpg-skills-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--SmartThemeBorderColor); + gap: 0.5rem; + overflow-x: auto; + overflow-y: hidden; + flex-wrap: nowrap; + scrollbar-width: thin; + scrollbar-color: var(--SmartThemeBorderColor) transparent; +} + +.rpg-skills-header::-webkit-scrollbar { + height: 6px; +} + +.rpg-skills-header::-webkit-scrollbar-track { + background: transparent; +} + +.rpg-skills-header::-webkit-scrollbar-thumb { + background: var(--SmartThemeBorderColor); + border-radius: 3px; +} + +.rpg-skills-header::-webkit-scrollbar-thumb:hover { + background: var(--rpg-accent); +} + +.rpg-skills-header h4 { + margin: 0; + font-size: 1.1rem; + color: var(--SmartThemeBodyColor); + white-space: nowrap; + min-width: fit-content; +} + +.rpg-skills-content { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +/* Skills Add Button */ +.rpg-skills-add-btn { + padding: 0.4rem 0.75rem; + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + background: transparent; + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); + white-space: nowrap; + min-width: fit-content; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.85rem; + display: flex; + align-items: center; + gap: 0.35rem; +} + +.rpg-skills-add-btn:hover { + background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1); + border-color: var(--rpg-highlight); +} + +/* Skills Empty State */ +.rpg-skills-empty { + padding: 2rem; + text-align: center; + color: var(--SmartThemeFastUISliderColColor); + font-style: italic; + font-size: 0.9rem; +} + +/* Skills Filter */ +.rpg-skills-filter { + margin-bottom: 0.75rem; +} + +.rpg-filter-input { + width: 100%; + padding: 0.5rem 0.75rem; + background: var(--SmartThemeBlurTintColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + color: var(--SmartThemeBodyColor); + font-size: 0.9rem; + font-family: inherit; +} + +.rpg-filter-input:focus { + outline: none; + border-color: var(--ac-style-color-matchedText); + box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2); +} + +.rpg-filter-input::placeholder { + color: var(--SmartThemeFastUISliderColColor); + font-style: italic; +} + +/* Category Headers (Collapsible) */ +.rpg-category-header { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background: var(--SmartThemeQuoteColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + cursor: pointer; + margin-top: 0.5rem; +} + +.rpg-category-toggle { + background: none; + border: none; + color: var(--SmartThemeBodyColor); + cursor: pointer; + padding: 0.25rem; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.2s ease; +} + +.rpg-category-toggle:hover { + color: var(--ac-style-color-matchedText); +} + +.rpg-category-toggle i { + transition: transform 0.2s ease; +} + +.rpg-category-header.collapsed .rpg-category-toggle i { + transform: rotate(-90deg); +} + +.rpg-category-name { + flex: 1; + margin: 0; + font-size: 1rem; + font-weight: 600; + color: #000000; +} + +.rpg-category-actions { + display: flex; + gap: 0.5rem; +} + +.rpg-category-action { + padding: 0.35rem 0.6rem; + background: var(--SmartThemeBlurTintColor); + border: 2px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + color: var(--SmartThemeFastUISliderColColor); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; + display: flex; + align-items: center; + justify-content: center; +} + +.rpg-category-action:hover { + background: var(--SmartThemeBlurTintColor); + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + +.rpg-category-content { + margin-top: 0.75rem; +} + +.rpg-category-header.collapsed + .rpg-category-content { + display: none; +} + +/* Skill Cards - List and Grid Views */ +.rpg-skills-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-height: 2rem; + padding: 0.5rem 0; +} + +.rpg-skill-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 0.75rem 1rem; + background: transparent; + border: 2px solid var(--rpg-highlight); + border-radius: 0.25rem; + color: var(--SmartThemeBodyColor); + font-size: 0.95rem; + transition: all 0.2s ease; +} + +.rpg-skill-card:hover { + border-color: var(--rpg-highlight); + background: rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.1); +} + +.rpg-skill-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.35rem; + min-width: 0; +} + +.rpg-skill-header-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.rpg-skill-name { + font-weight: 500; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: text; +} + +.rpg-skill-name.rpg-editable { + border-bottom: 1px dashed var(--SmartThemeBorderColor); + transition: all 0.2s ease; +} + +.rpg-skill-name.rpg-editable:hover { + border-bottom-color: var(--ac-style-color-matchedText); +} + +.rpg-skill-name.rpg-editable:focus { + outline: none; + border-bottom-color: var(--ac-style-color-matchedText); + background: var(--SmartThemeQuoteColor); +} + +.rpg-skill-level { + font-size: 0.85rem; + color: var(--rpg-highlight); + font-weight: 600; + white-space: nowrap; +} + +.rpg-skill-actions { + display: flex; + gap: 0.5rem; + align-items: center; + flex-shrink: 0; +} + +.rpg-skill-action { + padding: 0.3rem 0.6rem; + background: transparent; + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + color: var(--SmartThemeBodyColor); + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.85rem; + white-space: nowrap; +} + +.rpg-skill-action:hover { + background: var(--ac-style-color-matchedText); + border-color: var(--ac-style-color-matchedText); + color: white; +} + +.rpg-skill-action.rpg-level-up-btn { + border-color: var(--rpg-highlight); + color: var(--rpg-highlight); +} + +.rpg-skill-action.rpg-level-up-btn:hover { + background: var(--rpg-highlight); + color: white; +} + +.rpg-skill-action.rpg-level-down-btn { + border-color: var(--rpg-accent); + color: var(--rpg-accent); +} + +.rpg-skill-action.rpg-level-down-btn:hover { + background: var(--rpg-accent); + color: white; +} + +.rpg-skill-action.rpg-delete-btn { + color: var(--SmartThemeFastUISliderColColor); +} + +.rpg-skill-action.rpg-delete-btn:hover { + background: #dc3545; + border-color: #dc3545; + color: white; +} + +/* XP Progress Bar */ +.rpg-xp-bar { + position: relative; + width: 100%; + height: 1.25rem; + background: var(--SmartThemeBlurTintColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + overflow: hidden; +} + +.rpg-xp-fill { + position: absolute; + top: 0; + left: 0; + height: 100%; + background: linear-gradient(90deg, + var(--rpg-highlight), + rgba(var(--rpg-highlight-rgb, 233, 69, 96), 0.7)); + transition: width 0.3s ease; + border-radius: 0.25rem 0 0 0.25rem; +} + +.rpg-xp-text { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; + color: var(--SmartThemeBodyColor); + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +/* Grid View for Skills */ +.rpg-skills-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 0.75rem; + padding: 0.5rem 0; +} + +.rpg-skills-grid .rpg-skill-card { + flex-direction: column; + align-items: stretch; + padding: 1rem 0.75rem; + min-height: 100px; +} + +.rpg-skills-grid .rpg-skill-info { + align-items: center; + text-align: center; +} + +.rpg-skills-grid .rpg-skill-header-row { + flex-direction: column; + gap: 0.5rem; +} + +.rpg-skills-grid .rpg-skill-name { + text-align: center; + word-wrap: break-word; + overflow-wrap: break-word; + white-space: normal; + max-width: 100%; +} + +.rpg-skills-grid .rpg-skill-actions { + flex-direction: column; + width: 100%; +} + +.rpg-skills-grid .rpg-skill-action { + width: 100%; +} + +/* Quick View - Compact List */ +.rpg-skills-quick-list { + display: flex; + flex-direction: column; + gap: 0.35rem; + padding: 0.5rem 0; +} + +.rpg-skills-quick-list .rpg-skill-card { + padding: 0.5rem 0.75rem; + gap: 0.5rem; +} + +.rpg-skills-quick-list .rpg-skill-info { + gap: 0; +} + +.rpg-skills-quick-list .rpg-xp-bar { + display: none; +} + +.rpg-skills-quick-list .rpg-skill-action { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +/* Inline Forms for Skills and Categories */ +.rpg-add-skill-form, +.rpg-add-category-form { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.75rem; + background: var(--SmartThemeQuoteColor); + border: 1px solid var(--ac-style-color-matchedText); + border-radius: 0.25rem; + margin-bottom: 0.75rem; +} + +/* Header Actions (View Toggle + Add Button) */ +.rpg-skills-header-actions { + display: flex; + align-items: center; + gap: 0.75rem; + flex-wrap: nowrap; + min-width: fit-content; +} + +/* Sort and Filter Controls */ +.rpg-skills-controls { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; + margin-bottom: 0.75rem; +} + +.rpg-sort-dropdown { + padding: 0.4rem 0.75rem; + background: var(--SmartThemeBlurTintColor); + border: 1px solid var(--SmartThemeBorderColor); + border-radius: 0.25rem; + color: var(--SmartThemeBodyColor); + font-size: 0.85rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.rpg-sort-dropdown:hover { + border-color: var(--ac-style-color-matchedText); +} + +.rpg-sort-dropdown:focus { + outline: none; + border-color: var(--ac-style-color-matchedText); + box-shadow: 0 0 0 2px rgba(var(--ac-style-color-matchedText-rgb, 66, 135, 245), 0.2); +} + +/* Responsive Classes - Wide Layout */ +.rpg-skills-wide .rpg-skills-header h4 { + font-size: 1.2rem; +} + +.rpg-skills-wide .rpg-skill-card { + padding: 1rem 1.25rem; +} + +/* Responsive Classes - Compact Layout */ +.rpg-skills-compact .rpg-skills-header h4 { + font-size: 1rem; +} + +.rpg-skills-compact .rpg-skill-card { + padding: 0.5rem 0.75rem; + gap: 0.75rem; +} + +.rpg-skills-compact .rpg-skills-add-btn { + font-size: 0.8rem; + padding: 0.35rem 0.6rem; +} + +.rpg-skills-compact .rpg-skill-action { + padding: 0.25rem 0.5rem; + font-size: 0.8rem; +} + +.rpg-skills-compact .rpg-xp-bar { + height: 1rem; +} + +.rpg-skills-compact .rpg-xp-text { + font-size: 0.7rem; +} + +/* Mobile Responsiveness for Skills */ +@media (max-width: 768px) { + .rpg-skills-subtabs { + gap: 0.35rem; + } + + .rpg-skills-subtab { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + } + + .rpg-skills-subtab .rpg-subtab-label { + display: none; + } + + .rpg-skills-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 0.5rem; + } + + .rpg-skills-header { + flex-wrap: wrap; + } + + .rpg-skills-header h4 { + font-size: 1rem; + } + + .rpg-skill-card { + flex-direction: column; + align-items: flex-start; + gap: 0.75rem; + } + + .rpg-skill-actions { + width: 100%; + flex-wrap: wrap; + } + + .rpg-skill-action { + flex: 1; + min-width: fit-content; + } +} + /* ============================================ DESKTOP TABS SYSTEM ============================================ */ diff --git a/template.html b/template.html index b4aa08b..1820210 100644 --- a/template.html +++ b/template.html @@ -59,15 +59,10 @@ - - - - +
-