From 71727c0a5053eb62de31961cba52b1f5dea0b50b Mon Sep 17 00:00:00 2001 From: Spicy Marinara Date: Thu, 6 Nov 2025 20:05:33 +0100 Subject: [PATCH] Revert "feat: responsive dashboard layout" --- src/systems/dashboard/dashboardIntegration.js | 24 +- src/systems/dashboard/dashboardManager.js | 366 ++---- src/systems/dashboard/dashboardTemplate.html | 2 +- src/systems/dashboard/defaultLayout.js | 84 +- src/systems/dashboard/editModeManager.js | 10 + .../dashboard/widgets/infoBoxWidgets.js | 77 +- .../dashboard/widgets/inventoryWidget.js | 8 +- .../widgets/presentCharactersWidget.js | 16 +- src/systems/dashboard/widgets/questsWidget.js | 10 +- .../dashboard/widgets/userAttributesWidget.js | 16 +- .../dashboard/widgets/userInfoWidget.js | 48 +- .../dashboard/widgets/userSkillsWidget.js | 1079 ----------------- .../dashboard/widgets/userStatsWidget.js | 14 +- src/systems/generation/parser.js | 261 +--- src/systems/generation/promptBuilder.js | 57 +- style.css | 959 +-------------- template.html | 11 +- 17 files changed, 238 insertions(+), 2804 deletions(-) delete mode 100644 src/systems/dashboard/widgets/userSkillsWidget.js diff --git a/src/systems/dashboard/dashboardIntegration.js b/src/systems/dashboard/dashboardIntegration.js index 0da6eda..ba99a57 100644 --- a/src/systems/dashboard/dashboardIntegration.js +++ b/src/systems/dashboard/dashboardIntegration.js @@ -27,7 +27,6 @@ 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; @@ -255,9 +254,6 @@ function registerAllWidgets(registry, dependencies) { // Quest widget registerQuestsWidget(registry, dependencies); - // Skills widget - registerUserSkillsWidget(registry, dependencies); - console.log(`[RPG Companion] Registered ${registry.getAll().length} widgets`); } @@ -339,16 +335,24 @@ 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 now uses ID 'rpg-open-tracker-editor' - // Event handler is in trackerEditor.js using jQuery delegation + // Tracker Settings button (open tracker editor modal) + const trackerSettingsBtn = document.querySelector('#rpg-dashboard-tracker-settings'); + if (trackerSettingsBtn) { + trackerSettingsBtn.addEventListener('click', () => { + console.log('[RPG Companion] Tracker Settings button clicked'); + // Trigger the tracker editor button from main UI + const trackerEditorBtn = document.getElementById('rpg-open-tracker-editor'); + if (trackerEditorBtn) { + trackerEditorBtn.click(); + } else { + console.warn('[RPG Companion] Tracker editor button not found'); + } + }); + } // Done button (exit edit mode) const doneBtn = document.querySelector('#rpg-dashboard-done-edit'); diff --git a/src/systems/dashboard/dashboardManager.js b/src/systems/dashboard/dashboardManager.js index 289b63e..2527823 100644 --- a/src/systems/dashboard/dashboardManager.js +++ b/src/systems/dashboard/dashboardManager.js @@ -185,8 +185,18 @@ export class DashboardManager { }); // Initialize Tab Manager with dashboard data structure - // Note: Tabs will be populated by loadLayout() which runs after init() - // Default layout is set via setDefaultLayout() before init() is called + // 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'; + } + this.tabManager = new TabManager(this.dashboard); // Set current tab to active tab from TabManager @@ -949,8 +959,7 @@ export class DashboardManager { scene: [], social: [], inventory: [], - quests: [], - skills: [] + quests: [] }; widgets.forEach(widget => { @@ -1032,19 +1041,6 @@ 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 @@ -1084,8 +1080,7 @@ export class DashboardManager { 'social': 3, 'inventory': 4, 'quests': 5, - 'skills': 6, - 'other': 7 + 'other': 6 }; // Specific widget type ordering within user category @@ -1490,29 +1485,32 @@ 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 { - // 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(); + } 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); } } catch (error) { console.error('[DashboardManager] Failed to load layout:', error); - // Fallback: use resetLayout() for clean state recovery - console.log('[DashboardManager] Recovering with resetLayout()'); - await this.resetLayout(); + if (this.defaultLayout) { + this.applyDashboardConfig(this.defaultLayout); + } } } @@ -1564,8 +1562,7 @@ export class DashboardManager { // Skip initial switch in applyDashboardConfig since we'll switch after layout calculations this.applyDashboardConfig(this.defaultLayout, { skipInitialSwitch: true }); - // Apply column-aware widget sizes from widget definitions - // This makes widgets scale properly based on screen width (2-4 columns) + // Reset all widgets to default sizes const allWidgets = []; this.dashboard.tabs.forEach(tab => { if (tab.widgets && tab.widgets.length > 0) { @@ -1574,10 +1571,13 @@ export class DashboardManager { }); this.resetWidgetSizesToDefault(allWidgets); - // 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'); + // 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 }); + } + }); // Force re-render tabs this.renderTabs(); @@ -1631,146 +1631,6 @@ 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 @@ -1796,49 +1656,41 @@ export class DashboardManager { console.log(`[DashboardManager] Laying out ${currentTab.widgets.length} widgets on tab "${currentTab.name}"`); - // Check if we can use default layout positions - const useDefaultLayout = this.tryApplyDefaultLayoutToTab(currentTab, options); - - if (!useDefaultLayout) { - // Fallback to traditional auto-layout - console.log('[DashboardManager] Using gridEngine.autoLayout (custom widgets or no default layout)'); - - // 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); - } - } - }); + // 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(); currentTab.widgets.forEach(widget => { @@ -1869,50 +1721,42 @@ export class DashboardManager { console.log('[DashboardManager] ===== AUTO-LAYOUT WIDGETS CALLED ====='); console.log('[DashboardManager] Auto-layout widgets requested'); - // 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; + // 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); } + }); - 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 + 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 } /** diff --git a/src/systems/dashboard/dashboardTemplate.html b/src/systems/dashboard/dashboardTemplate.html index 587a730..79e61dd 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 31232b9..a0f42ae 100644 --- a/src/systems/dashboard/defaultLayout.js +++ b/src/systems/dashboard/defaultLayout.js @@ -39,46 +39,45 @@ export function generateDefaultDashboard() { icon: 'fa-solid fa-user', order: 0, widgets: [ - // Row 0-1: User Info (left column, vertical) + // Row 0: User Info (left) + User Mood (top right in 3-col) { id: 'widget-userinfo', type: 'userInfo', x: 0, y: 0, - w: 1, - h: 2, + w: 2, + h: 1, config: {} }, - // Row 0-2: User Stats (right side, tall, 2 cols wide) - { - id: 'widget-userstats', - type: 'userStats', - x: 1, - y: 0, - w: 2, - h: 3, - config: { - statBarGradient: true - } - }, - // Row 2: User Mood (below user info, left column) { id: 'widget-usermood', type: 'userMood', - x: 0, - y: 2, + x: 2, + y: 0, w: 1, h: 1, config: {} }, - // Row 3-6: User Attributes (full width below everything, 3 cols wide) + // Row 1-2: User Stats (health/energy bars) + { + id: 'widget-userstats', + type: 'userStats', + x: 0, + y: 1, + w: 2, + h: 2, + config: { + statBarGradient: true + } + }, + // Row 3-4: User Attributes { id: 'widget-userattributes', type: 'userAttributes', x: 0, y: 3, - w: 3, - h: 4, + w: 2, + h: 2, config: {} } ] @@ -90,36 +89,36 @@ export function generateDefaultDashboard() { icon: 'fa-solid fa-map', order: 1, widgets: [ - // Row 0-2: Scene Info (combined: calendar, weather, temp, clock, location) + // Row 0-1: Scene Info (combined: calendar, weather, temp, clock, location) { id: 'widget-sceneinfo', type: 'sceneInfo', x: 0, y: 0, - w: 3, - h: 3, + w: 2, + h: 2, config: {} }, - // Row 3-4: Recent Events (notebook style, full width) + // Row 2-3: Recent Events (notebook style, full width) { id: 'widget-recentevents', type: 'recentEvents', x: 0, - y: 3, - w: 3, + y: 2, + w: 2, h: 2, config: { maxEvents: 3 } }, - // Row 5-6: Present Characters (full width, fits 1080p screen) + // Row 4-7: Present Characters (full width, will expand with auto-layout) { id: 'widget-presentchars', type: 'presentCharacters', x: 0, - y: 5, - w: 3, - h: 2, + y: 4, + w: 2, + h: 4, config: { cardLayout: 'grid', showThoughtBubbles: true @@ -167,29 +166,6 @@ 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 61522b6..745098d 100644 --- a/src/systems/dashboard/editModeManager.js +++ b/src/systems/dashboard/editModeManager.js @@ -336,6 +336,15 @@ 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) => { @@ -344,6 +353,7 @@ 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 7ba58fd..a9626d6 100644 --- a/src/systems/dashboard/widgets/infoBoxWidgets.js +++ b/src/systems/dashboard/widgets/infoBoxWidgets.js @@ -529,19 +529,7 @@ export function registerRecentEventsWidget(registry, dependencies) { description: 'Recent events notebook', category: 'scene', minSize: { 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 }; - }, + defaultSize: { w: 2, h: 2 }, requiresSchema: false, /** @@ -550,40 +538,7 @@ export function registerRecentEventsWidget(registry, dependencies) { * @param {Object} config - Widget configuration */ render(container, config = {}) { - 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 { getInfoBoxData } = dependencies; const data = parseInfoBoxData(getInfoBoxData()); // Merge default config with user config @@ -800,31 +755,3 @@ 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 273e02d..d913994 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, full width on desktop + // Column-aware sizing: compact on mobile, spacious on desktop defaultSize: (columns) => { if (columns <= 2) { return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact) } - return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p) + return { w: 2, h: 6 }; // Desktop: 2×6 (default) }, maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 8 }; // Mobile: 2×8 max + return { w: 2, h: 8 }; // Mobile: 2×8 max (increased for expansion headroom) } - return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand) + return { w: 3, h: 8 }; // Desktop: 3×8 max (can expand) }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/presentCharactersWidget.js b/src/systems/dashboard/widgets/presentCharactersWidget.js index eab55a9..ae60e74 100644 --- a/src/systems/dashboard/widgets/presentCharactersWidget.js +++ b/src/systems/dashboard/widgets/presentCharactersWidget.js @@ -276,20 +276,8 @@ export function registerPresentCharactersWidget(registry, dependencies) { description: 'Character cards with avatars, traits, and relationships', category: 'scene', minSize: { w: 2, h: 2 }, - // 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) - }, + 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) requiresSchema: false, render(container, config = {}) { diff --git a/src/systems/dashboard/widgets/questsWidget.js b/src/systems/dashboard/widgets/questsWidget.js index fe08cd2..5bd8c1f 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, full width on desktop + // Column-aware sizing: compact on mobile, spacious on desktop defaultSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 5 }; // Mobile: 2×5 (full width, compact) + return { w: 2, h: 4 }; // Mobile: 2×4 (full width, compact) } - return { w: 3, h: 7 }; // Desktop: 3×7 (full width, spacious for 1080p) + return { w: 2, h: 5 }; // Desktop: 2×5 (default) }, maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 2, h: 8 }; // Mobile: 2×8 max + return { w: 2, h: 7 }; // Mobile: 2×7 max (increased for expansion headroom) } - return { w: 3, h: 10 }; // Desktop: 3×10 max (can expand) + return { w: 3, h: 7 }; // Desktop: 3×7 max (can expand) }, requiresSchema: false, diff --git a/src/systems/dashboard/widgets/userAttributesWidget.js b/src/systems/dashboard/widgets/userAttributesWidget.js index a94ad6b..a1babd4 100644 --- a/src/systems/dashboard/widgets/userAttributesWidget.js +++ b/src/systems/dashboard/widgets/userAttributesWidget.js @@ -35,20 +35,8 @@ export function registerUserAttributesWidget(registry, dependencies) { description: 'Customizable RPG attributes with +/- buttons (STR, DEX, etc.)', category: 'user', minSize: { w: 2, h: 2 }, - // 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 }; - }, + defaultSize: { w: 2, h: 2 }, + maxAutoSize: { w: 3, h: 5 }, // Max size for auto-arrange expansion requiresSchema: false, /** diff --git a/src/systems/dashboard/widgets/userInfoWidget.js b/src/systems/dashboard/widgets/userInfoWidget.js index 53479bf..d93dbf4 100644 --- a/src/systems/dashboard/widgets/userInfoWidget.js +++ b/src/systems/dashboard/widgets/userInfoWidget.js @@ -38,22 +38,19 @@ export function registerUserInfoWidget(registry, dependencies) { description: 'User avatar, name, and level display', category: 'user', minSize: { w: 1, h: 1 }, - // Column-aware default size: vertical 1x2 with mood below + // Column-aware default size: start at 2x1 in desktop so mood doesn't block expansion defaultSize: (columns) => { - // 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) + if (columns <= 2) { + return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout } - return { w: 1, h: 2 }; // Desktop (all widths): 1x2 vertical, mood sits below + return { w: 2, h: 1 }; // Desktop: 2x1 from the start }, - // Column-aware max size: same as defaultSize to prevent expansion + // Column-aware max size: same as defaultSize to prevent further expansion maxAutoSize: (columns) => { - const isMobile = window.innerWidth <= 1000; - if (isMobile) { - return { w: 1, h: 1 }; // Mobile: 1x1, compact + if (columns <= 2) { + return { w: 1, h: 1 }; // Mobile: 1x1, horizontal layout } - return { w: 1, h: 2 }; // Desktop: 1x2 vertical, mood below at y:2 + return { w: 2, h: 1 }; // Desktop: 2x1, mood sits in top-right }, requiresSchema: false, @@ -92,22 +89,15 @@ export function registerUserInfoWidget(registry, dependencies) { const html = `
- ${finalConfig.showAvatar ? `User Avatar` : ''} - - ${finalConfig.showName ? ` -
-
${userName}
-
- ` : ''} - - ${finalConfig.showLevel ? ` -
+ - ` : ''} + ` : ''} +
`; @@ -165,15 +155,11 @@ export function registerUserInfoWidget(registry, dependencies) { const infoContainer = container.querySelector('.rpg-user-info-container'); if (!infoContainer) return; - // 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 + // Apply compact mode class at narrow widths for smaller text + if (newW < 3) { infoContainer.classList.add('rpg-user-info-compact'); - infoContainer.classList.remove('rpg-user-info-wide'); + } else { + infoContainer.classList.remove('rpg-user-info-compact'); } } }); diff --git a/src/systems/dashboard/widgets/userSkillsWidget.js b/src/systems/dashboard/widgets/userSkillsWidget.js deleted file mode 100644 index ea6608a..0000000 --- a/src/systems/dashboard/widgets/userSkillsWidget.js +++ /dev/null @@ -1,1079 +0,0 @@ -/** - * 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 78b1ac1..f668596 100644 --- a/src/systems/dashboard/widgets/userStatsWidget.js +++ b/src/systems/dashboard/widgets/userStatsWidget.js @@ -33,19 +33,13 @@ export function registerUserStatsWidget(registry, dependencies) { description: 'Health, energy, satiety bars', category: 'user', minSize: { w: 1, h: 2 }, - // 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 + defaultSize: { w: 2, h: 2 }, + // Column-aware max size: full width in 3-4 col for horizontal spread maxAutoSize: (columns) => { if (columns <= 2) { - return { w: 1, h: 3 }; // Mobile: 1x3 + return { w: 2, h: 2 }; // Mobile: use full 2-col width } - return { w: 2, h: 3 }; // Desktop: 2x3 + return { w: 3, h: 3 }; // Desktop: span 3 columns horizontally }, requiresSchema: false, diff --git a/src/systems/generation/parser.js b/src/systems/generation/parser.js index 7f01ee5..3a6d020 100644 --- a/src/systems/generation/parser.js +++ b/src/systems/generation/parser.js @@ -56,10 +56,6 @@ 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(); @@ -71,7 +67,6 @@ 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. @@ -107,103 +102,23 @@ 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) - // 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, ''); // Remove lines that are just "Label: " with nothing 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(); } @@ -218,164 +133,6 @@ 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. @@ -413,13 +170,7 @@ 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 = ( @@ -600,12 +351,10 @@ export function parseUserStats(statsText) { // Parse skills section if enabled const skillsConfig = trackerConfig?.userStats?.skillsSection; if (skillsConfig?.enabled) { - 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'); + const skillsMatch = statsText.match(/Skills:\s*(.+)/i); + if (skillsMatch) { + extensionSettings.userStats.skills = skillsMatch[1].trim(); + debugLog('[RPG Parser] Skills extracted:', skillsMatch[1].trim()); } } diff --git a/src/systems/generation/promptBuilder.js b/src/systems/generation/promptBuilder.js index 17f1547..62be66c 100644 --- a/src/systems/generation/promptBuilder.js +++ b/src/systems/generation/promptBuilder.js @@ -10,53 +10,6 @@ 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. @@ -213,13 +166,9 @@ export function generateTrackerInstructions(includeHtmlPrompt = true, includeCon // Add skills section if enabled if (userStatsConfig?.skillsSection?.enabled) { - 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`; + const skillFields = userStatsConfig.skillsSection.customFields || []; + const skillFieldsText = skillFields.map(f => `[${f}]`).join(', '); + instructions += `Skills: [${skillFieldsText || 'Skill1, Skill2, etc.'}]\n`; } // Add inventory format based on feature flag diff --git a/style.css b/style.css index bca3d5d..ee88288 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: flex-end; + align-items: center; justify-content: center; height: 100%; width: 100%; @@ -1963,71 +1963,20 @@ body:has(.rpg-panel.rpg-position-left) #sheld { z-index: 1; } -/* 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 { +/* Text container with backdrop */ +.rpg-user-info-text { + display: flex; + flex-direction: column; + gap: 0.2rem; + align-items: center; + text-align: center; position: relative; z-index: 2; - padding: 0.3rem 0.6rem; - background: rgba(0, 0, 0, 0.15); + padding: 0.5rem 0.75rem; + background: rgba(0, 0, 0, 0.5); 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 */ @@ -2080,89 +2029,27 @@ body:has(.rpg-panel.rpg-position-left) #sheld { background: var(--rpg-bg); } -/* COMPACT LAYOUT (1x1): Round avatar with bottom nameplate */ +/* Compact mode for narrow widths (< 3 grid units) */ .rpg-user-info-compact { - align-items: center; - justify-content: center; - padding: 0 !important; + padding: 0.25rem !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-info-text { + gap: 0.15rem !important; + padding: 0.35rem 0.5rem !important; } .rpg-user-info-compact .rpg-user-name { - 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; + font-size: 0.75rem !important; } .rpg-user-info-compact .rpg-level-label { - font-size: 0.55rem; - font-weight: 600; - color: var(--rpg-text); - opacity: 0.7; + font-size: 0.65rem !important; } .rpg-user-info-compact .rpg-level-value { - 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; + font-size: 0.75rem !important; + padding: 0.1rem 0.3rem !important; } /* Stat bars - rem for text, vh for bar height */ @@ -2776,72 +2663,6 @@ 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 @@ -4436,82 +4257,6 @@ 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, @@ -5260,35 +5005,25 @@ body:has(.rpg-panel.rpg-position-left) #sheld { align-items: center; gap: 0.5em; padding: 0.5em; - background: var(--SmartThemeBlurTintColor); - border: 2px solid var(--SmartThemeBorderColor); + background: var(--rpg-accent); + border: 1px solid var(--rpg-border); border-radius: 0.375em; } -.rpg-stat-toggle, -.rpg-attr-toggle { +.rpg-stat-toggle { flex-shrink: 0; } -.rpg-stat-name, -.rpg-attr-name { +.rpg-stat-name { flex: 1; padding: 0.375em 0.5em; - background: var(--SmartThemeBlurTintColor); - border: 2px solid var(--SmartThemeBorderColor); + background: var(--rpg-bg); + border: 1px solid var(--rpg-border); border-radius: 0.25em; - color: var(--SmartThemeBodyColor); + color: var(--rpg-text); 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 { @@ -7879,648 +7614,6 @@ 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 1820210..b4aa08b 100644 --- a/template.html +++ b/template.html @@ -59,10 +59,15 @@ - + + + +
-